@opengsd/gsd-core 1.7.0-rc.5 → 1.7.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/agents/gsd-executor.md +2 -1
- package/agents/gsd-security-auditor.md +13 -15
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +1 -0
- package/bin/install.js +975 -196
- package/commands/gsd/mempalace-capture.md +27 -1
- package/commands/gsd/surface.md +6 -6
- package/gsd-core/bin/gsd-tools.cjs +63 -2
- package/gsd-core/bin/lib/api-coverage.cjs +3 -4
- package/gsd-core/bin/lib/audit.cjs +7 -6
- package/gsd-core/bin/lib/capability-registry.cjs +503 -87
- package/gsd-core/bin/lib/capability-validator.cjs +56 -18
- package/gsd-core/bin/lib/check-command-router.cjs +1 -1
- package/gsd-core/bin/lib/clock.cjs +19 -0
- package/gsd-core/bin/lib/commands.cjs +48 -9
- package/gsd-core/bin/lib/config-loader.cjs +6 -2
- package/gsd-core/bin/lib/config.cjs +12 -0
- package/gsd-core/bin/lib/core-utils.cjs +8 -2
- package/gsd-core/bin/lib/drift.cjs +4 -4
- package/gsd-core/bin/lib/frontmatter.cjs +22 -0
- package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
- package/gsd-core/bin/lib/host-integration.cjs +33 -8
- package/gsd-core/bin/lib/init.cjs +60 -53
- package/gsd-core/bin/lib/install-engine.cjs +93 -22
- package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
- package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
- package/gsd-core/bin/lib/markdown-table.cjs +698 -0
- package/gsd-core/bin/lib/mcp-server.cjs +18 -7
- package/gsd-core/bin/lib/milestone.cjs +217 -31
- package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
- package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
- package/gsd-core/bin/lib/phase-locator.cjs +23 -2
- package/gsd-core/bin/lib/phase.cjs +436 -61
- package/gsd-core/bin/lib/plan-scan.cjs +3 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
- package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
- package/gsd-core/bin/lib/roadmap.cjs +100 -49
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
- package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
- package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
- package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
- package/gsd-core/bin/lib/schema-detect.cjs +2 -1
- package/gsd-core/bin/lib/security.cjs +7 -37
- package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
- package/gsd-core/bin/lib/smart-entry.cjs +4 -3
- package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
- package/gsd-core/bin/lib/state-transition.cjs +100 -45
- package/gsd-core/bin/lib/state.cjs +391 -126
- package/gsd-core/bin/lib/surface.cjs +12 -8
- package/gsd-core/bin/lib/template.cjs +2 -1
- package/gsd-core/bin/lib/uat.cjs +54 -8
- package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
- package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
- package/gsd-core/bin/lib/verify.cjs +4 -3
- package/gsd-core/bin/lib/workstream.cjs +3 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
- package/gsd-core/bin/lib/write-set.cjs +38 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -3
- package/gsd-core/references/checkpoints.md +12 -0
- package/gsd-core/references/ui-consideration-probe.md +73 -0
- package/gsd-core/templates/UI-SPEC.md +25 -0
- package/gsd-core/templates/VALIDATION.md +2 -0
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +7 -4
- package/gsd-core/workflows/debug.md +2 -0
- package/gsd-core/workflows/execute-phase.md +5 -3
- package/gsd-core/workflows/fast.md +8 -22
- package/gsd-core/workflows/plan-phase.md +6 -0
- package/gsd-core/workflows/progress.md +2 -2
- package/gsd-core/workflows/quick.md +2 -0
- package/gsd-core/workflows/review.md +42 -3
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +7 -4
- package/gsd-core/workflows/ship.md +8 -2
- package/gsd-core/workflows/spec-phase.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +146 -1
- package/gsd-core/workflows/validate-phase.md +2 -2
- package/hooks/dist/gsd-statusline.js +164 -14
- package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
- package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
- package/hooks/dist/managed-hooks-registry.cjs +2 -0
- package/hooks/gsd-statusline.js +164 -14
- package/hooks/gsd-windsurf-pre-command.js +275 -0
- package/hooks/gsd-windsurf-pre-write.js +132 -0
- package/hooks/managed-hooks-registry.cjs +2 -0
- package/package.json +10 -4
- package/pi/gsd.cjs +354 -0
- package/scripts/build-hooks.js +3 -0
- package/scripts/ci-test-scope.cjs +39 -1
- package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
- package/scripts/gen-install-tree-fixtures.cjs +75 -0
- package/scripts/gen-registry.cjs +128 -0
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
- package/scripts/lint-table-schema-drift.cjs +157 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/registry-schema.cjs +565 -0
- package/scripts/validate-registry.cjs +117 -0
- package/skills/gsd-mempalace-capture/SKILL.md +27 -1
- package/skills/gsd-surface/SKILL.md +6 -6
- package/vscode/browser.js +197 -0
- package/vscode/extension.js +383 -0
- package/vscode/host-binding.js +113 -0
- package/vscode/package.json +96 -0
|
@@ -697,26 +697,35 @@ const VALID_CONVERTER_NAMES = new Set([
|
|
|
697
697
|
'convertClaudeAgentToCodebuddyAgent',
|
|
698
698
|
'convertClaudeAgentToClineAgent',
|
|
699
699
|
'convertClaudeAgentToCodexAgent',
|
|
700
|
+
// ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
|
|
701
|
+
'convertClaudeAgentToQwenAgent',
|
|
700
702
|
]);
|
|
701
703
|
|
|
702
704
|
// C3: Validate role:runtime body
|
|
703
705
|
const VALID_CONFIG_FORMATS = new Set(['settings-json', 'toml', 'markdown', 'markdown-dir', 'none']);
|
|
704
|
-
|
|
706
|
+
// 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) with
|
|
707
|
+
// no file-projected config directory at all.
|
|
708
|
+
const VALID_CONFIG_HOME_KINDS = new Set(['dot-home', 'dot-home-nested', 'xdg', 'generic-agents-root', 'none']);
|
|
705
709
|
const VALID_COMMAND_STYLES = new Set(['slash-hyphen', 'shell-var']);
|
|
706
|
-
const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'none']);
|
|
710
|
+
const VALID_HOOKS_SURFACES = new Set(['settings-json', 'codex-hooks-json', 'cursor-hooks-json', 'copilot-inline', 'cline-rules', 'kimi-hooks-toml', 'windsurf-hooks-json', 'none']);
|
|
707
711
|
const VALID_HOOK_EVENTS = new Set(['claude', 'gemini']);
|
|
708
712
|
// extensionEvents — the plugin/extension-system event dialect (ADR-1239 amendment / #1943).
|
|
709
713
|
// DISTINCT from hookEvents (managed-hook dialect): extensionEvents describes the
|
|
710
714
|
// plugin-owned event subset imperative hosts expose (opencode / pi); 'none' = the
|
|
711
715
|
// host exposes no extension surface (engine owns the bus, e.g. VS Code).
|
|
712
|
-
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'none']);
|
|
716
|
+
const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'none']);
|
|
713
717
|
const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
|
|
714
718
|
const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
|
|
715
719
|
const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
|
|
716
720
|
const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const
|
|
721
|
+
// 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) that
|
|
722
|
+
// are never CLI-installed (no allRuntimes membership, no install flag).
|
|
723
|
+
const VALID_INSTALL_SURFACES = new Set(['settings-json', 'codex-toml', 'copilot-instructions', 'cline-rules', 'cursor-hooks-json', 'profile-marker-only', 'none']);
|
|
724
|
+
// 'antigravity' added #2096 Phase B Upgrade 1 — settings.json permissions.allow writer.
|
|
725
|
+
const VALID_PERMISSION_WRITERS = new Set(['opencode', 'kilo', 'antigravity']);
|
|
726
|
+
// SubagentStart added #2092 Phase B Upgrade 2 (qwen-only today — see
|
|
727
|
+
// capabilities/qwen/capability.json's extendedHookEvents).
|
|
728
|
+
const VALID_EXTENDED_HOOK_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'BeforeAgent', 'AfterAgent', 'BeforeModel', 'SubagentStart']);
|
|
720
729
|
|
|
721
730
|
// ADR-1239 Phase A: hostIntegration axes (MUST stay parity-identical to HOST_INTEGRATION_AXES in src/host-integration.cts)
|
|
722
731
|
const VALID_EMBEDDING_MODES = new Set(['imperative', 'declarative']);
|
|
@@ -736,13 +745,18 @@ const INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES = new Map([
|
|
|
736
745
|
['copilot-instructions', new Set(['copilot-inline'])],
|
|
737
746
|
['cline-rules', new Set(['cline-rules'])],
|
|
738
747
|
['cursor-hooks-json', new Set(['cursor-hooks-json'])],
|
|
739
|
-
['profile-marker-only', new Set(['none'])],
|
|
748
|
+
['profile-marker-only', new Set(['none', 'kimi-hooks-toml', 'windsurf-hooks-json'])],
|
|
749
|
+
// 'none' added #2103 — VS Code has no CLI install surface at all; its only
|
|
750
|
+
// valid hooksSurface pairing is the other 'none' (engine owns the hook bus).
|
|
751
|
+
['none', new Set(['none'])],
|
|
740
752
|
]);
|
|
741
753
|
|
|
742
754
|
// GATE B: extended hook event families → required hookEvents value
|
|
743
755
|
// Gemini agent-events require hookEvents='gemini'; Claude-family events require hookEvents='claude'.
|
|
744
756
|
const GEMINI_AGENT_EVENTS = new Set(['BeforeAgent', 'AfterAgent', 'BeforeModel']);
|
|
745
|
-
|
|
757
|
+
// SubagentStart added #2092 Phase B Upgrade 2 — Claude hook-event dialect
|
|
758
|
+
// counterpart of SubagentStop (qwen-only today).
|
|
759
|
+
const CLAUDE_FAMILY_EVENTS = new Set(['SubagentStop', 'Stop', 'PreCompact', 'FileChanged', 'SubagentStart']);
|
|
746
760
|
|
|
747
761
|
/**
|
|
748
762
|
* Validate a runtime.configHome object per ADR-1016 Decision 1.
|
|
@@ -771,9 +785,18 @@ function validateConfigHome(capId, ch) {
|
|
|
771
785
|
);
|
|
772
786
|
}
|
|
773
787
|
|
|
774
|
-
// name — required string
|
|
775
|
-
|
|
776
|
-
|
|
788
|
+
// name — required string, except when kind === 'none': the runtime has no
|
|
789
|
+
// file-projected config directory at all, so a descriptive name is
|
|
790
|
+
// optional (a carve-out mirroring the dot-home-nested⇒parent conditional
|
|
791
|
+
// below, not a new validation mechanism). If present it must still be a
|
|
792
|
+
// non-empty string (e.g. vscode's configHome.name stays a descriptive
|
|
793
|
+
// "vscode" string even though it is never used to build a path).
|
|
794
|
+
if (ch.kind !== 'none') {
|
|
795
|
+
if (typeof ch.name !== 'string' || ch.name.length === 0) {
|
|
796
|
+
errors.push(ctx + '.name must be a non-empty string');
|
|
797
|
+
}
|
|
798
|
+
} else if (ch.name !== undefined && (typeof ch.name !== 'string' || ch.name.length === 0)) {
|
|
799
|
+
errors.push(ctx + '.name must be a non-empty string if present when kind is "none"');
|
|
777
800
|
}
|
|
778
801
|
|
|
779
802
|
// parent — required when kind == dot-home-nested
|
|
@@ -973,7 +996,7 @@ function validateRuntimeBody(cap) {
|
|
|
973
996
|
);
|
|
974
997
|
}
|
|
975
998
|
|
|
976
|
-
// hooksSurface — closed
|
|
999
|
+
// hooksSurface — closed 7-enum (ADR-1016 Decision 5); inline literal guard (CodeQL barrier)
|
|
977
1000
|
if (r.hooksSurface === '__proto__' || r.hooksSurface === 'constructor' || r.hooksSurface === 'prototype') {
|
|
978
1001
|
errors.push('runtime.hooksSurface "' + r.hooksSurface + '" is a reserved name');
|
|
979
1002
|
} else if (!VALID_HOOKS_SURFACES.has(r.hooksSurface)) {
|
|
@@ -1054,15 +1077,27 @@ function validateRuntimeBody(cap) {
|
|
|
1054
1077
|
// localConfigDir — REQUIRED non-empty dot-dir string (ADR-1239 Phase B #1679)
|
|
1055
1078
|
// Must start with '.' (e.g. ".claude", ".cursor"). Validated here so the registry
|
|
1056
1079
|
// generator catches any descriptor missing the field before regenerating.
|
|
1057
|
-
|
|
1080
|
+
//
|
|
1081
|
+
// #2103: conditional on configHome.kind !== 'none' — a Marketplace/VSIX
|
|
1082
|
+
// host with no file-projected config directory (e.g. VS Code) has no
|
|
1083
|
+
// local dir to name; localConfigDir may be null/absent for such runtimes.
|
|
1084
|
+
const configHomeKind = (r.configHome && typeof r.configHome === 'object') ? r.configHome.kind : undefined;
|
|
1085
|
+
if (configHomeKind !== 'none') {
|
|
1086
|
+
if (typeof r.localConfigDir !== 'string' || r.localConfigDir.length === 0) {
|
|
1087
|
+
errors.push(
|
|
1088
|
+
'runtime.localConfigDir is required and must be a non-empty string (e.g. ".claude"); ' +
|
|
1089
|
+
'got: ' + JSON.stringify(r.localConfigDir),
|
|
1090
|
+
);
|
|
1091
|
+
} else if (!r.localConfigDir.startsWith('.')) {
|
|
1092
|
+
errors.push(
|
|
1093
|
+
'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
} else if (r.localConfigDir !== null && r.localConfigDir !== undefined) {
|
|
1058
1097
|
errors.push(
|
|
1059
|
-
'runtime.localConfigDir
|
|
1098
|
+
'runtime.localConfigDir must be null or absent when configHome.kind is "none"; ' +
|
|
1060
1099
|
'got: ' + JSON.stringify(r.localConfigDir),
|
|
1061
1100
|
);
|
|
1062
|
-
} else if (!r.localConfigDir.startsWith('.')) {
|
|
1063
|
-
errors.push(
|
|
1064
|
-
'runtime.localConfigDir must start with "." (a dot-dir); got: ' + JSON.stringify(r.localConfigDir),
|
|
1065
|
-
);
|
|
1066
1101
|
}
|
|
1067
1102
|
|
|
1068
1103
|
// extendedHookEvents — required array; every element must be in closed enum
|
|
@@ -2168,6 +2203,9 @@ const INSTALL_SURFACE_TO_CONFIG_FORMAT = new Map([
|
|
|
2168
2203
|
['cline-rules', 'markdown-dir'],
|
|
2169
2204
|
['cursor-hooks-json', 'none'],
|
|
2170
2205
|
['profile-marker-only', 'none'],
|
|
2206
|
+
// 'none' added #2103 — a runtime with NO CLI install surface at all (e.g.
|
|
2207
|
+
// VS Code) has no config-file format to write either.
|
|
2208
|
+
['none', 'none'],
|
|
2171
2209
|
]);
|
|
2172
2210
|
|
|
2173
2211
|
/**
|
|
@@ -919,7 +919,7 @@ function cmdApiCoverageVerifyPre(projectDir, args, raw) {
|
|
|
919
919
|
// .planning/phases/ (or a milestone archive). The raw arg is never used as a
|
|
920
920
|
// path, so `..`, absolute paths, and arbitrary directories cannot reach a
|
|
921
921
|
// file read. Mirrors cmdVerifySchemaDrift's token-match approach.
|
|
922
|
-
let token =
|
|
922
|
+
let token = (0, shell_command_projection_cjs_1.posixNormalize)(phaseArg).split('/').filter(Boolean).pop() || '';
|
|
923
923
|
// A token like ".." or "." carries no phase identity → unresolvable.
|
|
924
924
|
if (token === '.' || token === '..')
|
|
925
925
|
token = '';
|
|
@@ -81,6 +81,25 @@ exports.realClock = {
|
|
|
81
81
|
today() {
|
|
82
82
|
return this.nowIso().split('T')[0];
|
|
83
83
|
},
|
|
84
|
+
/**
|
|
85
|
+
* Return today's date as a YYYY-MM-DD string in the HOST-LOCAL calendar day.
|
|
86
|
+
* Uses this.now() so the subprocess time-pin adapter (GSD_NOW_MS) is honoured
|
|
87
|
+
* exactly as today()/nowIso() are — deterministic when GSD_NOW_MS and TZ are
|
|
88
|
+
* both pinned (#2136).
|
|
89
|
+
*
|
|
90
|
+
* Operator-facing date-only fields (last_activity, "completed <date>", etc.)
|
|
91
|
+
* must use the local calendar day: an operator reads them as "the day I did
|
|
92
|
+
* this", and they must never name a day ahead of `last_updated`'s local date.
|
|
93
|
+
* `today()` (UTC) stays the source for internal/cosmetic stamps.
|
|
94
|
+
*
|
|
95
|
+
* @returns e.g. "2020-06-14" (local), which may differ from today() near UTC midnight
|
|
96
|
+
*/
|
|
97
|
+
localToday() {
|
|
98
|
+
const d = new Date(this.now());
|
|
99
|
+
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
100
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
101
|
+
return d.getFullYear() + '-' + mm + '-' + dd;
|
|
102
|
+
},
|
|
84
103
|
/**
|
|
85
104
|
* Synchronous sleep via Atomics.wait.
|
|
86
105
|
* This is the identical primitive acquireStateLock and withPlanningLock used
|
|
@@ -45,6 +45,7 @@ const { extractFrontmatter } = frontmatter;
|
|
|
45
45
|
const modelProfiles = require("./model-profiles.cjs");
|
|
46
46
|
const { MODEL_PROFILES, VALID_PHASE_TYPES } = modelProfiles;
|
|
47
47
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
48
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
48
49
|
// ─── Phase Status ─────────────────────────────────────────────────────────────
|
|
49
50
|
/**
|
|
50
51
|
* Determine phase status by checking plan/summary counts AND verification state.
|
|
@@ -592,6 +593,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
592
593
|
// Stage files
|
|
593
594
|
const explicitFiles = files && files.length > 0;
|
|
594
595
|
const filesToStage = explicitFiles ? files : ['.planning/'];
|
|
596
|
+
const stagedPaths = [];
|
|
595
597
|
for (const file of filesToStage) {
|
|
596
598
|
const fullPath = node_path_1.default.join(cwd, file);
|
|
597
599
|
if (!node_fs_1.default.existsSync(fullPath)) {
|
|
@@ -607,12 +609,32 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
|
|
607
609
|
}
|
|
608
610
|
else {
|
|
609
611
|
(0, shell_command_projection_cjs_1.execGit)(['add', file], { cwd });
|
|
612
|
+
stagedPaths.push(file);
|
|
610
613
|
}
|
|
611
614
|
}
|
|
612
|
-
// Commit
|
|
613
|
-
|
|
615
|
+
// Commit — when the caller declared a scope (--files), append a pathspec so
|
|
616
|
+
// only the declared files land in the commit, not the entire index (#2112).
|
|
617
|
+
// The pathspec uses stagedPaths (not filesToStage) so skipped missing files
|
|
618
|
+
// are excluded — otherwise git would record them as deletions (#2014).
|
|
619
|
+
// During a merge, git refuses partial commits — fall back to a bare commit.
|
|
620
|
+
// --amend is left without a pathspec: amending with -- <paths> is a different
|
|
621
|
+
// operation that rewrites the tip with only those paths.
|
|
622
|
+
if (explicitFiles && stagedPaths.length === 0 && !amend) {
|
|
623
|
+
const result = { committed: false, hash: null, reason: 'nothing_to_commit' };
|
|
624
|
+
output(result, raw, 'nothing');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const isMergeInProgress = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd }).exitCode === 0;
|
|
628
|
+
const canScope = explicitFiles && stagedPaths.length > 0 && !amend
|
|
629
|
+
&& !isMergeInProgress;
|
|
630
|
+
const commitArgs = amend
|
|
631
|
+
? ['commit', '--amend', '--no-edit']
|
|
632
|
+
: ['commit', '-m', sanitizedMessage];
|
|
614
633
|
if (noVerify)
|
|
615
634
|
commitArgs.push('--no-verify');
|
|
635
|
+
if (canScope) {
|
|
636
|
+
commitArgs.push('--', ...stagedPaths);
|
|
637
|
+
}
|
|
616
638
|
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd });
|
|
617
639
|
if (commitResult.exitCode !== 0) {
|
|
618
640
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
@@ -710,12 +732,21 @@ function cmdCommitToSubrepo(cwd, message, files, raw) {
|
|
|
710
732
|
for (const [repo, repoFiles] of Object.entries(grouped)) {
|
|
711
733
|
const repoCwd = node_path_1.default.join(cwd, repo);
|
|
712
734
|
// Stage files (strip sub-repo prefix for paths relative to that repo)
|
|
735
|
+
const stagedRelPaths = [];
|
|
713
736
|
for (const file of repoFiles) {
|
|
714
737
|
const relativePath = file.slice(repo.length + 1);
|
|
715
|
-
(0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
738
|
+
const addResult = (0, shell_command_projection_cjs_1.execGit)(['add', relativePath], { cwd: repoCwd });
|
|
739
|
+
if (addResult.exitCode === 0) {
|
|
740
|
+
stagedRelPaths.push(relativePath);
|
|
741
|
+
}
|
|
716
742
|
}
|
|
717
|
-
// Commit
|
|
718
|
-
const
|
|
743
|
+
// Commit — pathspec limits the commit to the staged files only (#2112)
|
|
744
|
+
const isMergeInProgressSub = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
|
|
745
|
+
const canScopeSub = stagedRelPaths.length > 0 && !isMergeInProgressSub;
|
|
746
|
+
const commitArgs = canScopeSub
|
|
747
|
+
? ['commit', '-m', message, '--', ...stagedRelPaths]
|
|
748
|
+
: ['commit', '-m', message];
|
|
749
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
719
750
|
if (commitResult.exitCode !== 0) {
|
|
720
751
|
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
|
721
752
|
repos[repo] = { committed: false, hash: null, files: repoFiles, reason: 'nothing_to_commit' };
|
|
@@ -837,8 +868,16 @@ function cmdPrSubrepo(cwd, repo, branch, commitMessage, raw) {
|
|
|
837
868
|
error(`Failed to stage ${file} in ${repo}: ${addResult.stderr}`);
|
|
838
869
|
}
|
|
839
870
|
}
|
|
840
|
-
// 5. Commit
|
|
841
|
-
|
|
871
|
+
// 5. Commit — pathspec limits the commit to the staged files only (#2112).
|
|
872
|
+
// changedFiles includes both old and new paths for renames so the full
|
|
873
|
+
// rename is captured atomically (pathspec on newPath alone would leave the
|
|
874
|
+
// deletion of oldPath stranded in the index).
|
|
875
|
+
const isMergeInProgressPr = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd: repoCwd }).exitCode === 0;
|
|
876
|
+
const canScopePr = changedFiles.length > 0 && !isMergeInProgressPr;
|
|
877
|
+
const commitArgs = canScopePr
|
|
878
|
+
? ['commit', '-m', commitMessage, '--', ...changedFiles]
|
|
879
|
+
: ['commit', '-m', commitMessage];
|
|
880
|
+
const commitResult = (0, shell_command_projection_cjs_1.execGit)(commitArgs, { cwd: repoCwd });
|
|
842
881
|
if (commitResult.exitCode !== 0) {
|
|
843
882
|
rollback();
|
|
844
883
|
error(`Failed to commit in ${repo}: ${commitResult.stderr}`);
|
|
@@ -1223,7 +1262,7 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
1223
1262
|
(0, shell_command_projection_cjs_1.platformEnsureDir)(completedDir);
|
|
1224
1263
|
// Read, add completion timestamp, move
|
|
1225
1264
|
let content = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
1226
|
-
const today =
|
|
1265
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
1227
1266
|
content = `completed: ${today}\n` + content;
|
|
1228
1267
|
(0, shell_command_projection_cjs_1.platformWriteSync)(node_path_1.default.join(completedDir, filename), content);
|
|
1229
1268
|
node_fs_1.default.unlinkSync(sourcePath);
|
|
@@ -1232,7 +1271,7 @@ function cmdTodoComplete(cwd, filename, raw) {
|
|
|
1232
1271
|
function cmdScaffold(cwd, type, options, raw) {
|
|
1233
1272
|
const { phase, name } = options;
|
|
1234
1273
|
const padded = phase ? normalizePhaseName(phase) : '00';
|
|
1235
|
-
const today =
|
|
1274
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
1236
1275
|
// Find phase directory
|
|
1237
1276
|
const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
|
|
1238
1277
|
const phaseDir = phaseInfo ? node_path_1.default.join(cwd, phaseInfo['directory']) : null;
|
|
@@ -158,11 +158,15 @@ const _warnedUnknownConfigKeys = new Set();
|
|
|
158
158
|
// ─── Git utilities ────────────────────────────────────────────────────────────
|
|
159
159
|
const _gitIgnoredCache = new Map();
|
|
160
160
|
function isGitIgnored(cwd, targetPath) {
|
|
161
|
-
|
|
161
|
+
// #2206: strip trailing slashes — `git check-ignore` has a quirk where a
|
|
162
|
+
// CRLF .gitignore with blank lines falsely reports a trailing-slash path
|
|
163
|
+
// (e.g. `.planning/`) as ignored. Normalizing here protects every call site.
|
|
164
|
+
const normalized = targetPath.replace(/\/+$/, '');
|
|
165
|
+
const key = cwd + '::' + normalized;
|
|
162
166
|
if (_gitIgnoredCache.has(key))
|
|
163
167
|
return _gitIgnoredCache.get(key);
|
|
164
168
|
// --no-index checks .gitignore rules regardless of whether the file is tracked.
|
|
165
|
-
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--',
|
|
169
|
+
const result = (0, shell_command_projection_cjs_1.execGit)(['check-ignore', '-q', '--no-index', '--', normalized], { cwd });
|
|
166
170
|
const ignored = result.exitCode === 0;
|
|
167
171
|
_gitIgnoredCache.set(key, ignored);
|
|
168
172
|
return ignored;
|
|
@@ -692,6 +692,18 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
|
|
692
692
|
const VALID_CONTEXT_POSITIONS = ['front', 'end'];
|
|
693
693
|
if (kp === 'statusline.context_position')
|
|
694
694
|
assertEnumValue(parsedValue, val, VALID_CONTEXT_POSITIONS, 'statusline.context_position');
|
|
695
|
+
// statusline.show_context_tokens — boolean only
|
|
696
|
+
if (kp === 'statusline.show_context_tokens') {
|
|
697
|
+
if (typeof parsedValue !== 'boolean') {
|
|
698
|
+
error(`Invalid statusline.show_context_tokens '${val}'. Must be a boolean (true or false).`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
// statusline.show_git — boolean only
|
|
702
|
+
if (kp === 'statusline.show_git') {
|
|
703
|
+
if (typeof parsedValue !== 'boolean') {
|
|
704
|
+
error(`Invalid statusline.show_git '${val}'. Must be a boolean (true or false).`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
695
707
|
// Fallow scope + profile enum validation (#3424)
|
|
696
708
|
const VALID_FALLOW_SCOPES = ['phase', 'repo'];
|
|
697
709
|
if (kp === 'code_quality.fallow.scope')
|
|
@@ -27,10 +27,16 @@ const { comparePhaseNum } = phaseIdModule;
|
|
|
27
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
28
|
const planningWorkspace = require("./planning-workspace.cjs");
|
|
29
29
|
const { findContextMdIn } = planningWorkspace;
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
31
|
+
const shellCommandProjection = require("./shell-command-projection.cjs");
|
|
30
32
|
// ─── Path helpers ────────────────────────────────────────────────────────────
|
|
31
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a relative path to always use forward slashes (cross-platform).
|
|
35
|
+
* Delegates to the single separator seam in shell-command-projection so there is
|
|
36
|
+
* exactly one implementation of native→POSIX conversion across the codebase.
|
|
37
|
+
*/
|
|
32
38
|
function toPosixPath(p) {
|
|
33
|
-
return
|
|
39
|
+
return shellCommandProjection.toPosixPath(p);
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Scan immediate child directories for separate git repos.
|
|
@@ -69,7 +69,7 @@ const SAFE_PATH_RE = /^(?!.*\.\.)(?:[A-Za-z0-9_.][A-Za-z0-9_.\-]*)(?:\/[A-Za-z0-
|
|
|
69
69
|
function classifyFile(file) {
|
|
70
70
|
if (typeof file !== 'string' || !file)
|
|
71
71
|
return null;
|
|
72
|
-
const norm =
|
|
72
|
+
const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
|
|
73
73
|
if (MIGRATION_RES.some((r) => r.test(norm)))
|
|
74
74
|
return 'migration';
|
|
75
75
|
if (ROUTE_RES.some((r) => r.test(norm)))
|
|
@@ -87,7 +87,7 @@ function classifyFile(file) {
|
|
|
87
87
|
* check `structureMd.includes('src/lib')` holds.
|
|
88
88
|
*/
|
|
89
89
|
function isPathMapped(file, structureMd) {
|
|
90
|
-
const norm =
|
|
90
|
+
const norm = (0, shell_command_projection_cjs_1.posixNormalize)(file);
|
|
91
91
|
const parts = norm.split('/');
|
|
92
92
|
// Check prefixes from longest to shortest; any hit means "mapped".
|
|
93
93
|
for (let i = parts.length - 1; i >= 1; i--) {
|
|
@@ -130,7 +130,7 @@ function detectDrift(input) {
|
|
|
130
130
|
const elements = [];
|
|
131
131
|
const seen = new Map();
|
|
132
132
|
for (const rawFile of added) {
|
|
133
|
-
const file =
|
|
133
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(rawFile);
|
|
134
134
|
const specific = classifyFile(file);
|
|
135
135
|
let category = specific;
|
|
136
136
|
if (!category) {
|
|
@@ -251,7 +251,7 @@ function chooseAffectedPaths(paths) {
|
|
|
251
251
|
for (const raw of paths || []) {
|
|
252
252
|
if (typeof raw !== 'string' || !raw)
|
|
253
253
|
continue;
|
|
254
|
-
const file =
|
|
254
|
+
const file = (0, shell_command_projection_cjs_1.posixNormalize)(raw);
|
|
255
255
|
const parts = file.split('/');
|
|
256
256
|
if (parts.length === 0)
|
|
257
257
|
continue;
|
|
@@ -522,6 +522,27 @@ const FRONTMATTER_SCHEMAS = {
|
|
|
522
522
|
summary: { required: ['phase', 'plan', 'subsystem', 'tags', 'duration', 'completed'] },
|
|
523
523
|
verification: { required: ['phase', 'verified', 'status', 'score'] },
|
|
524
524
|
};
|
|
525
|
+
/**
|
|
526
|
+
* Strip ALL frontmatter blocks from the start of `content`.
|
|
527
|
+
*
|
|
528
|
+
* Handles CRLF line endings and multiple stacked blocks (corruption
|
|
529
|
+
* recovery): greedily strips consecutive `---...---` blocks separated by
|
|
530
|
+
* optional whitespace, so a doubled/tripled frontmatter header (e.g. from a
|
|
531
|
+
* botched merge) is fully removed, not just the first block.
|
|
532
|
+
*
|
|
533
|
+
* Canonical home for this primitive (#2143 audit dedup): previously
|
|
534
|
+
* duplicated byte-identically in both `state.cts` and `state-transition.cts`.
|
|
535
|
+
*/
|
|
536
|
+
function stripFrontmatter(content) {
|
|
537
|
+
let result = content;
|
|
538
|
+
while (true) {
|
|
539
|
+
const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, '');
|
|
540
|
+
if (stripped === result)
|
|
541
|
+
break;
|
|
542
|
+
result = stripped;
|
|
543
|
+
}
|
|
544
|
+
return result;
|
|
545
|
+
}
|
|
525
546
|
function cmdFrontmatterGet(cwd, filePath, field, raw) {
|
|
526
547
|
if (!filePath) {
|
|
527
548
|
error('file path required');
|
|
@@ -654,6 +675,7 @@ module.exports = {
|
|
|
654
675
|
parseFrontmatter: extractFrontmatter,
|
|
655
676
|
reconstructFrontmatter,
|
|
656
677
|
spliceFrontmatter,
|
|
678
|
+
stripFrontmatter,
|
|
657
679
|
noOpObjectListSetError,
|
|
658
680
|
parseMustHavesBlock,
|
|
659
681
|
FRONTMATTER_SCHEMAS,
|
|
@@ -26,6 +26,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
26
26
|
const node_path_1 = __importDefault(require("node:path"));
|
|
27
27
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
28
28
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
29
|
+
const clock_cjs_1 = require("./clock.cjs");
|
|
29
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
31
|
const ioMod = require("./io.cjs");
|
|
31
32
|
const { output } = ioMod;
|
|
@@ -276,7 +277,7 @@ function buildStateMd(phaseMap) {
|
|
|
276
277
|
const status = currentEntry ? 'Ready to plan' : 'All phases complete';
|
|
277
278
|
const filled = Math.round(pct / 10);
|
|
278
279
|
const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
|
|
279
|
-
const today =
|
|
280
|
+
const today = clock_cjs_1.realClock.localToday();
|
|
280
281
|
return [
|
|
281
282
|
'# Project State',
|
|
282
283
|
'',
|
|
@@ -430,14 +430,25 @@ function hookEventSurfaceFor(hookEvents) {
|
|
|
430
430
|
// events (session/tool/file/permission); pi ~30 fine-grained extension events;
|
|
431
431
|
// 'none' = the host exposes no extension surface and the engine owns the bus
|
|
432
432
|
// (VS Code). Declarative hosts (no plugin API) do not set `extensionEvents`.
|
|
433
|
+
// OpenCode's plugin event surface (ADR-1239 §research; ~25 documented events,
|
|
434
|
+
// GSD binds this subset). Hoisted to a named const — rather than duplicated
|
|
435
|
+
// object literals — so the `kilo` dialect below (#2093) can reuse the IDENTICAL
|
|
436
|
+
// array instead of a copy-pasted one that could silently drift out of sync.
|
|
437
|
+
const OPENCODE_EXTENSION_EVENTS = Object.freeze([
|
|
438
|
+
'session.created', 'session.idle', 'experimental.session.compacting',
|
|
439
|
+
'tool.execute.before', 'tool.execute.after', 'file.edited',
|
|
440
|
+
// #2087 — additional documented plugin events GSD binds (opencode.ai/docs/plugins):
|
|
441
|
+
// permission decisions + session error surface.
|
|
442
|
+
'permission.asked', 'permission.replied', 'session.error',
|
|
443
|
+
]);
|
|
433
444
|
const EXTENSION_EVENT_SURFACES = Object.freeze({
|
|
434
|
-
opencode:
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
445
|
+
opencode: OPENCODE_EXTENSION_EVENTS,
|
|
446
|
+
// #2093 — Kilo Code is an OpenCode fork sharing the same plugin/extension
|
|
447
|
+
// event bus (host hook bus, UPGRADE 1): reuses OPENCODE_EXTENSION_EVENTS
|
|
448
|
+
// verbatim (not a re-derivation), so the two dialects stay pinned together
|
|
449
|
+
// by construction. See .kilo/plugins/gsd-core.js (copied verbatim from
|
|
450
|
+
// .opencode/plugins/gsd-core.js).
|
|
451
|
+
kilo: OPENCODE_EXTENSION_EVENTS,
|
|
441
452
|
// #2091 — Hermes Agent real plugin hook vocabulary (13 events).
|
|
442
453
|
// Cite: https://github.com/nousresearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
|
|
443
454
|
// Replaces the borrowed `hookEvents: "claude"` 6-event surface that silently
|
|
@@ -451,7 +462,21 @@ const EXTENSION_EVENT_SURFACES = Object.freeze({
|
|
|
451
462
|
'pre_gateway_dispatch', 'pre_approval_request',
|
|
452
463
|
'transform_tool_result',
|
|
453
464
|
]),
|
|
454
|
-
pi
|
|
465
|
+
// #2102 Stage 2 — pi's real ExtensionAPI event vocabulary (~30 fine-grained
|
|
466
|
+
// extension events; documentation-sourced, ADR-1239 §research). Replaces the
|
|
467
|
+
// placeholder single-event ['tool_call'] surface — the Stage 1 value only
|
|
468
|
+
// covered the one event pi/gsd.cjs happened to bind at the time, not the
|
|
469
|
+
// full declared surface.
|
|
470
|
+
pi: Object.freeze([
|
|
471
|
+
'session_start', 'project_trust', 'resources_discover', 'input',
|
|
472
|
+
'before_agent_start', 'agent_start', 'message_start', 'message_update',
|
|
473
|
+
'message_end', 'turn_start', 'context', 'before_provider_request',
|
|
474
|
+
'after_provider_response', 'tool_execution_start', 'tool_execution_update',
|
|
475
|
+
'tool_execution_end', 'tool_call', 'tool_result', 'turn_end', 'agent_end',
|
|
476
|
+
'session_before_switch', 'session_shutdown', 'session_before_fork',
|
|
477
|
+
'session_info_changed', 'session_before_compact', 'session_compact',
|
|
478
|
+
'session_before_tree', 'session_tree', 'thinking_level_select', 'model_select',
|
|
479
|
+
]),
|
|
455
480
|
none: Object.freeze([]),
|
|
456
481
|
});
|
|
457
482
|
/**
|