@smartmemory/compose 0.2.49-beta → 0.2.51-beta
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 +340 -70
- package/dist/assets/App-CT0vXPgd.js +724 -0
- package/dist/assets/_baseUniq-CsOHc_iS.js +1 -0
- package/dist/assets/{arc-B1TqAC_R.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-Dc6swoQo.js → c4Diagram-AHTNJAMY-nBIRLJTv.js} +1 -1
- package/dist/assets/channel-Dd4XaiYv.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CdqXgd68.js → chunk-4BX2VUAB-CNmGyhrp.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-MLT7I7w4.js +206 -0
- package/dist/assets/{chunk-55IACEB6-D7VauOmU.js → chunk-55IACEB6-D8KuKVLa.js} +1 -1
- package/dist/assets/{chunk-2J33WTMH-6HKDSnOw.js → chunk-EDXVE4YY-TPlt1bS2.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-C1Z5m8DP.js → chunk-FMBD7UC4-tYY8xcjr.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-yBPwT1AT.js +231 -0
- package/dist/assets/{chunk-QZHKN3VN-DY9GU2uD.js → chunk-QZHKN3VN-B4Wfybww.js} +1 -1
- package/dist/assets/{chunk-ND2GUHAM-DmqZMEQe.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-B34g9xzV.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-C05_Iqmb.js → erDiagram-SMLLAGMA-CtxJi57K.js} +1 -1
- package/dist/assets/{flowDiagram-I6XJVG4X-lXpnb-Ch.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-CgdsWdgu.js → ishikawaDiagram-UXIWVN3A-DWx-7BBy.js} +5 -5
- package/dist/assets/{journeyDiagram-JHISSGLW-CyvHmE3z.js → journeyDiagram-VCZTEJTY-C42hXNha.js} +1 -1
- package/dist/assets/{kanban-definition-UN3LZRKU-Bqjdyv0T.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-DecnRXf0.js → linear-D158F7OT.js} +1 -1
- package/dist/assets/min-hESxN-c0.js +1 -0
- package/dist/assets/{mindmap-definition-RKZ34NQL-DM1x9vNY.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-DD-Gg_y5.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-BPm2AEQj.js → timeline-definition-GMOUNBTQ-k_0vJQAK.js} +1 -1
- package/dist/assets/{vennDiagram-CIIHVFJN-DQCDWwlj.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-Ba05SXXF.js → xychartDiagram-5P7HB3ND-CynkZcUb.js} +1 -1
- package/dist/index.html +4 -4
- package/lib/build-all.js +2 -1
- package/lib/build.js +363 -38
- package/lib/checkpoint/checkpoint-writer.js +3 -2
- package/lib/codex-preflight.js +151 -0
- package/lib/completion-writer.js +59 -15
- package/lib/feature-json.js +14 -3
- package/lib/feature-validator.js +27 -10
- package/lib/feature-write-guard.js +5 -7
- package/lib/feature-writer.js +3 -3
- package/lib/followup-writer.js +2 -2
- package/lib/get-roadmap.js +2 -2
- package/lib/gsd.js +2 -1
- package/lib/hooks-status.js +135 -0
- package/lib/ideabox.js +3 -2
- package/lib/install-agent-defs.js +22 -0
- package/lib/journal-writer.js +3 -3
- package/lib/migrate-anon.js +277 -0
- package/lib/migrate-roadmap.js +2 -3
- package/lib/paths-core.js +47 -0
- package/lib/project-paths.js +46 -37
- package/lib/roadmap-gen.js +3 -4
- package/lib/roadmap-graph/collect.js +5 -5
- package/lib/roadmap-graph/index.js +2 -2
- package/lib/roadmap-parser.js +1 -1
- package/lib/state-migrations.js +3 -3
- package/lib/test-bootstrap.js +165 -2
- package/lib/tracker/local-provider.js +2 -2
- package/lib/triage.js +7 -5
- package/lib/vocabulary-inject.js +103 -0
- package/lib/xref-push.js +4 -4
- package/lib/xref-sync.js +4 -4
- 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/design-routes.js +9 -4
- package/server/drift-axes.js +5 -3
- package/server/feature-scaffold-routes.js +82 -0
- package/server/feature-scan.js +8 -2
- package/server/file-watcher.js +13 -7
- package/server/health-routes.js +261 -0
- package/server/ideabox-routes.js +12 -11
- package/server/project-root.js +7 -4
- package/server/qa-scope-routes.js +96 -0
- package/server/session-routes.js +2 -2
- package/server/stratum-sync.js +85 -9
- package/server/validate-routes.js +70 -0
- package/server/vision-routes.js +46 -2
- package/server/vision-server.js +25 -3
- package/server/vision-utils.js +23 -4
- package/dist/assets/App-DOerhoDp.js +0 -706
- package/dist/assets/architectureDiagram-3BPJPVTR-C4hfxNOw.js +0 -36
- package/dist/assets/blockDiagram-GPEHLZMM-DRqkGncy.js +0 -132
- package/dist/assets/channel-KsY1x-7s.js +0 -1
- package/dist/assets/chunk-727SXJPM-CeKpXDif.js +0 -206
- package/dist/assets/chunk-AQP2D5EJ-y2XL-YHV.js +0 -231
- package/dist/assets/classDiagram-4FO5ZUOK-zDEDoVPk.js +0 -1
- package/dist/assets/classDiagram-v2-Q7XG4LA2-zDEDoVPk.js +0 -1
- package/dist/assets/dagre-BM42HDAG-klZ_GOcw.js +0 -4
- package/dist/assets/diagram-2AECGRRQ-D0FmM6Wf.js +0 -43
- package/dist/assets/diagram-5GNKFQAL-DYuhby6q.js +0 -10
- package/dist/assets/diagram-KO2AKTUF-CCtQZgal.js +0 -3
- package/dist/assets/diagram-LMA3HP47-Cd1OqEln.js +0 -24
- package/dist/assets/diagram-OG6HWLK6-CiJrhoxo.js +0 -24
- package/dist/assets/ganttDiagram-6RSMTGT7-EPbL_2Hr.js +0 -292
- package/dist/assets/gitGraphDiagram-PVQCEYII-COCoF_vW.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-DlgxA8dX.js +0 -123
- package/dist/assets/infoDiagram-5YYISTIA-B1sJw6j5.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-CRtmCvwV.js +0 -30
- package/dist/assets/quadrantDiagram-W4KKPZXB-CsBLWg27.js +0 -7
- package/dist/assets/sankeyDiagram-5OEKKPKP-BPdMN8UU.js +0 -40
- package/dist/assets/sequenceDiagram-3UESZ5HK-D4AENdze.js +0 -162
- package/dist/assets/stateDiagram-AJRCARHV-DDplpCf_.js +0 -1
- package/dist/assets/stateDiagram-v2-BHNVJYJU-J4t9AVzF.js +0 -1
- package/dist/assets/wardley-L42UT6IY-WERqOmJt.js +0 -173
- package/dist/assets/wardleyDiagram-YWT4CUSO-5teUSOiI.js +0 -78
package/lib/build.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, renameSync, mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
12
|
-
import { join, resolve, dirname } from 'node:path';
|
|
12
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { homedir, tmpdir } from 'node:os';
|
|
15
15
|
import { execSync, execFileSync } from 'node:child_process';
|
|
@@ -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';
|
|
@@ -35,7 +36,8 @@ import { rtkPrefix } from './rtk.js';
|
|
|
35
36
|
|
|
36
37
|
import YAML from 'yaml';
|
|
37
38
|
// feature-json direct imports removed — mutations now go through TrackerProvider (T9)
|
|
38
|
-
import { loadFeaturesDir } from './project-paths.js';
|
|
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
|
*/
|
|
@@ -605,10 +648,19 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
605
648
|
// `docs/features/<featureCode>/`. Callers must use this (not inline
|
|
606
649
|
// string concatenation) so the bug-mode path stays in sync.
|
|
607
650
|
// COMP-MCP-MIGRATION-2: feature-mode honors paths.features override.
|
|
651
|
+
// COMP-PATHS-EXTERNAL D7: two representations on purpose.
|
|
652
|
+
// - `featuresDir` stays RELATIVE (loadFeaturesDir): it flows into
|
|
653
|
+
// context.featuresDir → the MCP-enforcement guard, which matches against
|
|
654
|
+
// repo-relative `git status` output. External artifacts live in another
|
|
655
|
+
// repo and are out of that guard's scope by construction; making this
|
|
656
|
+
// absolute would break the relative match. triage consumers are
|
|
657
|
+
// absolute-safe (resolvePathValue) so the relative value is fine there too.
|
|
658
|
+
// - `resolveItemDir` resolves the actual FILESYSTEM dir via
|
|
659
|
+
// resolveFeaturesPath (absolute — handles absolute/../-escaping config).
|
|
608
660
|
const featuresDir = loadFeaturesDir(cwd);
|
|
609
661
|
const resolveItemDir = (code) => isBugMode
|
|
610
662
|
? join(cwd, 'docs', 'bugs', code)
|
|
611
|
-
: join(cwd,
|
|
663
|
+
: join(resolveFeaturesPath(cwd), code);
|
|
612
664
|
|
|
613
665
|
// COMP-MCP-MIGRATION-1: per-build correlation ID stamped onto every audit
|
|
614
666
|
// row written during this run, so `executeShipStep`'s pre-stage scan can
|
|
@@ -663,7 +715,7 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
663
715
|
}
|
|
664
716
|
let composeConfig = {};
|
|
665
717
|
try { composeConfig = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* use defaults */ }
|
|
666
|
-
const contextDirPath =
|
|
718
|
+
const contextDirPath = resolveContextPath(cwd);
|
|
667
719
|
|
|
668
720
|
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY (D5): per-task pre-merge gate is opt-in.
|
|
669
721
|
// Resolve ONCE and thread into startFresh's planInputs only when the capability
|
|
@@ -737,28 +789,38 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
737
789
|
// verifyPipelineIntegrity() re-reads from disk, so we must compare against the original file content.
|
|
738
790
|
const specFileHash = _sha256(specYaml);
|
|
739
791
|
|
|
740
|
-
// Apply triage profile
|
|
741
|
-
|
|
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) {
|
|
742
799
|
try {
|
|
743
800
|
const specObj = YAML.parse(specYaml);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
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
|
+
}
|
|
760
819
|
}
|
|
761
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));
|
|
762
824
|
specYaml = YAML.stringify(specObj);
|
|
763
825
|
} catch (err) {
|
|
764
826
|
// Non-fatal — fall back to unmodified spec
|
|
@@ -864,6 +926,33 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
864
926
|
let response;
|
|
865
927
|
let isFreshStart = true;
|
|
866
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
|
+
|
|
867
956
|
// COMP-FIX-HARD T8: explicit `--resume` flag (compose fix <code> --resume).
|
|
868
957
|
// When opts.resumeFlowId is set, skip stratum.plan entirely and resume the
|
|
869
958
|
// given flow. CLI validates the flowId belongs to this code before calling.
|
|
@@ -888,6 +977,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
888
977
|
console.log(`Resuming flow ${opts.resumeFlowId} for ${featureCode}...`);
|
|
889
978
|
response = await stratum.resume(opts.resumeFlowId);
|
|
890
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);
|
|
891
983
|
// Refresh active-build.json so streaming/UI sees this as the live build.
|
|
892
984
|
const flowName = extractFlowName(specYaml, templateName);
|
|
893
985
|
writeActiveBuild(dataDir, {
|
|
@@ -904,6 +996,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
904
996
|
violations: [],
|
|
905
997
|
status: 'running',
|
|
906
998
|
resumedAt: new Date().toISOString(),
|
|
999
|
+
// COMP-CODEX-IMPL: carry the (restored or default) roles forward on resume-refresh.
|
|
1000
|
+
implementerAgent,
|
|
1001
|
+
reviewerAgent,
|
|
907
1002
|
});
|
|
908
1003
|
} else if (active && active.featureCode === featureCode && active.flowId) {
|
|
909
1004
|
// Same feature — try to resume or start fresh
|
|
@@ -916,10 +1011,10 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
916
1011
|
`Previous build for ${featureCode} was in ${active.mode} mode, ` +
|
|
917
1012
|
`current invocation is ${mode} mode. Starting fresh.`
|
|
918
1013
|
);
|
|
919
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1014
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
920
1015
|
} else if (active.status && active.status !== 'running') {
|
|
921
1016
|
console.log(`Previous build ${active.status}. Starting fresh.`);
|
|
922
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1017
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
923
1018
|
} else if (active.pid && active.pid !== process.pid && isProcessAlive(active.pid)) {
|
|
924
1019
|
// Same feature, different live process — block
|
|
925
1020
|
throw new Error(
|
|
@@ -932,10 +1027,12 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
932
1027
|
response = await stratum.resume(active.flowId);
|
|
933
1028
|
if (isTerminalFlow(response.status)) {
|
|
934
1029
|
console.log(`Previous build already ${response.status}. Starting fresh.`);
|
|
935
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1030
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
936
1031
|
} else {
|
|
937
1032
|
console.log(`Resuming from step: ${response.step_id}`);
|
|
938
1033
|
isFreshStart = false;
|
|
1034
|
+
// COMP-CODEX-IMPL: real resume — restore roles from the persisted build.
|
|
1035
|
+
restoreRolesFromActive(active);
|
|
939
1036
|
}
|
|
940
1037
|
} catch (err) {
|
|
941
1038
|
const recoverable = err?.code === 'flow_not_found'
|
|
@@ -943,7 +1040,7 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
943
1040
|
|| err?.message?.includes('No active flow');
|
|
944
1041
|
if (recoverable) {
|
|
945
1042
|
console.log('Previous flow not found. Starting fresh.');
|
|
946
|
-
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate);
|
|
1043
|
+
response = await startFresh(stratum, specYaml, featureCode, description, dataDir, templateName, mode, preMergeGate, roles);
|
|
947
1044
|
} else {
|
|
948
1045
|
throw err;
|
|
949
1046
|
}
|
|
@@ -953,7 +1050,54 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
953
1050
|
// Different feature or no active build — start fresh.
|
|
954
1051
|
// active-build.json is last-writer-wins: concurrent builds for
|
|
955
1052
|
// different features are allowed; the UI shows the most recent.
|
|
956
|
-
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
|
+
}
|
|
957
1101
|
}
|
|
958
1102
|
|
|
959
1103
|
// Update vision state
|
|
@@ -962,6 +1106,9 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
962
1106
|
// Stream writer — instantiated after plan/resume succeeds to prevent
|
|
963
1107
|
// a rejected/duplicate invocation from truncating an active build's stream.
|
|
964
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);
|
|
965
1112
|
streamWriter = new BuildStreamWriter(composeDir, featureCode, { truncate: isFreshStart });
|
|
966
1113
|
streamWriter.write({
|
|
967
1114
|
type: isFreshStart ? 'build_start' : 'build_resume',
|
|
@@ -1002,10 +1149,17 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1002
1149
|
contextDir: contextDirPath,
|
|
1003
1150
|
stepHistory,
|
|
1004
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,
|
|
1005
1155
|
enforceMcpForFeatureMgmt,
|
|
1006
1156
|
build_id,
|
|
1007
1157
|
buildStartedAt,
|
|
1008
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,
|
|
1009
1163
|
...(isBugMode ? { bug_code: featureCode } : {}),
|
|
1010
1164
|
};
|
|
1011
1165
|
|
|
@@ -1594,6 +1748,14 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1594
1748
|
// If no test framework is detected and no test directory exists, scaffold first.
|
|
1595
1749
|
// If the project has no tests at all (truly empty), skip coverage gracefully.
|
|
1596
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
|
+
}
|
|
1597
1759
|
const detected = detectTestFramework(agentCwd);
|
|
1598
1760
|
const hasTestDir = existsSync(join(agentCwd, 'test')) ||
|
|
1599
1761
|
existsSync(join(agentCwd, 'tests')) ||
|
|
@@ -1641,6 +1803,37 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1641
1803
|
}
|
|
1642
1804
|
}
|
|
1643
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
|
+
|
|
1644
1837
|
let childResult = await executeChildFlow(
|
|
1645
1838
|
response, stratum, context,
|
|
1646
1839
|
visionWriter, itemId, dataDir, opts.gateOpts ?? {}, progress,
|
|
@@ -1649,7 +1842,12 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1649
1842
|
|
|
1650
1843
|
// STRAT-REV-7: After review child flow completes, run cross-model synthesis if diff is large.
|
|
1651
1844
|
// If cross-model ran, skip the pipeline's separate codex_review step (avoid duplicate Codex pass).
|
|
1652
|
-
|
|
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) {
|
|
1653
1851
|
const mergedResult = childResult.output;
|
|
1654
1852
|
const synthesized = await runCrossModelReview(
|
|
1655
1853
|
mergedResult,
|
|
@@ -1677,6 +1875,18 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1677
1875
|
buildSignals.test_coverage = childResult.output;
|
|
1678
1876
|
} else if (childFlowName === 'parallel_review' && childResult?.output != null) {
|
|
1679
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
|
+
});
|
|
1680
1890
|
}
|
|
1681
1891
|
|
|
1682
1892
|
// Report child completion envelope to parent flow step.
|
|
@@ -1739,7 +1949,8 @@ export async function runBuild(featureCode, opts = {}) {
|
|
|
1739
1949
|
}
|
|
1740
1950
|
progress.retry('build', stepId, response.agent);
|
|
1741
1951
|
const violations = response.violations ?? [];
|
|
1742
|
-
|
|
1952
|
+
// STRAT-VOCAB-3: surface vocabulary violations as must-fix (they block ship).
|
|
1953
|
+
if (violations.length > 0) progress.findings(tagVocabularyViolations(violations));
|
|
1743
1954
|
// STRAT-REV-5: defensive fallback — for non-flow-step paths named 'review'.
|
|
1744
1955
|
// For flow-steps the ensure fires inside executeChildFlow (see handler below).
|
|
1745
1956
|
if ((response.step_id ?? stepId) === 'review') {
|
|
@@ -2219,6 +2430,43 @@ export function maybeEmitSectionsAfterPlanGate(stepId, featureDir, opts = {}) {
|
|
|
2219
2430
|
// Ship step — runs git commit in-process (not via agent)
|
|
2220
2431
|
// ---------------------------------------------------------------------------
|
|
2221
2432
|
|
|
2433
|
+
/**
|
|
2434
|
+
* The git repository toplevel that contains `dir`, or null if `dir` is not
|
|
2435
|
+
* inside any git work tree. Used so ship decides commit ownership per-file by
|
|
2436
|
+
* containing repo, not by assuming the workspace root == the repo
|
|
2437
|
+
* (COMP-PATHS-EXTERNAL D6a).
|
|
2438
|
+
*/
|
|
2439
|
+
function gitToplevel(dir) {
|
|
2440
|
+
try {
|
|
2441
|
+
return execSync('git rev-parse --show-toplevel', {
|
|
2442
|
+
cwd: dir, encoding: 'utf-8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
2443
|
+
}).trim() || null;
|
|
2444
|
+
} catch { return null; }
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* For each owned artifact (ROADMAP, the feature folder) that resolves into a
|
|
2449
|
+
* git repo OTHER than the build's repo, log a one-line "commit it there"
|
|
2450
|
+
* notice. v1 does not auto-commit other repos (that is COMP-PATHS-EXTERNAL-1).
|
|
2451
|
+
* Artifacts in the build's own repo, or in no repo at all, produce no notice.
|
|
2452
|
+
*/
|
|
2453
|
+
function noticeExternalArtifacts(cwd, featureCode, buildToplevel) {
|
|
2454
|
+
try {
|
|
2455
|
+
const candidates = [
|
|
2456
|
+
['ROADMAP.md', resolveRoadmapPath(cwd)],
|
|
2457
|
+
['feature folder', join(resolveFeaturesPath(cwd), featureCode)],
|
|
2458
|
+
];
|
|
2459
|
+
for (const [label, abs] of candidates) {
|
|
2460
|
+
if (!existsSync(abs)) continue;
|
|
2461
|
+
const top = gitToplevel(dirname(abs));
|
|
2462
|
+
if (top && top !== buildToplevel) {
|
|
2463
|
+
// eslint-disable-next-line no-console
|
|
2464
|
+
console.warn(`[build/ship] 📝 wrote ${label} in ${top} — commit it there (Compose does not auto-commit other repos in v1)`);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
} catch { /* notice is best-effort */ }
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2222
2470
|
/**
|
|
2223
2471
|
* Execute the ship step: run tests, stage feature files, commit.
|
|
2224
2472
|
* Returns a PhaseResult-shaped object.
|
|
@@ -2231,6 +2479,11 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2231
2479
|
? `docs/bugs/${featureCode}`
|
|
2232
2480
|
: `${featuresDir}/${featureCode}`;
|
|
2233
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
|
+
|
|
2234
2487
|
try {
|
|
2235
2488
|
// 0. Check if we're in a git repository — if not, skip git operations
|
|
2236
2489
|
let isGitRepo = false;
|
|
@@ -2240,22 +2493,57 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2240
2493
|
} catch { /* not a git repo */ }
|
|
2241
2494
|
|
|
2242
2495
|
if (!isGitRepo) {
|
|
2496
|
+
// COMP-PATHS-EXTERNAL D6b: there is no repo to commit into (e.g. a
|
|
2497
|
+
// forge-top-shaped workspace), but the lifecycle must still advance —
|
|
2498
|
+
// record a commit-less completion (null-SHA) so status flips to COMPLETE.
|
|
2499
|
+
// Best-effort: a completion failure must not fail ship.
|
|
2500
|
+
let completionWarning = null;
|
|
2501
|
+
if (featureCode) {
|
|
2502
|
+
try {
|
|
2503
|
+
const { recordCompletion } = await import('./completion-writer.js');
|
|
2504
|
+
await recordCompletion(cwd, {
|
|
2505
|
+
feature_code: featureCode,
|
|
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).
|
|
2509
|
+
tests_pass: true,
|
|
2510
|
+
files_changed: [],
|
|
2511
|
+
notes: description.split('\n')[0].slice(0, 72),
|
|
2512
|
+
...(builtVia ? { built_via: builtVia } : {}),
|
|
2513
|
+
});
|
|
2514
|
+
} catch (err) {
|
|
2515
|
+
completionWarning = `completion record failed (${err.code || 'UNKNOWN'}): ${err.message}`;
|
|
2516
|
+
// eslint-disable-next-line no-console
|
|
2517
|
+
console.warn(`[build/ship] ${featureCode}: ${completionWarning}`);
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2243
2520
|
return {
|
|
2244
2521
|
phase: 'ship',
|
|
2245
2522
|
artifact: 'no-git',
|
|
2246
2523
|
outcome: 'complete',
|
|
2247
|
-
summary: 'No git repository — commit skipped
|
|
2524
|
+
summary: 'No git repository — wrote artifacts, recorded completion (commit skipped)',
|
|
2525
|
+
commit: null,
|
|
2526
|
+
...(completionWarning ? { completionWarning } : {}),
|
|
2248
2527
|
};
|
|
2249
2528
|
}
|
|
2250
2529
|
|
|
2251
2530
|
// 1. Run feature-relevant tests (best-effort — don't block ship on test infra issues)
|
|
2252
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 };
|
|
2253
2536
|
try {
|
|
2254
2537
|
// COMP-TEST-BOOTSTRAP item 128: use detected test command instead of hard-coded npm test
|
|
2255
2538
|
const testFramework = detectTestFramework(agentCwd);
|
|
2256
2539
|
const testCommand = testFramework?.command ?? 'npm test';
|
|
2257
|
-
execSync(`${testCommand} 2>&1 || true`, { cwd: agentCwd, encoding: 'utf-8', timeout: 120_000 });
|
|
2258
|
-
|
|
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
|
+
}
|
|
2259
2547
|
|
|
2260
2548
|
// 2. Collect files to stage
|
|
2261
2549
|
const filesToStage = new Set();
|
|
@@ -2316,6 +2604,18 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2316
2604
|
const dataDir = join(cwd, '.compose', 'data');
|
|
2317
2605
|
const mode = readEnforcementMode(dataDir);
|
|
2318
2606
|
if (mode !== 'off') {
|
|
2607
|
+
// COMP-PATHS-EXTERNAL: the pre-stage guard matches repo-relative
|
|
2608
|
+
// `git status`, so relocated canon (an external paths.roadmap /
|
|
2609
|
+
// paths.features) is OUT of its scope — unauthorized edits there are
|
|
2610
|
+
// NOT enforced. Surface that visibly instead of failing silently.
|
|
2611
|
+
// Full external-canon enforcement needs cross-repo dirty detection
|
|
2612
|
+
// (tracked with COMP-PATHS-EXTERNAL-1).
|
|
2613
|
+
for (const [label, abs] of [['paths.roadmap', resolveRoadmapPath(cwd)], ['paths.features', resolveFeaturesPath(cwd)]]) {
|
|
2614
|
+
if (relative(cwd, abs).startsWith('..')) {
|
|
2615
|
+
// eslint-disable-next-line no-console
|
|
2616
|
+
console.warn(`[ship] MCP enforcement (${mode}) does NOT cover external ${label} (${abs}) — edits there are unguarded in v1.`);
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2319
2619
|
const events = readEvents(cwd, { since: context.buildStartedAt });
|
|
2320
2620
|
const { violations } = scanGuarded({
|
|
2321
2621
|
dirtyFiles: featureFiles,
|
|
@@ -2427,9 +2727,12 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2427
2727
|
await recordCompletion(cwd, {
|
|
2428
2728
|
feature_code: featureCode,
|
|
2429
2729
|
commit_sha: sha,
|
|
2430
|
-
|
|
2730
|
+
// COMP-TEST-BOOTSTRAP-4: derived from the parsed test run above
|
|
2731
|
+
// (true when unparseable — never a false block).
|
|
2732
|
+
tests_pass: testsPass,
|
|
2431
2733
|
files_changed: filesChanged,
|
|
2432
2734
|
notes: shortDesc,
|
|
2735
|
+
...(builtVia ? { built_via: builtVia } : {}),
|
|
2433
2736
|
});
|
|
2434
2737
|
if (progress) progress.toolUse('ship', `Recorded completion for ${featureCode}`);
|
|
2435
2738
|
} catch (err) {
|
|
@@ -2441,6 +2744,11 @@ export async function executeShipStep(featureCode, agentCwd, cwd, context, descr
|
|
|
2441
2744
|
}
|
|
2442
2745
|
}
|
|
2443
2746
|
|
|
2747
|
+
// COMP-PATHS-EXTERNAL D6a: if ROADMAP / the feature folder resolved into a
|
|
2748
|
+
// DIFFERENT git repo, they were written but not committed here — tell the
|
|
2749
|
+
// user to commit them there (v1 does not auto-commit other repos).
|
|
2750
|
+
noticeExternalArtifacts(cwd, featureCode, gitToplevel(agentCwd));
|
|
2751
|
+
|
|
2444
2752
|
return {
|
|
2445
2753
|
phase: 'ship',
|
|
2446
2754
|
artifact: sha ?? '',
|
|
@@ -2907,7 +3215,8 @@ export async function executeChildFlow(
|
|
|
2907
3215
|
});
|
|
2908
3216
|
}
|
|
2909
3217
|
const violations = resp.violations ?? [];
|
|
2910
|
-
|
|
3218
|
+
// STRAT-VOCAB-3: surface vocabulary violations as must-fix (they block ship).
|
|
3219
|
+
if (violations.length > 0 && progress) progress.findings(tagVocabularyViolations(violations));
|
|
2911
3220
|
// STRAT-REV-5: flow-step ensures fire here (inside executeChildFlow), not in the
|
|
2912
3221
|
// main dispatch loop — persist dirty lenses so triage selectively re-runs on retry.
|
|
2913
3222
|
if (resp.step_id === 'review') {
|
|
@@ -2918,7 +3227,14 @@ export async function executeChildFlow(
|
|
|
2918
3227
|
}
|
|
2919
3228
|
}
|
|
2920
3229
|
const stepAgent = resp.agent ?? 'claude';
|
|
2921
|
-
|
|
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';
|
|
2922
3238
|
|
|
2923
3239
|
if (progress) {
|
|
2924
3240
|
progress.fix(childFlowName, fixAgent, resp.step_id);
|
|
@@ -4299,7 +4615,7 @@ export async function executeParallelDispatch(
|
|
|
4299
4615
|
}
|
|
4300
4616
|
}
|
|
4301
4617
|
|
|
4302
|
-
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) {
|
|
4303
4619
|
const flowName = extractFlowName(specYaml, templateName);
|
|
4304
4620
|
console.log(`Starting ${flowName} for ${featureCode}...`);
|
|
4305
4621
|
// COMP-FIX-HARD T4: bug-mode flows take input as { task: <description> }
|
|
@@ -4308,9 +4624,14 @@ export async function startFresh(stratum, specYaml, featureCode, description, da
|
|
|
4308
4624
|
// COMP-PAR-MERGE-QUEUE-CONSUMER-RETRY (D5): fold pre_merge_gate into the
|
|
4309
4625
|
// feature plan envelope ONLY when resolved (undefined ⇒ key omitted, not [])
|
|
4310
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';
|
|
4311
4632
|
const planInputs = mode === 'bug'
|
|
4312
4633
|
? { task: description }
|
|
4313
|
-
: { featureCode, description, ...(preMergeGate !== undefined ? { pre_merge_gate: preMergeGate } : {}) };
|
|
4634
|
+
: { featureCode, description, implementer_agent: implementerAgent, reviewer_agent: reviewerAgent, ...(preMergeGate !== undefined ? { pre_merge_gate: preMergeGate } : {}) };
|
|
4314
4635
|
const response = await stratum.plan(specYaml, flowName, planInputs);
|
|
4315
4636
|
|
|
4316
4637
|
writeActiveBuild(dataDir, {
|
|
@@ -4327,6 +4648,10 @@ export async function startFresh(stratum, specYaml, featureCode, description, da
|
|
|
4327
4648
|
violations: [],
|
|
4328
4649
|
status: 'running',
|
|
4329
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,
|
|
4330
4655
|
});
|
|
4331
4656
|
|
|
4332
4657
|
return response;
|