@opengsd/gsd-core 1.6.0-rc.1 → 1.6.0-rc.3
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 +2 -1
- package/agents/gsd-planner.md +8 -57
- package/agents/gsd-roadmapper.md +6 -0
- package/agents/gsd-security-auditor.md +37 -18
- package/bin/install.js +243 -345
- package/commands/gsd/capture.md +5 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +53 -6
- package/gsd-core/bin/lib/adr-parser.cjs +21 -6
- package/gsd-core/bin/lib/audit-command-router.cjs +52 -14
- package/gsd-core/bin/lib/capability-lifecycle.cjs +30 -7
- package/gsd-core/bin/lib/capability-registry.cjs +96 -83
- package/gsd-core/bin/lib/capability-validator.cjs +22 -0
- package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +40 -2
- package/gsd-core/bin/lib/command-routing-hub.cjs +10 -3
- package/gsd-core/bin/lib/commands.cjs +247 -0
- package/gsd-core/bin/lib/config-loader.cjs +6 -0
- package/gsd-core/bin/lib/config-schema.cjs +1 -0
- package/gsd-core/bin/lib/config.cjs +70 -21
- package/gsd-core/bin/lib/coverage.cjs +464 -0
- package/gsd-core/bin/lib/frontmatter.cjs +7 -3
- package/gsd-core/bin/lib/graphify-command-router.cjs +53 -36
- package/gsd-core/bin/lib/init.cjs +8 -4
- package/gsd-core/bin/lib/install-profiles.cjs +6 -3
- package/gsd-core/bin/lib/intel-command-router.cjs +79 -60
- package/gsd-core/bin/lib/phase-id.cjs +25 -11
- package/gsd-core/bin/lib/phase.cjs +4 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +157 -13
- package/gsd-core/bin/lib/probe-core.cjs +7 -0
- package/gsd-core/bin/lib/profile-output.cjs +18 -6
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +59 -26
- package/gsd-core/bin/lib/roadmap-command-router.cjs +16 -3
- package/gsd-core/bin/lib/roadmap-parser.cjs +29 -8
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +47 -17
- package/gsd-core/bin/lib/roadmap.cjs +5 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +473 -16
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +77 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -31
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +15 -0
- package/gsd-core/bin/lib/runtime-name-policy.cjs +89 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +55 -1
- package/gsd-core/bin/lib/state.cjs +364 -52
- package/gsd-core/bin/lib/surface.cjs +47 -22
- package/gsd-core/bin/lib/update-context.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +5 -2
- package/gsd-core/bin/lib/verify.cjs +11 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +202 -0
- package/gsd-core/bin/shared/config-defaults.manifest.json +2 -1
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/context-budget.md +8 -8
- package/gsd-core/references/execute-phase-context-guard.md +16 -0
- package/gsd-core/references/planner-guidance.md +66 -0
- package/gsd-core/references/planning-config.md +3 -2
- package/gsd-core/references/prohibition-probe.md +15 -9
- package/gsd-core/references/security-asvs-levels.md +27 -0
- package/gsd-core/templates/SECURITY.md +6 -4
- package/gsd-core/templates/summary-complex.md +4 -0
- package/gsd-core/templates/summary-minimal.md +3 -0
- package/gsd-core/templates/summary-standard.md +4 -0
- package/gsd-core/templates/summary.md +41 -0
- package/gsd-core/workflows/autonomous.md +33 -33
- package/gsd-core/workflows/diagnose-issues.md +6 -1
- package/gsd-core/workflows/execute-phase.md +8 -6
- package/gsd-core/workflows/execute-plan.md +5 -0
- package/gsd-core/workflows/help/modes/full.md +10 -0
- package/gsd-core/workflows/list-seeds.md +63 -0
- package/gsd-core/workflows/manager.md +37 -37
- package/gsd-core/workflows/new-project.md +4 -4
- package/gsd-core/workflows/plan-phase.md +15 -0
- package/gsd-core/workflows/pr-branch.md +156 -0
- package/gsd-core/workflows/quick.md +6 -1
- package/gsd-core/workflows/review.md +10 -2
- package/gsd-core/workflows/secure-phase.md +13 -7
- package/gsd-core/workflows/spec-phase.md +8 -3
- package/gsd-core/workflows/verify-phase.md +2 -2
- package/gsd-core/workflows/verify-work.md +30 -1
- package/package.json +7 -2
- package/scripts/gen-plugin-skills.cjs +117 -0
- package/scripts/lint-test-file-count.allowlist.json +2 -1
- package/scripts/prompt-injection-scan.sh +5 -0
- package/scripts/release-notes/conventional-title.cjs +88 -0
- package/scripts/release-notes/format-github-release-notes.cjs +4 -3
- package/skills/gsd-add-tests/SKILL.md +38 -0
- package/skills/gsd-ai-integration-phase/SKILL.md +37 -0
- package/skills/gsd-audit-fix/SKILL.md +33 -0
- package/skills/gsd-audit-milestone/SKILL.md +37 -0
- package/skills/gsd-audit-uat/SKILL.md +25 -0
- package/skills/gsd-autonomous/SKILL.md +51 -0
- package/skills/gsd-capture/SKILL.md +67 -0
- package/skills/gsd-cleanup/SKILL.md +24 -0
- package/skills/gsd-code-review/SKILL.md +59 -0
- package/skills/gsd-complete-milestone/SKILL.md +142 -0
- package/skills/gsd-config/SKILL.md +56 -0
- package/skills/gsd-debug/SKILL.md +53 -0
- package/skills/gsd-discuss-phase/SKILL.md +77 -0
- package/skills/gsd-docs-update/SKILL.md +49 -0
- package/skills/gsd-eval-review/SKILL.md +33 -0
- package/skills/gsd-execute-phase/SKILL.md +65 -0
- package/skills/gsd-explore/SKILL.md +28 -0
- package/skills/gsd-extract-learnings/SKILL.md +22 -0
- package/skills/gsd-fast/SKILL.md +31 -0
- package/skills/gsd-forensics/SKILL.md +56 -0
- package/skills/gsd-graphify/SKILL.md +204 -0
- package/skills/gsd-health/SKILL.md +31 -0
- package/skills/gsd-help/SKILL.md +29 -0
- package/skills/gsd-import/SKILL.md +46 -0
- package/skills/gsd-inbox/SKILL.md +39 -0
- package/skills/gsd-ingest-docs/SKILL.md +43 -0
- package/skills/gsd-manager/SKILL.md +45 -0
- package/skills/gsd-map-codebase/SKILL.md +83 -0
- package/skills/gsd-mempalace-capture/SKILL.md +71 -0
- package/skills/gsd-mempalace-recall/SKILL.md +102 -0
- package/skills/gsd-milestone-summary/SKILL.md +51 -0
- package/skills/gsd-mvp-phase/SKILL.md +45 -0
- package/skills/gsd-new-milestone/SKILL.md +45 -0
- package/skills/gsd-new-project/SKILL.md +47 -0
- package/skills/gsd-ns-context/SKILL.md +24 -0
- package/skills/gsd-ns-ideate/SKILL.md +23 -0
- package/skills/gsd-ns-manage/SKILL.md +35 -0
- package/skills/gsd-ns-project/SKILL.md +26 -0
- package/skills/gsd-ns-review/SKILL.md +28 -0
- package/skills/gsd-ns-workflow/SKILL.md +33 -0
- package/skills/gsd-pause-work/SKILL.md +43 -0
- package/skills/gsd-phase/SKILL.md +57 -0
- package/skills/gsd-plan-phase/SKILL.md +63 -0
- package/skills/gsd-plan-review-convergence/SKILL.md +60 -0
- package/skills/gsd-pr-branch/SKILL.md +26 -0
- package/skills/gsd-profile-user/SKILL.md +47 -0
- package/skills/gsd-progress/SKILL.md +49 -0
- package/skills/gsd-quick/SKILL.md +174 -0
- package/skills/gsd-resume-work/SKILL.md +31 -0
- package/skills/gsd-review/SKILL.md +42 -0
- package/skills/gsd-review-backlog/SKILL.md +63 -0
- package/skills/gsd-secure-phase/SKILL.md +36 -0
- package/skills/gsd-settings/SKILL.md +29 -0
- package/skills/gsd-ship/SKILL.md +24 -0
- package/skills/gsd-sketch/SKILL.md +60 -0
- package/skills/gsd-spec-phase/SKILL.md +63 -0
- package/skills/gsd-spike/SKILL.md +57 -0
- package/skills/gsd-stats/SKILL.md +20 -0
- package/skills/gsd-surface/SKILL.md +162 -0
- package/skills/gsd-thread/SKILL.md +24 -0
- package/skills/gsd-ui-phase/SKILL.md +35 -0
- package/skills/gsd-ui-review/SKILL.md +33 -0
- package/skills/gsd-ultraplan-phase/SKILL.md +34 -0
- package/skills/gsd-undo/SKILL.md +35 -0
- package/skills/gsd-update/SKILL.md +50 -0
- package/skills/gsd-validate-phase/SKILL.md +36 -0
- package/skills/gsd-verify-work/SKILL.md +39 -0
- package/skills/gsd-workspace/SKILL.md +53 -0
- package/skills/gsd-workstreams/SKILL.md +70 -0
|
@@ -43,7 +43,14 @@ const io = require("./io.cjs");
|
|
|
43
43
|
const coreUtils = require("./core-utils.cjs");
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
45
|
const path = require("path");
|
|
46
|
+
// Phase 2 (#1646): route through the Hub per ADR-959 §III(B) line 75.
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
48
|
+
const commandRoutingHub = require("./command-routing-hub.cjs");
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
const cjsCommandRouterAdapter = require("./cjs-command-router-adapter.cjs");
|
|
46
51
|
const { ERROR_REASON } = io;
|
|
52
|
+
const { makeInvalidArgs } = commandRoutingHub;
|
|
53
|
+
const { routeHubCommandFamily } = cjsCommandRouterAdapter;
|
|
47
54
|
// Default CoreModule implementation assembled from leaf modules.
|
|
48
55
|
// _core seam overrides this entirely for test injection.
|
|
49
56
|
const _defaultCore = { output: io.output, timeAgo: coreUtils.timeAgo };
|
|
@@ -52,67 +59,79 @@ function routeIntelCommand({ args, cwd, raw, error, _intel, _core }) {
|
|
|
52
59
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment
|
|
53
60
|
const intel = _intel ?? require('./intel.cjs');
|
|
54
61
|
const c = _core ?? _defaultCore;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
62
|
+
// Phase 2 (#1646): routes through the Command Routing Hub per ADR-959 §III(B)
|
|
63
|
+
// line 75. Validation handlers return `makeInvalidArgs(...)` Results; the
|
|
64
|
+
// Hub → adapter translation preserves ERROR_REASON granularity via the
|
|
65
|
+
// exitReason field (Phase 1, #1644). Success handlers keep direct `c.output()`
|
|
66
|
+
// calls. The timeAgo mutation in non-raw `status` is preserved. Lazy require
|
|
67
|
+
// of intel.cjs inside the function is preserved (loads only when dispatched).
|
|
68
|
+
routeHubCommandFamily({
|
|
69
|
+
family: 'intel',
|
|
70
|
+
args,
|
|
71
|
+
// Alphabetical for stable unknownMessage text; the integration test asserts
|
|
72
|
+
// inclusion of all 9 subcommands, not order.
|
|
73
|
+
subcommands: ['api-surface', 'diff', 'extract-exports', 'patch-meta', 'query', 'snapshot', 'status', 'update', 'validate'],
|
|
74
|
+
handlers: {
|
|
75
|
+
query: () => {
|
|
76
|
+
const term = args[2];
|
|
77
|
+
if (!term) {
|
|
78
|
+
return makeInvalidArgs('term', 'Usage: gsd-tools intel query <term>', ERROR_REASON.USAGE);
|
|
79
|
+
}
|
|
80
|
+
const planningDir = path.join(cwd, '.planning');
|
|
81
|
+
c.output(intel.intelQuery(term, planningDir), raw);
|
|
82
|
+
},
|
|
83
|
+
status: () => {
|
|
84
|
+
const planningDir = path.join(cwd, '.planning');
|
|
85
|
+
const status = intel.intelStatus(planningDir);
|
|
86
|
+
if (!raw && status.files) {
|
|
87
|
+
for (const file of Object.values(status.files)) {
|
|
88
|
+
if (file.updated_at) {
|
|
89
|
+
file.updated_at = c.timeAgo(new Date(file.updated_at));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
c.output(status, raw);
|
|
94
|
+
},
|
|
95
|
+
diff: () => {
|
|
96
|
+
const planningDir = path.join(cwd, '.planning');
|
|
97
|
+
c.output(intel.intelDiff(planningDir), raw);
|
|
98
|
+
},
|
|
99
|
+
snapshot: () => {
|
|
100
|
+
const planningDir = path.join(cwd, '.planning');
|
|
101
|
+
c.output(intel.intelSnapshot(planningDir), raw);
|
|
102
|
+
},
|
|
103
|
+
'patch-meta': () => {
|
|
104
|
+
const filePath = args[2];
|
|
105
|
+
if (!filePath) {
|
|
106
|
+
return makeInvalidArgs('file-path', 'Usage: gsd-tools intel patch-meta <file-path>', ERROR_REASON.USAGE);
|
|
107
|
+
}
|
|
108
|
+
c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
|
|
109
|
+
},
|
|
110
|
+
validate: () => {
|
|
111
|
+
const planningDir = path.join(cwd, '.planning');
|
|
112
|
+
c.output(intel.intelValidate(planningDir), raw);
|
|
113
|
+
},
|
|
114
|
+
'extract-exports': () => {
|
|
115
|
+
const filePath = args[2];
|
|
116
|
+
if (!filePath) {
|
|
117
|
+
return makeInvalidArgs('file-path', 'Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
|
|
72
118
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
c.output(intel.intelPatchMeta(path.resolve(cwd, filePath)), raw);
|
|
92
|
-
}
|
|
93
|
-
else if (subcommand === 'validate') {
|
|
94
|
-
const planningDir = path.join(cwd, '.planning');
|
|
95
|
-
c.output(intel.intelValidate(planningDir), raw);
|
|
96
|
-
}
|
|
97
|
-
else if (subcommand === 'extract-exports') {
|
|
98
|
-
const filePath = args[2];
|
|
99
|
-
if (!filePath) {
|
|
100
|
-
error('Usage: gsd-tools intel extract-exports <file-path>', ERROR_REASON.USAGE);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
|
|
104
|
-
}
|
|
105
|
-
else if (subcommand === 'update') {
|
|
106
|
-
const planningDir = path.join(cwd, '.planning');
|
|
107
|
-
c.output(intel.intelUpdate(planningDir), raw);
|
|
108
|
-
}
|
|
109
|
-
else if (subcommand === 'api-surface') {
|
|
110
|
-
const planningDir = path.join(cwd, '.planning');
|
|
111
|
-
c.output(intel.intelApiSurface(planningDir), raw);
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
error('Unknown intel subcommand. Available: query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface', ERROR_REASON.SDK_UNKNOWN_COMMAND);
|
|
115
|
-
}
|
|
119
|
+
c.output(intel.intelExtractExports(path.resolve(cwd, filePath)), raw);
|
|
120
|
+
},
|
|
121
|
+
update: () => {
|
|
122
|
+
const planningDir = path.join(cwd, '.planning');
|
|
123
|
+
c.output(intel.intelUpdate(planningDir), raw);
|
|
124
|
+
},
|
|
125
|
+
'api-surface': () => {
|
|
126
|
+
const planningDir = path.join(cwd, '.planning');
|
|
127
|
+
c.output(intel.intelApiSurface(planningDir), raw);
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
unknownMessage: (subcommand, available) => `Unknown intel subcommand. Available: ${available.join(', ')}`,
|
|
131
|
+
error,
|
|
132
|
+
cwd,
|
|
133
|
+
raw,
|
|
134
|
+
});
|
|
116
135
|
}
|
|
117
136
|
module.exports = {
|
|
118
137
|
routeIntelCommand,
|
|
@@ -14,10 +14,24 @@
|
|
|
14
14
|
function escapeRegex(value) {
|
|
15
15
|
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
16
|
}
|
|
17
|
+
// project_code values start with an uppercase letter (e.g. PROJ, APP_CODE);
|
|
18
|
+
// leading underscores are not valid project codes per .planning/config.json.
|
|
19
|
+
const PROJECT_CODE_PREFIX_STRIP_RE = /^[A-Z][A-Z0-9_]*-(?=\d)/;
|
|
20
|
+
const PROJECT_CODE_PREFIX_STRIP_RE_I = /^[A-Z][A-Z0-9_]*-(?=\d)/i;
|
|
21
|
+
const PROJECT_CODE_PREFIX_CAPTURE_RE_I = /^([A-Z][A-Z0-9_]*)-(\d.*)/i;
|
|
22
|
+
const OPTIONAL_PROJECT_CODE_PREFIX_SOURCE = '(?:[A-Z][A-Z0-9_]*-)?';
|
|
23
|
+
function stripProjectCodePrefix(value, caseInsensitive = true) {
|
|
24
|
+
const input = String(value);
|
|
25
|
+
const re = caseInsensitive ? PROJECT_CODE_PREFIX_STRIP_RE_I : PROJECT_CODE_PREFIX_STRIP_RE;
|
|
26
|
+
return input.replace(re, '');
|
|
27
|
+
}
|
|
28
|
+
function hasProjectCodePrefix(value) {
|
|
29
|
+
return PROJECT_CODE_PREFIX_STRIP_RE_I.test(String(value));
|
|
30
|
+
}
|
|
17
31
|
function normalizePhaseName(phase) {
|
|
18
32
|
const str = String(phase);
|
|
19
33
|
// Strip optional project_code prefix (e.g., 'CK-01' → '01')
|
|
20
|
-
const stripped = str
|
|
34
|
+
const stripped = stripProjectCodePrefix(str, false);
|
|
21
35
|
// Milestone-prefixed phase IDs: M-NN or M-N-N (deep decomposition).
|
|
22
36
|
const milestoneMatch = stripped.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
23
37
|
if (milestoneMatch) {
|
|
@@ -39,8 +53,7 @@ function normalizePhaseName(phase) {
|
|
|
39
53
|
return str;
|
|
40
54
|
}
|
|
41
55
|
function getMilestoneFromPhaseId(phaseId) {
|
|
42
|
-
const
|
|
43
|
-
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
56
|
+
const stripped = stripProjectCodePrefix(phaseId);
|
|
44
57
|
const m = stripped.match(/^0*(\d+)-\d/);
|
|
45
58
|
if (!m)
|
|
46
59
|
return null;
|
|
@@ -50,8 +63,7 @@ function getMilestoneFromPhaseId(phaseId) {
|
|
|
50
63
|
return `v${major}.0`;
|
|
51
64
|
}
|
|
52
65
|
function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
|
|
53
|
-
const
|
|
54
|
-
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/i, '');
|
|
66
|
+
const stripped = stripProjectCodePrefix(phaseId);
|
|
55
67
|
const m = stripped.match(/^0*(\d+)-(0*(\d+(?:-\d+)*))$/);
|
|
56
68
|
if (!m)
|
|
57
69
|
return null;
|
|
@@ -70,7 +82,7 @@ function getPhaseDirFromPhaseId(phaseId, phaseName, projectCode) {
|
|
|
70
82
|
* prose regardless of zero-padding on either side.
|
|
71
83
|
*/
|
|
72
84
|
function phaseMarkdownRegexSource(phaseNum) {
|
|
73
|
-
const stripped =
|
|
85
|
+
const stripped = stripProjectCodePrefix(phaseNum);
|
|
74
86
|
// Milestone-prefixed IDs: M-NN or M-N-N (deep).
|
|
75
87
|
const milestoneSegments = stripped.match(/^(\d+)((?:-\d+)*)([A-Z]?(?:\.\d+)*)$/i);
|
|
76
88
|
if (milestoneSegments && milestoneSegments[2]) {
|
|
@@ -99,14 +111,14 @@ function phaseMarkdownRegexSource(phaseNum) {
|
|
|
99
111
|
*/
|
|
100
112
|
function phaseMarkdownRegexSourceExact(phaseNum) {
|
|
101
113
|
const raw = String(phaseNum);
|
|
102
|
-
if (
|
|
114
|
+
if (!hasProjectCodePrefix(raw))
|
|
103
115
|
return null;
|
|
104
116
|
return escapeRegex(raw);
|
|
105
117
|
}
|
|
106
118
|
function comparePhaseNum(a, b) {
|
|
107
119
|
// Strip optional project_code prefix before comparing
|
|
108
|
-
const sa =
|
|
109
|
-
const sb =
|
|
120
|
+
const sa = stripProjectCodePrefix(a);
|
|
121
|
+
const sb = stripProjectCodePrefix(b);
|
|
110
122
|
const milestoneA = sa.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
111
123
|
const milestoneB = sb.match(/^(\d+)((?:-\d+)+)([A-Z]?(?:\.\d+)*)$/i);
|
|
112
124
|
if (milestoneA && milestoneB) {
|
|
@@ -162,7 +174,7 @@ function comparePhaseNum(a, b) {
|
|
|
162
174
|
* Extract the phase token from a directory name.
|
|
163
175
|
*/
|
|
164
176
|
function extractPhaseToken(dirName) {
|
|
165
|
-
const codePrefixMatch = dirName.match(
|
|
177
|
+
const codePrefixMatch = dirName.match(PROJECT_CODE_PREFIX_CAPTURE_RE_I);
|
|
166
178
|
let prefix = '';
|
|
167
179
|
let rest = dirName;
|
|
168
180
|
if (codePrefixMatch) {
|
|
@@ -192,7 +204,7 @@ function phaseTokenMatches(dirName, normalized) {
|
|
|
192
204
|
const token = extractPhaseToken(dirName);
|
|
193
205
|
if (token.toUpperCase() === normalized.toUpperCase())
|
|
194
206
|
return true;
|
|
195
|
-
const stripped = dirName
|
|
207
|
+
const stripped = stripProjectCodePrefix(dirName);
|
|
196
208
|
if (stripped !== dirName) {
|
|
197
209
|
const strippedToken = extractPhaseToken(stripped);
|
|
198
210
|
if (strippedToken.toUpperCase() === normalized.toUpperCase())
|
|
@@ -202,6 +214,8 @@ function phaseTokenMatches(dirName, normalized) {
|
|
|
202
214
|
}
|
|
203
215
|
module.exports = {
|
|
204
216
|
escapeRegex,
|
|
217
|
+
OPTIONAL_PROJECT_CODE_PREFIX_SOURCE,
|
|
218
|
+
stripProjectCodePrefix,
|
|
205
219
|
normalizePhaseName,
|
|
206
220
|
getMilestoneFromPhaseId,
|
|
207
221
|
getPhaseDirFromPhaseId,
|
|
@@ -32,7 +32,7 @@ const coreUtilsMod = require("./core-utils.cjs");
|
|
|
32
32
|
const { toPosixPath, generateSlugInternal, readSubdirectories } = coreUtilsMod;
|
|
33
33
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-id.cjs is an export= CommonJS module
|
|
34
34
|
const phaseIdMod = require("./phase-id.cjs");
|
|
35
|
-
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches } = phaseIdMod;
|
|
35
|
+
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, comparePhaseNum, phaseTokenMatches, OPTIONAL_PROJECT_CODE_PREFIX_SOURCE, } = phaseIdMod;
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- phase-locator.cjs is an export= CommonJS module
|
|
37
37
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
38
38
|
const { findPhaseInternal, getArchivedPhaseDirs } = phaseLocatorMod;
|
|
@@ -167,7 +167,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
|
|
|
167
167
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
168
168
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
169
169
|
baseExists = dirs.some((d) => phaseTokenMatches(d, normalized));
|
|
170
|
-
const dirPattern = new RegExp(
|
|
170
|
+
const dirPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${escapeRegex(normalized)}\\.(\\d+)`);
|
|
171
171
|
for (const dir of dirs) {
|
|
172
172
|
const match = dir.match(dirPattern);
|
|
173
173
|
if (match)
|
|
@@ -310,7 +310,7 @@ function cmdFindPhase(cwd, phase, raw) {
|
|
|
310
310
|
const match = dirs.find((d) => phaseTokenMatches(d, normalized));
|
|
311
311
|
if (!match)
|
|
312
312
|
continue;
|
|
313
|
-
const dirMatch = match.match(
|
|
313
|
+
const dirMatch = match.match(new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}(\\d+[A-Z]?(?:\\.\\d+)*)-?(.*)`, 'i')) ||
|
|
314
314
|
match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
|
|
315
315
|
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
|
|
316
316
|
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
|
|
@@ -756,7 +756,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
|
|
756
756
|
try {
|
|
757
757
|
const entries = node_fs_1.default.readdirSync(phasesDir, { withFileTypes: true });
|
|
758
758
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
759
|
-
const decimalPattern = new RegExp(
|
|
759
|
+
const decimalPattern = new RegExp(`^${OPTIONAL_PROJECT_CODE_PREFIX_SOURCE}${escapeRegex(normalizedBase)}\\.(\\d+)`);
|
|
760
760
|
for (const dir of dirs) {
|
|
761
761
|
const dm = dir.match(decimalPattern);
|
|
762
762
|
if (dm)
|
|
@@ -33,6 +33,54 @@ process.on('exit', () => {
|
|
|
33
33
|
catch { /* already gone */ }
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Lock liveness probe (test seam) — audit M1
|
|
38
|
+
//
|
|
39
|
+
// mtime is a leaky proxy for "the holder is alive". The prior withPlanningLock
|
|
40
|
+
// timeout fallback unconditionally unlinked WHATEVER lock existed — even a fresh,
|
|
41
|
+
// live holder's — and re-acquired it, force-stealing a live writer's critical
|
|
42
|
+
// section. We backport capability-lock.cts's pid-liveness gate: a dead holder is
|
|
43
|
+
// stolen promptly inside the polite loop; a live holder is waited on. The
|
|
44
|
+
// indirection lets unit tests inject a deterministic isPidAlive without real pids.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/** Is `pid` a live process? process.kill(pid, 0) succeeds for a live (signalable) process. */
|
|
47
|
+
function _realIsPidAlive(pid) {
|
|
48
|
+
try {
|
|
49
|
+
process.kill(pid, 0);
|
|
50
|
+
return true; // signalable → alive
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
// EPERM = process exists but we cannot signal it (still ALIVE). ESRCH = gone.
|
|
54
|
+
return err.code === 'EPERM';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const _planningLockProbes = { isPidAlive: _realIsPidAlive };
|
|
58
|
+
function _planningLockIsPidAlive(pid) {
|
|
59
|
+
return _planningLockProbes.isPidAlive(pid);
|
|
60
|
+
}
|
|
61
|
+
const _planningLockTestHooks = {};
|
|
62
|
+
// Monotonic sequence for unique stale-steal rename targets (no crypto dependency).
|
|
63
|
+
let _planningStealSeq = 0;
|
|
64
|
+
/**
|
|
65
|
+
* Is the holder recorded in the .lock body VERIFIED-LIVE? The body is JSON
|
|
66
|
+
* { pid, cwd, acquired }. Returns true ONLY when the body parses AND the recorded
|
|
67
|
+
* pid signals alive. A garbage / pid-less / unreadable body (or a dead pid) is NOT
|
|
68
|
+
* verified-live, so the lock stays stealable — corrupt locks never block forever,
|
|
69
|
+
* and a live holder is never force-stolen.
|
|
70
|
+
*/
|
|
71
|
+
function _planningHolderVerifiedLive(lockPath) {
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(node_fs_1.default.readFileSync(lockPath, 'utf-8'));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false; // unreadable / unparseable body → cannot verify → not verified-live
|
|
78
|
+
}
|
|
79
|
+
const pid = parsed?.pid;
|
|
80
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
|
|
81
|
+
return false;
|
|
82
|
+
return _planningLockIsPidAlive(pid);
|
|
83
|
+
}
|
|
36
84
|
// Transient errno codes that indicate a temporary filesystem condition under
|
|
37
85
|
// concurrent O_EXCL races — Docker overlay-fs (ENOENT/EINVAL/EIO), NFS
|
|
38
86
|
// (ESTALE), and OS-level interrupt/retry signals (EAGAIN/EINTR). These are
|
|
@@ -97,6 +145,12 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
97
145
|
clock = clock_cjs_1.realClock;
|
|
98
146
|
const lockPath = node_path_1.default.join(planningDir(cwd), '.lock');
|
|
99
147
|
const lockTimeout = 10000; // 10 seconds
|
|
148
|
+
// Deadman ceiling (audit M1 / R4-FIX) — set ABOVE lockTimeout so a holder that reads
|
|
149
|
+
// as alive but is actually a pid-reuse alias (the .lock body has no startTime, so
|
|
150
|
+
// liveness alone cannot detect reuse) is still recovered once its lock ages past this
|
|
151
|
+
// absolute ceiling. Without it, a false-alive holder would make withPlanningLock throw
|
|
152
|
+
// on every call with no self-heal. Mirrors acquireStateLock's deadmanCeilingMs.
|
|
153
|
+
const deadmanCeilingMs = 60000;
|
|
100
154
|
const start = clock.now();
|
|
101
155
|
// Ensure .planning/ exists
|
|
102
156
|
try {
|
|
@@ -143,31 +197,104 @@ function withPlanningLock(cwd, fn, clock) {
|
|
|
143
197
|
continue;
|
|
144
198
|
}
|
|
145
199
|
if (nodeErr.code === 'EEXIST') {
|
|
146
|
-
//
|
|
200
|
+
// Liveness-gated steal (audit M1). Steal the lock PROMPTLY only when its
|
|
201
|
+
// recorded holder is NOT verified-live (crashed/dead pid or garbage body).
|
|
202
|
+
// A verified-live holder is waited on — never force-stolen — because nuking
|
|
203
|
+
// a slow-but-live writer's lock corrupts the .planning/ critical section.
|
|
204
|
+
// The steal is an ATOMIC rename-then-recreate guarded by an identity re-confirm
|
|
205
|
+
// so a racer that recreates a fresh lock in the decision→steal gap never has
|
|
206
|
+
// its replacement deleted (audit M2 / PR #1532 review, window b). The body is
|
|
207
|
+
// written atomically (writeFileSync …{flag:'wx'}) so there is no empty-body
|
|
208
|
+
// create window here — only the double-steal needs hardening.
|
|
147
209
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
210
|
+
const decisionStat = node_fs_1.default.statSync(lockPath);
|
|
211
|
+
// Snapshot the decision-time body too: (dev, ino) alone is defeated by inode
|
|
212
|
+
// REUSE (a racer's unlink+recreate can land on the same inode), so the body
|
|
213
|
+
// content binds the identity as well — mirrors capability-lock.cts's (dev,
|
|
214
|
+
// ino, ts) re-confirm.
|
|
215
|
+
let decisionBody;
|
|
216
|
+
try {
|
|
217
|
+
decisionBody = node_fs_1.default.readFileSync(lockPath, 'utf-8');
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
decisionBody = null;
|
|
221
|
+
}
|
|
222
|
+
let stealable = !_planningHolderVerifiedLive(lockPath);
|
|
223
|
+
if (!stealable) {
|
|
224
|
+
// Verified-live, but recover anyway once the lock crosses the absolute
|
|
225
|
+
// deadman ceiling — defeats a pid-reuse false-alive that would otherwise
|
|
226
|
+
// block forever (R4-FIX; mtime age is from lock creation, not this call).
|
|
227
|
+
const age = clock.now() - decisionStat.mtimeMs;
|
|
228
|
+
stealable = age > deadmanCeilingMs;
|
|
229
|
+
}
|
|
230
|
+
if (stealable) {
|
|
231
|
+
if (_planningLockTestHooks.beforeSteal)
|
|
232
|
+
_planningLockTestHooks.beforeSteal({ lockPath });
|
|
233
|
+
// Identity re-confirm immediately before the steal: a racer that stole +
|
|
234
|
+
// recreated a fresh lock in the decision→steal gap changes (dev, ino) → do
|
|
235
|
+
// NOT delete the replacement; back off and re-evaluate.
|
|
236
|
+
let confirmStat;
|
|
237
|
+
try {
|
|
238
|
+
confirmStat = node_fs_1.default.statSync(lockPath);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
continue; // vanished between decision and steal — retry the create.
|
|
242
|
+
}
|
|
243
|
+
let confirmBody;
|
|
244
|
+
try {
|
|
245
|
+
confirmBody = node_fs_1.default.readFileSync(lockPath, 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
confirmBody = null;
|
|
249
|
+
}
|
|
250
|
+
const sameInstance = typeof decisionStat.dev === 'number' && typeof decisionStat.ino === 'number' &&
|
|
251
|
+
confirmStat.dev === decisionStat.dev && confirmStat.ino === decisionStat.ino &&
|
|
252
|
+
decisionBody !== null && confirmBody === decisionBody;
|
|
253
|
+
if (!sameInstance) {
|
|
254
|
+
clock.sleep(100); // a racer won the steal + recreated — re-evaluate, don't delete it.
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
// Atomic steal: rename the inode aside, then remove it. Only ONE racer can
|
|
258
|
+
// win the rename; a failed rename means another process already stole it, so
|
|
259
|
+
// we must NOT fall through to a delete — back off and retry the create.
|
|
260
|
+
const stolen = lockPath + '.stale-' + process.pid + '-' + clock.now() + '-' + (_planningStealSeq++);
|
|
261
|
+
let renamed = false;
|
|
262
|
+
try {
|
|
263
|
+
node_fs_1.default.renameSync(lockPath, stolen);
|
|
264
|
+
renamed = true;
|
|
265
|
+
}
|
|
266
|
+
catch { /* another racer won */ }
|
|
267
|
+
if (renamed) {
|
|
268
|
+
try {
|
|
269
|
+
node_fs_1.default.rmSync(stolen, { force: true });
|
|
270
|
+
}
|
|
271
|
+
catch { /* best-effort */ }
|
|
272
|
+
continue; // dead/garbage/expired holder freed — retry immediately to grab it.
|
|
273
|
+
}
|
|
274
|
+
clock.sleep(100); // lost the steal race — back off and retry.
|
|
275
|
+
continue;
|
|
152
276
|
}
|
|
153
277
|
}
|
|
154
278
|
catch {
|
|
155
279
|
continue;
|
|
156
280
|
}
|
|
157
|
-
//
|
|
281
|
+
// Live holder — wait and retry (cross-platform, no shell dependency).
|
|
158
282
|
clock.sleep(100);
|
|
159
283
|
continue;
|
|
160
284
|
}
|
|
161
285
|
throw err;
|
|
162
286
|
}
|
|
163
287
|
}
|
|
164
|
-
// Timeout
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
288
|
+
// Timeout against a holder still present at budget exhaustion. The polite loop
|
|
289
|
+
// already stole any DEAD holder; reaching here means the holder is verified-live
|
|
290
|
+
// (or a pid-reuse alias we must not corrupt). Do NOT force-steal — the prior
|
|
291
|
+
// unconditional `unlinkSync(lockPath); acquireLock()` here (audit M1) robbed live
|
|
292
|
+
// writers, and its re-acquire sat OUTSIDE any try so a concurrent re-create raced
|
|
293
|
+
// a raw EEXIST out of the helper (audit M2). Surface a clear timeout error instead.
|
|
294
|
+
const timeoutErr = new Error('withPlanningLock: ' + lockPath + ' held by a live process for ' +
|
|
295
|
+
(clock.now() - start) + 'ms (exceeded ' + lockTimeout + 'ms budget)');
|
|
296
|
+
timeoutErr.lockTimeout = true;
|
|
297
|
+
throw timeoutErr;
|
|
171
298
|
}
|
|
172
299
|
function createPlanningWorkspace(cwd, opts = {}) {
|
|
173
300
|
return {
|
|
@@ -242,4 +369,21 @@ module.exports = {
|
|
|
242
369
|
getActiveWorkstream,
|
|
243
370
|
setActiveWorkstream,
|
|
244
371
|
findContextMdIn,
|
|
372
|
+
// Test seam (audit M1): inject a deterministic isPidAlive so the liveness-gated
|
|
373
|
+
// steal decision is exercised without real pids. Mirrors capability-lock.cts.
|
|
374
|
+
_setLockProbes(probes) {
|
|
375
|
+
if (typeof probes.isPidAlive === 'function')
|
|
376
|
+
_planningLockProbes.isPidAlive = probes.isPidAlive;
|
|
377
|
+
},
|
|
378
|
+
_resetLockProbes() {
|
|
379
|
+
_planningLockProbes.isPidAlive = _realIsPidAlive;
|
|
380
|
+
},
|
|
381
|
+
// Test seam (PR #1532 review): script the steal decision→steal gap (window b).
|
|
382
|
+
_setPlanningLockTestHooks(hooks) {
|
|
383
|
+
if ('beforeSteal' in hooks)
|
|
384
|
+
_planningLockTestHooks.beforeSteal = hooks.beforeSteal;
|
|
385
|
+
},
|
|
386
|
+
_resetPlanningLockTestHooks() {
|
|
387
|
+
delete _planningLockTestHooks.beforeSteal;
|
|
388
|
+
},
|
|
245
389
|
};
|
|
@@ -288,6 +288,13 @@ function projectProhibitions(items) {
|
|
|
288
288
|
if (typeof p.check_violation_fixture === 'string' && p.check_violation_fixture.trim() !== '') {
|
|
289
289
|
entry.check_violation_fixture = String(p.check_violation_fixture);
|
|
290
290
|
}
|
|
291
|
+
// `check_clean_fixture` (#1346) rides BOTH kinds — the KNOWN-CLEAN control subject the prover
|
|
292
|
+
// requires to stay GREEN (content-dependence proof). Emit ONLY a non-empty fixture (blank ->
|
|
293
|
+
// absent so no control runs; the documented residual remains). Like the violation fixture it is
|
|
294
|
+
// meaningless without the descriptor, so it lives inside this well-formed-descriptor branch.
|
|
295
|
+
if (typeof p.check_clean_fixture === 'string' && p.check_clean_fixture.trim() !== '') {
|
|
296
|
+
entry.check_clean_fixture = String(p.check_clean_fixture);
|
|
297
|
+
}
|
|
291
298
|
}
|
|
292
299
|
out.push(entry);
|
|
293
300
|
}
|
|
@@ -1010,19 +1010,31 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
|
|
1010
1010
|
// repo-root `CLAUDE.md`, so generated GSD content does not land next to — or
|
|
1011
1011
|
// pollute — a hand-crafted repo-root CLAUDE.md. An explicit `claude_md_path`
|
|
1012
1012
|
// config value or `--output` still wins.
|
|
1013
|
-
let configClaudeMdPath = '
|
|
1013
|
+
let configClaudeMdPath = '.claude/CLAUDE.md';
|
|
1014
1014
|
try {
|
|
1015
1015
|
const config = loadConfig(cwd);
|
|
1016
1016
|
if (config['claude_md_path'])
|
|
1017
1017
|
configClaudeMdPath = config['claude_md_path'];
|
|
1018
1018
|
if (config['claude_md_assembly'])
|
|
1019
1019
|
assemblyConfig = config['claude_md_assembly'];
|
|
1020
|
-
// #
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1020
|
+
// #1529: When no explicit --output is provided, derive the instruction
|
|
1021
|
+
// file from the runtime via the shared `getProjectInstructionFile` policy
|
|
1022
|
+
// (single source of truth in runtime-name-policy.cjs, shared with the
|
|
1023
|
+
// new-project.md bash workflow via `gsd-tools query
|
|
1024
|
+
// project-instruction-file`). Previously this was a codex-only override
|
|
1025
|
+
// (#3163) that left AGENTS-native runtimes (opencode/kilo/kimi) emitting
|
|
1026
|
+
// CLAUDE.md; copilot now resolves to .github/copilot-instructions.md, and
|
|
1027
|
+
// antigravity/gemini to GEMINI.md. GSD_RUNTIME env var takes precedence
|
|
1028
|
+
// over config.runtime, mirroring detectRuntime().
|
|
1029
|
+
//
|
|
1030
|
+
// Non-claude runtimes always win over a stale `claude_md_path` (the #3163
|
|
1031
|
+
// rationale: a Codex/AGENTS-native project must never write to CLAUDE.md
|
|
1032
|
+
// even if a prior Claude setup left a `claude_md_path` behind). For the
|
|
1033
|
+
// claude runtime, `claude_md_path` config is honored — it IS the
|
|
1034
|
+
// Claude-specific output setting (per #1098 and the #3163 non-codex test).
|
|
1023
1035
|
const effectiveRuntime = (0, runtime_name_policy_cjs_1.resolveRuntimeNameFromCandidates)(process.env['GSD_RUNTIME'], config['runtime']);
|
|
1024
|
-
if (!options.output && effectiveRuntime
|
|
1025
|
-
configClaudeMdPath =
|
|
1036
|
+
if (!options.output && effectiveRuntime && effectiveRuntime !== 'claude') {
|
|
1037
|
+
configClaudeMdPath = (0, runtime_name_policy_cjs_1.getProjectInstructionFile)(effectiveRuntime);
|
|
1026
1038
|
}
|
|
1027
1039
|
}
|
|
1028
1040
|
catch { /* use default */ }
|