@mmerterden/multi-agent-pipeline 16.27.0 → 16.28.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 (36) hide show
  1. package/CHANGELOG.md +71 -1
  2. package/package.json +4 -4
  3. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  4. package/pipeline/commands/multi-agent/issue/SKILL.md +1 -0
  5. package/pipeline/commands/multi-agent/jira/SKILL.md +3 -0
  6. package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
  7. package/pipeline/commands/multi-agent/review-issue/SKILL.md +1 -0
  8. package/pipeline/lib/issue-fetcher.sh +134 -5
  9. package/pipeline/lib/multi-repo-pipeline.sh +8 -0
  10. package/pipeline/multi-agent-refs/analysis/evidence.md +1 -1
  11. package/pipeline/multi-agent-refs/analysis/intake.md +11 -2
  12. package/pipeline/multi-agent-refs/analysis/locked.md +1 -0
  13. package/pipeline/multi-agent-refs/analysis/redesign.md +112 -0
  14. package/pipeline/multi-agent-refs/analysis/render.md +5 -0
  15. package/pipeline/multi-agent-refs/analysis/resolve.md +1 -0
  16. package/pipeline/multi-agent-refs/analysis/review.md +15 -0
  17. package/pipeline/multi-agent-refs/analysis/synthesis.md +1 -1
  18. package/pipeline/multi-agent-refs/analysis-template-corporate.md +3 -3
  19. package/pipeline/multi-agent-refs/analysis-template.md +36 -0
  20. package/pipeline/multi-agent-refs/cross-cli-contract.md +3 -0
  21. package/pipeline/multi-agent-refs/features/jira-context.md +101 -0
  22. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +1 -1
  23. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
  24. package/pipeline/multi-agent-refs/phases/phase-4-review.md +1 -1
  25. package/pipeline/multi-agent-refs/readiness-review.md +1 -1
  26. package/pipeline/schemas/agent-state.schema.json +19 -0
  27. package/pipeline/schemas/prefs.schema.json +26 -0
  28. package/pipeline/scripts/anonymize-findings.mjs +24 -0
  29. package/pipeline/scripts/build-references.mjs +10 -6
  30. package/pipeline/scripts/council-view.mjs +144 -0
  31. package/pipeline/scripts/phase-tracker.sh +18 -1
  32. package/pipeline/scripts/skill-siblings.mjs +41 -8
  33. package/pipeline/scripts/validate-analysis-doc.mjs +371 -7
  34. package/pipeline/scripts/validate-analysis.mjs +7 -5
  35. package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +1 -0
  36. package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +1 -0
package/CHANGELOG.md CHANGED
@@ -14,7 +14,77 @@ Internal file-layout changes that don't affect the slash-command surface are sti
14
14
 
15
15
  ---
16
16
 
17
- ## [Unreleased]
17
+ ## [16.28.0] - 2026-09-10
18
+
19
+ ### Added
20
+
21
+ - **The analysis sub-task the pipeline never read.** A development sub-task is usually filed with no description of its own. The fetcher already read the parent when that happened; it never read the siblings, so a board that keeps its analysis in a separate sub-task handed the pipeline an empty task and nothing said so. `issue-fetcher.sh` now reads the parent's other sub-tasks into `descriptor.relatedIssues[]`.
22
+
23
+ One search request buys all of them with their descriptions, and only when the issue has a parent, because an issue with no parent has no siblings. That cost claim is measured by `smoke-jira-context.sh` rather than asserted: it counts the requests the fetcher actually issues. The issue's own `subtasks` field lists ITS children, and the parent's lists siblings without descriptions, so either of those shapes would have cost one GET per sibling.
24
+
25
+ The payload never touches the `\x1f`-delimited parse the rest of the descriptor travels through - it arrives as one JSON argv - so the eight-field parse, its trailing-newline strip and its unfetched fallback are byte-for-byte what they were. The parent key is re-validated against an anchored pattern before it reaches the query: it comes from the API, but it goes into a URL.
26
+
27
+ It reaches `agent-state.json`, unlike the parent feature it sits next to. `/multi-agent:resume` rebuilds context from durable artefacts and never from the conversation, so an intake-only enrichment would be gone by the first resume. The task's own description and maturity are still not persisted; that asymmetry is now written into the schema so the next reader does not mistake it for an oversight.
28
+
29
+ `description_empty_sibling_available` fires only when the issue's description AND the parent's are both empty AND a sibling has content, downgrading what is today a hard blocker. The narrowness is the point: `score` loses ten points per warning and the picker only continues silently at `score >= 90`, so a warning that fired whenever an issue merely HAD siblings would turn every parented intake into a question.
30
+
31
+ - **How many reviewers actually saw a finding.** Phase 4 anonymizes findings so the judge cannot mark its own homework, but the merge stayed a pure judgement call: `validate-triage.mjs` surfaced a repeated fingerprint as a warning and left it there. `anonymize-findings.mjs` now stamps `corroboration: {count, of}` - distinct reviewers per fingerprint - computed where the whole panel is already in one payload, so it costs nothing.
32
+
33
+ It is context, not a rule. `docs/adr/0001-three-model-triage.md` rejected majority voting because the same hallucination shows up in two of three same-family reviewers, and that decision stands: the triage hedge now covers corroboration in the same sentence it covers prior art. The reading that pays is the other end - a count of 1 is the finding only one reviewer saw.
34
+
35
+ - **`council-view.mjs`** renders what each reviewer found and what triage did with it. Everything it needs was already on the state and nothing displayed it; `run-metrics.mjs` reduced it to a ratio and the rows behind the ratio were invisible. De-anonymization is safe here because it runs after triage has ruled, and the gate pins that the map reaches no prompt. `/multi-agent:log` renders it; exit 2 means the run never reached Phase 4.
36
+
37
+
38
+ - **A validator summary that says which checks ran.** `validate-analysis-doc.mjs --report` prints every check by name with a verdict each: `ok`, a finding count, or `skipped: <reason>`. The gap it closes is specific - the traceability matrix only runs in the corporate profile, so on a global document it never executed and the output was byte-identical to "ran, found nothing". Attribution is positional and reconciles by construction, so a check added without its mark misnames a finding but can never lose one; the gate asserts the printed set equals the set the file defines. Default output is unchanged for gate callers.
39
+
40
+ - **Provenance in the reports a human reads.** `evidence_digest` and `base_commit` were already in the document front-matter, on the side only a machine reads. The analysis Phase 5 report, the `review-analysis` verdict and `--report` now carry them too: a timestamp cannot separate two reports made the same day, and the first question anyone asks of an older verdict is which version of the document it judged.
41
+
42
+ - **A verdict prints its empty classes.** `review-analysis` now names all six rubric classes and every deterministic gate whether or not they fired, with `none` where the count is zero. A verdict that lists only what it found cannot be told apart from one that never looked.
43
+
44
+ - **The uncertainty marker, in both profiles.** The two-way `AS-NN` pairing between the body and Section 20 ran only under `profile: corporate`, which made the global profile the one place an inline marker could point at nothing. Two documents already said otherwise - Locked 36 makes an unstamped gap fail the dispatch gate with no profile clause, and `analysis/render.md` routes an external-dependency gap to `AS-NN` for every run - so the check was the only side that disagreed. A document carrying no markers is unaffected: every loop is empty.
45
+
46
+ - **Every open question names an owner.** "A gap with an owner is a plan; a gap without one is a defect" lived in `analysis/review.md`, which is a model's judgement. It is now a validator error. The owner column is located from the table header rather than by index, because the two profiles order Section 20 differently and an index would silently read the wrong cell in one of them.
47
+
48
+ - **`status: draft | final`, the closure gate.** Nothing stopped a document with open questions from being published. Under `final` an unresolved `AS-NN` or an open Section 20 row is now an ERROR; under `draft` it is counted and nothing blocks. The key is optional and absent means `draft`, so no document written before it starts failing. `/multi-agent:analysis-resolve` flips it at the end of a run, with a confirmation - `final` is a publication claim, and that command is built on one question per decision.
49
+
50
+ - **`not-for:` frontmatter, and a warning that shrinks.** `lint-skills.mjs` reported nine trigger-vocabulary collisions every run and the list never moved: an author read it, had nothing to do about it, and stopped reading. A skill may now name the siblings it must not be chosen for, which the model sees on the routing surface itself and the lint consumes to drop the pair. Suppression is never silent - settled pairs are counted - and a name that resolves to no sibling on the same surface is an error, because a typo would otherwise leave the pair unanswered while the author believes it is handled. `review-issue`/`review-jira` and `issue`/`jira` are tagged; the list is five.
51
+
52
+ - `ANALYSIS_CEILING` 154500 -> 155000. The report shape is defined only in `analysis/render.md` and a front-matter key is documented only where the front-matter block is, so those two additions had to land inside the count; everything else in this pass stayed outside it. Both were compressed once before measuring, and 295 bytes of headroom against 213 still owed left no way to avoid the raise.
53
+
54
+ - **Redesign mode.** `options.redesign` is an intake opt-in that adds three artefacts to the document, all of them claims about v1 rather than plans for v2: current behaviour with `CB-<slug>-NN` ids and `repo/file:line` citations, the v1 to v2 endpoint mapping, and a difference list over a closed status vocabulary (`Moved / Partial / Missing / New / Out of scope`). Every `Missing` and `Partial` owes a Section 20 row by `AS-NN` - a behaviour v2 drops with no decision is the thing a redesign loses and production finds.
55
+
56
+ It is an option, not a third `mode` value. `mode` says how many sections, `redesign` says which content, and since `mode === "full"` branches in four places inside the validator, a `mode: redesign` would have switched off the traceability matrix, the Test Plan requirement and the rule-to-test cross-check in exactly the documents that need them most. A small screen can also have a lite redesign.
57
+
58
+ Eight validator checks, of which two are worth naming: a `CB-` id present in one table and absent from the other fails in **both** directions, and the three sections must exist before any row check is trusted, because row checks over a missing section pass over nothing and show the gate green. Evidence and certainty are derived by the renderer from `repoEvidence`, never graded by the writer, which is Locked 24 applied to a second table. `options.redesign` is an `evidence_digest` input: without that one line, a redesign started within a day of a normal run on the same feature would reuse that cache, skip Phase 1b and render an empty behaviour table that all eight checks then pass over.
59
+
60
+ The contract, its reasoning and the check table live in `analysis/redesign.md`, which `analysis/SKILL.md` does not name and which therefore loads only on a redesign run.
61
+
62
+ - `ANALYSIS_CEILING` 155000 -> 158500, the largest single raise it has taken. What could not be moved out is the argument: the section skeletons belong in `analysis-template.md`, where every other conditional section (15.6, 16.2) is already defined; Locked 37 belongs in `analysis/locked.md`, because a decision recorded only where it is implemented is not locked; and the intake question belongs in `analysis/intake.md`, because the option is chosen before anything knows the run is a redesign. 5.9 kB stayed outside the count. Everything was compressed twice before the number was picked.
63
+ ### Fixed
64
+
65
+ - `learn-from-transcripts.mjs` called `process.exit(0)` on the line after writing its `--json` result, so a large mining result was cut at the 64 KB pipe buffer - the exact defect the gate added in 16.27.0 exists to prevent, in the file that motivated it. The gate caught it; the two sites it named are now a `main()` with a return.
66
+
67
+ - `skill-siblings.mjs` resolved its trees from the script's own location with no fallback, so from an install (`~/.claude/scripts`) it looked for `~/pipeline/commands/multi-agent`, found nothing, and exited 1. `observations.mjs` caught that and wrote the note `"<target>: not a skill surface"` about files that are skill surfaces - a friction log silently keeping a false record. Resolution is now two-candidate, repo first then the install, and from an install the authored surfaces are reported as not applicable rather than absent: `--audit` says the authored side was not checked instead of borrowing the repo's verdict.
68
+
69
+ - The stdout-exit detector in `smoke-consumer-smoke-surface.sh` only matched a literal `process.stdout.write` on the line, so `console.log` and any one-line helper wrapping either (`emit()`) were invisible to it. It now resolves same-file helpers one level, skips declaration lines and stops the lookahead at a control-flow terminator. The three analysis validators are on the watch list and converted to `process.exitCode`.
70
+
71
+ - `smoke-install-layout.sh` fixture regenerated: `council-view.mjs` and `features/jira-context.md` moved the installed counts.
72
+
73
+ - **The phase widget could not render on a Claude Code build without the tile API.** `TaskCreate` and `TaskUpdate` are not in every build, and `phase-tracker.sh` cannot see the model's tool list, so both the bootstrap and every phase boundary named a tool the host does not have and neither named what to do instead. The tracker state was written correctly, every phase advanced, and the screen stayed empty for the whole run. Both outputs now carry the fallback on the same line - the bordered card IS the widget on such a build, reprinted inside the reply at every boundary, which is what Copilot has always done - and `tiles` renders the card itself so the fallback content is already on screen. The branch is taken where the information lives: the model knows which tools it has, the shell does not.
74
+
75
+ - **The closure gate read the wrong row, and then read it wrongly.** `status: final` decided whether an `AS-NN` was still open by taking the first table row in the document that mentioned the id. The corporate template writes `| **Request** | EKLENECEK (AS-NN) |` in the body, so that row won and a final document shipped with live placeholders. Openness now comes from the Section 20 row only, and from its status CELL located by header - and where there is no status column (the corporate Section 20 has none) an `EKLENECEK` still stamped with the id is itself the signal.
76
+
77
+ The second half of the same line was a substring search: `açıklama` contains `Açık`, so a correctly closed Turkish row reading `Karar verildi / Decided` whose question text said "hata açıklaması" was reported as open and blocked. The status vocabulary is now matched per token across the bilingual `/`, never as a substring. English had the same trap in "reopened".
78
+
79
+ - **A finding that arrived inside a skipped check vanished from `--report`.** `mark()` printed the skip label whenever one was passed, so a malformed `status` value (caught by the closure gate, which then reports "draft document") and the redesign drift warning (raised by the branch that reports "not a redesign") were both on stderr and absent from the report. A count now outranks a skip label, which is what makes the registry's own claim - that a finding can be misnamed but never lost - true.
80
+
81
+ - **`council-view.mjs` rendered every ruled-out finding as `unruled`.** `triage-output.schema.json` defines a deferred or rejected item as `{finding, reason}` - the reason is the point of those buckets - while `accepted[]` is a flat finding. The view read the id off the wrapper. Its own fixture used the flat shape, so the gate passed; the fixture is now checked against the schema it represents.
82
+
83
+ - `council-view.mjs --iteration` with no value made the filter compare against `NaN` and exited 2 on a state full of iterations. `/multi-agent:log` reads exit 2 as "the run never reached Phase 4" and omits the section, so a usage error looked like an empty run. It is now exit 64.
84
+
85
+ - The endpoint-mapping column check counted raw `|` segments, so a valid GFM row without the optional trailing pipe was reported as short.
86
+
87
+ - The redesign drift warning fired on prose that merely used the words. `sectionBody` matches its keyword as a raw substring of the heading, deliberately, because Locked 2 re-flows section numbers - so `### 4.9 Notes on the old Current Behaviour audit process` matched, and Phase 4 runs `--strict`, which turned that warning into a blocked document. The rendered artefact is the honest signal: a current-behaviour table always carries `CB-<slug>-NN` ids and a paragraph using the words never does.
18
88
 
19
89
  ## [16.27.0] - 2026-09-10
20
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "16.27.0",
3
+ "version": "16.28.0",
4
4
  "description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -101,10 +101,10 @@
101
101
  "ajv": "^8.20.0",
102
102
  "ajv-formats": "^3.0.1",
103
103
  "c8": "^12.0.0",
104
- "eslint": "^10.8.0",
104
+ "eslint": "^10.10.0",
105
105
  "eslint-config-prettier": "^10.1.8",
106
- "eslint-plugin-n": "^18.2.2",
106
+ "eslint-plugin-n": "^18.3.0",
107
107
  "prettier": "^3.9.6",
108
- "publint": "0.3.22"
108
+ "publint": "0.3.24"
109
109
  }
110
110
  }
@@ -203,7 +203,7 @@ When the user supplies an ID/URL/free-text:
203
203
  2. **Pick account** - always asked (autopilot picks default)
204
204
  3. **Fetch issue** - `lib/issue-fetcher.sh "$INPUT"` (Jira/GitHub fetch + maturity)
205
205
  4. **Repo picker** - only for `gh-num` (e.g. `316`) when repo is ambiguous, multi-select
206
- 5. **Maturity check** - blocker halts (autopilot too), warning prompt/log. For a Jira input specifically, an empty own description with a non-empty parent surfaces as `description_empty_parent_available` (warning, not blocker) - ask the tailored parent-description question from `jira/SKILL.md`'s maturity section instead of the generic "Continue?" prompt.
206
+ 5. **Maturity check** - blocker halts (autopilot too), warning prompt/log. For a Jira input specifically, an empty own description with a non-empty parent surfaces as `description_empty_parent_available` (warning, not blocker) - ask the tailored parent-description question from `jira/SKILL.md`'s maturity section instead of the generic "Continue?" prompt. When the parent is empty too, `description_empty_sibling_available` says a sibling sub-task carries the content; `descriptor.relatedIssues[]` holds them either way.
207
207
  6. **Dev context** - always runs, empty submit allowed
208
208
  7. → Interactive Launcher → Phase 0
209
209
 
@@ -2,6 +2,7 @@
2
2
  description: "List unassigned GitHub issues, pick one, auto-assign, and launch the multi-agent pipeline. Use when a GitHub issue should be picked up and started without knowing its number in advance."
3
3
  description-tr: "Atanmamış GitHub issue'larını listeler, birini seçtirir, otomatik atar ve multi-agent pipeline'ı başlatır."
4
4
  argument-hint: "[autopilot] - optional: run the pipeline without confirmations"
5
+ not-for: jira
5
6
  ---
6
7
 
7
8
  # multi-agent issue - 4-Step Picker (Account → Repos → Issue → Dev Context)
@@ -85,11 +85,14 @@ After issue selection, inspect `maturity` from `~/.claude/lib/issue-fetcher.sh`:
85
85
  > - **Accept** → set the working `description` to `parentDescription` for every later phase (analysis/dev/review all read this one substituted value); log the substitution (source issue + parent key) to `agent-log.md`.
86
86
  > - **Decline** → same as a `description_empty` blocker: halt, advise fixing the issue's own description.
87
87
  > - **Autopilot**: auto-accepts "Use parent's description" (same "warnings auto-continue" rule as any other warning), logs the auto-accept + parent key to `agent-log.md`.
88
+ >
89
+ > **Sibling sub-tasks**: the fetcher also reads the parent's other sub-tasks into `descriptor.relatedIssues[]`. Show them as their own labelled block, never substituted into `description`. When the issue's description and the parent's are both empty but a sibling has content, `description_empty_sibling_available` replaces the `description_empty` blocker - same question, sibling key. Contract: `$HOME/.claude/multi-agent-refs/features/jira-context.md`.
88
90
 
89
91
  | Outcome | Behavior |
90
92
  |---|---|
91
93
  | `blockers` non-empty | **Halt** (autopilot too): show summary, advise "fix the issue first" |
92
94
  | `warnings` contains `description_empty_parent_available` | Tailored parent-description question above (not the generic one) |
95
+ | `warnings` contains `description_empty_sibling_available` | Same question, sibling key substituted for the parent's |
93
96
  | other `warnings` only | AskUserQuestion: show summary + "Continue?". Autopilot auto-continues; warnings logged to `agent-log.md` |
94
97
  | both empty (score ≥ 90) | Continue silently |
95
98
 
@@ -29,4 +29,10 @@ Show the task's detailed agent-log.md report.
29
29
  ```
30
30
  Exit 2 means no tracker data exists - say so in one line (`cost unavailable: no token telemetry recorded`) instead of leaving the section out silently.
31
31
 
32
- 5. **Provide a summary** - when the log is long, show only the Timeline table + Review Consensus + the cost block, and give the file path for the full log.
32
+ 5. **Council view** - the log records the verdict, not who found what. Render the panel from the state:
33
+ ```bash
34
+ node $HOME/.claude/scripts/council-view.mjs "<artifacts>/agent-state.json"
35
+ ```
36
+ Exit 2 means the run never reached Phase 4 - omit the section rather than reporting an error. The `Seen by` column is how many distinct reviewers raised that finding; it is information, not a verdict.
37
+
38
+ 6. **Provide a summary** - when the log is long, show only the Timeline table + Review Consensus + the cost block, and give the file path for the full log.
@@ -2,6 +2,7 @@
2
2
  description: "Assess whether a GitHub issue is ready for multi-agent development: fetch it, grade scope / acceptance criteria / repro / design / API / stack readiness, then (after confirm) post the gaps as an issue comment. Read-only on code. Use when deciding whether a GitHub issue is specified well enough to hand to development."
3
3
  description-tr: "Bir GitHub issue'sunun multi-agent geliştirmeye hazır olup olmadığını değerlendirir: getirir; kapsam / kabul kriterleri / repro / tasarım / API / stack hazırlığını puanlar, sonra (onay sonrası) eksikleri issue yorumu olarak gönderir. Kodda salt-okunur."
4
4
  argument-hint: "[#N | repo#N | GitHub issue URL] - optional; with no argument, pick from open issues"
5
+ not-for: review-jira
5
6
  ---
6
7
 
7
8
  # multi-agent review-issue - GitHub issue readiness review
@@ -123,8 +123,11 @@ branch_for() {
123
123
  # token never appears in argv (argv is visible to `ps` / process audit).
124
124
  jira_auth_cfg() { printf 'header = "Authorization: Bearer %s"\n' "$1"; }
125
125
 
126
- fetch_jira() {
127
- local key="$1"
126
+ # Resolve the credential helper once and issue one authenticated GET. Split
127
+ # out of fetch_jira so the sibling search reuses the same resolution instead
128
+ # of duplicating thirty lines of it.
129
+ jira_curl() {
130
+ local path="$1"
128
131
  if [ -z "$JIRA_HOST" ] || [ -z "$JIRA_TOKEN_KEY" ]; then
129
132
  echo "ERR: ACCOUNT_JIRA_HOST and ACCOUNT_JIRA_TOKEN_KEY must be set" >&2
130
133
  return 1
@@ -160,11 +163,28 @@ fetch_jira() {
160
163
  echo "ERR: Jira token not found in credential store ($JIRA_TOKEN_KEY)" >&2
161
164
  return 1
162
165
  fi
163
- curl -sf -K <(jira_auth_cfg "$token") \
164
- "https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions,parent" \
166
+ curl -sf -K <(jira_auth_cfg "$token") "https://$JIRA_HOST$path"
167
+ }
168
+
169
+ # The field list is a parameter because the related-issue lookup wants a
170
+ # narrow one; the default is the set every caller needed before it existed.
171
+ fetch_jira() {
172
+ local key="$1"
173
+ local fields="${2:-summary,status,issuetype,description,priority,resolution,fixVersions,parent}"
174
+ jira_curl "/rest/api/2/issue/$key?fields=$fields" \
165
175
  || { echo "ERR: Jira fetch failed for $key" >&2; return 1; }
166
176
  }
167
177
 
178
+ # One request returns every sibling WITH its description. The issue's own
179
+ # `subtasks` field lists ITS children, not its siblings, and the parent's
180
+ # lists siblings without descriptions - either shape costs one GET each.
181
+ # Failure is soft: the caller falls back to an empty list and the run goes on.
182
+ jira_search() {
183
+ local jql="$1" fields="$2" maxr="$3"
184
+ jira_curl "/rest/api/2/search?jql=$jql&fields=$fields&maxResults=$maxr" \
185
+ || { echo "WARN: Jira search failed - related issues unavailable" >&2; return 1; }
186
+ }
187
+
168
188
  fetch_github() {
169
189
  local owner="$1" repo="$2" num="$3"
170
190
  if ! command -v gh >/dev/null 2>&1; then
@@ -207,6 +227,16 @@ priority_weights = {
207
227
  }
208
228
  priority_weight = priority_weights.get(priority.lower(), 0)
209
229
 
230
+ # relatedIssues arrives as one JSON argv rather than through the \x1f channel,
231
+ # so the delimited parse below stays exactly eight fields wide. Anything
232
+ # unparseable degrades to an empty list; a fetcher must not die on context.
233
+ try:
234
+ related = json.loads(fields.get("relatedIssues") or "[]")
235
+ except Exception:
236
+ related = []
237
+ if not isinstance(related, list):
238
+ related = []
239
+
210
240
  descriptor = {
211
241
  "kind": fields.get("kind"),
212
242
  "key": fields.get("key") or None,
@@ -225,6 +255,7 @@ descriptor = {
225
255
  "owner": fields.get("owner") or None,
226
256
  "repo": fields.get("repo") or None,
227
257
  "branchHint": fields.get("branchHint") or None,
258
+ "relatedIssues": related,
228
259
  }
229
260
  extra = {}
230
261
  if fields.get("needsRepoPicker"):
@@ -238,6 +269,8 @@ itype = (descriptor.get("type") or "").lower()
238
269
 
239
270
  parent_desc = (descriptor.get("parentDescription") or "").strip()
240
271
  parent_key = descriptor.get("parentKey") or ""
272
+ sibling_key = next((r.get("key") or "" for r in related
273
+ if isinstance(r, dict) and (r.get("description") or "").strip()), "")
241
274
 
242
275
  blockers, warnings = [], []
243
276
  closed = {"done","closed","cancelled","canceled","resolved"}
@@ -253,6 +286,11 @@ if not desc.strip():
253
286
  # No parent / an equally-empty parent keeps the hard blocker as before.
254
287
  if parent_desc:
255
288
  warnings.append("description_empty_parent_available")
289
+ elif sibling_key:
290
+ # Same shape one level out: the analysis sibling is where the content
291
+ # actually is on some boards. Only fires when the parent has none, so
292
+ # it never costs a point on an issue that reads fine.
293
+ warnings.append("description_empty_sibling_available")
256
294
  else:
257
295
  blockers.append("description_empty")
258
296
  elif len(desc.strip()) < 80:
@@ -273,6 +311,7 @@ labels_tr = {
273
311
  "already_resolved": "Issue zaten resolved/merged ({})".format(resolution or " - "),
274
312
  "description_empty": "Açıklama boş",
275
313
  "description_empty_parent_available":"Açıklama boş ama parent'ta ({}) içerik var - oradan devam edilsin mi?".format(parent_key or " - "),
314
+ "description_empty_sibling_available":"Açıklama boş, parent da boş, ama kardeş maddede ({}) içerik var - oradan devam edilsin mi?".format(sibling_key or " - "),
276
315
  "short_description": "Açıklama çok kısa (<80 karakter)",
277
316
  "short_title": "Başlık çok kısa (<10 karakter)",
278
317
  "no_repro_steps": "Bug için repro adımları yok",
@@ -283,6 +322,7 @@ labels_en = {
283
322
  "already_resolved": "Issue already resolved/merged ({})".format(resolution or " - "),
284
323
  "description_empty": "Description is empty",
285
324
  "description_empty_parent_available":"Description is empty but the parent ({}) has content - continue from there?".format(parent_key or " - "),
325
+ "description_empty_sibling_available":"Description is empty and so is the parent, but a sibling issue ({}) has content - continue from there?".format(sibling_key or " - "),
286
326
  "short_description": "Description too short (<80 chars)",
287
327
  "short_title": "Title too short (<10 chars)",
288
328
  "no_repro_steps": "Bug missing reproduction steps",
@@ -383,13 +423,102 @@ sys.stdout.write((f.get("description") or "").replace("\x1f", " "))
383
423
  fi
384
424
  fi
385
425
  fi
426
+ # A development sub-task's siblings under the same parent carry the rest of
427
+ # the picture: the analysis sub-task is often the one with content while the
428
+ # dev task has none. One search returns all of them WITH their descriptions,
429
+ # so this costs exactly one extra request, and none at all on an issue with
430
+ # no parent. Gated on parentkey alone, not on an empty own description -
431
+ # the analysis sibling is worth reading even when the dev task is filled in.
432
+ # Read prefs only once parentkey is known non-empty: an issue with no parent
433
+ # is the common case and should not pay for an interpreter start. Env wins,
434
+ # which is what lets the offline gate drive every branch without a prefs file.
435
+ prefs_jiraContext_enabled="${JIRA_CONTEXT_ENABLED:-}"
436
+ prefs_jiraContext_maxItems="${JIRA_CONTEXT_MAX_ITEMS:-}"
437
+ prefs_jiraContext_maxCharsPerItem="${JIRA_CONTEXT_MAX_CHARS:-}"
438
+ if [ -n "${parentkey:-}" ] && { [ -z "$prefs_jiraContext_enabled" ] || \
439
+ [ -z "$prefs_jiraContext_maxItems" ] || [ -z "$prefs_jiraContext_maxCharsPerItem" ]; }; then
440
+ _jc=$(python3 - <<'PY' 2>/dev/null || true
441
+ import json, os
442
+ p = os.path.expanduser("~/.claude/multi-agent-preferences.json")
443
+ c = {}
444
+ try:
445
+ c = ((json.load(open(p, encoding="utf-8")).get("global") or {}).get("jiraContext") or {})
446
+ except Exception:
447
+ c = {}
448
+ print("%s %s %s" % (
449
+ "false" if c.get("enabled") is False else "true",
450
+ int(c.get("maxItems", 6)),
451
+ int(c.get("maxCharsPerItem", 1200)),
452
+ ))
453
+ PY
454
+ )
455
+ # Parameter expansion, not `set --`: the positional parameters belong to
456
+ # the script and the jira-url branch re-execs with them.
457
+ _jc="${_jc:-true 6 1200}"
458
+ _jc_rest="${_jc#* }"
459
+ [ -n "$prefs_jiraContext_enabled" ] || prefs_jiraContext_enabled="${_jc%% *}"
460
+ [ -n "$prefs_jiraContext_maxItems" ] || prefs_jiraContext_maxItems="${_jc_rest%% *}"
461
+ [ -n "$prefs_jiraContext_maxCharsPerItem" ] || prefs_jiraContext_maxCharsPerItem="${_jc_rest##* }"
462
+ unset _jc _jc_rest
463
+ fi
464
+ : "${prefs_jiraContext_enabled:=true}"
465
+ : "${prefs_jiraContext_maxItems:=6}"
466
+ : "${prefs_jiraContext_maxCharsPerItem:=1200}"
467
+ relatedjson="[]"
468
+ if [ -n "${parentkey:-}" ] && [ "$prefs_jiraContext_enabled" = "true" ] && [ "$prefs_jiraContext_maxItems" -gt 0 ]; then
469
+ # parentkey is API-supplied but goes straight into a URL query, so it is
470
+ # re-validated against the same anchored pattern detect_kind uses. That
471
+ # makes the encoding trivial and closes the JQL-injection path.
472
+ if printf '%s' "$parentkey" | grep -qE '^[A-Z][A-Z0-9]*-[0-9]+$'; then
473
+ sraw=$(jira_search "parent%3D%22$parentkey%22" \
474
+ "summary,issuetype,status,description" 20) || sraw=""
475
+ if [ -n "$sraw" ]; then
476
+ relatedjson=$(printf '%s' "$sraw" | SELF_KEY="$KEY" \
477
+ RELATED_MAX="$prefs_jiraContext_maxItems" RELATED_CHARS="$prefs_jiraContext_maxCharsPerItem" python3 -c '
478
+ import json, os, sys
479
+ try:
480
+ d = json.load(sys.stdin)
481
+ except Exception:
482
+ sys.stdout.write("[]"); raise SystemExit(0)
483
+ self_key = os.environ.get("SELF_KEY", "")
484
+ cap = int(os.environ.get("RELATED_MAX") or 0)
485
+ chars = int(os.environ.get("RELATED_CHARS") or 0)
486
+ out = []
487
+ for i in d.get("issues") or []:
488
+ key = i.get("key") or ""
489
+ if not key or key == self_key:
490
+ continue
491
+ f = i.get("fields") or {}
492
+ desc = (f.get("description") or "").replace("\x1f", " ")
493
+ truncated = False
494
+ if chars and len(desc) > chars:
495
+ desc = desc[:chars] + "..."
496
+ truncated = True
497
+ out.append({
498
+ "key": key,
499
+ "relation": "sibling",
500
+ "type": ((f.get("issuetype") or {}).get("name") or ""),
501
+ "status": ((f.get("status") or {}).get("name") or ""),
502
+ "summary": (f.get("summary") or ""),
503
+ "description": desc,
504
+ "truncated": truncated,
505
+ })
506
+ # Siblings that carry content come first, so the cap keeps the useful ones.
507
+ out.sort(key=lambda r: 0 if r["description"].strip() else 1)
508
+ sys.stdout.write(json.dumps(out[:cap], ensure_ascii=False))
509
+ ') || relatedjson="[]"
510
+ fi
511
+ fi
512
+ fi
513
+ [ -n "$relatedjson" ] || relatedjson="[]"
386
514
  branch=$(branch_for "$KEY" "$itype" "$title")
387
515
  emit_descriptor \
388
516
  "kind=jira" "key=$KEY" "title=$title" "type=$itype" "status=$status" \
389
517
  "description=$description" "host=$JIRA_HOST" \
390
518
  "url=https://$JIRA_HOST/browse/$KEY" "branchHint=$branch" \
391
519
  "priority=$priority" "resolution=$resolution" "fixVersions=$fixversions" \
392
- "parentKey=$parentkey" "parentDescription=$parentdesc"
520
+ "parentKey=$parentkey" "parentDescription=$parentdesc" \
521
+ "relatedIssues=$relatedjson"
393
522
  ;;
394
523
 
395
524
  jira-url)
@@ -18,6 +18,7 @@
18
18
  # "branch":"feature/PROJ-1",
19
19
  # "baseBranch":"develop",
20
20
  # "issueRef":{"kind":"jira","key":"PROJ-1","title":"...", ...},
21
+ # (may carry relatedIssues[] from issue-fetcher.sh)
21
22
  # "primary": {"name":"my-ios-app","cloneUrl":"...","provider":"github"},
22
23
  # "extras": [{"name":"common","cloneUrl":"...","provider":"github"}, ...],
23
24
  # "worktreeRoot":".worktrees/PROJ-1-20260427",
@@ -300,6 +301,13 @@ agent_state = {
300
301
  "projects": [repo_entry(r, i == 0) for i, r in enumerate(all_repos)],
301
302
  }
302
303
 
304
+ # Written only when non-empty, so a GitHub or free-text bridge produces the
305
+ # same bytes it always did. Phase 1 and Phase 2 read it; without it here the
306
+ # context would be intake-only and every resume would start without it.
307
+ related = issue.get("relatedIssues") or []
308
+ if isinstance(related, list) and related:
309
+ agent_state["relatedIssues"] = related
310
+
303
311
  logs_dir = os.path.expanduser(os.path.join("~/.claude/logs/multi-agent", primary["name"], state["taskId"]))
304
312
  os.makedirs(logs_dir, exist_ok=True)
305
313
  out_path = os.path.join(logs_dir, "agent-state.json")
@@ -202,7 +202,7 @@ Phase 2 Section 20 Risks reads this list and emits one open question per entry.
202
202
 
203
203
  **Fallback source**: when `confidence == "none"` AND `evidence.standards[]` does not contain an explicit rule for that field, the renderer reads `$HOME/.claude/multi-agent-refs/conventions-defaults.md` and applies the platform default.
204
204
 
205
- **Caching (Locked 27)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || hash(evidence.repoEvidence) || hash(evidence.conventions))`. Cache key on disk: `/tmp/multi-agent-analysis-cache/<digest>.json` with mtime <= 24h. Cache hit skips Phase 1b and Phase 1c. `--no-cache` flag forces re-run.
205
+ **Caching (Locked 27)**: compute `evidence_digest = sha256(featureName || sorted(platforms) || options.redesign || hash(evidence.repoEvidence) || hash(evidence.conventions))`. `options.redesign` is a digest input: without it a redesign within a day of a normal run on the same feature reuses that cache, skips Phase 1b, and renders an empty current-behaviour table every redesign check then passes over. Cache key on disk: `/tmp/multi-agent-analysis-cache/<digest>.json` with mtime <= 24h. Cache hit skips Phase 1b and Phase 1c. `--no-cache` flag forces re-run.
206
206
 
207
207
  Phase 1d is deliberately absent from the digest inputs. Community signal changes by the hour, so folding it in would produce a new digest on every run, invalidate the cache every time, and re-run the two expensive repo phases the cache exists to skip. The consequence is worth stating plainly: a cache hit reuses yesterday's signal rows. That is the correct trade for an advisory tier, and `--no-cache` is the way to refresh them.
208
208
 
@@ -239,9 +239,9 @@ Firebase question - Auto-detect mode probe order (option 2):
239
239
  2. Generated `AnalyticsEvents/*.swift` / `AnalyticsEvents/*.kt` if present (treat each public struct conforming to `AnalyticsEvent` as one event)
240
240
  3. Repo-level `firebase-events.json` / `analytics/events.json` files
241
241
 
242
- #### Step 5a - Coverage options (opt-in, 2 questions)
242
+ #### Step 5a - Coverage options (opt-in, 3 questions)
243
243
 
244
- One `AskUserQuestion` call with 2 parallel yes/no questions - or 1 when
244
+ One `AskUserQuestion` call with 3 parallel questions - or 2 when
245
245
  `state.analysisSpec.platforms[]` is empty, since `uiTests` only gates a section the
246
246
  development layer would have carried (see the gating table in Step 5). These are opt-INs, not source intake: an empty answer is NOT consent (per `feedback_no-inferred-defaults-from-empty-answer`) - re-ask on empty rather than defaulting silently once the picker is shown.
247
247
 
@@ -257,12 +257,21 @@ Q2: header="A11y depth"
257
257
  options:
258
258
  - label: "Basic checklist" -> state.analysisSpec.options.a11yDepth = "basic" (default)
259
259
  - label: "Full walkthrough" -> state.analysisSpec.options.a11yDepth = "full"
260
+
261
+ Q3: header="Redesign"
262
+ question: <localized: "Does this replace something that already exists? A redesign also records what v1 does today and where each behaviour goes.">
263
+ options:
264
+ - label: "New work" -> state.analysisSpec.options.redesign = false (default)
265
+ - label: "Redesign" -> state.analysisSpec.options.redesign = true
260
266
  ```
261
267
 
262
268
  `options.uiTests` gates Section 15.6 (UI test flows), which lives in the development
263
269
  layer; `options.a11yDepth` gates the Section 16 VoiceOver / TalkBack walkthrough, which
264
270
  does not. Both default to the lighter choice so the doc stays lean unless the user opts in.
265
271
 
272
+ `options.redesign` gates Sections 4.5, 4.6 and 9.5 and loads `analysis/redesign.md`, read
273
+ on no other run (Locked 37).
274
+
266
275
  #### Step 5b - Repo-evidence collector (automatic, no prompt)
267
276
 
268
277
  Runs after Step 5a submits and before Phase 1 begins. Reads from `state.analysisSpec.repos[]`. For each repo, walks the platform whitelist and produces a 13-bucket evidence catalogue. No user interaction. Output: `state.analysisSpec.evidence.repoEvidence[<repo>]`. See Phase 1b for the bucket list and tagging rules.
@@ -57,3 +57,4 @@ When citing a Locked decision in code or docs, prefer `Locked <n> (<short label>
57
57
  34. **References are built from the evidence record, not written.** Section 21 is emitted by `$HOME/.claude/scripts/build-references.mjs` from `state.analysisSpec.evidence.*` in both profiles. Each row carries a precision anchor in its `Sürüm / Ref` column - Figma node id, Confluence `pageId` plus page version, the commit SHA a repo was read at, the Swagger spec version - because a reference with no anchor points at a moving target. Each row carries an `Erişim / Access` cell: a declared source that could not be fetched still gets a row reading `erişilemedi (<reason>)`, since a silently dropped source reads to the next person as a source that never existed. User statements from the conversation that no fetched source contains are recorded as `Serbest metin` rows, quoted verbatim, with the decision they settled. **Coverage gate**: every entry in `evidence.figma[]`, `confluence[]`, `jira[]`, `swagger[]`, `repo[]`, `standards[]`, `firebase[]`, `documents[]`, `outside[]`, `freeText[]` and every entry in `evidence.fetchErrors[]` must appear as a row, and every row must map to an evidence entry. A source that shaped the document but is missing from References fails the dispatch gate; so does an invented row with no evidence behind it.
58
58
  35. **Stack-optional render.** Platform and repo selection are optional. When `state.analysisSpec.platforms[]` is empty, the run still completes: the analysis layers that do not need a target repository render in full - Part A and Part B in the corporate profile, Sections 1-12 and 16-17 in the global profile - and only the development layer is dropped (corporate Part C; global Sections 13, 14, 15) along with the Pass B projection, since there are no conventions to project onto. A Section 20 row records that the development analysis awaits a repo selection. **The channel split survives the missing repo.** Channels are derived from the evidence instead of repo stack tags (`intake.md` Step 3 carries the signal table) and one document is emitted per derived channel - `mobile`, `web`, or both. A phone screen and a browser screen carry different requirements before anyone has picked a repository; the split (Locked 9) exists to carry that difference and only its *projection* half needs conventions. `mobile` stays one channel rather than iOS plus Android, since without conventions nothing tells the two apart. Evidence with no interface at all yields a single channel-agnostic `<feature>.md`. Files land under `~/Desktop/multiAgentAnalysis/<feature-name>/`, named `<feature>-<channel>.md` (or `<feature>.md` for the channel-agnostic case): the repo-relative `analysis/` path has nothing to be relative to without a repo, and the current working directory is never used, since for a repo-less run it is arbitrary. Desktop rather than a hidden directory because the document is a deliverable somebody is meant to open and hand over, and `multiAgentAnalysis` rather than a bare `Analysis` because a generic word collides with whatever else is on a desktop while the producer name groups every run this command ever writes. The Phase 3.5 picker shows the resolved path and takes an override through its Other input. A requirements document is useful before anyone has decided which repository will hold the code, and refusing to produce one until that decision exists inverts the order the work actually happens in.
59
59
  36. **The document is reviewed before it is published.** An analysis run used to go from draft straight to dispatch behind a deterministic validator, so nothing read what it was about to publish: one run put a channel it never searched for, an open question about a frame it never opened, and twenty-three unowned `EKLENECEK` markers onto a live page. Every one is what a reader catches on the first pass. Phase 3.2 runs `phases/phase-4-review.md` Step 0 (strict validator, the host's three-reviewer set, triage) on the draft before the destination is chosen: a finding is cheap while nothing is written. Reviewers are subagents holding `analysis/review.md`, never the context that wrote the document, which cannot notice a search it never thought to run. A blocking finding returns to Phase 2b with dispatch closed and never becomes an open question, since "the document is wrong" is not something to ask the reader; capped at two returns. Phase 3.3 sorts every remaining gap into searched-and-closed, asked-and-answered, or `AS-NN`; an unstamped gap fails the dispatch gate. Autopilot runs both; only the asking degrades, into rows stamped `autopilot: could not ask`.
60
+ 37. **A redesign records v1 before it plans v2.** `options.redesign` is an opt-in on the `options.uiTests` axis, never a third `mode` value: `mode` says how many sections, `redesign` says which content, and a `mode: redesign` would switch off the Full-mode traceability, Test Plan and rule-to-test gates in exactly the documents that need them. It adds three sub-sections and no top-level section: current behaviour with `CB-<slug>-NN` ids and `repo/file:line` citations, the v1 to v2 endpoint mapping, and a difference list over a closed status vocabulary. Every `Missing` and `Partial` owes a Section 20 row by `AS-NN`, and a `CB-` id in one table but not the other fails in both directions - a behaviour that is in the code and on nobody's difference list is what a redesign loses and production finds. Evidence and certainty are derived from `repoEvidence`, never graded by the writer (Locked 24, same reason), and `options.redesign` is an `evidence_digest` input. Contract and the eight checks: `analysis/redesign.md`, loaded only on a redesign run.
@@ -0,0 +1,112 @@
1
+ # Redesign mode - the contract
2
+
3
+ Loaded only when `state.analysisSpec.options.redesign` is true, the same way
4
+ `analysis/review.md` is loaded only by a reviewer subagent. A run that is not a
5
+ redesign never pays for this file.
6
+
7
+ ## What the mode is for
8
+
9
+ A redesign is not a feature. The thing already exists, somebody is going to
10
+ replace it, and the question a reader has is not "what should v2 do" but "what
11
+ does v1 do that v2 must not lose". A normal analysis answers the first question
12
+ and is silent on the second, so the losses surface in production: a validation
13
+ rule nobody wrote down, an error state only the old screen had, a query
14
+ parameter one caller still sends.
15
+
16
+ So the mode adds exactly three artefacts, and every one of them is a claim about
17
+ v1 rather than a plan for v2.
18
+
19
+ ## Why it is an option and not a mode
20
+
21
+ `mode` says how many sections the document carries. `redesign` says which
22
+ content is required. They are different axes, and collapsing them costs real
23
+ checks: `mode === "full"` branches in four places inside
24
+ `validate-analysis-doc.mjs`, so a `mode: redesign` value would silently switch
25
+ off the traceability matrix, the Test Plan requirement and the business-rule to
26
+ test-scenario cross-check - in the documents that need them most. A small screen
27
+ can also have a lite redesign, and that is a real thing to want.
28
+
29
+ The shape therefore matches `options.uiTests` and `options.a11yDepth` exactly: an
30
+ intake opt-in, written into the front-matter, enforced by the validator's
31
+ "front-matter says so, the section must exist" rule.
32
+
33
+ ## The three artefacts
34
+
35
+ Section numbers are per profile. The global profile uses sub-sections of existing
36
+ sections (4.5, 4.6, 9.5) and the corporate profile uses 2.1, 2.3 and 5.N+4; no
37
+ new top-level section is introduced in either, because section numbers are quoted
38
+ in 172 places and renumbering them to add a mode is a worse trade than nesting.
39
+
40
+ ### 1. Current behaviour
41
+
42
+ Every behaviour v1 has that a reader could otherwise only find by reading v1.
43
+ One row per behaviour, with a stable `CB-<slug>-NN` id and a `repo/file:line`
44
+ citation.
45
+
46
+ | Column | Contract |
47
+ |---|---|
48
+ | Id | `CB-<slug>-NN`, `<slug>` the same feature slug the `BR-` ids use |
49
+ | Behaviour | one sentence, present tense, no v2 language |
50
+ | Evidence | `<repo>/<path>:<line>`, or the evidence label when no line can be named |
51
+ | Certainty | `confirmed` or `uncertain` |
52
+
53
+ `Evidence` and `Certainty` are **derived by the renderer** from
54
+ `state.analysisSpec.evidence.repoEvidence`, never asked of the author and never
55
+ written by the model from its own impression. A `direct-match` or `same-domain`
56
+ hit yields `confirmed` with its path and line; a `cross-cutting` hit yields
57
+ `uncertain` with the module name. Letting the writer grade its own evidence is
58
+ the failure Locked 24 already refuses for the concept table, and it is the same
59
+ failure here.
60
+
61
+ ### 2. Endpoint mapping
62
+
63
+ The v1 call and the v2 call side by side, one row per endpoint, plus what
64
+ changed in the shape. A redesign that keeps the same paths still gets the table,
65
+ with the rows saying so: "unchanged" is an answer, and its absence is not.
66
+
67
+ ### 3. Difference list
68
+
69
+ Where each v1 behaviour went. One row per `CB-` id, and the status comes from a
70
+ closed five-value vocabulary:
71
+
72
+ | Status | Meaning |
73
+ |---|---|
74
+ | `Moved` | v2 keeps the behaviour, in a different place; the row names where |
75
+ | `Partial` | v2 keeps part of it; the row names what is dropped |
76
+ | `Missing` | v2 does not have it, and nobody has decided that yet |
77
+ | `New` | v2 behaviour with no v1 counterpart |
78
+ | `Out of scope` | deliberately dropped, with the decision recorded |
79
+
80
+ The cell is bilingual in the same shape Section 20 uses for `Açık / Open`, so a
81
+ Turkish document reads as Turkish while the machine-checked half stays a fixed
82
+ English token.
83
+
84
+ `Missing` and `Partial` are the two that mean work is unfinished, so each owes a
85
+ Section 20 row by `AS-NN`. That is the reason the mode exists: a behaviour that
86
+ v2 drops without a decision is exactly the thing that gets found in production.
87
+
88
+ ## The eight checks
89
+
90
+ All in `validate-analysis-doc.mjs`, all live only when the front-matter says
91
+ `redesign: true`.
92
+
93
+ | Check | Severity | What it catches |
94
+ |---|---|---|
95
+ | the three sections exist | ERROR | Without the sections the row checks below pass over nothing, which is the defect that shows a gate green |
96
+ | a row has no `file:line` and is not marked `uncertain` | ERROR | An unmarked guess. Prose can only ask an author to mark it; this finds the row that was not marked |
97
+ | evidence is `cross-cutting` only, yet `confirmed` | WARN | A legitimate rule in a shared module reads this way, so it must not block - but Phase 4 runs `--strict`, so it blocks in review |
98
+ | a status value outside the vocabulary | ERROR | A controlled vocabulary decaying into free text |
99
+ | a `Missing` or `Partial` row with no Section 20 counterpart | ERROR | The reason the mode exists |
100
+ | a `CB-` id in one table and not the other, both directions | ERROR | A behaviour that is in the code and on nobody's difference list: the thing that disappears in a redesign and is found in production |
101
+ | the endpoint table's column count, then a half-written row | ERROR | A renderer that adds a column silently disabling the row checks |
102
+ | the sections are present but the front-matter does not say `redesign` | WARN | Drift |
103
+
104
+ ## The cache, and the silent failure to avoid
105
+
106
+ `evidence_digest` (Locked 27) summarises `featureName || platforms ||
107
+ repoEvidence || conventions`, with a 24-hour TTL. `options.redesign` **is a
108
+ digest input**. Without it, a redesign started within a day of a normal run on
109
+ the same feature hits that run's cache, skips Phase 1b entirely, and renders a
110
+ redesign document whose current-behaviour table is empty - at which point all
111
+ eight checks above pass over nothing. This is the most likely quiet failure in
112
+ the whole mode, and one digest input is the whole fix.
@@ -191,8 +191,13 @@ Outputs:
191
191
  https://{CONFLUENCE_HOST}/pages/viewpage.action?pageId=... (Android)
192
192
  https://{CONFLUENCE_HOST}/pages/viewpage.action?pageId=... (Backend)
193
193
  - Jira: {JIRA_KEY}-12345 (description updated with 3 platform sections)
194
+
195
+ Provenance:
196
+ evidence_digest: sha256:1f3a9c2 base_commit: 4c1b2de
194
197
  ```
195
198
 
199
+ **Provenance line.** Print the front-matter's `evidence_digest` and `base_commit` in the report too. A timestamp cannot separate two reports made the same day; these two say which side moved.
200
+
196
201
  **Stop. Do not chain into a dev run. Do not open a worktree. Do not create a branch.**
197
202
 
198
203
  **Open-question follow-up**: when any rendered file's Section 20 (Risks and Open Questions) has rows with status `Acik / Open`, append one line to the report: `<localized: "Section 20 has <N> open rows. Run /multi-agent:analysis-resolve to resolve them interactively before dispatching to dev.">`. This is a suggestion line only - never auto-invoke the resolver.
@@ -71,6 +71,7 @@ For each open row in source order:
71
71
  1. **Sibling propagation** (only if front-matter `siblings[]` is non-empty AND at least one resolved row's question text appears verbatim with an open status in a sibling file on disk): one AskUserQuestion, `header: "Siblings"`, question `<localized: "N resolved rows also appear open in sibling file(s) <list>. Apply the same resolutions there?">`, options `Apply to all listed` / `Skip siblings`. On apply: repeat the Phase 2 apply step per matching row per sibling, then give each touched sibling its own changelog row. Platform-specific classes (`convention-fallback`, `reuse-vs-new`) are excluded from matching (New Locked 9).
72
72
  2. **Changelog**: append one row to the Changelog section of every touched file: next version (integer scheme `v1 -> v2`; dotted scheme bumps the minor), today's date, author `analysis-resolve`, change `Resolved <N> of <M> Section 20 rows; <K> deferred`.
73
73
  3. **Punctuation gate**: run the Locked 7 verification grep over every touched file; fix any hit before reporting.
74
+ 3b. **Closure**: when a touched file has zero rows left at `Acik / Open` or `Girdi bekleniyor / Pending input` AND no `AS-NN` survives in its body, ask once per file - `header: "Publish"`, question `<localized: "<file> has no open questions left. Mark it status: final?">`, options `Mark final` / `Keep as draft`. On `Mark final`, set front-matter `status: final`; otherwise leave the key alone. Never flip it without the answer: `final` is a publication claim, and this command is built on one question per decision. `status: final` with anything still open is an ERROR in `validate-analysis-doc.mjs`, so the flip is what makes the closure gate real rather than advisory. Nothing flips back to `draft` here - reopening a question is a new run's decision.
74
75
  4. **Report** (in `outputLanguage`, max 12 lines): doc path(s) + new version, counts (resolved / deferred / follow-ups created / still open), `design-gap` rows that need a `/multi-agent:analysis` re-run (list inputs to re-supply), and - if open rows remain - a reminder that re-running this command resumes where it left off (state is the doc itself; no separate state file).
75
76
 
76
77
  **Stop. No commit, no branch, no dispatch.** The user reviews the diff and commits manually (Locked 6).