@opengsd/gsd-core 1.5.0 → 1.6.0-rc.2
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-plugin/plugin.json +1 -1
- package/agents/gsd-plan-checker.md +34 -0
- package/agents/gsd-planner.md +2 -0
- package/agents/gsd-roadmapper.md +6 -0
- package/bin/install.js +199 -365
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +695 -5
- package/gsd-core/bin/lib/adr-parser.cjs +45 -23
- package/gsd-core/bin/lib/audit.cjs +2 -2
- package/gsd-core/bin/lib/capability-consent.cjs +763 -0
- package/gsd-core/bin/lib/capability-ledger.cjs +831 -0
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1551 -0
- package/gsd-core/bin/lib/capability-loader.cjs +764 -0
- package/gsd-core/bin/lib/capability-lock.cjs +553 -0
- package/gsd-core/bin/lib/capability-registry.cjs +198 -4
- package/gsd-core/bin/lib/capability-source.cjs +1242 -0
- package/gsd-core/bin/lib/capability-state.cjs +9 -6
- package/gsd-core/bin/lib/capability-trust.cjs +550 -0
- package/gsd-core/bin/lib/capability-validator.cjs +2066 -0
- package/gsd-core/bin/lib/capability-writer.cjs +14 -5
- package/gsd-core/bin/lib/check-command-router.cjs +69 -18
- package/gsd-core/bin/lib/command-aliases.cjs +8 -0
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +98 -84
- package/gsd-core/bin/lib/config-schema.cjs +26 -7
- package/gsd-core/bin/lib/config.cjs +7 -1
- package/gsd-core/bin/lib/decisions.cjs +149 -60
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/gap-checker.cjs +126 -11
- package/gsd-core/bin/lib/init.cjs +91 -22
- package/gsd-core/bin/lib/legacy-cleanup.cjs +96 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +26 -2
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +471 -0
- package/gsd-core/bin/lib/milestone.cjs +41 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +5 -0
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase-lifecycle.cjs +14 -5
- package/gsd-core/bin/lib/phase.cjs +33 -4
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/project-root.cjs +89 -2
- package/gsd-core/bin/lib/resolution.cjs +26 -0
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +73 -106
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +423 -3
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -28
- package/gsd-core/bin/lib/runtime-homes.cjs +53 -1
- package/gsd-core/bin/lib/runtime-name-policy.cjs +44 -0
- package/gsd-core/bin/lib/semver-compare.cjs +127 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state-document.cjs +4 -2
- package/gsd-core/bin/lib/state.cjs +317 -161
- package/gsd-core/bin/lib/surface.cjs +12 -19
- package/gsd-core/bin/lib/uat-predicate.cjs +7 -47
- package/gsd-core/bin/lib/uat.cjs +39 -26
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +40 -15
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +6 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +5 -1
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-between-wave-reset.md +43 -0
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/execute-phase-wave-guard.md +33 -0
- package/gsd-core/references/planner-antipatterns.md +48 -0
- package/gsd-core/references/planning-config.md +4 -0
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/scout-codebase.md +2 -2
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/discuss-phase/templates/context.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +1 -2
- package/gsd-core/workflows/execute-phase.md +12 -12
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/package.json +6 -3
- package/scripts/gen-capability-matrix.cjs +284 -0
- package/scripts/gen-capability-registry.cjs +96 -1853
- package/scripts/lint-regression-test-names.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.allowlist.json +1 -0
- package/scripts/lint-resolution-provenance.cjs +192 -0
- package/scripts/lint-test-file-count.allowlist.json +9 -0
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/run-tests.cjs +14 -0
- package/scripts/sync-manifest-versions.cjs +77 -5
|
@@ -42,13 +42,22 @@ function deriveProgressFromRoadmap(roadmapContent) {
|
|
|
42
42
|
completedPhases = completeMatches ? completeMatches.length : null;
|
|
43
43
|
// Count total phase rows in the progress table.
|
|
44
44
|
// Identify the table by looking for Phase|...|Status|...|Completed header.
|
|
45
|
-
const progressTableMatch = roadmapContent.match(
|
|
45
|
+
const progressTableMatch = roadmapContent.match(
|
|
46
|
+
// allow-adhoc-markdown: table-scoped regex with heading lookahead as stop; table parsing, out of seam scope; pending #1372
|
|
47
|
+
/\|\s*Phase\s*\|[^|]*\|[^|]*Status[^|]*\|[^|]*Completed[^|]*\|[\s\S]*?(?=\n\n|\n##|$)/i);
|
|
46
48
|
if (progressTableMatch) {
|
|
47
49
|
const tableText = progressTableMatch[0];
|
|
48
|
-
// Count data rows (rows starting with pipe then a phase number)
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
50
|
+
// Count data rows (rows starting with pipe then a phase number),
|
|
51
|
+
// excluding 999.x backlog phases. Mirrors init.cts /^999(?:\.|$)/ filter.
|
|
52
|
+
const dataRowPattern = /^\|\s*(\d+[^|]*)\|/gm;
|
|
53
|
+
let dataRowCount = 0;
|
|
54
|
+
let drm;
|
|
55
|
+
while ((drm = dataRowPattern.exec(tableText)) !== null) {
|
|
56
|
+
if (/^999\b/.test(drm[1].trim()))
|
|
57
|
+
continue;
|
|
58
|
+
dataRowCount++;
|
|
59
|
+
}
|
|
60
|
+
totalPhases = dataRowCount > 0 ? dataRowCount : null;
|
|
52
61
|
}
|
|
53
62
|
// Sum plan counts from M/N columns in progress table
|
|
54
63
|
let totalPlansSum = 0;
|
|
@@ -32,7 +32,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
32
32
|
const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
34
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches } = phaseIdMod;
|
|
35
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdMod;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
37
37
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
38
38
|
const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
|
|
@@ -167,7 +167,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
167
167
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
168
168
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
169
169
|
baseExists = dirs.some((d) => phaseTokenMatches(d, normalized));
|
|
170
|
-
const dirPattern = new RegExp(
|
|
170
|
+
const dirPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${escapeRegex(normalized)}\\.(\\d+)`);
|
|
171
171
|
for (const dir of dirs) {
|
|
172
172
|
const match = dir.match(dirPattern);
|
|
173
173
|
if (match)
|
|
@@ -310,7 +310,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
310
310
|
const match = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
311
311
|
if (!match)
|
|
312
312
|
continue;
|
|
313
|
-
const dirMatch = match.match(
|
|
313
|
+
const dirMatch = match.match(new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(\\d+[A-Z]?(?:\\.\\d+)*)-?(.*)`, 'i')) ||
|
|
314
314
|
match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
315
315
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
316
316
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
@@ -756,7 +756,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
756
756
|
try {
|
|
757
757
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
758
758
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
759
|
-
const decimalPattern = new RegExp(
|
|
759
|
+
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${escapeRegex(normalizedBase)}\\.(\\d+)`);
|
|
760
760
|
for (const dir of dirs) {
|
|
761
761
|
const dm = dir.match(decimalPattern);
|
|
762
762
|
if (dm)
|
|
@@ -1430,6 +1430,34 @@ function cmdPhaseUatPassed(cwd, phaseNum, raw, opts = {}) {
|
|
|
1430
1430
|
const report = evaluateUatPassed(phaseFullDir, { policy: opts.policy });
|
|
1431
1431
|
output({ phase: phaseNum, ...report }, raw);
|
|
1432
1432
|
}
|
|
1433
|
+
// #1437 — phase.list-plans: list plan files for a given phase number.
|
|
1434
|
+
// Returns the full scan result from scanPhasePlans so callers can read plan
|
|
1435
|
+
// paths without re-discovering the phase directory themselves.
|
|
1436
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-scan.cjs is an export= CommonJS module
|
|
1437
|
+
const planScanMod = require("./plan-scan.cjs");
|
|
1438
|
+
const { scanPhasePlans } = planScanMod;
|
|
1439
|
+
function cmdPhaseListPlans(cwd, phaseNum, raw) {
|
|
1440
|
+
if (!phaseNum) {
|
|
1441
|
+
error('phase number required for phase list-plans');
|
|
1442
|
+
}
|
|
1443
|
+
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
1444
|
+
if (!phaseInfo) {
|
|
1445
|
+
output({ phase: phaseNum, plan_count: 0, has_plans: false, plans: [], phase_dir: null }, raw);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
const phaseDir = node_path_1.default.join(cwd, phaseInfo['directory']);
|
|
1449
|
+
const scan = scanPhasePlans(phaseDir);
|
|
1450
|
+
const phaseRel = phaseInfo['directory'];
|
|
1451
|
+
// Build absolute-usable relative paths for each plan file.
|
|
1452
|
+
const plans = scan.planFiles.map((f) => toPosixPath(node_path_1.default.join(phaseRel, f)));
|
|
1453
|
+
output({
|
|
1454
|
+
phase: phaseNum,
|
|
1455
|
+
phase_dir: phaseRel,
|
|
1456
|
+
plan_count: scan.planCount,
|
|
1457
|
+
has_plans: scan.planCount > 0,
|
|
1458
|
+
plans,
|
|
1459
|
+
}, raw);
|
|
1460
|
+
}
|
|
1433
1461
|
module.exports = {
|
|
1434
1462
|
cmdPhasesList,
|
|
1435
1463
|
cmdPhaseNextDecimal,
|
|
@@ -1442,5 +1470,6 @@ module.exports = {
|
|
|
1442
1470
|
cmdPhaseRemove,
|
|
1443
1471
|
cmdPhaseComplete,
|
|
1444
1472
|
cmdPhaseUatPassed,
|
|
1473
|
+
cmdPhaseListPlans,
|
|
1445
1474
|
computeDependencyLevels,
|
|
1446
1475
|
};
|
|
@@ -288,6 +288,13 @@ function projectProhibitions(items) {
|
|
|
288
288
|
if (typeof p.check_violation_fixture === 'string' && p.check_violation_fixture.trim() !== '') {
|
|
289
289
|
entry.check_violation_fixture = String(p.check_violation_fixture);
|
|
290
290
|
}
|
|
291
|
+
// `check_clean_fixture` (#1346) rides BOTH kinds — the KNOWN-CLEAN control subject the prover
|
|
292
|
+
// requires to stay GREEN (content-dependence proof). Emit ONLY a non-empty fixture (blank ->
|
|
293
|
+
// absent so no control runs; the documented residual remains). Like the violation fixture it is
|
|
294
|
+
// meaningless without the descriptor, so it lives inside this well-formed-descriptor branch.
|
|
295
|
+
if (typeof p.check_clean_fixture === 'string' && p.check_clean_fixture.trim() !== '') {
|
|
296
|
+
entry.check_clean_fixture = String(p.check_clean_fixture);
|
|
297
|
+
}
|
|
291
298
|
}
|
|
292
299
|
out.push(entry);
|
|
293
300
|
}
|
|
@@ -79,8 +79,9 @@ const probe_core_cjs_1 = require("./probe-core.cjs");
|
|
|
79
79
|
* - `null`/`undefined`/non-object input -> `null`.
|
|
80
80
|
* - `check_kind` ABSENT -> `null` (no descriptor -> producer locates nothing -> fail-closed).
|
|
81
81
|
* - `check_kind` present -> `{ kind: check_kind, target: check_target }`, adding `rule: check_rule`
|
|
82
|
-
* ONLY when `check_rule` is a non-empty string,
|
|
83
|
-
* ONLY when that scalar is a non-empty string (
|
|
82
|
+
* ONLY when `check_rule` is a non-empty string, `violationFixture: check_violation_fixture`
|
|
83
|
+
* ONLY when that scalar is a non-empty string (composes #1278 locate with #1279 proof), and
|
|
84
|
+
* `cleanFixture: check_clean_fixture` ONLY when that scalar is non-empty (#1346 causation control).
|
|
84
85
|
* - `failFirst` is NEVER sourced from the projection — it stays a verify-time caller attestation
|
|
85
86
|
* (#1279 machine-proves it; out of scope here). The returned descriptor carries no `failFirst`.
|
|
86
87
|
* - The adapter does NOT strictly validate kind/target/rule: it faithfully reconstructs whatever
|
|
@@ -118,6 +119,13 @@ function descriptorFromProjection(projected) {
|
|
|
118
119
|
const fixture = scalar(projected.check_violation_fixture);
|
|
119
120
|
if (fixture.trim().length > 0)
|
|
120
121
|
descriptor.violationFixture = fixture;
|
|
122
|
+
// `cleanFixture` (#1346) rides BOTH kinds — reconstruct it from `check_clean_fixture` so the
|
|
123
|
+
// causation control runs end-to-end: when present the prover also requires the check to stay GREEN
|
|
124
|
+
// against this known-clean subject (proving the violation RED is content-dependent). Absent/blank ->
|
|
125
|
+
// no control (the documented residual remains; backward-compatible with the #1314 compose path).
|
|
126
|
+
const clean = scalar(projected.check_clean_fixture);
|
|
127
|
+
if (clean.trim().length > 0)
|
|
128
|
+
descriptor.cleanFixture = clean;
|
|
121
129
|
return descriptor;
|
|
122
130
|
}
|
|
123
131
|
/** node --test argv. Forces the TAP reporter so the summary counts are parseable + version-stable;
|
|
@@ -360,6 +368,31 @@ const CHECK_MAX_BUFFER = 16 * 1024 * 1024;
|
|
|
360
368
|
function posTimeout(timeoutMs, def) {
|
|
361
369
|
return typeof timeoutMs === 'number' && timeoutMs > 0 ? timeoutMs : def;
|
|
362
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Spawn the negative `node --test` against a single subject (set via the `GSD_PROHIB_SUBJECT`
|
|
373
|
+
* convention, #1279) and return its TAP output. Reuses the bounded-subprocess machinery
|
|
374
|
+
* (`process.execPath`, arg arrays → no shell, `childEnv`, bounded `timeout`/`maxBuffer`) and NEVER
|
|
375
|
+
* throws — a RED run exits non-zero, so the partial TAP (with the `# fail` summary) is recovered from
|
|
376
|
+
* the thrown error's `stdout`. The prover calls this once per subject: the KNOWN-BAD violation fixture
|
|
377
|
+
* (expect RED) and, for the #1346 causation control, the KNOWN-CLEAN control subject (expect GREEN).
|
|
378
|
+
*/
|
|
379
|
+
function runNodeTestWithSubject(check, cwd, subject, timeoutMs) {
|
|
380
|
+
try {
|
|
381
|
+
return (0, node_child_process_1.execFileSync)(process.execPath, buildNodeTestArgs(check), {
|
|
382
|
+
cwd,
|
|
383
|
+
encoding: 'utf-8',
|
|
384
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
385
|
+
windowsHide: true,
|
|
386
|
+
env: { ...childEnv(), GSD_PROHIB_SUBJECT: subject },
|
|
387
|
+
timeout: posTimeout(timeoutMs, NODE_TEST_TIMEOUT_MS),
|
|
388
|
+
maxBuffer: CHECK_MAX_BUFFER,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
const stdout = e && typeof e === 'object' && 'stdout' in e ? e.stdout : '';
|
|
393
|
+
return typeof stdout === 'string' ? stdout : '';
|
|
394
|
+
}
|
|
395
|
+
}
|
|
363
396
|
function defaultRunCheck(check, cwd, timeoutMs) {
|
|
364
397
|
try {
|
|
365
398
|
if (check.kind === 'node-test') {
|
|
@@ -482,36 +515,36 @@ function defaultProveFailFirst(check, cwd, timeoutMs) {
|
|
|
482
515
|
// a setup crash, not from the prohibition firing. Requiring the fixture to exist before spawning
|
|
483
516
|
// closes the realistic typo/stale-path case (#1279 review, Major 1).
|
|
484
517
|
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
518
|
+
// CAUSATION (#1346): existence + a non-vacuous red is necessary but not sufficient — a deceptive
|
|
519
|
+
// negative test that reds merely BECAUSE `GSD_PROHIB_SUBJECT` is set (rather than because the
|
|
520
|
+
// subject's CONTENT violates the must-NOT) would otherwise be accepted. The OPTIONAL `cleanFixture`
|
|
521
|
+
// control below proves content-dependence when supplied (red on bad AND green on clean). When NO
|
|
522
|
+
// clean fixture is authored the control cannot run, so the residual remains a documented constraint
|
|
523
|
+
// for that case (an author opts into the stronger proof by supplying a known-clean control subject).
|
|
490
524
|
// Resolve the fixture against `cwd` (NOT the verify process's cwd): the spawned test reads
|
|
491
525
|
// `GSD_PROHIB_SUBJECT` and resolves a relative subject against `cwd`, so the existence check must
|
|
492
526
|
// use the SAME base or it could pass here yet ENOENT in the child (re-opening the fail-open hole).
|
|
493
527
|
if (!fixture || !node_fs_1.default.existsSync(node_path_1.default.resolve(cwd, fixture)))
|
|
494
528
|
return { provenFailFirst: false };
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
out = typeof stdout === 'string' ? stdout : '';
|
|
529
|
+
// Run the negative test against the KNOWN-BAD subject and require a NON-VACUOUS red.
|
|
530
|
+
const redOut = runNodeTestWithSubject(check, cwd, fixture, timeoutMs);
|
|
531
|
+
if (!isNonVacuousNodeTestRed(redOut, check.target))
|
|
532
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
533
|
+
// #1346 CAUSATION CONTROL (optional): if a clean control subject is supplied, run the SAME test
|
|
534
|
+
// against it and require it to stay GREEN. This proves the red above was caused by the subject's
|
|
535
|
+
// CONTENT — a deceptive test that reds merely because GSD_PROHIB_SUBJECT is SET reds here too →
|
|
536
|
+
// not content-dependent → not proven. Absent → no control (documented residual; backward-compat).
|
|
537
|
+
const clean = check.cleanFixture;
|
|
538
|
+
if (clean) {
|
|
539
|
+
// A supplied-but-missing/typo'd control path can't run the control → fail-closed, symmetric
|
|
540
|
+
// with the violation-fixture existence guard (resolve against the SAME `cwd` as the child).
|
|
541
|
+
if (!node_fs_1.default.existsSync(node_path_1.default.resolve(cwd, clean)))
|
|
542
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
543
|
+
const cleanOut = runNodeTestWithSubject(check, cwd, clean, timeoutMs);
|
|
544
|
+
if (!isNonVacuousNodeTestPass(cleanOut, check.target))
|
|
545
|
+
return { provenFailFirst: false, method: 'violation-fixture' };
|
|
513
546
|
}
|
|
514
|
-
return { provenFailFirst:
|
|
547
|
+
return { provenFailFirst: true, method: 'violation-fixture' };
|
|
515
548
|
}
|
|
516
549
|
// Unknown kind — defensive; the LOCATE guard already rejects it.
|
|
517
550
|
return { provenFailFirst: false };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Project-Root Resolution Module — resolves a project root from a starting
|
|
4
|
-
* directory by walking the ancestor chain and applying
|
|
4
|
+
* directory by walking the ancestor chain and applying five heuristics:
|
|
5
5
|
* (0) own .planning/ guard (#1362)
|
|
6
6
|
* (1) parent .planning/config.json sub_repos
|
|
7
7
|
* (2) legacy multiRepo: true + ancestor .git
|
|
8
8
|
* (3) .git heuristic with parent .planning/
|
|
9
|
+
* (4) nearest ancestor .planning/ (#1414, Resolution Provenance P1)
|
|
9
10
|
* Bounded by FIND_PROJECT_ROOT_MAX_DEPTH ancestors. Sync I/O.
|
|
10
11
|
*
|
|
11
12
|
* ADR-457 build-at-publish: the hand-written bin/lib/project-root.cjs
|
|
@@ -17,6 +18,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
17
18
|
};
|
|
18
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
20
|
exports.findProjectRoot = findProjectRoot;
|
|
21
|
+
exports.consentProjectRoot = consentProjectRoot;
|
|
20
22
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
23
|
const node_path_1 = __importDefault(require("node:path"));
|
|
22
24
|
const node_os_1 = __importDefault(require("node:os"));
|
|
@@ -107,13 +109,98 @@ function findProjectRoot(startDir) {
|
|
|
107
109
|
}
|
|
108
110
|
if (matched)
|
|
109
111
|
return parent;
|
|
110
|
-
// Heuristic: parent has .planning/ and we're inside a git repo.
|
|
112
|
+
// Heuristic (3): parent has .planning/ and we're inside a git repo.
|
|
113
|
+
// Before returning, check if any further ancestor has sub_repos that explicitly
|
|
114
|
+
// claims our startDir — explicit sub_repos config takes precedence over the
|
|
115
|
+
// implicit .git signal. (#1422)
|
|
111
116
|
if (isInsideGitRepo(parent)) {
|
|
117
|
+
// Lookahead: walk ancestors above `parent` to find a sub_repos claim.
|
|
118
|
+
let ancestor = node_path_1.default.dirname(parent);
|
|
119
|
+
let ancestorDepth = 0;
|
|
120
|
+
while (ancestor !== fsRoot && ancestor !== home && ancestorDepth < FIND_PROJECT_ROOT_MAX_DEPTH) {
|
|
121
|
+
const ancestorPlanning = ancestor + node_path_1.default.sep + '.planning';
|
|
122
|
+
try {
|
|
123
|
+
if (node_fs_1.default.existsSync(ancestorPlanning) && node_fs_1.default.statSync(ancestorPlanning).isDirectory()) {
|
|
124
|
+
const ancestorConfig = ancestor + node_path_1.default.sep + '.planning' + node_path_1.default.sep + 'config.json';
|
|
125
|
+
const rawA = node_fs_1.default.readFileSync(ancestorConfig, 'utf-8');
|
|
126
|
+
const cfgA = JSON.parse(rawA);
|
|
127
|
+
const subReposValueA = cfgA['sub_repos'] ??
|
|
128
|
+
(cfgA['planning'] && typeof cfgA['planning'] === 'object'
|
|
129
|
+
? cfgA['planning']['sub_repos']
|
|
130
|
+
: undefined);
|
|
131
|
+
const subReposA = Array.isArray(subReposValueA) ? subReposValueA : [];
|
|
132
|
+
if (subReposA.length > 0) {
|
|
133
|
+
const relPathA = node_path_1.default.relative(ancestor, resolvedStart);
|
|
134
|
+
const topSegmentA = relPathA.split(node_path_1.default.sep)[0];
|
|
135
|
+
if (subReposA.includes(topSegmentA)) {
|
|
136
|
+
return ancestor;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore — config missing or unparseable, keep walking
|
|
143
|
+
}
|
|
144
|
+
const nextAncestor = node_path_1.default.dirname(ancestor);
|
|
145
|
+
if (nextAncestor === ancestor)
|
|
146
|
+
break;
|
|
147
|
+
ancestor = nextAncestor;
|
|
148
|
+
ancestorDepth += 1;
|
|
149
|
+
}
|
|
112
150
|
return parent;
|
|
113
151
|
}
|
|
114
152
|
}
|
|
115
153
|
dir = parent;
|
|
116
154
|
depth += 1;
|
|
117
155
|
}
|
|
156
|
+
// Heuristic (4): nearest ancestor .planning/ — last resort before fallback.
|
|
157
|
+
// Runs only after heuristics (1)–(3) have been exhausted without a match,
|
|
158
|
+
// ensuring sub_repos / multiRepo / .git-based resolution always wins when
|
|
159
|
+
// applicable. Walks upward again within the same FIND_PROJECT_ROOT_MAX_DEPTH
|
|
160
|
+
// bound; returns the nearest ancestor directory that contains a .planning/
|
|
161
|
+
// subdirectory so config resolves correctly when invoked from a plain
|
|
162
|
+
// descendant of a single-repo project. (#1414)
|
|
163
|
+
let dir2 = resolvedStart;
|
|
164
|
+
let depth2 = 0;
|
|
165
|
+
while (dir2 !== fsRoot && depth2 < FIND_PROJECT_ROOT_MAX_DEPTH) {
|
|
166
|
+
const parent2 = node_path_1.default.dirname(dir2);
|
|
167
|
+
if (parent2 === dir2)
|
|
168
|
+
break;
|
|
169
|
+
try {
|
|
170
|
+
const candidatePlanning = parent2 + node_path_1.default.sep + '.planning';
|
|
171
|
+
if (node_fs_1.default.existsSync(candidatePlanning) && node_fs_1.default.statSync(candidatePlanning).isDirectory()) {
|
|
172
|
+
return parent2;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// ignore fs errors and continue walking
|
|
177
|
+
}
|
|
178
|
+
if (parent2 === home)
|
|
179
|
+
break;
|
|
180
|
+
dir2 = parent2;
|
|
181
|
+
depth2 += 1;
|
|
182
|
+
}
|
|
118
183
|
return startDir;
|
|
119
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* #1459 (IC-01 / CB-4): THE single canonical derivation of the PROJECT ROOT used to bind/lookup a
|
|
187
|
+
* project-scope consent record. Install (the CLI/lifecycle RECORD site), the loader (the LOOKUP
|
|
188
|
+
* site), and `trust revoke` (CB-4) MUST all derive the consent root through this one helper so the
|
|
189
|
+
* recorded key always matches the looked-up key — otherwise installing from a SUBDIR records consent
|
|
190
|
+
* at `realpath(subdir)` while the loader looks it up at `realpath(findProjectRoot)` and the freshly
|
|
191
|
+
* installed cap is immediately INACTIVE (install-then-inactive).
|
|
192
|
+
*
|
|
193
|
+
* The rule: `realpath(findProjectRoot(cwd))` (findProjectRoot is total — it returns `cwd` itself when
|
|
194
|
+
* no project root is found, so there is no null branch), falling back to `path.resolve(cwd)` when the
|
|
195
|
+
* resolved root cannot be realpath'd (e.g. it does not exist yet). The consent store realpaths
|
|
196
|
+
* whatever it is given, so passing the SAME logical root from every site is what guarantees the match.
|
|
197
|
+
*/
|
|
198
|
+
function consentProjectRoot(cwd) {
|
|
199
|
+
const root = findProjectRoot(cwd);
|
|
200
|
+
try {
|
|
201
|
+
return node_fs_1.default.realpathSync(root);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return node_path_1.default.resolve(root);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Resolution Convention — canonical shape for config-interpreting read verbs.
|
|
4
|
+
*
|
|
5
|
+
* Extracted as the anchor for ADR-1411 P3 (Resolution Provenance, #1416).
|
|
6
|
+
* Exports the `Resolution<T>` envelope used when a verb reads and interprets
|
|
7
|
+
* configuration (e.g. agent-skills). Not used by mutation verbs (see
|
|
8
|
+
* capability-writer's `SetCapabilityStateResult` for the mutation shape) or
|
|
9
|
+
* plain read verbs (see capability-state's `ResolveCapabilityRuntimeStateResult`).
|
|
10
|
+
*
|
|
11
|
+
* This is a pure types+builder leaf — no other src/ imports.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.makeResolution = makeResolution;
|
|
15
|
+
// ─── Builder ──────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Construct a `Resolution<T>` envelope from a value and its provenance fields.
|
|
18
|
+
*/
|
|
19
|
+
function makeResolution(value, opts) {
|
|
20
|
+
return {
|
|
21
|
+
value,
|
|
22
|
+
configured: opts.configured,
|
|
23
|
+
reason: opts.reason,
|
|
24
|
+
warnings: opts.warnings,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -150,10 +150,23 @@ function routeRoadmapCommand({ roadmap, args, cwd, raw, error }) {
|
|
|
150
150
|
},
|
|
151
151
|
'upgrade': () => {
|
|
152
152
|
const dryRun = !args.includes('--apply');
|
|
153
|
-
|
|
153
|
+
// Parse `--convention <value>` and `--convention=<value>`. When the flag is
|
|
154
|
+
// absent entirely, default to the only supported convention; when present
|
|
155
|
+
// with a missing/unsupported value, fall through to the rejection below
|
|
156
|
+
// (fail-closed — never silently run a migration the user did not request).
|
|
157
|
+
let convention = 'milestone-prefixed';
|
|
158
|
+
const conventionFlagIdx = args.findIndex((a) => a === '--convention' || a.startsWith('--convention='));
|
|
159
|
+
if (conventionFlagIdx !== -1) {
|
|
160
|
+
const token = args[conventionFlagIdx];
|
|
161
|
+
convention = token.includes('=')
|
|
162
|
+
? token.slice(token.indexOf('=') + 1)
|
|
163
|
+
: (args[conventionFlagIdx + 1] ?? '');
|
|
164
|
+
}
|
|
154
165
|
if (convention !== 'milestone-prefixed') {
|
|
155
|
-
|
|
156
|
-
process.exit
|
|
166
|
+
// No-throw hub contract (ADR-0012): a hub-dispatched handler must not call
|
|
167
|
+
// process.exit. Throw instead — the hub converts this to HandlerFailure and
|
|
168
|
+
// the adapter routes it through the injected error() boundary.
|
|
169
|
+
throw new Error('Only --convention milestone-prefixed is supported');
|
|
157
170
|
}
|
|
158
171
|
const plan = roadmapUpgrade.computeMigrationPlan(cwd);
|
|
159
172
|
roadmapUpgrade.applyMigration(cwd, plan, { dryRun });
|