Skip to content

0.0.10

Choose a tag to compare

@github-actions github-actions released this 20 Aug 17:30
· 6 commits to master since this release
f36eb9a

Added

  • artisan start now records its session per project, so two apps can be driven at once. ~/.artisan/state.json was one global slot: a second project's start silently 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]>/, holding state.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.json is 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 when start cannot boot an app, and read() falls back to it when this project has no session of its own. Two projects still need distinct --port, --vm-service-port and --cdp-port; start fails fast when one is taken.

  • --state=<path> and ARTISAN_STATE_FILE name 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.

  • status reports ownedByThisProject and projectRoot. 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 reading running: true from a checkout with nothing up would conclude its own app is live and act on that.

  • sessionOwnershipError refuses a command that would act on another project's app. The legacy pointer describes whichever app started last, so a stop run 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, reload and hot-restart. A working directory INSIDE the project passes, state with no recorded projectRoot passes (hand-written recovery state usually omits it), and an explicit --state passes.

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=/x from any project with a bin/dispatcher.dart therefore 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_FILE was 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.path honours 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 restart ignored stop's refusal and relaunched on the other project's settings. restart is CommandBoot.none, so the connected-mode guard never runs, and it discarded StopCommand.handle's exit code. Run from project A while the pointer described B: stop printed the refusal and returned 1, restart carried on, and sessionOverridesFrom carried B's web port, VM Service port, CDP port and device into A's relaunch, reproducing the Address already in use this same release set out to fix. A non-zero stop now aborts the restart.

  • Two artisan start runs 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.tmp and the loser's rename threw FileSystemException after flutter run had 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.delete compared paths exactly and deleted an unattributed pointer. A stop from 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 as sessionOwnershipError. A pointer with no recorded projectRoot is 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 when start has already failed them.

  • artisan restart relaunched 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 with Address 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. restart now carries the device and all three ports across, and declares each as a flag so an explicit value still wins.

  • logs looked for flutter-dev.log beside the state file, which now resolves inside the session directory. It reads the session log and falls back to the shared ~/.artisan/flutter-dev.log for 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 listed tinker_eval among the tools that read ~/.artisan/state.json; the tool is artisan_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 in McpToolDescriptor'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.8 to every client. The Implementation.version string in McpServer carries the comment "Keep in sync with pubspec.yaml version: 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/ai climbed 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_dispatch stays 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-cli landed 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 the references/cli-commands.md title, intro, and artisan_list heading. Core Law 6's inline list was 11 items long against its own count of 12: mcp:uninstall was 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 of start in 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 _safeArtisanCommandNames constant 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 --timeout flag and the restart CDP-port preservation the skill now describes. (skills/fluttersdk-artisan/SKILL.md)