@mmerterden/multi-agent-pipeline 19.1.3 → 19.1.4

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 (40) hide show
  1. package/CHANGELOG.md +56 -7
  2. package/README.md +42 -15
  3. package/README.tr.md +38 -12
  4. package/docs/ecosystem.md +3 -4
  5. package/docs/facts.json +3 -3
  6. package/install/_common.mjs +5 -5
  7. package/install/_mcp-register.mjs +1 -1
  8. package/install/_plugin-skills.mjs +3 -4
  9. package/install/copilot.mjs +5 -5
  10. package/manifest.json +42 -42
  11. package/package.json +1 -1
  12. package/pipeline/commands/multi-agent/setup/SKILL.md +4 -4
  13. package/pipeline/commands/sim-test.md +4 -4
  14. package/pipeline/multi-agent-refs/analysis/locked.md +2 -2
  15. package/pipeline/multi-agent-refs/channels/jira.md +8 -8
  16. package/pipeline/multi-agent-refs/features/base-branch-evidence.md +2 -2
  17. package/pipeline/multi-agent-refs/features/scope-check.md +1 -1
  18. package/pipeline/multi-agent-refs/features/stack-skill-routing.md +1 -1
  19. package/pipeline/multi-agent-refs/phases/operations.md +1 -1
  20. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +3 -3
  21. package/pipeline/multi-agent-refs/rules.md +1 -1
  22. package/pipeline/scripts/autopilot-runner.mjs +6 -7
  23. package/pipeline/scripts/build-references.mjs +3 -3
  24. package/pipeline/scripts/build-stack-plugins.mjs +1 -1
  25. package/pipeline/scripts/bulk-read.sh +6 -4
  26. package/pipeline/scripts/doctor.mjs +4 -4
  27. package/pipeline/scripts/learnings-ledger.mjs +1 -1
  28. package/pipeline/scripts/match-skills.mjs +4 -4
  29. package/pipeline/scripts/memory-load.sh +3 -3
  30. package/pipeline/scripts/plan-coverage-gate.mjs +3 -3
  31. package/pipeline/scripts/run-aggregator.mjs +2 -2
  32. package/pipeline/scripts/runs-index.mjs +7 -7
  33. package/pipeline/scripts/scope-check-gate.mjs +1 -1
  34. package/pipeline/scripts/smoke-schema-validation.sh +9 -12
  35. package/pipeline/scripts/usage-report.mjs +5 -5
  36. package/pipeline/scripts/validate-analysis-doc.mjs +3 -3
  37. package/pipeline/scripts/write-state.mjs +22 -11
  38. package/pipeline/skills/.skill-manifest.json +3 -3
  39. package/pipeline/skills/shared/core/multi-agent/SKILL.md +2 -2
  40. package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +4 -5
@@ -11,9 +11,9 @@
11
11
  # scripts/_retrieval.mjs) and the most relevant are printed. Without it the
12
12
  # index order is kept, which is what every existing caller gets.
13
13
  #
14
- # The cap used to be a bare `head -30`. That is a truncation, not a summary: the
15
- # thirty-first pointer was invisible however precisely it matched the task, and
16
- # the block got less useful the longer a repo was worked on - the opposite of
14
+ # The cap is not a bare `head -30`. That is a truncation, not a summary: the
15
+ # thirty-first pointer stays invisible however precisely it matches the task, and
16
+ # the block gets less useful the longer a repo is worked on - the opposite of
17
17
  # what accumulated memory is for.
18
18
  #
19
19
  # Honors `prefs.global.perRepoMemory`. When the pref is off OR the prefs file
@@ -77,9 +77,9 @@ export function filesToAdd(markdown) {
77
77
  if (!path || /^-+$/.test(path)) continue;
78
78
  if (path.includes("<") || tag.includes("<")) continue;
79
79
  // No header-row filter: the tag column of the header reads "Etiket / Tag",
80
- // which the `Add new` test below already rejects. The filter that used to be
81
- // here matched on the PATH column instead and would have skipped a real row
82
- // whose path begins with a `File/` directory.
80
+ // which the `Add new` test below already rejects. A filter here would have to
81
+ // match on the PATH column instead, and would skip a real row whose path
82
+ // begins with a `File/` directory.
83
83
  if (!/^add new$/i.test(tag)) continue;
84
84
  rows.push(path);
85
85
  }
@@ -87,8 +87,8 @@ function die(msg) {
87
87
  // Path resolution lives in _run-paths.mjs: tracker-state.json is written to
88
88
  // the flat <root>/<task-id>/ while agent-state.json documents the nested
89
89
  // <root>/<project>/<task-id>/, and both layouts are populated in practice.
90
- // This used to be a hand-rolled candidate list here, a second one in
91
- // render-cost-summary.sh and a third in render-agent-log-cost.sh.
90
+ // Resolved once here rather than as a hand-rolled candidate list in each of
91
+ // render-cost-summary.sh and render-agent-log-cost.sh as well.
92
92
  function resolveTrackerCandidates() {
93
93
  const candidates = [];
94
94
  if (flags["task-dir"]) {
@@ -44,9 +44,9 @@ import { invokedDirectly } from "../lib/invoked-directly.mjs";
44
44
 
45
45
  /**
46
46
  * The phase from which a run counts as "waiting on you", read from the phase
47
- * contract rather than written here. This threshold moved once already (it was
48
- * 6 under the eight-phase contract, it is 4 under six) and nothing connected it
49
- * to the renumbering, so it would have silently regrouped every run.
47
+ * contract rather than written here. The threshold follows the phase count (6
48
+ * under an eight-phase contract, 4 under six), so a literal would survive a
49
+ * renumbering unchanged and silently regroup every run.
50
50
  */
51
51
  const PHASE_WAITING_FROM = JSON.parse(
52
52
  readFileSync(new URL("../schemas/phases.json", import.meta.url), "utf8"),
@@ -203,10 +203,10 @@ export function buildIndex() {
203
203
  salvaged: Boolean(statePath && statePath.includes(`${run.dir}/artifacts/`)),
204
204
  // What KIND of record this is, before asking whether it is healthy.
205
205
  //
206
- // 74 of the 103 runs on this machine have a tracker file and no agent
207
- // state, and the first version of this index called every one of them
208
- // `stateReadable: false` - so a panel built on it announced "74 runs
209
- // could not be read" about records that are not damaged and never had
206
+ // A run with a tracker file and no agent state is the common case, not
207
+ // damage: most runs on a machine are shaped that way. Calling them
208
+ // `stateReadable: false` makes a panel announce "N runs could not be
209
+ // read" about records that are intact and never had
210
210
  // agent state to begin with. `/multi-agent:analysis` says so in its own
211
211
  // description ("no worktree, no commits, no dev chain"), and design-check
212
212
  // is the same shape.
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Phase 3 writes `.pipeline/scope-check.json` before the handoff: one reason
6
6
  * per touched file, the things it deliberately did not do, and the simplifier
7
- * rationales that used to be discarded. This gate compares that record with
7
+ * rationales that are otherwise discarded. This gate compares that record with
8
8
  * the real diff so an unjustified file cannot ride into review unnoticed.
9
9
  *
10
10
  * Usage:
@@ -26,11 +26,10 @@ LIVE_PREFS="$HOME/.claude/multi-agent-preferences.json"
26
26
 
27
27
  # The migration target, derived the way migrate-prefs.mjs derives it: the last
28
28
  # entry of the schema's schemaVersion enum, read from the schema that sits
29
- # beside the migrator being checked. It used to be grepped as a
30
- # `TARGET_VERSION = "x.y.z"` literal out of the migrator, and when that literal
31
- # was replaced by the schema read - precisely because a literal had drifted a
32
- # minor behind - the grep started matching nothing and both checks that depend
33
- # on it reported "no reference point". A gate that reads the source of truth
29
+ # beside the migrator being checked. Not grepped as a `TARGET_VERSION = "x.y.z"`
30
+ # literal out of the migrator: a literal drifts, and the moment it is replaced by
31
+ # a schema read the grep matches nothing and both checks that depend on it report
32
+ # "no reference point". A gate that reads the source of truth
34
33
  # cannot go stale against it; a gate that reads a transcription of it can.
35
34
  migration_target() {
36
35
  local schema="$1/../schemas/prefs.schema.json"
@@ -213,15 +212,13 @@ if [ -f "$LIVE_PREFS" ]; then
213
212
 
214
213
  # The target is read from migrate-prefs.mjs, never hardcoded here.
215
214
  #
216
- # This block used to say `if [ "$LVER" = "2.1.0" ]; then pass "already v2.1.0"`
217
- # with everything else falling through to `fail "unknown schemaVersion"`. The
218
- # migration target had since moved to 2.4.0, which inverted the gate: a prefs
219
- # file left behind at 2.1.0 reported PASS as "already current", while a file
220
- # correctly migrated to 2.4.0 hit the else arm and FAILED as "unknown". The
221
- # gate was rejecting the only fully-migrated state it exists to encourage.
215
+ # A hardcoded version here inverts the gate the moment the migration target
216
+ # moves: the stale version passes as "already current" and the correctly
217
+ # migrated one falls through to "unknown schemaVersion", so the gate rejects
218
+ # the only fully-migrated state it exists to encourage.
222
219
  #
223
220
  # Reading the target and the accepted set from the same schema enum the
224
- # migrator reads means this can never disagree with it again.
221
+ # migrator reads means this cannot disagree with it.
225
222
  MIGRATOR="$SMOKE_DIR/migrate-prefs.mjs"
226
223
  TARGET=$(migration_target "$(dirname "$MIGRATOR")")
227
224
  KNOWN=$(node -e "
@@ -612,11 +612,11 @@ if (invokedDirectly) {
612
612
  // Exit 0 stays: usage telemetry must never be the reason a run fails, and a
613
613
  // non-zero exit here would propagate into whatever called it.
614
614
  //
615
- // But the silence goes. `catch(() => process.exit(0))` reported success for
616
- // every failure, so a telemetry path that had been broken for weeks looked
617
- // exactly like one that worked - there was no surface on which anyone could
618
- // notice. One line on stderr costs nothing and is the difference between a
619
- // degraded feature and an invisible one.
615
+ // But the silence goes. A bare `catch(() => process.exit(0))` reports success
616
+ // for every failure, so a broken telemetry path looks exactly like one that
617
+ // works and there is no surface on which to notice it. One line on stderr
618
+ // costs nothing and is the difference between a degraded feature and an
619
+ // invisible one.
620
620
  main().catch((err) => {
621
621
  process.stderr.write(`usage-report: not sent - ${err?.message ?? err}\n`);
622
622
  process.exit(0);
@@ -935,10 +935,10 @@ function main() {
935
935
 
936
936
  mark("placeholder-bodies", profile === "global" ? undefined : "corporate profile");
937
937
 
938
- // 4b. Locked 2's numbering half, which was ungated until v19.0.0.
938
+ // 4b. Locked 2's numbering half.
939
939
  //
940
- // It is NOT "numbering re-flows 1..N". That is what the rule used to say and
941
- // it could not hold: Locked 30 threads ids across the document by section
940
+ // It is NOT "numbering re-flows 1..N", which could not hold: Locked 30
941
+ // threads ids across the document by section
942
942
  // NUMBER ("Section 15.1 scenario", "Section 4.4", "Section 3/5 layout cells"),
943
943
  // so a re-flowed document sends every one of those references to the wrong
944
944
  // section. No emitted document ever re-flowed. The rule was the wrong half.
@@ -40,6 +40,9 @@
40
40
  * WRITE_STATE_LOCK_ABANDON_MS ceiling past which even a lock that probes as
41
41
  * alive is reclaimed, covering PID reuse
42
42
  * (default: 10x STALE_MS)
43
+ * WRITE_STATE_TEST_DELAY_MS test seam: pause between acquiring the lock
44
+ * and the write, so the exit-4 refusal can be
45
+ * driven on purpose (default 0, off)
43
46
  * A dead holder (PID no longer alive) is reclaimed immediately regardless of
44
47
  * age. A live holder keeps its lock: liveness outranks age. See
45
48
  * lockIdentityIfStale() for why that order is load-bearing.
@@ -109,11 +112,11 @@ async function acquireLock(
109
112
  const staleMs = Number(process.env.WRITE_STATE_LOCK_STALE_MS) || 30_000;
110
113
  const deadline = Date.now() + timeoutMs;
111
114
  // The lock file must never be observable without its owning PID inside it.
112
- // It used to be created with openSync(wx) and filled on the next line, so
113
- // between those two calls it existed and was empty. A writer arriving in
114
- // that window read an unparseable PID, concluded the lock was stale, and
115
- // deleted a live one - after which two writers held it and one write was
116
- // lost. Under CPU load that reproduced 3 times in 15 runs.
115
+ // Created with openSync(wx) and filled on the next line, it exists and is
116
+ // empty between those two calls. A writer arriving in that window reads an
117
+ // unparseable PID, concludes the lock is stale, and deletes a live one -
118
+ // after which two writers hold it and one write is lost. The window is only
119
+ // wide enough to hit under CPU load, which is where it matters.
117
120
  //
118
121
  // Writing the PID into a private temp file and hard-linking it into place
119
122
  // closes the window: link() fails with EEXIST if the lock is held, and the
@@ -141,8 +144,8 @@ async function acquireLock(
141
144
  // deleting it, the holder can release and a THIRD writer can acquire a
142
145
  // fresh one - and an unlink by path deletes that live lock, after which
143
146
  // two writers hold it and one update is lost. Same failure the PID window
144
- // above once caused, at a different point in the loop; it survived because
145
- // it only reproduces under load, where the gap is wide enough to lose.
147
+ // above describes, at a different point in the loop, and it only reproduces
148
+ // under load, where the gap is wide enough to lose.
146
149
  //
147
150
  // The inode is the identity: a newly acquired lock is a different file
148
151
  // even at the same path. Delete only if the file we judged is still the
@@ -176,10 +179,10 @@ async function acquireLock(
176
179
  * A lock is stale when its owning PID is no longer alive. Age alone is not
177
180
  * staleness.
178
181
  *
179
- * The order here is the whole point. Age used to be checked first and returned
180
- * "stale" on its own, so a holder that was demonstrably ALIVE lost its lock the
181
- * moment the file passed 30 seconds - and the next writer then deleted a live
182
- * lock and overwrote the update behind it. Liveness is direct evidence;
182
+ * The order here is the whole point. Checking age first and returning "stale" on
183
+ * its own costs a holder that is demonstrably ALIVE its lock the moment the file
184
+ * passes 30 seconds - and the next writer then deletes a live lock and overwrites
185
+ * the update behind it. Liveness is direct evidence;
183
186
  * a timestamp is a guess about it, and a guess must not overrule the evidence.
184
187
  *
185
188
  * Age survives for the one case liveness cannot answer: PID reuse. A dead
@@ -330,6 +333,14 @@ async function main() {
330
333
  try {
331
334
  heldLockIno = await acquireLock(path);
332
335
  heldLockFor = path;
336
+ // Test seam. The refusal below is the one path that cannot be reached on
337
+ // demand: it needs another writer to take the lock inside the window
338
+ // between acquiring it and renaming, which on an idle machine is too
339
+ // narrow to hit and on a loaded one is a coin flip. A gate that can only
340
+ // observe this path by accident is not holding it. Unset in every real
341
+ // run; the whole seam is one sleep.
342
+ const testDelayMs = Number(process.env.WRITE_STATE_TEST_DELAY_MS) || 0;
343
+ if (testDelayMs > 0) await new Promise((r) => setTimeout(r, testDelayMs));
333
344
  } catch (err) {
334
345
  if (err.code === "LOCK_TIMEOUT") {
335
346
  console.error(err.message);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-09-21T06:17:16Z",
3
+ "generatedAt": "2026-09-21T08:28:08Z",
4
4
  "skillCount": 216,
5
5
  "entries": [
6
6
  {
@@ -45,7 +45,7 @@
45
45
  },
46
46
  {
47
47
  "path": "shared/core/multi-agent-channels/SKILL.md",
48
- "sha256": "414eead38b461c8aa95f6af3175366be905338233e6872313f5d4f59a02351af"
48
+ "sha256": "e4f59e784d77018326e719632ef371e50e9fb9af61318a7d5bf713c386c0fd3f"
49
49
  },
50
50
  {
51
51
  "path": "shared/core/multi-agent-complaint-analysis/SKILL.md",
@@ -253,7 +253,7 @@
253
253
  },
254
254
  {
255
255
  "path": "shared/core/multi-agent/SKILL.md",
256
- "sha256": "9ab7af2ffb01f8eb0fae624ae40ea9ff2f6d69ad61544bd266b1b97f60eb4a0f"
256
+ "sha256": "7ea676d07cd0104cc929b825463c521aff66047c6dcdfc28e9e37ca3752107da"
257
257
  },
258
258
  {
259
259
  "path": "shared/external/accessibility-compliance-accessibility-audit/SKILL.md",
@@ -708,5 +708,5 @@ When `multi-agent status` is called, show:
708
708
  ## Help Display
709
709
 
710
710
  Rendered by the `multi-agent-help` skill (`/multi-agent:help`), which owns
711
- the full text in both languages. It used to be duplicated here in English
712
- only, on a path this skill takes for exactly one input.
711
+ the full text in both languages. Duplicating it here would mean English only,
712
+ on a path this skill takes for exactly one input.
@@ -152,11 +152,10 @@ h2. Bağlantılar
152
152
  Reproduction scenarios first, then regressions for whatever the change could
153
153
  have disturbed. Full contract: `channels/jira.md`.
154
154
 
155
- `{{file.ext → func}}`, `*Root cause:*` and `*Changed files:*` used to be this
156
- template. They are gone deliberately: that shape put a hash-table mutation
157
- walk in front of a tester, and `{{file → func}}` is exactly where a Swift
158
- selector like `hash(into:)` ends up - whose trailing `:)` Jira renders as a
159
- smiley.
155
+ `{{file.ext → func}}`, `*Root cause:*` and `*Changed files:*` are deliberately
156
+ NOT this template. That shape puts a hash-table mutation walk in front of a
157
+ tester, and `{{file → func}}` is exactly where a Swift selector like
158
+ `hash(into:)` ends up - whose trailing `:)` Jira renders as a smiley.
160
159
 
161
160
  **Post through the publisher, never a hand-rolled `curl`:**
162
161