@opengsd/gsd-core 1.4.4 → 1.5.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/README.md +3 -3
- package/agents/gsd-code-fixer.md +3 -2
- package/agents/gsd-debug-session-manager.md +2 -1
- package/agents/gsd-debugger.md +4 -3
- package/agents/gsd-executor.md +17 -16
- package/agents/gsd-intel-updater.md +38 -41
- package/agents/gsd-phase-researcher.md +8 -8
- package/agents/gsd-plan-checker.md +23 -13
- package/agents/gsd-planner.md +32 -188
- package/agents/gsd-project-researcher.md +5 -4
- package/agents/gsd-research-synthesizer.md +2 -1
- package/agents/gsd-ui-researcher.md +2 -1
- package/agents/gsd-verifier.md +12 -11
- package/bin/install.js +965 -1486
- package/commands/gsd/autonomous.md +5 -1
- package/commands/gsd/ns-manage.md +8 -1
- package/commands/gsd/ns-project.md +5 -0
- package/commands/gsd/ns-review.md +4 -1
- package/commands/gsd/ns-workflow.md +7 -1
- package/commands/gsd/plan-review-convergence.md +5 -4
- package/commands/gsd/surface.md +12 -5
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +198 -101
- package/gsd-core/bin/gsd_run +20 -0
- package/gsd-core/bin/lib/audit-command-router.cjs +61 -0
- package/gsd-core/bin/lib/capability-registry.cjs +2234 -0
- package/gsd-core/bin/lib/capability-state.cjs +336 -0
- package/gsd-core/bin/lib/check-command-router.cjs +133 -2
- package/gsd-core/bin/lib/cli-exit.cjs +22 -3
- package/gsd-core/bin/lib/config-loader.cjs +716 -0
- package/gsd-core/bin/lib/configuration.cjs +4 -34
- package/gsd-core/bin/lib/core-utils.cjs +198 -0
- package/gsd-core/bin/lib/core.cjs +107 -1817
- package/gsd-core/bin/lib/edge-probe.cjs +173 -0
- package/gsd-core/bin/lib/fallow-runner.cjs +63 -25
- package/gsd-core/bin/lib/federated-config.cjs +182 -0
- package/gsd-core/bin/lib/graphify-command-router.cjs +74 -0
- package/gsd-core/bin/lib/init.cjs +58 -12
- package/gsd-core/bin/lib/install-profiles.cjs +157 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +116 -0
- package/gsd-core/bin/lib/intel.cjs +3 -3
- package/gsd-core/bin/lib/io.cjs +222 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +105 -0
- package/gsd-core/bin/lib/loop-resolver.cjs +460 -0
- package/gsd-core/bin/lib/model-resolver.cjs +426 -0
- package/gsd-core/bin/lib/phase-command-router.cjs +20 -0
- package/gsd-core/bin/lib/phase-id.cjs +215 -0
- package/gsd-core/bin/lib/phase-locator.cjs +148 -0
- package/gsd-core/bin/lib/phase.cjs +17 -0
- package/gsd-core/bin/lib/probe-core.cjs +257 -0
- package/gsd-core/bin/lib/profile-pipeline.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +443 -0
- package/gsd-core/bin/lib/roadmap.cjs +44 -1
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +92 -95
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +68 -29
- package/gsd-core/bin/lib/runtime-homes.cjs +163 -87
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +1439 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +2 -1
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +13 -0
- package/gsd-core/bin/lib/state-document.cjs +8 -0
- package/gsd-core/bin/lib/state.cjs +114 -2
- package/gsd-core/bin/lib/surface.cjs +66 -14
- package/gsd-core/bin/lib/uat-predicate.cjs +329 -0
- package/gsd-core/bin/lib/update-context.cjs +4 -1
- package/gsd-core/bin/lib/verify.cjs +104 -1
- package/gsd-core/bin/lib/worktree-base-ref.cjs +33 -8
- package/gsd-core/bin/shared/model-catalog.json +11 -6
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -1
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +7 -0
- package/gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +8 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json +1 -0
- package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json +4 -0
- package/gsd-core/references/edge-probe.md +261 -0
- package/gsd-core/references/planner-antipatterns.md +41 -0
- package/gsd-core/references/planner-guidance.md +186 -0
- package/gsd-core/references/planner-reviews.md +5 -2
- package/gsd-core/templates/phase-prompt.md +7 -7
- package/gsd-core/templates/project.md +19 -2
- package/gsd-core/templates/spec.md +12 -0
- package/gsd-core/templates/summary-complex.md +1 -0
- package/gsd-core/templates/summary-minimal.md +1 -0
- package/gsd-core/templates/summary-standard.md +1 -0
- package/gsd-core/templates/summary.md +1 -0
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +111 -51
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +6 -4
- package/gsd-core/workflows/code-review.md +53 -17
- package/gsd-core/workflows/complete-milestone.md +11 -5
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +8 -1
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +4 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +8 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +1 -1
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +1 -1
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +1 -1
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +9 -1
- package/gsd-core/workflows/new-project.md +9 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase.md +65 -28
- package/gsd-core/workflows/plan-review-convergence.md +60 -33
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +2 -2
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +1 -1
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -7
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +2 -2
- package/gsd-core/workflows/ship.md +8 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/spec-phase.md +130 -1
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +2 -2
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +8 -1
- package/package.json +11 -3
- package/scripts/base64-scan.sh +1 -1
- package/scripts/changeset/cli.cjs +8 -1
- package/scripts/changeset/lint.cjs +38 -2
- package/scripts/ci-test-scope.cjs +21 -10
- package/scripts/gen-capability-registry.cjs +2293 -0
- package/scripts/gen-loop-host-contract.cjs +471 -0
- package/scripts/lib/allowlist-ratchet.cjs +101 -1
- package/scripts/lint-regression-test-names.allowlist.json +269 -0
- package/scripts/lint-regression-test-names.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +25 -4
- package/scripts/lint-windows-test-portability.cjs +178 -0
- package/scripts/prompt-injection-scan.sh +4 -4
- package/scripts/research-profiles.cjs +10 -10
- package/scripts/run-tests.cjs +133 -29
- package/scripts/secret-scan.sh +3 -3
- package/scripts/sync-next-version.cjs +133 -0
- package/scripts/sync-runtime-launcher.cjs +21 -5
- package/scripts/update-size-baseline.cjs +68 -0
- package/scripts/workflow-policy.cjs +42 -9
- package/scripts/workflow-size.cjs +90 -0
- package/scripts/run-cross-platform-tests.cjs +0 -67
|
@@ -28,11 +28,12 @@ const FALLBACK_ALIASES = {
|
|
|
28
28
|
copilot: ['copilot', 'copilot-cli', 'github-copilot'],
|
|
29
29
|
antigravity: ['antigravity', 'antigravity-cli', 'antigravity-agent'],
|
|
30
30
|
cursor: ['cursor', 'cursor-cli', 'cursor-nightly'],
|
|
31
|
-
windsurf: ['windsurf', 'windsurf-cli', 'windsurf-next'],
|
|
31
|
+
windsurf: ['windsurf', 'windsurf-cli', 'windsurf-next', 'devin-desktop'],
|
|
32
32
|
augment: ['augment', 'augment-code', 'augment-cli'],
|
|
33
33
|
trae: ['trae', 'trae-cli'],
|
|
34
34
|
qwen: ['qwen', 'qwen-code', 'qwen-cli'],
|
|
35
35
|
hermes: ['hermes', 'hermes-agent', 'hermes-cli'],
|
|
36
|
+
kimi: ['kimi'],
|
|
36
37
|
codebuddy: ['codebuddy', 'codebuddy-cli'],
|
|
37
38
|
cline: ['cline', 'cline-cli'],
|
|
38
39
|
};
|
|
@@ -61,8 +61,13 @@ function formatGsdSlash(commandName, runtime) {
|
|
|
61
61
|
const tail = wsMatch && wsMatch[2] ? wsMatch[2] : '';
|
|
62
62
|
const runtimeText = (typeof runtime === 'string' && runtime ? runtime : 'claude').toLowerCase();
|
|
63
63
|
const rt = (0, runtime_name_policy_cjs_1.canonicalizeRuntimeName)(runtimeText) || runtimeText;
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
// Descriptor-driven: look up commandStyle from the capability registry.
|
|
65
|
+
// Mirrors the lazy-require pattern from runtime-homes.cts §getGlobalConfigDir.
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
67
|
+
const { runtimes } = require('./capability-registry.cjs');
|
|
68
|
+
const style = runtimes[rt]?.runtime?.commandStyle;
|
|
69
|
+
if (style === 'shell-var') {
|
|
70
|
+
// shell-var runtimes (currently: codex) use $gsd-<cmd> syntax. The command
|
|
66
71
|
// token is lowercased because shell-var identifiers are conventionally
|
|
67
72
|
// lowercase; matches the convertCodexSlash() projection in bin/install.js.
|
|
68
73
|
return `$gsd-${token.toLowerCase()}${tail}`;
|
|
@@ -232,6 +232,19 @@ function isManagedHookCommand(commandText, opts = {}) {
|
|
|
232
232
|
const managedBasenames = managedHookCommandSurfaceSet(surface, includeLegacyAliases);
|
|
233
233
|
if (!managedBasenames || managedBasenames.size === 0)
|
|
234
234
|
return false;
|
|
235
|
+
// args-form check: the managed hook filename may appear in args[] rather than
|
|
236
|
+
// in command when a windowless launcher wraps the Node invocation. (#976)
|
|
237
|
+
// Only treat as managed when an arg basename matches the managed hook set —
|
|
238
|
+
// prevents false-positives for non-GSD entries that happen to share a path segment.
|
|
239
|
+
if (Array.isArray(opts.args) && opts.args.length > 0) {
|
|
240
|
+
for (const arg of opts.args) {
|
|
241
|
+
if (typeof arg !== 'string')
|
|
242
|
+
continue;
|
|
243
|
+
const argBasename = arg.replace(/\\/g, '/').split('/').pop() || '';
|
|
244
|
+
if (isManagedHookBasename(argBasename, { surface }))
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
235
248
|
const normalizedCommand = commandText.replace(/\\/g, '/');
|
|
236
249
|
if (typeof opts.configDir === 'string' && opts.configDir.length > 0) {
|
|
237
250
|
const normalizedHooksDir = `${node_path_1.default.join(opts.configDir, 'hooks').replace(/\\/g, '/')}/`;
|
|
@@ -176,6 +176,14 @@ exports.KNOWN_STATUS_PATTERNS = [
|
|
|
176
176
|
/^Phase\s+\d+\s+complete/i,
|
|
177
177
|
/^Verifying Phase\s+\d+/i,
|
|
178
178
|
/^Phase complete/i,
|
|
179
|
+
// #1070: LLM executors (e.g. OpenCode) may write "Complete ✓" or bare "Complete"
|
|
180
|
+
// when finishing a phase. Only bare terminal markers yield to the next phase's
|
|
181
|
+
// "Ready to execute" during planned-phase. The pattern is anchored at both ends
|
|
182
|
+
// so that statuses with trailing prose (e.g. "Complete but needs manual QA",
|
|
183
|
+
// "Complete — ready for verification") are NOT matched and are preserved as
|
|
184
|
+
// executor-authored values. Only exact forms like "Complete", "Complete ✓",
|
|
185
|
+
// "Complete✓", or "Complete ☑ " (trailing whitespace) match.
|
|
186
|
+
/^Complete\s*[✓✔✅☑]?\s*$/i,
|
|
179
187
|
];
|
|
180
188
|
/**
|
|
181
189
|
* Returns true when the given value is a known template default for the field,
|
|
@@ -607,6 +607,7 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
607
607
|
}
|
|
608
608
|
const now = clock_cjs_1.realClock.nowIso();
|
|
609
609
|
const updated = [];
|
|
610
|
+
let sessionCreated = false;
|
|
610
611
|
readModifyWriteStateMd(statePath, (content) => {
|
|
611
612
|
// Update Last session / Last Date
|
|
612
613
|
let result = (0, state_document_cjs_1.stateReplaceField)(content, 'Last session', now);
|
|
@@ -661,10 +662,80 @@ function cmdStateRecordSession(cwd, options, raw) {
|
|
|
661
662
|
}
|
|
662
663
|
}
|
|
663
664
|
}
|
|
665
|
+
// Bug #944: DWIM normalize/auto-create — when the caller supplied --stopped-at or
|
|
666
|
+
// --resume-file but the body lacks the canonical labels (in-place replace
|
|
667
|
+
// returned a miss), persist the values durably. Mirrors the DWIM pattern used
|
|
668
|
+
// by add-decision, add-blocker, and record-metric. Never silently drop
|
|
669
|
+
// caller-supplied values.
|
|
670
|
+
//
|
|
671
|
+
// Guard: only act when the caller actually supplied a value. When no
|
|
672
|
+
// --stopped-at / --resume-file are given and the body already had no session
|
|
673
|
+
// labels (nothing was updated), we return recorded:false — the existing
|
|
674
|
+
// behaviour for a no-op call that didn't supply any values.
|
|
675
|
+
//
|
|
676
|
+
// Correctness invariant: both buildStateFrontmatter and cmdStateSnapshot read
|
|
677
|
+
// only the FIRST `## Session` block (via a /##\s*Session\s*\n…/i regex).
|
|
678
|
+
// If we blindly append a second `## Session` block when one already exists, the
|
|
679
|
+
// newly-written Stopped at / Resume file end up in the second (invisible) block.
|
|
680
|
+
// Fix: when a `## Session` heading already exists, normalize THAT block in place
|
|
681
|
+
// (insert / replace canonical bold-label lines within the existing section).
|
|
682
|
+
// Only append a brand-new section when NO `## Session` heading exists at all.
|
|
683
|
+
const callerSuppliedValues = !!(options.stopped_at || (options.resume_file !== undefined && options.resume_file !== null));
|
|
684
|
+
const needsStoppedAt = options.stopped_at && !updated.includes('Stopped At');
|
|
685
|
+
const needsResumeFile = options.resume_file !== undefined && options.resume_file !== null && !updated.includes('Resume File');
|
|
686
|
+
const needsLastSession = !updated.includes('Last session') && !updated.includes('Last Date');
|
|
687
|
+
if (callerSuppliedValues && (needsStoppedAt || needsResumeFile || needsLastSession)) {
|
|
688
|
+
const resumeValue = (options.resume_file !== undefined && options.resume_file !== null)
|
|
689
|
+
? options.resume_file
|
|
690
|
+
: 'None';
|
|
691
|
+
const stoppedAtValue = options.stopped_at || 'None';
|
|
692
|
+
// Determine whether a ## Session heading already exists in the body.
|
|
693
|
+
const existingSessionHeading = /^## Session\s*$/im.test(content);
|
|
694
|
+
if (existingSessionHeading) {
|
|
695
|
+
// Normalize in place: replace the ENTIRE BODY of the existing ## Session
|
|
696
|
+
// section (heading + all content up to the next ## heading or EOF) with
|
|
697
|
+
// canonical bold-label lines. The negative-lookahead per-line pattern
|
|
698
|
+
// `(?!^## )[\s\S]` consumes every line that doesn't start with "## ",
|
|
699
|
+
// which correctly stops at the next section boundary without consuming it.
|
|
700
|
+
// A trailing blank line is added so the next ## heading keeps its spacing.
|
|
701
|
+
content = content.replace(/^(## Session[ \t]*\n(?:(?!^## )[\s\S])*)/m, [
|
|
702
|
+
'## Session',
|
|
703
|
+
'',
|
|
704
|
+
`**Last session:** ${now}`,
|
|
705
|
+
`**Stopped at:** ${stoppedAtValue}`,
|
|
706
|
+
`**Resume file:** ${resumeValue}`,
|
|
707
|
+
'',
|
|
708
|
+
'',
|
|
709
|
+
].join('\n'));
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
// No ## Session heading exists at all — append a new canonical section.
|
|
713
|
+
const scaffold = [
|
|
714
|
+
'',
|
|
715
|
+
'## Session',
|
|
716
|
+
'',
|
|
717
|
+
`**Last session:** ${now}`,
|
|
718
|
+
`**Stopped at:** ${stoppedAtValue}`,
|
|
719
|
+
`**Resume file:** ${resumeValue}`,
|
|
720
|
+
'',
|
|
721
|
+
].join('\n');
|
|
722
|
+
content = content.trimEnd() + '\n' + scaffold;
|
|
723
|
+
}
|
|
724
|
+
sessionCreated = true;
|
|
725
|
+
if (needsLastSession)
|
|
726
|
+
updated.push('Last session');
|
|
727
|
+
if (needsStoppedAt)
|
|
728
|
+
updated.push('Stopped At');
|
|
729
|
+
if (needsResumeFile)
|
|
730
|
+
updated.push('Resume File');
|
|
731
|
+
}
|
|
664
732
|
return content;
|
|
665
733
|
}, cwd);
|
|
666
734
|
if (updated.length > 0) {
|
|
667
|
-
|
|
735
|
+
const result = { recorded: true, updated };
|
|
736
|
+
if (sessionCreated)
|
|
737
|
+
result['created'] = true;
|
|
738
|
+
output(result, raw, 'true');
|
|
668
739
|
}
|
|
669
740
|
else {
|
|
670
741
|
output({ recorded: false, reason: 'No session fields found in STATE.md' }, raw, 'false');
|
|
@@ -748,8 +819,12 @@ function cmdStateSnapshot(cwd, raw) {
|
|
|
748
819
|
const sessionMatch = body.match(/##\s*Session\s*\n([\s\S]*?)(?=\n##|$)/i);
|
|
749
820
|
if (sessionMatch) {
|
|
750
821
|
const sessionSection = sessionMatch[1];
|
|
822
|
+
// Accept both `**Last Date:**` (canonical template form) and `**Last session:**`
|
|
823
|
+
// (the form written by the DWIM auto-create / normalize path added for #944).
|
|
751
824
|
const lastDateMatch = sessionSection.match(/\*\*Last Date:\*\*\s*(.+)/i)
|
|
752
|
-
|| sessionSection.match(/^Last Date:\s*(.+)/im)
|
|
825
|
+
|| sessionSection.match(/^Last Date:\s*(.+)/im)
|
|
826
|
+
|| sessionSection.match(/\*\*Last session:\*\*\s*(.+)/i)
|
|
827
|
+
|| sessionSection.match(/^Last session:\s*(.+)/im);
|
|
753
828
|
const stoppedAtMatch = sessionSection.match(/\*\*Stopped At:\*\*\s*(.+)/i)
|
|
754
829
|
|| sessionSection.match(/^Stopped At:\s*(.+)/im);
|
|
755
830
|
const resumeFileMatch = sessionSection.match(/\*\*Resume File:\*\*\s*(.+)/i)
|
|
@@ -974,12 +1049,39 @@ function syncStateFrontmatter(content, cwd) {
|
|
|
974
1049
|
if (derivedFm['status'] === 'unknown' && existingFm['status'] && existingFm['status'] !== 'unknown') {
|
|
975
1050
|
derivedFm['status'] = existingFm['status'];
|
|
976
1051
|
}
|
|
1052
|
+
// Bug #948: preserve `milestone_name` / `milestone` when the derived value
|
|
1053
|
+
// is the template placeholder 'milestone'. getMilestoneInfo returns the
|
|
1054
|
+
// literal string 'milestone' when it cannot match the version from the roadmap
|
|
1055
|
+
// (e.g. no ROADMAP.md, roadmap lacks the heading for the stored version, or the
|
|
1056
|
+
// milestone version read from STATE.md itself triggers the lookup before the
|
|
1057
|
+
// file is fully written). A placeholder must never overwrite a real name that the
|
|
1058
|
+
// existing frontmatter already holds; only an empty derived value falls through
|
|
1059
|
+
// to this guard (the primary #905 preserve path below handles that).
|
|
1060
|
+
const MILESTONE_NAME_PLACEHOLDER = 'milestone';
|
|
1061
|
+
if (derivedFm['milestone_name'] === MILESTONE_NAME_PLACEHOLDER &&
|
|
1062
|
+
existingFm['milestone_name'] &&
|
|
1063
|
+
existingFm['milestone_name'] !== MILESTONE_NAME_PLACEHOLDER) {
|
|
1064
|
+
derivedFm['milestone_name'] = existingFm['milestone_name'];
|
|
1065
|
+
// Keep the stored milestone version consistent with the preserved name.
|
|
1066
|
+
if (existingFm['milestone']) {
|
|
1067
|
+
derivedFm['milestone'] = existingFm['milestone'];
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
977
1070
|
// Bug #905: preserve scalar fields that buildStateFrontmatter can only derive
|
|
978
1071
|
// from body annotations (Current Phase:, Current Plan:, etc.). When those
|
|
979
1072
|
// annotations are absent — e.g. after an agent or tool rewrites the body —
|
|
980
1073
|
// buildStateFrontmatter returns no value for those keys. Mirror the same
|
|
981
1074
|
// fallback pattern used in cmdStateJson so the existing frontmatter values
|
|
982
1075
|
// survive every writeStateMd call.
|
|
1076
|
+
//
|
|
1077
|
+
// For stopped_at / paused_at: the original #905 "fall back when derived is
|
|
1078
|
+
// absent" rule is preserved here. The stale-body-overwrites-frontmatter
|
|
1079
|
+
// scenario from #948 is prevented by the no-op guard in
|
|
1080
|
+
// readModifyWriteStateMd: when the transform produces no change the file is
|
|
1081
|
+
// never written, so syncStateFrontmatter never even runs. Attempting to
|
|
1082
|
+
// "always prefer frontmatter" here breaks legitimate callers like phase.complete
|
|
1083
|
+
// that intentionally write a new stopped_at value to the body and expect
|
|
1084
|
+
// syncStateFrontmatter to pick it up.
|
|
983
1085
|
if (!derivedFm['stopped_at'] && existingFm['stopped_at']) {
|
|
984
1086
|
derivedFm['stopped_at'] = existingFm['stopped_at'];
|
|
985
1087
|
}
|
|
@@ -1155,6 +1257,16 @@ function readModifyWriteStateMd(statePath, transformFn, cwd, options, clock) {
|
|
|
1155
1257
|
// restore it when resync is false.
|
|
1156
1258
|
const preFm = resync ? null : extractFrontmatter(content);
|
|
1157
1259
|
const modified = transformFn(content);
|
|
1260
|
+
// Bug #948: no-op guard — if the transform produced no change, do NOT write
|
|
1261
|
+
// the file. An unconditional write would bump `last_updated`, reset
|
|
1262
|
+
// `milestone_name` to the template placeholder, and resurrect stale
|
|
1263
|
+
// body-derived `stopped_at` values via syncStateFrontmatter. Skipping the
|
|
1264
|
+
// write when content is unchanged is safe because every caller that mutates
|
|
1265
|
+
// content already returns the mutated string, and callers that detect a
|
|
1266
|
+
// no-op explicitly return the original content unchanged.
|
|
1267
|
+
if (modified === content) {
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1158
1270
|
let synced = syncStateFrontmatter(modified, cwd);
|
|
1159
1271
|
if (!resync && preFm && preFm['progress']) {
|
|
1160
1272
|
// Re-apply the curated progress block that syncStateFrontmatter just
|
|
@@ -12,11 +12,18 @@
|
|
|
12
12
|
* Exports:
|
|
13
13
|
* readSurface(runtimeConfigDir)
|
|
14
14
|
* writeSurface(runtimeConfigDir, surfaceState)
|
|
15
|
-
* resolveSurface(runtimeConfigDir, manifest, clusterMap)
|
|
16
|
-
* applySurface(runtimeConfigDir, layout, manifest, clusterMap)
|
|
17
|
-
* listSurface(runtimeConfigDir, manifest, clusterMap)
|
|
15
|
+
* resolveSurface(runtimeConfigDir, manifest, clusterMap?, registry?)
|
|
16
|
+
* applySurface(runtimeConfigDir, layout, manifest, clusterMap?, registry?)
|
|
17
|
+
* listSurface(runtimeConfigDir, manifest, clusterMap?, registry?)
|
|
18
18
|
* pruneSkillDirs(skillsDir, retainedNames, prefix, manifest)
|
|
19
19
|
*
|
|
20
|
+
* The optional `registry` param (ADR-857 phase 4c) accepts the capability-registry
|
|
21
|
+
* object. When present, capability clusters are merged into the effective cluster
|
|
22
|
+
* map and the registry is threaded into resolveProfile so capability-contributed
|
|
23
|
+
* skills participate in the base set and disable-ability. Absent or undefined
|
|
24
|
+
* leaves behaviour identical to the pre-registry path (no-op for current registry
|
|
25
|
+
* where UI=full and the full profile returns '*' regardless).
|
|
26
|
+
*
|
|
20
27
|
* ADR-457 build-at-publish: the hand-written bin/lib/surface.cjs collapsed
|
|
21
28
|
* to a TypeScript source of truth. Behaviour is preserved byte-for-behaviour
|
|
22
29
|
* from the prior hand-written .cjs; only types are added.
|
|
@@ -86,10 +93,11 @@ function clustersToSkills(clusterNames, clusterMap) {
|
|
|
86
93
|
const result = new Set();
|
|
87
94
|
for (const name of clusterNames) {
|
|
88
95
|
const members = clusterMap[name];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
// FIX 5: guard against non-iterable members — malformed registry must never throw
|
|
97
|
+
if (!Array.isArray(members))
|
|
98
|
+
continue;
|
|
99
|
+
for (const s of members)
|
|
100
|
+
result.add(s);
|
|
93
101
|
}
|
|
94
102
|
return result;
|
|
95
103
|
}
|
|
@@ -132,19 +140,63 @@ function normalizeSkillManifest(runtimeConfigDir, manifest) {
|
|
|
132
140
|
* 2. Remove skills in disabled clusters
|
|
133
141
|
* 3. Add explicitAdds (and their transitive closure)
|
|
134
142
|
* 4. Remove explicitRemoves (only the stem itself, no cascade)
|
|
143
|
+
*
|
|
144
|
+
* ADR-857 phase 4c: optional registry param. When present:
|
|
145
|
+
* - capability clusters are merged into the effective cluster map so
|
|
146
|
+
* capability-owned skill groups are disable-able.
|
|
147
|
+
* - registry is threaded into resolveProfile so capability skills
|
|
148
|
+
* participate in the base skill set and their requires: chains expand.
|
|
135
149
|
*/
|
|
136
|
-
function resolveSurface(runtimeConfigDir, manifest, clusterMap) {
|
|
137
|
-
|
|
150
|
+
function resolveSurface(runtimeConfigDir, manifest, clusterMap, registry) {
|
|
151
|
+
// Merge capability clusters into the cluster map when registry is provided.
|
|
152
|
+
// The ADR-857 phase 4a HARD gate guarantees that when a capId matches a CLUSTERS
|
|
153
|
+
// key, the values are EQUAL — so the spread is idempotent for matching names.
|
|
154
|
+
// Defense-in-depth: if a capId collides with a hand-authored CLUSTERS key AND
|
|
155
|
+
// the values DIFFER (future drift bypassing the gate), prefer the hand-authored
|
|
156
|
+
// value so disable behavior is never silently changed by a stale registry entry.
|
|
157
|
+
// Also guard: skip entries whose value is not a string[] (malformed registry).
|
|
158
|
+
let cm = clusterMap || clusters_cjs_1.CLUSTERS;
|
|
159
|
+
if (registry && registry.capabilityClusters && typeof registry.capabilityClusters === 'object') {
|
|
160
|
+
const baseCm = cm;
|
|
161
|
+
const capClusters = registry.capabilityClusters;
|
|
162
|
+
const merged = { ...baseCm };
|
|
163
|
+
for (const capId of Object.keys(capClusters)) {
|
|
164
|
+
const val = capClusters[capId];
|
|
165
|
+
// FIX 5: skip malformed (non-array) entries — never throw on bad registry
|
|
166
|
+
if (!Array.isArray(val))
|
|
167
|
+
continue;
|
|
168
|
+
// FIX 4: if the capId matches an existing cluster key, only override when
|
|
169
|
+
// the values are identical (guaranteed by 4a gate). If they differ, the
|
|
170
|
+
// hand-authored value wins — prefer known-correct disable behavior over
|
|
171
|
+
// a potentially stale registry entry.
|
|
172
|
+
if (Object.prototype.hasOwnProperty.call(baseCm, capId)) {
|
|
173
|
+
const existing = baseCm[capId];
|
|
174
|
+
if (!Array.isArray(existing)) {
|
|
175
|
+
merged[capId] = val;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
// Values differ → hand-authored wins (skip the override)
|
|
179
|
+
if (existing.length !== val.length || existing.some((v, i) => v !== val[i]))
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// Prototype-pollution guard (parity with _capabilitySkillsForMode in install-profiles.cts)
|
|
183
|
+
if (capId === '__proto__' || capId === 'constructor' || capId === 'prototype')
|
|
184
|
+
continue;
|
|
185
|
+
merged[capId] = val;
|
|
186
|
+
}
|
|
187
|
+
cm = merged;
|
|
188
|
+
}
|
|
138
189
|
const skillManifest = normalizeSkillManifest(runtimeConfigDir, manifest);
|
|
139
190
|
const surface = readSurface(runtimeConfigDir);
|
|
140
191
|
// Determine base profile name: from surface state or from .gsd-profile marker
|
|
141
192
|
const baseProfileName = (surface && surface.baseProfile)
|
|
142
193
|
? surface.baseProfile
|
|
143
194
|
: (readActiveProfile(runtimeConfigDir) || 'full');
|
|
144
|
-
// Resolve base profile
|
|
195
|
+
// Resolve base profile — thread registry so capability skills are included.
|
|
145
196
|
const baseResolved = resolveProfile({
|
|
146
197
|
modes: baseProfileName.split(',').map((s) => s.trim()),
|
|
147
198
|
manifest: skillManifest,
|
|
199
|
+
registry,
|
|
148
200
|
});
|
|
149
201
|
// If full, we need to enumerate all skills from the manifest
|
|
150
202
|
let skills;
|
|
@@ -205,12 +257,12 @@ function resolveSurface(runtimeConfigDir, manifest, clusterMap) {
|
|
|
205
257
|
* Re-stage the active surface using the resolved layout.
|
|
206
258
|
* Iterates layout.kinds and syncs each artifact kind to its destination.
|
|
207
259
|
*/
|
|
208
|
-
function applySurface(runtimeConfigDir, layout, manifest, clusterMap) {
|
|
260
|
+
function applySurface(runtimeConfigDir, layout, manifest, clusterMap, registry) {
|
|
209
261
|
if (node_path_1.default.resolve(runtimeConfigDir) !== node_path_1.default.resolve(layout.configDir)) {
|
|
210
262
|
throw new TypeError('applySurface runtimeConfigDir must match layout.configDir');
|
|
211
263
|
}
|
|
212
264
|
const skillManifest = normalizeSkillManifest(layout.configDir, manifest);
|
|
213
|
-
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap);
|
|
265
|
+
const resolved = resolveSurface(layout.configDir, skillManifest, clusterMap, registry);
|
|
214
266
|
// Mirror installRuntimeArtifacts: skills kinds get per-runtime path rewrites
|
|
215
267
|
// so SKILL.md bodies reference the install target (pathPrefix), not the
|
|
216
268
|
// converter's default ~/.claude paths (#813). Computed lazily so command-only
|
|
@@ -409,9 +461,9 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
|
|
|
409
461
|
* Token cost = sum of description lengths ÷ 4 (mirrors audit script).
|
|
410
462
|
* Descriptions are read from the install source (findInstallSourceRoot).
|
|
411
463
|
*/
|
|
412
|
-
function listSurface(runtimeConfigDir, manifest, clusterMap) {
|
|
464
|
+
function listSurface(runtimeConfigDir, manifest, clusterMap, registry) {
|
|
413
465
|
const skillManifest = normalizeSkillManifest(runtimeConfigDir, manifest);
|
|
414
|
-
const resolved = resolveSurface(runtimeConfigDir, skillManifest, clusterMap);
|
|
466
|
+
const resolved = resolveSurface(runtimeConfigDir, skillManifest, clusterMap, registry);
|
|
415
467
|
// All known stems from manifest (exclude _calls_agents_ meta keys)
|
|
416
468
|
const allStems = [];
|
|
417
469
|
for (const [key] of skillManifest) {
|