baldart 4.57.0 → 4.59.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.
- package/CHANGELOG.md +32 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/coder.md +1 -1
- package/framework/.claude/skills/new/SKILL.md +63 -3
- package/framework/.claude/skills/new/references/codex-gate.md +2 -2
- package/framework/.claude/skills/new/references/commit.md +2 -2
- package/framework/.claude/skills/new/references/completeness.md +5 -3
- package/framework/.claude/skills/new/references/final-review.md +37 -7
- package/framework/.claude/skills/new/references/implement.md +2 -2
- package/framework/.claude/skills/new/references/merge-cleanup.md +18 -1
- package/framework/.claude/skills/new/references/production-readiness.md +17 -2
- package/framework/.claude/skills/new/references/review-cycle.md +20 -7
- package/framework/.claude/skills/new/references/setup.md +11 -5
- package/framework/.claude/skills/new/references/team-mode.md +9 -7
- package/framework/.claude/skills/simplify/SKILL.md +7 -3
- package/framework/.claude/workflows/new-card-review.js +76 -19
- package/framework/.claude/workflows/new-final-review.js +212 -15
- package/framework/templates/baldart.config.template.yml +11 -0
- package/package.json +1 -1
- package/src/commands/configure.js +13 -0
- package/src/commands/doctor.js +33 -0
- 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],
|
|
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 —
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
|
358
|
-
{ label:
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
@@ -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?'],
|
package/src/commands/doctor.js
CHANGED
|
@@ -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`
|
package/src/commands/update.js
CHANGED
|
@@ -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,
|