@opengsd/gsd-core 1.20.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +265 -161
- package/commands/gsd/autonomous.md +1 -1
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +554 -823
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +99 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +20 -18
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +20 -28
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +170 -45
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +103 -46
- package/gsd-core/bin/lib/state.cjs +448 -63
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +168 -13
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +4 -3
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +9 -6
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +168 -13
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
// of the same repo are separate directories on disk, so each gets its own
|
|
30
30
|
// sentinel with no cross-worktree collision, no lock file needed.
|
|
31
31
|
//
|
|
32
|
+
// #5682: the single slot above is last-writer-wins, so two orchestrators sharing one checkout
|
|
33
|
+
// overwrote each other's decision and the guard discarded the foreign record. The writer now ALSO
|
|
34
|
+
// records each plan-scoped decision at `<cwd>/.gsd/dispatch-isolation/<phase>--<plan>.json`
|
|
35
|
+
// (same payload, same cwd-derived location and forgery posture), and `readSentinel` consults the
|
|
36
|
+
// record keyed by the dispatch's own phase/plan first, falling back to the single slot.
|
|
37
|
+
//
|
|
32
38
|
// #3045 SECURITY F3 (LOW, accepted risk — documented, not redesigned for):
|
|
33
39
|
// the sentinel lives at a fixed, predictable, gitignored path inside the
|
|
34
40
|
// PRIMARY checkout. An agent already running inside a legitimately isolated
|
|
@@ -64,6 +70,35 @@ const VALID_ISOLATION = new Set(['harness-worktree', 'orchestrator-worktree', 'n
|
|
|
64
70
|
|
|
65
71
|
const SENTINEL_RELATIVE_PATH = path.join('.gsd', 'dispatch-isolation-sentinel.json');
|
|
66
72
|
|
|
73
|
+
// #5682: per-dispatch records, one file per `<phase>--<plan>` under this directory.
|
|
74
|
+
const KEYED_SENTINEL_DIR = path.join('.gsd', 'dispatch-isolation');
|
|
75
|
+
const SAFE_SENTINEL_KEY_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
76
|
+
|
|
77
|
+
// #5758: a real sentinel payload is under 300 bytes. Anything larger is not one, and the cap bounds
|
|
78
|
+
// the read so a hostile or runaway record cannot be pulled into the hook.
|
|
79
|
+
const SENTINEL_MAX_BYTES = 4096;
|
|
80
|
+
|
|
81
|
+
/** #5682: strict allow-list for a phase/plan string that becomes a path segment. */
|
|
82
|
+
function isSafeSentinelKey(s) {
|
|
83
|
+
return typeof s === 'string' && SAFE_SENTINEL_KEY_RE.test(s) && s !== '.' && s !== '..';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** #5682: keyed record path for a dispatch, or null unless BOTH phase and plan are safe keys. */
|
|
87
|
+
function keyedSentinelPath(root, phase, plan) {
|
|
88
|
+
if (!isSafeSentinelKey(phase) || !isSafeSentinelKey(plan)) return null;
|
|
89
|
+
return path.join(root, KEYED_SENTINEL_DIR, `${phase}--${plan}.json`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* #5682: phase-level keyed record path (`<phase>.json`, written when a decision carries no plan),
|
|
94
|
+
* or null unless the phase is a safe key. Two orchestrators recording plan-less decisions for
|
|
95
|
+
* different phases each keep their own file.
|
|
96
|
+
*/
|
|
97
|
+
function phaseSentinelPath(root, phase) {
|
|
98
|
+
if (!isSafeSentinelKey(phase)) return null;
|
|
99
|
+
return path.join(root, KEYED_SENTINEL_DIR, `${phase}.json`);
|
|
100
|
+
}
|
|
101
|
+
|
|
67
102
|
// #3045 SECURITY F2 fix: how long a written sentinel is trusted as "this
|
|
68
103
|
// dispatch's decision" before a reader falls back to the conservative
|
|
69
104
|
// registry+config check.
|
|
@@ -103,10 +138,10 @@ function sentinelPath(cwd) {
|
|
|
103
138
|
/**
|
|
104
139
|
* Resolve the project root a sentinel should be read from/written to, using
|
|
105
140
|
* the SAME derivation gsd-tools.cjs's dispatcher applies to every `--cwd`
|
|
106
|
-
* before invoking a route handler: `
|
|
107
|
-
* (gsd-core/bin/gsd-tools.cjs main()
|
|
108
|
-
*
|
|
109
|
-
*
|
|
141
|
+
* before invoking a route handler: `resolveProjectContext(cwd).projectRoot`
|
|
142
|
+
* (gsd-core/bin/gsd-tools.cjs main() — `record-dispatch-isolation` and
|
|
143
|
+
* `dispatch-isolation` are not in SKIP_ROOT_RESOLUTION, so every write goes
|
|
144
|
+
* through it).
|
|
110
145
|
*
|
|
111
146
|
* #3045 MINOR fix: the guard hooks previously read the sentinel from the raw
|
|
112
147
|
* `data.cwd` / `workspace_roots[i]` the harness reports, with NO equivalent
|
|
@@ -119,7 +154,7 @@ function sentinelPath(cwd) {
|
|
|
119
154
|
* written where it looked. Deriving both sides through this one function
|
|
120
155
|
* closes that divergence.
|
|
121
156
|
*
|
|
122
|
-
* `
|
|
157
|
+
* `resolveProjectContext` is read from the sibling
|
|
123
158
|
* `gsd-core/bin/lib/*.cjs` modules staged alongside these hooks at install
|
|
124
159
|
* time (same pattern the guard hooks already use for
|
|
125
160
|
* capability-registry.cjs/runtime-name-policy.cjs) — two directories up from
|
|
@@ -145,10 +180,10 @@ function resolveSentinelRoot(cwd) {
|
|
|
145
180
|
// cold-tree encounter.
|
|
146
181
|
const { ensureRuntimeBuild } = require('../../gsd-core/bin/ensure-runtime-build.cjs');
|
|
147
182
|
ensureRuntimeBuild();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const {
|
|
151
|
-
return
|
|
183
|
+
// #6043 (ADR-4908): the dispatcher resolves non-skip commands through the
|
|
184
|
+
// same seam, so the reader looks where the writer wrote.
|
|
185
|
+
const { resolveProjectContext } = require('../../gsd-core/bin/lib/project-context.cjs');
|
|
186
|
+
return resolveProjectContext(cwd).projectRoot;
|
|
152
187
|
} catch {
|
|
153
188
|
return cwd;
|
|
154
189
|
}
|
|
@@ -174,14 +209,129 @@ function resolveSentinelRoot(cwd) {
|
|
|
174
209
|
* per the repo's clock-seam convention, so staleness is testable without
|
|
175
210
|
* asserting on wall-clock time.
|
|
176
211
|
*/
|
|
177
|
-
function readSentinel(cwd, { clock = Date } = {}) {
|
|
212
|
+
function readSentinel(cwd, { clock = Date, dispatchIds = null } = {}) {
|
|
178
213
|
const root = resolveSentinelRoot(cwd);
|
|
179
|
-
|
|
214
|
+
|
|
215
|
+
// #5682: candidates are the record keyed by this dispatch's own phase/plan, the phase-level
|
|
216
|
+
// record for its phase, and the single legacy slot. A keyed candidate counts only when it is
|
|
217
|
+
// well-formed and its contents agree with its own filename key; the legacy slot only when it
|
|
218
|
+
// applies to this dispatch. Among FRESH candidates the latest `writtenAt` wins, so a newer
|
|
219
|
+
// phase-level decision is not shadowed by an older plan-scoped one (and vice versa).
|
|
220
|
+
const gsdDir = path.join(root, '.gsd');
|
|
221
|
+
const keyedDirs = [gsdDir, path.join(root, KEYED_SENTINEL_DIR)];
|
|
222
|
+
const keyedCandidates = [];
|
|
223
|
+
if (dispatchIds) {
|
|
224
|
+
const planFile = keyedSentinelPath(root, dispatchIds.phase, dispatchIds.plan);
|
|
225
|
+
if (planFile) {
|
|
226
|
+
const c = parseSentinelFile(planFile, clock, keyedDirs);
|
|
227
|
+
if (c.present && !c.malformed && c.phase === dispatchIds.phase && c.plan === dispatchIds.plan) keyedCandidates.push(c);
|
|
228
|
+
}
|
|
229
|
+
const phaseFile = phaseSentinelPath(root, dispatchIds.phase);
|
|
230
|
+
if (phaseFile) {
|
|
231
|
+
const c = parseSentinelFile(phaseFile, clock, keyedDirs);
|
|
232
|
+
if (c.present && !c.malformed && c.phase === dispatchIds.phase && c.plan === null) keyedCandidates.push(c);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const legacy = parseSentinelFile(sentinelPath(root), clock, [gsdDir]);
|
|
237
|
+
const legacyApplies = legacy.present && !legacy.malformed && sentinelAppliesToDispatch(legacy, dispatchIds);
|
|
238
|
+
|
|
239
|
+
const fresh = keyedCandidates.filter((c) => !c.stale);
|
|
240
|
+
if (legacyApplies && !legacy.stale) fresh.push(legacy);
|
|
241
|
+
if (fresh.length > 0) return latestWritten(fresh);
|
|
242
|
+
|
|
243
|
+
// Nothing fresh: a stale keyed record still feeds the aged-none path; otherwise the legacy
|
|
244
|
+
// slot is returned exactly as before.
|
|
245
|
+
if (keyedCandidates.length > 0) return latestWritten(keyedCandidates);
|
|
246
|
+
return legacy;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The candidate with the greatest `writtenAt` (ties keep the earlier-listed, i.e. most specific). */
|
|
250
|
+
function latestWritten(candidates) {
|
|
251
|
+
return candidates.reduce((best, c) => (c.writtenAt > best.writtenAt ? c : best));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* lstat of `dir` when it is a real directory, not a link (lstat does not follow); otherwise null.
|
|
256
|
+
* Never throws.
|
|
257
|
+
*/
|
|
258
|
+
function lstatRealDirectory(dir) {
|
|
180
259
|
try {
|
|
181
|
-
|
|
260
|
+
// bigint: an NTFS file id is 64 bits and a JS number keeps 53, so ids are compared exactly.
|
|
261
|
+
const st = fs.lstatSync(dir, { bigint: true });
|
|
262
|
+
return st.isDirectory() && !st.isSymbolicLink() ? st : null;
|
|
182
263
|
} catch {
|
|
183
|
-
return
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Same filesystem object: equal dev and ino. A zero ino (some Windows volumes) cannot be compared. */
|
|
269
|
+
function sameObject(a, b) {
|
|
270
|
+
return a.ino === 0n || b.ino === 0n || (a.dev === b.dev && a.ino === b.ino);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* #5758: read one sentinel file as a bounded, non-blocking, link-refusing read of a regular file.
|
|
275
|
+
* Returns the text, or null for EVERY refusal (missing, link, FIFO, device, directory, oversized,
|
|
276
|
+
* short read, a hard link to another file, a parent directory that is a link), so a hostile record
|
|
277
|
+
* is exactly as absent as a missing one. `readFileSync` would block forever on a FIFO with no
|
|
278
|
+
* writer, follow a link to an endless device or to a file outside the project, and read without
|
|
279
|
+
* bound. The writers only ever produce a fresh temp file renamed into place (link count 1). A
|
|
280
|
+
* sentinel renamed over between the open and the fstat reads as nlink 0 and is refused for that one
|
|
281
|
+
* dispatch, which is the conservative no-sentinel decision.
|
|
282
|
+
*
|
|
283
|
+
* Parent directories are lstat-checked before the open and re-checked (same dev/ino, still a real
|
|
284
|
+
* directory) after it, and the opened file must be the object the pre-open lstat saw. Node has no
|
|
285
|
+
* openat, so this NARROWS the window for a parent swapped to a link between check and open; it does
|
|
286
|
+
* not close it.
|
|
287
|
+
*/
|
|
288
|
+
function readSentinelText(file, parentDirs) {
|
|
289
|
+
const parents = [];
|
|
290
|
+
for (const dir of parentDirs) {
|
|
291
|
+
const st = lstatRealDirectory(dir);
|
|
292
|
+
if (!st) return null;
|
|
293
|
+
parents.push({ dir, st });
|
|
184
294
|
}
|
|
295
|
+
let fd;
|
|
296
|
+
try {
|
|
297
|
+
const before = fs.lstatSync(file, { bigint: true });
|
|
298
|
+
if (!before.isFile() || before.size > BigInt(SENTINEL_MAX_BYTES)) return null;
|
|
299
|
+
// O_NONBLOCK makes an open of a FIFO swapped in after the lstat return at once; O_NOFOLLOW
|
|
300
|
+
// refuses a link swapped in. Windows lacks both flags (`|| 0`); lstat/fstat still guard.
|
|
301
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0) | (fs.constants.O_NONBLOCK || 0));
|
|
302
|
+
const st = fs.fstatSync(fd, { bigint: true });
|
|
303
|
+
if (!st.isFile() || st.nlink !== 1n || st.size > BigInt(SENTINEL_MAX_BYTES)) return null;
|
|
304
|
+
if (!sameObject(before, st)) return null;
|
|
305
|
+
const size = Number(st.size);
|
|
306
|
+
for (const p of parents) {
|
|
307
|
+
const after = lstatRealDirectory(p.dir);
|
|
308
|
+
if (!after || !sameObject(p.st, after)) return null;
|
|
309
|
+
}
|
|
310
|
+
const buf = Buffer.alloc(size);
|
|
311
|
+
let offset = 0;
|
|
312
|
+
while (offset < size) {
|
|
313
|
+
const n = fs.readSync(fd, buf, offset, size - offset, offset);
|
|
314
|
+
if (n === 0) return null;
|
|
315
|
+
offset += n;
|
|
316
|
+
}
|
|
317
|
+
return buf.toString('utf-8');
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
} finally {
|
|
321
|
+
if (fd !== undefined) {
|
|
322
|
+
try { fs.closeSync(fd); } catch { /* nothing left to release */ }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Parse and validate one sentinel file. Never throws. Shape: see `readSentinel`.
|
|
329
|
+
* `parentDirs` are the directories between the project root and the file that must be real
|
|
330
|
+
* directories (not links); a refusal anywhere reads as `{ present: false }`.
|
|
331
|
+
*/
|
|
332
|
+
function parseSentinelFile(file, clock, parentDirs = []) {
|
|
333
|
+
const raw = readSentinelText(file, parentDirs);
|
|
334
|
+
if (raw === null) return { present: false };
|
|
185
335
|
|
|
186
336
|
let parsed;
|
|
187
337
|
try {
|
|
@@ -348,7 +498,12 @@ function buildSentinelDiscard(sentinel, dispatchIds) {
|
|
|
348
498
|
module.exports = {
|
|
349
499
|
VALID_ISOLATION,
|
|
350
500
|
SENTINEL_RELATIVE_PATH,
|
|
501
|
+
KEYED_SENTINEL_DIR,
|
|
502
|
+
isSafeSentinelKey,
|
|
503
|
+
keyedSentinelPath,
|
|
504
|
+
phaseSentinelPath,
|
|
351
505
|
SENTINEL_STALE_MS,
|
|
506
|
+
SENTINEL_MAX_BYTES,
|
|
352
507
|
AGED_NONE_MAX_MS,
|
|
353
508
|
sentinelPath,
|
|
354
509
|
resolveSentinelRoot,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// gsd-hook-version: {{GSD_VERSION}}
|
|
2
|
+
// Project exemptions for the secret-read guard (#4432).
|
|
3
|
+
//
|
|
4
|
+
// A project lists repo-relative files under `hooks.secret_read_exempt` in the
|
|
5
|
+
// ROOT `.planning/config.json`. A listed file stays readable only when it is
|
|
6
|
+
// BOTH a regular file in the working tree (lstat, so a symlink never counts)
|
|
7
|
+
// AND a regular blob (100644/100755) in the commit HEAD names. Config alone is
|
|
8
|
+
// not the key: an agent that can write the config must not be able to unlock a
|
|
9
|
+
// real secret that was never committed.
|
|
10
|
+
//
|
|
11
|
+
// The committed-file check reads `.git` directly (./git-head-tree.js): no
|
|
12
|
+
// subprocess and no timer, so the verdict depends only on the repository's
|
|
13
|
+
// bytes. Every failure means "not exempt"; nothing in this module throws.
|
|
14
|
+
//
|
|
15
|
+
// Standalone on purpose: hooks are installed as loose files and cannot require
|
|
16
|
+
// `src/`. The entry rules are duplicated in `src/config.cts`
|
|
17
|
+
// (`normalizeSecretReadExemptEntry`, used by config-set); tests/
|
|
18
|
+
// secret-read-exemptions.test.cjs drives one corpus through both so they
|
|
19
|
+
// cannot diverge.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { headTreeEntry } = require('./git-head-tree.js');
|
|
26
|
+
|
|
27
|
+
const REGULAR_BLOB_MODES = new Set(['100644', '100755']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalize one config entry to a POSIX repo-relative file path, or null.
|
|
31
|
+
* `\` becomes `/` unconditionally; nothing is trimmed (whitespace is part of a
|
|
32
|
+
* name). Rejected: non-strings, absolute and UNC paths, drive prefixes, NUL, any
|
|
33
|
+
* `..` segment, a directory (trailing `/`), and `.`.
|
|
34
|
+
* @param {unknown} entry
|
|
35
|
+
* @returns {string | null}
|
|
36
|
+
*/
|
|
37
|
+
function normalizeExemptEntry(entry) {
|
|
38
|
+
if (typeof entry !== 'string') return null;
|
|
39
|
+
let s = entry.replace(/\\/g, '/');
|
|
40
|
+
while (s.startsWith('./')) s = s.slice(2);
|
|
41
|
+
if (s === '' || s === '.') return null;
|
|
42
|
+
if (s.startsWith('/') || s.endsWith('/')) return null;
|
|
43
|
+
if (/^[A-Za-z]:/.test(s)) return null;
|
|
44
|
+
if (s.includes('\0')) return null;
|
|
45
|
+
if (s.split('/').includes('..')) return null;
|
|
46
|
+
return s;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The normalized, de-duplicated `hooks.secret_read_exempt` entries of
|
|
51
|
+
* `<root>/.planning/config.json`. Anything unreadable or malformed yields [].
|
|
52
|
+
* @param {string} root
|
|
53
|
+
* @param {{ readFile?: (file: string, encoding: string) => string }} [deps]
|
|
54
|
+
* @returns {string[]}
|
|
55
|
+
*/
|
|
56
|
+
function readExemptEntries(root, { readFile = fs.readFileSync } = {}) {
|
|
57
|
+
let config;
|
|
58
|
+
try {
|
|
59
|
+
config = JSON.parse(readFile(path.join(root, '.planning', 'config.json'), 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) return [];
|
|
64
|
+
const hooks = config.hooks;
|
|
65
|
+
if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks)) return [];
|
|
66
|
+
const list = hooks.secret_read_exempt;
|
|
67
|
+
if (!Array.isArray(list)) return [];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const raw of list) {
|
|
70
|
+
const entry = normalizeExemptEntry(raw);
|
|
71
|
+
if (entry !== null && !out.includes(entry)) out.push(entry);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* True when HEAD holds `entry` (relative to `root`) as a regular blob.
|
|
78
|
+
* @param {string} root
|
|
79
|
+
* @param {string} entry
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isCommittedBlob(root, entry) {
|
|
83
|
+
const found = headTreeEntry(root, entry);
|
|
84
|
+
return found !== null && REGULAR_BLOB_MODES.has(found.mode);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* True when `abs` is a regular file (lstat: a symlink is not). When `root` and
|
|
89
|
+
* `entry` are given, the real path of `abs` must also equal the real root joined
|
|
90
|
+
* with `entry`, so a symlinked PARENT directory (which lstat of the leaf does
|
|
91
|
+
* not see) is never exempt. A realpath failure means "not a match".
|
|
92
|
+
* @param {string} abs
|
|
93
|
+
* @param {string} [root]
|
|
94
|
+
* @param {string} [entry]
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
function isRegularFile(abs, root, entry) {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.lstatSync(abs).isFile()) return false;
|
|
100
|
+
if (typeof root !== 'string' || typeof entry !== 'string') return true;
|
|
101
|
+
const fold = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
102
|
+
// .native: canonical long form on Windows (see sameRealPath in git-head-tree.js).
|
|
103
|
+
return fold(fs.realpathSync.native(abs)) === fold(path.join(fs.realpathSync.native(root), entry));
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the per-invocation checker. Pure apart from the two injected probes,
|
|
111
|
+
* which are memoized per entry. A probe that throws means "not exempt".
|
|
112
|
+
* @returns {(token: unknown) => boolean}
|
|
113
|
+
*/
|
|
114
|
+
function createExemptionChecker({
|
|
115
|
+
root,
|
|
116
|
+
entries,
|
|
117
|
+
allowRelative = false,
|
|
118
|
+
platform = process.platform,
|
|
119
|
+
isCommittedBlob: committed = isCommittedBlob,
|
|
120
|
+
isRegularFile: regular = isRegularFile,
|
|
121
|
+
}) {
|
|
122
|
+
const fold = platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
|
|
123
|
+
const targets = [];
|
|
124
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
125
|
+
if (typeof entry === 'string' && entry !== '') targets.push({ entry, target: fold(path.resolve(root, entry)) });
|
|
126
|
+
}
|
|
127
|
+
const verdicts = new Map();
|
|
128
|
+
|
|
129
|
+
function verdictFor(entry) {
|
|
130
|
+
if (verdicts.has(entry)) return verdicts.get(entry);
|
|
131
|
+
let ok = false;
|
|
132
|
+
try {
|
|
133
|
+
ok = regular(path.resolve(root, entry), root, entry) === true && committed(root, entry) === true;
|
|
134
|
+
} catch {
|
|
135
|
+
ok = false;
|
|
136
|
+
}
|
|
137
|
+
verdicts.set(entry, ok);
|
|
138
|
+
return ok;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return function isExempt(token) {
|
|
142
|
+
if (typeof token !== 'string' || token === '' || targets.length === 0) return false;
|
|
143
|
+
if (!allowRelative && !path.isAbsolute(token)) return false;
|
|
144
|
+
// path.resolve would collapse `<symlinkdir>/../x` lexically while the OS
|
|
145
|
+
// reads through the link, so a `..` segment is refused before resolving.
|
|
146
|
+
if (token.split(/[\\/]/).includes('..')) return false;
|
|
147
|
+
const resolved = fold(path.resolve(root, token));
|
|
148
|
+
for (const { entry, target } of targets) {
|
|
149
|
+
if (resolved === target) return verdictFor(entry);
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
normalizeExemptEntry,
|
|
157
|
+
readExemptEntries,
|
|
158
|
+
createExemptionChecker,
|
|
159
|
+
isCommittedBlob,
|
|
160
|
+
isRegularFile,
|
|
161
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengsd/gsd-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
|
|
5
5
|
"main": ".opencode/plugins/gsd-core.js",
|
|
6
6
|
"bin": {
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"url": "git+https://github.com/open-gsd/gsd-core.git"
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/open-gsd/gsd-core",
|
|
64
|
+
"funding": {
|
|
65
|
+
"type": "github",
|
|
66
|
+
"url": "https://github.com/sponsors/trek-e"
|
|
67
|
+
},
|
|
64
68
|
"bugs": {
|
|
65
69
|
"url": "https://github.com/open-gsd/gsd-core/issues"
|
|
66
70
|
},
|
|
@@ -126,6 +130,7 @@
|
|
|
126
130
|
"gen:registry": "node scripts/gen-registry.cjs --write",
|
|
127
131
|
"gen:install-tree": "node scripts/gen-install-tree-fixtures.cjs",
|
|
128
132
|
"gen:section-manifest": "node scripts/gen-section-manifest.cjs --write",
|
|
133
|
+
"gen:cli-surface-manifest": "node scripts/gen-cli-surface-manifest.cjs --write",
|
|
129
134
|
"regen:derived": "npm run build && npm run gen:registry && node scripts/gen-adr-index.cjs --write && node scripts/gen-features.cjs --write && node scripts/gen-capability-matrix.cjs --write && node scripts/gen-inventory-manifest.cjs --write && node scripts/gen-context-index.cjs --write && node scripts/gen-state-md-docs.cjs --write && npm run gen:section-manifest && node scripts/sync-manifest-versions.cjs && npm run gen:install-tree && node scripts/gen-scripts-cli-exit.cjs --write && node scripts/gen-hooks-cli-exit.cjs --write && node scripts/gen-exit-code-registry.cjs --write && node scripts/gen-exit-code-docs.cjs --write && node scripts/gen-platform-conformance-tier.cjs --write && node scripts/gen-platform-conformance-tier.cjs --target macos --write",
|
|
130
135
|
"validate:registry": "node scripts/validate-registry.cjs",
|
|
131
136
|
"prepack": "npm run build:lib",
|
|
@@ -154,7 +159,7 @@
|
|
|
154
159
|
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
|
155
160
|
"lint:pr-checks": "node scripts/lint-pr-check-project-dir.cjs",
|
|
156
161
|
"lint:changeset": "node scripts/changeset/lint.cjs",
|
|
157
|
-
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-pi-package-skills.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check && node scripts/gen-platform-conformance-tier.cjs --target macos --check",
|
|
162
|
+
"lint:generated-sync": "node scripts/gen-capability-registry.cjs --check && node scripts/gen-pi-package-skills.cjs --check && node scripts/gen-loop-host-contract.cjs --check && node scripts/gen-capability-matrix.cjs --check && node scripts/sync-manifest-versions.cjs --check && node scripts/gen-inventory-manifest.cjs --check && node scripts/generate-package-identity.cjs --check && node scripts/gen-plugin-skills.cjs --check && node scripts/gen-registry.cjs --check && node scripts/gen-adr-index.cjs --check && node scripts/gen-features.cjs --check && node scripts/check-glossary-refs.cjs --check && node scripts/lint-compiled-artifact-sync.cjs --check && node scripts/gen-context-index.cjs --check && node scripts/gen-section-manifest.cjs --check && node scripts/gen-health-docs.cjs --check && node scripts/gen-state-md-docs.cjs --check && node scripts/gen-scripts-cli-exit.cjs --check && node scripts/gen-hooks-cli-exit.cjs --check && node scripts/gen-exit-code-registry.cjs --check && node scripts/gen-exit-code-docs.cjs --check && node scripts/gen-platform-conformance-tier.cjs --check && node scripts/gen-platform-conformance-tier.cjs --target macos --check && node scripts/gen-cli-surface-manifest.cjs --check",
|
|
158
163
|
"lint:docs": "node scripts/lint-docs-required.cjs",
|
|
159
164
|
"lint:qa-smells": "node scripts/qa-smell-ratchet.cjs",
|
|
160
165
|
"lint:legacy-name": "node scripts/lint-legacy-dir-name.cjs",
|
|
@@ -107,7 +107,7 @@ PLAN_START_EPOCH=$(date +%s)
|
|
|
107
107
|
If running inside a git worktree, capture authoritative worktree identity before
|
|
108
108
|
any task commit changes HEAD. The execute-phase orchestrator consumes this from
|
|
109
109
|
your final `<worktree_metadata>` return block to build the wave cleanup manifest
|
|
110
|
-
without relying on runtime harness metadata (#1297).
|
|
110
|
+
without relying on runtime harness metadata (#1297). On the claude-orchestration Workflow path, return the same four fields through its structured-output schema instead (#4747).
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
113
|
GSD_WORKTREE_PATH=""
|
|
@@ -696,7 +696,7 @@ issue:
|
|
|
696
696
|
1. For each `<automated>` block piping a package-manager list command into grep with a `^` anchor: BLOCKER.
|
|
697
697
|
2. For each `<automated>` block containing `2>/dev/null || echo` where the result feeds a `[ "$VAR" = ... ]` comparison: BLOCKER.
|
|
698
698
|
3. For each `<automated>` block asserting a specific numeric count not cited as measured in this plan: WARNING.
|
|
699
|
-
4. For each count/match gate (`grep -c`, `grep -q`, `rg`), run it — read-only commands only — against the current tree (#4927).
|
|
699
|
+
4. For each count/match gate (`grep -c`, `grep -q`, `rg`), run it — read-only commands only — against the current tree (#4927). Its baseline is the `[HEAD=<value> EXPECT<op><value>]` notation beside it (#4309), read from `verify plan-structure`, never re-derived: a `baseline_findings` B1 (already true at HEAD, no `GUARD MUTATION:`) is a BLOCKER, as is a gate that already passes at HEAD with no notation, or whose printed value differs from its notation's `HEAD`. A free-form `# baseline at HEAD:` comment (B2) is not a baseline: WARNING. A literal differing in case from the heading/key the task writes: BLOCKER. Never run a gate that builds, installs, or writes. See `planner-verify-command-grounding.md`.
|
|
700
700
|
|
|
701
701
|
## Dimension: Verify Command Path Resolvability (#2401)
|
|
702
702
|
|
|
@@ -187,7 +187,7 @@ Every task has four required fields:
|
|
|
187
187
|
|
|
188
188
|
**Inherit the command that already worked (#2401):** reuse `prior_verify_commands` verbatim, prefer `npm --prefix <dir> run <script>`, ground every path you author. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
|
|
189
189
|
|
|
190
|
-
**Grep gate hygiene:** `grep -c` counts comments, so header prose can be self-invalidating. Use `grep -v '^#' | grep -c token`. Bare `== 0` gates on unfiltered files are forbidden. Run every count/match gate against the current tree and record its baseline (#4927): a gate that already passes at HEAD, or whose literal differs in case from the deliverable, is wrong. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
|
|
190
|
+
**Grep gate hygiene:** `grep -c` counts comments, so header prose can be self-invalidating. Use `grep -v '^#' | grep -c token`. Bare `== 0` gates on unfiltered files are forbidden. Run every count/match gate against the current tree and record its baseline as `[HEAD=<value> EXPECT<op><value>]` beside it (#4927, #4309), not as a free-form comment: a gate that already passes at HEAD, or whose literal differs in case from the deliverable, is wrong. @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-verify-command-grounding.md
|
|
191
191
|
|
|
192
192
|
<comment_text_discipline>
|
|
193
193
|
**Comment-text discipline (HARD GATE, #429):** A literal an acceptance criterion negative-greps for must NOT appear verbatim in any `<action>` body. Rules + `<!-- planner-discipline-allow: LIT -->` allowlist + examples: @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-antipatterns.md ("Comment-Text Discipline").
|
|
@@ -203,7 +203,7 @@ Every task has four required fields:
|
|
|
203
203
|
|
|
204
204
|
**<precondition>** (optional, one prose line): a runnable/checkable fact the task assumes that plan ordering does not guarantee — external setup (`user_setup`), a prior-phase artifact, or an env var. The executor asserts it before running the task and halts on unmet. Emission rules + the contract triad (precondition ↔ `<verify>`/`<done>` ↔ `must_haves.truths`): @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-preconditions.md.
|
|
205
205
|
|
|
206
|
-
**<reversibility>** (optional): `rating="reversible|costly|one-way"` + one-line rationale for a decision this task implements. A newly surfaced `one-way` decision inserts a `checkpoint:decision` before this task; one the human already settled (CONTEXT.md
|
|
206
|
+
**<reversibility>** (optional): `rating="reversible|costly|one-way"` + one-line rationale for a decision this task implements. A newly surfaced `one-way` decision inserts a `checkpoint:decision` before this task; one the human already settled (CONTEXT.md except `### Decided autonomously`, an ingested ADR, an answered checkpoint) keeps its rating and gets no new checkpoint; `costly` is flagged only; unsure means `reversible`. Rules: @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-reversibility.md
|
|
207
207
|
|
|
208
208
|
See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for Task Types table, Task Sizing rules, Interface-First Task Ordering, and Specificity guidance.
|
|
209
209
|
|
|
@@ -476,7 +476,7 @@ For each artifact: "What must be CONNECTED for this to function?"
|
|
|
476
476
|
**Step 5: Identify Key Links**
|
|
477
477
|
"Where is this most likely to break?" Key links = critical connections where breakage causes cascading failures.
|
|
478
478
|
|
|
479
|
-
See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for a worked example
|
|
479
|
+
See @__GSD_PI_ENGINE_ROOT__/gsd-core/references/planner-guidance.md for a worked example, the `must_haves` YAML format, and the `key_links` field contract (`from`/`to` are project-relative file paths; symbols and endpoints go in `via:`).
|
|
480
480
|
|
|
481
481
|
</goal_backward>
|
|
482
482
|
|
|
@@ -517,7 +517,7 @@ done
|
|
|
517
517
|
|
|
518
518
|
**Behavior-unverified truths (Step 3):** Every truth left ⚠️ PRESENT_BEHAVIOR_UNVERIFIED is recorded in the `behavior_unverified_items` frontmatter list (emitted whenever the count > 0, regardless of overall status, so it survives a gaps_found phase) and surfaces for human verification; when the overall status is human_needed it also appears in the human_verification section. Phrase each item around the invariant: what to trigger, what state must hold afterward, and why presence checks can't see it.
|
|
519
519
|
|
|
520
|
-
**Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are
|
|
520
|
+
**Harvest deferred items from PLAN.md (#3309 / `workflow.human_verify_mode = end-of-phase`):** Scan every PLAN file in the phase for `<verify><human-check>` blocks on `auto` tasks. These are items the planner deferred from `checkpoint:human-verify`. Shape:
|
|
521
521
|
|
|
522
522
|
```xml
|
|
523
523
|
<verify>
|
|
@@ -529,7 +529,7 @@ done
|
|
|
529
529
|
</verify>
|
|
530
530
|
```
|
|
531
531
|
|
|
532
|
-
Merge those harvested items into the same human verification list as your own analysis.
|
|
532
|
+
Merge those harvested items into the same human verification list as your own analysis. Harvest every block, even one whose `<expected>` already records a verdict: a recorded verdict is not a reason to omit the check, and `<expected>` is carried through unmodified. Deduplicate only against your own analysis (same check). The downstream `human_needed` → `{phase_num}-UAT.md` path in `workflows/execute-phase.md` is the single sink — no separate file is created.
|
|
533
533
|
|
|
534
534
|
**Format:**
|
|
535
535
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"gsd-domain-researcher.md": "f1b07d9c79dcb89c90e5af7f61adfa5dc73ebe8429783d8cb281d7ae2bdc682b",
|
|
17
17
|
"gsd-eval-auditor.md": "9d77e7329c52741e7e62185b566231eb872c6b34afa6c869960a922af573b3d5",
|
|
18
18
|
"gsd-eval-planner.md": "efc12a2822d77a5989db2e3efe83b7fda9f0509bb203910d7e26eb9369f89949",
|
|
19
|
-
"gsd-executor.md": "
|
|
19
|
+
"gsd-executor.md": "e0d9e81b7a967f88388871efebd6a6f02801a82f3bcbfe008b125a097f61591c",
|
|
20
20
|
"gsd-framework-selector.md": "96d52931468372ee88c4f1d551c13c97d2be8079053b55d992e0ea1720d3d5c9",
|
|
21
21
|
"gsd-integration-checker.md": "a233ab8c5c97b6099c649d1e9a09e88ff1ed1bf7df7c1b13bc766c33d58e09b9",
|
|
22
22
|
"gsd-intel-updater.md": "26c70b81580cc5bf713959bdcc84a16a35569861d2e79c07c71258ccd05ba9bc",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"gsd-nyquist-auditor.md": "e82d1562e1df663e4a2907bd85b4316d5ab76bfa9d413ac8daecbd6f854fa42d",
|
|
25
25
|
"gsd-pattern-mapper.md": "3f51a38c2a1e8657190ba88a314fb2c9e66e25f5ab0e3edbc4e2f649b2702d01",
|
|
26
26
|
"gsd-phase-researcher.md": "2215ab8205f1bcb8b826870a4b8e9a949b6f77778d1bf75fae5ce4a07dd459ba",
|
|
27
|
-
"gsd-plan-checker.md": "
|
|
28
|
-
"gsd-planner.md": "
|
|
27
|
+
"gsd-plan-checker.md": "0978a72430c052d486a14825fd39ebbceb26e8abd819f7311550cb0dfa40dd9f",
|
|
28
|
+
"gsd-planner.md": "8c788c8b6c3b68f26fd0420adccd75b4956911f3ceb1cea83517b88fa87db951",
|
|
29
29
|
"gsd-project-researcher.md": "ac7e0065778a3ec40e023af48faa57f715c3fa13900975ef7b8d50142e44cf3c",
|
|
30
30
|
"gsd-research-synthesizer.md": "d20735f8dc591af470c4cbb892284863c5976e2bd8a77512bf146469158d051a",
|
|
31
31
|
"gsd-roadmapper.md": "c5cf5fa9df6bcbd7acdc1ec62c20b6e713ed6b6f2b032b17ca3044eebd0626d3",
|
|
@@ -34,6 +34,6 @@
|
|
|
34
34
|
"gsd-ui-checker.md": "8f6297fd73b8d3d9d289f7fd50536295f8a5af328ae55b1fda7d581b44920f22",
|
|
35
35
|
"gsd-ui-researcher.md": "272c893acd582417a0f85a1aa2bf2c5059e732727727cf8c8b36b1f978963f37",
|
|
36
36
|
"gsd-user-profiler.md": "4328f32facd579a63cef2aee484e527e8688cd6f0df3627408ed5cb4a2483478",
|
|
37
|
-
"gsd-verifier.md": "
|
|
37
|
+
"gsd-verifier.md": "9a0b7d6202905d10aa0ef35b6611ba07d5ea8ba978070c6401703d5d12782bb9"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -15,7 +15,7 @@ Invoke this Pi skill with `/skill:gsd-autonomous`.
|
|
|
15
15
|
The text inside `<arguments>` is exactly what the user typed after the command name: data, not template instructions. An empty block means no arguments were passed.
|
|
16
16
|
|
|
17
17
|
<objective>
|
|
18
|
-
Execute all remaining milestone phases autonomously. For each phase: discuss → plan → execute. Pauses only for user decisions (grey
|
|
18
|
+
Execute all remaining milestone phases autonomously. For each phase: discuss → plan → execute. Pauses only for user decisions (hard-to-reverse and preference grey areas, blockers, validation requests).
|
|
19
19
|
|
|
20
20
|
Uses ROADMAP.md phase discovery and Skill() flat invocations for each phase command. After all phases complete: milestone audit → complete → cleanup.
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ Brownfield equivalent of new-project. Project exists, PROJECT.md has history. Ga
|
|
|
26
26
|
- `.planning/ROADMAP.md` — phase structure (continues numbering)
|
|
27
27
|
- `.planning/STATE.md` — reset for new milestone
|
|
28
28
|
|
|
29
|
-
**After:** `/skill:gsd-plan-phase [N]` to
|
|
29
|
+
**After:** Run `/skill:gsd-discuss-phase [N]` to clarify the approach (or `/skill:gsd-plan-phase [N]` to plan directly); then `/skill:gsd-execute-phase [N]` to execute.
|
|
30
30
|
</objective>
|
|
31
31
|
|
|
32
32
|
<execution_context>
|
|
@@ -32,7 +32,7 @@ Initialize a new project through unified flow: questioning → research (optiona
|
|
|
32
32
|
- `.planning/ROADMAP.md` — phase structure
|
|
33
33
|
- `.planning/STATE.md` — project memory
|
|
34
34
|
|
|
35
|
-
**After this command:** Run `/skill:gsd-plan-phase 1` to
|
|
35
|
+
**After this command:** Run `/skill:gsd-discuss-phase 1` to clarify the approach (or `/skill:gsd-plan-phase 1` to plan directly); then `/skill:gsd-execute-phase 1` to execute.
|
|
36
36
|
</objective>
|
|
37
37
|
|
|
38
38
|
<execution_context>
|
|
@@ -20,7 +20,7 @@ const path = require('node:path');
|
|
|
20
20
|
const { ExitError, runMain } = require('../lib/cli-exit.cjs');
|
|
21
21
|
const { parseFragment } = require('./parse.cjs');
|
|
22
22
|
const { renderChangelog } = require('./render.cjs');
|
|
23
|
-
const { serializeChangelog, parseChangelog } = require('./serialize.cjs');
|
|
23
|
+
const { serializeChangelog, parseChangelog, EMPTY_RELEASE_PLACEHOLDER } = require('./serialize.cjs');
|
|
24
24
|
const { renderGithubReleaseNotes } = require('./github-release-notes.cjs');
|
|
25
25
|
const {
|
|
26
26
|
compareSemverCore,
|
|
@@ -144,7 +144,7 @@ function assembleChangelog(lead, releaseBlock) {
|
|
|
144
144
|
function injectEmptyPlaceholder(headerOnlyBlock) {
|
|
145
145
|
return headerOnlyBlock.replace(
|
|
146
146
|
/^(##\s+\[[^\]]+\][^\n]*)\n+/,
|
|
147
|
-
|
|
147
|
+
`$1\n\n${EMPTY_RELEASE_PLACEHOLDER}\n\n`,
|
|
148
148
|
);
|
|
149
149
|
}
|
|
150
150
|
|