0.0.10
Added
-
artisan startnow records its session per project, so two apps can be driven at once.~/.artisan/state.jsonwas one global slot: a second project'sstartsilently took it, and every connected command from the first project then drove the second app, succeeding each time. The measured case had a worktree in another repository rewrite the file mid-session, and two commands later produced a screenshot of an entirely different product. The session is now a DIRECTORY at~/.artisan/sessions/<sha256(projectRoot)[0:12]>/, holdingstate.json, the log and the FIFO, because those last two collided for exactly the same reason and fixing one member of the set is not fixing the set. Keyed by a digest rather than a slugified path, since a project path can contain separators, spaces and non-ASCII.~/.artisan/state.jsonis still written, as a pointer to whichever session started last. That is an interop contract, not a shim: hand-writing that file is the documented recovery recipe whenstartcannot boot an app, andread()falls back to it when this project has no session of its own. Two projects still need distinct--port,--vm-service-portand--cdp-port;startfails fast when one is taken. -
--state=<path>andARTISAN_STATE_FILEname the session explicitly. A global flag consumed before dispatch rather than a per-command option, because every connected command needs it and none of them owns it. The flag wins over the environment variable, and naming a session also bypasses the ownership check below, since the caller has already answered the question it asks. -
statusreportsownedByThisProjectandprojectRoot. It reads rather than acts, so it surfaces a foreign session instead of refusing it, but it must not present one as this project's: an agent readingrunning: truefrom a checkout with nothing up would conclude its own app is live and act on that. -
sessionOwnershipErrorrefuses a command that would act on another project's app. The legacy pointer describes whichever app started last, so astoprun from a project with nothing up reached across and killed a sibling's running app while reporting a clean success, and a connected command dialled the wrong VM Service. Wired into the connected-mode boot path,stop,reloadandhot-restart. A working directory INSIDE the project passes, state with no recordedprojectRootpasses (hand-written recovery state usually omits it), and an explicit--statepasses.
Fixed
-
--state=<path>never reached the command when a consumer wrapper owned the invocation. The flag is consumed before dispatch into a static on this isolate, but a wrapper delegation spawns a SEPARATE Dart process, and the stripped args carried nothing across.artisan stop --state=/xfrom any project with abin/dispatcher.darttherefore acted on the auto-resolved session instead, silently: the exact failure the flag exists to prevent, on the exact path most consumers take. The flag is now re-attached to the delegated args.ARTISAN_STATE_FILEwas unaffected, since the child inherits the environment. -
The ownership check ignored
ARTISAN_STATE_FILE, so it refused the session that variable had just pointed it at.StateFile.pathhonours the flag and the environment variable equally, and the two are documented as equivalent, but all five guard call sites read the flag alone. With the variable set and no flag, artisan read the named foreign session and then declined to drive it.StateFile.explicitPath()now answers "did the caller name a session", by either spelling. -
artisan restartignoredstop's refusal and relaunched on the other project's settings.restartisCommandBoot.none, so the connected-mode guard never runs, and it discardedStopCommand.handle's exit code. Run from project A while the pointer described B: stop printed the refusal and returned 1, restart carried on, andsessionOverridesFromcarried B's web port, VM Service port, CDP port and device into A's relaunch, reproducing theAddress already in usethis same release set out to fix. A non-zero stop now aborts the restart. -
Two
artisan startruns raced on the shared pointer's staging file. Every write goes through.tmp+ rename, but the legacy pointer is one path for every project, so both processes staged~/.artisan/state.json.tmpand the loser's rename threwFileSystemExceptionafterflutter runhad already spawned, leaving an orphan with no state file to find it by. The staging name now carries the pid. The per-project session files were never affected; the mirror re-introduced the shared slot they exist to remove. -
StateFile.deletecompared paths exactly and deleted an unattributed pointer. Astopfrom a subdirectory has no session file of its own, so the fallback root is that subdirectory and the pointer was left behind, still advertising a session that had just been stopped; the check now uses the same is-within rule assessionOwnershipError. A pointer with no recordedprojectRootis now left alone: that is the hand-written recovery file the read path goes out of its way to honour, and deleting it took away the escape hatch people reach for precisely whenstarthas already failed them. -
artisan restartrelaunched on the default web port, VM Service port and device, keeping only the CDP port. Several worktrees run their own dev server at once, so 3100 is usually held by a sibling: the stop half succeeded, the relaunch failed withAddress already in use, and the app being driven was simply gone. The error names a port that appears in no command the caller ran, so it reads as a machine problem rather than a missing flag. A restart onto the default web-server device is quieter and worse: nothing renders, and every screenshot after it comes back byte-identical.restartnow carries the device and all three ports across, and declares each as a flag so an explicit value still wins. -
logslooked forflutter-dev.logbeside the state file, which now resolves inside the session directory. It reads the session log and falls back to the shared~/.artisan/flutter-dev.logfor an app started by an older artisan. -
artisan_start's tool description pointed agents at a tool that does not exist. Two lines of the description listedtinker_evalamong the tools that read~/.artisan/state.json; the tool isartisan_tinker. An agent taking the description at its word calls a name the server never registered and gets an unknown-tool error. Also corrected the same stale name inMcpToolDescriptor's own docstring example. (lib/src/mcp/mcp_server.dart,lib/src/mcp/mcp_tool_descriptor.dart,skills/fluttersdk-artisan/references/mcp-tools.md,skills/fluttersdk-artisan/references/tinker-eval.md) -
The MCP server reported itself as
0.0.8to every client. TheImplementation.versionstring inMcpServercarries the comment "Keep in sync with pubspec.yamlversion:on each release cut" and was missed on the 0.0.9 cut, so an MCP client's initialize handshake read a version one release behind while the package on disk was 0.0.9. Client-side version gating and bug reports both keyed on the wrong number. (lib/src/mcp/mcp_server.dart)
Documentation
- The registry dispatch fires on a published release now, not on every push that touches the skill. Under the push trigger
fluttersdk/aiclimbed to v1.3.75, and most of those releases re-published identical skill content: a docs commit and a release commit each cost the registry a version. The registry version now tracks published artisan releases instead of counting commits.workflow_dispatchstays as the manual escape hatch when a skill fix has to reach users before the next release. (.github/workflows/dispatch-to-registry.yml) - The skill undercounted the builtin commands by one and the CLI-only set with it.
make:fast-clilanded in 0.0.2 and the counts were never rolled forward, so the skill promised "21 builtin CLI commands" and "11 CLI-only" against a real 22 and 12. Corrected in the frontmatter description, Core Law 6, the section 14 reference table, and thereferences/cli-commands.mdtitle, intro, andartisan_listheading. Core Law 6's inline list was 11 items long against its own count of 12:mcp:uninstallwas missing, and it is now in both the list and the exclusion-reason table. (skills/fluttersdk-artisan/SKILL.md,skills/fluttersdk-artisan/references/cli-commands.md) start --timeout=<seconds>was undiscoverable from the skill. 0.0.9 made the VM Service URI scrape window configurable, but the skill still presented 90s as a fixed property ofstartin three places, so an agent facing a slow cold boot had no documented way out and would conclude the boot was broken. The MCP schema exposes no timeout parameter, which makes this a CLI-only escape hatch worth naming explicitly. (skills/fluttersdk-artisan/SKILL.md,skills/fluttersdk-artisan/references/mcp-tools.md,skills/fluttersdk-artisan/references/state-and-recovery.md)- The skill cited the substrate allowlist as
mcp_server.dart:871-882; the range had drifted by two lines. Both citations now name the_safeArtisanCommandNamesconstant instead, which does not rot when the file moves. (skills/fluttersdk-artisan/SKILL.md,skills/fluttersdk-artisan/references/mcp-tools.md) - Skill version 0.0.4, stamped against package 0.0.9. The previous stamp claimed 0.0.7, which predates the
--timeoutflag and therestartCDP-port preservation the skill now describes. (skills/fluttersdk-artisan/SKILL.md)