@smartmemory/compose 0.3.6-beta → 0.3.8
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 +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.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-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- 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 +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- 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/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- 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 +66 -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 -14
- 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/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- 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 +428 -153
- package/lib/review-normalize.js +4 -0
- 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 +391 -31
- 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 +124 -24
- package/server/compose-mcp.js +91 -790
- 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 +35 -3
- 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-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { execFile as defaultExecFile } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { access, lstat as defaultLstat, mkdtemp as defaultMkdtemp, readFile as defaultReadFile, rm as defaultRm, stat as defaultStat, writeFile as defaultWriteFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CUSTODY_TIMEOUT_MS, custodyBackend as defaultCustodyBackend, custodySign as defaultCustodySign,
|
|
11
|
+
GUARD_DIR, HINT_ENROL, PRIVATE_DIR, PUBKEY_PATH, SIGNER_PATH, SUDO_ENV,
|
|
12
|
+
SUDOERS_PATH, SUDO_LOCAL_PATH,
|
|
13
|
+
} from './guard-custody.js';
|
|
14
|
+
import { guardDescriptors as defaultGuardDescriptors } from '../server/stratum-client.js';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const sshsig = await import(require.resolve('@smartmemory/stratum/dist/guard/sshsig.js'));
|
|
18
|
+
const { parseAllowedSigners, sshFingerprint, verifySshsig } = sshsig;
|
|
19
|
+
const DESCRIPTOR_NAMESPACE = 'stratum-guard-descriptors';
|
|
20
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const PACKAGED_SIGNER = path.resolve(MODULE_DIR, '..', 'scripts', 'guard-sign', 'compose-guard-sign.sh');
|
|
22
|
+
|
|
23
|
+
export const PRINCIPAL_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
24
|
+
export const SUDOERS_TEXT = (principal) =>
|
|
25
|
+
`Defaults!${SIGNER_PATH} timestamp_timeout=0\n${principal} ALL=(root) ${SIGNER_PATH}\n`;
|
|
26
|
+
|
|
27
|
+
function spawnWithInput(execFile, file, args, options, bytes) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
let settled = false;
|
|
30
|
+
const done = (error, stdout = '', stderr = '') => {
|
|
31
|
+
if (settled) return;
|
|
32
|
+
settled = true;
|
|
33
|
+
resolve({ error, stdout, stderr });
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const child = execFile(file, args, options, done);
|
|
37
|
+
child?.stdin?.end(bytes);
|
|
38
|
+
child?.on?.('error', (error) => done(error));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
done(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function firstLine(value, fallback = 'command failed') {
|
|
46
|
+
return String(value || '').split(/\r?\n/, 1)[0] || fallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Check every existing ancestor without following a symlink at any level. */
|
|
50
|
+
export async function validateAncestry(target, deps = {}) {
|
|
51
|
+
const lstat = deps.lstat ?? defaultLstat;
|
|
52
|
+
const stat = deps.stat ?? defaultStat;
|
|
53
|
+
let current = path.resolve(target);
|
|
54
|
+
for (;;) {
|
|
55
|
+
let lst;
|
|
56
|
+
try {
|
|
57
|
+
lst = await lstat(current);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.code === 'ENOENT') {
|
|
60
|
+
const parent = path.dirname(current);
|
|
61
|
+
if (parent === current) return { ok: false, offending: { path: current, reason: 'missing' } };
|
|
62
|
+
current = parent;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, offending: { path: current, reason: firstLine(error?.message) } };
|
|
66
|
+
}
|
|
67
|
+
if (lst.isSymbolicLink()) return { ok: false, offending: { path: current, reason: 'symlink' } };
|
|
68
|
+
let info;
|
|
69
|
+
try { info = await stat(current); } catch (error) {
|
|
70
|
+
return { ok: false, offending: { path: current, reason: firstLine(error?.message) } };
|
|
71
|
+
}
|
|
72
|
+
if (info.uid !== 0) return { ok: false, offending: { path: current, reason: 'not root-owned' } };
|
|
73
|
+
if ((info.mode & 0o022) !== 0) return { ok: false, offending: { path: current, reason: 'group or world writable' } };
|
|
74
|
+
const parent = path.dirname(current);
|
|
75
|
+
if (parent === current) return { ok: true, offending: null };
|
|
76
|
+
current = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Resolve the package's reviewable source trust root, never an environment path. */
|
|
81
|
+
export async function locateTrustRoot(deps = {}) {
|
|
82
|
+
const packageJson = deps.packageJsonPath ?? require.resolve('@smartmemory/stratum/package.json');
|
|
83
|
+
const packageRoot = deps.packageRoot ?? path.dirname(packageJson);
|
|
84
|
+
const exists = deps.exists ?? (async (file) => access(file).then(() => true, () => false));
|
|
85
|
+
const source = path.join(packageRoot, 'contracts', 'guard-signers.allowed');
|
|
86
|
+
if (await exists(source)) return { kind: 'source', path: source, checkoutRoot: packageRoot };
|
|
87
|
+
const dist = path.join(packageRoot, 'dist', 'contracts', 'guard-signers.allowed');
|
|
88
|
+
if (await exists(dist)) return { kind: 'dist', path: dist, checkoutRoot: null };
|
|
89
|
+
throw new Error(`could not locate Stratum guard trust root under ${packageRoot}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Produce a whole-file parsed trust-root candidate, never a partial append. */
|
|
93
|
+
export function candidateTrustRoot(current, principal, publicKeyLine) {
|
|
94
|
+
if (!PRINCIPAL_RE.test(principal)) throw new Error(`invalid principal ${JSON.stringify(principal)}; expected ${PRINCIPAL_RE}`);
|
|
95
|
+
if (typeof publicKeyLine !== 'string' || /[^\x20-\x7e]/.test(publicKeyLine.trim())) {
|
|
96
|
+
throw new Error('public key line must be printable ASCII');
|
|
97
|
+
}
|
|
98
|
+
const key = parseAllowedSigners(`candidate ${publicKeyLine.trim()}\n`)[0];
|
|
99
|
+
if (!key) throw new Error('public key line did not contain a signer');
|
|
100
|
+
const signers = parseAllowedSigners(current);
|
|
101
|
+
const present = signers.some((entry) => entry.fingerprint === key.fingerprint);
|
|
102
|
+
const line = `${principal} ${publicKeyLine.trim()}`;
|
|
103
|
+
const text = present ? current : `${current}${current && !current.endsWith('\n') ? '\n' : ''}${line}\n`;
|
|
104
|
+
const parsed = parseAllowedSigners(text);
|
|
105
|
+
return { present, text, candidate: text, line, fingerprint: key.fingerprint, signers: parsed };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function installPlan(principal) {
|
|
109
|
+
if (!PRINCIPAL_RE.test(principal)) throw new Error(`invalid principal ${JSON.stringify(principal)}; expected ${PRINCIPAL_RE}`);
|
|
110
|
+
return [
|
|
111
|
+
`install root-owned signer: ${SIGNER_PATH}`,
|
|
112
|
+
`create root-owned signing key: ${PRIVATE_DIR}/signing-key`,
|
|
113
|
+
`publish public key: ${PUBKEY_PATH}`,
|
|
114
|
+
`install sudoers rule: ${SUDOERS_PATH}`,
|
|
115
|
+
`enable Touch ID for sudo: ${SUDO_LOCAL_PATH}`,
|
|
116
|
+
`add signer principal to Stratum trust root: ${principal}`,
|
|
117
|
+
`root installation script:\n${installScript(principal)}`,
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const SIGNER_HEREDOC_DELIMITER = 'COMPOSE_GUARD_SIGNER_SCRIPT';
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Exact root-side installation payload. It is intentionally shell-only and
|
|
125
|
+
* idempotent.
|
|
126
|
+
*
|
|
127
|
+
* Every `${…}` interpolated into this template is either a constant path
|
|
128
|
+
* exported by guard-custody.js (GUARD_DIR, PRIVATE_DIR, SIGNER_PATH,
|
|
129
|
+
* PUBKEY_PATH, SUDOERS_PATH, SUDO_LOCAL_PATH — never a runtime/user-supplied
|
|
130
|
+
* string) or shell text already embedded via a quoted heredoc (`sudoers`,
|
|
131
|
+
* `signerScript`) rather than interpolated inline. The packaged signer's
|
|
132
|
+
* module-directory path is never interpolated into the script at all: its
|
|
133
|
+
* bytes are read here and written to SIGNER_PATH via a quoted heredoc, so a
|
|
134
|
+
* compose install path containing a space or shell metacharacter cannot break
|
|
135
|
+
* or inject into the root shell.
|
|
136
|
+
*/
|
|
137
|
+
export function installScript(principal, deps = {}) {
|
|
138
|
+
if (!PRINCIPAL_RE.test(principal)) throw new Error(`invalid principal ${JSON.stringify(principal)}; expected ${PRINCIPAL_RE}`);
|
|
139
|
+
const sudoers = SUDOERS_TEXT(principal).trimEnd();
|
|
140
|
+
const readSigner = deps.readSigner ?? (() => readFileSync(PACKAGED_SIGNER, 'utf8'));
|
|
141
|
+
const signerScript = readSigner();
|
|
142
|
+
if (signerScript.includes(SIGNER_HEREDOC_DELIMITER)) {
|
|
143
|
+
throw new Error(`packaged signer script contains the heredoc delimiter ${SIGNER_HEREDOC_DELIMITER}; choose a different delimiter`);
|
|
144
|
+
}
|
|
145
|
+
return `set -eu\numask 022\nPATH=/usr/bin:/bin:/usr/sbin\nexport PATH\n/bin/mkdir -p /Library/Compose/guard/private/tmp\n/bin/chmod 0755 /Library/Compose ${GUARD_DIR}\n/bin/chmod 0700 ${PRIVATE_DIR} ${PRIVATE_DIR}/tmp\n/bin/cat > ${SIGNER_PATH} <<'${SIGNER_HEREDOC_DELIMITER}'\n${signerScript.trimEnd()}\n${SIGNER_HEREDOC_DELIMITER}\n/usr/sbin/chown root:wheel ${SIGNER_PATH}\n/bin/chmod 0755 ${SIGNER_PATH}\nif [ ! -f ${PRIVATE_DIR}/signing-key ]; then\n /usr/bin/ssh-keygen -t ed25519 -N '' -q -f ${PRIVATE_DIR}/signing-key -C compose-guard\nfi\n/usr/sbin/chown root:wheel ${PRIVATE_DIR}/signing-key ${PRIVATE_DIR}/signing-key.pub\n/bin/chmod 0600 ${PRIVATE_DIR}/signing-key\n/bin/cp ${PRIVATE_DIR}/signing-key.pub ${PUBKEY_PATH}\n/usr/sbin/chown root:wheel ${PUBKEY_PATH}\n/bin/chmod 0644 ${PUBKEY_PATH}\n_sudoers_tmp=${SUDOERS_PATH}.tmp.$$\n/bin/cat > "$_sudoers_tmp" <<'COMPOSE_GUARD_SUDOERS'\n${sudoers}\nCOMPOSE_GUARD_SUDOERS\n/usr/sbin/visudo -cf "$_sudoers_tmp"\n/usr/bin/install -o root -g wheel -m 0440 "$_sudoers_tmp" ${SUDOERS_PATH}\n/bin/rm -f "$_sudoers_tmp"\nif [ ! -f ${SUDO_LOCAL_PATH} ]; then\n /bin/cat > ${SUDO_LOCAL_PATH} <<'COMPOSE_GUARD_PAM'\nauth sufficient pam_tid.so\nCOMPOSE_GUARD_PAM\nelif ! /usr/bin/grep -Eq '^[[:space:]]*auth[[:space:]]+sufficient[[:space:]]+pam_tid\\.so' ${SUDO_LOCAL_PATH}; then\n /bin/cat >> ${SUDO_LOCAL_PATH} <<'COMPOSE_GUARD_PAM'\nauth sufficient pam_tid.so\nCOMPOSE_GUARD_PAM\nfi\n/usr/sbin/chown root:wheel ${SUDO_LOCAL_PATH}\n/bin/chmod 0644 ${SUDO_LOCAL_PATH}\necho 'ok directories'\necho 'ok signer'\necho 'ok key'\necho 'ok sudoers'\necho 'ok pam_tid'\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Sign and cryptographically verify a durable fixture against exactly one public key. */
|
|
149
|
+
export async function roundTrip({ custodySign, publicKeyLine, deps = {} }) {
|
|
150
|
+
const mkdtemp = deps.mkdtemp ?? defaultMkdtemp;
|
|
151
|
+
const writeFile = deps.writeFile ?? defaultWriteFile;
|
|
152
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
153
|
+
const rm = deps.rm ?? defaultRm;
|
|
154
|
+
const dir = await mkdtemp(path.join(tmpdir(), 'compose-guard-enrol-'));
|
|
155
|
+
const file = path.join(dir, 'descriptors.json');
|
|
156
|
+
const bytes = Buffer.from('{"version":1,"descriptors":[]}\n');
|
|
157
|
+
try {
|
|
158
|
+
await writeFile(file, bytes, { mode: 0o600 });
|
|
159
|
+
const signed = await custodySign({ bytes, namespace: DESCRIPTOR_NAMESPACE });
|
|
160
|
+
if (!signed?.ok) throw new Error(signed?.message || 'fixture signature was not approved');
|
|
161
|
+
await writeFile(`${file}.sig`, signed.armored, { mode: 0o600 });
|
|
162
|
+
const signer = parseAllowedSigners(`fixture ${publicKeyLine.trim()}\n`)[0];
|
|
163
|
+
if (!signer) throw new Error('public key line did not contain a signer');
|
|
164
|
+
verifySshsig(await readFile(file), await readFile(`${file}.sig`, 'utf8'), DESCRIPTOR_NAMESPACE, [signer.publicKey]);
|
|
165
|
+
return { dir, file, sig: `${file}.sig`, namespace: DESCRIPTOR_NAMESPACE, fingerprint: signer.fingerprint, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
await rm(dir, { recursive: true, force: true });
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function rebuildStratumDist(checkoutRoot, deps = {}) {
|
|
173
|
+
const { error, stderr } = await spawnWithInput(deps.execFile ?? defaultExecFile, 'npm', ['run', 'build'], { cwd: checkoutRoot, timeout: CUSTODY_TIMEOUT_MS }, Buffer.alloc(0));
|
|
174
|
+
if (error) throw new Error(`Stratum build failed: ${firstLine(stderr || error.message)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function result(status, lines, extra = {}) { return { status, lines, ...extra }; }
|
|
178
|
+
|
|
179
|
+
/** Complete, deliberately ordered enrolment ceremony. Every side effect is injectable. */
|
|
180
|
+
export async function runEnrol({ principal = process.env.USER || process.env.USERNAME || '' } = {}, deps = {}) {
|
|
181
|
+
// The plan must always reach a human before the root step runs — a silent
|
|
182
|
+
// no-op emitter would let any in-process caller (including a bare
|
|
183
|
+
// `runGuardEnrol()`) reach `sudo -k /bin/sh -s` with the plan never shown.
|
|
184
|
+
// So the emitter is mandatory, checked before the backend check, before any
|
|
185
|
+
// spawn or filesystem access.
|
|
186
|
+
if (typeof deps.emit !== 'function') {
|
|
187
|
+
return result('refused', ['enrol requires a line emitter so the plan is shown before the root step'], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
188
|
+
}
|
|
189
|
+
const backend = (deps.custodyBackend ?? defaultCustodyBackend)();
|
|
190
|
+
if (backend !== 'sudo') return result('refused', ['enrol requires the sudo custody backend on macOS'], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
191
|
+
if (!PRINCIPAL_RE.test(principal)) return result('refused', [`invalid principal ${JSON.stringify(principal)}; expected ${PRINCIPAL_RE}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
192
|
+
|
|
193
|
+
const ancestry = deps.validateAncestry ?? validateAncestry;
|
|
194
|
+
const installTargets = [GUARD_DIR, SIGNER_PATH, PUBKEY_PATH, PRIVATE_DIR, SUDOERS_PATH, SUDO_LOCAL_PATH];
|
|
195
|
+
for (const target of installTargets) {
|
|
196
|
+
const checked = await ancestry(target, deps);
|
|
197
|
+
if (!checked.ok) return result('refused', [`unsafe install ancestry at ${checked.offending.path}: ${checked.offending.reason}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
198
|
+
}
|
|
199
|
+
// `installScript` now reads the packaged signer from disk and can throw (a
|
|
200
|
+
// missing/unreadable signer, or a heredoc-delimiter collision); `emit` is
|
|
201
|
+
// caller-supplied and can throw too. Neither has spawned or written
|
|
202
|
+
// anything yet, so a failure here must still return the refused envelope,
|
|
203
|
+
// not reject the promise.
|
|
204
|
+
let script;
|
|
205
|
+
try {
|
|
206
|
+
script = installScript(principal, deps);
|
|
207
|
+
const emit = deps.emit;
|
|
208
|
+
for (const line of installPlan(principal)) emit(line);
|
|
209
|
+
emit('root installation follows:');
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return result('refused', [`enrol preparation failed: ${firstLine(error.message)}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
212
|
+
}
|
|
213
|
+
const lines = [];
|
|
214
|
+
const rootStep = await spawnWithInput(deps.execFile ?? defaultExecFile, '/usr/bin/sudo', ['-k', '/bin/sh', '-s'], { env: SUDO_ENV(), timeout: CUSTODY_TIMEOUT_MS, maxBuffer: 1 << 20 }, script);
|
|
215
|
+
if (rootStep.error) return result('refused', [...lines, `root installation failed: ${firstLine(rootStep.stderr || rootStep.error.message)}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
216
|
+
lines.push(...String(rootStep.stdout || '').split(/\r?\n/).filter((line) => line.startsWith('ok ')));
|
|
217
|
+
|
|
218
|
+
for (const target of installTargets) {
|
|
219
|
+
const checked = await ancestry(target, deps);
|
|
220
|
+
if (!checked.ok) return result('refused', [...lines, `unsafe installed ancestry at ${checked.offending.path}: ${checked.offending.reason}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
221
|
+
}
|
|
222
|
+
const readFile = deps.readFile ?? defaultReadFile;
|
|
223
|
+
let publicKeyLine;
|
|
224
|
+
try { publicKeyLine = (await readFile(PUBKEY_PATH, 'utf8')).trim(); } catch (error) {
|
|
225
|
+
return result('refused', [...lines, `could not read enrolled public key: ${firstLine(error.message)}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let fixture;
|
|
229
|
+
try {
|
|
230
|
+
fixture = await roundTrip({ custodySign: deps.custodySign ?? defaultCustodySign, publicKeyLine, deps });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return result('refused', [...lines, `round-trip signature failed: ${firstLine(error.message)}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const trust = await (deps.locateTrustRoot ?? locateTrustRoot)(deps);
|
|
236
|
+
const current = await readFile(trust.path, 'utf8');
|
|
237
|
+
const candidate = candidateTrustRoot(current, principal, publicKeyLine);
|
|
238
|
+
if (trust.kind !== 'source') {
|
|
239
|
+
return result('refused', [...lines, `add this line to ${trust.path} in a Stratum source release:`, candidate.line], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
240
|
+
}
|
|
241
|
+
if (!candidate.present) await (deps.writeFile ?? defaultWriteFile)(trust.path, candidate.text, 'utf8');
|
|
242
|
+
await (deps.rebuildStratumDist ?? rebuildStratumDist)(trust.checkoutRoot, deps);
|
|
243
|
+
const inspected = await (deps.guardDescriptors ?? defaultGuardDescriptors)(fixture.file);
|
|
244
|
+
const expected = `verified: signed by ${principal} (${fixture.fingerprint})`;
|
|
245
|
+
if (inspected?.status !== 'ok' || !String(inspected?.signature || '').startsWith(expected) || inspected?.group_or_world_writable !== false) {
|
|
246
|
+
return result('refused', [...lines, `final Stratum verification failed: ${inspected?.signature || 'no verifier verdict'}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
247
|
+
}
|
|
248
|
+
return result('done', [...lines, `enrolled ${principal} (${fixture.fingerprint})`, 'done'], { fingerprint: fixture.fingerprint });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return result('refused', [...lines, `enrolment failed: ${firstLine(error?.message)}`], { code: 'upgrade_descriptor_unavailable', hint: HINT_ENROL });
|
|
251
|
+
} finally {
|
|
252
|
+
await fixture.cleanup();
|
|
253
|
+
}
|
|
254
|
+
}
|
package/lib/health-score.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Review findings from parallel_review's ReviewResult (canonical — STRAT-CLAUDE-EFFORT-PARITY)
|
|
7
7
|
* - Test pass/fail from coverage_check result
|
|
8
8
|
* - Contract compliance from Stratum ensure results
|
|
9
|
-
* - Doc freshness from lib/
|
|
9
|
+
* - Doc freshness from lib/lineage.js (findStaleArtifacts)
|
|
10
10
|
* - Plan completion from plan_completion ensure builtin
|
|
11
11
|
* - Runtime errors from build_step_done violations
|
|
12
12
|
*/
|
package/lib/hooks-status.js
CHANGED
|
@@ -24,6 +24,14 @@ export const HOOK_MARKERS = {
|
|
|
24
24
|
'pre-push': '# Compose pre-push hook —',
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Current baked versions for hooks whose content contract is versioned.
|
|
29
|
+
* Post-commit remains unversioned until its template changes.
|
|
30
|
+
*/
|
|
31
|
+
export const HOOK_VERSIONS = {
|
|
32
|
+
'pre-push': '1',
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
/** The hook types Compose manages, in display order. */
|
|
28
36
|
export const HOOK_TYPE_LIST = Object.keys(HOOK_MARKERS);
|
|
29
37
|
|
|
@@ -33,6 +41,12 @@ export function extractBakedWorkspaceId(content) {
|
|
|
33
41
|
return m ? m[1] : null;
|
|
34
42
|
}
|
|
35
43
|
|
|
44
|
+
/** Pull the baked HOOK_VERSION="..." value out of hook content, or null. */
|
|
45
|
+
export function extractBakedHookVersion(content) {
|
|
46
|
+
const m = content.match(/^HOOK_VERSION="([^"]*)"$/m);
|
|
47
|
+
return m ? m[1] : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* Compute the raw status facts for every managed hook type.
|
|
38
52
|
*
|
|
@@ -55,15 +69,19 @@ export function computeHooksStatus({ projectRoot, expectedWsId = null, composeNo
|
|
|
55
69
|
|
|
56
70
|
function computeOne({ type, hooksDir, expectedWsId, composeNode, composeBin }) {
|
|
57
71
|
const marker = HOOK_MARKERS[type];
|
|
72
|
+
const expectedVersion = HOOK_VERSIONS[type] ?? null;
|
|
58
73
|
const dest = join(hooksDir, type);
|
|
59
74
|
|
|
60
75
|
const empty = {
|
|
61
76
|
reason: null,
|
|
62
77
|
bakedWorkspace: null,
|
|
78
|
+
bakedVersion: null,
|
|
79
|
+
expectedVersion,
|
|
63
80
|
expectedWorkspace: expectedWsId ?? null,
|
|
64
81
|
wsVerified: false,
|
|
65
82
|
nodeMatch: false,
|
|
66
83
|
binMatch: false,
|
|
84
|
+
versionMatch: false,
|
|
67
85
|
};
|
|
68
86
|
|
|
69
87
|
if (!existsSync(dest)) return { state: 'absent', ...empty };
|
|
@@ -75,26 +93,33 @@ function computeOne({ type, hooksDir, expectedWsId, composeNode, composeBin }) {
|
|
|
75
93
|
const binMatch = content.includes(`COMPOSE_BIN="${composeBin}"`);
|
|
76
94
|
const hasRawToken = content.includes('__COMPOSE_WORKSPACE_ID__');
|
|
77
95
|
const bakedWorkspace = extractBakedWorkspaceId(content);
|
|
96
|
+
const bakedVersion = extractBakedHookVersion(content);
|
|
97
|
+
const versionMatch = expectedVersion == null || bakedVersion === expectedVersion;
|
|
78
98
|
// Mirror of statusOne: raw token never matches; a known expected id must match
|
|
79
99
|
// the baked value; an unknown (null) expected id is treated leniently as a match.
|
|
80
100
|
const wsMatch = hasRawToken ? false : expectedWsId ? content.includes(`COMPOSE_WORKSPACE_ID="${expectedWsId}"`) : true;
|
|
81
101
|
|
|
82
102
|
const base = {
|
|
83
103
|
bakedWorkspace,
|
|
104
|
+
bakedVersion,
|
|
105
|
+
expectedVersion,
|
|
84
106
|
expectedWorkspace: expectedWsId ?? null,
|
|
85
107
|
wsVerified: expectedWsId != null,
|
|
86
108
|
nodeMatch,
|
|
87
109
|
binMatch,
|
|
110
|
+
versionMatch,
|
|
88
111
|
};
|
|
89
112
|
|
|
90
|
-
if (nodeMatch && binMatch && wsMatch && !hasRawToken) {
|
|
113
|
+
if (nodeMatch && binMatch && wsMatch && !hasRawToken && versionMatch) {
|
|
91
114
|
return { state: 'installed-current', reason: null, ...base };
|
|
92
115
|
}
|
|
93
116
|
const reason = hasRawToken
|
|
94
117
|
? 'MISSING_WORKSPACE_ID'
|
|
95
118
|
: expectedWsId && !wsMatch
|
|
96
119
|
? 'STALE_WORKSPACE_ID'
|
|
97
|
-
:
|
|
120
|
+
: !nodeMatch || !binMatch
|
|
121
|
+
? 'stale paths'
|
|
122
|
+
: 'HOOK_VERSION_DRIFT';
|
|
98
123
|
return { state: 'installed-stale', reason, ...base };
|
|
99
124
|
}
|
|
100
125
|
|
|
@@ -118,6 +143,7 @@ export function formatHookStatusLines(type, s, { composeNode, composeBin }) {
|
|
|
118
143
|
// installed-stale
|
|
119
144
|
const lines = [`${type}: installed (${s.reason} — re-run install)`];
|
|
120
145
|
if (s.reason === 'STALE_WORKSPACE_ID') lines.push(` expected COMPOSE_WORKSPACE_ID="${s.expectedWorkspace}"`);
|
|
146
|
+
if (s.reason === 'HOOK_VERSION_DRIFT') lines.push(` expected HOOK_VERSION="${s.expectedVersion}"`);
|
|
121
147
|
if (!s.nodeMatch) lines.push(` expected COMPOSE_NODE="${composeNode}"`);
|
|
122
148
|
if (!s.binMatch) lines.push(` expected COMPOSE_BIN="${composeBin}"`);
|
|
123
149
|
return lines;
|
|
@@ -126,10 +152,13 @@ export function formatHookStatusLines(type, s, { composeNode, composeBin }) {
|
|
|
126
152
|
/**
|
|
127
153
|
* @typedef {object} HookStatusRaw
|
|
128
154
|
* @property {'absent'|'foreign'|'installed-current'|'installed-stale'} state
|
|
129
|
-
* @property {null|'MISSING_WORKSPACE_ID'|'STALE_WORKSPACE_ID'|'stale paths'} reason
|
|
155
|
+
* @property {null|'MISSING_WORKSPACE_ID'|'STALE_WORKSPACE_ID'|'stale paths'|'HOOK_VERSION_DRIFT'} reason
|
|
130
156
|
* @property {string|null} bakedWorkspace
|
|
157
|
+
* @property {string|null} bakedVersion
|
|
158
|
+
* @property {string|null} expectedVersion
|
|
131
159
|
* @property {string|null} expectedWorkspace
|
|
132
160
|
* @property {boolean} wsVerified
|
|
133
161
|
* @property {boolean} nodeMatch
|
|
134
162
|
* @property {boolean} binMatch
|
|
163
|
+
* @property {boolean} versionMatch
|
|
135
164
|
*/
|