@smartmemory/compose 0.2.50-beta → 0.2.51
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/.claude/skills/compose/SKILL.md +24 -1
- package/.claude/skills/compose/references/hermes-tools.md +80 -0
- package/README.md +1 -1
- package/bin/compose.js +310 -52
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-DeHak63Z.js → arc-BXht3LyE.js} +1 -1
- package/dist/assets/architectureDiagram-Q4EWVU46-BV1r86-w.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-BgcMJjAR.js +132 -0
- package/dist/assets/{browser-DW2JlJCC.js → browser-CnKiSnlr.js} +6 -6
- package/dist/assets/{c4Diagram-AAUBKEIU-BsFJmPuS.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-XO4S2_89.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-CtFSLInj.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-AEu6HIoY.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BMGd_Y9a.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DHhUJ8RK.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-BlrOECEr.js → chunk-YZCP3GAM-DE6_5aqh.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DshSQDF9.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DshSQDF9.js +1 -0
- package/dist/assets/clone-CBsbNGAa.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-C3M0mZqk.js → cose-bilkent-S5V4N54A-DzzRyJtB.js} +1 -1
- package/dist/assets/dagre-KV5264BT-CTcTVGdU.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-qR6mc8kJ.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-CUcGXyNb.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-B68cPonH.js +43 -0
- package/dist/assets/diagram-TYMM5635-q8Id8eFG.js +24 -0
- package/dist/assets/{erDiagram-TEJ5UH35-AMDitV0b.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-C0t2ZGQv.js → flowDiagram-DWJPFMVM-BzzO4JYU.js} +1 -1
- package/dist/assets/ganttDiagram-T4ZO3ILL-Dn0wIyhR.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-tnNFXfMM.js +106 -0
- package/dist/assets/graph-DZe55uk8.js +331 -0
- package/dist/assets/graph-Tq_bs_r0.js +1 -0
- package/dist/assets/index-8UhRLbGq.js +123 -0
- package/dist/assets/index-CRqB9els.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BXLvLeS0.js +2 -0
- package/dist/assets/{ishikawaDiagram-YF4QCWOH-CCBwgjFs.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-zUJN38EU.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-BNxWi-8J.js → kanban-definition-6JOO6SKY-CEq930ew.js} +1 -1
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-qtUgN9BC.js +1 -0
- package/dist/assets/{linear-CRH9b4g7.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DolYwq7Y.js → mindmap-definition-QFDTVHPH-d9SSh2nG.js} +7 -7
- package/dist/assets/mobile-1gVCT0OK.css +1 -0
- package/dist/assets/mobile-Chw8RWyH.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-gglmeMav.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-CqFC_htU.js +7 -0
- package/dist/assets/{requirementDiagram-4Y6WPE33-DsXZ05jo.js → requirementDiagram-MS252O5E-B1ZFIotK.js} +1 -1
- package/dist/assets/sankeyDiagram-XADWPNL6-BvItvZt5.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-CAIn5Z3U.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CyJrGzib.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DprhF_Ho.js +1 -0
- package/dist/assets/{timeline-definition-PNZ67QCA-Bvnhw58e.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-5WnyjZqf.js → vennDiagram-DHZGUBPP-TZAW60b0.js} +4 -4
- package/dist/assets/wardley-RL74JXVD-DzQEoG6X.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-BAoVVIjZ.js +20 -0
- package/dist/assets/{xychartDiagram-2RQKCTM6-BCShes6B.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build.js +272 -33
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +21 -1
- package/lib/feature-validator.js +5 -1
- package/lib/hooks-status.js +135 -0
- package/lib/install-agent-defs.js +22 -0
- package/lib/migrate-anon.js +277 -0
- package/lib/roadmap-parser.js +1 -1
- package/lib/test-bootstrap.js +165 -2
- package/lib/vocabulary-inject.js +103 -0
- package/package.json +2 -1
- package/pipelines/build-quick.stratum.yaml +433 -0
- package/pipelines/build.stratum.yaml +60 -3
- package/scripts/watch-server.sh +32 -0
- package/server/build-routes.js +89 -9
- package/server/build-stream-bridge.js +12 -0
- package/server/compose-mcp-tools.js +73 -1
- package/server/compose-mcp.js +27 -0
- package/server/feature-scaffold-routes.js +82 -0
- package/server/health-routes.js +261 -0
- package/server/qa-scope-routes.js +96 -0
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +29 -0
- package/server/vision-server.js +21 -0
- package/dist/assets/App-Bxyif1yv.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C5Nkyl9y.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-BilnUBG2.js +0 -132
- package/dist/assets/channel-ZgbQ1k0u.js +0 -1
- package/dist/assets/chunk-727SXJPM-CZeBRpM9.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-_vmxcBc4.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-KXxOorUx.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-KXxOorUx.js +0 -1
- package/dist/assets/dagre-BM42HDAG-DJlm4fEk.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-BmcONQG5.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-5JbWGIwd.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-DAnIvyI0.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Bmh7tvmI.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-hnHVMNnk.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-BPlM-BEN.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-Lv_YTxnb.js +0 -106
- package/dist/assets/graph-BBXaecIU.js +0 -331
- package/dist/assets/graph-CAnANduQ.js +0 -1
- package/dist/assets/index-BwLfbbOu.css +0 -1
- package/dist/assets/index-COq21Zym.js +0 -119
- package/dist/assets/infoDiagram-5YYISTIA-Dul1vdUm.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DGIYPm2g.js +0 -1
- package/dist/assets/mobile-Cag5dHlF.css +0 -1
- package/dist/assets/mobile-DwmxS_O4.js +0 -17
- package/dist/assets/pieDiagram-4H26LBE5-oxNfr2TX.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-BmZCvD-z.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-DsnYaawP.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-Dt5mu3g8.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-BFR5ZINQ.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-BDrQD8fR.js +0 -1
- package/dist/assets/wardley-L42UT6IY-CTxW4xow.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-K8Y1EOvb.js +0 -78
package/lib/build.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
18
18
|
import { StratumMcpClient, StratumError } from './stratum-mcp-client.js';
|
|
19
19
|
import { runAndNormalize, AgentTimeoutError, UserInterruptError, AgentError } from './result-normalizer.js';
|
|
20
20
|
import { checkCapabilityViolation } from './capability-checker.js';
|
|
21
|
+
import { preflightCodexWorktreeProbe, codexProbeAbortMessage } from './codex-preflight.js';
|
|
21
22
|
import { buildStepPrompt, buildRetryPrompt, buildGateContext, clearAmbientContextCache, formatBounceForPrompt } from './step-prompt.js';
|
|
22
23
|
import { promptGate } from './gate-prompt.js';
|
|
23
24
|
import { VisionWriter, ServerUnreachableError } from './vision-writer.js';
|
|
@@ -36,6 +37,7 @@ import { rtkPrefix } from './rtk.js';
|
|
|
36
37
|
import YAML from 'yaml';
|
|
37
38
|
// feature-json direct imports removed — mutations now go through TrackerProvider (T9)
|
|
38
39
|
import { loadFeaturesDir, resolveContextPath, resolveRoadmapPath, resolveFeaturesPath } from './project-paths.js';
|
|
40
|
+
import { vocabularyEnabled, injectVocabularyEnsure, tagVocabularyViolations } from './vocabulary-inject.js';
|
|
39
41
|
|
|
40
42
|
// Lazy provider accessor — avoids circular import risk (factory → local-provider
|
|
41
43
|
// does NOT import build.js, so a static import is safe, but lazy is used for
|
|
@@ -50,7 +52,7 @@ import { shouldRunCrossModel, LENS_DEFINITIONS } from './review-lenses.js';
|
|
|
50
52
|
import { injectCertInstructions } from './cert-inject.js';
|
|
51
53
|
import { buildReviewPrompt } from './review-prompt.js';
|
|
52
54
|
import { normalizeCrossModelResult } from './review-normalize.js';
|
|
53
|
-
import { detectTestFramework, scaffoldTestFramework } from './test-bootstrap.js';
|
|
55
|
+
import { detectTestFramework, scaffoldTestFramework, parseTestSummary, deriveTestsPass, isTestFile } from './test-bootstrap.js';
|
|
54
56
|
import { classifyStepAsTier, evaluateTiers } from './gate-tiers.js';
|
|
55
57
|
import { mapFilesToRoutes, classifyRoutes, isDocsOnlyDiff } from './qa-scoping.js';
|
|
56
58
|
import { computeCompositeScore } from './health-score.js';
|
|
@@ -473,6 +475,47 @@ function clearPriorDirtyLenses(composeDir) {
|
|
|
473
475
|
if (existsSync(p)) unlinkSync(p);
|
|
474
476
|
}
|
|
475
477
|
|
|
478
|
+
// COMP-TEST-BOOTSTRAP-4-1: repo-relative test files touched since HEAD
|
|
479
|
+
// (changed-vs-HEAD + untracked), filtered to test paths. Used to scope the
|
|
480
|
+
// post-coverage test-review pass to the tests this build produced.
|
|
481
|
+
function listChangedTestFiles(cwd) {
|
|
482
|
+
try {
|
|
483
|
+
const out = execSync('git diff --name-only HEAD 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null', {
|
|
484
|
+
cwd, encoding: 'utf-8', timeout: 5000,
|
|
485
|
+
}).trim();
|
|
486
|
+
if (!out) return [];
|
|
487
|
+
return [...new Set(out.split('\n').map(s => s.trim()).filter(Boolean))].filter(isTestFile);
|
|
488
|
+
} catch {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// COMP-TEST-BOOTSTRAP-4-1: the pre-coverage test-file snapshot is persisted to a
|
|
494
|
+
// sidecar (not just in-memory context) so a build that RESUMES after coverage and
|
|
495
|
+
// lands on test_review still has the correct baseline — otherwise scope would widen
|
|
496
|
+
// to every changed test file. Mirrors the prior_dirty_lenses sidecar pattern.
|
|
497
|
+
function preCoverageTestsPath(composeDir) {
|
|
498
|
+
return join(composeDir, 'pre_coverage_tests.json');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function persistPreCoverageTests(composeDir, files) {
|
|
502
|
+
mkdirSync(composeDir, { recursive: true });
|
|
503
|
+
writeFileSync(preCoverageTestsPath(composeDir), JSON.stringify(files ?? [], null, 2));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function loadPreCoverageTests(composeDir) {
|
|
507
|
+
try {
|
|
508
|
+
return new Set(JSON.parse(readFileSync(preCoverageTestsPath(composeDir), 'utf-8')));
|
|
509
|
+
} catch {
|
|
510
|
+
return new Set();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function clearPreCoverageTests(composeDir) {
|
|
515
|
+
const p = preCoverageTestsPath(composeDir);
|
|
516
|
+
if (existsSync(p)) unlinkSync(p);
|
|
517
|
+
}
|
|
518
|
+
|
|
476
519
|
/**
|
|
477
520
|
* Check whether a process with the given PID is still alive.
|
|
478
521
|
*/
|
|
@@ -746,28 +789,38 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
746
789
|
// verifyPipelineIntegrity() re-reads from disk, so we must compare against the original file content.
|
|
747
790
|
const specFileHash = _sha256(specYaml);
|
|
748
791
|
|
|
749
|
-
// Apply triage profile
|
|
750
|
-
|
|
792
|
+
// Apply spec mutations: triage profile (skip_if toggles) + STRAT-VOCAB-3
|
|
793
|
+
// vocabulary-ensure injection. Parsed once, applied conditionally, stringified
|
|
794
|
+
// once. The tamper hash above is taken from the on-disk file BEFORE mutation,
|
|
795
|
+
// so in-memory edits here don't trip verifyPipelineIntegrity (same pattern the
|
|
796
|
+
// triage profile already relied on).
|
|
797
|
+
const vocabOn = vocabularyEnabled(cwd, composeConfig);
|
|
798
|
+
if (buildProfile || vocabOn) {
|
|
751
799
|
try {
|
|
752
800
|
const specObj = YAML.parse(specYaml);
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
801
|
+
if (buildProfile) {
|
|
802
|
+
const flows = specObj?.flows ?? {};
|
|
803
|
+
// Find the build flow (or first flow)
|
|
804
|
+
const flowKey = Object.keys(flows).includes('build') ? 'build' : Object.keys(flows)[0];
|
|
805
|
+
const steps = flows[flowKey]?.steps ?? [];
|
|
806
|
+
const skippableSteps = ['prd', 'architecture', 'verification', 'report'];
|
|
807
|
+
for (const step of steps) {
|
|
808
|
+
if (!skippableSteps.includes(step.id)) continue;
|
|
809
|
+
const needsKey = `needs_${step.id}`;
|
|
810
|
+
if (buildProfile[needsKey] === true) {
|
|
811
|
+
// Enable step — remove skip_if/skip_reason
|
|
812
|
+
delete step.skip_if;
|
|
813
|
+
delete step.skip_reason;
|
|
814
|
+
} else if (buildProfile[needsKey] === false) {
|
|
815
|
+
// Disable step — mark as unconditionally skipped
|
|
816
|
+
step.skip_if = 'true';
|
|
817
|
+
step.skip_reason = `Skipped by triage (tier ${_buildTierLabel})`;
|
|
818
|
+
}
|
|
769
819
|
}
|
|
770
820
|
}
|
|
821
|
+
// STRAT-VOCAB-3: append vocabulary_compliance to the EXECUTED flow's review
|
|
822
|
+
// step (extractFlowName resolves the same flow Stratum will run).
|
|
823
|
+
if (vocabOn) injectVocabularyEnsure(specObj, extractFlowName(specYaml, templateName));
|
|
771
824
|
specYaml = YAML.stringify(specObj);
|
|
772
825
|
} catch (err) {
|
|
773
826
|
// Non-fatal — fall back to unmodified spec
|
|
@@ -873,6 +926,33 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
873
926
|
let response;
|
|
874
927
|
let isFreshStart = true;
|
|
875
928
|
|
|
929
|
+
// COMP-CODEX-IMPL: implementer/reviewer roles. A FRESH start derives them from
|
|
930
|
+
// the flag (--codex flips Claude-implements/Codex-reviews → Codex/Claude); the
|
|
931
|
+
// `roles` snapshot below carries those flag-derived values into startFresh.
|
|
932
|
+
//
|
|
933
|
+
// A RESUME instead restores roles from active-build state — the build context is
|
|
934
|
+
// rebuilt locally each invocation, so a `--codex` build resumed WITHOUT the flag
|
|
935
|
+
// must keep its Codex role (else fix-routing + cross-model suppression silently
|
|
936
|
+
// revert). Restoration is gated on ACTUALLY resuming (done in the resume branches
|
|
937
|
+
// below), never eagerly — otherwise a completed `--codex` build's persisted role
|
|
938
|
+
// would bleed into a later plain `compose build <same-code>` that starts fresh
|
|
939
|
+
// (Codex impl-review finding #1).
|
|
940
|
+
let implementerAgent = opts.codex ? 'codex' : 'claude';
|
|
941
|
+
let reviewerAgent = opts.codex ? 'claude' : 'codex';
|
|
942
|
+
const roles = { implementerAgent, reviewerAgent };
|
|
943
|
+
// Restore persisted roles when (and only when) a resume actually happens.
|
|
944
|
+
const restoreRolesFromActive = (src) => {
|
|
945
|
+
if (mode !== 'feature' || !src || !src.implementerAgent) return;
|
|
946
|
+
if (src.implementerAgent !== implementerAgent) {
|
|
947
|
+
console.warn(
|
|
948
|
+
`⚠ Resuming ${featureCode}: build started with implementer=${src.implementerAgent}; ` +
|
|
949
|
+
`honoring the persisted role over the current invocation's flag.`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
implementerAgent = src.implementerAgent;
|
|
953
|
+
reviewerAgent = src.reviewerAgent ?? reviewerAgent;
|
|
954
|
+
};
|
|
955
|
+
|
|
876
956
|
// COMP-FIX-HARD T8: explicit `--resume` flag (compose fix <code> --resume).
|
|
877
957
|
// When opts.resumeFlowId is set, skip stratum.plan entirely and resume the
|
|
878
958
|
// given flow. CLI validates the flowId belongs to this code before calling.
|
|
@@ -897,6 +977,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
897
977
|
console.log(`Resuming flow ${opts.resumeFlowId} for ${featureCode}...`);
|
|
898
978
|
response = await stratum.resume(opts.resumeFlowId);
|
|
899
979
|
isFreshStart = false;
|
|
980
|
+
// COMP-CODEX-IMPL: this is a real resume — restore roles from persisted state
|
|
981
|
+
// (the refresh-write below then persists the restored roles, not flag-derived).
|
|
982
|
+
restoreRolesFromActive(activeNow);
|
|
900
983
|
// Refresh active-build.json so streaming/UI sees this as the live build.
|
|
901
984
|
const flowName = extractFlowName(specYaml, templateName);
|
|
902
985
|
writeActiveBuild(dataDir, {
|
|
@@ -913,6 +996,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
913
996
|
violations: [],
|
|
914
997
|
status: 'running',
|
|
915
998
|
resumedAt: new Date().toISOString(),
|
|
999
|
+
// COMP-CODEX-IMPL: carry the (restored or default) roles forward on resume-refresh.
|
|
1000
|
+
implementerAgent,
|
|
1001
|
+
reviewerAgent,
|
|
916
1002
|
});
|
|
917
1003
|
} else if (active && active.featureCode === featureCode && active.flowId) {
|
|
918
1004
|
// Same feature — try to resume or start fresh
|
|
@@ -925,10 +1011,10 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
925
1011
|
`Previous build for ${featureCode} was in ${active.mode} mode, ` +
|
|
926
1012
|
`current invocation is ${mode} mode. Starting fresh.`
|
|
927
1013
|
);
|
|
928
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1014
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
929
1015
|
} else if (active.status && active.status !== 'running') {
|
|
930
1016
|
console.log(`Previous build ${active.status}. Starting fresh.`);
|
|
931
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1017
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
932
1018
|
} else if (active.pid && active.pid !== process.pid && isProcessAlive(active.pid)) {
|
|
933
1019
|
// Same feature, different live process — block
|
|
934
1020
|
throw new Error(
|
|
@@ -941,10 +1027,12 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
941
1027
|
response = await stratum.resume(active.flowId);
|
|
942
1028
|
if (isTerminalFlow(response.status)) {
|
|
943
1029
|
console.log(`Previous build already ${response.status}. Starting fresh.`);
|
|
944
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1030
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
945
1031
|
} else {
|
|
946
1032
|
console.log(`Resuming from step: ${response.step_id}`);
|
|
947
1033
|
isFreshStart = false;
|
|
1034
|
+
// COMP-CODEX-IMPL: real resume — restore roles from the persisted build.
|
|
1035
|
+
restoreRolesFromActive(active);
|
|
948
1036
|
}
|
|
949
1037
|
} catch (err) {
|
|
950
1038
|
const recoverable = err?.code === 'flow_not_found'
|
|
@@ -952,7 +1040,7 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
952
1040
|
|| err?.message?.includes('No active flow');
|
|
953
1041
|
if (recoverable) {
|
|
954
1042
|
console.log('Previous flow not found. Starting fresh.');
|
|
955
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1043
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
956
1044
|
} else {
|
|
957
1045
|
throw err;
|
|
958
1046
|
}
|
|
@@ -962,7 +1050,54 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
962
1050
|
// Different feature or no active build — start fresh.
|
|
963
1051
|
// active-build.json is last-writer-wins: concurrent builds for
|
|
964
1052
|
// different features are allowed; the UI shows the most recent.
|
|
965
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1053
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// COMP-CODEX-IMPL: verify Codex can write inside a detached git worktree (the
|
|
1057
|
+
// execute step's isolation primitive) before any step dispatches. Runs on the
|
|
1058
|
+
// EFFECTIVE role — after resume restoration above — so a resumed Codex build is
|
|
1059
|
+
// probed too (cached per-repo, so resume is normally a no-op); gating on the
|
|
1060
|
+
// pre-restore flag would skip resumed Codex builds (Codex impl-review finding).
|
|
1061
|
+
// The plan/resume above only created the flow object; no agent/worktree work has
|
|
1062
|
+
// happened yet, so aborting here still means we never reach `execute` (Codex
|
|
1063
|
+
// review finding #2). Cached + skippable via COMPOSE_SKIP_CODEX_PROBE.
|
|
1064
|
+
if (implementerAgent === 'codex') {
|
|
1065
|
+
const probe = await preflightCodexWorktreeProbe({
|
|
1066
|
+
cwd: agentCwd,
|
|
1067
|
+
stratum,
|
|
1068
|
+
dataDir,
|
|
1069
|
+
ts: new Date().toISOString().replace(/[:.]/g, '-'),
|
|
1070
|
+
});
|
|
1071
|
+
if (!probe.ok) {
|
|
1072
|
+
// The plan/resume above already persisted feature.json=IN_PROGRESS and
|
|
1073
|
+
// active-build.json='running'. A preflight abort throws before the normal
|
|
1074
|
+
// terminal handlers run, so roll BOTH back here (mirrors the killed/failed
|
|
1075
|
+
// teardown) — otherwise a build that never dispatched a step strands stale
|
|
1076
|
+
// state (Codex impl-review findings).
|
|
1077
|
+
if (!isBugMode) {
|
|
1078
|
+
try {
|
|
1079
|
+
const _bp = await getBuildProvider(cwd);
|
|
1080
|
+
const _feat = await _bp.getFeature(featureCode);
|
|
1081
|
+
if (_feat) await _bp.persistFeatureRaw(featureCode, { ..._feat, status: 'PLANNED' });
|
|
1082
|
+
} catch { /* best-effort */ }
|
|
1083
|
+
}
|
|
1084
|
+
// Identity-guarded active-build downgrade: active-build.json is last-writer-wins
|
|
1085
|
+
// across concurrent feature builds and the probe can sit up to its timeout, so
|
|
1086
|
+
// only downgrade if the on-disk state is still THIS build (same pattern as
|
|
1087
|
+
// persistHealthGateDowngrade) — never clobber a concurrent build's state.
|
|
1088
|
+
try {
|
|
1089
|
+
const cur = readActiveBuild(dataDir);
|
|
1090
|
+
const sameFlow = !cur?.flowId || !response?.flow_id || cur.flowId === response.flow_id;
|
|
1091
|
+
const sameFeature = !cur?.featureCode || cur.featureCode === featureCode;
|
|
1092
|
+
if (cur && sameFlow && sameFeature) {
|
|
1093
|
+
writeActiveBuild(dataDir, { ...cur, status: 'aborted', completedAt: new Date().toISOString() });
|
|
1094
|
+
}
|
|
1095
|
+
} catch { /* best-effort cleanup */ }
|
|
1096
|
+
throw new Error(codexProbeAbortMessage(probe.reason));
|
|
1097
|
+
}
|
|
1098
|
+
if (!probe.cached && !probe.skipped) {
|
|
1099
|
+
console.log(`✓ Codex worktree probe passed — ${probe.reason}`);
|
|
1100
|
+
}
|
|
966
1101
|
}
|
|
967
1102
|
|
|
968
1103
|
// Update vision state
|
|
@@ -971,6 +1106,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
971
1106
|
// Stream writer — instantiated after plan/resume succeeds to prevent
|
|
972
1107
|
// a rejected/duplicate invocation from truncating an active build's stream.
|
|
973
1108
|
// Only truncate on fresh starts; resumed builds append to existing stream.
|
|
1109
|
+
// COMP-TEST-BOOTSTRAP-4-1: clear any stale pre-coverage test snapshot on a fresh
|
|
1110
|
+
// start (it is per-build; a resume must keep the prior session's snapshot).
|
|
1111
|
+
if (isFreshStart) clearPreCoverageTests(composeDir);
|
|
974
1112
|
streamWriter = new BuildStreamWriter(composeDir, featureCode, { truncate: isFreshStart });
|
|
975
1113
|
streamWriter.write({
|
|
976
1114
|
type: isFreshStart ? 'build_start' : 'build_resume',
|
|
@@ -1011,10 +1149,17 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1011
1149
|
contextDir: contextDirPath,
|
|
1012
1150
|
stepHistory,
|
|
1013
1151
|
mode,
|
|
1152
|
+
// COMP-BUILD-QUICK-1: the pipeline template (e.g. 'build-quick') so the ship
|
|
1153
|
+
// step can stamp built_via onto feature.json for the validator's exemption.
|
|
1154
|
+
templateName,
|
|
1014
1155
|
enforceMcpForFeatureMgmt,
|
|
1015
1156
|
build_id,
|
|
1016
1157
|
buildStartedAt,
|
|
1017
1158
|
featuresDir,
|
|
1159
|
+
// COMP-CODEX-IMPL: roles drive fix-routing (fixer = implementer) and Codex
|
|
1160
|
+
// self-review suppression. Restored from active-build state on resume (above).
|
|
1161
|
+
implementerAgent,
|
|
1162
|
+
reviewerAgent,
|
|
1018
1163
|
...(isBugMode ? { bug_code: featureCode } : {}),
|
|
1019
1164
|
};
|
|
1020
1165
|
|
|
@@ -1603,6 +1748,14 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1603
1748
|
// If no test framework is detected and no test directory exists, scaffold first.
|
|
1604
1749
|
// If the project has no tests at all (truly empty), skip coverage gracefully.
|
|
1605
1750
|
if (childFlowName === 'coverage_check') {
|
|
1751
|
+
// COMP-TEST-BOOTSTRAP-4-1: snapshot test files present BEFORE coverage runs,
|
|
1752
|
+
// so the later test_review step can review only the tests coverage generates.
|
|
1753
|
+
// Persisted to a sidecar so a resume landing on test_review keeps the baseline.
|
|
1754
|
+
{
|
|
1755
|
+
const preCov = listChangedTestFiles(agentCwd);
|
|
1756
|
+
context._preCoverageTestFiles = new Set(preCov);
|
|
1757
|
+
persistPreCoverageTests(composeDir, preCov);
|
|
1758
|
+
}
|
|
1606
1759
|
const detected = detectTestFramework(agentCwd);
|
|
1607
1760
|
const hasTestDir = existsSync(join(agentCwd, 'test')) ||
|
|
1608
1761
|
existsSync(join(agentCwd, 'tests')) ||
|
|
@@ -1650,6 +1803,37 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1650
1803
|
}
|
|
1651
1804
|
}
|
|
1652
1805
|
|
|
1806
|
+
// COMP-TEST-BOOTSTRAP-4-1: the test_review step reviews the test files the
|
|
1807
|
+
// coverage step produced this build (the implementation review runs BEFORE
|
|
1808
|
+
// coverage, so generated tests are never otherwise reviewed). Scope to NEW
|
|
1809
|
+
// test files (not present before coverage); skip the step entirely when none.
|
|
1810
|
+
if (childFlowName === 'test_review') {
|
|
1811
|
+
// Resume-safe: fall back to the sidecar when the in-memory snapshot is absent
|
|
1812
|
+
// (a build resumed after coverage rebuilds context fresh).
|
|
1813
|
+
const preCoverage = context._preCoverageTestFiles ?? loadPreCoverageTests(composeDir);
|
|
1814
|
+
const generatedTests = listChangedTestFiles(agentCwd).filter(f => !preCoverage.has(f));
|
|
1815
|
+
if (generatedTests.length === 0) {
|
|
1816
|
+
progress.subFlowStep('test_review', 'skipped — no generated tests');
|
|
1817
|
+
const skipResult = { clean: true, summary: 'No generated test files this build — test review skipped', findings: [] };
|
|
1818
|
+
buildSignals.test_review = skipResult;
|
|
1819
|
+
streamWriter?.write({ type: 'test_review', clean: true, summary: skipResult.summary, findings: [] });
|
|
1820
|
+
verifyPipelineIntegrity(specPath, specFileHash);
|
|
1821
|
+
response = await stratum.stepDone(parentFlowId, parentStepId, { status: 'ok', output: skipResult });
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
// Inject the exact file list into the review intent (child-flow inputs are
|
|
1825
|
+
// declarative; intent-append is the established runtime-injection pattern).
|
|
1826
|
+
if (response.child_step?.intent !== undefined) {
|
|
1827
|
+
response = {
|
|
1828
|
+
...response,
|
|
1829
|
+
child_step: {
|
|
1830
|
+
...response.child_step,
|
|
1831
|
+
intent: `${response.child_step.intent}\n\nGenerated test files to review (written this build):\n${generatedTests.map(f => `- ${f}`).join('\n')}`,
|
|
1832
|
+
},
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1653
1837
|
let childResult = await executeChildFlow(
|
|
1654
1838
|
response, stratum, context,
|
|
1655
1839
|
visionWriter, itemId, dataDir, opts.gateOpts ?? {}, progress,
|
|
@@ -1658,7 +1842,12 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1658
1842
|
|
|
1659
1843
|
// STRAT-REV-7: After review child flow completes, run cross-model synthesis if diff is large.
|
|
1660
1844
|
// If cross-model ran, skip the pipeline's separate codex_review step (avoid duplicate Codex pass).
|
|
1661
|
-
|
|
1845
|
+
// COMP-CODEX-IMPL: when Codex is the IMPLEMENTER, this Codex second-opinion pass
|
|
1846
|
+
// would be Codex reviewing its own diff — suppress it. The Claude parallel_review
|
|
1847
|
+
// lenses already provide cross-model review of Codex's implementation.
|
|
1848
|
+
if (childFlowName === 'parallel_review' && context.implementerAgent === 'codex') {
|
|
1849
|
+
streamWriter?.write({ type: 'cross_model_review', status: 'codex_skipped', reason: 'Codex is the implementer — its own diff is reviewed by Claude lenses, not a Codex second opinion' });
|
|
1850
|
+
} else if (childFlowName === 'parallel_review' && childResult?.output && (context.filesChanged?.length ?? 0) > 0) {
|
|
1662
1851
|
const mergedResult = childResult.output;
|
|
1663
1852
|
const synthesized = await runCrossModelReview(
|
|
1664
1853
|
mergedResult,
|
|
@@ -1686,6 +1875,18 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1686
1875
|
buildSignals.test_coverage = childResult.output;
|
|
1687
1876
|
} else if (childFlowName === 'parallel_review' && childResult?.output != null) {
|
|
1688
1877
|
buildSignals.review_findings = childResult.output;
|
|
1878
|
+
} else if (childFlowName === 'test_review' && childResult?.output != null) {
|
|
1879
|
+
// COMP-TEST-BOOTSTRAP-4-1: advisory — never blocks ship. Surface findings on
|
|
1880
|
+
// the build stream (buildSignals alone is not read by any consumer) so the
|
|
1881
|
+
// cockpit/report can show them.
|
|
1882
|
+
const tr = childResult.output;
|
|
1883
|
+
buildSignals.test_review = tr;
|
|
1884
|
+
streamWriter?.write({
|
|
1885
|
+
type: 'test_review',
|
|
1886
|
+
clean: tr.clean ?? true,
|
|
1887
|
+
summary: typeof tr.summary === 'string' ? tr.summary : '',
|
|
1888
|
+
findings: Array.isArray(tr.findings) ? tr.findings : [],
|
|
1889
|
+
});
|
|
1689
1890
|
}
|
|
1690
1891
|
|
|
1691
1892
|
// Report child completion envelope to parent flow step.
|
|
@@ -1748,7 +1949,8 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1748
1949
|
}
|
|
1749
1950
|
progress.retry('build', stepId, response.agent);
|
|
1750
1951
|
const violations = response.violations ?? [];
|
|
1751
|
-
|
|
1952
|
+
// STRAT-VOCAB-3: surface vocabulary violations as must-fix (they block ship).
|
|
1953
|
+
if (violations.length > 0) progress.findings(tagVocabularyViolations(violations));
|
|
1752
1954
|
// STRAT-REV-5: defensive fallback — for non-flow-step paths named 'review'.
|
|
1753
1955
|
// For flow-steps the ensure fires inside executeChildFlow (see handler below).
|
|
1754
1956
|
if ((response.step_id ?? stepId) === 'review') {
|
|
@@ -2277,6 +2479,11 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2277
2479
|
? `docs/bugs/${featureCode}`
|
|
2278
2480
|
: `${featuresDir}/${featureCode}`;
|
|
2279
2481
|
|
|
2482
|
+
// COMP-BUILD-QUICK-1: when a feature was built via the trimmed quick lifecycle
|
|
2483
|
+
// (which omits the report phase by design), stamp built_via onto feature.json so
|
|
2484
|
+
// the validator exempts it from MISSING_COMPLETION_REPORT. Null for normal builds.
|
|
2485
|
+
const builtVia = context?.templateName === 'build-quick' ? 'build-quick' : null;
|
|
2486
|
+
|
|
2280
2487
|
try {
|
|
2281
2488
|
// 0. Check if we're in a git repository — if not, skip git operations
|
|
2282
2489
|
let isGitRepo = false;
|
|
@@ -2297,9 +2504,12 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2297
2504
|
await recordCompletion(cwd, {
|
|
2298
2505
|
feature_code: featureCode,
|
|
2299
2506
|
// commit_sha omitted — non-git workspace, stamped with the null-SHA
|
|
2507
|
+
// COMP-TEST-BOOTSTRAP-4: this path returns before the test run, so
|
|
2508
|
+
// there is no parsed signal — degrade to true (no block).
|
|
2300
2509
|
tests_pass: true,
|
|
2301
2510
|
files_changed: [],
|
|
2302
2511
|
notes: description.split('\n')[0].slice(0, 72),
|
|
2512
|
+
...(builtVia ? { built_via: builtVia } : {}),
|
|
2303
2513
|
});
|
|
2304
2514
|
} catch (err) {
|
|
2305
2515
|
completionWarning = `completion record failed (${err.code || 'UNKNOWN'}): ${err.message}`;
|
|
@@ -2319,12 +2529,21 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2319
2529
|
|
|
2320
2530
|
// 1. Run feature-relevant tests (best-effort — don't block ship on test infra issues)
|
|
2321
2531
|
if (progress) progress.toolUse('ship', 'Running tests...');
|
|
2532
|
+
// COMP-TEST-BOOTSTRAP-4: parse the run output into a structured signal and
|
|
2533
|
+
// derive a real tests_pass for the completion attestation. Degrades to
|
|
2534
|
+
// `true` (no block) whenever the output can't be parsed — see deriveTestsPass.
|
|
2535
|
+
let testSummary = { test_count: 0, pass_rate: 0, parsed: false };
|
|
2322
2536
|
try {
|
|
2323
2537
|
// COMP-TEST-BOOTSTRAP item 128: use detected test command instead of hard-coded npm test
|
|
2324
2538
|
const testFramework = detectTestFramework(agentCwd);
|
|
2325
2539
|
const testCommand = testFramework?.command ?? 'npm test';
|
|
2326
|
-
execSync(`${testCommand} 2>&1 || true`, { cwd: agentCwd, encoding: 'utf-8', timeout: 120_000 });
|
|
2327
|
-
|
|
2540
|
+
const testOutput = execSync(`${testCommand} 2>&1 || true`, { cwd: agentCwd, encoding: 'utf-8', timeout: 120_000 });
|
|
2541
|
+
testSummary = parseTestSummary(testFramework?.framework, testOutput);
|
|
2542
|
+
} catch { /* test runner not available or timed out — proceed (testSummary stays unparsed) */ }
|
|
2543
|
+
const testsPass = deriveTestsPass(testSummary);
|
|
2544
|
+
if (progress && testSummary.parsed) {
|
|
2545
|
+
progress.toolUse('ship', `Tests: ${testSummary.test_count} run, ${testSummary.pass_rate}% passing`);
|
|
2546
|
+
}
|
|
2328
2547
|
|
|
2329
2548
|
// 2. Collect files to stage
|
|
2330
2549
|
const filesToStage = new Set();
|
|
@@ -2508,9 +2727,12 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2508
2727
|
await recordCompletion(cwd, {
|
|
2509
2728
|
feature_code: featureCode,
|
|
2510
2729
|
commit_sha: sha,
|
|
2511
|
-
|
|
2730
|
+
// COMP-TEST-BOOTSTRAP-4: derived from the parsed test run above
|
|
2731
|
+
// (true when unparseable — never a false block).
|
|
2732
|
+
tests_pass: testsPass,
|
|
2512
2733
|
files_changed: filesChanged,
|
|
2513
2734
|
notes: shortDesc,
|
|
2735
|
+
...(builtVia ? { built_via: builtVia } : {}),
|
|
2514
2736
|
});
|
|
2515
2737
|
if (progress) progress.toolUse('ship', `Recorded completion for ${featureCode}`);
|
|
2516
2738
|
} catch (err) {
|
|
@@ -2993,7 +3215,8 @@ export async function executeChildFlow(
|
|
|
2993
3215
|
});
|
|
2994
3216
|
}
|
|
2995
3217
|
const violations = resp.violations ?? [];
|
|
2996
|
-
|
|
3218
|
+
// STRAT-VOCAB-3: surface vocabulary violations as must-fix (they block ship).
|
|
3219
|
+
if (violations.length > 0 && progress) progress.findings(tagVocabularyViolations(violations));
|
|
2997
3220
|
// STRAT-REV-5: flow-step ensures fire here (inside executeChildFlow), not in the
|
|
2998
3221
|
// main dispatch loop — persist dirty lenses so triage selectively re-runs on retry.
|
|
2999
3222
|
if (resp.step_id === 'review') {
|
|
@@ -3004,7 +3227,14 @@ export async function executeChildFlow(
|
|
|
3004
3227
|
}
|
|
3005
3228
|
}
|
|
3006
3229
|
const stepAgent = resp.agent ?? 'claude';
|
|
3007
|
-
|
|
3230
|
+
// COMP-CODEX-IMPL: the fixer is the IMPLEMENTER — whoever wrote the code fixes
|
|
3231
|
+
// the review findings against it. This replaces the old hardcoded
|
|
3232
|
+
// `stepAgent === 'codex' ? 'claude' : stepAgent` swap, which assumed codex is
|
|
3233
|
+
// always the reviewer and claude always the implementer; that assumption inverts
|
|
3234
|
+
// under --codex. Default (implementer=claude) reproduces the old outcome exactly:
|
|
3235
|
+
// a codex reviewer's finding is fixed by claude, the implementer. (stepAgent is
|
|
3236
|
+
// still the reviewer, used for the retry display below.)
|
|
3237
|
+
const fixAgent = context.implementerAgent || 'claude';
|
|
3008
3238
|
|
|
3009
3239
|
if (progress) {
|
|
3010
3240
|
progress.fix(childFlowName, fixAgent, resp.step_id);
|
|
@@ -4385,7 +4615,7 @@ export async function executeParallelDispatch(
|
|
|
4385
4615
|
}
|
|
4386
4616
|
}
|
|
4387
4617
|
|
|
4388
|
-
export async function startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode = 'feature', preMergeGate) {
|
|
4618
|
+
export async function startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode = 'feature', preMergeGate, roles) {
|
|
4389
4619
|
const flowName = extractFlowName(specYaml, templateName);
|
|
4390
4620
|
console.log(`Starting ${flowName} for ${featureCode}...`);
|
|
4391
4621
|
// COMP-FIX-HARD T4: bug-mode flows take input as { task: <description> }
|
|
@@ -4394,9 +4624,14 @@ export async function startFresh(stratum, specYaml, featureCode, description, da
|
|
|
4394
4624
|
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY (D5): fold pre_merge_gate into the
|
|
4395
4625
|
// feature plan envelope ONLY when resolved (undefined ⇒ key omitted, not [])
|
|
4396
4626
|
// so the default-OFF path is byte-identical to pre-feature behavior.
|
|
4627
|
+
// COMP-CODEX-IMPL: feature flows always carry implementer_agent/reviewer_agent so
|
|
4628
|
+
// the interpolated execute/review agents (STRAT-AGENT-INTERP) always resolve.
|
|
4629
|
+
// Defaults reproduce today's behavior (claude implements, codex reviews) byte-identically.
|
|
4630
|
+
const implementerAgent = roles?.implementerAgent ?? 'claude';
|
|
4631
|
+
const reviewerAgent = roles?.reviewerAgent ?? 'codex';
|
|
4397
4632
|
const planInputs = mode === 'bug'
|
|
4398
4633
|
? { task: description }
|
|
4399
|
-
: { featureCode, description, ...(preMergeGate !== undefined ? { pre_merge_gate: preMergeGate } : {}) };
|
|
4634
|
+
: { featureCode, description, implementer_agent: implementerAgent, reviewer_agent: reviewerAgent, ...(preMergeGate !== undefined ? { pre_merge_gate: preMergeGate } : {}) };
|
|
4400
4635
|
const response = await stratum.plan(specYaml, flowName, planInputs);
|
|
4401
4636
|
|
|
4402
4637
|
writeActiveBuild(dataDir, {
|
|
@@ -4413,6 +4648,10 @@ export async function startFresh(stratum, specYaml, featureCode, description, da
|
|
|
4413
4648
|
violations: [],
|
|
4414
4649
|
status: 'running',
|
|
4415
4650
|
startedAt: new Date().toISOString(),
|
|
4651
|
+
// COMP-CODEX-IMPL: roles are durable across resume (the build context is rebuilt
|
|
4652
|
+
// locally each invocation, so a resume without --codex must restore them).
|
|
4653
|
+
implementerAgent,
|
|
4654
|
+
reviewerAgent,
|
|
4416
4655
|
});
|
|
4417
4656
|
|
|
4418
4657
|
return response;
|