baldart 4.58.0 → 4.60.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 (29) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -1
  3. package/VERSION +1 -1
  4. package/framework/.claude/agents/REGISTRY.md +2 -0
  5. package/framework/.claude/agents/codebase-architect.md +9 -0
  6. package/framework/.claude/agents/doc-reviewer.md +11 -0
  7. package/framework/.claude/agents/plan-auditor.md +11 -0
  8. package/framework/.claude/agents/senior-researcher.md +10 -0
  9. package/framework/.claude/skills/new/SKILL.md +63 -3
  10. package/framework/.claude/skills/new/references/codex-gate.md +2 -2
  11. package/framework/.claude/skills/new/references/commit.md +2 -2
  12. package/framework/.claude/skills/new/references/completeness.md +5 -3
  13. package/framework/.claude/skills/new/references/final-review.md +37 -7
  14. package/framework/.claude/skills/new/references/implement.md +2 -2
  15. package/framework/.claude/skills/new/references/merge-cleanup.md +18 -1
  16. package/framework/.claude/skills/new/references/production-readiness.md +17 -2
  17. package/framework/.claude/skills/new/references/review-cycle.md +20 -7
  18. package/framework/.claude/skills/new/references/setup.md +11 -5
  19. package/framework/.claude/skills/new/references/team-mode.md +9 -7
  20. package/framework/.claude/workflows/new-card-review.js +75 -18
  21. package/framework/.claude/workflows/new-final-review.js +212 -15
  22. package/framework/agents/index.md +2 -0
  23. package/framework/agents/return-contract-protocol.md +130 -0
  24. package/framework/templates/agent-return-contract.snippet.md +30 -0
  25. package/framework/templates/baldart.config.template.yml +11 -0
  26. package/package.json +1 -1
  27. package/src/commands/configure.js +13 -0
  28. package/src/commands/doctor.js +33 -0
  29. package/src/commands/update.js +3 -0
@@ -20,9 +20,16 @@ export const meta = {
20
20
  // archBaselinePaths string[] | null per-card baselines; null → spawn architect (F.2)
21
21
  // hasApiDataFiles boolean gates api-perf-cost-auditor (default true)
22
22
  // config object resolved baldart.config.yml (paths.*, …)
23
+ // applyFixes boolean (default false) when true, run the opt-in Fix phase (P0-A, v4.59.0)
24
+ // editableFiles string[] (default []) union ownership map — files writers MAY edit (Fix scope)
23
25
  //
24
26
  // Return value (consumed by the skill at F.5):
25
- // { codexEngine, findings:[…classified, FALSE_POSITIVE dropped], gateTable, summary }
27
+ // applyFixes !== true → EXACTLY { codexEngine, findings:[…classified, FALSE_POSITIVE dropped],
28
+ // noActionFindings, gateTable, summary } (byte-compatible; new2 untouched)
29
+ // applyFixes === true → that PLUS additive { fixesApplied:[…1-line], docFixesApplied:[…1-line],
30
+ // residual:[…slim findings] }. `findings` STAYS the full classified
31
+ // actionable set (telemetry/compat); the skill reads `residual` to
32
+ // decide what (if anything) it still spawns at F.5.
26
33
  // ───────────────────────────────────────────────────────────────────────────
27
34
 
28
35
  // Tolerate args delivered as a JSON string (parse-or-default) — the Workflow tool
@@ -36,6 +43,16 @@ const cfg = a.config || {}
36
43
  const highRisk = (cfg.paths && cfg.paths.high_risk_modules) || [] // security-domain hint
37
44
  const protocolRef = '.claude/skills/new/references/final-review.md'
38
45
 
46
+ // P0-A (v4.59.0) — opt-in Fix phase. Both default safe-OFF: a caller that does not opt in (new2,
47
+ // or any pre-v4.59.0 caller) gets a byte-identical return. Gated below on applyFixes && editable.length.
48
+ const applyFixes = a.applyFixes === true
49
+ const editableFiles = Array.isArray(a.editableFiles) ? a.editableFiles.filter(Boolean) : []
50
+ // Curated toolchain (parity with new-card-review.js): when features.has_toolchain is on the consumer
51
+ // records LITERAL gate commands in toolchain.commands.* — the Fix-phase writers run THOSE. Empty /
52
+ // absent → silent fallback to the project-standard default. Read from args.config, never hardcoded.
53
+ const tcCmds = (cfg.toolchain && cfg.toolchain.commands) || {}
54
+ const tc = (key, fallback) => (tcCmds[key] && String(tcCmds[key]).trim()) || fallback
55
+
39
56
  // Concurrency cap (v4.53.6 — API rate-limit guardrail; mirrors new-card-review.js + the team-mode
40
57
  // coder cap). The org-level rate limit is SHARED across every terminal/session; a wide agent fan-out
41
58
  // saturates it and the server throttles. Cap concurrent agents at 3 via a rolling worker pool (NOT
@@ -135,6 +152,38 @@ const VERDICT_SCHEMA = {
135
152
  rationale: { type: 'string' },
136
153
  },
137
154
  }
155
+ const FIX_SCHEMA = {
156
+ type: 'object', required: ['applied', 'unresolved', 'checks'], additionalProperties: false,
157
+ properties: {
158
+ applied: {
159
+ type: 'array',
160
+ items: {
161
+ type: 'object', required: ['finding_id'], additionalProperties: false,
162
+ properties: { finding_id: { type: 'string' }, note: { type: 'string' } },
163
+ },
164
+ },
165
+ unresolved: { type: 'array', items: { type: 'string', description: 'finding_id left unfixed' } },
166
+ checks: {
167
+ type: 'object', required: ['lint', 'tsc', 'build'], additionalProperties: false,
168
+ properties: { lint: { enum: ['PASS', 'FAIL', 'SKIP'] }, tsc: { enum: ['PASS', 'FAIL', 'SKIP'] }, build: { enum: ['PASS', 'FAIL', 'SKIP'] } },
169
+ },
170
+ note: { type: 'string' },
171
+ },
172
+ }
173
+ // SC-1 — collision-safe finding_id reconciliation. The fan-in rewrites every id to `${source}#${i}:${orig}`,
174
+ // but a writer often echoes only the human-readable tail → an exact-Set match drops applied fixes into
175
+ // residual. Accept exact equality OR a UNIQUE colon-delimited tail (id.endsWith(':'+token)); a bare suffix
176
+ // (e.g. '001' vs 'CR-001') must NOT match → only ':'+token. 0 or >1 hits → return the token unchanged
177
+ // (caller logs the mismatch; never silently mis-attribute across cards).
178
+ function makeResolveId(canonIds) {
179
+ return (returned) => {
180
+ const t = String(returned == null ? '' : returned).trim()
181
+ if (!t) return returned
182
+ if (canonIds.includes(t)) return t
183
+ const hits = canonIds.filter((id) => id === t || id.endsWith(':' + t))
184
+ return hits.length === 1 ? hits[0] : returned
185
+ }
186
+ }
138
187
 
139
188
  // ---- Shared brief fragments -------------------------------------------------
140
189
  const scopeBrief = [
@@ -342,20 +391,34 @@ for (const item of reviewResults) {
342
391
  }
343
392
  }
344
393
 
345
- // F-040 — ONE deterministic fallback: either the pre-flight found no companion, or it ran
346
- // but did not complete. Either way the primary code review must still happen (F.4 step 8).
347
- // PROSE-ONLY salvage (v4.56.0): when Codex FINISHED but emitted prose (no sentinel JSON), seed the
348
- // fallback with that prose as UNVERIFIED leads the relay never parses prose; the fallback
349
- // code-reviewer (real reviewer WITH code access) re-verifies each lead against the actual diff and
350
- // emits the schema, so Codex's cross-model finds are recovered, not discarded. Same trust model.
351
- if (!codexRan) {
352
- codexEngine = codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)'
353
- const seedBlock = codexProse
354
- ? `\n\nCodex (a DIFFERENT model) reviewed this exact batch diff and reported the following findings in PROSE — it failed to emit machine-readable JSON, so these are UNVERIFIED leads, NOT validated findings. VERIFY each against the actual diff at its cited file:line, DROP any that don't hold, then run your own full independent review for anything Codex missed. Treat the prose as hints to check, never as ground truth:\n<codex-prose>\n${codexProse}\n</codex-prose>`
355
- : ''
394
+ // F-040 — fallback when Codex did not produce parseable JSON. TWO distinct sub-branches:
395
+ // SEEDED (codexProse present, v4.59.0 Opt-2): Codex FINISHED and reported findings in PROSE.
396
+ // The cross-model coverage is ALREADY OBTAINED re-running a full re-hunting code-reviewer over
397
+ // the whole diff would be a wasted duplicate pass. Instead run a CHEAP CONVERTER that ONLY turns
398
+ // the prose into the finding schema (no independent re-hunt) and marks the findings source
399
+ // 'codex' but NON-preValidated the existing Verify phase validates each over the real code.
400
+ // This preserves the trust model (Verify validates) while removing the duplicate full re-hunt;
401
+ // layered coverage stays (per-card simplify/security + qa + Verify + the rest of this Final pass).
402
+ // COLD (no prose at all): genuinely no Codex output → run today's FULL code-reviewer as the primary
403
+ // review engine; its own FP check makes it preValidated (a second pass would self-judge).
404
+ if (!codexRan && codexProse) {
405
+ codexEngine = 'codex (prose-converted)'
406
+ const conv = await agent(
407
+ `Convert the Codex PROSE findings below into the structured finding schema. For each finding: keep the cited file:line + severity + a one-line minimal_fix_direction; do a LIGHT plausibility check (drop a lead whose cited file:line is obviously wrong/empty); do NOT perform an independent code review or hunt for new issues. Return findings[].\n\n` +
408
+ `Codex (a DIFFERENT model) reviewed this exact batch diff and reported the following in PROSE (it failed to emit machine-readable JSON):\n<codex-prose>\n${codexProse}\n</codex-prose>`,
409
+ { label: 'codex-prose-convert', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
410
+ )
411
+ // NON-preValidated (no `preValidated:true`): the converter did not re-hunt or independently
412
+ // verify, so verifyFinding routes each to its domain specialist over the real code. Clamp
413
+ // confidence < 80 — verifyFinding short-circuits to VERIFIED at >= 80, which would skip the
414
+ // domain-specialist validation SC-3 relies on (the converter just transcribes Codex's own confidence).
415
+ if (conv && Array.isArray(conv.findings)) raw.push(...conv.findings.map((f) => ({ ...f, source: 'codex', confidence: Math.min(Number(f.confidence) || 0, 70) })))
416
+ } else if (!codexRan) {
417
+ // COLD branch: no Codex JSON and no prose → the primary code review must still happen (F.4 step 8).
418
+ codexEngine = 'code-reviewer (fallback)'
356
419
  const fb = await agent(
357
- `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied. An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.${seedBlock}`,
358
- { label: codexProse ? 'code-reviewer (codex-seeded)' : 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
420
+ `Codex was unavailable for the batch final review. Run the FULL code review yourself over the batch diff, per ${protocolRef} Step F.3.\n\n${scopeBrief}\n\n${baselineBrief}\n\nReturn findings using the schema fields, with a self false-positive check applied. An observation you VERIFIED needs NO change (fix direction "no fix required" / "acceptable as-is") is not work — set requires_action:false (recorded, never sent to a fixer), or do not emit it.`,
421
+ { label: 'code-reviewer (fallback)', phase: 'Review', agentType: 'code-reviewer', schema: FINDINGS_SCHEMA }
359
422
  )
360
423
  // the fallback code-reviewer IS the primary code-review engine here and applied its own FP
361
424
  // check (prompt above) → trusted, exactly like Codex. A SECOND code-reviewer pass over its own
@@ -438,8 +501,142 @@ const summary = {
438
501
  }
439
502
  log(`Final review done: ${summary.verified} verified (${summary.blockers} BLOCKER / ${summary.highs} HIGH), ${summary.noAction} no-action (recorded), ${summary.needsManual} needs-manual, ${summary.failingGates.length} failing gate(s). Engine: ${codexEngine}.`)
440
503
 
441
- return { codexEngine, findings, noActionFindings, gateTable, summary }
504
+ // ───────────────────────────────────────────────────────────────────────────
505
+ // Phase Fix (P0-A, v4.59.0) — OPT-IN. Mirrors new-card-review.js's Fix+Doc phases EXACTLY:
506
+ // security writer FIRST (owns the security-invariant contract — never folded into the coder pass),
507
+ // then the coder (code/perf/migration/test/simplify), then a doc-reviewer audit-and-apply pass
508
+ // (WORKTREE ISOLATION). Passes run SEQUENTIALLY so edits on shared files never conflict — no
509
+ // disjoint-ownership gate needed; the last code pass to run carries the build it verified.
510
+ // Gated on applyFixes && editableFiles.length: a caller that does NOT opt in (new2, any pre-v4.59.0
511
+ // caller) skips the whole phase and returns the byte-identical shape below.
512
+ // Unfixed BLOCKER/HIGH (and every unresolved actionable / NEEDS_MANUAL) flow into `residual` —
513
+ // never silently dropped.
514
+ // ───────────────────────────────────────────────────────────────────────────
515
+ let fixesApplied = []
516
+ let docFixesApplied = []
517
+ let residual = []
518
+ let checksFailed = false
519
+ if (applyFixes && editableFiles.length) {
520
+ phase('Fix')
521
+ const isDoc = (f) => /doc|wiki|ssot|readme/.test(String(f.domain).toLowerCase())
522
+ // 'security' domain → security-reviewer. migration STAYS coder (canonical writer map), so match the
523
+ // exact 'security' domain, not the broader verifier regex.
524
+ const isSecurity = (f) => String(f.domain).toLowerCase() === 'security'
525
+ const isManual = (f) => f.classification === 'NEEDS_MANUAL_CONFIRMATION'
526
+ // Partition the actionable set out of `findings` (= VERIFIED + NEEDS_MANUAL; FP + no-action already
527
+ // removed above). No overlap:
528
+ // securityFix = VERIFIED security → security-reviewer applies (owns the security invariants).
529
+ // codeActionable = VERIFIED non-doc non-security → the coder fixes these.
530
+ // docFindings = VERIFIED doc → the doc-reviewer audit-and-apply pass.
531
+ // manualResidual = NEEDS_MANUAL any → human gate, owned by the skill (carries its classification out).
532
+ const securityFix = findings.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && isSecurity(f))
533
+ const codeActionable = findings.filter((f) => f.classification === 'VERIFIED' && !isDoc(f) && !isSecurity(f))
534
+ const docFindings = findings.filter((f) => f.classification === 'VERIFIED' && isDoc(f))
535
+ const manualResidual = findings.filter(isManual)
536
+
537
+ const SKIP_CHECKS = { lint: 'SKIP', tsc: 'SKIP', build: 'SKIP' }
538
+
539
+ // One fix pass: the domain WRITER applies its verified findings to the worktree, then re-verifies.
540
+ async function applyFixPass(items, writer, label, role) {
541
+ if (!items.length) return { applied: [], unresolved: [], checks: { ...SKIP_CHECKS }, ran: false }
542
+ const fixBrief =
543
+ `Apply ALL of the verified ${role} review findings below to the worktree, then verify the build. You are the ${writer} fix pass for this batch's FINAL review.\n\n` +
544
+ `Worktree: ${a.worktreePath || '(cwd)'} — cd into it.\n` +
545
+ `You MAY edit ONLY these files (ownership map — touching anything else is a violation):\n${editableFiles.join('\n')}\n\n` +
546
+ `Findings to fix (fix the code, not the tests unless a test itself is wrong; do NOT expand scope beyond the finding):\n` +
547
+ items.map((f) => `- [${f.finding_id}] (${f.domain} / ${f.severity}) ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n') +
548
+ `\n\nSCOPE DISCIPLINE (do not run away — every finding already cites the exact site to fix): go DIRECTLY to each finding's \`evidence\` file:line and apply its \`direction\`. Do NOT re-map the architecture, do NOT re-survey the codebase, do NOT read files beyond the cited evidence + their immediate dependencies. A verified finding is a precise instruction, not a research prompt.` +
549
+ `\n\nAfter applying: run \`${tc('lint', 'npm run lint')}\` and (when the project uses typescript) \`${tc('typecheck', 'npx tsc --noEmit')}\` and \`${tc('build', 'npm run build')}\` in the worktree. If a check fails because of an edit you made, fix the regression — at most 2 retries — staying within the allowed files. ` +
550
+ `Do NOT commit. Do NOT git stash (refs/stash is shared across worktrees). ` +
551
+ `Return: applied (finding_ids you fixed — list a finding here if you MADE the edit, even when the build stays red for reasons OUTSIDE this finding / your ownership; the build status is reported separately in checks), unresolved (finding_ids you could NOT fix within the allowed files / 2 retries), and checks (PASS/FAIL/SKIP for lint, tsc, build).`
552
+ const r = await agent(fixBrief, { label, phase: 'Fix', agentType: writer, schema: FIX_SCHEMA })
553
+ const res = (r && typeof r === 'object') ? r : { applied: [], unresolved: items.map((f) => f.finding_id), checks: { ...SKIP_CHECKS } }
554
+ if (!Array.isArray(res.applied)) res.applied = []
555
+ if (!Array.isArray(res.unresolved)) res.unresolved = []
556
+ if (!res.checks || typeof res.checks !== 'object') res.checks = { ...SKIP_CHECKS }
557
+ res.ran = true
558
+ log(`Fix: ${writer} applied ${res.applied.length}/${items.length} ${label} finding(s); checks lint=${res.checks.lint} tsc=${res.checks.tsc} build=${res.checks.build}.`)
559
+ return res
560
+ }
561
+
562
+ // Security writer FIRST, then the coder — sequential → no edit conflict on shared files.
563
+ const secResult = await applyFixPass(securityFix, 'security-reviewer', 'fix-security', 'security')
564
+ const codeFixResult = await applyFixPass(codeActionable, 'coder', 'fix-coder', 'code/perf/simplify')
565
+ if (!securityFix.length && !codeActionable.length) log('Fix: no actionable code/perf/security/simplify findings — fixers skipped.')
566
+
567
+ // Reconcile applied/unresolved via SC-1 (the writer may echo only the human-readable id tail).
568
+ const fixPasses = [secResult, codeFixResult]
569
+ const allActionable = [...securityFix, ...codeActionable]
570
+ const canonIds = allActionable.map((f) => f.finding_id)
571
+ const resolveId = makeResolveId(canonIds)
572
+ const appliedIds = new Set(fixPasses.flatMap((p) => (p.applied || []).map((x) => resolveId(x.finding_id))))
573
+ const unresolvedIds = new Set(fixPasses.flatMap((p) => (p.unresolved || []).map(resolveId)))
574
+ const matched = [...appliedIds].filter((id) => canonIds.includes(id)).length
575
+ const claimed = fixPasses.reduce((n, p) => n + (p.applied || []).length, 0)
576
+ if (matched < claimed) log(`Fix: id-reconciliation — ${claimed - matched} applied id(s) did not map to a canonical finding_id (left as residual; check writer echo).`)
577
+ const ranChecks = fixPasses.filter((p) => p.ran).map((p) => p.checks)
578
+ const mergedChecks = ['lint', 'tsc', 'build'].reduce((acc, k) => {
579
+ acc[k] = ranChecks.some((c) => c[k] === 'FAIL') ? 'FAIL' : (ranChecks.some((c) => c[k] === 'PASS') ? 'PASS' : 'SKIP')
580
+ return acc
581
+ }, {})
582
+ checksFailed = ['lint', 'tsc', 'build'].some((k) => mergedChecks[k] === 'FAIL')
583
+ const codeResidual = allActionable.filter((f) => !appliedIds.has(f.finding_id) || unresolvedIds.has(f.finding_id))
584
+
585
+ // Doc audit-and-apply pass (WORKTREE ISOLATION — worktree-rooted paths, do NOT git stash).
586
+ const docDir = (cfg.paths && (cfg.paths.docs_dir || cfg.paths.references_dir)) || 'docs'
587
+ let docApplied = []
588
+ let docResidualOut = docFindings // default (no-run branch) keeps the contract explicit
589
+ const DOC_SURFACE_RE = /(^|\/)(docs?|api|server\/data|lib\/server|schema|migrations?)(\/|\.|$)|\.(md|sql)$/i
590
+ const docSurfaceTouched = scope.some((f) => DOC_SURFACE_RE.test(String(f)))
591
+ if (docSurfaceTouched || docFindings.length) {
592
+ const docBrief =
593
+ `You OWN the \`doc\` domain end-to-end for this batch's FINAL review: AUDIT the committed diff for doc drift AND APPLY the fixes in THIS invocation (audit-and-apply mode). Do NOT defer doc writes to another agent.\n\n` +
594
+ `**WORKTREE ISOLATION (per AGENTS.md § Worktree isolation):** every doc you Edit/Write MUST be UNDER the worktree root ${a.worktreePath || '(cwd)'}. NEVER write a path under the main repo — it is checked out live by other sessions; \`cd\` alone does not stop an absolute main-repo path. cd into the worktree first and edit the worktree-rooted copy of each doc (e.g. ${docDir}/… UNDER the worktree).\n\n` +
595
+ `${scopeBrief}\n\n${baselineBrief}\n\n` +
596
+ (docFindings.length
597
+ ? `Verified doc-domain findings from this batch's review (apply each):\n${docFindings.map((f) => `- [${f.finding_id}] ${f.title}\n evidence: ${f.evidence}\n direction: ${f.minimal_fix_direction}`).join('\n')}\n\n`
598
+ : '') +
599
+ `Then run your standard doc audit on the changed files: sync canonical reference docs (api/, data-model, ssot-registry as applicable), set freshness_status:fresh + last_verified_from_code:<today> on docs you touch. ` +
600
+ `Do NOT commit (the orchestrator commits). Do NOT git stash (refs/stash is shared across worktrees). ` +
601
+ `A doc-drift→bug finding rooted in CODE (the impl contradicts a documented contract) is the ONE thing you do NOT fix yourself — return it in \`unresolved\` with the conflicting code file:line + the doc it violates.\n\n` +
602
+ `Return: applied (one entry per doc fix you MADE — finding_id REQUIRED: use the listed finding's id when it maps to one, else a short synthetic id like "doc-audit-1"; put a 1-line summary in note), unresolved (finding_ids / descriptions you could NOT resolve, incl. code-rooted drift), checks (lint/tsc/build PASS/SKIP — SKIP unless you touched a source-adjacent file).`
603
+ const dr = await agent(docBrief, { label: 'fix-doc', phase: 'Fix', agentType: 'doc-reviewer', schema: FIX_SCHEMA })
604
+ const dres = (dr && typeof dr === 'object') ? dr : {}
605
+ docApplied = Array.isArray(dres.applied) ? dres.applied : []
606
+ const docUnresolved = Array.isArray(dres.unresolved) ? dres.unresolved : []
607
+ // SC-1 reconciliation over the doc canon (synthetic doc-audit#i ids return unchanged — correct).
608
+ const resolveDoc = makeResolveId(docFindings.map((f) => f.finding_id))
609
+ const docAppliedIds = new Set(docApplied.map((x) => resolveDoc(x && x.finding_id)).filter(Boolean))
610
+ docResidualOut = docFindings.filter((f) => !docAppliedIds.has(f.finding_id))
611
+ const extraDocResidual = docUnresolved
612
+ .filter((u) => typeof u === 'string' && u.trim() && !docFindings.some((f) => f.finding_id === u))
613
+ .map((u, i) => ({ finding_id: `doc-audit#${i}`, title: String(u).slice(0, 160), severity: 'MEDIUM', domain: 'doc', evidence: '(doc-reviewer audit)', minimal_fix_direction: String(u), classification: 'VERIFIED' }))
614
+ docResidualOut = [...docResidualOut, ...extraDocResidual]
615
+ log(`Fix: doc-reviewer applied ${docApplied.length} doc fix(es)${docFindings.length ? ` (incl. ${docFindings.length} routed finding(s))` : ''}; ${docResidualOut.length} unresolved → residual.`)
616
+ } else {
617
+ log('Fix: doc audit SKIPPED (no doc-relevant surface in the batch diff and no verified doc finding).')
618
+ }
619
+
620
+ // Assemble the additive return fields.
621
+ for (const f of allActionable) {
622
+ if (appliedIds.has(f.finding_id) && !unresolvedIds.has(f.finding_id)) fixesApplied.push(`[${f.finding_id}] ${f.title}`)
623
+ }
624
+ for (const x of docApplied) {
625
+ const note = (x && (x.note || x.finding_id)) ? `${x.finding_id ? `[${x.finding_id}] ` : ''}${x.note || ''}`.trim() : '(doc fix)'
626
+ docFixesApplied.push(note)
627
+ }
628
+ // Residual = unfixed code/perf/security + unresolved doc + every NEEDS_MANUAL (BLOCKER/HIGH ALWAYS land here).
629
+ residual = [...codeResidual, ...docResidualOut, ...manualResidual].map(slimFinding)
630
+ log(`Final Fix phase done: ${fixesApplied.length} fixed, ${docFixesApplied.length} doc-applied, ${residual.length} residual${checksFailed ? ', post-fix checks FAILED' : ''}.`)
631
+ }
632
+
633
+ const base = { codexEngine, findings, noActionFindings, gateTable, summary }
634
+ // Additive ONLY when the Fix phase ran — otherwise byte-identical to today's shape (new2 untouched).
635
+ return applyFixes ? { ...base, fixesApplied, docFixesApplied, residual } : base
442
636
 
443
637
  function emptySummary() {
444
638
  return { total: 0, verified: 0, falsePositive: 0, needsManual: 0, blockers: 0, highs: 0, failingGates: [] }
445
639
  }
640
+ function slimFinding(f) {
641
+ return { finding_id: f.finding_id, title: f.title, severity: f.severity, domain: f.domain, evidence: f.evidence, minimal_fix_direction: f.minimal_fix_direction, classification: f.classification }
642
+ }
@@ -36,6 +36,7 @@ Route agents to the right module with minimal reading.
36
36
  - If touching performance limits or scaling -> read `agents/performance.md`.
37
37
  - If touching monitoring/logging -> read `agents/observability.md`.
38
38
  - If tuning reasoning depth — setting a skill's `effort:` baseline or honoring an inline `effort=<level>` override -> read `agents/effort-protocol.md`.
39
+ - If authoring/auditing a subagent whose return would flood the orchestrator context (free-form analysis/audit/research) -> read `agents/return-contract-protocol.md` and bound its final message: COMPACT headline + `path:line` findings + disk pointer, persist the long form. The input-side twin of the effort protocol.
39
40
  - If building or maintaining a derived LLM wiki overlay (`${paths.wiki_dir}/`) -> read `agents/llm-wiki-methodology.md` and invoke `wiki-curator` / `/capture`.
40
41
  - For day-to-day status -> read and update `${paths.references_dir}/project-status.md`.
41
42
  - For legacy context -> read `archive/project_full_legacy.md` if it exists.
@@ -71,6 +72,7 @@ When adding or updating agents, update REGISTRY.md — not this file.
71
72
  - `agents/i18n-protocol.md` — Internationalization discipline: no hardcoded user-facing strings, context registry (`paths.i18n_registry`) + ICU + context-aware translation; BLOCKING anti-hardcoded lint gate (since v4.52.0, gated on `features.has_i18n`)
72
73
  - `agents/design-system-protocol.md` — Registry-first discipline for UI work: BLOCKING cascade on `INDEX.md` + `tokens-reference.md` + `components/<Name>.md` (since v3.11.0, gated on `features.has_design_system`)
73
74
  - `agents/card-schema.md` — Atomic Card Baseline Schema: the universal, profile-aware (epic/child/standalone) field contract every backlog card satisfies, plus the consumer HALT/BACK-FILL/WARN contract (since v4.35.0)
75
+ - `agents/return-contract-protocol.md` — Subagent return-message economy: COMPACT (bounded headline + `path:line` findings + disk pointer) vs FULL, persist-then-summarize — the input-side twin of `effort-protocol.md` (since v4.59.0)
74
76
 
75
77
  ## Where to Document (Decision Tree)
76
78
 
@@ -0,0 +1,130 @@
1
+ <!-- contamination-scan: skip
2
+ The only token the scanner flags here is the substring "fidelity" inside the
3
+ framework agent name `visual-fidelity-verifier` (a real BALDART agent cited as
4
+ a return-shape exemplar). It is framework content, not consumer project-leak. -->
5
+ # Return Contract Protocol
6
+
7
+ ## Purpose
8
+
9
+ Define how a BALDART **subagent** shapes the *final message* it returns to the
10
+ orchestrator that spawned it. The goal is **context economy of the orchestrator**:
11
+ a subagent's full reasoning is often large (analysis, audits, research, doc
12
+ sweeps), and when that whole body lands verbatim in the orchestrator's context it
13
+ crowds out everything else and inflates token cost for every subsequent turn. The
14
+ contract is simple — **persist the long form, return the short form**: the
15
+ deliverable lives on disk (or as structured findings), and the message back to the
16
+ orchestrator is a bounded headline + pointers, never a reasoning dump.
17
+
18
+ This is the input-side twin of the `effort-protocol`: effort governs how *deeply*
19
+ an agent reasons; this governs how *compactly* it reports back.
20
+
21
+ ## Scope
22
+
23
+ **In**: The shape and size ceiling of a subagent's *return message* (its last
24
+ action before yielding to the orchestrator), the COMPACT vs FULL distinction, and
25
+ where the long-form body goes instead.
26
+ **Out**: What an agent writes to disk (that is the agent's own deliverable format —
27
+ docs, reports, locale files, cards), the native effort knob (see
28
+ `effort-protocol.md`), and runtime/wire-level token compression (BALDART does not
29
+ ship a runtime; this is a prompt-level authoring convention, not a proxy).
30
+
31
+ ## Background — why the return message is the lever
32
+
33
+ A subagent invoked via the `Task` tool runs in its own context window; only its
34
+ **final message** is injected back into the orchestrator's context. So the only
35
+ text that costs the orchestrator tokens is that final message — not the agent's
36
+ intermediate reading, thinking, or tool calls. Therefore the entire optimization
37
+ reduces to one rule: **make the final message bounded and structured.** Everything
38
+ the agent needed to reason over stays in its own (discarded) context; the
39
+ durable long-form output, when it is a deliverable, goes to disk where the
40
+ orchestrator can read it on demand instead of paying for it on every turn.
41
+
42
+ ## The two modes
43
+
44
+ ### COMPACT (default for every orchestrator-facing subagent)
45
+
46
+ The final message is bounded and consists only of:
47
+
48
+ 1. **Headline / verdict** — one line: the outcome (`PASS | FAIL`, "analysis
49
+ ready", "3 findings", "report written").
50
+ 2. **Findings or key points** — as `path:line` references, not pasted code or
51
+ prose. If the agent emits review findings, use the shared pooled YAML findings
52
+ schema (see "Reuse the existing findings schema" below) — do not invent a new
53
+ one.
54
+ 3. **Disk pointer** — `Report: <path>` (or `Doc: <path>`, `Cards: <path>`) when
55
+ the full analysis was persisted, so the orchestrator can read the long form
56
+ only if it needs to.
57
+
58
+ Keep it tight — model the ceiling on `qa-sentinel`'s "Return Protocol" (a short
59
+ final message, *not* the full report) and `visual-fidelity-verifier`'s strict
60
+ JSON. A good COMPACT return is tens of lines, not hundreds. **No preamble, no
61
+ restating the task, no narrating what you read.**
62
+
63
+ ### FULL (opt-in, narrow)
64
+
65
+ The agent returns extended narrative **only** when:
66
+ - the agent was invoked **directly by the user** (not by an orchestrator) for an
67
+ explanation/exploration where the prose *is* the answer; or
68
+ - the agent's deliverable is inherently the long-form text AND there is no disk to
69
+ persist it to.
70
+
71
+ Even in FULL mode, prefer **persist-then-summarize**: if the agent can write the
72
+ long form to disk (a report, a doc, a research file), do that and return COMPACT
73
+ with the pointer. FULL is the exception, not the fallback.
74
+
75
+ ## Persist-then-summarize (the guiding rule)
76
+
77
+ If your reasoning is a deliverable, **write it to disk and return the pointer.**
78
+ The orchestrator's context is not an archive — it is a working set. An agent that
79
+ writes its report to `/qa/<CARD-ID>.md`, `${paths.references_dir}/...`, or a
80
+ research file and returns a three-line pointer has done its job better than one
81
+ that pastes 800 lines back, because the orchestrator pays for those 800 lines on
82
+ *every* subsequent turn of the batch.
83
+
84
+ ## Reuse the existing findings schema
85
+
86
+ Review/audit agents that emit findings already share one pooled YAML schema
87
+ (`finding_id / title / source / category / severity / confidence /
88
+ evidence{file,lines,quote ≤8} / minimal_fix_direction / …`), parsed by
89
+ `/codexreview` and the `/new` review fan-in. The COMPACT return **reuses that
90
+ schema** — it is already terse and machine-readable. Never define a per-agent
91
+ findings shape.
92
+
93
+ ## Honest limits
94
+
95
+ This is a **prompt-level authoring convention**, best-effort — not a parser that
96
+ truncates output. It works because the agent follows its own contract, exactly
97
+ like the thinking-keyword escalation in `effort-protocol.md`. Two consequences:
98
+
99
+ 1. **Don't drop signal to hit a line count.** COMPACT means *no ceremony and no
100
+ dumps*, not lossy omission of a real BLOCKER. If the findings genuinely need 60
101
+ lines, use 60 — the win is cutting preamble/narration/echo, not truncating
102
+ substance. (This mirrors headroom's verbosity L1–L3: remove ceremony and echo
103
+ first; "caveman" L4 fragmentation is for prose, never for findings.)
104
+ 2. **Agents already structured stay as they are.** `qa-sentinel` (Return
105
+ Protocol), `visual-fidelity-verifier` (JSON schema), `code-reviewer` /
106
+ `security-reviewer` / `api-perf-cost-auditor` (verdict line + pooled YAML
107
+ findings, capped narrative) already satisfy this contract — they are the
108
+ reference exemplars, not edit targets.
109
+
110
+ ## The canonical body block
111
+
112
+ Every orchestrator-facing subagent that returns free-form analysis pastes a short
113
+ `## Return Contract` section into its body (after `## Core Responsibilities` /
114
+ `## Mission`, or after the role preamble). Keep it dense — cite this module, do
115
+ not re-explain the modes:
116
+
117
+ ```markdown
118
+ ## Return Contract
119
+
120
+ **Mode:** COMPACT (default). Your final message to the orchestrator is bounded —
121
+ headline/verdict + key points as `path:line` (no reasoning dump, no pasted code) +
122
+ `Report: <path>` when the full analysis is on disk. Persist the long form, return
123
+ the short form. FULL narrative only when the user invoked you directly for prose.
124
+ Findings schema, mode definitions, and the persist-then-summarize rule:
125
+ `framework/agents/return-contract-protocol.md`.
126
+ ```
127
+
128
+ A copy-paste starter lives at `framework/templates/agent-return-contract.snippet.md`.
129
+ When adding a new orchestrator-facing agent, paste this block — the convention is
130
+ recorded in `framework/.claude/agents/REGISTRY.md` (the agent SSOT).
@@ -0,0 +1,30 @@
1
+ <!-- contamination-scan: skip
2
+ The only token the scanner flags is "fidelity" inside the framework agent name
3
+ `visual-fidelity-verifier` (cited as an exemplar) — framework content, not leak. -->
4
+ <!--
5
+ RETURN CONTRACT HEADER — paste this into any orchestrator-facing BALDART
6
+ subagent that would otherwise return free-form analysis/audit/research prose.
7
+
8
+ Paste the `## Return Contract` block below right after `## Core Responsibilities`
9
+ / `## Mission` (or after the role preamble). It bounds the agent's FINAL MESSAGE
10
+ to the orchestrator so a long body doesn't flood the orchestrator's context —
11
+ the long form goes to disk, the return is a short headline + pointers.
12
+
13
+ Skip for agents that ALREADY have a tight return shape (qa-sentinel's Return
14
+ Protocol, visual-fidelity-verifier's JSON, the verdict+pooled-YAML reviewers) —
15
+ they are the reference exemplars, not edit targets.
16
+
17
+ Full protocol: framework/agents/return-contract-protocol.md
18
+
19
+ Keep this header DENSE (4-5 lines). Do not re-explain the modes —
20
+ cite framework/agents/return-contract-protocol.md instead.
21
+ -->
22
+
23
+ ## Return Contract
24
+
25
+ **Mode:** COMPACT (default). Your final message to the orchestrator is bounded —
26
+ headline/verdict + key points as `path:line` (no reasoning dump, no pasted code) +
27
+ `Report: <path>` when the full analysis is on disk. Persist the long form, return
28
+ the short form. FULL narrative only when the user invoked you directly for prose.
29
+ Findings schema, mode definitions, and the persist-then-summarize rule:
30
+ `framework/agents/return-contract-protocol.md`.
@@ -410,6 +410,17 @@ git:
410
410
  # worktree-isolated and respect the terminal-isolation rule.
411
411
  merge_strategy: pr
412
412
 
413
+ # Auto-deploy allowlist (since v4.59.0). Bounds what the `/new` `-auto-ship`
414
+ # autonomous mode may execute as an OUTWARD/IRREVERSIBLE action (remote schema
415
+ # deploy, env/feature-flag writes, etc.). A list of deploy targets/intents the
416
+ # orchestrator is authorized to run unattended; default empty = NOTHING
417
+ # auto-deploys (bare `-auto` ships nothing outward — outward actions defer to a
418
+ # follow-up card). This is layered ON TOP of `stack.schema_deploy_from_trunk_only`,
419
+ # which remains a hard branch floor — the allowlist authorizes WHAT may ship, the
420
+ # trunk-only invariant still constrains FROM WHERE. Free-form labels matched by
421
+ # the production-readiness gate, e.g. ["supabase-db-push", "vercel-prod"].
422
+ auto_deploy: []
423
+
413
424
  # ─── TOOLS ───────────────────────────────────────────────────────────────
414
425
  # Which AI CLI tools should the framework target on this machine?
415
426
  # Each enabled tool gets its own per-item skill symlinks pointing at the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baldart",
3
- "version": "4.58.0",
3
+ "version": "4.60.0",
4
4
  "description": "Claude Agent Framework - Reusable framework for coordinating AI agents and humans in software projects",
5
5
  "bin": {
6
6
  "baldart": "./bin/baldart.js"
@@ -663,6 +663,19 @@ async function interactivePrompts(merged, detected) {
663
663
  UI.warning('You picked `local-push` but develop appears protected on origin — pushes will be rejected. Switch back to `pr` if it fails.');
664
664
  }
665
665
 
666
+ // ---- Auto-deploy allowlist (since v4.59.0) ----------------------------
667
+ // Bounds what `/new -auto-ship` may execute as an OUTWARD action. Empty =
668
+ // nothing auto-deploys (safe default). Layered ON TOP of
669
+ // stack.schema_deploy_from_trunk_only, which stays a hard branch floor.
670
+ const currentAutoDeploy = Array.isArray(merged.git.auto_deploy) ? merged.git.auto_deploy : [];
671
+ const autoDeployAnswer = (await UI.input(
672
+ `Auto-deploy allowlist for \`/new -auto-ship\` (comma-separated deploy targets/intents; blank = none) (current: ${currentAutoDeploy.join(', ') || '—'})`,
673
+ currentAutoDeploy.join(', ')
674
+ )).trim();
675
+ merged.git.auto_deploy = autoDeployAnswer
676
+ ? autoDeployAnswer.split(',').map((s) => s.trim()).filter(Boolean)
677
+ : [];
678
+
666
679
  UI.section('Features (explicit yes/no — option A: always ask)');
667
680
  for (const flag of [
668
681
  ['has_design_system', 'Project has a documented design system?'],
@@ -464,6 +464,21 @@ async function detectState(cwd, opts = {}) {
464
464
  }
465
465
  } catch (_) { /* never block doctor on i18n probe */ }
466
466
 
467
+ // ---- Auto-deploy allowlist presence (since v4.59.0) ----------------
468
+ // Purely informational: git.auto_deploy bounds what `/new -auto-ship`
469
+ // may execute as an OUTWARD action. An empty allowlist is the SAFE
470
+ // default (nothing auto-deploys), so this is only worth noting — never
471
+ // a fix. Surfaced so a user who relies on `-auto-ship` knows whether the
472
+ // gate is open. Empty/unset = closed (the common, safe case).
473
+ state.autoDeployConfigured = false;
474
+ try {
475
+ if (config && !config.__malformed && config.git
476
+ && Array.isArray(config.git.auto_deploy) && config.git.auto_deploy.length > 0) {
477
+ state.autoDeployConfigured = true;
478
+ state.autoDeployTargets = config.git.auto_deploy.slice();
479
+ }
480
+ } catch (_) { /* never block doctor on auto_deploy probe */ }
481
+
467
482
  // ---- External-tool version currency (since v4.38.0) ----------------
468
483
  // BALDART pins none of the external tools it installs (graphifyy via pipx,
469
484
  // language servers via npm/system) — pipx/npm never auto-upgrade, so a
@@ -983,6 +998,24 @@ function planActions(state) {
983
998
  });
984
999
  }
985
1000
 
1001
+ // Auto-deploy allowlist (since v4.59.0). Advisory print-only — surfaced ONLY
1002
+ // when the allowlist is non-empty (the safe default of empty/unset opens
1003
+ // nothing, so it warrants no nag). Reminds the user that `/new -auto-ship`
1004
+ // may execute the listed OUTWARD targets unattended. Never a fix; safe in
1005
+ // --auto since run() only prints. The trunk-only branch floor still applies.
1006
+ if (state.autoDeployConfigured) {
1007
+ actions.push({
1008
+ key: 'auto-deploy-allowlist',
1009
+ label: `Auto-deploy allowlist active: ${state.autoDeployTargets.join(', ')}`,
1010
+ why: `git.auto_deploy authorizes \`/new -auto-ship\` to auto-execute these OUTWARD deploy targets unattended. Only relevant when you run with -auto-ship; otherwise nothing auto-deploys. stack.schema_deploy_from_trunk_only still constrains FROM WHERE a schema deploy may run. Empty the list in baldart.config.yml to close the gate.`,
1011
+ autoOk: true, // advisory print-only; run() does NOT mutate config
1012
+ run: () => {
1013
+ UI.info(`git.auto_deploy = [${state.autoDeployTargets.join(', ')}] — \`/new -auto-ship\` may auto-execute these targets.`);
1014
+ UI.info('Empty `git.auto_deploy` in baldart.config.yml to disable all unattended deploys.');
1015
+ },
1016
+ });
1017
+ }
1018
+
986
1019
  // External-tool version currency (since v4.38.0). One non-blocking upgrade
987
1020
  // action per managed tool confirmed behind upstream — the tool-dependency
988
1021
  // analogue of the `baldart` CLI's own UpdateNotifier. `unknown`/`current`
@@ -1283,6 +1283,9 @@ async function update(options = {}, unknownArgs = []) {
1283
1283
  .filter((k) => !(k in (cur2.features || {})));
1284
1284
  const missingPaths = Object.keys(tpl.paths || {})
1285
1285
  .filter((k) => !(k in (cur2.paths || {})));
1286
+ // `git.*` has no type filter — ALL new keys surface regardless of
1287
+ // type, so scalars (trunk_branch, merge_strategy) AND ARRAYS
1288
+ // (git.auto_deploy, the allowlist since v4.59.0) are both caught.
1286
1289
  const missingGit = Object.keys(tpl.git || {})
1287
1290
  .filter((k) => !(k in (cur2.git || {})));
1288
1291
  // Scalar top-level stack.* keys — e.g. stack.database,