@smartmemory/compose 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/compose/SKILL.md +12 -3
- package/.compose-deps.json +51 -25
- package/README.md +79 -7
- package/bin/compose.js +495 -360
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/{_baseUniq-Bo837sRJ.js → _baseUniq-ClWoCPFl.js} +1 -1
- package/dist/assets/{arc-BafGpyqE.js → arc-DY26UIVo.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BOBfUsqL.js → architectureDiagram-Q4EWVU46-6Ggq4DqJ.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dwodev1a.js → blockDiagram-DXYQGD6D-CH3Ked0l.js} +1 -1
- package/dist/assets/{browser-1ntj1-x_.js → browser-BWkrenen.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CU_bhYag.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-p8WsDwnO.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-B8h7-eR0.js → chunk-4TB4RGXK-JytR14a9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DxeEr98s.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-BYt8F151.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DGSOVeie.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B-QdgYR2.js → chunk-OYMX7WX6-BySQzVxc.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Du5UAZLs.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-C8JbNBSk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-O1ESaqge.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-CPTmFPHw.js → dagre-KV5264BT-Cp3F5KTn.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-B3PNrWs5.js → diagram-5BDNPKRD-DiR6_2q_.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-Cscfr6vc.js → diagram-G4DWMVQ6-w0i-p5HX.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CSfqZ-TM.js → diagram-MMDJMWI5-tIHhwUv3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cg4aYS7W.js → diagram-TYMM5635-BAeY3B19.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-_ZqwG5pl.js → erDiagram-SMLLAGMA-Ckx_Knko.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C83boxFT.js → flowDiagram-DWJPFMVM-DeoNka6J.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CWnIjuEi.js → ganttDiagram-T4ZO3ILL-BmGnFbEg.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DrMdxZfH.js → gitGraphDiagram-UUTBAWPF-Dk48IHsx.js} +1 -1
- package/dist/assets/{graph-RE4I7Ty7.js → graph-BNzKGvoy.js} +1 -1
- package/dist/assets/{graph-Bi99_6Yf.js → graph-CI_1htl0.js} +1 -1
- package/dist/assets/{index-Rm2RE-c0.js → index-BEfrNBp8.js} +3 -3
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-BLmP4Epr.js → infoDiagram-42DDH7IO-BRf827i0.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-yuWWshKN.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BOfhaJov.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-Bbolde15.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +1 -1
- package/dist/assets/{layout-BSf33zm8.js → layout-BI8cXFPI.js} +1 -1
- package/dist/assets/{linear-AvSTWMqx.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/{min-QBM8H4xN.js → min-vPHfnXcC.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BuvgtqIc.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +1 -1
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/{pieDiagram-DEJITSTG-DIzF16vh.js → pieDiagram-DEJITSTG-Cno-gETh.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D-mbUIjS.js → quadrantDiagram-34T5L4WZ-BUQM1Hfm.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-CEs4kCLd.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DFsnCr9n.js → sankeyDiagram-XADWPNL6-Crynd3_b.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-BEJYdTjQ.js → sequenceDiagram-FGHM5R23-D9fZdCM8.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-BBXs57uY.js → stateDiagram-FHFEXIEX-CW9qVec8.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-BGvLoVAY.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-9LaBTMe0.js → vennDiagram-DHZGUBPP-BfytJcWk.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-P4MEqMTP.js → wardley-RL74JXVD-DLj-IjyB.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-o-tmxnlC.js → wardleyDiagram-NUSXRM2D-Ds0Ue68c.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-Dpn7V6qk.js → xychartDiagram-5P7HB3ND-vjWDXFL6.js} +1 -1
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +9 -0
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +874 -170
- package/lib/canon-guard.js +28 -6
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +104 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +26 -13
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/deps.js +164 -7
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +3 -3
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +57 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -23
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +9 -1
- package/lib/judgment/store/records.js +1 -1
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +5 -1
- package/lib/judgment-writer.js +56 -2
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +52 -1
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +396 -199
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +271 -36
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +109 -23
- package/server/compose-mcp.js +88 -882
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +34 -2
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-PkZzHeMj.js +0 -894
- package/dist/assets/channel-qVK_qn4E.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-B8UcfC1q.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-B8UcfC1q.js +0 -1
- package/dist/assets/clone-Pu3RyLUh.js +0 -1
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/mobile-BnXEOE3U.js +0 -17
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BqKuX4rj.js +0 -1
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/lib/lineage.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lineage.js — COMP-PROV-LINEAGE: W3C PROV-O artifact lineage (vocabulary only).
|
|
3
|
+
*
|
|
4
|
+
* Compose artifacts form a derivation chain — design.md produces blueprint.md
|
|
5
|
+
* produces plan.md produces report.md — but nothing recorded it, so when an
|
|
6
|
+
* upstream artifact changes, downstream artifacts went stale silently.
|
|
7
|
+
*
|
|
8
|
+
* We borrow the W3C PROV-O vocabulary (https://www.w3.org/TR/prov-o/), NOT the
|
|
9
|
+
* RDF stack. See docs/features/COMP-PROV-LINEAGE/prov-o-mapping.md for the full
|
|
10
|
+
* term mapping. In Compose terms:
|
|
11
|
+
*
|
|
12
|
+
* prov:Entity → a lifecycle artifact (design.md, blueprint.md, ...)
|
|
13
|
+
* prov:Activity → a lifecycle phase (explore_design, blueprint, ...)
|
|
14
|
+
* prov:wasGeneratedBy → the phase that produced this artifact (Entity → Activity)
|
|
15
|
+
* prov:wasDerivedFrom → the upstream artifact(s) this was built on (Entity → Entity)
|
|
16
|
+
*
|
|
17
|
+
* `wasDerivedFrom` is the load-bearing edge: staleness becomes a graph
|
|
18
|
+
* reachability query over it rather than bespoke logic.
|
|
19
|
+
*
|
|
20
|
+
* Storage: the lineage lives as HTML-comment markers in the first lines of each
|
|
21
|
+
* artifact (COMP-PROV-LINEAGE decision, embedded markers over a feature.json
|
|
22
|
+
* block — canonical artifacts stay auto-discovered, never registered):
|
|
23
|
+
*
|
|
24
|
+
* <!-- wasGeneratedBy: blueprint -->
|
|
25
|
+
* <!-- wasDerivedFrom: design.md -->
|
|
26
|
+
*
|
|
27
|
+
* The reachability query does NOT require the markers to have been stamped: for
|
|
28
|
+
* canonical artifacts it falls back to CANONICAL_CHAIN, so it works on any
|
|
29
|
+
* feature folder today. Markers only override the default derivation.
|
|
30
|
+
*
|
|
31
|
+
* Explicitly out of scope: RDF runtime, triple store, SPARQL, JSON-LD. A
|
|
32
|
+
* JSON-LD export stays possible later by walking these same edges.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { readFileSync, writeFileSync, existsSync, statSync, utimesSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import { artifactsOf, edgeEvidenceOf } from './lifecycle-modes.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Canonical derivation chain for the `build` lifecycle, in derivation order.
|
|
41
|
+
* Each entry is { file, phase } where `phase` is the prov:Activity that
|
|
42
|
+
* generates the prov:Entity `file`.
|
|
43
|
+
*
|
|
44
|
+
* Ordered so that entry N wasDerivedFrom the nearest EXISTING predecessor
|
|
45
|
+
* (predecessors may be skipped — prd/architecture/report are skippable phases).
|
|
46
|
+
*
|
|
47
|
+
* Mirrors lib/lifecycle-modes.js build mode. A test
|
|
48
|
+
* (test/lineage.test.js) asserts this stays consistent with
|
|
49
|
+
* `artifactsOf('build')` / `edgeEvidenceOf('build')` so the two never drift.
|
|
50
|
+
*/
|
|
51
|
+
export const CANONICAL_CHAIN = [
|
|
52
|
+
{ file: 'design.md', phase: 'explore_design' },
|
|
53
|
+
{ file: 'prd.md', phase: 'prd' },
|
|
54
|
+
{ file: 'architecture.md', phase: 'architecture' },
|
|
55
|
+
{ file: 'blueprint.md', phase: 'blueprint' },
|
|
56
|
+
{ file: 'plan.md', phase: 'plan' },
|
|
57
|
+
{ file: 'report.md', phase: 'report' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const CHAIN_FILES = CANONICAL_CHAIN.map((e) => e.file);
|
|
61
|
+
const PHASE_BY_FILE = new Map(CANONICAL_CHAIN.map((e) => [e.file, e.phase]));
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The prov:Activity (phase) that generates a canonical artifact, or null if the
|
|
65
|
+
* file is not a known canonical artifact.
|
|
66
|
+
* @param {string} filename
|
|
67
|
+
* @returns {string|null}
|
|
68
|
+
*/
|
|
69
|
+
export function generatingPhaseOf(filename) {
|
|
70
|
+
return PHASE_BY_FILE.get(filename) ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract PROV-O lineage markers from the first lines of artifact text.
|
|
75
|
+
* Reads only the header region (first 8 lines) — a cheap header scan.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} content
|
|
78
|
+
* @returns {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }}
|
|
79
|
+
*/
|
|
80
|
+
export function extractLineageMarkers(content) {
|
|
81
|
+
const lines = content.split('\n').slice(0, 8);
|
|
82
|
+
let wasGeneratedBy = null;
|
|
83
|
+
let wasDerivedFrom = null; // null = no usable marker present
|
|
84
|
+
for (const line of lines) {
|
|
85
|
+
// Anchored to a full line (optional surrounding whitespace) so a marker must
|
|
86
|
+
// BE the line, not merely appear inside prose or a fenced code example.
|
|
87
|
+
const g = line.match(/^\s*<!--\s*wasGeneratedBy:\s*([\w_-]+)\s*-->\s*$/);
|
|
88
|
+
if (g) wasGeneratedBy = g[1];
|
|
89
|
+
const d = line.match(/^\s*<!--\s*wasDerivedFrom:\s*([^>]*?)\s*-->\s*$/);
|
|
90
|
+
if (d) {
|
|
91
|
+
const parents = d[1]
|
|
92
|
+
.split(',')
|
|
93
|
+
.map((s) => s.trim())
|
|
94
|
+
// Parents are sibling artifact filenames — never a path. Drop anything
|
|
95
|
+
// with a separator or traversal so a hand-edited marker cannot point the
|
|
96
|
+
// derivation graph outside the feature folder.
|
|
97
|
+
.filter((s) => s && !s.includes('/') && !s.includes('\\') && !s.includes('..'));
|
|
98
|
+
// An empty marker (`<!-- wasDerivedFrom: -->`) is treated as no override
|
|
99
|
+
// rather than an explicit "[]" state — Compose has no mid-chain
|
|
100
|
+
// origin-override use case, and the phantom [] broke stamp idempotency.
|
|
101
|
+
wasDerivedFrom = parents.length > 0 ? parents : null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { wasGeneratedBy, wasDerivedFrom };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Compute the canonical lineage for an artifact filename given which sibling
|
|
109
|
+
* artifacts actually exist in the feature folder. `wasDerivedFrom` is the
|
|
110
|
+
* nearest EXISTING predecessor in CANONICAL_CHAIN (so skipped phases don't
|
|
111
|
+
* break the edge); the first artifact in the chain derives from nothing.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} filename
|
|
114
|
+
* @param {string[]} existingFiles filenames present in the feature folder
|
|
115
|
+
* @returns {{ entity: string, wasGeneratedBy: string|null, wasDerivedFrom: string[] }}
|
|
116
|
+
*/
|
|
117
|
+
export function canonicalLineageOf(filename, existingFiles) {
|
|
118
|
+
const idx = CHAIN_FILES.indexOf(filename);
|
|
119
|
+
if (idx === -1) {
|
|
120
|
+
return { entity: filename, wasGeneratedBy: null, wasDerivedFrom: [] };
|
|
121
|
+
}
|
|
122
|
+
const present = new Set(existingFiles);
|
|
123
|
+
let wasDerivedFrom = [];
|
|
124
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
125
|
+
if (present.has(CHAIN_FILES[i])) {
|
|
126
|
+
wasDerivedFrom = [CHAIN_FILES[i]];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
entity: filename,
|
|
132
|
+
wasGeneratedBy: PHASE_BY_FILE.get(filename) ?? null,
|
|
133
|
+
wasDerivedFrom,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the effective lineage for an on-disk artifact: markers override the
|
|
139
|
+
* canonical chain. Returns null if the file does not exist.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} featureDir
|
|
142
|
+
* @param {string} filename
|
|
143
|
+
* @param {string[]} [existingFiles] optional precomputed sibling list
|
|
144
|
+
* @returns {{ entity: string, wasGeneratedBy: string|null, wasDerivedFrom: string[], source: 'marker'|'canonical' }|null}
|
|
145
|
+
*/
|
|
146
|
+
export function lineageOf(featureDir, filename, existingFiles) {
|
|
147
|
+
const filePath = join(featureDir, filename);
|
|
148
|
+
if (!existsSync(filePath)) return null;
|
|
149
|
+
const siblings = existingFiles ?? CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
|
|
150
|
+
|
|
151
|
+
let content;
|
|
152
|
+
try {
|
|
153
|
+
content = readFileSync(filePath, 'utf-8');
|
|
154
|
+
} catch {
|
|
155
|
+
content = '';
|
|
156
|
+
}
|
|
157
|
+
const markers = extractLineageMarkers(content);
|
|
158
|
+
const canonical = canonicalLineageOf(filename, siblings);
|
|
159
|
+
|
|
160
|
+
// Markers override; fall back to canonical per-field.
|
|
161
|
+
const usesMarker = markers.wasGeneratedBy !== null || markers.wasDerivedFrom !== null;
|
|
162
|
+
return {
|
|
163
|
+
entity: filename,
|
|
164
|
+
wasGeneratedBy: markers.wasGeneratedBy ?? canonical.wasGeneratedBy,
|
|
165
|
+
wasDerivedFrom: markers.wasDerivedFrom ?? canonical.wasDerivedFrom,
|
|
166
|
+
source: usesMarker ? 'marker' : 'canonical',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the derivation graph (child -> parents) for the canonical artifacts that
|
|
172
|
+
* exist in a feature folder, honouring marker overrides.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} featureDir
|
|
175
|
+
* @returns {Map<string, string[]>} filename -> its wasDerivedFrom parents
|
|
176
|
+
*/
|
|
177
|
+
export function buildDerivationGraph(featureDir) {
|
|
178
|
+
const existing = CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
|
|
179
|
+
const graph = new Map();
|
|
180
|
+
for (const file of existing) {
|
|
181
|
+
const lin = lineageOf(featureDir, file, existing);
|
|
182
|
+
graph.set(file, lin ? lin.wasDerivedFrom : []);
|
|
183
|
+
}
|
|
184
|
+
return graph;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reachability query — the core deliverable.
|
|
189
|
+
*
|
|
190
|
+
* Given an artifact that changed, return the downstream artifacts (transitive
|
|
191
|
+
* descendants via wasDerivedFrom) that are now STALE: they exist, they descend
|
|
192
|
+
* from the changed artifact, and their last-modified time is older than the
|
|
193
|
+
* changed artifact's (they were derived before the upstream changed).
|
|
194
|
+
*
|
|
195
|
+
* @param {string} featureDir
|
|
196
|
+
* @param {string} changedFile filename of the artifact that changed
|
|
197
|
+
* @returns {Array<{ file: string, stale: boolean, derivedFromChanged: boolean, mtimeMs: number }>}
|
|
198
|
+
* descendants of changedFile, stale ones first
|
|
199
|
+
*/
|
|
200
|
+
export function findStaleDescendants(featureDir, changedFile) {
|
|
201
|
+
const graph = buildDerivationGraph(featureDir); // child -> parents
|
|
202
|
+
|
|
203
|
+
// Invert to parent -> children for forward reachability.
|
|
204
|
+
const children = new Map();
|
|
205
|
+
for (const [child, parents] of graph) {
|
|
206
|
+
for (const parent of parents) {
|
|
207
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
208
|
+
children.get(parent).push(child);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// BFS forward from changedFile to collect all transitive descendants.
|
|
213
|
+
const descendants = new Set();
|
|
214
|
+
const queue = [...(children.get(changedFile) ?? [])];
|
|
215
|
+
while (queue.length > 0) {
|
|
216
|
+
const node = queue.shift();
|
|
217
|
+
if (descendants.has(node)) continue;
|
|
218
|
+
descendants.add(node);
|
|
219
|
+
for (const c of children.get(node) ?? []) queue.push(c);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const changedPath = join(featureDir, changedFile);
|
|
223
|
+
let changedMtime = 0;
|
|
224
|
+
try {
|
|
225
|
+
changedMtime = statSync(changedPath).mtimeMs;
|
|
226
|
+
} catch {
|
|
227
|
+
return []; // changed file gone — nothing to compare against
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const file of descendants) {
|
|
232
|
+
let mtimeMs = 0;
|
|
233
|
+
try {
|
|
234
|
+
mtimeMs = statSync(join(featureDir, file)).mtimeMs;
|
|
235
|
+
} catch {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
results.push({
|
|
239
|
+
file,
|
|
240
|
+
stale: mtimeMs < changedMtime,
|
|
241
|
+
derivedFromChanged: true,
|
|
242
|
+
mtimeMs,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
// Stale first, then by chain order for stable output.
|
|
246
|
+
results.sort((a, b) => {
|
|
247
|
+
if (a.stale !== b.stale) return a.stale ? -1 : 1;
|
|
248
|
+
return CHAIN_FILES.indexOf(a.file) - CHAIN_FILES.indexOf(b.file);
|
|
249
|
+
});
|
|
250
|
+
return results;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Global staleness scan — the changed-file-free form of the reachability query,
|
|
255
|
+
* for callers (the build health signal) that want "which artifacts are stale?"
|
|
256
|
+
* without a specific changed anchor.
|
|
257
|
+
*
|
|
258
|
+
* An artifact is stale when any of its transitive ancestors (via wasDerivedFrom)
|
|
259
|
+
* has a newer mtime than the artifact itself — i.e. an upstream it was built on
|
|
260
|
+
* has changed since. Returns one row per existing canonical artifact, shape
|
|
261
|
+
* `{ file, stale, staleAgainst }`, compatible with scoreDocFreshness (which only
|
|
262
|
+
* reads `.stale`).
|
|
263
|
+
*
|
|
264
|
+
* @param {string} featureDir
|
|
265
|
+
* @returns {Array<{ file: string, stale: boolean, staleAgainst: string[] }>}
|
|
266
|
+
*/
|
|
267
|
+
export function findStaleArtifacts(featureDir) {
|
|
268
|
+
const graph = buildDerivationGraph(featureDir); // child -> parents
|
|
269
|
+
|
|
270
|
+
const mtime = new Map();
|
|
271
|
+
for (const file of graph.keys()) {
|
|
272
|
+
try {
|
|
273
|
+
mtime.set(file, statSync(join(featureDir, file)).mtimeMs);
|
|
274
|
+
} catch { /* unreadable — omit from comparison */ }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const results = [];
|
|
278
|
+
for (const file of graph.keys()) {
|
|
279
|
+
if (!mtime.has(file)) continue;
|
|
280
|
+
|
|
281
|
+
// Collect transitive ancestors (cycle-safe).
|
|
282
|
+
const ancestors = new Set();
|
|
283
|
+
const queue = [...(graph.get(file) ?? [])];
|
|
284
|
+
while (queue.length > 0) {
|
|
285
|
+
const p = queue.shift();
|
|
286
|
+
if (ancestors.has(p)) continue;
|
|
287
|
+
ancestors.add(p);
|
|
288
|
+
for (const gp of graph.get(p) ?? []) queue.push(gp);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const staleAgainst = [];
|
|
292
|
+
for (const anc of ancestors) {
|
|
293
|
+
if (mtime.has(anc) && mtime.get(anc) > mtime.get(file)) staleAgainst.push(anc);
|
|
294
|
+
}
|
|
295
|
+
results.push({ file, stale: staleAgainst.length > 0, staleAgainst });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
results.sort((a, b) => CHAIN_FILES.indexOf(a.file) - CHAIN_FILES.indexOf(b.file));
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Render the PROV-O marker header lines for an artifact's lineage.
|
|
304
|
+
* @param {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }} lineage
|
|
305
|
+
* @returns {string[]} marker lines (may be empty)
|
|
306
|
+
*/
|
|
307
|
+
function renderMarkers(lineage) {
|
|
308
|
+
const out = [];
|
|
309
|
+
if (lineage.wasGeneratedBy) out.push(`<!-- wasGeneratedBy: ${lineage.wasGeneratedBy} -->`);
|
|
310
|
+
if (lineage.wasDerivedFrom && lineage.wasDerivedFrom.length > 0) {
|
|
311
|
+
out.push(`<!-- wasDerivedFrom: ${lineage.wasDerivedFrom.join(', ')} -->`);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const MARKER_RE = /^<!--\s*(?:wasGeneratedBy|wasDerivedFrom):[^>]*-->\s*$/;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Stamp PROV-O lineage markers into an artifact's content, idempotently.
|
|
320
|
+
* Existing wasGeneratedBy/wasDerivedFrom marker lines are removed and replaced,
|
|
321
|
+
* so re-stamping never duplicates. A leading `<!-- phase: -->` marker, if any
|
|
322
|
+
* artifact still carries one, is left untouched and lineage is inserted after it.
|
|
323
|
+
* Returns the new content (unchanged if lineage is empty and no markers present).
|
|
324
|
+
*
|
|
325
|
+
* Pure string transform — does not touch the filesystem. Callers persist.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} content
|
|
328
|
+
* @param {{ wasGeneratedBy: string|null, wasDerivedFrom: string[] }} lineage
|
|
329
|
+
* @returns {string}
|
|
330
|
+
*/
|
|
331
|
+
export function stampLineageContent(content, lineage) {
|
|
332
|
+
const markers = renderMarkers(lineage);
|
|
333
|
+
const lines = content.split('\n');
|
|
334
|
+
|
|
335
|
+
// Remove any existing lineage markers (anywhere in the header region or body —
|
|
336
|
+
// they are only ever ours), preserving everything else.
|
|
337
|
+
const kept = lines.filter((line) => !MARKER_RE.test(line.trim()));
|
|
338
|
+
|
|
339
|
+
if (markers.length === 0) return kept.join('\n');
|
|
340
|
+
|
|
341
|
+
// Insert markers directly after a leading `<!-- phase: -->` marker if present,
|
|
342
|
+
// otherwise at the very top, so all lineage lives together in the header.
|
|
343
|
+
let insertAt = 0;
|
|
344
|
+
if (kept[0] && /^<!--\s*phase:/.test(kept[0].trim())) insertAt = 1;
|
|
345
|
+
kept.splice(insertAt, 0, ...markers);
|
|
346
|
+
return kept.join('\n');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Stamp PROV-O lineage markers onto every existing canonical artifact in a
|
|
351
|
+
* feature folder, on disk. Effective lineage comes from `lineageOf`, so any
|
|
352
|
+
* hand-authored marker override is preserved (re-written identically, no diff).
|
|
353
|
+
* Idempotent: running twice makes no second change.
|
|
354
|
+
*
|
|
355
|
+
* This is the "lifecycle writer" surface — the point at which lineage markers
|
|
356
|
+
* are materialised into artifacts during the lifecycle (invoked by
|
|
357
|
+
* `compose lineage stamp`).
|
|
358
|
+
*
|
|
359
|
+
* @param {string} featureDir
|
|
360
|
+
* @returns {Array<{ file: string, changed: boolean, wasGeneratedBy: string|null, wasDerivedFrom: string[] }>}
|
|
361
|
+
*/
|
|
362
|
+
export function stampFeatureLineage(featureDir) {
|
|
363
|
+
const existing = CHAIN_FILES.filter((f) => existsSync(join(featureDir, f)));
|
|
364
|
+
const out = [];
|
|
365
|
+
for (const file of existing) {
|
|
366
|
+
const filePath = join(featureDir, file);
|
|
367
|
+
let content;
|
|
368
|
+
try {
|
|
369
|
+
content = readFileSync(filePath, 'utf-8');
|
|
370
|
+
} catch {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const lin = lineageOf(featureDir, file, existing);
|
|
374
|
+
const next = stampLineageContent(content, lin);
|
|
375
|
+
const changed = next !== content;
|
|
376
|
+
if (changed) {
|
|
377
|
+
// Stamping lineage markers is a metadata annotation, NOT a regeneration of
|
|
378
|
+
// the artifact — it must not reset the mtime that findStaleDescendants uses
|
|
379
|
+
// as the derivation clock, or a retrofit stamp would erase existing
|
|
380
|
+
// staleness. Preserve the original mtime across the write.
|
|
381
|
+
let mtime = null;
|
|
382
|
+
try {
|
|
383
|
+
mtime = statSync(filePath).mtime;
|
|
384
|
+
} catch { /* no prior stat — leave mtime as written */ }
|
|
385
|
+
writeFileSync(filePath, next);
|
|
386
|
+
if (mtime) {
|
|
387
|
+
try {
|
|
388
|
+
utimesSync(filePath, mtime, mtime);
|
|
389
|
+
} catch { /* best effort */ }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
out.push({
|
|
393
|
+
file,
|
|
394
|
+
changed,
|
|
395
|
+
wasGeneratedBy: lin.wasGeneratedBy,
|
|
396
|
+
wasDerivedFrom: lin.wasDerivedFrom,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { processTermination } from './process-termination.js';
|
|
1
3
|
/**
|
|
2
4
|
* local-claude-connector.js — V2/V3 (STRAT-TS-FANOUT-CONSUMER).
|
|
3
5
|
*
|
|
@@ -123,6 +125,20 @@ function recordDispatch(dispatchId, context, capture, outcome, elapsedMs, effort
|
|
|
123
125
|
*/
|
|
124
126
|
export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
125
127
|
const query = opts.query ?? sdkQuery;
|
|
128
|
+
const controller = opts.abortController;
|
|
129
|
+
controller?.signal.throwIfAborted();
|
|
130
|
+
// C2: group cancellation needs POSIX process groups. Windows has none, so
|
|
131
|
+
// instead of refusing the run (which killed EVERY local dispatch there,
|
|
132
|
+
// cancellable or not) we skip the custom spawn hook and let the SDK's own
|
|
133
|
+
// abortController tear the run down. That is weaker — the SDK kills only the
|
|
134
|
+
// CLI leader, so grandchildren can outlive a cancel — but a non-cancel run
|
|
135
|
+
// must never fail before spawn.
|
|
136
|
+
const platform = opts.platform ?? process.platform;
|
|
137
|
+
const ownProcessGroup = Boolean(controller) && platform !== 'win32';
|
|
138
|
+
const children = [];
|
|
139
|
+
let stderr = '';
|
|
140
|
+
const terminate = () => { for (const child of children) void child.terminate(); };
|
|
141
|
+
controller?.signal.addEventListener('abort', terminate, { once: true });
|
|
126
142
|
const { telemetry } = opts;
|
|
127
143
|
const telemetryContext = telemetry && typeof telemetry === 'object'
|
|
128
144
|
? telemetry
|
|
@@ -138,6 +154,24 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
138
154
|
model: opts.model ?? process.env.CLAUDE_MODEL ?? 'claude-sonnet-4-6',
|
|
139
155
|
permissionMode: 'acceptEdits',
|
|
140
156
|
env,
|
|
157
|
+
...(ownProcessGroup ? { spawnClaudeCodeProcess: (options) => {
|
|
158
|
+
controller.signal.throwIfAborted();
|
|
159
|
+
const child = spawn(options.command, options.args, {
|
|
160
|
+
cwd: options.cwd, env: options.env, detached: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
161
|
+
});
|
|
162
|
+
const termination = processTermination(child, true, opts.cancellationGraceMs);
|
|
163
|
+
children.push(termination);
|
|
164
|
+
child.stderr.setEncoding('utf8');
|
|
165
|
+
child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-16384); });
|
|
166
|
+
const sdkAbort = () => { void termination.terminate(); };
|
|
167
|
+
options.signal.addEventListener('abort', sdkAbort, { once: true });
|
|
168
|
+
void termination.close.then(() => options.signal.removeEventListener('abort', sdkAbort));
|
|
169
|
+
if (options.signal.aborted) sdkAbort();
|
|
170
|
+
return { stdin: child.stdin, stdout: child.stdout, pid: child.pid,
|
|
171
|
+
get killed() { return child.killed; }, get exitCode() { return child.exitCode; },
|
|
172
|
+
kill: () => { void termination.terminate(); return true; },
|
|
173
|
+
on: child.on.bind(child), once: child.once.bind(child), off: child.off.bind(child) };
|
|
174
|
+
} } : {}),
|
|
141
175
|
...(opts.abortController ? { abortController: opts.abortController } : {}),
|
|
142
176
|
...(opts.thinking !== undefined ? { thinking: opts.thinking } : {}),
|
|
143
177
|
...(appliedEffort !== null ? { effort: appliedEffort } : {}),
|
|
@@ -189,7 +223,12 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
189
223
|
if (raw.type === 'assistant' && isRecord(raw.message) && Array.isArray(raw.message.content)) {
|
|
190
224
|
for (const block of raw.message.content) {
|
|
191
225
|
if (!isRecord(block)) continue;
|
|
192
|
-
if (block.type === 'text' && typeof block.text === 'string')
|
|
226
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
227
|
+
assistantText += block.text;
|
|
228
|
+
// COMP-AGENT-LANES: optional live-relay seam, mirroring onToolUse.
|
|
229
|
+
// Without a subscriber the text only accumulates (historical shape).
|
|
230
|
+
if (typeof opts.onAssistantText === 'function') opts.onAssistantText(block.text);
|
|
231
|
+
}
|
|
193
232
|
if (block.type === 'tool_use' && typeof block.name === 'string' && typeof opts.onToolUse === 'function') {
|
|
194
233
|
opts.onToolUse({ tool: block.name, input: isRecord(block.input) ? block.input : {} });
|
|
195
234
|
}
|
|
@@ -239,6 +278,12 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
239
278
|
}
|
|
240
279
|
}
|
|
241
280
|
|
|
281
|
+
// H2: do NOT throwIfAborted() here. A run that RESOLVES after its
|
|
282
|
+
// timeout/abort fired still reported billable usage, and the caller
|
|
283
|
+
// (result-normalizer's late-resolve branch) is the single place that turns
|
|
284
|
+
// that into AgentTimeoutError/AgentAbortedError/UserInterruptError with the
|
|
285
|
+
// usage attached. Throwing a bare AbortError here loses the usage and the
|
|
286
|
+
// attempt is never billed.
|
|
242
287
|
const result = {
|
|
243
288
|
text: finalText ?? assistantText,
|
|
244
289
|
usage: {
|
|
@@ -289,6 +334,12 @@ export async function runLocalClaudeAgent(prompt, opts = {}) {
|
|
|
289
334
|
appliedEffort,
|
|
290
335
|
);
|
|
291
336
|
attachDispatchId(error, dispatchId);
|
|
337
|
+
if (controller?.signal.aborted) { terminate(); await Promise.all(children.map(child => child.finish())); }
|
|
338
|
+
if (stderr) { error.stderr = stderr; error.cause ??= new Error(stderr.trim()); }
|
|
292
339
|
throw error;
|
|
340
|
+
} finally {
|
|
341
|
+
controller?.signal.removeEventListener('abort', terminate);
|
|
342
|
+
if (controller?.signal.aborted) terminate();
|
|
343
|
+
await Promise.all(children.map(child => child.finish()));
|
|
293
344
|
}
|
|
294
345
|
}
|