baldart 4.89.2 → 4.92.1
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 +87 -0
- package/VERSION +1 -1
- package/framework/.claude/agents/api-perf-cost-auditor.md +9 -0
- package/framework/.claude/agents/code-reviewer.md +9 -0
- package/framework/.claude/agents/codebase-architect.md +6 -0
- package/framework/.claude/agents/coder.md +1 -1
- package/framework/.claude/agents/markup-fidelity-verifier.md +24 -1
- package/framework/.claude/agents/qa-sentinel.md +9 -0
- package/framework/.claude/agents/security-reviewer.md +9 -0
- package/framework/.claude/agents/ui-expert.md +49 -0
- package/framework/.claude/agents/visual-fidelity-verifier.md +16 -12
- package/framework/.claude/skills/e2e-review/CHANGELOG.md +59 -0
- package/framework/.claude/skills/e2e-review/SKILL.md +187 -26
- package/framework/.claude/skills/new/CHANGELOG.md +29 -0
- package/framework/.claude/skills/new/SKILL.md +44 -2
- package/framework/.claude/skills/new/references/completeness.md +1 -0
- package/framework/.claude/skills/new/references/implement.md +19 -3
- package/framework/.claude/skills/new/references/review-cycle.md +23 -1
- package/framework/.claude/skills/new/references/team-mode.md +4 -0
- package/framework/.claude/skills/new/scripts/verify-item.sh +54 -0
- package/framework/.claude/skills/new2/CHANGELOG.md +46 -0
- package/framework/.claude/skills/new2/SKILL.md +58 -10
- package/framework/.claude/skills/ui-implement/CHANGELOG.md +20 -0
- package/framework/.claude/skills/ui-implement/SKILL.md +37 -7
- package/framework/.claude/skills/ui-implement/references/integration.md +12 -3
- package/framework/.claude/skills/ui-implement/scripts/check-bindings.mjs +103 -0
- package/framework/.claude/workflows/new-card-review.js +19 -5
- package/framework/.claude/workflows/new-final-review.js +24 -9
- package/framework/.claude/workflows/new2.js +84 -5
- package/framework/agents/code-search-protocol.md +24 -0
- package/framework/agents/design-system-protocol.md +27 -0
- package/framework/docs/WORKFLOWS.md +9 -4
- package/framework/scripts/structural-compare.mjs +183 -0
- package/package.json +1 -1
|
@@ -203,6 +203,12 @@ const PREFLIGHT_SCHEMA = {
|
|
|
203
203
|
// F-016 — ACs whose only implementation file is outside the card MAY-EDIT,
|
|
204
204
|
// pre-classified deferred-by-policy (never routed to resolve).
|
|
205
205
|
policyDeferredACs: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
206
|
+
// T1.A (v4.90.0) — the mockup's code-form sources, so the workflow can run the
|
|
207
|
+
// route-free structural fidelity lane (markup-fidelity-verifier) without
|
|
208
|
+
// re-reading the card. Empty string when absent.
|
|
209
|
+
designSrcDir: { type: 'string', description: "abs path of links.design_src / mockups/_src dir ('' when absent)" },
|
|
210
|
+
designHtml: { type: 'string', description: "abs path of an .html links.design mockup ('' when absent)" },
|
|
211
|
+
hasBindings: { type: 'boolean', description: 'card carries a non-empty component_bindings map' },
|
|
206
212
|
},
|
|
207
213
|
},
|
|
208
214
|
},
|
|
@@ -277,7 +283,7 @@ try {
|
|
|
277
283
|
preflight = await agentSafe(
|
|
278
284
|
`You are the deterministic PRE-FLIGHT for an autonomous /new batch (variant new2). Follow ${REF}/setup.md (Phase 0 + Pre-flight) and ${REF}/implement.md (Phase 1 depends-on gate) for the SEMANTICS, but replace EVERY AskUserQuestion with the deterministic policy below. You run all git/bash yourself (the workflow cannot).\n\n` +
|
|
279
285
|
`${projectBrief}\n\nCards in batch (Read each YAML):\n${cardPaths.join('\n')}\nCard IDs: ${cardIds.join(' ')}\n\n` +
|
|
280
|
-
`Create/maintain the recovery tracker at /
|
|
286
|
+
`Create/maintain the recovery tracker at "$(git -C "${MAIN}" rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md" (mkdir -p its dir first; per setup.md § Context Tracking — DURABLE location, v4.91.0: /tmp is volatile and a paused/overnight run lost it — B4).\n\n` +
|
|
281
287
|
`ROLE BOUNDARY (specialization integrity): you are the OPS/GIT agent. You NEVER edit source or doc files — any needed content change belongs to the coder specialist; report it instead.\n\n` +
|
|
282
288
|
`DETERMINISTIC GATE POLICIES (NO user prompts):\n` +
|
|
283
289
|
`• G1 dirty-tree (main repo ${MAIN}): partition framework-managed noise exactly as setup.md step 3 ($METRICS=${METRICS}, .baldart/generated|state.json|skill-conflicts.json — NOT overlays/). Genuine user work → auto-stash 'baldart-new2-${firstCard}' (main checkout) and record the label. Never commit/abort/prompt.\n` +
|
|
@@ -286,7 +292,7 @@ try {
|
|
|
286
292
|
g3Bullet +
|
|
287
293
|
`• G4 card-field validation (setup.md 1b/1c, card-schema.md consumer contract): card missing acceptance_criteria/files_likely_touched/scope → EXCLUDE (excluded[] + reason). \`requirements\` is CONDITIONAL, not an auto-exclude: if it is missing BUT acceptance_criteria + scope are both present and non-empty, FAITHFULLY DERIVE it (a concrete restatement/decomposition of the existing AC + scope — faithful, never inventing new scope) and WRITE it back to the card YAML on disk in ${MAIN}/${paths.backlog_dir || 'backlog'} (F-040: main repo, not the worktree copy; the card artifact is NOT a source/doc file, so this is allowed despite the role boundary), logging \`[BACKFILL] <id>: requirements=<N items, AC+scope-derived>\`. Only EXCLUDE for missing requirements when acceptance_criteria OR scope is ALSO empty (a genuinely thin card). Never HALT for one bad card.\n` +
|
|
288
294
|
`• G5 depends-on: a card whose depends_on names a non-DONE card NOT in this batch → EXCLUDE it AND every in-batch card that transitively depends on it.\n` +
|
|
289
|
-
`• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card has a non-empty links.design OR links.design_src; drives the mockup-first build override — implement.md §6a), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
|
|
295
|
+
`• cardGraph (REQUIRED, F-021): for every runnable card return { id, dependsOn:[IN-BATCH deps only], ownerAgent (the card's owner_agent; G25 unknown→'coder'), hasMockup (BOOLEAN — true IFF the card has a non-empty links.design OR links.design_src; drives the mockup-first build override — implement.md §6a), designSrcDir (ABS path of links.design_src / the PRD mockups/_src dir; '' when absent), designHtml (ABS path when links.design is an .html file; '' otherwise), hasBindings (BOOLEAN — non-empty component_bindings), reviewProfile (the card's review_profile; default 'balanced'), policyDeferredACs, alreadyCommitted, alreadyCommittedSha, isEpic (implement.md §6b epic guard: id ends '-00' OR filename ends '-epic.yml' OR group.is_epic:true OR review_profile 'skip' with no requirements), filesLikelyTouched (verbatim from the YAML) }.\n` +
|
|
290
296
|
`• B1/F-026 idempotency (per card, AFTER the worktree exists): set alreadyCommitted:true (+ alreadyCommittedSha) IFF ALL hold: (a) a commit referencing the card id exists in ${TRUNK}..HEAD of the worktree; (b) the card's validation_commands re-run GREEN right now; (c) NO open follow-up card for it exists in ${paths.backlog_dir || 'backlog'}. On a FRESH worktree ${TRUNK}..HEAD is empty → all false, zero extra work.\n` +
|
|
291
297
|
`• F-016 AC↔ownership consistency: for each acceptance_criterion, derive the file(s) it requires editing. If those files are NOT a subset of the card's MAY-EDIT/files_likely_touched → add the AC to policyDeferredACs:[{n,text,owningCard|owningFile,reason}] (it will become ONE follow-up, never a resolve). Do the same for any AC whose remedy is an owner-gated infra action (remote db push / deploy / secret / DNS).\n` +
|
|
292
298
|
(migrationApplied
|
|
@@ -594,16 +600,21 @@ async function runCard(cardId, cardPath) {
|
|
|
594
600
|
const phase1Brief = architectOk
|
|
595
601
|
? `Phase 1 already ran as specialist agents. READ the architecture baseline at ${baselinePath} BEFORE writing any code — it is your codebase context; do NOT redo the exploration.${auditCorrections.length ? `\nPlan-audit corrections (apply them as amendments to this briefing — do NOT modify the backlog YAML):\n${auditCorrections.map((c, i) => ` ${i + 1}. ${c}`).join('\n')}` : ''}`
|
|
596
602
|
: `Phase 1 fallback: the architect specialist was unavailable — do the Phase 1 claim+architect exploration yourself per ${REF}/implement.md and persist the baseline to ${baselinePath} before coding.`
|
|
603
|
+
// T1.A (v4.90.0) — mockup cards: binding-compliance is verified deterministically by the
|
|
604
|
+
// check-bindings.mjs script (ui-implement Step 5b is the SSOT; this is the new2 parallel).
|
|
605
|
+
const bindingBit = node.hasMockup && node.hasBindings
|
|
606
|
+
? `\nBINDING GATE (deterministic — ui-implement Step 5b parallel): after building, run \`node "$(ls .claude/skills/ui-implement/scripts/check-bindings.mjs .framework/framework/.claude/skills/ui-implement/scripts/check-bindings.mjs 2>/dev/null | head -1)" --bindings-json '<the card component_bindings as JSON>' --files <your touched implementation files>\`. Exit 3 = a reuse/reuse-variant binding is NOT used in the code: the map is AUTHORITATIVE — fix it before returning (one pass). Include the script's one-line JSON output as bindingCheck in your return.\n`
|
|
607
|
+
: ''
|
|
597
608
|
let impl
|
|
598
609
|
try {
|
|
599
610
|
impl = await agentSafe(
|
|
600
|
-
`Implement card ${cardId} per ${REF}/implement.md Phase 2 — you ARE the owner_agent '${ownerAgent}' — and ${REF}/completeness.md (Phase 2.5 + 2.5b AC-closure ledger). Run all gates/bash yourself.\n\n${phase1Brief}\n\n${cardBrief}\n\n` +
|
|
611
|
+
`Implement card ${cardId} per ${REF}/implement.md Phase 2 — you ARE the owner_agent '${ownerAgent}' — and ${REF}/completeness.md (Phase 2.5 + 2.5b AC-closure ledger). Run all gates/bash yourself.\n${bindingBit}\n${phase1Brief}\n\n${cardBrief}\n\n` +
|
|
601
612
|
`POLICIES: G26 Phase-2 lint/tsc/test/build failing after the module's retry cap → buildBlocked:true + blockedGate. Build the AC Closure Ledger (one row per AC: implemented|unmet|policy-deferred). DO NOT silently defer; report unmet rows (excluding policy-deferred). Persist the diff to /tmp/diff-${cardId}.txt.\n\n` +
|
|
602
613
|
`E4 OWNERSHIP RECONCILE (implement.md §11b — do this BEFORE returning): the card's MAY-EDIT includes files_likely_touched ∪ paths NAMED EXPLICITLY in this card's acceptance_criteria/definition_of_done (e.g. an ADR the DoD says to update, the data-model / ER doc for a schema change). Editing THOSE is in-scope. For any OTHER dirty file outside MAY-EDIT (another card's file, or unrelated): \`git checkout -- <file>\` to revert it (NEVER leave it orphaned), list it in revertedOutOfOwnership. Set fileDiffViolation:true ONLY if such an edit genuinely could not be reverted (then say why in note) — it is no longer a silent label.\n\n` +
|
|
603
614
|
`Return: { epic, buildBlocked, blockedGate, unmetACs:[{n,text}], scopeFiles, mayEditPaths, revertedOutOfOwnership:[paths], fileDiffViolation, note }`,
|
|
604
615
|
{ label: `implement:${cardId}`, phase: 'Implement', agentType: ownerAgent,
|
|
605
616
|
schema: { type: 'object', required: ['epic', 'buildBlocked', 'unmetACs', 'scopeFiles'], additionalProperties: true,
|
|
606
|
-
properties: { epic: { type: 'boolean' }, buildBlocked: { type: 'boolean' }, blockedGate: { type: 'string' }, unmetACs: { type: 'array', items: { type: 'object', additionalProperties: true } }, scopeFiles: { type: 'array', items: { type: 'string' } }, mayEditPaths: { type: 'array', items: { type: 'string' } }, revertedOutOfOwnership: { type: 'array', items: { type: 'string' } }, fileDiffViolation: { type: 'boolean' }, note: { type: 'string' } } } }
|
|
617
|
+
properties: { epic: { type: 'boolean' }, buildBlocked: { type: 'boolean' }, blockedGate: { type: 'string' }, unmetACs: { type: 'array', items: { type: 'object', additionalProperties: true } }, scopeFiles: { type: 'array', items: { type: 'string' } }, mayEditPaths: { type: 'array', items: { type: 'string' } }, revertedOutOfOwnership: { type: 'array', items: { type: 'string' } }, fileDiffViolation: { type: 'boolean' }, note: { type: 'string' }, bindingCheck: { type: 'object', additionalProperties: true, description: 'check-bindings.mjs one-line JSON (mockup+bindings cards only)' } } } }
|
|
607
618
|
)
|
|
608
619
|
} catch (e) {
|
|
609
620
|
if (e && e.transientExhausted) { noteDegraded('outage'); return { card: cardId, status: 'pending', gates } }
|
|
@@ -642,6 +653,74 @@ async function runCard(cardId, cardPath) {
|
|
|
642
653
|
if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
|
|
643
654
|
}
|
|
644
655
|
|
|
656
|
+
// --- R1-F2 (v4.92.1): the binding gate result is CONSUMED, not write-only. The agent
|
|
657
|
+
// was already told to run check-bindings.mjs and fix before returning; here the workflow
|
|
658
|
+
// verifies the report: violations survive the agent's own pass → resolve (ui domain);
|
|
659
|
+
// report absent on a bindings card → fail-open but LEDGERED (audit trail, never silent).
|
|
660
|
+
if (node.hasMockup && node.hasBindings) {
|
|
661
|
+
const bc = impl && impl.bindingCheck
|
|
662
|
+
const bcViol = (bc && Array.isArray(bc.violations)) ? bc.violations : []
|
|
663
|
+
if (!bc) g('binding-gate', 'NOT-REPORTED', 'agent returned no bindingCheck — fail-open (script may be absent on older installs), recorded for audit')
|
|
664
|
+
else if (bcViol.length) {
|
|
665
|
+
g('binding-gate', 'VIOLATIONS', bcViol.map((v) => v.component).join(', '))
|
|
666
|
+
for (const v of bcViol) {
|
|
667
|
+
const r = await resolve('blocker', cardId, `binding-not-honored: region "${v.region || ''}" is bound action:${v.action || 'reuse'} → ${v.component}, but the component appears in NO touched implementation file — the binding map is authoritative (card-schema.md § component_bindings)`, { mayEditPaths: mayEdit, scopeFiles, domain: 'ui' })
|
|
668
|
+
if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
|
|
669
|
+
}
|
|
670
|
+
} else g('binding-gate', 'OK', `${(bc && bc.honored) || 0}/${(bc && bc.checked) || 0} bindings honored`)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// --- T1.A (v4.90.0): route-free structural fidelity lane — the new2 parallel of
|
|
674
|
+
// /e2e-review Phase 2.7. new2 has NO browser lanes (visual/quality verify runs only on
|
|
675
|
+
// the /new path), so the code-form structural check is its ONE per-card fidelity lane.
|
|
676
|
+
// Running it is a Lane-Ledger obligation (design-system-protocol § 6. Verification-Lane
|
|
677
|
+
// Ledger): a mockup card with a code-form source may NOT complete with zero fidelity
|
|
678
|
+
// verification (the FEAT-0064 under-run). Verifier unavailable → verification-coverage-gap
|
|
679
|
+
// residual + the card cannot be auto-DONE — an under-run blocks, never passes silently.
|
|
680
|
+
if (node.hasMockup && (node.designSrcDir || node.designHtml)) {
|
|
681
|
+
const structSrc = node.designSrcDir
|
|
682
|
+
? { level: 'design_src', design_src_dir: node.designSrcDir }
|
|
683
|
+
: { level: 'html', mockup_path: node.designHtml }
|
|
684
|
+
const spawnMfv = () => agentSafe(
|
|
685
|
+
`Compare the implementation's code structure against the mockup's code structure and return JSON per your output schema (route-free — no browser, no dev server; cd into ${sharedCtx.worktreePath}).\n\nInput payload:\n${JSON.stringify({ card_id: cardId, route: '(card-level)', mockup_source: structSrc, implementation_files: scopeFiles, tolerance: 'strict' })}\n\nReturn STRICT JSON only: { status, findings:[{category,severity,description,expected,actual,fix_hint}] }`,
|
|
686
|
+
{ label: `fidelity:${cardId}:structural`, phase: 'Implement', agentType: 'markup-fidelity-verifier',
|
|
687
|
+
schema: { type: 'object', required: ['findings'], additionalProperties: true, properties: { status: { type: 'string' }, findings: { type: 'array', items: { type: 'object', additionalProperties: true } } } } }
|
|
688
|
+
)
|
|
689
|
+
let mfv = null
|
|
690
|
+
try { mfv = await spawnMfv() } catch (e) { if (e && e.transientExhausted) noteDegraded('outage') }
|
|
691
|
+
// R1-F1 (v4.92.1): a verifier that RETURNS but self-reports `status:"skipped"`
|
|
692
|
+
// (no_structural_ground_truth — unreadable mockup / no layout-bearing impl) is an
|
|
693
|
+
// under-run exactly like an unavailable verifier, NEVER a PASS.
|
|
694
|
+
const mfvSkipped = mfv && /skip/i.test(String(mfv.status || ''))
|
|
695
|
+
if (!mfv || mfvSkipped) {
|
|
696
|
+
g('fidelity-structural', 'UNDERRUN', `planned lane did not verify (${!mfv ? 'verifier unavailable' : 'verifier skipped: ' + String((mfv && mfv.reason) || 'no_structural_ground_truth')}) — verification-coverage-gap, card cannot auto-DONE`)
|
|
697
|
+
deferredOpen = true; deferredClasses.add('unresolved')
|
|
698
|
+
residuals.push({ card: cardId, kind: 'verification-coverage-gap', evidence: `structural fidelity lane planned but did not verify (${!mfv ? 'markup-fidelity-verifier unavailable' : 'verifier skipped'})`, materialized: false })
|
|
699
|
+
} else {
|
|
700
|
+
const gatingOf = (x) => ((x && x.findings) || []).filter((f) => /critical|major/i.test(String(f.severity || '')))
|
|
701
|
+
const gating = gatingOf(mfv)
|
|
702
|
+
g('fidelity-structural', gating.length ? 'BLOCKED' : 'PASS', gating.length ? `${gating.length} gating structural finding(s)` : `ran — ${(mfv.findings || []).length} finding(s), none gating`)
|
|
703
|
+
let anyResolved = false
|
|
704
|
+
for (const f of gating) {
|
|
705
|
+
const r = await resolve('blocker', cardId, `structural fidelity (${f.category || 'layout'}): ${f.description || ''} — expected: ${f.expected || '(see mockup)'}`, { mayEditPaths: mayEdit, scopeFiles, domain: 'ui' })
|
|
706
|
+
if (r.status !== 'resolved') { deferredOpen = true; deferredClasses.add(r.deferralClass || 'unresolved') }
|
|
707
|
+
else anyResolved = true
|
|
708
|
+
}
|
|
709
|
+
// R1-F3 (v4.92.1): a resolved structural fix is RE-VERIFIED by the deterministic
|
|
710
|
+
// verifier once (mirror of /e2e-review Phase 5b step 2 — a fixer's self-report is
|
|
711
|
+
// not the verifier's confirmation). Single re-check, no loop: still-gating after
|
|
712
|
+
// the re-run → deferral (the follow-up carries it), never a second fix pass here.
|
|
713
|
+
if (anyResolved) {
|
|
714
|
+
let mfvRe = null
|
|
715
|
+
try { mfvRe = await spawnMfv() } catch (e) { if (e && e.transientExhausted) noteDegraded('outage') }
|
|
716
|
+
const reGating = mfvRe && !/skip/i.test(String(mfvRe.status || '')) ? gatingOf(mfvRe) : null
|
|
717
|
+
if (reGating === null) { g('fidelity-structural-reverify', 'UNDERRUN', 're-verify did not run — treated as unresolved'); deferredOpen = true; deferredClasses.add('unresolved') }
|
|
718
|
+
else if (reGating.length) { g('fidelity-structural-reverify', 'STILL-BLOCKED', `${reGating.length} gating finding(s) survive the fix — deferred`); deferredOpen = true; deferredClasses.add('unresolved'); residuals.push({ card: cardId, kind: 'blocker', domain: 'ui', evidence: `structural fidelity still failing after fix: ${reGating.map((f) => f.category).join(', ')}`, materialized: false }) }
|
|
719
|
+
else g('fidelity-structural-reverify', 'PASS', 'fix confirmed by the verifier')
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
645
724
|
// --- Review fan-out (F-024/F-025): specialized agents, trimmed by review_profile. ---
|
|
646
725
|
// G5 — scopeFiles tokens alone miss a security card whose files don't carry them. Also
|
|
647
726
|
// trigger security-reviewer on the card's RAW owner_agent and its brief (title/requirements).
|
|
@@ -1106,7 +1185,7 @@ if (!committed.length) {
|
|
|
1106
1185
|
} else {
|
|
1107
1186
|
try {
|
|
1108
1187
|
mergeResult = await agentSafe(
|
|
1109
|
-
`Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md: Phase 6 via the DETERMINISTIC script merge-worktree.sh, then Phase 6b status reconciliation + Phase 6c hygiene. Run git yourself.\n\n${projectBrief}\nWorktree: ${sharedCtx.worktreePath}\nBranch: ${sharedCtx.branch}\nmerge_strategy: ${mergeStrategy}\nCommitted cards: ${committed.map((r) => r.card).join(' ')}\nPhase-0 stash to restore (if any): see /
|
|
1188
|
+
`Auto-merge the batch worktree to ${TRUNK} per ${REF}/merge-cleanup.md: Phase 6 via the DETERMINISTIC script merge-worktree.sh, then Phase 6b status reconciliation + Phase 6c hygiene. Run git yourself.\n\n${projectBrief}\nWorktree: ${sharedCtx.worktreePath}\nBranch: ${sharedCtx.branch}\nmerge_strategy: ${mergeStrategy}\nCommitted cards: ${committed.map((r) => r.card).join(' ')}\nPhase-0 stash to restore (if any): see "$(git -C "${MAIN}" rev-parse --git-common-dir)/baldart/run/batch-tracker-${firstCard}.md".\n\n` +
|
|
1110
1189
|
`PHASE 6 — run the SSOT script (the merge analogue of setup-worktree.sh; NOT a hand-rolled merge, NOT /mw inline):\n` +
|
|
1111
1190
|
` MERGE_SH="$(ls .framework/framework/.claude/skills/worktree-manager/scripts/merge-worktree.sh .claude/skills/worktree-manager/scripts/merge-worktree.sh 2>/dev/null | head -1)"\n` +
|
|
1112
1191
|
` bash "$MERGE_SH" --worktree ${sharedCtx.worktreePath} --manifest /tmp/new2-merge-status-${firstCard}.txt --skip-checks\n` +
|
|
@@ -91,6 +91,30 @@ Before picking a tier, classify the query:
|
|
|
91
91
|
~8–12M tokens with three coders independently re-discovering one NUL byte in a
|
|
92
92
|
single multi-hundred-KB `data.ts`.)
|
|
93
93
|
|
|
94
|
+
## Large-file read discipline (hot files)
|
|
95
|
+
|
|
96
|
+
A **hot file** is any source file **> 800 lines** that multiple agents touch in the
|
|
97
|
+
same run (a form client, a data layer, a locale dictionary). Forensics on real
|
|
98
|
+
`/new` batches showed hot-file FULL re-reads are the dominant repeated-read cost —
|
|
99
|
+
NOT the framework modules (read 1× each): one 2110-line client component (~29k
|
|
100
|
+
tokens per full view) was read **28 times** across architect → coder → fixer →
|
|
101
|
+
verifier spawns in a single run, because each fresh context re-read it whole.
|
|
102
|
+
|
|
103
|
+
Rules (every agent, every runtime):
|
|
104
|
+
|
|
105
|
+
1. **Map once, range-read after.** The `codebase-architect` baseline carries a
|
|
106
|
+
`## Hot-File Map` for every in-scope file > 800 lines (per-symbol line ranges —
|
|
107
|
+
see `codebase-architect.md` § terse contract). When you need a hot file and a
|
|
108
|
+
map exists, `Read` the **target range only** (offset/limit around the symbol),
|
|
109
|
+
never the whole file.
|
|
110
|
+
2. **No map → locate first, then range-read.** `rg -n "<symbol>"` (or LSP
|
|
111
|
+
go-to-definition when the flag is on) to find the line, then `Read` a bounded
|
|
112
|
+
window around it. A whole-file `Read` of a hot file is justified ONLY when the
|
|
113
|
+
edit genuinely spans dispersed regions of it — say so in one line when you do it.
|
|
114
|
+
3. **Briefs pass paths, not bodies.** An orchestrator briefing an agent passes the
|
|
115
|
+
baseline **path** (e.g. `/tmp/arch-baseline-<CARD-ID>.md`), never its inlined
|
|
116
|
+
content — and never inlines a hot file into a prompt.
|
|
117
|
+
|
|
94
118
|
## Fallback rules
|
|
95
119
|
|
|
96
120
|
- LSP tool not loaded / plugin missing → silently fall back to Grep with the
|
|
@@ -654,6 +654,33 @@ card's `owner_agent: coder` (correct for its logic/review ownership) sent the UI
|
|
|
654
654
|
`coder`. `owner_agent` still governs the review/ownership semantics; it just does not pick
|
|
655
655
|
the build agent when a mockup is present.
|
|
656
656
|
|
|
657
|
+
### 6. Verification-Lane Ledger — planned vs executed (no silent lane under-run)
|
|
658
|
+
|
|
659
|
+
Checks 1–4 above each guard ONE lane. This check guards the **whole matrix**: the run
|
|
660
|
+
that silently executes *fewer lanes than it planned*. The driving lesson (FEAT-0064,
|
|
661
|
+
post-mortem of run `a73a52d0`): a mockup card went through "fidelity review" in which
|
|
662
|
+
**only** the structural verifier ever spawned — no `visual-fidelity-verifier`, no
|
|
663
|
+
`ui-quality-critic` — and nothing in the verdict recorded the under-run; the escaped
|
|
664
|
+
polish surfaced later as a hand-worked bug card. A lane that is planned and then not
|
|
665
|
+
executed is invisible to per-lane guards, so the ledger is reconciled globally:
|
|
666
|
+
|
|
667
|
+
- **At plan time** the verifying orchestrator (`/e2e-review` Phase 2; the degraded
|
|
668
|
+
direct-trio path in `/ui-implement`; the `new2.js` structural lane) derives, per
|
|
669
|
+
route/surface, the set of REQUIRED lanes — `structural` (when a code-form mockup
|
|
670
|
+
exists), `visual` per in-scope viewport, `quality` (when a quality oracle exists),
|
|
671
|
+
`functional` (when scenarios exist) — and persists it.
|
|
672
|
+
- **At verdict time** it reconciles planned vs executed. Every planned lane must be
|
|
673
|
+
`ran` or `skipped(<authorized reason>)` where the authorized reasons are ONLY the
|
|
674
|
+
ones this protocol enumerates (pixel-diff pre-filter pass, `render_unavailable` with
|
|
675
|
+
structural covered, `no_quality_oracle`, harness-render fidelity guard,
|
|
676
|
+
desktop-only route for the responsive pass). A planned lane that is simply
|
|
677
|
+
**`not_run`** synthesizes a **`verification-coverage-gap`** (Critical) — the matrix
|
|
678
|
+
twin of `coverage-gap`: not fixable by an agent, bypasses the self-heal loop, maps
|
|
679
|
+
to `blocked`. A verdict of `passed` with an unreconciled lane ledger is a protocol
|
|
680
|
+
violation, and no self-served "accepted (harness limit)" style verdict exists —
|
|
681
|
+
accepting an un-run lane requires the explicit override path (human reason) or the
|
|
682
|
+
caller's escalation, never the verifier's own discretion.
|
|
683
|
+
|
|
657
684
|
### Gating philosophy (calibration)
|
|
658
685
|
|
|
659
686
|
Vision-model reliability research (*MLLM as a UI Judge*, 2025) shows multimodal judges are
|
|
@@ -15,12 +15,17 @@ workflows are unavailable behaves exactly as before.
|
|
|
15
15
|
| :--- | :--- | :--- |
|
|
16
16
|
| `new-final-review` | `/new` Final Review (Step F.1.5) | Runs the read-only cross-batch review fan-out — architecture baseline + Codex ‖ doc-reviewer ‖ api-perf-cost-auditor ‖ qa-sentinel — then adversarially verifies low-confidence findings and returns them classified. Applies no fixes (the skill owns fix application + user gates). **v4.17.1+:** Codex availability is resolved by a **deterministic pre-flight glob + background poll** (no false negatives from a synchronous-run timeout); a **single-card batch** slims the breadth finders **per-finder, coverage-gated** (caller-supplied `slimDoc`/`slimApi`): it drops a duplicate Claude reviewer only when that reviewer already ran per-card on the same card — `new2` drops both doc + api (it runs both per-card, relevance-gated); classic `/new` drops only `doc-reviewer` (when Phase 3 ran it) and **keeps `api-perf-cost-auditor`** (deferred-to-final by design — the final is its only run). Codex + qa gates always run. Backward-compatible: callers without the flags get the full reviewer set. |
|
|
17
17
|
| `new-card-review` (v4.34.0) | `/new` per-card review cluster — sequential (`review-cycle.md` § Phase 2.5x) **and** team-mode (`team-mode.md` D.1.6) | Hosts the **per-wave** review-cluster OUTSIDE the orchestrator context — the biggest prefix-growth source on long epics. Takes **1..N co-located cards** (1 = sequential per-card; N = a team-mode group, so it runs **once per wave, not per card**) and fans out the finders per card: Simplify + cross-model Codex (agent-launched binary, `code-reviewer` fallback) + qa-sentinel (group max tier) + security-reviewer (high-risk only), each specialist FP-checking its own findings. Then **ONE `coder` applies all VERIFIED code/perf/security/simplify findings in a single pass** (files disjoint by ownership) and re-verifies lint/tsc/build. Returns a compact `{perCard:{fixesApplied,residual}, gateTable, summary}` — the minority `residual` (doc, needs-manual, scope, unconverged) the skill resolves with the right specialist / user gate. **Doc-review and E2E stay in the skill** (doc is write-mode + must see final code; E2E is human-gated + nests a skill); **api-perf is deferred to the Final FULL gate**. Maps to `review-cycle.md` Phase 2.55+3.5+3.7. |
|
|
18
|
-
| `new2` (v4.17.2) | `/new2` skill (the whole batch) | **
|
|
18
|
+
| `new2` (v4.17.2, promoted v4.90.0) | `/new2` skill (the whole batch) **+ `/new -auto` via its delegation gate** | **Workflow-hosted batch engine of `/new`.** Hosts the ENTIRE batch in the background runtime so subagent output never enters the main context. A **dependency-gated DAG scheduler** runs a card only when all in-batch deps are *committed* (and blocks transitive dependents of a failed dep instead of routing them to resolve); each card uses its **owner_agent** + a **specialized review fan-out** (not general-purpose); the worktree is kept **atomic per card** (rollback-to-HEAD on failure); transient API errors are retried and a sustained **outage degrades cleanly** (`degraded` return + durable resume via the skill); a **run ledger** dedups resolves and records accepted deferrals (no re-routing loop); the **merge is integrity-gated** (never force-DONE, never `git add` unreviewed code, never merge an incomplete/degraded batch); the commit step runs on **Haiku** while **follow-up cards are written by `prd-card-writer`**; telemetry carries real **cost** (`total_tokens` via `budget.spent()`, `agent_count`, skill-stamped `wall_clock_s`) + `degraded`. **v4.17.2:** the pre-flight **G3 cross-card Codex check is deterministic** (glob-first + background poll, skipped on single-card batches; `codex_resolved` in telemetry); a non-transient card crash is terminal-with-residual (no orphaned self-healing). Agents Read `/new`'s reference modules for semantics. |
|
|
19
19
|
| `new2-resolve` (v4.17.2) | `new2` (self-healing) | Resolution pass for any gate that would otherwise need a human (`ac-unmet · blocker · qa-fail · e2e-blocked · merge-blocker · scope-expansion`). A **terminal short-circuit** skips the costly multi-attempt when the problem is impossible-by-definition (`out-of-ownership` verified in JS; other terminal reasons ratified by a judge); a **MANDATORY adversarial judge** cross-checks every `verified` claim — the judge independently greps the files and the workflow verifies **at least one** falls inside MAY-EDIT (`.some()`, so listing adjacent changed files is not mistaken for fabrication); accepts a **batched `findings` list** (one resolve per fix-area). **The domain is normalized** (freeform `documentation`→`doc`, …) before routing the **fixer** (doc→doc-reviewer, ui→ui-expert, security→security-reviewer, else coder) and judge, and a **doc finding gets doc-tree MAY-EDIT** (not the card's code scope); the 3-angle Tier-2 fan-out is reserved for code domains (single retry for doc/test). Follow-ups are written by **`prd-card-writer`**, offline-safe (deferred to the skill if no agent can write). |
|
|
20
20
|
|
|
21
|
-
> **`new2` is
|
|
22
|
-
>
|
|
23
|
-
>
|
|
21
|
+
> **`new2` is the engine, `/new` is the SSOT.** Since v4.90.0 `/new -auto`
|
|
22
|
+
> delegates the whole batch here by default (AUTONOMOUS OFF-CONTEXT DELEGATION
|
|
23
|
+
> GATE — measured driver: 53–59% of an inline `-auto` run's billed tokens is the
|
|
24
|
+
> orchestrator's own turn-tax; escape hatch `-inline`; `-auto-ship`, declared
|
|
25
|
+
> migrations, interactive runs and every Codex run stay on the classic inline
|
|
26
|
+
> path). `/new` remains the semantic SSOT and the cross-tool default — `new2`'s
|
|
27
|
+
> agents Read `/new`'s reference modules; the gate map in `new2.js` encodes only
|
|
28
|
+
> orchestration shape + deterministic policy. `new2` is Claude-only and its
|
|
24
29
|
> skill HALTs with a pointer to `/new` when the `Workflow` tool is unavailable.
|
|
25
30
|
|
|
26
31
|
## How distribution works
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// structural-compare.mjs — deterministic STRUCTURAL pre-gate for the mockup→code
|
|
3
|
+
// fidelity chain (T1.B, framework v4.90.0). Zero-dep, heuristic, CONSERVATIVE.
|
|
4
|
+
//
|
|
5
|
+
// Compares a code-form mockup (an .html file, or a design_src dir of .jsx/.tsx/.css)
|
|
6
|
+
// against the implementation files on a small set of CLEAR structural signals:
|
|
7
|
+
// - landmark census (header/nav/aside/main/footer present in mockup, absent in impl)
|
|
8
|
+
// - max grid column count (multi-column mockup built single-column — the FEAT-0061 class)
|
|
9
|
+
// - responsive signals (@media breakpoints / Tailwind responsive prefixes present in
|
|
10
|
+
// the mockup, entirely absent in the impl)
|
|
11
|
+
//
|
|
12
|
+
// SEMANTICS — signals, not verdicts. The implementation may COMPOSE primitives (a
|
|
13
|
+
// `<SplitView>` that owns the grid, a `<TopBar>` that renders the <header>), and this
|
|
14
|
+
// script only sees the card-touched files — so a "divergence" here is a deterministic
|
|
15
|
+
// SIGNAL with a per-finding `confidence`, never a standalone verdict:
|
|
16
|
+
// - in-build (ui-expert self-check): the builder judges each signal — it knows which
|
|
17
|
+
// primitives it composed — and fixes what is real BEFORE returning.
|
|
18
|
+
// - in Phase 2.7 (/e2e-review): the signals are passed to `markup-fidelity-verifier`
|
|
19
|
+
// as `deterministic_signals` focus hints; the verifier confirms or dismisses each
|
|
20
|
+
// (it can read the composed primitives). The verifier spawn is NEVER skipped on a
|
|
21
|
+
// "match" — a regex match is necessary, not sufficient.
|
|
22
|
+
//
|
|
23
|
+
// Contract:
|
|
24
|
+
// node structural-compare.mjs --mockup <file-or-dir> --impl <f1> <f2> ...
|
|
25
|
+
// → one JSON line: { status: "match" | "divergence" | "skipped",
|
|
26
|
+
// mockup_signature, impl_signature,
|
|
27
|
+
// findings:[{category,severity,description,expected,actual}] }
|
|
28
|
+
// Exit: 0 = match/skipped (nothing provable), 3 = divergence (findings are evidence).
|
|
29
|
+
// Internal errors fail OPEN ({status:"skipped"}, exit 0) — pre-gate, not tribunal.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
|
|
34
|
+
function emit(obj, code) {
|
|
35
|
+
process.stdout.write(JSON.stringify(obj) + '\n');
|
|
36
|
+
process.exit(code);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const a = { mockup: null, impl: [] };
|
|
41
|
+
for (let i = 2; i < argv.length; i++) {
|
|
42
|
+
if (argv[i] === '--mockup') a.mockup = argv[++i];
|
|
43
|
+
else if (argv[i] === '--impl') {
|
|
44
|
+
while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) a.impl.push(argv[++i]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return a;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readSources(target) {
|
|
51
|
+
const texts = [];
|
|
52
|
+
const push = (p) => { try { texts.push(fs.readFileSync(p, 'utf8')); } catch { /* fail-open per file */ } };
|
|
53
|
+
try {
|
|
54
|
+
const st = fs.statSync(target);
|
|
55
|
+
if (st.isDirectory()) {
|
|
56
|
+
for (const f of fs.readdirSync(target)) {
|
|
57
|
+
if (/\.(html?|jsx?|tsx?|css|scss|svelte|vue)$/i.test(f)) push(path.join(target, f));
|
|
58
|
+
}
|
|
59
|
+
} else push(target);
|
|
60
|
+
} catch { /* absent target → empty */ }
|
|
61
|
+
return texts;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Count top-level tracks in a grid-template-columns value: "repeat(2, minmax(0,1fr))" → 2,
|
|
65
|
+
// "1fr 320px" → 2. Parens-aware split; `repeat(N, ...)` contributes N (integer N only).
|
|
66
|
+
function countTracks(value) {
|
|
67
|
+
const v = value.trim();
|
|
68
|
+
if (!v || /^(subgrid|none|inherit|initial|unset)/.test(v)) return 0;
|
|
69
|
+
const parts = [];
|
|
70
|
+
let depth = 0, cur = '';
|
|
71
|
+
for (const ch of v) {
|
|
72
|
+
if (ch === '(') { depth++; cur += ch; }
|
|
73
|
+
else if (ch === ')') { depth--; cur += ch; }
|
|
74
|
+
else if (/\s/.test(ch) && depth === 0) { if (cur) { parts.push(cur); cur = ''; } }
|
|
75
|
+
else cur += ch;
|
|
76
|
+
}
|
|
77
|
+
if (cur) parts.push(cur);
|
|
78
|
+
let n = 0;
|
|
79
|
+
for (const p of parts) {
|
|
80
|
+
const rep = p.match(/^repeat\(\s*(\d+)\s*,/);
|
|
81
|
+
if (rep) n += parseInt(rep[1], 10);
|
|
82
|
+
else if (/^\[.*\]$/.test(p)) continue; // named grid line, not a track
|
|
83
|
+
else n += 1;
|
|
84
|
+
}
|
|
85
|
+
return n;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function signature(texts) {
|
|
89
|
+
const sig = {
|
|
90
|
+
landmarks: { header: 0, nav: 0, aside: 0, main: 0, footer: 0 },
|
|
91
|
+
grid_cols_max: 0,
|
|
92
|
+
grid_cols_responsive_max: 0, // max cols behind a responsive gate (md:/lg:/@media)
|
|
93
|
+
has_media_queries: false,
|
|
94
|
+
has_responsive_prefixes: false,
|
|
95
|
+
files: texts.length,
|
|
96
|
+
};
|
|
97
|
+
for (const t of texts) {
|
|
98
|
+
for (const lm of Object.keys(sig.landmarks)) {
|
|
99
|
+
const m = t.match(new RegExp(`<${lm}[\\s/>]`, 'gi'));
|
|
100
|
+
if (m) sig.landmarks[lm] += m.length;
|
|
101
|
+
}
|
|
102
|
+
// CSS: grid-template-columns (also inside <style> blocks / .css files / inline style=)
|
|
103
|
+
for (const m of t.matchAll(/grid-template-columns\s*:\s*([^;"'}]+)/gi)) {
|
|
104
|
+
sig.grid_cols_max = Math.max(sig.grid_cols_max, countTracks(m[1]));
|
|
105
|
+
}
|
|
106
|
+
// @media-scoped multi-col: approximate — a grid-template-columns inside a file that
|
|
107
|
+
// also has @media counts as responsive-capable (regex cannot scope blocks reliably).
|
|
108
|
+
if (/@media[^{]*\(\s*(min|max)-width/i.test(t)) {
|
|
109
|
+
sig.has_media_queries = true;
|
|
110
|
+
for (const m of t.matchAll(/grid-template-columns\s*:\s*([^;"'}]+)/gi)) {
|
|
111
|
+
sig.grid_cols_responsive_max = Math.max(sig.grid_cols_responsive_max, countTracks(m[1]));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Tailwind: grid-cols-N (+ responsive variants), incl. arbitrary grid-cols-[a_b].
|
|
115
|
+
// Arbitrary values use `_` as the space stand-in and may contain functions —
|
|
116
|
+
// `grid-cols-[repeat(2,_minmax(0,1fr))]` is 2 tracks, not split('_').length=3:
|
|
117
|
+
// normalize `_`→space and reuse the parens-aware countTracks().
|
|
118
|
+
for (const m of t.matchAll(/(?:^|[\s"'`])(?:(sm|md|lg|xl|2xl):)?grid-cols-(\d+|\[[^\]]+\])/g)) {
|
|
119
|
+
const cols = m[2].startsWith('[')
|
|
120
|
+
? countTracks(m[2].slice(1, -1).replace(/_/g, ' '))
|
|
121
|
+
: parseInt(m[2], 10);
|
|
122
|
+
sig.grid_cols_max = Math.max(sig.grid_cols_max, cols);
|
|
123
|
+
if (m[1]) {
|
|
124
|
+
sig.has_responsive_prefixes = true;
|
|
125
|
+
sig.grid_cols_responsive_max = Math.max(sig.grid_cols_responsive_max, cols);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (/(?:^|[\s"'`])(?:sm|md|lg|xl|2xl):[\w[-]/m.test(t)) sig.has_responsive_prefixes = true;
|
|
129
|
+
}
|
|
130
|
+
return sig;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main() {
|
|
134
|
+
const args = parseArgs(process.argv);
|
|
135
|
+
if (!args.mockup || args.impl.length === 0) emit({ status: 'skipped', reason: 'usage: --mockup <file-or-dir> --impl <files...>' }, 0);
|
|
136
|
+
|
|
137
|
+
const mockTexts = readSources(args.mockup);
|
|
138
|
+
if (mockTexts.length === 0) emit({ status: 'skipped', reason: 'mockup unreadable: ' + args.mockup }, 0);
|
|
139
|
+
const implTexts = args.impl.flatMap((f) => readSources(f));
|
|
140
|
+
if (implTexts.length === 0) emit({ status: 'skipped', reason: 'no readable implementation files' }, 0);
|
|
141
|
+
|
|
142
|
+
const mock = signature(mockTexts);
|
|
143
|
+
const impl = signature(implTexts);
|
|
144
|
+
const findings = [];
|
|
145
|
+
|
|
146
|
+
// Confidence encodes the composition caveat: the impl files are the card-touched set,
|
|
147
|
+
// and grids/landmarks may live INSIDE a reused primitive this scan cannot see.
|
|
148
|
+
// 1. Multi-column mockup built single-column (the FEAT-0061 layout-break class).
|
|
149
|
+
if (mock.grid_cols_max >= 2 && impl.grid_cols_max <= 1) {
|
|
150
|
+
findings.push({
|
|
151
|
+
category: 'layout-break', severity: 'critical', confidence: 'medium',
|
|
152
|
+
description: `mockup declares a ${mock.grid_cols_max}-column grid; implementation files declare no multi-column grid (unless a composed primitive owns it — verify)`,
|
|
153
|
+
expected: `grid with ${mock.grid_cols_max} columns (mockup)`, actual: `max ${impl.grid_cols_max} column(s) found in touched files`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// 2. Mockup gates multi-col behind a breakpoint; impl has NO responsive machinery at all.
|
|
157
|
+
if ((mock.has_media_queries || mock.has_responsive_prefixes) && !impl.has_media_queries && !impl.has_responsive_prefixes) {
|
|
158
|
+
findings.push({
|
|
159
|
+
category: 'responsiveness-break', severity: 'major', confidence: 'medium',
|
|
160
|
+
description: 'mockup is breakpoint-aware (@media / responsive prefixes); implementation files have neither (unless a composed primitive owns the breakpoints — verify)',
|
|
161
|
+
expected: 'breakpoint-gated layout as in the mockup', actual: 'no @media, no responsive class prefixes in touched files',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 3. Landmark regions present in the mockup, entirely absent in the implementation.
|
|
165
|
+
// LOW confidence by construction: component-based impls render landmarks inside
|
|
166
|
+
// reused shell primitives (<TopBar> → <header>) invisible to this scan.
|
|
167
|
+
for (const lm of Object.keys(mock.landmarks)) {
|
|
168
|
+
if (mock.landmarks[lm] > 0 && impl.landmarks[lm] === 0) {
|
|
169
|
+
findings.push({
|
|
170
|
+
category: 'component-missing', severity: 'minor', confidence: 'low',
|
|
171
|
+
description: `mockup contains <${lm}> landmark region(s); none in the touched files (may be rendered by a composed shell primitive — verify)`,
|
|
172
|
+
expected: `<${lm}> region (mockup has ${mock.landmarks[lm]})`, actual: 'absent from touched files',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
emit(
|
|
178
|
+
{ status: findings.length ? 'divergence' : 'match', mockup_signature: mock, impl_signature: impl, findings },
|
|
179
|
+
findings.length ? 3 : 0
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try { main(); } catch (e) { emit({ status: 'skipped', reason: 'internal error: ' + (e && e.message) }, 0); }
|