@opengsd/gsd-core 1.5.0-rc.3 → 1.5.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-writer.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -0
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.md +1 -1
- package/agents/gsd-roadmapper.md +55 -2
- package/agents/gsd-security-auditor.md +1 -0
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.md +1 -1
- package/agents/gsd-verifier.md +13 -2
- package/bin/install.js +36 -57
- package/commands/gsd/progress.md +2 -1
- package/gemini-extension.json +1 -1
- package/gsd-core/bin/gsd-tools.cjs +167 -3
- package/gsd-core/bin/lib/active-workstream-store.cjs +6 -0
- package/gsd-core/bin/lib/capability-state.cjs +97 -3
- package/gsd-core/bin/lib/capability-writer.cjs +354 -0
- package/gsd-core/bin/lib/config.cjs +80 -24
- package/gsd-core/bin/lib/edge-probe.cjs +25 -2
- package/gsd-core/bin/lib/frontmatter.cjs +53 -1
- package/gsd-core/bin/lib/git-base-branch.cjs +194 -0
- package/gsd-core/bin/lib/init.cjs +28 -6
- package/gsd-core/bin/lib/install-profiles.cjs +55 -0
- package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
- package/gsd-core/bin/lib/phase.cjs +28 -12
- package/gsd-core/bin/lib/plan-drift-guard.cjs +117 -0
- package/gsd-core/bin/lib/probe-core.cjs +117 -1
- package/gsd-core/bin/lib/roadmap-parser.cjs +13 -3
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +246 -0
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +34 -2
- package/gsd-core/bin/lib/state.cjs +240 -59
- package/gsd-core/bin/lib/verify.cjs +73 -4
- package/gsd-core/bin/lib/worktree-safety.cjs +2 -1
- package/gsd-core/references/edge-probe.md +11 -0
- package/gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json +14 -0
- package/gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json +4 -0
- package/gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json +32 -0
- package/gsd-core/references/prohibition-probe.md +248 -0
- package/gsd-core/templates/spec.md +14 -0
- package/gsd-core/workflows/complete-milestone.md +1 -5
- package/gsd-core/workflows/execute-phase.md +4 -3
- package/gsd-core/workflows/execute-plan.md +21 -6
- package/gsd-core/workflows/help/modes/full.md +4 -0
- package/gsd-core/workflows/next.md +50 -2
- package/gsd-core/workflows/pause-work.md +7 -1
- package/gsd-core/workflows/plan-phase.md +2 -0
- package/gsd-core/workflows/plan-review-convergence.md +14 -4
- package/gsd-core/workflows/pr-branch.md +4 -2
- package/gsd-core/workflows/quick.md +3 -2
- package/gsd-core/workflows/resume-project.md +17 -1
- package/gsd-core/workflows/settings.md +27 -1
- package/gsd-core/workflows/ship.md +1 -5
- package/gsd-core/workflows/spec-phase.md +75 -0
- package/gsd-core/workflows/verify-phase.md +14 -4
- package/hooks/dist/gsd-ensure-canonical-path.js +305 -0
- package/hooks/dist/gsd-statusline.js +1 -1
- package/hooks/dist/managed-hooks-registry.cjs +1 -0
- package/hooks/gsd-ensure-canonical-path.js +305 -0
- package/hooks/gsd-statusline.js +1 -1
- package/hooks/hooks.json +1 -0
- package/hooks/managed-hooks-registry.cjs +1 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +7 -0
- package/scripts/changeset/new.cjs +17 -3
- package/scripts/fix-slash-commands.cjs +15 -3
- package/scripts/gen-capability-registry.cjs +14 -1
- package/scripts/lint-allow-test-rule-refs.allowlist.json +2 -1
- package/scripts/lint-test-file-count.allowlist.json +6 -0
- package/scripts/mutation-matrix.cjs +108 -7
- package/scripts/pr-target-policy.cjs +63 -0
- package/scripts/research-profiles.cjs +5 -5
- package/scripts/run-tests.cjs +107 -6
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git Base-Branch Resolver — issue #1146.
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth for detecting the repository's default branch.
|
|
6
|
+
* Replaces the duplicated per-workflow bash detection that only consulted
|
|
7
|
+
* `refs/remotes/origin/HEAD` then hardcoded `:-main`, which silently
|
|
8
|
+
* returned "main" for repos whose default branch is "master" whenever
|
|
9
|
+
* origin/HEAD was unset (git init + remote add / fetch without set-head /
|
|
10
|
+
* most CI checkouts / many worktrees).
|
|
11
|
+
*
|
|
12
|
+
* Precedence ladder (highest to lowest):
|
|
13
|
+
* 1. `git.base_branch` config override from .planning/config.json
|
|
14
|
+
* 2. `git symbolic-ref --short refs/remotes/origin/HEAD` (fast, no network)
|
|
15
|
+
* 3. `git remote show origin` HEAD branch ← AUTHORITATIVE; works when #2 unset
|
|
16
|
+
* 4. Local branch existence: "master" present + "main" absent → "master";
|
|
17
|
+
* "main" present → "main"
|
|
18
|
+
* 5. "main" (last-resort default)
|
|
19
|
+
*
|
|
20
|
+
* Every git subprocess is bounded with a timeout (≤ 30 s); on timeout/error
|
|
21
|
+
* the resolver degrades gracefully to the next tier — it never throws.
|
|
22
|
+
*
|
|
23
|
+
* Pure/testable: all I/O is injectable via the `deps` argument so unit
|
|
24
|
+
* tests can run without touching the real filesystem or spawning real git.
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.readConfigBaseBranch = readConfigBaseBranch;
|
|
31
|
+
exports.trySymbolicRef = trySymbolicRef;
|
|
32
|
+
exports.tryRemoteShow = tryRemoteShow;
|
|
33
|
+
exports.tryLocalBranch = tryLocalBranch;
|
|
34
|
+
exports.resolveBaseBranch = resolveBaseBranch;
|
|
35
|
+
exports.cmdGitBaseBranch = cmdGitBaseBranch;
|
|
36
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
38
|
+
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
39
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
40
|
+
/**
|
|
41
|
+
* Safely look up `git.base_branch` from the project's config.json.
|
|
42
|
+
* Returns the configured value (a non-empty, non-null string) or null.
|
|
43
|
+
*/
|
|
44
|
+
function readConfigBaseBranch(planningDir, deps) {
|
|
45
|
+
const readFile = deps?.readFile ??
|
|
46
|
+
((p) => { try {
|
|
47
|
+
return node_fs_1.default.readFileSync(p, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
} });
|
|
52
|
+
const configPath = node_path_1.default.join(planningDir, 'config.json');
|
|
53
|
+
const raw = readFile(configPath);
|
|
54
|
+
if (!raw)
|
|
55
|
+
return null;
|
|
56
|
+
let cfg;
|
|
57
|
+
try {
|
|
58
|
+
cfg = JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
|
|
64
|
+
return null;
|
|
65
|
+
const top = cfg;
|
|
66
|
+
// Support both "git.base_branch" (nested) and "base_branch" (flat legacy)
|
|
67
|
+
const gitSection = top.git;
|
|
68
|
+
if (gitSection && typeof gitSection === 'object' && !Array.isArray(gitSection)) {
|
|
69
|
+
const nested = gitSection.base_branch;
|
|
70
|
+
if (typeof nested === 'string' && nested.trim())
|
|
71
|
+
return nested.trim();
|
|
72
|
+
}
|
|
73
|
+
const flat = top.base_branch;
|
|
74
|
+
if (typeof flat === 'string' && flat.trim())
|
|
75
|
+
return flat.trim();
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Try `git symbolic-ref --short refs/remotes/origin/HEAD` (no network).
|
|
80
|
+
* Strips the `origin/` prefix to return just the branch name.
|
|
81
|
+
* Returns null if unset or on error/timeout.
|
|
82
|
+
*/
|
|
83
|
+
function trySymbolicRef(cwd, execGit) {
|
|
84
|
+
try {
|
|
85
|
+
const r = execGit(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], { cwd, timeout: 5_000 });
|
|
86
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
87
|
+
return null;
|
|
88
|
+
// Output is e.g. "origin/main" — strip the prefix
|
|
89
|
+
const branch = r.stdout.trim().replace(/^origin\//, '');
|
|
90
|
+
return branch || null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Try `git remote show origin` to read the HEAD branch.
|
|
98
|
+
* This is authoritative when origin/HEAD is unset locally.
|
|
99
|
+
* Requires network access but succeeds in the common CI case where
|
|
100
|
+
* origin/HEAD was never set after `git init && git remote add origin`.
|
|
101
|
+
*
|
|
102
|
+
* Parses the line: `HEAD branch: <name>`
|
|
103
|
+
* Returns null on error, timeout, or if the output is malformed.
|
|
104
|
+
*/
|
|
105
|
+
function tryRemoteShow(cwd, execGit) {
|
|
106
|
+
try {
|
|
107
|
+
const r = execGit(['remote', 'show', 'origin'], { cwd, timeout: 15_000 });
|
|
108
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
109
|
+
return null;
|
|
110
|
+
// The line looks like: " HEAD branch: master"
|
|
111
|
+
const m = r.stdout.match(/^\s*HEAD branch:\s*(\S+)\s*$/m);
|
|
112
|
+
if (!m)
|
|
113
|
+
return null;
|
|
114
|
+
const branch = m[1];
|
|
115
|
+
// git emits "(unknown)" when the remote is offline but the local cache
|
|
116
|
+
// resolved it; treat that as non-authoritative and fall through.
|
|
117
|
+
if (!branch || branch === '(unknown)')
|
|
118
|
+
return null;
|
|
119
|
+
return branch;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Detect local branch existence as a tie-breaker when no remote info is available.
|
|
127
|
+
*
|
|
128
|
+
* Rules:
|
|
129
|
+
* - "master" present AND "main" absent → "master"
|
|
130
|
+
* - "main" present → "main"
|
|
131
|
+
* - Neither → null (fall through to default)
|
|
132
|
+
*
|
|
133
|
+
* Returns null on error/timeout.
|
|
134
|
+
*/
|
|
135
|
+
function tryLocalBranch(cwd, execGit) {
|
|
136
|
+
try {
|
|
137
|
+
const r = execGit(['branch', '--list', 'main', 'master'], { cwd, timeout: 5_000 });
|
|
138
|
+
if (r.exitCode !== 0 || !r.stdout)
|
|
139
|
+
return null;
|
|
140
|
+
// `git branch --list main master` outputs one line per matching branch
|
|
141
|
+
const lines = r.stdout.split('\n').map(l => l.trim().replace(/^\*\s*/, ''));
|
|
142
|
+
const hasMain = lines.includes('main');
|
|
143
|
+
const hasMaster = lines.includes('master');
|
|
144
|
+
if (hasMaster && !hasMain)
|
|
145
|
+
return 'master';
|
|
146
|
+
if (hasMain)
|
|
147
|
+
return 'main';
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Resolve the default/base branch for the repository at `cwd`.
|
|
156
|
+
*
|
|
157
|
+
* Consults the full precedence ladder and always returns a non-empty string.
|
|
158
|
+
* Never throws.
|
|
159
|
+
*/
|
|
160
|
+
function resolveBaseBranch(cwd, deps) {
|
|
161
|
+
const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
|
|
162
|
+
// Derive .planning dir relative to cwd (mirrors planningDir() in core.cjs)
|
|
163
|
+
const planningDir = node_path_1.default.join(cwd, '.planning');
|
|
164
|
+
// 1. Config override
|
|
165
|
+
const configured = readConfigBaseBranch(planningDir, deps);
|
|
166
|
+
if (configured)
|
|
167
|
+
return configured;
|
|
168
|
+
// 2. symbolic-ref (fast, no network)
|
|
169
|
+
const symref = trySymbolicRef(cwd, execGit);
|
|
170
|
+
if (symref)
|
|
171
|
+
return symref;
|
|
172
|
+
// 3. git remote show origin (authoritative when origin/HEAD unset)
|
|
173
|
+
const remoteShow = tryRemoteShow(cwd, execGit);
|
|
174
|
+
if (remoteShow)
|
|
175
|
+
return remoteShow;
|
|
176
|
+
// 4. Local branch existence
|
|
177
|
+
const local = tryLocalBranch(cwd, execGit);
|
|
178
|
+
if (local)
|
|
179
|
+
return local;
|
|
180
|
+
// 5. Last-resort default
|
|
181
|
+
return 'main';
|
|
182
|
+
}
|
|
183
|
+
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
184
|
+
/**
|
|
185
|
+
* CLI command: `gsd-tools git base-branch`
|
|
186
|
+
* Resolves the default branch and writes it to stdout (raw string, newline-terminated).
|
|
187
|
+
* Called by workflows via `gsd_run query git.base-branch`.
|
|
188
|
+
*/
|
|
189
|
+
function cmdGitBaseBranch(cwd, _args, deps) {
|
|
190
|
+
const branch = resolveBaseBranch(cwd, deps);
|
|
191
|
+
const write = deps?.write ?? ((s) => process.stdout.write(s));
|
|
192
|
+
write(branch + '\n');
|
|
193
|
+
return branch;
|
|
194
|
+
}
|
|
@@ -1533,7 +1533,9 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1533
1533
|
// It returns [] cheaply when no roots are configured, so the realpath cost only
|
|
1534
1534
|
// occurs when the caller has actually set trusted_global_roots.
|
|
1535
1535
|
const trustedGlobalRoots = (0, security_cjs_1.loadTrustedGlobalRoots)(config);
|
|
1536
|
-
|
|
1536
|
+
// Each entry is either a filesystem include ({ kind: 'include', ref, display }) or a
|
|
1537
|
+
// Skill-tool directive ({ kind: 'directive', name }) for plugin-provided namespaced skills.
|
|
1538
|
+
const validEntries = [];
|
|
1537
1539
|
for (const skillPath of skillPaths) {
|
|
1538
1540
|
if (typeof skillPath !== 'string')
|
|
1539
1541
|
continue;
|
|
@@ -1543,10 +1545,25 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1543
1545
|
process.stderr.write(`[agent-skills] WARNING: "global:" prefix with empty skill name — skipping\n`);
|
|
1544
1546
|
continue;
|
|
1545
1547
|
}
|
|
1546
|
-
|
|
1548
|
+
// Accept: one or more [A-Za-z0-9_-]+ segments joined by single colons.
|
|
1549
|
+
// Rejects: empty segments (::), leading/trailing colon, dots, slashes, backslashes.
|
|
1550
|
+
if (!/^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$/.test(skillName)) {
|
|
1547
1551
|
process.stderr.write(`[agent-skills] WARNING: Invalid global skill name "${skillName}" — skipping\n`);
|
|
1548
1552
|
continue;
|
|
1549
1553
|
}
|
|
1554
|
+
const isNamespaced = skillName.includes(':');
|
|
1555
|
+
if (isNamespaced) {
|
|
1556
|
+
// Plugin-provided namespaced skill: no filesystem path exists locally.
|
|
1557
|
+
if (runtime === 'claude') {
|
|
1558
|
+
// Emit a natural-language Skill-tool directive (not a @-include).
|
|
1559
|
+
validEntries.push({ kind: 'directive', name: skillName });
|
|
1560
|
+
}
|
|
1561
|
+
else {
|
|
1562
|
+
process.stderr.write(`[agent-skills] WARNING: Plugin-namespaced skill "global:${skillName}" requires a Skill-tool-capable runtime (claude) — skipping on runtime "${runtime}"\n`);
|
|
1563
|
+
}
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
// Non-namespaced bare name: attempt filesystem resolution as before.
|
|
1550
1567
|
if (globalSkillsBase === null) {
|
|
1551
1568
|
process.stderr.write(`[agent-skills] WARNING: Runtime "${runtime}" does not use a skills directory — "global:${skillName}" is not supported on this runtime\n`);
|
|
1552
1569
|
continue;
|
|
@@ -1570,7 +1587,7 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1570
1587
|
}
|
|
1571
1588
|
process.stderr.write(`[agent-skills] NOTE: Global skill "${skillName}" accepted via trusted_global_roots (resolves outside the default skills dir)\n`);
|
|
1572
1589
|
}
|
|
1573
|
-
|
|
1590
|
+
validEntries.push({ kind: 'include', ref: `${globalSkillDir}/SKILL.md`, display: displayPath });
|
|
1574
1591
|
continue;
|
|
1575
1592
|
}
|
|
1576
1593
|
const pathCheck = (0, security_cjs_1.validatePath)(skillPath, projectRoot);
|
|
@@ -1583,11 +1600,16 @@ function buildAgentSkillsBlock(config, agentType, projectRoot) {
|
|
|
1583
1600
|
process.stderr.write(`[agent-skills] WARNING: Skill not found at "${skillPath}/SKILL.md" — skipping\n`);
|
|
1584
1601
|
continue;
|
|
1585
1602
|
}
|
|
1586
|
-
|
|
1603
|
+
validEntries.push({ kind: 'include', ref: `${skillPath}/SKILL.md`, display: skillPath });
|
|
1587
1604
|
}
|
|
1588
|
-
if (
|
|
1605
|
+
if (validEntries.length === 0)
|
|
1589
1606
|
return '';
|
|
1590
|
-
const lines =
|
|
1607
|
+
const lines = validEntries.map((entry) => {
|
|
1608
|
+
if (entry.kind === 'directive') {
|
|
1609
|
+
return `- Load the \`${entry.name}\` skill via the Skill tool before proceeding (plugin-provided).`;
|
|
1610
|
+
}
|
|
1611
|
+
return `- @${entry.ref}`;
|
|
1612
|
+
}).join('\n');
|
|
1591
1613
|
return `<agent_skills>\nRead these user-configured skills:\n${lines}\n</agent_skills>`;
|
|
1592
1614
|
}
|
|
1593
1615
|
function cmdAgentSkills(cwd, agentType, raw, jsonMode) {
|
|
@@ -505,6 +505,60 @@ function stageSkillsForRuntimeAsSkills(srcCommandsDir, resolvedProfile, converte
|
|
|
505
505
|
ensureExitCleanup();
|
|
506
506
|
return stageDir;
|
|
507
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Stage a converted copy of the agents directory for a given runtime.
|
|
510
|
+
*
|
|
511
|
+
* Analogous to `stageCommandsForRuntimeFlat` but for agent `.md` files. Each
|
|
512
|
+
* source `.md` is passed through `converter` and written as a flat `${name}.md`
|
|
513
|
+
* file in the staging directory. Agent filenames are kept verbatim (no prefix
|
|
514
|
+
* added here — the prefix is already embedded in agent stems, e.g. `gsd-planner.md`).
|
|
515
|
+
*
|
|
516
|
+
* This is used by the descriptor-driven `dispatchKindEntry` when an `agents` kind
|
|
517
|
+
* entry carries a non-null converter (ADR-457 / #1173). When `converter` is null,
|
|
518
|
+
* `agentsKind` falls back to the existing raw-copy path (`stageAgentsForProfile`).
|
|
519
|
+
*
|
|
520
|
+
* For the `full` profile (`skills === '*'`), all `.md` files are staged.
|
|
521
|
+
* For tiered profiles, only agents whose full stem is in `resolvedProfile.agents`
|
|
522
|
+
* are staged (mirrors `stageAgentsForProfile` behaviour).
|
|
523
|
+
*
|
|
524
|
+
* @param srcAgentsDir source agents directory (e.g. agents/)
|
|
525
|
+
* @param resolvedProfile profile filter from resolveProfile()
|
|
526
|
+
* @param converter (content: string) → string pure per-file converter
|
|
527
|
+
*/
|
|
528
|
+
function stageAgentsForRuntimeWithConverter(srcAgentsDir, resolvedProfile, converter) {
|
|
529
|
+
if (!node_fs_1.default.existsSync(srcAgentsDir))
|
|
530
|
+
return srcAgentsDir;
|
|
531
|
+
const stageDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'gsd-profile-runtime-agents-'));
|
|
532
|
+
try {
|
|
533
|
+
const entries = node_fs_1.default.readdirSync(srcAgentsDir, { withFileTypes: true });
|
|
534
|
+
for (const entry of entries) {
|
|
535
|
+
if (!entry.isFile())
|
|
536
|
+
continue;
|
|
537
|
+
if (!entry.name.endsWith('.md'))
|
|
538
|
+
continue;
|
|
539
|
+
// For tiered profiles, gate by agent stem (full filename without extension).
|
|
540
|
+
if (resolvedProfile.skills !== '*') {
|
|
541
|
+
const stem = entry.name.slice(0, -3);
|
|
542
|
+
if (!(resolvedProfile.agents instanceof Set && resolvedProfile.agents.has(stem))) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(srcAgentsDir, entry.name), 'utf8');
|
|
547
|
+
const converted = converter(content);
|
|
548
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(stageDir, entry.name), converted, 'utf8');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
try {
|
|
553
|
+
node_fs_1.default.rmSync(stageDir, { recursive: true, force: true });
|
|
554
|
+
}
|
|
555
|
+
catch { /* best-effort */ }
|
|
556
|
+
throw err;
|
|
557
|
+
}
|
|
558
|
+
STAGED_DIRS.add(stageDir);
|
|
559
|
+
ensureExitCleanup();
|
|
560
|
+
return stageDir;
|
|
561
|
+
}
|
|
508
562
|
/**
|
|
509
563
|
* Stage converted command files as flat `.md` files.
|
|
510
564
|
*
|
|
@@ -723,6 +777,7 @@ module.exports = {
|
|
|
723
777
|
mostRestrictiveProfile,
|
|
724
778
|
stageSkillsForProfile,
|
|
725
779
|
stageAgentsForProfile,
|
|
780
|
+
stageAgentsForRuntimeWithConverter,
|
|
726
781
|
stageSkillsForRuntimeAsSkills,
|
|
727
782
|
stageCommandsForRuntimeFlat,
|
|
728
783
|
STAGED_DIRS,
|
|
@@ -38,6 +38,7 @@ exports.BUNDLED_GSD_HOOK_FILES = Object.freeze(new Set([
|
|
|
38
38
|
'hooks/gsd-context-monitor.js',
|
|
39
39
|
'hooks/gsd-cursor-post-tool.js',
|
|
40
40
|
'hooks/gsd-cursor-session-start.js',
|
|
41
|
+
'hooks/gsd-ensure-canonical-path.js',
|
|
41
42
|
'hooks/gsd-graphify-update.sh',
|
|
42
43
|
'hooks/gsd-phase-boundary.sh',
|
|
43
44
|
'hooks/gsd-prompt-guard.js',
|
|
@@ -560,16 +560,29 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
560
560
|
_dirName = `${prefix}${_newPhaseId}-${slug}`;
|
|
561
561
|
}
|
|
562
562
|
else {
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
// Collect all phase numbers visible in the current-milestone content.
|
|
564
|
+
// Three sources are scanned so that a phase in ANY representation
|
|
565
|
+
// (section header, roadmap bullet, or on-disk directory) is counted:
|
|
566
|
+
// 1) Section headers: ### Phase N: / ## Phase N: / #### Phase N:
|
|
567
|
+
const headerPattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
|
|
568
|
+
// 2) Roadmap bullet entries: - [ ] **Phase N: ...** (all checkbox variants)
|
|
569
|
+
// The lookahead accepts colon, decimal-dot, whitespace, bold-close asterisk,
|
|
570
|
+
// or end-of-line so titleless forms ("- [ ] **Phase 11**", "- [ ] Phase 11")
|
|
571
|
+
// are counted and cannot collide with a freshly-added phase. (#1229)
|
|
572
|
+
const bulletPattern = /^[ \t]*-[ \t]*\[[^\]]*\][ \t]*\*{0,2}Phase[ \t]+(\d+)(?=[:.\s*]|$)/gim;
|
|
573
|
+
const usedPhaseNums = new Set();
|
|
565
574
|
let m;
|
|
566
|
-
while ((m =
|
|
575
|
+
while ((m = headerPattern.exec(content)) !== null) {
|
|
567
576
|
const num = parseInt(m[1], 10);
|
|
568
|
-
if (num
|
|
569
|
-
|
|
570
|
-
if (num > maxPhase)
|
|
571
|
-
maxPhase = num;
|
|
577
|
+
if (num !== 999)
|
|
578
|
+
usedPhaseNums.add(num);
|
|
572
579
|
}
|
|
580
|
+
while ((m = bulletPattern.exec(content)) !== null) {
|
|
581
|
+
const num = parseInt(m[1], 10);
|
|
582
|
+
if (num !== 999)
|
|
583
|
+
usedPhaseNums.add(num);
|
|
584
|
+
}
|
|
585
|
+
// 3) On-disk phase directories (e.g. phases/11-foo/ with no header yet)
|
|
573
586
|
const phasesOnDisk = node_path_1.default.join(planningDir(cwd), 'phases');
|
|
574
587
|
if (node_fs_1.default.existsSync(phasesOnDisk)) {
|
|
575
588
|
const dirNumPattern = /^(?:[A-Z][A-Z0-9]*-)?(\d+)-/;
|
|
@@ -578,13 +591,16 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
|
|
578
591
|
if (!match)
|
|
579
592
|
continue;
|
|
580
593
|
const num = parseInt(match[1], 10);
|
|
581
|
-
if (num
|
|
582
|
-
|
|
583
|
-
if (num > maxPhase)
|
|
584
|
-
maxPhase = num;
|
|
594
|
+
if (num !== 999)
|
|
595
|
+
usedPhaseNums.add(num);
|
|
585
596
|
}
|
|
586
597
|
}
|
|
587
|
-
|
|
598
|
+
// phase.add appends after the highest *used* number. Collecting numbers from
|
|
599
|
+
// section headers, roadmap bullets, AND on-disk dirs above is what prevents the
|
|
600
|
+
// #1229 collision (a bullet-only Phase N is now counted), so max+1 cannot reuse
|
|
601
|
+
// an existing number.
|
|
602
|
+
const maxUsed = usedPhaseNums.size > 0 ? Math.max(...usedPhaseNums) : 0;
|
|
603
|
+
_newPhaseId = maxUsed + 1;
|
|
588
604
|
const paddedNum = String(_newPhaseId).padStart(2, '0');
|
|
589
605
|
_dirName = `${prefix}${paddedNum}-${slug}`;
|
|
590
606
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ADR-22 Drift-Guard Decision Module
|
|
4
|
+
*
|
|
5
|
+
* Implements the authority ladder and severity classification table from
|
|
6
|
+
* ADR-22 (docs/adr/0022-source-grounding-drift-guard.md).
|
|
7
|
+
*
|
|
8
|
+
* Design constraints:
|
|
9
|
+
* - Pure module: no I/O, no require() calls, no side effects.
|
|
10
|
+
* - All inputs are validated; unknown values throw a TypeError.
|
|
11
|
+
* - Consumed by the `gsd-tools drift-guard` CLI seam and by tests.
|
|
12
|
+
*
|
|
13
|
+
* Authority ladder (rung values determine MISSING severity):
|
|
14
|
+
* grep=0 intel=1 treesitter=2 lsp=3 scip=4
|
|
15
|
+
*
|
|
16
|
+
* Hard-block threshold: rung >= 3 (lsp, scip) — these adapters can prove
|
|
17
|
+
* absence, so MISSING is a definite error (severity HIGH, hardBlock true).
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.AUTHORITY_RUNGS = void 0;
|
|
21
|
+
exports.getEffectiveAuthority = getEffectiveAuthority;
|
|
22
|
+
exports.classifyDriftSeverity = classifyDriftSeverity;
|
|
23
|
+
/**
|
|
24
|
+
* Frozen map from authority name to its rung number.
|
|
25
|
+
*
|
|
26
|
+
* Rung determines whether a MISSING symbol triggers a hard block:
|
|
27
|
+
* rung >= 3 (lsp, scip) → hard block; rung < 3 → acknowledgement only.
|
|
28
|
+
*/
|
|
29
|
+
exports.AUTHORITY_RUNGS = Object.freeze({
|
|
30
|
+
grep: 0,
|
|
31
|
+
intel: 1,
|
|
32
|
+
treesitter: 2,
|
|
33
|
+
lsp: 3,
|
|
34
|
+
scip: 4,
|
|
35
|
+
});
|
|
36
|
+
/** Rung at which MISSING transitions to hard-block (inclusive). */
|
|
37
|
+
const HARD_BLOCK_RUNG_THRESHOLD = 3;
|
|
38
|
+
const VALID_AUTHORITIES = new Set(Object.keys(exports.AUTHORITY_RUNGS));
|
|
39
|
+
const VALID_STATUSES = new Set(['VERIFIED', 'MISSING', 'AMBIGUOUS', 'UNCHECKABLE']);
|
|
40
|
+
/**
|
|
41
|
+
* Validate and return an authority value, normalising undefined to 'grep'.
|
|
42
|
+
*
|
|
43
|
+
* Throws TypeError for any non-null unknown string value so callers surface
|
|
44
|
+
* configuration errors at call time rather than silently defaulting.
|
|
45
|
+
*
|
|
46
|
+
* @param value - raw authority string from config or CLI arg
|
|
47
|
+
* @returns a validated Authority value
|
|
48
|
+
*/
|
|
49
|
+
function validateAuthority(value) {
|
|
50
|
+
if (value === undefined || value === null || value === '') {
|
|
51
|
+
return 'grep';
|
|
52
|
+
}
|
|
53
|
+
if (!VALID_AUTHORITIES.has(value)) {
|
|
54
|
+
throw new TypeError(`Unknown authority: ${JSON.stringify(value)}. ` +
|
|
55
|
+
`Valid values: ${[...VALID_AUTHORITIES].join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Return the effective authority after applying the ADR-22 auto-upgrade rule.
|
|
61
|
+
*
|
|
62
|
+
* Auto-upgrade rule: if the configured authority is 'grep' AND intel is
|
|
63
|
+
* enabled (`intelEnabled === true`), upgrade to 'intel'. All other authority
|
|
64
|
+
* values are returned unchanged regardless of intelEnabled.
|
|
65
|
+
*
|
|
66
|
+
* @param authority - configured authority (undefined → 'grep')
|
|
67
|
+
* @param intelEnabled - whether the intel capability is active in this project
|
|
68
|
+
* @returns the effective Authority after upgrade
|
|
69
|
+
* @throws TypeError if authority is not one of the five valid values
|
|
70
|
+
*/
|
|
71
|
+
function getEffectiveAuthority(authority, intelEnabled) {
|
|
72
|
+
const validated = validateAuthority(authority);
|
|
73
|
+
if (validated === 'grep' && intelEnabled === true) {
|
|
74
|
+
return 'intel';
|
|
75
|
+
}
|
|
76
|
+
return validated;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Classify a symbol verification result into a drift severity and hard-block flag.
|
|
80
|
+
*
|
|
81
|
+
* ADR-22 decision table:
|
|
82
|
+
*
|
|
83
|
+
* | Status | Authority rung | severity | hardBlock |
|
|
84
|
+
* |------------- |--------------- |----------------------- |---------- |
|
|
85
|
+
* | VERIFIED | any | 'none' | false |
|
|
86
|
+
* | MISSING | rung >= 3 | 'HIGH' | true |
|
|
87
|
+
* | MISSING | rung 0-2 | 'needs-acknowledgement'| false |
|
|
88
|
+
* | AMBIGUOUS | any | 'MEDIUM' | false |
|
|
89
|
+
* | UNCHECKABLE | any | 'INFO' | false |
|
|
90
|
+
*
|
|
91
|
+
* @param opts.status - verdict from the source-grounding adapter
|
|
92
|
+
* @param opts.authority - the effective authority adapter used
|
|
93
|
+
* @returns { severity, hardBlock }
|
|
94
|
+
* @throws TypeError for unknown status or authority values
|
|
95
|
+
*/
|
|
96
|
+
function classifyDriftSeverity({ status, authority, }) {
|
|
97
|
+
if (!VALID_STATUSES.has(status)) {
|
|
98
|
+
throw new TypeError(`Unknown status: ${JSON.stringify(status)}. ` +
|
|
99
|
+
`Valid values: ${[...VALID_STATUSES].join(', ')}`);
|
|
100
|
+
}
|
|
101
|
+
// authority validation (also catches unknown values)
|
|
102
|
+
const validatedAuthority = validateAuthority(authority);
|
|
103
|
+
const rung = exports.AUTHORITY_RUNGS[validatedAuthority];
|
|
104
|
+
switch (status) {
|
|
105
|
+
case 'VERIFIED':
|
|
106
|
+
return { severity: 'none', hardBlock: false };
|
|
107
|
+
case 'MISSING':
|
|
108
|
+
if (rung >= HARD_BLOCK_RUNG_THRESHOLD) {
|
|
109
|
+
return { severity: 'HIGH', hardBlock: true };
|
|
110
|
+
}
|
|
111
|
+
return { severity: 'needs-acknowledgement', hardBlock: false };
|
|
112
|
+
case 'AMBIGUOUS':
|
|
113
|
+
return { severity: 'MEDIUM', hardBlock: false };
|
|
114
|
+
case 'UNCHECKABLE':
|
|
115
|
+
return { severity: 'INFO', hardBlock: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -30,10 +30,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
30
30
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
31
31
|
};
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
-
exports.VALID_STATUS = void 0;
|
|
33
|
+
exports.PROHIBITION_VALIDATORS = exports.VALID_STATUS = void 0;
|
|
34
34
|
exports.validateRequirement = validateRequirement;
|
|
35
35
|
exports.validateResolution = validateResolution;
|
|
36
36
|
exports.analyzeCoverage = analyzeCoverage;
|
|
37
|
+
exports.validateProhibitionResolution = validateProhibitionResolution;
|
|
38
|
+
exports.projectProhibitions = projectProhibitions;
|
|
39
|
+
exports.dispositionForProhibition = dispositionForProhibition;
|
|
37
40
|
exports.runProbeCli = runProbeCli;
|
|
38
41
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
39
42
|
/** The LOCKED set of valid lifecycle statuses (the re-cut: no covered/backstop). */
|
|
@@ -203,6 +206,119 @@ function analyzeCoverage(items, resolutions = [], validators) {
|
|
|
203
206
|
}
|
|
204
207
|
return { items: merged, coverage: { applicable, resolved, unresolved, byVerification } };
|
|
205
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The prohibition adapter's injected runtime validators (ADR-550 #5). There is no closed
|
|
211
|
+
* category taxonomy (recall is open-vocabulary values/safety/ethics prose), so `categories`
|
|
212
|
+
* is intentionally empty — `analyzeCoverage` is not the prohibition entry point and the
|
|
213
|
+
* round-trip schema layer does not gate on category. The verification tiers are
|
|
214
|
+
* `test | judgment` (ADR-550 D7a); both require only a present `resolution`/`reason` per their
|
|
215
|
+
* lifecycle (a resolved prohibition's checkable content is the `statement`, validated by the
|
|
216
|
+
* schema layer, not a `resolution` string), so `requiredFieldsByVerification` is the minimal
|
|
217
|
+
* fail-closed set: a dismissed item still needs its reason (enforced by `validateResolution`).
|
|
218
|
+
*/
|
|
219
|
+
exports.PROHIBITION_VALIDATORS = {
|
|
220
|
+
categories: [],
|
|
221
|
+
verification: ['test', 'judgment'],
|
|
222
|
+
// A resolved prohibition's checkable content is the `statement` (schema-layer validated), NOT a
|
|
223
|
+
// `resolution` string — the canonical fixtures and the reference doc's worked examples all carry
|
|
224
|
+
// `resolution: null`. So the per-tier required set is empty: `resolved` still requires a present
|
|
225
|
+
// verification tier (enforced in validateResolution) and `dismissed` still requires a reason
|
|
226
|
+
// (enforced unconditionally), but neither tier requires a `resolution`. This matches the corpus
|
|
227
|
+
// the docs-fixtures parity test pins; the validators.test.cjs regression keeps them aligned.
|
|
228
|
+
requiredFieldsByVerification: { test: [], judgment: [] },
|
|
229
|
+
};
|
|
230
|
+
/** Validate a prohibition resolution against the prohibition verification vocabulary. */
|
|
231
|
+
function validateProhibitionResolution(resolution) {
|
|
232
|
+
return validateResolution(resolution, exports.PROHIBITION_VALIDATORS);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Deterministically project resolved prohibition items into the `must_haves.prohibitions:`
|
|
236
|
+
* list shape (the SPEC<->plan projection; ADR-550 Decision 5c). This is a FUNCTION the parity
|
|
237
|
+
* assertion round-trips, never a prompt: the same input always yields the same output, and the
|
|
238
|
+
* output is the exact re-readable block shape `parseMustHavesBlock(content, 'prohibitions')`
|
|
239
|
+
* returns — `{ statement, status, verification }` plus `reason` only when present (a dismissed
|
|
240
|
+
* item's audit trail). `resolution`/`requirement_id`/`category` are recall-stage bookkeeping
|
|
241
|
+
* and are intentionally NOT projected into the plan block (which is keyed on the must-NOT
|
|
242
|
+
* statement, not the source requirement). A non-array input projects to `[]` (fail-soft on the
|
|
243
|
+
* empty/zero-prohibition case), never a throw.
|
|
244
|
+
*/
|
|
245
|
+
function projectProhibitions(items) {
|
|
246
|
+
if (!Array.isArray(items))
|
|
247
|
+
return [];
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const item of items) {
|
|
250
|
+
if (item == null || typeof item !== 'object')
|
|
251
|
+
continue;
|
|
252
|
+
const p = item;
|
|
253
|
+
const statement = typeof p.statement === 'string' ? p.statement : '';
|
|
254
|
+
const entry = {
|
|
255
|
+
statement,
|
|
256
|
+
status: typeof p.status === 'string' ? p.status : 'unresolved',
|
|
257
|
+
};
|
|
258
|
+
if (p.verification != null)
|
|
259
|
+
entry.verification = String(p.verification);
|
|
260
|
+
if (p.reason != null && String(p.reason).trim())
|
|
261
|
+
entry.reason = String(p.reason);
|
|
262
|
+
out.push(entry);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Deterministic verify-time disposition for a single prohibition — the FAIL-CLOSED default
|
|
268
|
+
* (ADR-550 Decision 5d, the safety half of the 2026-06-12 "B-with-guard" maintainer decision).
|
|
269
|
+
*
|
|
270
|
+
* This is the cheap safety guarantee: a well-formed prohibition that reaches verify-phase with NO
|
|
271
|
+
* wired enforcement evidence can NEVER be a silent pass. It is `{ status: 'unverified', flagged:
|
|
272
|
+
* true }` — never `green` — exactly like an unresolved judgment item. The HEAVY half (a real
|
|
273
|
+
* fail-first negative-test enforcement mechanism that, given evidence, would flip a test-tier item
|
|
274
|
+
* to green) is OUT of #644 scope and defers to a follow-up PR: #644's corpus is entirely
|
|
275
|
+
* judgment-tier, so wiring a contrived test-tier consumer here would be the delete-bad-tests /
|
|
276
|
+
* gold-plating failure mode. Until that follow-up lands, ANY prohibition without enforcement
|
|
277
|
+
* evidence — test- or judgment-tier — disposes as flagged-unverified.
|
|
278
|
+
*
|
|
279
|
+
* The function is pure: same input always yields the same disposition (no LLM judgment, ADR-550
|
|
280
|
+
* D5). The LLM-judge soft-gate for judgment-tier items is a verify-phase PROSE concern (the
|
|
281
|
+
* verifier records a non-authoritative verdict + the unverified-prohibition flag); this helper
|
|
282
|
+
* only owns the deterministic fail-closed default that the plan-01-01 CI safety assertion pins.
|
|
283
|
+
*/
|
|
284
|
+
function dispositionForProhibition(prohibition, context = {}) {
|
|
285
|
+
const p = (prohibition ?? {});
|
|
286
|
+
const tier = p.verification === 'test' || p.verification === 'judgment' ? p.verification : null;
|
|
287
|
+
const evidence = Array.isArray(context.enforcementEvidence) ? context.enforcementEvidence : [];
|
|
288
|
+
const hasEnforcement = evidence.length > 0;
|
|
289
|
+
// FAIL CLOSED: no wired enforcement evidence -> flagged unverified, never green. This holds for
|
|
290
|
+
// every tier today (the real enforcement mechanism that could flip a test-tier item to green is
|
|
291
|
+
// deferred to a follow-up PR). The guard the safety assertion proves: an unwired item can never
|
|
292
|
+
// be silently skipped.
|
|
293
|
+
if (!hasEnforcement) {
|
|
294
|
+
return {
|
|
295
|
+
status: 'unverified',
|
|
296
|
+
flagged: true,
|
|
297
|
+
tier,
|
|
298
|
+
reason: tier === 'test'
|
|
299
|
+
? 'test-tier prohibition has no wired enforcement evidence — flagged unverified (fail-closed; real negative-test enforcement deferred to a follow-up PR, ADR-550 D5d)'
|
|
300
|
+
: 'prohibition has no enforcement evidence — flagged unverified (fail-closed; never a silent pass, ADR-550 D5d)',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// D4 GUARD: a judgment-tier (or unknown-tier) prohibition is NEVER a silent green from this
|
|
304
|
+
// deterministic helper — it always routes to human/LLM judgment review (ADR-550 D4; verify-phase.md).
|
|
305
|
+
// Only a test-tier item with wired enforcement evidence may go green, and even that is the deferred
|
|
306
|
+
// heavy half until the real negative-test enforcement mechanism lands (no #644 caller passes evidence).
|
|
307
|
+
if (tier === 'test') {
|
|
308
|
+
return {
|
|
309
|
+
status: 'green',
|
|
310
|
+
flagged: false,
|
|
311
|
+
tier,
|
|
312
|
+
reason: 'test-tier prohibition has wired enforcement evidence',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
status: 'unverified',
|
|
317
|
+
flagged: true,
|
|
318
|
+
tier,
|
|
319
|
+
reason: 'judgment-tier prohibition routes to judgment review — never a silent green (ADR-550 D4)',
|
|
320
|
+
};
|
|
321
|
+
}
|
|
206
322
|
/**
|
|
207
323
|
* Read the requirements file (and optional resolutions file), run the adapter's `analyze`,
|
|
208
324
|
* and write the report as pretty JSON + newline. With no requirements path, writes the usage
|