fluttersdk_artisan
Composable Dart CLI framework and stdio MCP server for Flutter. Scaffolding, code generation, transactional plugin installs, hot reload, REPL, and AI agent tooling.
0.0.13
20K downloads/mo
#2190 most downloaded on pub.dev
fluttersdk/artisan
What this package is like to depend on
Last release 3 days ago
20 Aug 2026
Release timing varies
gaps range from 8 days to 2 months
Nearly every release is documented
notes for 13 of 13 stable releases
Nothing withdrawn
no release was ever pulled
3 months old
13 releases · first in 2026
13 releases in the last 12 months
see the full history below
Release timeline
13 releases · May 2026 to Aug 2026Releases
latest 13-
0.0.1320 Aug 2026Release notes
Open source →Documentation
-
The docs still told readers the session is one global file, in ten files. 0.0.10 moved the session to
~/.artisan/sessions/<hash>/and the code changed with it, but the skill, the MCP tool descriptions and the command pages kept describing~/.artisan/state.jsonas the place the running app is recorded. This is not cosmetic: a field report has an agent concluding it had corrupted a sibling's session, because the tooling it was reading said there was only one slot to corrupt. It had reasoned correctly from a document that was wrong.Swept
skills/fluttersdk-artisan/SKILL.md(including law 2, which stated the old model as a rule),references/mcp-tools.md,references/tinker-eval.md,references/state-and-recovery.md,doc/mcp/{overview,setup,tool-reference}.md,doc/getting-started/quickstart.md,doc/commands/{index,start,tinker,mcp-serve}.md. The mentions that remain are the ones that are still true: the legacy pointer exists, is read as a fallback, and is what the hand-written recovery recipe targets.Also corrected while there: the recovery for "no app detected" said to remove
~/.artisan/state.json, which clears the pointer and not the session; the troubleshooting table quoted two error strings that no longer exist; and neither carried thebooting: truepath. Skill version 0.0.5 -> 0.0.6.
Release notes
Open source →Documentation
-
The docs still told readers the session is one global file, in ten files. 0.0.10 moved the session to
~/.artisan/sessions/<hash>/and the code changed with it, but the skill, the MCP tool descriptions and the command pages kept describing~/.artisan/state.jsonas the place the running app is recorded. This is not cosmetic: a field report has an agent concluding it had corrupted a sibling's session, because the tooling it was reading said there was only one slot to corrupt. It had reasoned correctly from a document that was wrong.Swept
skills/fluttersdk-artisan/SKILL.md(including law 2, which stated the old model as a rule),references/mcp-tools.md,references/tinker-eval.md,references/state-and-recovery.md,doc/mcp/{overview,setup,tool-reference}.md,doc/getting-started/quickstart.md,doc/commands/{index,start,tinker,mcp-serve}.md. The mentions that remain are the ones that are still true: the legacy pointer exists, is read as a fallback, and is what the hand-written recovery recipe targets.Also corrected while there: the recovery for "no app detected" said to remove
~/.artisan/state.json, which clears the pointer and not the session; the troubleshooting table quoted two error strings that no longer exist; and neither carried thebooting: truepath. Skill version 0.0.5 -> 0.0.6.
-
-
0.0.1220 Aug 2026Release notes
Open source →Fixed
-
A
startinterrupted by its caller left the app running and unrecorded. The session was written only after the VM Service URI was scraped, which is the last and longest thingstartwaits for. An MCP client kills a tool call at 60s and an iOS build routinely takes longer, so the app came up, the process survived (the wrapper detaches it), and nothing recorded it:statusansweredrunning: falseabout an app that was listening on 8183,stophad no pid to reap, and the operator had to hand-write the state file. Reported from the field ondevice=50BAD9FA-....The session is now written as soon as the child PIDs are known, carrying the pid, the FIFO, the ports and the device, with
vmServiceUri: nullandbooting: truesaying the record is incomplete rather than wrong. The URI is filled in when the scrape lands. And a connected command that finds no URI reads the last one out of the session log and keeps it, so an interrupted start heals on the next call instead of needing a hand-written file. -
statusreported an unfinished start as a healthy session. That is where the reported symptom chain started: the operator readrunning: falseabout an app that was serving, concluded the start had failed, and went looking for a recovery. It now reportsbooting: truealongside the record and says in plain words that the URI is not there yet and how it gets filled in. -
artisan_start's MCP description still told agents the state was one global file. "writes ... to~/.artisan/state.json" and "ONLY ONE Flutter app per machine can be tracked at a time (single-slot state)" have both been false since 0.0.10, and an agent acting on them concluded it had corrupted a sibling's session when the two were never sharing one. The description now says sessions are per project, and adds the line the timeout above needed: if the call times out the app is probably still starting, callartisan_status, and do not hand-write the state file. Same correction in the skill reference, whose recovery section also carried the old error text. -
The documented state schema omitted
stdinPipe, so following it produced a file that could not hot restart. The docblock is the recipe an operator reaches for precisely whenstarthas failed them, and it listed eleven keys without the onereloadandhot-restartneed. Both now documentstdinPipe,stdinHolderPidandbooting. -
reloadandhot-restartblamed an old artisan for a missingstdinPipe. "the app was started by an older artisan that pre-dates the FIFO refactor" is one cause; a state file hand-written from the schema above is the likelier one now, and the message said nothing about what the key holds. Both messages name both causes and describe the value.
Release notes
Open source →Fixed
-
A
startinterrupted by its caller left the app running and unrecorded. The session was written only after the VM Service URI was scraped, which is the last and longest thingstartwaits for. An MCP client kills a tool call at 60s and an iOS build routinely takes longer, so the app came up, the process survived (the wrapper detaches it), and nothing recorded it:statusansweredrunning: falseabout an app that was listening on 8183,stophad no pid to reap, and the operator had to hand-write the state file. Reported from the field ondevice=50BAD9FA-....The session is now written as soon as the child PIDs are known, carrying the pid, the FIFO, the ports and the device, with
vmServiceUri: nullandbooting: truesaying the record is incomplete rather than wrong. The URI is filled in when the scrape lands. And a connected command that finds no URI reads the last one out of the session log and keeps it, so an interrupted start heals on the next call instead of needing a hand-written file. -
statusreported an unfinished start as a healthy session. That is where the reported symptom chain started: the operator readrunning: falseabout an app that was serving, concluded the start had failed, and went looking for a recovery. It now reportsbooting: truealongside the record and says in plain words that the URI is not there yet and how it gets filled in. -
artisan_start's MCP description still told agents the state was one global file. "writes ... to~/.artisan/state.json" and "ONLY ONE Flutter app per machine can be tracked at a time (single-slot state)" have both been false since 0.0.10, and an agent acting on them concluded it had corrupted a sibling's session when the two were never sharing one. The description now says sessions are per project, and adds the line the timeout above needed: if the call times out the app is probably still starting, callartisan_status, and do not hand-write the state file. Same correction in the skill reference, whose recovery section also carried the old error text. -
The documented state schema omitted
stdinPipe, so following it produced a file that could not hot restart. The docblock is the recipe an operator reaches for precisely whenstarthas failed them, and it listed eleven keys without the onereloadandhot-restartneed. Both now documentstdinPipe,stdinHolderPidandbooting. -
reloadandhot-restartblamed an old artisan for a missingstdinPipe. "the app was started by an older artisan that pre-dates the FIFO refactor" is one cause; a state file hand-written from the schema above is the likelier one now, and the message said nothing about what the key holds. Both messages name both causes and describe the value.
-
-
0.0.1120 Aug 2026Release notes
Open source →Fixed
-
The per-project session was keyed on the working directory, not the project, so the isolation only held for callers standing in the repo root.
sessionOwnershipErrordeliberately blesses running frombackend/or a package subdirectory, butsessionPathForhashed the cwd: a command from there missed its own session file, fell back to the shared~/.artisan/state.jsonpointer, and with two apps up was then refused for driving somebody else's. A false refusal, and the defeat of the very isolation 0.0.10 shipped.StateFile.projectRootFornow walks up to the nearest ancestor holding apubspec.yaml. Nearest rather than outermost, because that is the unitartisan startboots: two packages in one repository are two apps and want two sessions. No pubspec anywhere up the chain falls back to the directory itself, so non-Dart callers and the hand-written recovery recipe do not all land in one shared session.startrecords the same walked root inprojectRoot, since a raw cwd there would make a start from a subdirectory record a root the ownership check then measures every later command against.
Release notes
Open source →Fixed
-
The per-project session was keyed on the working directory, not the project, so the isolation only held for callers standing in the repo root.
sessionOwnershipErrordeliberately blesses running frombackend/or a package subdirectory, butsessionPathForhashed the cwd: a command from there missed its own session file, fell back to the shared~/.artisan/state.jsonpointer, and with two apps up was then refused for driving somebody else's. A false refusal, and the defeat of the very isolation 0.0.10 shipped.StateFile.projectRootFornow walks up to the nearest ancestor holding apubspec.yaml. Nearest rather than outermost, because that is the unitartisan startboots: two packages in one repository are two apps and want two sessions. No pubspec anywhere up the chain falls back to the directory itself, so non-Dart callers and the hand-written recovery recipe do not all land in one shared session.startrecords the same walked root inprojectRoot, since a raw cwd there would make a start from a subdirectory record a root the ownership check then measures every later command against.
-
-
0.0.1020 Aug 2026Release notes
Open source →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)
Release notes
Open source →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)
-
-
0.0.929 Jul 2026Release notes
Open source →Added
start --timeout=<n>option (default90): configures the maximum seconds the VM Service URI scrape loop waits forflutter runto print the debug URI in the log file. Previously the deadline was hardcoded to 90 s; cold starts on slow CI machines or after a fresh Flutter SDK install can exceed this limit. Setting--timeout=120(or higher) prevents false-timeout failures. The error message now reports the configured value rather than a literal "90s". Applies to the--cdp-portbranch only; the non-CDP branch retains its own hardcoded deadline.start --cdp-port=<n>now probes the CDP port for availability BEFORE launching Chrome. When the port is already in use, the command exits 1 immediately with a clear message: "CDP port N is already in use; pass --cdp-port or free it before running start." This replaces the previous misleading "Is Chrome installed?" catch-all that fired when Chrome failed to open the debug port it was asked to bind.
Changed
plugin:install'sinstall.yamlbootstrap_commandnow AUTO-RUNS after a successful manifest install instead of only printing a hint. Once the plugin is registered (plugins.json+lib/app/_plugins.g.dartregenerated), the declared command is spawned as a fresh dispatcher subprocess (./bin/fsa <cmd> --non-interactivewhenbin/fsaexists, elsedart run <consumer>:artisan <cmd> --non-interactive), so the just-registered plugin command actually executes.--non-interactiveis always forwarded so an interactive bootstrap (e.g.starter:install) cannot hang.--bootstrap-command=<name>overrides the manifest value;--no-bootstrapskips the auto-run and falls back to the hint. When no dispatcher resolves (nobin/fsa, no consumer pubspec name), the one-lineBootstrap with: artisan <cmd>hint is printed as before.
Fixed
plugin:installnow surfaces a failingbootstrap_commandinstead of implying success.BootstrapCommandRunner.runreturns aBootstrapRunResultcarrying the subprocess exit code and captured stderr (it previously discarded theProcessResult), andplugin:installwarns with the exit code + stderr and prints the manual bootstrap hint when the chained command exits non-zero. A bootstrap that fails (stale fast-CLI bundle, unknown command, scaffold error) is no longer reported to the operator as if it had completed.start --timeout=<n>now rejects zero and negative values immediately with an actionable error ("--timeout must be a positive integer"), instead of silently passing a non-positive deadline to the VM Service scrape loop and producing a confusing "Timed out after 0s" failure.- Passing an unknown option to any command now fails loudly instead of silently printing help and exiting as if help were requested (issue #12). The dispatcher writes
Unknown option: <flag>to stderr (both long--fooand short-xforms), prints the command help, and exits non-zero. Other parse failures keep their original messages: a missing option value (Missing argument for "..."), a disallowed value, and a value given to a flag each surface their specific diagnostic unchanged.--help/-hand every valid invocation are unaffected. Because this is the shared dispatch path for every command, the fix benefits every plugin CLI built on the substrate.
Release notes
Open source →Added
start --timeout=<n>option (default90): configures the maximum seconds the VM Service URI scrape loop waits forflutter runto print the debug URI in the log file. Previously the deadline was hardcoded to 90 s; cold starts on slow CI machines or after a fresh Flutter SDK install can exceed this limit. Setting--timeout=120(or higher) prevents false-timeout failures. The error message now reports the configured value rather than a literal "90s". Applies to the--cdp-portbranch only; the non-CDP branch retains its own hardcoded deadline.start --cdp-port=<n>now probes the CDP port for availability BEFORE launching Chrome. When the port is already in use, the command exits 1 immediately with a clear message: "CDP port N is already in use; pass --cdp-port <free-port> or free it before running start." This replaces the previous misleading "Is Chrome installed?" catch-all that fired when Chrome failed to open the debug port it was asked to bind.
Changed
plugin:install'sinstall.yamlbootstrap_commandnow AUTO-RUNS after a successful manifest install instead of only printing a hint. Once the plugin is registered (plugins.json+lib/app/_plugins.g.dartregenerated), the declared command is spawned as a fresh dispatcher subprocess (./bin/fsa <cmd> --non-interactivewhenbin/fsaexists, elsedart run <consumer>:artisan <cmd> --non-interactive), so the just-registered plugin command actually executes.--non-interactiveis always forwarded so an interactive bootstrap (e.g.starter:install) cannot hang.--bootstrap-command=<name>overrides the manifest value;--no-bootstrapskips the auto-run and falls back to the hint. When no dispatcher resolves (nobin/fsa, no consumer pubspec name), the one-lineBootstrap with: artisan <cmd>hint is printed as before.
Fixed
plugin:installnow surfaces a failingbootstrap_commandinstead of implying success.BootstrapCommandRunner.runreturns aBootstrapRunResultcarrying the subprocess exit code and captured stderr (it previously discarded theProcessResult), andplugin:installwarns with the exit code + stderr and prints the manual bootstrap hint when the chained command exits non-zero. A bootstrap that fails (stale fast-CLI bundle, unknown command, scaffold error) is no longer reported to the operator as if it had completed.start --timeout=<n>now rejects zero and negative values immediately with an actionable error ("--timeout must be a positive integer"), instead of silently passing a non-positive deadline to the VM Service scrape loop and producing a confusing "Timed out after 0s" failure.- Passing an unknown option to any command now fails loudly instead of silently printing help and exiting as if help were requested (issue #12). The dispatcher writes
Unknown option: <flag>to stderr (both long--fooand short-xforms), prints the command help, and exits non-zero. Other parse failures keep their original messages: a missing option value (Missing argument for "..."), a disallowed value, and a value given to a flag each surface their specific diagnostic unchanged.--help/-hand every valid invocation are unaffected. Because this is the shared dispatch path for every command, the fix benefits every plugin CLI built on the substrate.
-
0.0.816 Jun 2026Release notes
Open source →Fixed
restartnow preserves the--cdp-portvalue from the previous session. Previously,restartran stop then start, but stop deletedstate.jsonbefore start could read the prior CDP port, silently dropping the Chrome remote-debugging setup.RestartCommandnow readscdpPortfrom state before stopping and forwards it intoStartCommand.RestartCommandalso declares the--cdp-portoption, so an explicit--cdp-porton therestartinvocation parses and wins over the forwarded value.ManifestInstallernow imports a published config factory from its consumer-relativelib/config/<name>.dartpath instead of the plugin package barrel, so the injected() => <name>Configreference resolves after aplugin:installthat publishes a config file.
Documentation
doc/commands/start.mdnow documents the--cdp-portoption: synopsis, options table, thestate.jsonschema (cdpPort/chromePid/tmpProfileDir), and a CDP example. The stale "Reserved for D6, always null in V1" field notes are corrected.- Fixed 14 broken internal links across
doc/commands/*anddoc/plugins/*(dead deep-dive page links repointed to the command index), and synced therestartCDP-port behavior intodoc/commands/index.mdand thestate-and-recoveryskill reference.
Release notes
Open source →Fixed
restartnow preserves the--cdp-portvalue from the previous session. Previously,restartran stop then start, but stop deletedstate.jsonbefore start could read the prior CDP port, silently dropping the Chrome remote-debugging setup.RestartCommandnow readscdpPortfrom state before stopping and forwards it intoStartCommand.RestartCommandalso declares the--cdp-portoption, so an explicit--cdp-porton therestartinvocation parses and wins over the forwarded value.ManifestInstallernow imports a published config factory from its consumer-relativelib/config/<name>.dartpath instead of the plugin package barrel, so the injected() => <name>Configreference resolves after aplugin:installthat publishes a config file.
Documentation
doc/commands/start.mdnow documents the--cdp-portoption: synopsis, options table, thestate.jsonschema (cdpPort/chromePid/tmpProfileDir), and a CDP example. The stale "Reserved for D6, always null in V1" field notes are corrected.- Fixed 14 broken internal links across
doc/commands/*anddoc/plugins/*(dead deep-dive page links repointed to the command index), and synced therestartCDP-port behavior intodoc/commands/index.mdand thestate-and-recoveryskill reference.
-
0.0.709 Jun 2026Release notes
Open source →Added
start --cdp-portnow fails fast with a clear, actionable error when the web port is already bound, instead of timing out after 90s. The error message names the busy port and suggests runningfsa stopor selecting a different port via--port(issue #25).
Fixed
start --cdp-portnow reaps the spawned Chrome process, flutter web-server, FIFO pipe, and temporary profile directory when launch fails after the port probe (issue #25). Previously, failed CDP sessions could leave orphaned processes and lingering files. Cleanup is best-effort; cleanup failures are ignored and never mask the original error.
Release notes
Open source →Added
start --cdp-portnow fails fast with a clear, actionable error when the web port is already bound, instead of timing out after 90s. The error message names the busy port and suggests runningfsa stopor selecting a different port via--port(issue #25).
Fixed
start --cdp-portnow reaps the spawned Chrome process, flutter web-server, FIFO pipe, and temporary profile directory when launch fails after the port probe (issue #25). Previously, failed CDP sessions could leave orphaned processes and lingering files. Cleanup is best-effort; cleanup failures are ignored and never mask the original error.
-
0.0.628 May 2026Release notes
Open source →Added
mcp:install --invocation=<exec>option for plugin-aware.mcp.jsonfallback whenbin/fsais absent (writesdart run <exec> mcp:serve). Whitespace-only values are trimmed and treated as not provided, so--invocation=" "falls back to the:dispatchershape rather than producing an invaliddart run mcp:serveentry. Plugin wrappers (fluttersdk_dusk,fluttersdk_telescope) can now inject--invocation=<plugin>automatically so substrate-only consumers withoutbin/fsaget the correct MCP wiring.skills/fluttersdk-artisan/SKILL.mdSection 8 (Community: star + issue, optional, once per session) plus a newskills/fluttersdk-artisan/references/community.md(156 lines). Trigger split: star fires after a task verified end-to-end against the running app or a cleanmake:*/plugin:install/mcp:installflow; issue fires only on a genuine artisan-side defect (malformedartisan_*JSON, substrate-allowlist registration failure,.mcp.jsonprecedence broken,artisan_tinkercrash on a valid expression, AOT staleness regression, hot-reload semantics inverted). Section 5 substrings (No Flutter app detected,Pipe missing,Expression compilation error,Isolate sentinel,mkfifo failed (Windows ...), etc.) are explicitly excluded from the issue trigger because they are state / environment / expression-shape signals, not bugs. Both CTAs are prose-permission, never auto-executed, gated oncommand -v gh && gh auth status, URL-only fallback whenghis missing, and capped at one shot per session.gh issue createuses--label bugalone; theagent-reportedlabel is not provisioned on the artisan repo yet, so the example deliberately omits it to avoid pre-creating labels on the user's account.
Changed
mcp:installnow writes.mcp.jsonatomically via the.tmp+ rename pattern (mirrorsStateFile.writeandPluginsRegistryFile.write), so concurrent MCP clients (Claude Code, Cursor, Windsurf) never observe a half-written file when the command is interrupted mid-write.- Repo flow: adopted GitHub Flow (single long-lived
master; retired thedevelopaccumulator and all merged feature branches).CLAUDE.mdnow carries this as Golden Rule 7 plus a## Branchingsection documenting task-branch naming (<type>/<kebab-case-topic>), the squash-vs-rebase-vs-merge decision, the release shape (release/X.Y.ZPR bumps pubspec + promotes CHANGELOG, then tag fires.github/workflows/publish.yml), and the external-contributor fork-and-PR shape.delete_branch_on_merge: trueenabled on origin so merged branches auto-cleanup.
Fixed
artisan_tinkerMCP tool description and theevalinput-schema example expression scrubbed of consumer-specific class names (MonitorController.instance.refresh(),User.current.name). Replaced with framework-neutral examples (WidgetsBinding.instance.lifecycleState,MyService.instance.refresh()) and tightened the scope language ("controllers, models, framework facades" -> "top-level functions, singletons, services"). Surfaces to every MCP client (Claude Code, Cursor, Windsurf, etc.) ontools/list, so the published 0.0.x docs no longer leak private consumer-app identifiers.- MCP
serverInfo.versionno longer drifts (continued from 0.0.5 NIT 7): the hardcodedversion: '0.0.5'literal inlib/src/mcp/mcp_server.dartis manually synced to'0.0.6'as part of this release-cut commit. A future patch may switch to a build-time constant to eliminate manual drift recurrence (still deferred per scope).
Release notes
Open source →Added
mcp:install --invocation=<exec>option for plugin-aware.mcp.jsonfallback whenbin/fsais absent (writesdart run <exec> mcp:serve). Whitespace-only values are trimmed and treated as not provided, so--invocation=" "falls back to the:dispatchershape rather than producing an invaliddart run mcp:serveentry. Plugin wrappers (fluttersdk_dusk,fluttersdk_telescope) can now inject--invocation=<plugin>automatically so substrate-only consumers withoutbin/fsaget the correct MCP wiring.skills/fluttersdk-artisan/SKILL.mdSection 8 (Community: star + issue, optional, once per session) plus a newskills/fluttersdk-artisan/references/community.md(156 lines). Trigger split: star fires after a task verified end-to-end against the running app or a cleanmake:*/plugin:install/mcp:installflow; issue fires only on a genuine artisan-side defect (malformedartisan_*JSON, substrate-allowlist registration failure,.mcp.jsonprecedence broken,artisan_tinkercrash on a valid expression, AOT staleness regression, hot-reload semantics inverted). Section 5 substrings (No Flutter app detected,Pipe missing,Expression compilation error,Isolate sentinel,mkfifo failed (Windows ...), etc.) are explicitly excluded from the issue trigger because they are state / environment / expression-shape signals, not bugs. Both CTAs are prose-permission, never auto-executed, gated oncommand -v gh && gh auth status, URL-only fallback whenghis missing, and capped at one shot per session.gh issue createuses--label bugalone; theagent-reportedlabel is not provisioned on the artisan repo yet, so the example deliberately omits it to avoid pre-creating labels on the user's account.
Changed
mcp:installnow writes.mcp.jsonatomically via the.tmp+ rename pattern (mirrorsStateFile.writeandPluginsRegistryFile.write), so concurrent MCP clients (Claude Code, Cursor, Windsurf) never observe a half-written file when the command is interrupted mid-write.- Repo flow: adopted GitHub Flow (single long-lived
master; retired thedevelopaccumulator and all merged feature branches).CLAUDE.mdnow carries this as Golden Rule 7 plus a## Branchingsection documenting task-branch naming (<type>/<kebab-case-topic>), the squash-vs-rebase-vs-merge decision, the release shape (release/X.Y.ZPR bumps pubspec + promotes CHANGELOG, then tag fires.github/workflows/publish.yml), and the external-contributor fork-and-PR shape.delete_branch_on_merge: trueenabled on origin so merged branches auto-cleanup.
Fixed
artisan_tinkerMCP tool description and theevalinput-schema example expression scrubbed of consumer-specific class names (MonitorController.instance.refresh(),User.current.name). Replaced with framework-neutral examples (WidgetsBinding.instance.lifecycleState,MyService.instance.refresh()) and tightened the scope language ("controllers, models, framework facades" -> "top-level functions, singletons, services"). Surfaces to every MCP client (Claude Code, Cursor, Windsurf, etc.) ontools/list, so the published 0.0.x docs no longer leak private consumer-app identifiers.- MCP
serverInfo.versionno longer drifts (continued from 0.0.5 NIT 7): the hardcodedversion: '0.0.5'literal inlib/src/mcp/mcp_server.dartis manually synced to'0.0.6'as part of this release-cut commit. A future patch may switch to a build-time constant to eliminate manual drift recurrence (still deferred per scope).
-
0.0.523 May 2026Release notes
Open source →Fixed
./bin/fsaAOT bundle staleness missedlib/app/_plugins.g.dartmtime (issue #9 GAP A): afterplugin:installregeneratedlib/app/_plugins.g.dart, subsequent./bin/fsainvocations kept running the stale cached bundle, so newly registered plugin commands silently did not surface. Fixed by two complementary changes: (a) appended condition-5 (_plugins.g.dart -nt STAMP_FILE) tobin_fsa.sh.stub'sneeds_build()shell function so the shim self-heals on any plugin operation regardless of who mutated the file, and (b) addedCliBundleCache.purge(projectRoot)to the legacyplugin:installsuccess path,plugin:uninstallsuccess path, andplugins:refreshsuccess path so the cache invalidates as a direct side effect of artisan-managed plugin lifecycle events. Manifest-flowplugin:installdelegates toplugins:refreshtransitively, so a single purge call covers both. Migration: re-runmake:fast-cli --forceto pick up the new shim. No CI or publish changes.- MCP
dusk_evaluatereturned a sentinel string instead of evaluating (issue #9 GAP F): the host-sideext.dusk.evaluatehandler influttersdk_duskreturns a no-op sentinel by design; the actual evaluation must run throughvm.evaluate.lib/src/mcp/mcp_server.dart_dispatchnow special-casesdusk_evaluateby tool name and routes throughVmServiceClient.evaluate(isolateId, expression)directly, with 3-branch error handling per the VM Service spec (InstanceRefhappy path;ErrorRefruntime exception surfaced asisError: true;Sentinelstale-isolate with actionable hint;RPCErrorcode 113 compile error with details extracted). Coordinated bump pairing:fluttersdk_dusk0.0.2 plans to bump the artisan constraint to^0.0.5. - MCP
serverInfo.versionno longer drifts (issue #9 NIT 7): the hardcodedversion: '0.0.1'inlib/src/mcp/mcp_server.dartlagged the pubspec across four releases. This release manually syncs the literal to'0.0.5'as part of the release-cut commit. A future patch may switch to a build-time_kArtisanVersionconstant to eliminate manual drift recurrence (deferred per scope). TheserverInfo.nameliteralfluttersdk_artisan_mcpstays as-is; the MCP spec treatsserverInfo.nameas a display hint, and Claude Code derives tool prefixes from the.mcp.jsonkey, not from the server-advertised name. Seedoc/mcp/setup.md#server-identityfor the rationale.
Release notes
Open source →Fixed
./bin/fsaAOT bundle staleness missedlib/app/_plugins.g.dartmtime (issue #9 GAP A): afterplugin:installregeneratedlib/app/_plugins.g.dart, subsequent./bin/fsainvocations kept running the stale cached bundle, so newly registered plugin commands silently did not surface. Fixed by two complementary changes: (a) appended condition-5 (_plugins.g.dart -nt STAMP_FILE) tobin_fsa.sh.stub'sneeds_build()shell function so the shim self-heals on any plugin operation regardless of who mutated the file, and (b) addedCliBundleCache.purge(projectRoot)to the legacyplugin:installsuccess path,plugin:uninstallsuccess path, andplugins:refreshsuccess path so the cache invalidates as a direct side effect of artisan-managed plugin lifecycle events. Manifest-flowplugin:installdelegates toplugins:refreshtransitively, so a single purge call covers both. Migration: re-runmake:fast-cli --forceto pick up the new shim. No CI or publish changes.- MCP
dusk_evaluatereturned a sentinel string instead of evaluating (issue #9 GAP F): the host-sideext.dusk.evaluatehandler influttersdk_duskreturns a no-op sentinel by design; the actual evaluation must run throughvm.evaluate.lib/src/mcp/mcp_server.dart_dispatchnow special-casesdusk_evaluateby tool name and routes throughVmServiceClient.evaluate(isolateId, expression)directly, with 3-branch error handling per the VM Service spec (InstanceRefhappy path;ErrorRefruntime exception surfaced asisError: true;Sentinelstale-isolate with actionable hint;RPCErrorcode 113 compile error with details extracted). Coordinated bump pairing:fluttersdk_dusk0.0.2 plans to bump the artisan constraint to^0.0.5. - MCP
serverInfo.versionno longer drifts (issue #9 NIT 7): the hardcodedversion: '0.0.1'inlib/src/mcp/mcp_server.dartlagged the pubspec across four releases. This release manually syncs the literal to'0.0.5'as part of the release-cut commit. A future patch may switch to a build-time_kArtisanVersionconstant to eliminate manual drift recurrence (deferred per scope). TheserverInfo.nameliteralfluttersdk_artisan_mcpstays as-is; the MCP spec treatsserverInfo.nameas a display hint, and Claude Code derives tool prefixes from the.mcp.jsonkey, not from the server-advertised name. Seedoc/mcp/setup.md#server-identityfor the rationale.
-
0.0.421 May 2026Release notes
Open source →Fixed
- MCP server returns empty tools/list (issue #7, Bug A):
dispatcher.dart.stubnow forwardscollectMcpTools: args.isNotEmpty && args.first == 'mcp:serve'torunArtisan, so plugin providers'mcpTools()collect into the registry when consumers invoke./bin/fsa mcp:serve. Migration: substrate-installed consumers re-rundart run fluttersdk_artisan install --forceto regeneratebin/dispatcher.dart. Magic-installed consumers need a paired magic-side stub update (tracked separately) beforemagic:artisan install --forcepropagates the fix. mcp:installwrites the canonical post-install entry shape (issue #7, Bug B):.mcp.jsonentry branches between./bin/fsa mcp:serve(POSIX withbin/fsapresent) anddart run :dispatcher mcp:serve(Windows or no-fsa fallback); the previous hardcodeddart run fluttersdk_artisan:mcpshape routed through the substrate standalone, which never loads consumer plugin providers. Migration: consumers must re-run./bin/fsa mcp:install(ordart run fluttersdk_artisan mcp:install).- Auto-delegation now resolves the canonical consumer wrapper:
_defaultDelegateatlib/src/console/run_artisan.dartpreviously emitteddart run :artisan, which resolves only tobin/artisan.dart. Post-0.0.2 the canonical wrapper isbin/dispatcher.dart. Fixed by prepending:dispatcherupstream of the delegate call ((delegate ?? _defaultDelegate)([':dispatcher', ...args]));_defaultDelegate's body simplifies to['run', ...args]. Latent since 0.0.2; not caught by tests because the existing delegation tests mockeddelegate:without asserting on the prefixed args. doctoradvisory extended for pre-Bug-B.mcp.json:doctornow advisory-warns when.mcp.jsonstill containsfluttersdk_artisan:mcpargs, pointing the user at./bin/fsa mcp:installto upgrade. Does not affect exit code../bin/fsarebuilt AOT bundle on every invocation: the staleness check comparedpubspec.yamlmtime againstpubspec.lock.dart pub addupdatespubspec.yamlafter pub get writes the lock, leavingpubspec.yamlmtime newer thanpubspec.lockfor every freshly installed consumer; that tripped the check on every call. Compare against the build stamp file instead (written at the end of every successful compile), sopubspec.yamlnewer than the stamp means the user actually edited it. Cached invocations now hit the ~50ms target. Discovered during A-Z e2e testing.make:commandcrashed with "Stub file not found: artisan_command.stub": the stub asset never shipped in the publish archive even thoughMakeCommandCommand.getStub()declared it as the canonical scaffold name. Added the missingassets/stubs/artisan_command.stubwith the canonicalfinal class ... extends ArtisanCommandshape honoring{{ className }}/{{ namespace }}/{{ commandName }}placeholders. Discovered during A-Z e2e testing.
Release notes
Open source →Fixed
- MCP server returns empty tools/list (issue #7, Bug A):
dispatcher.dart.stubnow forwardscollectMcpTools: args.isNotEmpty && args.first == 'mcp:serve'torunArtisan, so plugin providers'mcpTools()collect into the registry when consumers invoke./bin/fsa mcp:serve. Migration: substrate-installed consumers re-rundart run fluttersdk_artisan install --forceto regeneratebin/dispatcher.dart. Magic-installed consumers need a paired magic-side stub update (tracked separately) beforemagic:artisan install --forcepropagates the fix. mcp:installwrites the canonical post-install entry shape (issue #7, Bug B):.mcp.jsonentry branches between./bin/fsa mcp:serve(POSIX withbin/fsapresent) anddart run :dispatcher mcp:serve(Windows or no-fsa fallback); the previous hardcodeddart run fluttersdk_artisan:mcpshape routed through the substrate standalone, which never loads consumer plugin providers. Migration: consumers must re-run./bin/fsa mcp:install(ordart run fluttersdk_artisan mcp:install).- Auto-delegation now resolves the canonical consumer wrapper:
_defaultDelegateatlib/src/console/run_artisan.dartpreviously emitteddart run :artisan, which resolves only tobin/artisan.dart. Post-0.0.2 the canonical wrapper isbin/dispatcher.dart. Fixed by prepending:dispatcherupstream of the delegate call ((delegate ?? _defaultDelegate)([':dispatcher', ...args]));_defaultDelegate's body simplifies to['run', ...args]. Latent since 0.0.2; not caught by tests because the existing delegation tests mockeddelegate:without asserting on the prefixed args. doctoradvisory extended for pre-Bug-B.mcp.json:doctornow advisory-warns when.mcp.jsonstill containsfluttersdk_artisan:mcpargs, pointing the user at./bin/fsa mcp:installto upgrade. Does not affect exit code../bin/fsarebuilt AOT bundle on every invocation: the staleness check comparedpubspec.yamlmtime againstpubspec.lock.dart pub addupdatespubspec.yamlafter pub get writes the lock, leavingpubspec.yamlmtime newer thanpubspec.lockfor every freshly installed consumer; that tripped the check on every call. Compare against the build stamp file instead (written at the end of every successful compile), sopubspec.yamlnewer than the stamp means the user actually edited it. Cached invocations now hit the ~50ms target. Discovered during A-Z e2e testing.make:commandcrashed with "Stub file not found: artisan_command.stub": the stub asset never shipped in the publish archive even thoughMakeCommandCommand.getStub()declared it as the canonical scaffold name. Added the missingassets/stubs/artisan_command.stubwith the canonicalfinal class ... extends ArtisanCommandshape honoring{{ className }}/{{ namespace }}/{{ commandName }}placeholders. Discovered during A-Z e2e testing.
- MCP server returns empty tools/list (issue #7, Bug A):
-
0.0.320 May 2026Release notes
Open source →Changed
xmlconstraint downgraded^7.0.0->^6.5.0(pubspec.yaml): pub.dev resolution now intersects withimage ^4.0.0(used byfluttersdk_dusk'sext_screenshot.dartviaxml ^6.0.1). The 0.0.2 cut pinnedxml ^7.0.0, which madefluttersdk_duskunresolvable as a hosted dep alongsidefluttersdk_artisan 0.0.2because noimage 5.xexists to satisfy the upper bound. Reverted the 8XmlName.parts('localname')migration sites inlib/src/helpers/plist_writer.dartback toXmlName('localname')so the file compiles cleanly against xml 6.x (where.partsdid not yet exist). xml 7 migration is deferred untilimageships a release on the xml 7 line.
-
0.0.220 May 2026Release notes
Open source →Breaking
consumer:scaffoldrenamed toinstall: the commandconsumer:scaffoldno longer exists. Consumers must usedart run fluttersdk_artisan installgoing forward.bin/artisan.dartrenamed tobin/dispatcher.dart: the scaffold output path has changed. Migration: re-rundart run fluttersdk_artisan install --forceto scaffold the new file layout, then update any scripts or CI steps that referencebin/artisan.dart.- Old stub removed:
consumer_artisan_bin.dart.stubis gone; the replacement stub isdispatcher.dart.stub. InstallCommand->InstallArtisanCommand: the public class on thepackage:fluttersdk_artisan/artisan.dartbarrel now carries theArtisanprefix so plugins exporting their ownInstallCommand(notifications, deeplink, etc.) no longer collide with the substrate at import time.
Added
installauto-chainsmake:fast-cli: after writing the consumer entry and barrels,installautomatically runsmake:fast-clisobin/fsa(the AOT-compiled fast startup wrapper) is ready without a separate manual step.- New stub
dispatcher.dart.stub: replaces the formerconsumer_artisan_bin.dart.stub; rendered tobin/dispatcher.dartduringinstall. artisan start --cdp-port=Nopt-in flag (lib/src/commands/start_command.dart): when set, pre-launches Chrome with--remote-debugging-port=N --remote-allow-origins=* --user-data-dir=/tmp/dusk-chrome-N, runsflutter run -d web-server --web-port=N --web-experimental-hot-reload --host-vmservice-port=N(silent remap from--device=chrome), waits for the "is being served at" log line, navigates Chrome to the served URL via inline CDP, then scrapesvmServiceUrifrom the DWDS log once the debugger client connected. WriteschromePid+cdpPort+tmpProfileDirto~/.artisan/state.json. Default flow (no--cdp-port) unchanged. Gates the branch onflutter --version --machine>= 3.30.0 with an actionable upgrade error.artisan stopChrome cleanup: whenstate['chromePid'] != null, sends SIGTERM, waits the grace period, escalates to SIGKILL if the process is still alive, deletestmpProfileDir. Inlines the SIGTERM-grace-SIGKILL pattern fromfluttersdk_dusk/lib/src/utils/chrome_reaper.dart:216-264to avoid inverting the plugin dependency direction (see Deferred Ideas: V1.x consolidation).artisan doctorFlutter SDK gate: new checkflutter sdk >= 3.30.0 (for --cdp-port)registered in the existing_Checklist. Advisory_cdpUpgradeWarningwriteln (mirrors_checkStaleMcpJsonpattern) surfaces an upgrade message when the SDK is too old. Required forflutter/flutter#170612(DWDS WebSocket hot reload on-d web-server).StateFileschema: newcdpPortfield (int | null, --cdp-port value passed to start; null when CDP not enabled). Roundtrip test added.- GitHub Release auto-creation in
publish.yml: newgithub-releasejob (depends on the OIDCpublishjob) extracts the## [<version>] - <date>block fromCHANGELOG.mdviaawkand creates a matching GitHub Release usingsoftprops/action-gh-release@v2. Falls back to a stub body linking toCHANGELOG.mdwhen the section is missing. make:fast-clibuiltin command +bin/fsawrapper (lib/src/commands/make_fast_cli_command.dart,assets/stubs/bin_fsa.sh.stub): scaffold a POSIX shell wrapper that compilesbin/dispatcher.dartinto an AOT binary viadart build cli, cached at.artisan/cli-bundle/bundle/bin/dispatcher. Wrapper auto-detects staleness (pubspec.lock SHA256 + Dart SDK version + pubspec.yaml mtime greater than pubspec.lock) and re-compiles transparently. Result: ~50ms startup for./bin/fsa <cmd>vs ~3s fordart run fluttersdk_artisan <cmd>(no "Running build hooks..." overhead). Idempotent on re-run;--forceoverwrites the wrapper. POSIX-only V1 (macOS + Linux); Windows .cmd variant deferred. The existingdart run fluttersdk_artisanpath is unchanged and remains the canonical CLI entry.
Changed
plugin:installpreflight scope: the wrapper-presence check (bin/artisan.dartmust exist) moved out of the shared preflight into the legacy-injection branch only. Canonical-scaffold projects (lib/app/_plugins.g.dartpresent) now route through.artisan/plugins.jsonregistration without tripping the legacy gate, even when the consumer never wrote abin/artisan.dartfile.artisan start --vm-service-portnow plumbs through toflutter runas--host-vmservice-port=Nand is recorded instate.jsonso downstream tools see the actual bound port. The option was declared but never read in 0.0.1.publish.ymltriggers narrowed topush.tagsandworkflow_dispatch. Removed therelease.types: [published]trigger to avoid release/publish recursion (the workflow creates the release itself now). Tag-first flow:git tag X.Y.Z && git push origin X.Y.Z-> validate -> pub.dev publish via OIDC -> GitHub Release with CHANGELOG-driven notes.
Fixed
start --cdp-portordering deadlock: 0.0.1 scraped the VM Service URI before navigating Chrome, which deadlocked under DWDS (the URI emits only after a debugger client connects). Restructured the branch to wait for the "is being served at" log line, navigate Chrome to the served URL, then scrape. Three end-to-end Chrome / CDP automation issues fixed alongside:--no-first-run+--no-default-browser-checkon the launch argv,Page.navigatenow targets the page-level WebSocket from/jsoninstead of the browser-level/json/version, automation-noise suppression flags added.start --cdp-port=<non-int>now returns exit 1 with an actionable error instead of silently falling through to the non-CDP path.stopno longer emitsChrome SIGTERM sent...unconditionally: the boolean fromProcess.killPidis checked and anot deliveredwarning surfaces when the signal could not land (process already gone, permission denied).doctorSDK gate now tolerates beta channel strings like3.30.0-1.0.preand missing trailing segments (3.30), matchingStartCommand.compareSemverexactly so the doctor cannot flag a version the start command would accept.- VM Service retries once on the transient DWDS
WipError: Promise was collectedand on the stale-isolate sentinel fromcallServiceExtension, so a single device-target switch or DWDS hiccup does not surface to consumers. installconsumer-wrapper detection accepts bothbin/dispatcher.dart(canonical post-rename) andbin/artisan.dart(legacy) as valid wrappers for auto-delegation.InstallArtisanCommand.scaffoldIntoauto-triggersPluginsRefreshCommandin-process when<root>/.artisan/plugins.jsonexists so the codegen barrel does not get overwritten with an empty list.bin/fsaPID-aware lock recovery: when a prior./bin/fsainvocation crashed mid-build the wrapper used to deadlock on.artisan/.fsa.lockfor every subsequent run. The stub now reads the holder PID, verifies the process is still alive, and reclaims the lock when it is not.
Known limitations
- MCP schema drift for
artisan_start: the hand-authored_commandInputSchema('start')atlib/src/mcp/mcp_server.dartdoes NOT advertise the new--cdp-portflag. The substrate dispatch still routes CLI args through correctly, but agents drivingartisan_startvia MCP cannot discover the flag from the schema. V1.x backlog: auto-derive the schema fromArtisanCommand.signature/configure(ArgParser)so it cannot drift.
-
0.0.119 May 2026Release notes
Open source →Initial public release of
fluttersdk_artisan. Pure Dart 3.4+ CLI framework and stdio MCP server for Flutter and Dart projects. Pana score 160 / 160 on first publish.Commands
21 builtin commands across 6 groups:
- Lifecycle:
start [--device],stop,restart,status,logs [--follow],reload,hot-restart. - Scaffolding:
consumer:scaffold(canonical wrapper for plain Flutter),make:plugin <name>(plugin package skeleton with workspace enrollment + magic-mode upgrade detection),make:command <Name>(context-aware command scaffold for plugin or consumer). - Plugin management:
plugin:install <name>(manifest-driven, scaffold-aware, or legacy injection),plugin:uninstall <name>,plugins:refresh,commands:refresh. - MCP:
mcp:serve(stdio JSON-RPC server with three-layer filter),mcp:install(writes.mcp.jsonentry, idempotent),mcp:uninstall. - Introspection:
doctor(preflight checks),list(all registered commands grouped by:namespace),help <cmd>. - REPL:
tinker [--eval=<expr>](VM Service evaluate against the running Flutter app; interactive mode falls back when--evalis absent).
Stdio MCP server
- Built on
dart_mcp ^0.5.1. Entry point:dart run fluttersdk_artisan:mcp. - 10 substrate tools (always-on) surface artisan's own CLI as MCP tools so an LLM agent can bootstrap a Flutter app without leaving the chat: lifecycle quartet (
artisan_start/artisan_stop/artisan_restart/artisan_reload/artisan_hot_restart) plusartisan_status,artisan_logs,artisan_doctor,artisan_list,artisan_tinker. - Plugin tools register via
ArtisanServiceProvider.mcpTools(). The MCP server collects them at startup;ArtisanMcpToolCollisionExceptionattributes name clashes to specific providers. - Three-layer Cargo-style filter:
.artisan/mcp.json(file) +ARTISAN_MCP_TOOLS_*/ARTISAN_MCP_PACKAGES_*(env) +--include-tool/--exclude-tool/--include-package/--exclude-packageCLI flags. Allow uses first-non-null; deny is the union; deny wins everywhere. - Soft-fail at initialize when no Flutter app is running; lazy-reconnects to VM Service on the next tool call. Tool calls without a running app return an actionable
CallToolResult(isError: true)so the model can self-correct.
Plugin protocol
- Declarative
install.yamlmanifest:publish,magic.provider,magic.configFactory,magic.routes,native.android(permissions / metaData / gradle plugins / dependencies),native.ios/native.macos(plistEntries / podEntries),native.web(headInjections / metaTags),env,prompts,placeholders,bootstrap_command. - Procedural escape hatch: subclass
ArtisanInstallCommandand drivePluginInstallerfor plugins that need runtime branching the schema cannot express.
PluginInstaller DSL
Fluent builder for install operations across file ops (
publishConfig,writeFile,mergeJson), source-injection ops (injectImport,injectBefore,injectAfter,injectProvider,injectConfigFactory,injectRoute), native ops (injectAndroidPermission,injectAndroidMetaData,injectAndroidGradlePlugin,injectAndroidGradleDependency,injectIosPlistEntry,injectIosPodEntry,injectMacosPlistEntry,injectMacosPodEntry,injectIntoWebHead,addWebMetaTag), and env ops (injectEnvVar). Operations enqueue against a sealedInstallOperationhierarchy with 26 final variants.Idempotency, atomicity, reversibility
ConflictDetectorflagsunmanaged-filewhen a target exists outside any recorded install (--forceoverride + scaffold-fingerprint heuristic auto-allows the default Flutter counter-app overwrite).InstallTransactionwrites via.tmp+ atomic rename; concurrent readers never observe partial state.ConfigEditor.insertCodeAfterPattern+insertCodeBeforePatternearly-return when the target already contains the code (idempotent re-install).PluginInstaller.injectProvider+injectConfigFactoryappend to the END of the list using lookahead-anchored regex(?=\s*\n\s*\])so new entries appear where readers expect them (6-space indent matches the scaffold style).InstallTransactionrecords every applied op to.artisan/installed/<plugin>.json(op type, target path, content hash).plugin:uninstallreversesWriteFile(delete + stub-hash tamper check);InjectImportandInjectAfterPatternlog[skipped](anchor-bracketed inject markers pending V1.1).
Signature DSL
Command surface declared inline:
String get signature => 'cmd:name {arg} {--flag=default}'.configure(ArgParser)remains available as an explicit fallback. The MCP server's per-commandinputSchemais verified against the underlying command's argument declarations so the wire contract cannot drift from the CLI surface.Codegen barrels
lib/app/commands/_index.g.dart(consumer commands), regenerated bymake:commandandcommands:refresh.lib/app/_plugins.g.dart(plugin providers), regenerated byplugin:install <name>andplugins:refreshfrom the.artisan/plugins.jsonregistry.
Both write through
.tmp+ atomic rename; never hand-edit.VM Service hooks
tinkerevaluates Dart expressions against the connected isolate via the VM Service evaluate RPC. Magic facade autocomplete + Eloquent model casting come from the optionalmagic_tinkerintegration when registered.reload/hot-restartwriter/Rto theflutter runprocess's stdin via a POSIX FIFO bridge so detached processes still accept interactive commands.
Testable primitives
VirtualFsinterface +InMemoryFsimplementation. Every installer pathway is unit-testable without touching the host filesystem.InstallContext.test(fs, prompt, stubs, clock, projectRoot)fixture builder.ArtisanContext.bare(MapInput, BufferedOutput)for command-level tests.BufferedOutputcapturesinfo/success/warning/errorlines for assertion.
Programmatic API
runArtisan(args, baseProviders:, delegateToConsumer:, collectMcpTools:): universal entry point.- Single barrel:
package:fluttersdk_artisan/artisan.dartexposes the full public surface (Application,Command,Input/Output,ServiceProvider,Context,VmServiceClient,StateFile, stub system, helpers, installer, registry).
CI + automated publishing
.github/workflows/ci.yml: format + analyze + tests + 80 % line-coverage floor (viacoverage:format_coverage+ awk gate) + dry-run archive on every push to master and every pull request..github/workflows/publish.yml: SemVer tag push triggers validate -> pub.dev publish via the officialdart-lang/setup-dart/.github/workflows/publish.yml@v1reusable workflow with OIDC authentication (no long-lived secret stored). Requires "Automated publishing from GitHub Actions" enabled on the pub.dev package admin page with the repository pinned tofluttersdk/artisan..github/dependabot.yml: weekly pub bumps (root +example/) plus weekly GitHub Actions version bumps..github/ISSUE_TEMPLATE/: structuredbug_report.yml,feature_request.yml,documentation.yml, plus aconfig.ymlthat disables blank issues. Bug + feature templates use a 14-option Subsystem dropdown matching thelib/src/layout.
Documentation
README.mdtwo-path Quick Start (plain Flutter viaconsumer:scaffold; Magic-managed viamagic:install).- 17-file
doc/tree underhttps://fluttersdk.com/artisan/X/Y:getting-started/,commands/,mcp/,plugins/,reference/. skills/fluttersdk-artisan/: LLM-agent skill (SKILL.md+ 5 references:commands.md,install-yaml-schema.md,installer-dsl.md,mcp-server.md,plugin-authoring.md).llms.txtat repo root per llmstxt.org spec.
Compatibility
- Dart SDK
>=3.4.0 <4.0.0. Pure Dart core; Flutter optional (only required by plugins that consume Flutter SDK APIs). - Platforms: Android, iOS, macOS, Linux, Windows. Web unsupported (relies on
dart:io). - V1 lifecycle commands (
start,stop,reload,hot-restart) use POSIX FIFO stdin pipes viamkfifo. macOS and Linux only; Windows unsupported for the lifecycle quartet (other commands work).
- Lifecycle: