@mmerterden/multi-agent-pipeline 16.14.0 → 16.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +5 -4
  3. package/README.tr.md +5 -4
  4. package/docs/adr/0010-own-code-graph.md +6 -0
  5. package/docs/adr/0011-dormant-ci.md +87 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/architecture.md +2 -2
  8. package/docs/ecosystem.md +5 -5
  9. package/install/_common.mjs +0 -1
  10. package/install/_dev-only-files.mjs +1 -0
  11. package/install/claude.mjs +14 -4
  12. package/install/index.mjs +8 -1
  13. package/package.json +6 -5
  14. package/pipeline/commands/multi-agent/help/SKILL.md +44 -39
  15. package/pipeline/commands/multi-agent/steer/SKILL.md +109 -0
  16. package/pipeline/commands/multi-agent/sync/SKILL.md +5 -7
  17. package/pipeline/multi-agent-refs/cross-cli-contract.md +4 -4
  18. package/pipeline/multi-agent-refs/phases/operations.md +23 -0
  19. package/pipeline/multi-agent-refs/phases/phase-0-init.md +4 -0
  20. package/pipeline/multi-agent-refs/phases/phase-7-report.md +1 -1
  21. package/pipeline/multi-agent-refs/phases.md +38 -0
  22. package/pipeline/preferences-template.json +2 -10
  23. package/pipeline/rules/outside-the-pipeline.md +1 -1
  24. package/pipeline/schemas/agent-state.schema.json +35 -0
  25. package/pipeline/schemas/analysis-spec.schema.json +8 -2
  26. package/pipeline/schemas/conventions-output.schema.json +2 -13
  27. package/pipeline/scripts/build-references.mjs +22 -6
  28. package/pipeline/scripts/code-graph-rules/android.json +4 -21
  29. package/pipeline/scripts/code-graph-rules/go.json +4 -19
  30. package/pipeline/scripts/code-graph-rules/node.json +4 -21
  31. package/pipeline/scripts/feedback-send.mjs +3 -1
  32. package/pipeline/scripts/gate-linux.sh +62 -0
  33. package/pipeline/scripts/localize-commands.mjs +1 -2
  34. package/pipeline/scripts/pre-push-check.sh +6 -4
  35. package/pipeline/scripts/usage-report.mjs +16 -14
  36. package/pipeline/scripts/validate-analysis-doc.mjs +46 -27
  37. package/pipeline/scripts/validate-analysis.mjs +7 -1
  38. package/pipeline/scripts/validate-complaint-doc.mjs +24 -8
  39. package/pipeline/scripts/write-state.mjs +71 -12
  40. package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +111 -0
  41. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -4
@@ -0,0 +1,109 @@
1
+ ---
2
+ description: "Queue an instruction for a task that is already running. It is applied at the next phase boundary, not mid-phase. Use when a run is going the wrong way and killing it would throw away good work."
3
+ description-tr: "Zaten koşmakta olan bir göreve talimat bırakır. Talimat faz ortasında değil, bir sonraki faz sınırında uygulanır. Koşu yanlış yöne gidiyorsa ve kill etmek iyi işi de çöpe atacaksa kullanılır."
4
+ argument-hint: "#id \"<instruction>\" - e.g. #3 \"the field is called web, not frontend\". With no instruction, you are asked for it."
5
+ ---
6
+
7
+ # multi-agent steer - correct a run without stopping it
8
+
9
+ **Input**: $ARGUMENTS
10
+
11
+ Leave one instruction for a running task. The next phase reads it before it
12
+ starts work, applies it, and marks it consumed.
13
+
14
+ This exists because the alternative was losing the run. `kill` stops a task and
15
+ deletes its worktree; `resume` picks a stopped one back up. Neither helps while
16
+ a phase is in flight, so a correction that arrived mid-run - "that field is
17
+ called `web`, not `frontend`", "keep the analysis, drop the rest" - had nowhere
18
+ to go, and the run carried on in the wrong direction until it finished.
19
+
20
+ **Not a second prompt.** One instruction is queued at a time. Steering a task
21
+ that already has an unconsumed instruction replaces it, after showing you what
22
+ is being replaced.
23
+
24
+ ## Steps
25
+
26
+ 1. **Find the task** - parse `#N` or `{JIRA-KEY}-XXXXX` from the argument,
27
+ the same way `kill` and `resume` do. Locate its `agent-state.json`:
28
+
29
+ ```bash
30
+ find {repo}/.worktrees/ -name "agent-state.json" -maxdepth 2
31
+ find $HOME/.claude/logs/multi-agent -maxdepth 4 -name agent-state.json -path '*/artifacts/*'
32
+ ```
33
+
34
+ Not found → `ERR: no task #N. '/multi-agent:status' lists what is running.`
35
+
36
+ 2. **Check it can still be steered** - read `status` and `currentPhase`:
37
+
38
+ | State | What to do |
39
+ |---|---|
40
+ | `in_progress` | Queue it. This is the case the command is for. |
41
+ | `paused` / `failed` | Say the task is not running, and that `/multi-agent:resume #N` will re-enter with the instruction applied at that phase's entry. Queue it. |
42
+ | `complete` | Refuse. Nothing will read it. Point at `/multi-agent` for a follow-up run. |
43
+
44
+ `currentPhase` is 7 and status is `in_progress` → warn that Phase 7 is the
45
+ last one, so an instruction queued now may never be consumed.
46
+
47
+ 3. **Read the instruction** - from the argument, or ask for it when the
48
+ argument carries only an id. Verbatim, up to 4000 characters. Do not
49
+ summarize or rewrite it: the phase that consumes it needs the user's own
50
+ words, and a paraphrase is where the meaning goes.
51
+
52
+ 4. **Show what will be queued, and ask**:
53
+
54
+ ```
55
+ Steer #3 ({JIRA-KEY}-12345, Phase 3 Dev, in_progress)
56
+
57
+ "the field is called web, not frontend"
58
+
59
+ Applied at the entry to Phase 4. The current phase finishes as it is.
60
+ ```
61
+
62
+ Already carrying an unconsumed `pendingSteer` → print the old text above the
63
+ new one and ask whether to replace it.
64
+
65
+ 5. **Write it** - through the state writer, never by editing the file, because
66
+ the running task is writing to it too. `$STATE_FILE` is the path from step 1
67
+ and `$INSTRUCTION` the text from step 3:
68
+
69
+ ```bash
70
+ printf '{"pendingSteer":{"text":%s,"at":"%s","appliedAt":null,"appliedPhase":null}}' \
71
+ "$(node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$INSTRUCTION")" \
72
+ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
73
+ | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE"
74
+ ```
75
+
76
+ The instruction is JSON-encoded by `node -e`, not by hand: it is arbitrary
77
+ user text, and a quote or newline in it would otherwise produce invalid JSON
78
+ or, worse, a payload that merges into fields nobody meant to touch.
79
+
80
+ Exit 2 (lock timeout) → the task is mid-write. Retry once, then report it
81
+ rather than forcing the write.
82
+
83
+ 6. **Confirm**: `🧭 Steer queued for #N - applies at the entry to Phase {N+1}`
84
+
85
+ ## What the phase does with it
86
+
87
+ Phase entry, before any work: read a `pendingSteer` that has no `appliedAt`.
88
+ When present, apply it to that phase's context, set `appliedAt` and
89
+ `appliedPhase` - which is what stops it being read a second time - and write a
90
+ `Steer applied` line into `agent-log.md`. The record itself stays, so the run
91
+ keeps the trace of what was asked and when. The contract lives in
92
+ `$HOME/.claude/multi-agent-refs/phases.md` under "Phase entry - pending
93
+ steer" (the file has a second, unrelated "Phase entry" line inside the tracker
94
+ block).
95
+
96
+ Applied at entry rather than the moment it arrives, on purpose: a phase that
97
+ changes target halfway through throws away the work it already did, which is
98
+ the outcome this command exists to avoid.
99
+
100
+ An instruction that contradicts the plan is not silently obeyed. The phase says
101
+ what it is changing, and a contradiction that would invalidate an approved plan
102
+ halts for the user instead of quietly rewriting it.
103
+
104
+ **One limit worth knowing.** The reader is the phase contract, so a task started
105
+ by an install that predates it will not consume the field: the instruction is
106
+ written and nothing picks it up. There is no version stamp on a state file to
107
+ detect this from, so the honest advice is that steer applies to runs started
108
+ after the install carrying it. A run already in flight from an older install is
109
+ still a `kill` or a wait.
@@ -59,7 +59,7 @@ Run every step automatically:
59
59
  ```
60
60
  Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
61
61
  Step 1.5: DETECT Compare timestamps, find stale targets
62
- Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 54 sub-command skills)
62
+ Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 55 sub-command skills)
63
63
  Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 51 specs as refs + 8 agent TOML)
64
64
  Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
65
65
  Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
@@ -166,7 +166,7 @@ If nothing is stale → report "All targets up to date" and stop.
166
166
  Unlike the Copilot step, this one does **not** hand-copy files. The Codex tree is a
167
167
  *transform* of the Claude tree, not a mirror of it, and the transform is real work:
168
168
 
169
- - the 54 sub-command specs become reference files, because Codex silently truncates
169
+ - the 55 sub-command specs become reference files, because Codex silently truncates
170
170
  its skills block (see `cross-cli-contract.md` 2.6 for the measurement)
171
171
  - every `$HOME/.claude/...` reference to a CLI-owned tree is retargeted, with
172
172
  `agents/<persona>.md` becoming `.toml` and the dispatcher becoming the router skill
@@ -479,17 +479,15 @@ When invoked with the `release` argument:
479
479
 
480
480
  ## Sub-Command Sync (Claude Code <-> Copilot CLI Skills)
481
481
 
482
- > Codex takes the Step 2b path instead; see that section.
483
-
484
482
  This runs on the Claude <-> Copilot axis. Codex is NOT synced here: it receives the
485
- same 51 specs as reference files rather than as peer skills, via Step 2b - see
483
+ same 55 specs as reference files rather than as peer skills, via Step 2b - see
486
484
  `cross-cli-contract.md` 2.6 for why the parity axis differs per host.
487
485
 
488
486
  | Claude Code | Copilot CLI |
489
487
  |-------------|-------------|
490
488
  | `~/.claude/commands/multi-agent/{cmd}/SKILL.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
491
489
 
492
- **54 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
490
+ **55 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
493
491
 
494
492
  ```
495
493
  analysis, analysis-resolve, autopilot, build-optimize, channels,
@@ -498,7 +496,7 @@ dev-local-autopilot, diff-explain, feedback, forget, garbage-collect,
498
496
  graph, help, ios-coding-standard, issue, jira, kill, language, local,
499
497
  local-autopilot, log, manual-test, prune-logs, prune-prompts, purge,
500
498
  refactor, resume, resume-local, review, review-analysis, review-issue,
501
- review-jira, routines, save, scan, search, setup, stack, status,
499
+ review-jira, routines, save, scan, search, setup, stack, status, steer,
502
500
  store-ready, sync, test, test-accessibility, test-dark-mode,
503
501
  test-dynamic-type, test-screenshots, testflight-validation, uninstall, update
504
502
  ```
@@ -6,7 +6,7 @@
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Command Inventory (54 files, 50 live commands)
9
+ ## 1. Command Inventory (55 files, 51 live commands)
10
10
 
11
11
  ```
12
12
  analysis, analysis-resolve, autopilot, build-optimize, channels,
@@ -15,7 +15,7 @@ dev-local-autopilot, diff-explain, feedback, forget, garbage-collect,
15
15
  graph, help, ios-coding-standard, issue, jira, kill, language, local,
16
16
  local-autopilot, log, manual-test, prune-logs, prune-prompts, purge,
17
17
  refactor, resume, resume-local, review, review-analysis, review-issue,
18
- review-jira, routines, save, scan, search, setup, stack, status,
18
+ review-jira, routines, save, scan, search, setup, stack, status, steer,
19
19
  store-ready, sync, test, test-accessibility, test-dark-mode,
20
20
  test-dynamic-type, test-screenshots, testflight-validation, uninstall, update
21
21
  ```
@@ -27,12 +27,12 @@ Categories:
27
27
  - **Pipeline entries**: `autopilot`, `local`, `local-autopilot` (plus the bare `/multi-agent` in the dispatcher). Depth is not a command: `/multi-agent` and `local` ask Full or Short at Phase 0 Step 7.5; the two autopilot entries never ask and always run Full.
28
28
  - **Retired stubs** (v16.0.0, deleted next minor - they print a redirect and run no phase): `dev`, `dev-local` redirect to the picker entries with Short; `dev-autopilot`, `dev-local-autopilot` have no equivalent, because fast-plus-unattended no longer exists
29
29
  - **Tail modes** (run the pipeline tail over already-done local work): `resume-local`
30
- - **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `purge`, `uninstall`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `complaint-analysis`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `graph`, `prune-logs`, `prune-prompts`
30
+ - **Ops commands** (one-shot, no worktree): `status`, `log`, `kill`, `steer`, `purge`, `uninstall`, `resume`, `review`, `review-jira`, `review-issue`, `analysis`, `analysis-resolve`, `complaint-analysis`, `build-optimize`, `channels`, `scan`, `search`, `diff-explain`, `garbage-collect`, `graph`, `prune-logs`, `prune-prompts`
31
31
  - **Local audits** (worktree only to build; no commit, push, PR or channels): `design-check`, `testflight-validation`, `ios-coding-standard`. `testflight-validation` additionally never invokes `altool --upload-app` - a validation run must not be able to ship a build by accident.
32
32
  - **Meta-ops**: `setup`, `sync`, `update`, `help`, `refactor`, `test`, `stack`, `manual-test`, `language`
33
33
  - **Routines** (user-defined routine registry; the routines they create are local-only and never synced): `save`, `routines`, `forget`
34
34
 
35
- The count is 54 files and 50 live commands until the four stubs are deleted, at which point both numbers become 50. A stub is still installed and still invocable, so counting it as absent would be wrong; counting it as a command would be worse.
35
+ The count is 55 files and 51 live commands until the four stubs are deleted, at which point both numbers become 51. A stub is still installed and still invocable, so counting it as absent would be wrong; counting it as a command would be worse.
36
36
 
37
37
  > **Inventory drift is a contract violation.** Adding a slash command under `pipeline/commands/multi-agent/` without updating this list + its counterpart Copilot dir (`pipeline/skills/shared/core/multi-agent-<cmd>/`) is a merge blocker. `smoke-commands-skills-parity.sh` enforces command ↔ skill directory parity; `smoke-cross-cli-behavior.sh` enforces behavior parity. This doc is the authoritative command list - bump the count + table together.
38
38
 
@@ -95,6 +95,29 @@ halt per the halt-visibility rule), `3` I/O error.
95
95
  Reads need no wrapper; the rename makes any read see either the old or the new
96
96
  document, never a truncated one.
97
97
 
98
+ **`rev`, and reading before writing back.** Every successful write bumps
99
+ `state.rev`, a counter that only increases. It exists because a read and the
100
+ write that follows it are not one operation. The lock above makes a single write
101
+ atomic and nothing more; the concurrent worktrees named there, and
102
+ `/multi-agent:steer`, which writes into the state file of a task that is still
103
+ running, both land inside the gap between a phase's read and its write-back.
104
+
105
+ A phase that will write back what it read keeps the `rev` it read, re-reads
106
+ immediately before writing, and re-derives its patch when the number has moved.
107
+ `write-state.mjs` takes the higher of the on-disk `rev` and the one in the
108
+ incoming patch, so a stale patch can never walk the counter backwards - but that
109
+ only protects the counter, not the fields. Deciding what to do about a
110
+ concurrent change is the caller's job.
111
+
112
+ A record written before `rev` existed has none: treat a missing `rev` as 0.
113
+
114
+ **Stale locks: liveness outranks age.** A lock is reclaimed when its holder PID
115
+ is no longer alive. A holder that is alive keeps its lock however old the file
116
+ is - the reverse order (age first) is what let a writer delete a live lock and
117
+ lose the update behind it. `WRITE_STATE_LOCK_STALE_MS` now applies only to a
118
+ lock with no readable PID, and `WRITE_STATE_LOCK_ABANDON_MS` (default ten times
119
+ that) is the last-resort ceiling for a PID that has been recycled.
120
+
98
121
  **Halt visibility (required, autopilot included).** A halt is never silent. Whenever a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable), in addition to the `agent-log.md` line: (a) write `state.status = "paused"` and `state.haltReason = "<phase>:<cause>"`; (b) record the cause on the tracker via `phase-tracker.sh meta <phase> halt "<cause>"` and `phase-tracker.sh update <phase> failed`; (c) emit one `>&2` alert line `HALT phase <N>: <cause> - resume with /multi-agent:resume #<id>`; (d) if `prefs.global.usageLog.enabled` is true, emit the end-of-run report so a run that never reaches Phase 7 is still recorded with the phase it stopped at (`state.currentPhase` + `haltReason`) - the emitter no-ops when it is off or unconfigured:
99
122
 
100
123
  ```bash
@@ -631,8 +631,12 @@ Phase 0 owns `agent-state.json`. Do not call
631
631
 
632
632
  ```bash
633
633
  node "$HOME/.claude/scripts/phase0-exit-gate.mjs" "$TASK_ID" --input "$ORIGINAL_INPUT"
634
+ node "$HOME/.claude/scripts/usage-report.mjs" --task-id "$TASK_ID" >/dev/null 2>&1 || true
634
635
  ```
635
636
 
637
+ The second line reports the run as started: reporting only from Phase 7 reported
638
+ only runs that finish, and few do. Phase 7 upserts the same key over it.
639
+
636
640
  It asserts three things, each of which has failed silently in a real run:
637
641
 
638
642
  1. **`agent-state.json` exists.** A run once reported Phase 0 `completed` with only
@@ -196,7 +196,7 @@ $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 7 task.completed \
196
196
  duration_ms=$TOTAL_DURATION
197
197
  ```
198
198
 
199
- **Operational reporting.** On by default since v16.8.0; `enabled: false` or `optOut: true` silences it. Sends command, duration, tokens, outcome - never a repo name or path. One POST per run, fire-and-forget, coarse run metadata only - no prompts, code, diffs, or absolute paths. The script no-ops when `usageLog.enabled` is not true or no token resolves, so the call is unconditional and never blocks the run.
199
+ **Operational reporting.** On by default since v16.8.0; `enabled: false` or `optOut: true` silences it. Coarse run metadata only, never prompts, code, diffs, repo names or paths. Fire-and-forget: it no-ops without a token and never blocks the run, so the call is unconditional. Phase 0 already reported this run as started; this upserts the final state over it.
200
200
 
201
201
  ```bash
202
202
  node $HOME/.claude/scripts/usage-report.mjs --state "$STATE_FILE" >/dev/null 2>&1 || true
@@ -26,6 +26,44 @@ Short: 0-Init -> (1,2 skipped) --------------> 3-Dev -> 4-Review -> 5-Test -
26
26
  Full or Short is the Phase 0 Step 7.5 question, not a command name. Autopilot never asks and always runs Full.
27
27
  ```
28
28
 
29
+ ## Phase entry - pending steer (every phase, every mode)
30
+
31
+ Before a phase does any work, read `pendingSteer` from `agent-state.json`. It
32
+ is how a correction reaches a run that is already going: `/multi-agent:steer #N
33
+ "<instruction>"` queues one, and the next phase to start consumes it.
34
+
35
+ ```bash
36
+ STEER=$(node -e 'try{const s=require(process.argv[1]);const p=s.pendingSteer;if(p&&p.text&&!p.appliedAt)process.stdout.write(p.text)}catch{}' "$STATE_FILE")
37
+ ```
38
+
39
+ The `catch` is not decoration: a state file that does not exist yet (Phase 0
40
+ before its first write) makes `require` throw, and an unguarded read prints a
41
+ stack trace at every phase entry that reads like a failed phase.
42
+
43
+ Empty → carry on, nothing to do. Non-empty:
44
+
45
+ 1. Apply it to this phase's context before starting. It is the user's own
46
+ words; treat it as an instruction from them, arriving now.
47
+ 2. Say what changed, in one line, so the correction is visible in the run and
48
+ not only in the state file.
49
+ 3. Mark it consumed. The record stays as a trace; `appliedAt` is what stops it
50
+ being read again, and the next steer overwrites all four keys:
51
+ ```bash
52
+ printf '{"pendingSteer":{"appliedAt":"%s","appliedPhase":<N>}}' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
53
+ | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE"
54
+ ```
55
+
56
+ `<N>` is this phase's number, substituted literally and unquoted - it is a
57
+ JSON number. Leaving it as an unset shell variable produces
58
+ `"appliedPhase":}`, which is invalid JSON and exits 1.
59
+ 4. Record it on the tracker and in the log: `bash $HOME/.claude/scripts/phase-tracker.sh meta <N> steer "applied"`, plus a `Steer applied: <text>` line in `agent-log.md`.
60
+
61
+ Two limits, both deliberate. It is read at phase **entry** only, never mid-phase:
62
+ a phase that changes target halfway discards the work it already did, which is
63
+ the outcome steer exists to avoid. And an instruction that contradicts an
64
+ approved plan halts for the user rather than silently rewriting the plan -
65
+ steering corrects a run, it does not re-authorize one.
66
+
29
67
  ## Visual Phase Tracker
30
68
 
31
69
  Two channels run in parallel at every phase boundary. Both are required in their target CLIs - skipping either is the #1 source of "I see no progress" complaints.
@@ -48,12 +48,7 @@
48
48
  "autoDiff": false,
49
49
  "manualNote": false
50
50
  },
51
- "wikiScope": [
52
- "main",
53
- "ios",
54
- "screenshots",
55
- "index"
56
- ],
51
+ "wikiScope": ["main", "ios", "screenshots", "index"],
57
52
  "autopilotReportTimeoutSeconds": 1800,
58
53
  "promptLanguage": "en",
59
54
  "outputLanguage": "en",
@@ -185,10 +180,7 @@
185
180
  "localPath": "plugins/<my-plugin>/skills/workflow",
186
181
  "upstreamMarketplace": "<installed-marketplace-name>",
187
182
  "upstreamPlugin": "<upstream-plugin-name>",
188
- "upstreamSkills": [
189
- "<skill-a>",
190
- "<skill-b>"
191
- ],
183
+ "upstreamSkills": ["<skill-a>", "<skill-b>"],
192
184
  "derivedFromVersion": "0.0.0",
193
185
  "upstreamVersionSource": "marketplace.json",
194
186
  "upstreamLocalClone": "$HOME/<upstream-repo-working-copy>",
@@ -21,6 +21,6 @@ available. Read the effective `enabledPlugins` and load each enabled toolkit's
21
21
  `ai-common-toolkit` and `ai-analyst-toolkit` are on everywhere. Nothing enabled
22
22
  is a normal state.
23
23
 
24
- **multi-agent-toolkit MCP.** 83 tools for a running app: `ui-inspect`,
24
+ **multi-agent-toolkit MCP.** 80+ tools for a running app: `ui-inspect`,
25
25
  `crash-logs`, `design-check`, `ios-app-store-audit`, `ios-testflight`. Use them
26
26
  instead of guessing about on-screen state. Not registered is a silent no-op.
@@ -104,6 +104,41 @@
104
104
  "type": ["string", "null"],
105
105
  "description": "Set when a phase halts on a hard error (validator failed twice, no subagent returned, dispatch error past fallback, lock irrecoverable). Format '<phase>:<cause>'. Surfaced to the user and cleared on successful resume. See operations.md 'Halt visibility'."
106
106
  },
107
+ "rev": {
108
+ "type": "integer",
109
+ "minimum": 0,
110
+ "description": "Monotonic revision, bumped by write-state.mjs on every successful write. A reader keeps the rev it read and compares before writing back; a changed rev means the record moved underneath it. Optional, so a record written by an earlier version stays valid: treat a missing rev as 0. No schemaVersion bump - the field is additive and nothing needs migrating."
111
+ },
112
+ "pendingSteer": {
113
+ "type": ["object", "null"],
114
+ "additionalProperties": false,
115
+ "required": ["text", "at"],
116
+ "description": "A mid-run instruction from the user, waiting for the next phase boundary to consume it. Written by /multi-agent:steer; a phase consumes it at entry by setting appliedAt, which is what stops it being read again. The record is kept as a trace and overwritten wholesale by the next steer. Never applied mid-phase: changing the target of work already in flight throws that work away.",
117
+ "properties": {
118
+ "text": {
119
+ "type": "string",
120
+ "minLength": 1,
121
+ "maxLength": 4000,
122
+ "description": "The instruction, verbatim as the user typed it."
123
+ },
124
+ "at": {
125
+ "type": "string",
126
+ "format": "date-time",
127
+ "description": "When it was queued."
128
+ },
129
+ "appliedAt": {
130
+ "type": ["string", "null"],
131
+ "format": "date-time",
132
+ "description": "When a phase consumed it. Set alongside clearing the record, so the log keeps the trace."
133
+ },
134
+ "appliedPhase": {
135
+ "type": ["integer", "null"],
136
+ "minimum": 0,
137
+ "maximum": 7,
138
+ "description": "The phase that consumed it."
139
+ }
140
+ }
141
+ },
107
142
  "telemetry": {
108
143
  "type": "object",
109
144
  "additionalProperties": true,
@@ -104,7 +104,10 @@
104
104
  "platforms": {
105
105
  "type": "array",
106
106
  "minItems": 1,
107
- "items": { "type": "string", "enum": ["ios", "android", "web", "backend", "mobile", "frontend"] },
107
+ "items": {
108
+ "type": "string",
109
+ "enum": ["ios", "android", "web", "backend", "mobile", "frontend"]
110
+ },
108
111
  "description": "Platforms selected in Phase 0 step 3 (multi-select)."
109
112
  },
110
113
  "repos": {
@@ -114,7 +117,10 @@
114
117
  "additionalProperties": false,
115
118
  "required": ["platform", "name", "canPush"],
116
119
  "properties": {
117
- "platform": { "type": "string", "enum": ["ios", "android", "web", "backend", "mobile", "frontend"] },
120
+ "platform": {
121
+ "type": "string",
122
+ "enum": ["ios", "android", "web", "backend", "mobile", "frontend"]
123
+ },
118
124
  "name": { "type": "string", "minLength": 1 },
119
125
  "cloneUrl": { "type": ["string", "null"] },
120
126
  "canPush": { "type": "boolean" }
@@ -66,13 +66,7 @@
66
66
  "conventionBucket": {
67
67
  "type": "object",
68
68
  "additionalProperties": false,
69
- "required": [
70
- "pattern",
71
- "example",
72
- "confidence",
73
- "evidenceFiles",
74
- "alternativeCandidates"
75
- ],
69
+ "required": ["pattern", "example", "confidence", "evidenceFiles", "alternativeCandidates"],
76
70
  "properties": {
77
71
  "pattern": {
78
72
  "type": "string",
@@ -84,12 +78,7 @@
84
78
  },
85
79
  "confidence": {
86
80
  "type": "string",
87
- "enum": [
88
- "high",
89
- "medium",
90
- "low",
91
- "none"
92
- ],
81
+ "enum": ["high", "medium", "low", "none"],
93
82
  "description": "high: 5+ examples; medium: 3-4; low: 1-2 or mixed; none: no evidence."
94
83
  },
95
84
  "evidenceFiles": {
@@ -34,7 +34,7 @@ const NONE = "-";
34
34
 
35
35
  function argOf(argv, flag) {
36
36
  const i = argv.indexOf(flag);
37
- return i === -1 ? null : argv[i + 1] ?? null;
37
+ return i === -1 ? null : (argv[i + 1] ?? null);
38
38
  }
39
39
 
40
40
  function readState(path) {
@@ -102,15 +102,27 @@ function rowsFrom(spec, lang) {
102
102
  c.url,
103
103
  ref || NONE,
104
104
  c.embeddedApiTable
105
- ? lang === "tr" ? "API kontratı" : "API contract"
106
- : lang === "tr" ? "özellik spesifikasyonu" : "feature spec",
105
+ ? lang === "tr"
106
+ ? "API kontratı"
107
+ : "API contract"
108
+ : lang === "tr"
109
+ ? "özellik spesifikasyonu"
110
+ : "feature spec",
107
111
  OK[lang],
108
112
  NONE,
109
113
  );
110
114
  }
111
115
 
112
116
  for (const j of ev.jira ?? []) {
113
- push("Jira", j.id, j.url ?? NONE, NONE, lang === "tr" ? "ticket" : "ticket", OK[lang], j.summary);
117
+ push(
118
+ "Jira",
119
+ j.id,
120
+ j.url ?? NONE,
121
+ NONE,
122
+ lang === "tr" ? "ticket" : "ticket",
123
+ OK[lang],
124
+ j.summary,
125
+ );
114
126
  }
115
127
 
116
128
  for (const s of ev.swagger ?? []) {
@@ -266,13 +278,17 @@ function tableLocators(block) {
266
278
  const found = [];
267
279
  for (const line of block.split(/\r?\n/)) {
268
280
  if (!line.trim().startsWith("|")) continue;
269
- const cells = line.split("|").slice(1, -1).map((c) => c.trim());
281
+ const cells = line
282
+ .split("|")
283
+ .slice(1, -1)
284
+ .map((c) => c.trim());
270
285
  if (cells.length < 3) continue;
271
286
  if (/^:?-{3,}:?$/.test(cells[0])) continue;
272
287
  const locator = cells[2];
273
288
  const source = cells[1];
274
289
  if (!locator) continue;
275
- if (HEADER_CELLS.has(locator.toLowerCase()) && HEADER_CELLS.has(cells[0].toLowerCase())) continue;
290
+ if (HEADER_CELLS.has(locator.toLowerCase()) && HEADER_CELLS.has(cells[0].toLowerCase()))
291
+ continue;
276
292
  found.push({ locator, source, raw: line });
277
293
  }
278
294
  return found;
@@ -3,19 +3,9 @@
3
3
  "stack": "android",
4
4
  "description": "Kotlin + Java code-graph rules. Adds what the graph needs on top of test-gap-rules/android.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants internal types too, because Phase 1 narrows scope on the whole module, not just its public surface.",
5
5
  "comments": {
6
- "line": [
7
- "//"
8
- ],
9
- "block": [
10
- [
11
- "/*",
12
- "*/"
13
- ]
14
- ],
15
- "string": [
16
- "\"\"\"",
17
- "\""
18
- ]
6
+ "line": ["//"],
7
+ "block": [["/*", "*/"]],
8
+ "string": ["\"\"\"", "\""]
19
9
  },
20
10
  "definitionPatterns": [
21
11
  {
@@ -60,14 +50,7 @@
60
50
  "regex": "^\\s*import\\s+(?:static\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*\\.)+([A-Z][A-Za-z0-9_]*)"
61
51
  }
62
52
  ],
63
- "referenceKinds": [
64
- "class",
65
- "interface",
66
- "object",
67
- "enum",
68
- "record",
69
- "typealias"
70
- ],
53
+ "referenceKinds": ["class", "interface", "object", "enum", "record", "typealias"],
71
54
  "referenceKindsNote": "fun is deliberately absent, for the reason Swift's rules record for func: a bare lowercase name matched across files is almost never a call to that exact declaration, and on a large app the generic ones (invoke, build, create, get) take the top god-node slots purely because each happened to be declared once. Composables are the tempting exception - they are capitalised and behave like types - but they are declared with fun, so including them means including every other fun with them.",
72
55
  "ignoredIdentifiers": [
73
56
  "this",
@@ -3,20 +3,9 @@
3
3
  "stack": "go",
4
4
  "description": "Go code-graph rules. Adds what the graph needs on top of test-gap-rules/go.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants unexported declarations too, because Phase 1 narrows scope on the whole package, not just its exported surface.",
5
5
  "comments": {
6
- "line": [
7
- "//"
8
- ],
9
- "block": [
10
- [
11
- "/*",
12
- "*/"
13
- ]
14
- ],
15
- "string": [
16
- "`",
17
- "\"",
18
- "'"
19
- ]
6
+ "line": ["//"],
7
+ "block": [["/*", "*/"]],
8
+ "string": ["`", "\"", "'"]
20
9
  },
21
10
  "importSpecifiersAreStrings": true,
22
11
  "definitionPatterns": [
@@ -47,11 +36,7 @@
47
36
  "regex": "^[ \\t]*(?:import[ \\t]+)?(?:[A-Za-z_.][A-Za-z0-9_]*[ \\t]+)?\"(?:[^\"]*/)?([A-Za-z_][A-Za-z0-9_.-]*)\"[ \\t]*$"
48
37
  }
49
38
  ],
50
- "referenceKinds": [
51
- "struct",
52
- "interface",
53
- "type"
54
- ],
39
+ "referenceKinds": ["struct", "interface", "type"],
55
40
  "referenceKindsNote": "func is deliberately absent, for the reason the Swift, Kotlin, Node and Python rules all record: a bare name matched across files is almost never a call to that exact declaration, and Go's short-name convention (New, Run, Get, Close, Error) makes it worse than most. Functions still reach the graph through their defines edge, so they stay findable by name.",
56
41
  "ignoredIdentifiers": [
57
42
  "package",
@@ -3,20 +3,9 @@
3
3
  "stack": "node",
4
4
  "description": "TypeScript + JavaScript code-graph rules. Adds what the graph needs on top of test-gap-rules/node.json, which already owns sourceExtensions, excludePathGlobs and the test-path predicates. Definition patterns here are deliberately broader than the test-gap publicApiPatterns: the graph wants unexported declarations too, because Phase 1 narrows scope on the whole module, not just its public surface.",
5
5
  "comments": {
6
- "line": [
7
- "//"
8
- ],
9
- "block": [
10
- [
11
- "/*",
12
- "*/"
13
- ]
14
- ],
15
- "string": [
16
- "`",
17
- "\"",
18
- "'"
19
- ]
6
+ "line": ["//"],
7
+ "block": [["/*", "*/"]],
8
+ "string": ["`", "\"", "'"]
20
9
  },
21
10
  "definitionPatterns": [
22
11
  {
@@ -56,13 +45,7 @@
56
45
  "regex": "(?:\\bfrom|\\brequire\\s*\\(|\\bimport)\\s*\\(?\\s*[\"'](?:[^\"']*[\\/:])?([A-Za-z_$][A-Za-z0-9_$-]*)(?:\\.[A-Za-z0-9]+)*[\"']"
57
46
  }
58
47
  ],
59
- "referenceKinds": [
60
- "class",
61
- "interface",
62
- "type",
63
- "enum",
64
- "const"
65
- ],
48
+ "referenceKinds": ["class", "interface", "type", "enum", "const"],
66
49
  "referenceKindsNote": "function is deliberately absent, for the reason Swift's rules record for func: a bare name matched across files is almost never a call to that exact declaration, and the generic ones (run, main, get, parse, handler) would take the top god-node slots purely because each happened to be declared once. Functions still reach the graph through their defines edge, so they stay findable by name. const is present but narrowed to an uppercase-initial declaration at column 0, which is what a component, a singleton or an exported table looks like; a lowercase const is a local and is not a declaration anyone references across files. Measured on this repo (126 sources): including function put `ok` at degree 52, `f` at 38, `tokens` at 15 and `arg` at 12 - local helper names that happen to be declared exactly once, so the ambiguity rule does not catch them either. Excluding it leaves a thin symbol layer, and that is the honest shape of this stack: a JavaScript module's exported unit is usually a function, so the useful graph here is the import graph between files. `graph-affected \"<file>.mjs\"` answers who imports a module, which is the question this stack actually gets asked.",
67
50
  "ignoredIdentifiers": [
68
51
  "this",
@@ -115,7 +115,9 @@ function endpointAllowed(endpoint) {
115
115
  async function main() {
116
116
  const text = String(arg("--text", "") ?? "").trim();
117
117
  if (!text) {
118
- process.stderr.write('usage: feedback-send.mjs --text "<message>" [--kind bug|idea|question]\n');
118
+ process.stderr.write(
119
+ 'usage: feedback-send.mjs --text "<message>" [--kind bug|idea|question]\n',
120
+ );
119
121
  process.exit(2);
120
122
  }
121
123
  if (text.length > TEXT_MAX) {