@opengsd/gsd-core 1.8.0 → 1.9.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +31 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.md +36 -0
- package/agents/gsd-executor.md +20 -7
- package/agents/gsd-intel-updater.md +3 -3
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +20 -0
- package/agents/gsd-planner.md +15 -23
- package/agents/gsd-project-researcher.md +2 -2
- package/agents/gsd-ui-auditor.md +0 -40
- package/bin/install.js +186 -55
- package/commands/gsd/plan-review-convergence.md +5 -1
- package/gsd-core/bin/gsd-tools.cjs +849 -2
- package/gsd-core/bin/lib/api-coverage.cjs +22 -8
- package/gsd-core/bin/lib/audit.cjs +8 -8
- package/gsd-core/bin/lib/capability-consent.cjs +40 -1
- package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
- package/gsd-core/bin/lib/capability-loader.cjs +23 -1
- package/gsd-core/bin/lib/capability-registry.cjs +1353 -132
- package/gsd-core/bin/lib/capability-trust.cjs +468 -33
- package/gsd-core/bin/lib/capability-validator.cjs +882 -6
- package/gsd-core/bin/lib/check-command-router.cjs +12 -2
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +102 -12
- package/gsd-core/bin/lib/claude-orchestration.cjs +125 -22
- package/gsd-core/bin/lib/commands.cjs +246 -18
- package/gsd-core/bin/lib/config-loader.cjs +200 -28
- package/gsd-core/bin/lib/config.cjs +90 -5
- package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
- package/gsd-core/bin/lib/frontmatter.cjs +125 -15
- package/gsd-core/bin/lib/host-integration.cjs +215 -8
- package/gsd-core/bin/lib/init.cjs +44 -19
- package/gsd-core/bin/lib/install-engine.cjs +1 -0
- package/gsd-core/bin/lib/milestone.cjs +5 -5
- package/gsd-core/bin/lib/model-catalog.cjs +51 -1
- package/gsd-core/bin/lib/observability/logger.cjs +7 -2
- package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
- package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
- package/gsd-core/bin/lib/phase-id.cjs +278 -5
- package/gsd-core/bin/lib/phase.cjs +57 -5
- package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
- package/gsd-core/bin/lib/plan-scan.cjs +1 -1
- package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
- package/gsd-core/bin/lib/profile-output.cjs +34 -8
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
- package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
- package/gsd-core/bin/lib/roadmap-parser.cjs +54 -6
- package/gsd-core/bin/lib/roadmap.cjs +10 -4
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +31 -4
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +1 -1
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +140 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
- package/gsd-core/bin/lib/smart-entry.cjs +1 -1
- package/gsd-core/bin/lib/state-document.cjs +164 -20
- package/gsd-core/bin/lib/state-transition.cjs +28 -10
- package/gsd-core/bin/lib/state.cjs +141 -21
- package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
- package/gsd-core/bin/lib/uat.cjs +9 -7
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
- package/gsd-core/bin/lib/unusable-input.cjs +216 -0
- package/gsd-core/bin/lib/validate.cjs +32 -0
- package/gsd-core/bin/lib/verification.cjs +51 -14
- package/gsd-core/bin/lib/verify.cjs +128 -20
- package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
- package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -13
- package/gsd-core/bin/shared/model-catalog.json +5 -0
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
- package/gsd-core/references/context-budget.md +40 -0
- package/gsd-core/references/gate-prompts.md +6 -3
- package/gsd-core/references/model-profile-resolution.md +64 -13
- package/gsd-core/references/offer-next.md +88 -0
- package/gsd-core/references/planning-config.md +2 -1
- package/gsd-core/references/reviewer-instances.md +28 -21
- package/gsd-core/references/runtime-aware-dispatch.md +42 -0
- package/gsd-core/references/ui-consideration-probe.md +2 -2
- package/gsd-core/references/worktree-branch-check.md +4 -4
- package/gsd-core/templates/summary-minimal.md +4 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +7 -0
- package/gsd-core/workflows/ai-integration-phase.md +4 -4
- package/gsd-core/workflows/audit-fix.md +4 -0
- package/gsd-core/workflows/audit-milestone.md +8 -0
- package/gsd-core/workflows/autonomous.md +19 -15
- package/gsd-core/workflows/check-todos.md +2 -2
- package/gsd-core/workflows/code-review-fix.md +14 -6
- package/gsd-core/workflows/code-review.md +76 -19
- package/gsd-core/workflows/debug.md +10 -2
- package/gsd-core/workflows/diagnose-issues.md +4 -0
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
- package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
- package/gsd-core/workflows/discuss-phase-assumptions.md +15 -9
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/docs-update.md +8 -0
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
- package/gsd-core/workflows/execute-phase.md +85 -115
- package/gsd-core/workflows/execute-plan.md +5 -4
- package/gsd-core/workflows/explore.md +4 -0
- package/gsd-core/workflows/extract-learnings.md +21 -0
- package/gsd-core/workflows/help/modes/full.md +3 -3
- package/gsd-core/workflows/import.md +4 -1
- package/gsd-core/workflows/ingest-docs.md +4 -0
- package/gsd-core/workflows/map-codebase.md +13 -6
- package/gsd-core/workflows/new-milestone.md +10 -2
- package/gsd-core/workflows/new-project.md +11 -4
- package/gsd-core/workflows/next.md +5 -2
- package/gsd-core/workflows/plan-phase.md +42 -46
- package/gsd-core/workflows/plan-review-convergence.md +18 -14
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +14 -3
- package/gsd-core/workflows/review.md +146 -575
- package/gsd-core/workflows/scan.md +9 -1
- package/gsd-core/workflows/secure-phase.md +10 -2
- package/gsd-core/workflows/ship.md +41 -11
- package/gsd-core/workflows/smart-entry.md +1 -1
- package/gsd-core/workflows/ui-phase.md +8 -1
- package/gsd-core/workflows/ui-review.md +8 -1
- package/gsd-core/workflows/update.md +104 -5
- package/gsd-core/workflows/validate-phase.md +10 -2
- package/gsd-core/workflows/verify-work.md +8 -1
- package/hooks/dist/gsd-cursor-session-start.js +6 -2
- package/hooks/dist/gsd-cursor-stop.js +6 -2
- package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
- package/hooks/dist/gsd-graphify-update.sh +9 -0
- package/hooks/dist/gsd-phase-boundary.sh +14 -2
- package/hooks/dist/gsd-prompt-guard.js +101 -2
- package/hooks/dist/gsd-read-guard.js +100 -2
- package/hooks/dist/gsd-read-injection-scanner.js +109 -2
- package/hooks/dist/gsd-statusline.js +9 -6
- package/hooks/dist/gsd-workflow-guard.js +110 -6
- package/hooks/dist/gsd-worktree-path-guard.js +132 -8
- package/hooks/dist/lib/cursor-workspace.js +74 -0
- package/hooks/gsd-cursor-session-start.js +6 -2
- package/hooks/gsd-cursor-stop.js +6 -2
- package/hooks/gsd-cursor-subagent-start.js +6 -2
- package/hooks/gsd-graphify-update.sh +9 -0
- package/hooks/gsd-phase-boundary.sh +14 -2
- package/hooks/gsd-prompt-guard.js +101 -2
- package/hooks/gsd-read-guard.js +100 -2
- package/hooks/gsd-read-injection-scanner.js +109 -2
- package/hooks/gsd-statusline.js +9 -6
- package/hooks/gsd-workflow-guard.js +110 -6
- package/hooks/gsd-worktree-path-guard.js +132 -8
- package/hooks/lib/cursor-workspace.js +74 -0
- package/package.json +7 -7
- package/pi/gsd.cjs +26 -1
- package/scripts/check-coverage-gate.cjs +51 -0
- package/scripts/check-glossary-refs.cjs +24 -0
- package/scripts/ci-test-scope.cjs +67 -17
- package/scripts/gen-adr-index.cjs +6 -4
- package/scripts/gen-capability-matrix.cjs +26 -2
- package/scripts/gen-capability-registry.cjs +132 -34
- package/scripts/gen-emitted-baseline.cjs +145 -0
- package/scripts/lint-compiled-artifact-sync.cjs +146 -0
- package/scripts/lint-emitted-drift-ack.cjs +149 -0
- package/scripts/lint-fix-has-regression-test.cjs +131 -0
- package/scripts/lint-resolution-provenance.cjs +9 -0
- package/scripts/mutation-matrix.cjs +4 -0
- package/scripts/prompt-injection-scan.sh +6 -0
- package/scripts/registry-schema.cjs +57 -8
- package/scripts/release-notes/conventional-title.cjs +19 -1
- package/scripts/release-notes/format-github-release-notes.cjs +7 -3
- package/scripts/workflow-size.cjs +16 -8
- package/skills/gsd-plan-review-convergence/SKILL.md +5 -1
- package/vscode/package.json +1 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
- package/scripts/update-size-baseline.cjs +0 -68
|
@@ -24,7 +24,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
24
24
|
const { toPosixPath, generateSlugInternal, extractOneLinerFromBody } = coreUtilsMod;
|
|
25
25
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
26
26
|
const phaseIdMod = require("./phase-id.cjs");
|
|
27
|
-
const { normalizePhaseName, comparePhaseNum, extractPhaseToken } = phaseIdMod;
|
|
27
|
+
const { normalizePhaseName, comparePhaseNum, extractPhaseToken, PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
|
28
28
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
29
29
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
30
30
|
const { getArchivedPhaseDirs, findPhaseInternal } = phaseLocatorMod;
|
|
@@ -39,6 +39,8 @@ const agentCommandRouterMod = require("./agent-command-router.cjs");
|
|
|
39
39
|
const { AGENT_FAILURE_CLASSES } = agentCommandRouterMod;
|
|
40
40
|
const model_catalog_cjs_1 = require("./model-catalog.cjs");
|
|
41
41
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
|
+
const hostIntegrationMod = require("./host-integration.cjs");
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
42
44
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
43
45
|
const { planningDir, planningPaths } = planningWorkspace;
|
|
44
46
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -104,12 +106,13 @@ function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
|
|
|
104
106
|
const files = node_fs_1.default.readdirSync(phaseDir);
|
|
105
107
|
const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
|
|
106
108
|
if (verificationFile) {
|
|
107
|
-
const
|
|
109
|
+
const verificationFilePath = node_path_1.default.join(phaseDir, verificationFile);
|
|
110
|
+
const content = (0, shell_command_projection_cjs_1.platformReadSync)(verificationFilePath) || '';
|
|
108
111
|
// #1159 (Defect A): read ONLY the frontmatter `status` key to avoid false
|
|
109
112
|
// matches from historical body metadata such as `previous_status: gaps_found`.
|
|
110
113
|
// Full-text regexes like /status:\s*gaps_found/ match the substring inside
|
|
111
114
|
// `previous_status: gaps_found`, producing incorrect phase status labels.
|
|
112
|
-
const fm = extractFrontmatter(content);
|
|
115
|
+
const fm = extractFrontmatter(content, verificationFilePath);
|
|
113
116
|
// Normalise to lower-case to preserve the prior case-insensitive behaviour
|
|
114
117
|
// while reading only the frontmatter `status` key (not the full body text).
|
|
115
118
|
const fmStatus = typeof fm['status'] === 'string' ? fm['status'].trim().toLowerCase() : '';
|
|
@@ -261,7 +264,7 @@ function cmdListSeeds(cwd, statusFilter, raw) {
|
|
|
261
264
|
const content = (0, shell_command_projection_cjs_1.platformReadSync)(safeFilePath);
|
|
262
265
|
if (content === null)
|
|
263
266
|
continue;
|
|
264
|
-
const fm = extractFrontmatter(content);
|
|
267
|
+
const fm = extractFrontmatter(content, safeFilePath);
|
|
265
268
|
const status = (fmStr(fm.status) || 'dormant').toLowerCase().trim() || 'dormant';
|
|
266
269
|
// Match on the raw lowercased status (both sides already normalized);
|
|
267
270
|
// sanitizeForDisplay is for output, not comparison.
|
|
@@ -347,11 +350,12 @@ function cmdHistoryDigest(cwd, raw) {
|
|
|
347
350
|
for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
|
|
348
351
|
const summaries = node_fs_1.default.readdirSync(dirPath).filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
|
349
352
|
for (const summary of summaries) {
|
|
350
|
-
const
|
|
353
|
+
const summaryFilePath = node_path_1.default.join(dirPath, summary);
|
|
354
|
+
const content = (0, shell_command_projection_cjs_1.platformReadSync)(summaryFilePath);
|
|
351
355
|
if (content === null)
|
|
352
356
|
continue;
|
|
353
357
|
try {
|
|
354
|
-
const fm = extractFrontmatter(content);
|
|
358
|
+
const fm = extractFrontmatter(content, summaryFilePath);
|
|
355
359
|
const phaseNum = fm['phase'] || dir.split('-')[0];
|
|
356
360
|
if (!digest.phases[phaseNum]) {
|
|
357
361
|
digest.phases[phaseNum] = {
|
|
@@ -440,7 +444,7 @@ function cmdResolveGranularity(cwd, phaseType, raw, override) {
|
|
|
440
444
|
* fast_mode, fast_mode_supported, [unknown_agent] }
|
|
441
445
|
*
|
|
442
446
|
* Flags: --effort <level>, --fast-mode <true|false>, --attempt <n>,
|
|
443
|
-
* --failure-class <class> (#2296)
|
|
447
|
+
* --failure-class <class> (#2296), --host <runtime-id> (#2481)
|
|
444
448
|
*/
|
|
445
449
|
function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
446
450
|
if (!agentType) {
|
|
@@ -496,12 +500,54 @@ function cmdResolveExecution(cwd, agentType, raw, opts) {
|
|
|
496
500
|
fast_mode: fastMode,
|
|
497
501
|
fast_mode_supported: fastModeSupported,
|
|
498
502
|
};
|
|
503
|
+
// ADR-1239 amendment (#2481) / ADR-443 path (a): invocation-time effort for a
|
|
504
|
+
// named host. The host's negotiated `effortSurface` decides WHETHER an argument
|
|
505
|
+
// is emitted; the catalog knows the syntax. Absent --host the contract is
|
|
506
|
+
// byte-identical to before, so every existing caller is unaffected.
|
|
507
|
+
if (typeof opts.host === 'string' && opts.host.length > 0) {
|
|
508
|
+
const surface = effortSurfaceForHost(cwd, opts.host);
|
|
509
|
+
const argvRendered = (0, model_catalog_cjs_1.renderEffortArgv)(opts.host, effort, surface);
|
|
510
|
+
result['host'] = opts.host;
|
|
511
|
+
result['effort_surface'] = surface;
|
|
512
|
+
result['effort_argv'] = argvRendered.argv;
|
|
513
|
+
result['effort_argv_string'] = argvRendered.argv.join(' ');
|
|
514
|
+
result['effort_argv_value'] = argvRendered.value;
|
|
515
|
+
}
|
|
499
516
|
if (!agentModels)
|
|
500
517
|
result['unknown_agent'] = true;
|
|
501
518
|
if (escalation)
|
|
502
519
|
result['escalation'] = escalation;
|
|
503
520
|
output(result, raw, effort);
|
|
504
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* ADR-1239 amendment (#2481) — resolve a host's negotiated `effortSurface`.
|
|
524
|
+
*
|
|
525
|
+
* Reads the host's runtime descriptor from the generated capability registry and
|
|
526
|
+
* runs it through the Host-Integration negotiation so the trust-boundary invariant
|
|
527
|
+
* applies here exactly as everywhere else: an unknown host, a missing axis, or the
|
|
528
|
+
* `undocumented` sentinel all degrade to the safe floor rather than being trusted.
|
|
529
|
+
* Never throws — a lookup failure yields `'none'`, which renders no argument.
|
|
530
|
+
*/
|
|
531
|
+
function effortSurfaceForHost(cwd, host) {
|
|
532
|
+
void cwd;
|
|
533
|
+
try {
|
|
534
|
+
// Mirrors the lazy-require pattern from runtime-slash.cts §runtimeSlash —
|
|
535
|
+
// capability-registry.cjs is generated and carries no type declarations.
|
|
536
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
537
|
+
const { runtimes } = require('./capability-registry.cjs');
|
|
538
|
+
const declared = runtimes[host]?.runtime?.hostIntegration;
|
|
539
|
+
if (!declared || typeof declared !== 'object')
|
|
540
|
+
return 'none';
|
|
541
|
+
// The descriptor is untrusted JSON; negotiation applies the trust-boundary
|
|
542
|
+
// invariant (effective ⊆ host-declared ∩ engine-known) and fails closed.
|
|
543
|
+
const negotiated = hostIntegrationMod.negotiateHostCapabilities(declared);
|
|
544
|
+
const surface = negotiated?.effective?.effortSurface;
|
|
545
|
+
return typeof surface === 'string' ? surface : 'none';
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
return 'none';
|
|
549
|
+
}
|
|
550
|
+
}
|
|
505
551
|
/**
|
|
506
552
|
* #488 — Replace or inject the `effort:` value in YAML frontmatter.
|
|
507
553
|
* Unlike injectEffortFrontmatter (install.js), this overwrites an existing value.
|
|
@@ -594,6 +640,59 @@ function cmdEffortSync(cwd, raw, opts) {
|
|
|
594
640
|
}
|
|
595
641
|
output({ synced, skipped, changes, dry_run: dryRun, agents_dir: agentsDir }, raw, synced > 0 ? 'changed' : 'ok');
|
|
596
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Detect the phase number for a commit from its `--files` path list.
|
|
645
|
+
*
|
|
646
|
+
* #2539: the extraction is anchored to the directory segment immediately under
|
|
647
|
+
* `.planning/phases/` or `.planning/milestones/<version>-phases/`, then run
|
|
648
|
+
* through the project-code-aware `extractPhaseToken` helper. The prior
|
|
649
|
+
* unanchored `match(/(\d+(?:\.\d+)*)-/)` returned the leftmost digit-run-then-
|
|
650
|
+
* hyphen anywhere in the joined path, so a project_code ending in a digit
|
|
651
|
+
* (e.g. PROJECT_V2) made `…/PROJECT_V2-07-name/…` match the `2-` inside `V2-`
|
|
652
|
+
* before the real `07-` phase token — resolving phase "2" instead of "7".
|
|
653
|
+
*
|
|
654
|
+
* Returns the phase number string (e.g. '07', '45.14'), or null when no phase
|
|
655
|
+
* directory segment is present in any of the file paths (e.g. a commit of
|
|
656
|
+
* `.planning/ROADMAP.md` has no phase segment, so no branch is resolved —
|
|
657
|
+
* matching the prior regex-no-match behaviour).
|
|
658
|
+
*/
|
|
659
|
+
function detectPhaseNumberFromFiles(files) {
|
|
660
|
+
if (!files || files.length === 0)
|
|
661
|
+
return null;
|
|
662
|
+
// A phase directory lives one segment below a `phases` parent segment:
|
|
663
|
+
// .planning/phases/<phase-dir>/…
|
|
664
|
+
// .planning/milestones/v1.0-phases/<phase-dir>/…
|
|
665
|
+
// The segment immediately after the `…phases` segment is the phase directory
|
|
666
|
+
// name. extractPhaseToken owns the project-code-aware token read.
|
|
667
|
+
for (const file of files) {
|
|
668
|
+
const norm = String(file).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
669
|
+
const segments = norm.split('/');
|
|
670
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
671
|
+
if (segments[i] === 'phases' || segments[i].endsWith('-phases')) {
|
|
672
|
+
const phaseDir = segments[i + 1];
|
|
673
|
+
if (!phaseDir)
|
|
674
|
+
continue;
|
|
675
|
+
const token = extractPhaseToken(phaseDir);
|
|
676
|
+
// extractPhaseToken falls back to returning dirName unchanged when no
|
|
677
|
+
// numeric token is found. normalizePhaseName is the canonical arbiter
|
|
678
|
+
// of "is this a real phase token": it strips the project-code prefix
|
|
679
|
+
// and returns a zero-padded numeric form for a genuine phase token, or
|
|
680
|
+
// the input unchanged otherwise. Accept the token only when it
|
|
681
|
+
// normalizes to a numeric phase form (the single-owner rule shared by
|
|
682
|
+
// every other phase-token reader — see #2528).
|
|
683
|
+
const normalized = normalizePhaseName(token);
|
|
684
|
+
// Built from the single-owner PHASE_NUMBER_TOKEN_SOURCE (the canonical
|
|
685
|
+
// phase-number grammar — #2128 anti-divergence guard) so this read-side
|
|
686
|
+
// acceptance check cannot drift from every other phase-token reader.
|
|
687
|
+
const phaseTokenShape = new RegExp(`^${PHASE_NUMBER_TOKEN_SOURCE}$`, 'i');
|
|
688
|
+
if (token !== phaseDir && phaseTokenShape.test(normalized)) {
|
|
689
|
+
return token;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
597
696
|
function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
598
697
|
if (!message && !amend) {
|
|
599
698
|
error('commit message required');
|
|
@@ -629,10 +728,21 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
629
728
|
if (branchingStrategy && branchingStrategy !== 'none') {
|
|
630
729
|
let branchName = null;
|
|
631
730
|
if (branchingStrategy === 'phase') {
|
|
632
|
-
// Determine which phase we're committing for from the file paths
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
731
|
+
// Determine which phase we're committing for from the file paths.
|
|
732
|
+
// #2539: the extraction is anchored to the directory SEGMENT immediately
|
|
733
|
+
// under `.planning/phases/` (or `.planning/milestones/<v>-phases/`) and
|
|
734
|
+
// runs through the project-code-aware extractPhaseToken helper, NOT a
|
|
735
|
+
// free unanchored regex. The prior `match(/(\d+(?:\.\d+)*)-/)` returned
|
|
736
|
+
// the leftmost digit-run-then-hyphen anywhere in the joined path, so a
|
|
737
|
+
// project_code ending in a digit (PROJECT_V2) made `.../PROJECT_V2-07-…`
|
|
738
|
+
// match the `2-` inside `V2-` before the real `07-` phase token —
|
|
739
|
+
// resolving phase "2" instead of phase "7" and silently checking out the
|
|
740
|
+
// wrong branch. extractPhaseToken already owns project-code-aware phase-
|
|
741
|
+
// token parsing (it is the single owner shared by the other 6 call sites
|
|
742
|
+
// — see #2528 for the parallel drift problem in phase-locator/phase),
|
|
743
|
+
// so this is the canonical path-segment-bound read, not a fourth copy.
|
|
744
|
+
const phaseNum = detectPhaseNumberFromFiles(files);
|
|
745
|
+
if (phaseNum) {
|
|
636
746
|
const phaseInfo = findPhaseInternal(cwd, phaseNum);
|
|
637
747
|
if (phaseInfo) {
|
|
638
748
|
branchName = config['phase_branch_template']
|
|
@@ -652,10 +762,23 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
652
762
|
if (branchName) {
|
|
653
763
|
const currentBranch = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
|
|
654
764
|
if (currentBranch.exitCode === 0 && currentBranch.stdout.trim() !== branchName) {
|
|
655
|
-
//
|
|
765
|
+
// #2539: the #1278 intent is to CREATE the phase/milestone branch
|
|
766
|
+
// before the FIRST commit on it — not to force-switch an already-
|
|
767
|
+
// checked-out working branch onto a DIFFERENT existing branch. The
|
|
768
|
+
// prior fallback to a bare `git checkout <branch>` silently switched
|
|
769
|
+
// the whole working tree onto an existing unrelated branch in the same
|
|
770
|
+
// call that then committed (the only trace was a reflog entry). So:
|
|
771
|
+
// create-if-absent only. If the resolved branch already exists and the
|
|
772
|
+
// tree is on some other branch, do NOT switch — but never silently: log
|
|
773
|
+
// the resolution so the operator sees that the phase branch was
|
|
774
|
+
// resolved and deliberately not switched to (#2539 AC2: an auto-
|
|
775
|
+
// checkout mid-commit must never happen silently).
|
|
656
776
|
const create = (0, shell_command_projection_cjs_1.execGit)(['checkout', '-b', branchName], { cwd });
|
|
657
777
|
if (create.exitCode !== 0) {
|
|
658
|
-
|
|
778
|
+
// `git checkout -b` fails (non-zero) when the branch already exists.
|
|
779
|
+
// The operator is on the branch they intend to be on; commit there.
|
|
780
|
+
process.stderr.write(`Warning: resolved ${branchingStrategy} branch "${branchName}" already exists; ` +
|
|
781
|
+
`committing on the current branch "${currentBranch.stdout.trim()}" instead of switching.\n`);
|
|
659
782
|
}
|
|
660
783
|
}
|
|
661
784
|
}
|
|
@@ -664,8 +787,21 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
664
787
|
const explicitFiles = files && files.length > 0;
|
|
665
788
|
const filesToStage = explicitFiles ? files : ['.planning/'];
|
|
666
789
|
const stagedPaths = [];
|
|
790
|
+
// #2608: a `git add` that fails must abort the commit, not be skipped.
|
|
791
|
+
// #2523 stopped a failed path entering the commit pathspec, but skipping it
|
|
792
|
+
// silently left two bad outcomes: a PARTIAL commit when only some requested
|
|
793
|
+
// paths failed, and a misleading `nothing_to_commit` when all of them did —
|
|
794
|
+
// in both cases the original staging error (permissions, unwritable index in
|
|
795
|
+
// a linked worktree, timeout) was discarded and the operator saw a downstream
|
|
796
|
+
// pathspec error pointing at an innocent file.
|
|
797
|
+
const stagingFailures = [];
|
|
798
|
+
// Paths already in the index BEFORE this call. On a staging failure the
|
|
799
|
+
// rollback below unstages only what THIS call added — unstaging a path the
|
|
800
|
+
// caller had staged themselves would destroy their work.
|
|
801
|
+
const preStaged = new Set((0, shell_command_projection_cjs_1.execGit)(['diff', '--cached', '--name-only'], { cwd })
|
|
802
|
+
.stdout.split('\n').map(s => s.trim()).filter(Boolean));
|
|
667
803
|
for (const file of filesToStage) {
|
|
668
|
-
const fullPath = node_path_1.default.
|
|
804
|
+
const fullPath = node_path_1.default.resolve(cwd, file);
|
|
669
805
|
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
670
806
|
if (explicitFiles) {
|
|
671
807
|
// Caller passed an explicit --files list: missing files are skipped.
|
|
@@ -675,13 +811,74 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
675
811
|
}
|
|
676
812
|
// Default mode (staging all of .planning/): stage the deletion so
|
|
677
813
|
// removed planning files are not left dangling in the index.
|
|
678
|
-
|
|
814
|
+
// This mutates the index exactly like `git add` does, so it fails closed
|
|
815
|
+
// the same way — an unwritable index must not be swallowed here either.
|
|
816
|
+
// `--ignore-unmatch` already makes "no such path" a success, so a non-zero
|
|
817
|
+
// exit is a real I/O failure, not a missing file.
|
|
818
|
+
const rmResult = (0, shell_command_projection_cjs_1.execGit)(['rm', '--cached', '--ignore-unmatch', file], { cwd });
|
|
819
|
+
if (rmResult.exitCode !== 0) {
|
|
820
|
+
const rmErr = rmResult.error;
|
|
821
|
+
stagingFailures.push({
|
|
822
|
+
file,
|
|
823
|
+
error: rmResult.stderr || rmResult.stdout,
|
|
824
|
+
timed_out: rmResult.signal === 'SIGTERM' && rmErr?.code === 'ETIMEDOUT',
|
|
825
|
+
});
|
|
826
|
+
}
|
|
679
827
|
}
|
|
680
828
|
else {
|
|
681
|
-
(0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
|
|
682
|
-
|
|
829
|
+
const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
|
|
830
|
+
// Only record paths that actually staged — a failed `git add` (permissions,
|
|
831
|
+
// out-of-repo edge) must not enter the commit pathspec (#2523). Mirrors
|
|
832
|
+
// cmdCommitToSubrepo's exitCode-gated push.
|
|
833
|
+
if (addResult.exitCode === 0) {
|
|
834
|
+
stagedPaths.push(file);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
// `SpawnResultOutput.error` is typed `Error | null`; widen to the errno
|
|
838
|
+
// shape by ANNOTATION rather than assertion — `Error` is assignable to
|
|
839
|
+
// `NodeJS.ErrnoException` (its extra fields are optional), so an `as`
|
|
840
|
+
// cast here trips no-unnecessary-type-assertion.
|
|
841
|
+
const addErr = addResult.error;
|
|
842
|
+
stagingFailures.push({
|
|
843
|
+
file,
|
|
844
|
+
error: addResult.stderr || addResult.stdout,
|
|
845
|
+
// The projection exposes a timeout distinctly (#2608 AC5); this is the
|
|
846
|
+
// same SIGTERM+ETIMEDOUT idiom worktree-safety.cts uses.
|
|
847
|
+
timed_out: addResult.signal === 'SIGTERM' && addErr?.code === 'ETIMEDOUT',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
683
850
|
}
|
|
684
851
|
}
|
|
852
|
+
// #2608: fail closed before `git commit` runs. Checked ahead of the
|
|
853
|
+
// nothing_to_commit branch below so a run where EVERY path failed to stage
|
|
854
|
+
// reports the staging cause rather than "nothing to commit", and ahead of the
|
|
855
|
+
// commit itself so a multi-file scope never partially commits the subset that
|
|
856
|
+
// happened to stage.
|
|
857
|
+
if (stagingFailures.length > 0) {
|
|
858
|
+
// Fail closed AND clean. Without this the paths that DID stage stay in the
|
|
859
|
+
// index with no commit made, so the next bare `git commit` sweeps them up —
|
|
860
|
+
// the same silent partial commit this fix exists to prevent, deferred one
|
|
861
|
+
// step. Mirrors cmdPrSubrepo's rollback-then-error convention. Only paths
|
|
862
|
+
// this call staged are unstaged (preStaged is excluded), and the reset is
|
|
863
|
+
// best-effort: if the index is unwritable — the very failure being reported
|
|
864
|
+
// — the reset cannot succeed either, and the staging error is still what
|
|
865
|
+
// gets returned.
|
|
866
|
+
const toUnstage = stagedPaths.filter(p => !preStaged.has(p));
|
|
867
|
+
if (toUnstage.length > 0) {
|
|
868
|
+
(0, shell_command_projection_cjs_1.execGit)(['reset', '-q', '--', ...toUnstage], { cwd });
|
|
869
|
+
}
|
|
870
|
+
const first = stagingFailures[0];
|
|
871
|
+
const result = {
|
|
872
|
+
committed: false,
|
|
873
|
+
hash: null,
|
|
874
|
+
reason: first.timed_out ? 'staging_timeout' : 'staging_failed',
|
|
875
|
+
file: first.file,
|
|
876
|
+
error: first.error,
|
|
877
|
+
failures: stagingFailures,
|
|
878
|
+
};
|
|
879
|
+
output(result, raw, 'failed');
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
685
882
|
// Commit — when the caller declared a scope (--files), append a pathspec so
|
|
686
883
|
// only the declared files land in the commit, not the entire index (#2112).
|
|
687
884
|
// The pathspec uses stagedPaths (not filesToStage) so skipped missing files
|
|
@@ -802,13 +999,44 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
802
999
|
for (const [repo, repoFiles] of Object.entries(grouped)) {
|
|
803
1000
|
const repoCwd = node_path_1.default.join(cwd, repo);
|
|
804
1001
|
// Stage files (strip sub-repo prefix for paths relative to that repo)
|
|
1002
|
+
// #2608: this is the sub-repo twin of cmdCommit's staging loop and carried
|
|
1003
|
+
// the identical defect — a failed `git add` was dropped silently and the
|
|
1004
|
+
// function went straight on to commit the subset that happened to stage,
|
|
1005
|
+
// discarding git's stderr. Fails closed per-repo, with the same rollback of
|
|
1006
|
+
// only what this call staged.
|
|
1007
|
+
const preStagedSub = new Set((0, shell_command_projection_cjs_1.execGit)(['diff', '--cached', '--name-only'], { cwd: repoCwd })
|
|
1008
|
+
.stdout.split('\n').map(s => s.trim()).filter(Boolean));
|
|
805
1009
|
const stagedRelPaths = [];
|
|
1010
|
+
const subStagingFailures = [];
|
|
806
1011
|
for (const file of repoFiles) {
|
|
807
1012
|
const relativePath = file.slice(repo.length + 1);
|
|
808
1013
|
const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
809
1014
|
if (addResult.exitCode === 0) {
|
|
810
1015
|
stagedRelPaths.push(relativePath);
|
|
811
1016
|
}
|
|
1017
|
+
else {
|
|
1018
|
+
const addErr = addResult.error;
|
|
1019
|
+
subStagingFailures.push({
|
|
1020
|
+
file,
|
|
1021
|
+
error: addResult.stderr || addResult.stdout,
|
|
1022
|
+
timed_out: addResult.signal === 'SIGTERM' && addErr?.code === 'ETIMEDOUT',
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (subStagingFailures.length > 0) {
|
|
1027
|
+
const toUnstageSub = stagedRelPaths.filter(p => !preStagedSub.has(p));
|
|
1028
|
+
if (toUnstageSub.length > 0) {
|
|
1029
|
+
(0, shell_command_projection_cjs_1.execGit)(['reset', '-q', '--', ...toUnstageSub], { cwd: repoCwd });
|
|
1030
|
+
}
|
|
1031
|
+
const firstSub = subStagingFailures[0];
|
|
1032
|
+
repos[repo] = {
|
|
1033
|
+
committed: false,
|
|
1034
|
+
hash: null,
|
|
1035
|
+
files: repoFiles,
|
|
1036
|
+
reason: firstSub.timed_out ? 'staging_timeout' : 'staging_failed',
|
|
1037
|
+
error: firstSub.error,
|
|
1038
|
+
};
|
|
1039
|
+
continue;
|
|
812
1040
|
}
|
|
813
1041
|
// Commit — pathspec limits the commit to the staged files only (#2112)
|
|
814
1042
|
const isMergeInProgressSub = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
|
|
@@ -994,7 +1222,7 @@ function cmdSummaryExtract(cwd, summaryPath, fields, raw) {
|
|
|
994
1222
|
return;
|
|
995
1223
|
}
|
|
996
1224
|
const content = node_fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
997
|
-
const fm = extractFrontmatter(content);
|
|
1225
|
+
const fm = extractFrontmatter(content, fullPath);
|
|
998
1226
|
// Parse key-decisions into structured format
|
|
999
1227
|
const parseDecisions = (decisionsList) => {
|
|
1000
1228
|
if (!decisionsList || !Array.isArray(decisionsList))
|