@opengsd/gsd-core 1.16.0 → 1.18.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 -2
- package/.opencode/plugins/gsd-core.js +251 -1
- package/.opencode/plugins/gsd-statusline/tui.tsx +55 -0
- package/GEMINI.md +1 -1
- package/agents/gsd-advisor-researcher.compact.md +1 -1
- package/agents/gsd-advisor-researcher.md +1 -1
- package/agents/gsd-assumptions-analyzer.compact.md +1 -1
- package/agents/gsd-assumptions-analyzer.md +1 -1
- package/agents/gsd-code-fixer.compact.md +1 -1
- package/agents/gsd-code-fixer.md +1 -1
- package/agents/gsd-code-reviewer.compact.md +1 -1
- package/agents/gsd-code-reviewer.md +1 -1
- package/agents/gsd-codebase-mapper.compact.md +1 -1
- package/agents/gsd-codebase-mapper.md +1 -1
- package/agents/gsd-debug-session-manager.compact.md +6 -0
- package/agents/gsd-debug-session-manager.md +6 -0
- package/agents/gsd-debugger.md +1 -1
- package/agents/gsd-doc-verifier.compact.md +4 -4
- package/agents/gsd-doc-verifier.md +4 -3
- package/agents/gsd-doc-writer.compact.md +23 -5
- package/agents/gsd-doc-writer.md +19 -6
- package/agents/gsd-eval-auditor.compact.md +1 -1
- package/agents/gsd-eval-auditor.md +1 -1
- package/agents/gsd-executor.md +7 -3
- package/agents/gsd-integration-checker.compact.md +1 -1
- package/agents/gsd-integration-checker.md +1 -1
- package/agents/gsd-nyquist-auditor.compact.md +1 -1
- package/agents/gsd-nyquist-auditor.md +1 -1
- package/agents/gsd-phase-researcher.md +1 -1
- package/agents/gsd-plan-checker.md +2 -1
- package/agents/gsd-planner.md +5 -4
- package/agents/gsd-project-researcher.compact.md +1 -1
- package/agents/gsd-project-researcher.md +1 -1
- package/agents/gsd-research-synthesizer.compact.md +5 -5
- package/agents/gsd-research-synthesizer.md +5 -5
- package/agents/gsd-roadmapper.compact.md +1 -1
- package/agents/gsd-roadmapper.md +1 -1
- package/agents/gsd-security-auditor.compact.md +1 -1
- package/agents/gsd-security-auditor.md +1 -1
- package/agents/gsd-ui-auditor.compact.md +1 -1
- package/agents/gsd-ui-auditor.md +1 -1
- package/agents/gsd-ui-checker.compact.md +1 -1
- package/agents/gsd-ui-checker.md +1 -1
- package/agents/gsd-ui-researcher.compact.md +13 -1
- package/agents/gsd-ui-researcher.md +28 -1
- package/agents/gsd-verifier.md +1 -1
- package/bin/gsd-mcp-server.js +3 -1
- package/bin/install.js +398 -95
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +34 -12
- package/gsd-core/bin/lib/adr-parser.cjs +37 -5
- package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
- package/gsd-core/bin/lib/capability-lock.cjs +29 -7
- package/gsd-core/bin/lib/capability-registry.cjs +138 -80
- package/gsd-core/bin/lib/capability-source.cjs +3 -1
- package/gsd-core/bin/lib/capability-state.cjs +6 -3
- package/gsd-core/bin/lib/capability-validator.cjs +8 -5
- package/gsd-core/bin/lib/capability-writer.cjs +2 -1
- package/gsd-core/bin/lib/commands.cjs +33 -9
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
- package/gsd-core/bin/lib/gate-context-drift.cjs +5 -4
- package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
- package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
- package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
- package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
- package/gsd-core/bin/lib/gate-ui-safety.cjs +2 -1
- package/gsd-core/bin/lib/graphify.cjs +3 -2
- package/gsd-core/bin/lib/init.cjs +43 -16
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- package/gsd-core/bin/lib/install-engine.cjs +11 -0
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
- package/gsd-core/bin/lib/install-profiles.cjs +5 -3
- package/gsd-core/bin/lib/install-shadow-report.cjs +12 -0
- package/gsd-core/bin/lib/installer-migrations/011-opencode-mcp-no-prompts.cjs +99 -0
- package/gsd-core/bin/lib/installer-migrations.cjs +54 -1
- package/gsd-core/bin/lib/jsonc-edit.cjs +418 -0
- package/gsd-core/bin/lib/loop-host-contract.cjs +2 -1
- package/gsd-core/bin/lib/mcp-server.cjs +5 -1
- package/gsd-core/bin/lib/opencode-mcp-entry.cjs +39 -0
- package/gsd-core/bin/lib/opencode-version.cjs +486 -0
- package/gsd-core/bin/lib/phase.cjs +141 -11
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +2 -1
- package/gsd-core/bin/lib/report-parser.cjs +57 -1
- package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
- package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
- package/gsd-core/bin/lib/roadmap.cjs +56 -5
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +142 -6
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +58 -3
- package/gsd-core/bin/lib/runtime-slash.cjs +13 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +100 -53
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +98 -19
- package/gsd-core/bin/lib/state.cjs +544 -126
- package/gsd-core/bin/lib/task-command-router.cjs +2 -3
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
- package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
- package/gsd-core/bin/lib/update-context.cjs +18 -1
- package/gsd-core/bin/lib/verification.cjs +107 -17
- package/gsd-core/bin/lib/verify.cjs +36 -1
- package/gsd-core/references/agent-contracts.md +1 -1
- package/gsd-core/references/agent-skills-bootstrap.md +26 -7
- package/gsd-core/references/git-integration.md +3 -3
- package/gsd-core/references/planner-reversibility.md +18 -4
- package/gsd-core/references/planner-verify-command-grounding.md +18 -0
- package/gsd-core/references/tdd.md +4 -3
- package/gsd-core/references/verifier-phase-gates.md +8 -1
- package/gsd-core/workflows/add-backlog.md +10 -2
- package/gsd-core/workflows/add-phase.md +5 -1
- package/gsd-core/workflows/add-tests.md +5 -1
- package/gsd-core/workflows/add-todo.md +5 -1
- package/gsd-core/workflows/ai-integration-phase.md +5 -1
- package/gsd-core/workflows/audit-milestone.md +21 -9
- package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
- package/gsd-core/workflows/autonomous.md +52 -64
- package/gsd-core/workflows/check-todos.md +5 -1
- package/gsd-core/workflows/code-review-fix.md +5 -1
- package/gsd-core/workflows/code-review.md +21 -2
- package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
- package/gsd-core/workflows/complete-milestone.md +25 -5
- package/gsd-core/workflows/debug.md +5 -1
- package/gsd-core/workflows/diagnose-issues.md +5 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
- package/gsd-core/workflows/discuss-phase.md +5 -1
- package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
- package/gsd-core/workflows/docs-update.md +27 -5
- package/gsd-core/workflows/edit-phase.md +5 -1
- package/gsd-core/workflows/eval-review.md +11 -3
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
- package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
- package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
- package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
- package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
- package/gsd-core/workflows/execute-phase.md +40 -7
- package/gsd-core/workflows/execute-plan.md +8 -4
- package/gsd-core/workflows/extract-learnings.md +6 -2
- package/gsd-core/workflows/forensics.md +5 -1
- package/gsd-core/workflows/import.md +5 -1
- package/gsd-core/workflows/insert-phase.md +5 -1
- package/gsd-core/workflows/manager.md +10 -2
- package/gsd-core/workflows/map-codebase.md +5 -1
- package/gsd-core/workflows/milestone-summary.md +10 -2
- package/gsd-core/workflows/new-milestone.md +13 -5
- package/gsd-core/workflows/new-project.md +9 -5
- package/gsd-core/workflows/next.md +8 -4
- package/gsd-core/workflows/pause-work.md +3 -3
- package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
- package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
- package/gsd-core/workflows/plan-phase.md +41 -73
- package/gsd-core/workflows/plan-review-convergence.md +17 -5
- package/gsd-core/workflows/plant-seed.md +8 -4
- package/gsd-core/workflows/progress.md +6 -2
- package/gsd-core/workflows/quick-batch.md +5 -1
- package/gsd-core/workflows/quick.md +22 -7
- package/gsd-core/workflows/remove-phase.md +5 -1
- package/gsd-core/workflows/resume-project.md +19 -11
- package/gsd-core/workflows/review.md +6 -2
- package/gsd-core/workflows/scan.md +5 -1
- package/gsd-core/workflows/secure-phase.md +6 -2
- package/gsd-core/workflows/settings.md +3 -3
- package/gsd-core/workflows/ship.md +13 -10
- package/gsd-core/workflows/thread.md +10 -2
- package/gsd-core/workflows/transition.md +5 -1
- package/gsd-core/workflows/ui-phase.md +5 -1
- package/gsd-core/workflows/ui-review.md +6 -2
- package/gsd-core/workflows/ultraplan-phase.md +5 -1
- package/gsd-core/workflows/update.md +5 -1
- package/gsd-core/workflows/validate-phase.md +6 -2
- package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
- package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
- package/gsd-core/workflows/verify-work.md +11 -3
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
- package/hooks/dist/gsd-check-update.js +26 -20
- package/hooks/dist/gsd-context-monitor.js +14 -5
- package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +189 -15
- package/hooks/dist/gsd-statusline.js +321 -22
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-check-update.js +26 -20
- package/hooks/gsd-context-monitor.js +14 -5
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +189 -15
- package/hooks/gsd-statusline.js +321 -22
- package/hooks/gsd-validate-commit.sh +27 -5
- package/package.json +1 -1
- package/scripts/audit-workflow-script-paths.cjs +1 -1
- package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
- package/scripts/diagnose-validate-commit-phases.cjs +348 -0
- package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-capability-registry.cjs +7 -1
- package/scripts/gen-loop-host-contract.cjs +4 -1
- package/scripts/lib/macos-conformance-tier.generated.cjs +13 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +26 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/mutation-matrix.cjs +3 -1
- package/scripts/release-tarball-smoke.cjs +15 -7
- package/scripts/xtrace-phase-timing.cjs +205 -0
- package/skills/gsd-code-review/SKILL.md +8 -0
- package/skills/gsd-pause-work/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
13
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
14
|
const security_cjs_1 = require("./security.cjs");
|
|
15
|
+
const tdd_test_files_cjs_1 = require("./tdd-test-files.cjs");
|
|
15
16
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
17
|
const ioMod = require("./io.cjs");
|
|
17
18
|
const { output, error, ERROR_REASON } = ioMod;
|
|
@@ -37,9 +38,7 @@ function isBehaviorAddingTaskContent(content) {
|
|
|
37
38
|
.filter(Boolean);
|
|
38
39
|
hasSourceFiles = fileLines.some((file) => !/\.md$/i.test(file) &&
|
|
39
40
|
!/\.json$/i.test(file) &&
|
|
40
|
-
|
|
41
|
-
!/\.spec\.[^.]+$/i.test(file) &&
|
|
42
|
-
!/(^|[\\/])tests?[\\/]/i.test(file) &&
|
|
41
|
+
!(0, tdd_test_files_cjs_1.isTestFilePath)(file) &&
|
|
43
42
|
!/\.(yml|yaml|toml|ini|cfg|conf|properties)$/i.test(file) &&
|
|
44
43
|
!/(^|[\\/])\.env(\..+)?$/i.test(file));
|
|
45
44
|
}
|
|
@@ -10,6 +10,7 @@ exports.RedEvidenceReason = {
|
|
|
10
10
|
TargetFailed: 'target_test_failed', Green: 'unexpected_green', Empty: 'zero_tests_discovered',
|
|
11
11
|
NoFailure: 'nonzero_exit_without_test_failure', LoadFailure: 'fixture_or_load_failure',
|
|
12
12
|
NoTarget: 'no_target_test_failure', Invalid: 'invalid_record', Unreadable: 'unreadable_record',
|
|
13
|
+
BuildFailure: 'build_failure',
|
|
13
14
|
};
|
|
14
15
|
/** Basename of a path-like string ('' for non-strings) — separators `/` and `\`. */
|
|
15
16
|
function baseOf(p) {
|
|
@@ -59,6 +60,10 @@ function classifyRedEvidence(input) {
|
|
|
59
60
|
};
|
|
60
61
|
if (exitCode === 0)
|
|
61
62
|
return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.Green, evidence };
|
|
63
|
+
// A package that did not compile or load asserted nothing: refuse it as its own reason, ahead of
|
|
64
|
+
// the generic unusable-report refusal, because the fix is different (make the test compile).
|
|
65
|
+
if (report.buildFailed)
|
|
66
|
+
return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.BuildFailure, evidence };
|
|
62
67
|
if (!report.valid)
|
|
63
68
|
return { verdict: exports.RedEvidenceVerdict.Invalid, reason: exports.RedEvidenceReason.Invalid, evidence };
|
|
64
69
|
if (report.tests.length === 0)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The single definition of "a test file" for the TDD gate (#5106).
|
|
4
|
+
*
|
|
5
|
+
* Two places decide what a test file is: the behavior-adding check (`task-command-router.cts`) and
|
|
6
|
+
* the RED-commit pathspec in `workflows/execute-phase.md`. They were separate lists, so the pathspec
|
|
7
|
+
* learned the Go / Elixir / RSpec / pytest names in #4715 while the router never did, and a task
|
|
8
|
+
* listing only `placement_test.go` was classified behavior-adding — the gate then asked for a RED
|
|
9
|
+
* commit for the task that IS the RED commit.
|
|
10
|
+
*
|
|
11
|
+
* `TEST_FILE_PATHSPECS` is the list in git-pathspec spelling, which is what the workflow passes to
|
|
12
|
+
* `evaluation-scope --pathspec`; `isTestFilePath` evaluates the same list for a task's `<files>`.
|
|
13
|
+
* A test pins the workflow's pathspec flags to this array, so the two cannot drift apart again.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.TEST_FILE_PATHSPECS = void 0;
|
|
17
|
+
exports.isTestFilePath = isTestFilePath;
|
|
18
|
+
/** Git-pathspec spelling. A trailing `/` is a directory, anything else a basename glob. */
|
|
19
|
+
exports.TEST_FILE_PATHSPECS = [
|
|
20
|
+
'*.test.*', '*.spec.*', 'tests/', '__tests__/',
|
|
21
|
+
'*_test.go', 'test_*.py', '*_test.py', '*_test.exs', '*_spec.rb', '*_test.rb',
|
|
22
|
+
];
|
|
23
|
+
/** Directory names a `<files>` entry may sit under. `test/` is the singular spelling many projects use. */
|
|
24
|
+
const TEST_DIRECTORY_NAMES = new Set([
|
|
25
|
+
...exports.TEST_FILE_PATHSPECS.filter((spec) => spec.endsWith('/')).map((spec) => spec.slice(0, -1)),
|
|
26
|
+
'test',
|
|
27
|
+
]);
|
|
28
|
+
function escapeRegex(text) {
|
|
29
|
+
return text.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
}
|
|
31
|
+
/** A basename glob where `*` is any run of non-separator characters. */
|
|
32
|
+
const FILE_GLOBS = exports.TEST_FILE_PATHSPECS
|
|
33
|
+
.filter((spec) => !spec.endsWith('/'))
|
|
34
|
+
.map((spec) => new RegExp(`^${spec.split('*').map(escapeRegex).join('[^/]*')}$`, 'i'));
|
|
35
|
+
/** True when `file` (either separator) is a test file by the shared convention list. */
|
|
36
|
+
function isTestFilePath(file) {
|
|
37
|
+
const segments = file.split(/[\\/]/).filter(Boolean);
|
|
38
|
+
const base = segments[segments.length - 1] ?? '';
|
|
39
|
+
if (FILE_GLOBS.some((glob) => glob.test(base)))
|
|
40
|
+
return true;
|
|
41
|
+
return segments.slice(0, -1).some((segment) => TEST_DIRECTORY_NAMES.has(segment.toLowerCase()));
|
|
42
|
+
}
|
|
@@ -147,7 +147,24 @@ function resolveGlobalCandidate(fs, env, home, preferred) {
|
|
|
147
147
|
/**
|
|
148
148
|
* Pure resolver. Returns { installedVersion, scope, runtime, gsdDir }.
|
|
149
149
|
*/
|
|
150
|
-
function resolveUpdateContext(
|
|
150
|
+
function resolveUpdateContext(opts) {
|
|
151
|
+
const base = resolveBaseUpdateContext(opts);
|
|
152
|
+
if (base.runtime !== 'opencode' || !base.gsdDir)
|
|
153
|
+
return base;
|
|
154
|
+
// The install records which OpenCode generation it targets; surface it when present.
|
|
155
|
+
const raw = opts.fs.readFile(node_path_1.default.join(base.gsdDir, 'gsd-install-state.json'));
|
|
156
|
+
if (raw == null)
|
|
157
|
+
return base;
|
|
158
|
+
try {
|
|
159
|
+
const hg = JSON.parse(raw).hostGeneration;
|
|
160
|
+
if (hg && hg.runtime === 'opencode' && (hg.generation === 1 || hg.generation === 2)) {
|
|
161
|
+
return { ...base, hostGeneration: hg.generation };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch { /* an unreadable state file contributes nothing */ }
|
|
165
|
+
return base;
|
|
166
|
+
}
|
|
167
|
+
function resolveBaseUpdateContext({ home, cwd, env = {}, fs, preferredConfigDir = '', preferredRuntime = '', }) {
|
|
151
168
|
// Expand a leading `~/` before any probe.
|
|
152
169
|
preferredConfigDir = expandHome(preferredConfigDir, home);
|
|
153
170
|
const preferred = preferredRuntime || inferPreferredRuntime({ fs, env, preferredConfigDir });
|
|
@@ -258,6 +258,11 @@ function reportStatusOf(fm, filePath) {
|
|
|
258
258
|
* too): it re-runs the verifier, which regenerates VERIFICATION.md and its
|
|
259
259
|
* digest (#4682, #4887).
|
|
260
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* The one remedy for a stale report (#4682 / #5118), shared by the route table and by the more specific
|
|
263
|
+
* diagnostic below so the two cannot drift: only re-running the verifier regenerates VERIFICATION.md.
|
|
264
|
+
*/
|
|
265
|
+
const STALE_REMEDY = 'Re-run execute-phase for this phase: it resumes at the verification gates and re-runs the verifier, regenerating VERIFICATION.md and its digest. verify-work alone cannot refresh a stale report.';
|
|
261
266
|
const VERIFICATION_ROUTES = Object.freeze({
|
|
262
267
|
passed: Object.freeze({
|
|
263
268
|
next_action: 'Verification passed — continue.',
|
|
@@ -279,7 +284,7 @@ const VERIFICATION_ROUTES = Object.freeze({
|
|
|
279
284
|
stale: Object.freeze({
|
|
280
285
|
// #4682 / #5118: the only remedy for a stale report is re-running the
|
|
281
286
|
// verifier; /gsd-verify-work on its own never rewrites VERIFICATION.md.
|
|
282
|
-
next_action:
|
|
287
|
+
next_action: `Verification is stale — covered source files changed after the verifier last ran. ${STALE_REMEDY}`,
|
|
283
288
|
command: 'execute-phase',
|
|
284
289
|
tail: '',
|
|
285
290
|
}),
|
|
@@ -540,7 +545,7 @@ function phaseArtifactPaths(phaseDir, projectRoot) {
|
|
|
540
545
|
const scan = scanPhasePlans(phaseDir);
|
|
541
546
|
// Fail closed on a non-COMPLETE scan (unreadable phase dir, or an
|
|
542
547
|
// unreadable nested plans/ dir) — a partial scan's invisible contents can
|
|
543
|
-
// never be proven covered (mirrors
|
|
548
|
+
// never be proven covered (mirrors uncoveredCurrentArtifacts's own
|
|
544
549
|
// fail-closed contract).
|
|
545
550
|
if (scan.scope !== SCOPE.COMPLETE) {
|
|
546
551
|
return { ok: false, reason: `phase directory scan is incomplete (scope: ${scan.scope}): ${phaseDir}` };
|
|
@@ -849,9 +854,9 @@ function computeCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_V
|
|
|
849
854
|
* each is represented in `coveredFiles` — matched by suffix (mirrors
|
|
850
855
|
* `matchRequestedFile`'s convention) since `coveredFiles` holds
|
|
851
856
|
* project-root-relative paths while the scan returns phase-relative
|
|
852
|
-
* filenames. Returns
|
|
853
|
-
*
|
|
854
|
-
*
|
|
857
|
+
* filenames. Returns the current plans/summaries the report never declared
|
|
858
|
+
* (empty when every one is covered), so the stale diagnostic can name them (#4817); `null` when the
|
|
859
|
+
* scan itself was incomplete, which callers treat as stale.
|
|
855
860
|
*
|
|
856
861
|
* Fails CLOSED on an incomplete scan: `scanPhasePlans` never throws on a
|
|
857
862
|
* readdir failure — it reports it via `scope` (`SCOPE.UNREADABLE` for the
|
|
@@ -860,18 +865,18 @@ function computeCoveredDigest(projectRoot, coveredFiles, version = FINGERPRINT_V
|
|
|
860
865
|
* contract (`planning-scope.cts`): zero items under a non-`COMPLETE` scope
|
|
861
866
|
* is a NON-answer, never "this phase has no plans." Branching on `scope`
|
|
862
867
|
* here (rather than a try/catch, which this scan never triggers) is what
|
|
863
|
-
* makes an unreadable `plans/` dir report `
|
|
868
|
+
* makes an unreadable `plans/` dir report `null` (stale) instead of silently
|
|
864
869
|
* treating its invisible contents as vacuously covered — the same
|
|
865
870
|
* fail-open regression #3057 B3 fixed for the legacy path.
|
|
866
871
|
*/
|
|
867
|
-
function
|
|
872
|
+
function uncoveredCurrentArtifacts(phaseDir, coveredFiles) {
|
|
868
873
|
const scan = scanPhasePlans(phaseDir);
|
|
869
874
|
if (scan.scope !== SCOPE.COMPLETE)
|
|
870
|
-
return
|
|
875
|
+
return null;
|
|
871
876
|
const coveredPosix = canonicalizeCoveredFiles(coveredFiles);
|
|
872
|
-
return [...scan.allPlanFiles, ...scan.summaryFiles].
|
|
877
|
+
return [...scan.allPlanFiles, ...scan.summaryFiles].filter((artifact) => {
|
|
873
878
|
const artifactPosix = toPosix(artifact);
|
|
874
|
-
return coveredPosix.some((c) => c === artifactPosix || c.endsWith(`/${artifactPosix}`));
|
|
879
|
+
return !coveredPosix.some((c) => c === artifactPosix || c.endsWith(`/${artifactPosix}`));
|
|
875
880
|
});
|
|
876
881
|
}
|
|
877
882
|
/**
|
|
@@ -943,6 +948,73 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
|
|
|
943
948
|
}
|
|
944
949
|
return commitTimes;
|
|
945
950
|
}
|
|
951
|
+
/**
|
|
952
|
+
* #4857: which declared covered files changed since the report was written.
|
|
953
|
+
* `covered_digest` is one aggregate hash, so it can say THAT something moved
|
|
954
|
+
* but not what; this names the files by time instead — each covered file
|
|
955
|
+
* (report paths and, for v2+, shared planning documents excluded, exactly as
|
|
956
|
+
* the digest excludes them) whose clean commit time, or mtime when it is
|
|
957
|
+
* uncommitted or edited since, is newer than the report's own. A file that
|
|
958
|
+
* can no longer be statted counts as moved. Advisory only: it never decides
|
|
959
|
+
* staleness (the digest does), and an unreadable clock yields fewer names,
|
|
960
|
+
* never a wrong status. Timed through the same injectable resolver the
|
|
961
|
+
* legacy mtime check uses, rooted at the project so the covered paths
|
|
962
|
+
* (project-root-relative) resolve.
|
|
963
|
+
*/
|
|
964
|
+
function coveredFilesMovedSinceReport(projectRoot, phaseDir, reportPath, coveredFiles, version, fsImpl, resolver) {
|
|
965
|
+
const sharedRoots = version >= 2 ? sharedRootsFor(projectRoot, phaseDir, null) : [];
|
|
966
|
+
const files = canonicalizeCoveredFiles(coveredFiles, { version })
|
|
967
|
+
.filter((f) => !isVerificationReportPath(f) && !isSharedPlanningDoc(f, sharedRoots));
|
|
968
|
+
if (files.length === 0)
|
|
969
|
+
return [];
|
|
970
|
+
const reportRel = toPosix(node_path_1.default.relative(projectRoot, reportPath));
|
|
971
|
+
let commitTimes;
|
|
972
|
+
try {
|
|
973
|
+
commitTimes = resolver(projectRoot, [reportRel, ...files]);
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
commitTimes = new Map();
|
|
977
|
+
}
|
|
978
|
+
const timeOf = (rel) => {
|
|
979
|
+
const committed = commitTimes.get(rel);
|
|
980
|
+
if (committed !== undefined)
|
|
981
|
+
return committed;
|
|
982
|
+
try {
|
|
983
|
+
return fsImpl.statSync(node_path_1.default.join(projectRoot, rel)).mtimeMs;
|
|
984
|
+
}
|
|
985
|
+
catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const reportTime = timeOf(reportRel);
|
|
990
|
+
if (reportTime === null)
|
|
991
|
+
return [];
|
|
992
|
+
return files.filter((rel) => {
|
|
993
|
+
const t = timeOf(rel);
|
|
994
|
+
return t === null || t > reportTime;
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
/** At most this many uncovered artifact names are spelled out in `next_action`; the rest are counted. */
|
|
998
|
+
const STALE_NAMED_ARTIFACT_LIMIT = 5;
|
|
999
|
+
function staleNextAction(detail) {
|
|
1000
|
+
const name = (items) => {
|
|
1001
|
+
const shown = items.slice(0, STALE_NAMED_ARTIFACT_LIMIT).map((a) => io.formatDiagnosticToken(a));
|
|
1002
|
+
const more = items.length - shown.length;
|
|
1003
|
+
return `${shown.join(', ')}${more > 0 ? ` and ${more} more` : ''}`;
|
|
1004
|
+
};
|
|
1005
|
+
const reasons = [];
|
|
1006
|
+
if (detail.moved.length > 0) {
|
|
1007
|
+
reasons.push(`covered source files changed after the verifier last ran: ${name(detail.moved)}`);
|
|
1008
|
+
}
|
|
1009
|
+
else if (detail.digestMoved) {
|
|
1010
|
+
reasons.push('covered source files changed after the verifier last ran');
|
|
1011
|
+
}
|
|
1012
|
+
if (detail.uncovered.length > 0) {
|
|
1013
|
+
reasons.push(`the report never covered ${name(detail.uncovered)} (a current phase plan or summary it did not declare in covered_files)`);
|
|
1014
|
+
}
|
|
1015
|
+
const tail = detail.digestMoved ? '' : '; no covered file changed';
|
|
1016
|
+
return `Verification is stale — ${reasons.join(', and ')}${tail}. ${STALE_REMEDY}`;
|
|
1017
|
+
}
|
|
946
1018
|
/**
|
|
947
1019
|
* The one result builder (#5118): every `VerificationStatusResult` is
|
|
948
1020
|
* projected from `VERIFICATION_ROUTES[status]` here — `route` (the bare
|
|
@@ -951,12 +1023,18 @@ function defaultPhaseCleanCommitTimesMs(phaseDir, files, execGitFn = shell_comma
|
|
|
951
1023
|
*/
|
|
952
1024
|
function routeResult(status, ctx) {
|
|
953
1025
|
const entry = VERIFICATION_ROUTES[status];
|
|
1026
|
+
const detail = status === VERIFICATION_STATUS.STALE && ctx.staleDetail
|
|
1027
|
+
&& (ctx.staleDetail.uncovered.length > 0 || ctx.staleDetail.moved.length > 0)
|
|
1028
|
+
? ctx.staleDetail
|
|
1029
|
+
: undefined;
|
|
954
1030
|
return {
|
|
955
1031
|
status,
|
|
956
|
-
next_action: entry.next_action,
|
|
1032
|
+
next_action: detail ? staleNextAction(detail) : entry.next_action,
|
|
957
1033
|
next_command: projectNextCommand(entry.command, ctx.runtime, `${ctx.phaseArg}${entry.tail}`),
|
|
958
1034
|
route: entry.command,
|
|
959
1035
|
...(ctx.message !== undefined ? { message: ctx.message } : {}),
|
|
1036
|
+
...(detail && detail.uncovered.length > 0 ? { uncovered_artifacts: detail.uncovered } : {}),
|
|
1037
|
+
...(detail && detail.moved.length > 0 ? { moved_files: detail.moved } : {}),
|
|
960
1038
|
...(ctx.staleCheckIndeterminate ? { staleCheckIndeterminate: true } : {}),
|
|
961
1039
|
};
|
|
962
1040
|
}
|
|
@@ -1396,12 +1474,13 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
1396
1474
|
typeof coveredDigestVal === 'string' &&
|
|
1397
1475
|
coveredDigestVal.trim().length > 0;
|
|
1398
1476
|
let staleCheckIndeterminate = false;
|
|
1477
|
+
let staleDetail;
|
|
1399
1478
|
let isStale;
|
|
1400
1479
|
if (declaresFingerprint) {
|
|
1401
1480
|
// Stated directly rather than relying on `null !== coveredDigestVal`
|
|
1402
1481
|
// being true whenever the pair is malformed: `!hasWellFormedFingerprint`
|
|
1403
1482
|
// fails closed explicitly, and its `||` short-circuit means
|
|
1404
|
-
// computeCoveredDigest/
|
|
1483
|
+
// computeCoveredDigest/uncoveredCurrentArtifacts never run on a
|
|
1405
1484
|
// malformed (wrong-shaped) `coveredFilesVal`. The two `||`s after it
|
|
1406
1485
|
// short-circuit in turn: the live-directory re-scan (for a plan/summary
|
|
1407
1486
|
// added AFTER verification and never declared in covered_files) only
|
|
@@ -1423,17 +1502,28 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
1423
1502
|
// #5095 (R1): v3's digest is computed over the declared set UNIONED
|
|
1424
1503
|
// with the phase's own live artifacts — a plan/summary added after
|
|
1425
1504
|
// fingerprinting already moves the digest itself, so the separate
|
|
1426
|
-
// live-directory re-scan (`
|
|
1505
|
+
// live-directory re-scan (`uncoveredCurrentArtifacts`) is redundant
|
|
1427
1506
|
// for v3 and is not run.
|
|
1428
1507
|
isStale =
|
|
1429
1508
|
computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal;
|
|
1509
|
+
if (isStale) {
|
|
1510
|
+
const moved = coveredFilesMovedSinceReport(projectRoot, phaseDir, located.filePath, coveredFilesVal, storedVersion, fsImpl, phaseCleanCommitTimesMs);
|
|
1511
|
+
if (moved.length > 0)
|
|
1512
|
+
staleDetail = { uncovered: [], moved, digestMoved: true };
|
|
1513
|
+
}
|
|
1430
1514
|
}
|
|
1431
1515
|
else {
|
|
1432
1516
|
// v1/v2: unchanged — the digest alone cannot see a plan/summary that
|
|
1433
1517
|
// was never declared, so the live-directory re-scan still runs.
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1518
|
+
const digestMoved = computeCoveredDigest(projectRoot, coveredFilesVal, storedVersion, { phaseDir }) !== coveredDigestVal;
|
|
1519
|
+
const uncovered = uncoveredCurrentArtifacts(phaseDir, coveredFilesVal);
|
|
1520
|
+
isStale = digestMoved || uncovered === null || uncovered.length > 0;
|
|
1521
|
+
const moved = digestMoved
|
|
1522
|
+
? coveredFilesMovedSinceReport(projectRoot, phaseDir, located.filePath, coveredFilesVal, storedVersion, fsImpl, phaseCleanCommitTimesMs)
|
|
1523
|
+
: [];
|
|
1524
|
+
if ((uncovered !== null && uncovered.length > 0) || moved.length > 0) {
|
|
1525
|
+
staleDetail = { uncovered: uncovered ? [...uncovered].sort() : [], moved, digestMoved };
|
|
1526
|
+
}
|
|
1437
1527
|
}
|
|
1438
1528
|
}
|
|
1439
1529
|
else {
|
|
@@ -1449,7 +1539,7 @@ function readVerificationStatus(phaseDir, opts = {}) {
|
|
|
1449
1539
|
if (isStale) {
|
|
1450
1540
|
// #4682 / #5118: the one stale route — execute-phase's verify_phase_goal
|
|
1451
1541
|
// step re-runs the verifier, regenerating VERIFICATION.md and its digest.
|
|
1452
|
-
return route(VERIFICATION_STATUS.STALE);
|
|
1542
|
+
return route(VERIFICATION_STATUS.STALE, staleDetail ? { staleDetail } : {});
|
|
1453
1543
|
}
|
|
1454
1544
|
// 3. Route the writer-set member through the one table.
|
|
1455
1545
|
return route(reportStatus, { staleCheckIndeterminate });
|
|
@@ -1147,6 +1147,38 @@ function unreadableFileVerdict(evidence, displayPath) {
|
|
|
1147
1147
|
: { error: 'File unreadable', path: displayPath, read_error: evidence.reason };
|
|
1148
1148
|
return (0, gate_verdict_cjs_1.gateUnreadable)(false, payload);
|
|
1149
1149
|
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Issue #4927 — a PLAN.md can be cut off by a tool that still reported the write
|
|
1152
|
+
* as successful (runtimes cap tool-call output; a ~450-line plan stopped inside
|
|
1153
|
+
* an XML tag). The planner and the plan checker both run `verify plan-structure`
|
|
1154
|
+
* on every plan, so this is where a silent partial write is caught, independent
|
|
1155
|
+
* of what the write call returned.
|
|
1156
|
+
*
|
|
1157
|
+
* Three signals, each a strong sign of a partial file and none of them a shape a
|
|
1158
|
+
* finished plan carries:
|
|
1159
|
+
* - the incremental-write sentinel (`<!-- gsd:write-continue -->`) left behind
|
|
1160
|
+
* (the planner's large-file fallback never removed it);
|
|
1161
|
+
* - `<tasks>` opened and never closed;
|
|
1162
|
+
* - the file ends inside a tag (`...</ta`). Prose such as `a < b` does not
|
|
1163
|
+
* match: a tag needs a name character straight after the `<` or `</`.
|
|
1164
|
+
*/
|
|
1165
|
+
function scanPlanTruncation(content) {
|
|
1166
|
+
const errors = [];
|
|
1167
|
+
const text = content || '';
|
|
1168
|
+
if (/<!--\s*gsd:write-continue\s*-->/.test(text)) {
|
|
1169
|
+
errors.push('Plan appears truncated: the incremental-write sentinel `<!-- gsd:write-continue -->` is still present — '
|
|
1170
|
+
+ 'the last section was never written. Finish the write (or re-write the plan) before treating it as complete.');
|
|
1171
|
+
}
|
|
1172
|
+
if (/<tasks>/.test(text) && !/<\/tasks>/.test(text)) {
|
|
1173
|
+
errors.push('Plan appears truncated: `<tasks>` is opened but `</tasks>` never appears — the file was cut off before the task list ended.');
|
|
1174
|
+
}
|
|
1175
|
+
const trimmed = text.replace(/\s+$/, '');
|
|
1176
|
+
const lastLt = trimmed.lastIndexOf('<');
|
|
1177
|
+
if (lastLt !== -1 && trimmed.indexOf('>', lastLt) === -1 && /^<\/?[A-Za-z_!][^<>]{0,200}$/.test(trimmed.slice(lastLt))) {
|
|
1178
|
+
errors.push(`Plan appears truncated: the file ends inside a tag (\`${trimmed.slice(lastLt).slice(0, 40)}\`) — the write stopped mid-payload.`);
|
|
1179
|
+
}
|
|
1180
|
+
return errors;
|
|
1181
|
+
}
|
|
1150
1182
|
function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
1151
1183
|
if (!filePath) {
|
|
1152
1184
|
error('file path required');
|
|
@@ -1172,6 +1204,8 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
1172
1204
|
const fm = extractFrontmatter(content, fullPath);
|
|
1173
1205
|
const errors = [];
|
|
1174
1206
|
const warnings = [];
|
|
1207
|
+
// #4927: a plan the write tool reported as written but that ends mid-payload.
|
|
1208
|
+
errors.push(...scanPlanTruncation(content));
|
|
1175
1209
|
const required = ['phase', 'plan', 'type', 'wave', 'depends_on', 'files_modified', 'autonomous', 'must_haves'];
|
|
1176
1210
|
for (const field of required) {
|
|
1177
1211
|
if (fm[field] === undefined)
|
|
@@ -1220,7 +1254,8 @@ function cmdVerifyPlanStructure(cwd, filePath, raw) {
|
|
|
1220
1254
|
continue;
|
|
1221
1255
|
if (!decisionCheckpointOffsets.some((offset) => offset < at)) {
|
|
1222
1256
|
warnings.push('Task rated <reversibility rating="one-way"> has no preceding checkpoint:decision — '
|
|
1223
|
-
+ 'a one-way door must be confirmed before the agent walks through it'
|
|
1257
|
+
+ 'a one-way door must be confirmed before the agent walks through it (unless the decision is already settled '
|
|
1258
|
+
+ 'in CONTEXT.md, an ingested ADR, or an answered checkpoint)');
|
|
1224
1259
|
}
|
|
1225
1260
|
}
|
|
1226
1261
|
const echoScan = scanNegativeGrepCommentEcho(content);
|
|
@@ -33,7 +33,7 @@ This doc describes what IS, not what should be. Casing inconsistencies are docum
|
|
|
33
33
|
| gsd-code-reviewer | Source-code review | No marker (writes REVIEW.md) | `gsd-core/workflows/code-review.md` reads REVIEW.md | artifact+query |
|
|
34
34
|
| gsd-assumptions-analyzer | Assumption extraction | No marker (returns `## Assumptions` sections) | `gsd-core/workflows/discuss-phase-assumptions.md` reads the inline `## Assumptions` sections from the agent's return | structured-return |
|
|
35
35
|
| gsd-doc-classifier | Planning-doc classification | No marker (writes `.planning/intel/classifications/*.json`) | `gsd-core/workflows/ingest-docs.md` reads the classification JSON | artifact+query |
|
|
36
|
-
| gsd-doc-verifier | Doc validation | No marker (writes JSON to `.planning/tmp/`) |
|
|
36
|
+
| gsd-doc-verifier | Doc validation | No marker (writes JSON to `.planning/tmp/`) | `{project_root}/.planning/tmp/verify-{doc_key}.json` artifact (the orchestrator-assigned `result_path`, keyed by the doc's full relative path), read by `gsd-core/workflows/docs-update.md` | artifact+query |
|
|
37
37
|
| gsd-doc-writer | Doc generation | No marker (writes docs directly) | generated doc files, consumed by `gsd-core/workflows/docs-update.md` and `gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md` | artifact+query |
|
|
38
38
|
| gsd-domain-researcher | Domain research | No marker (writes the AI-SPEC.md domain section via Edit) | `gsd-core/workflows/ai-integration-phase.md` reads the AI-SPEC.md section after the agent returns | artifact+query |
|
|
39
39
|
| gsd-eval-auditor | Evaluation coverage audit | No marker (writes the REVIEW.md audit section) | `gsd-core/workflows/eval-review.md` reads REVIEW.md | artifact+query |
|
|
@@ -22,22 +22,41 @@ In your mandatory init step — right after `mandatory-initial-read.md` / the
|
|
|
22
22
|
injection also runs (e.g. Claude Code). The guard is what keeps the two seams from
|
|
23
23
|
doubling the block.
|
|
24
24
|
|
|
25
|
-
2. **Query your configured skills.** Use **your own agent type
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
2. **Query your configured skills.** Use **your own agent type**: the one your agent
|
|
26
|
+
file states on its `agent_skills:` pointer line, as `(agent type: \`gsd-executor\`)`.
|
|
27
|
+
Do not read it from your frontmatter `name:` — Claude Code shows a subagent only the
|
|
28
|
+
body, the Kimi converter drops frontmatter, and Codex keeps it in a TOML key. Run the
|
|
29
|
+
query with `--skills-only`, in the **same bash call** as the launcher so `gsd_run` is
|
|
30
|
+
defined when it runs. Substitute your type for `<your-agent-type>`:
|
|
29
31
|
|
|
30
32
|
```bash
|
|
31
|
-
|
|
33
|
+
# 1. Launcher: paste the bash block from gsd-run-resolver.md here unless gsd_run
|
|
34
|
+
# is already defined in this shell (@~/.claude/gsd-core/references/gsd-run-resolver.md).
|
|
35
|
+
# 2. Query:
|
|
36
|
+
_AGENT_SKILLS=$(gsd_run query agent-skills <your-agent-type> --skills-only)
|
|
32
37
|
```
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
Not every agent file defines `gsd_run`, so supply it as above rather than assuming
|
|
40
|
+
your init did. The query is read-only and idempotent. Its stderr and exit status stay
|
|
41
|
+
visible, so a missing launcher or a failed query is never mistaken for "nothing
|
|
42
|
+
configured".
|
|
36
43
|
|
|
37
44
|
3. **Read every listed skill.** The block emits entries as `@<path>/SKILL.md`
|
|
38
45
|
includes — `Read` each one before starting work. If the block is empty, there is
|
|
39
46
|
nothing to do (zero overhead).
|
|
40
47
|
|
|
48
|
+
## What the query returns, per runtime
|
|
49
|
+
|
|
50
|
+
| Call | Claude | Non-Claude (Codex, Kimi, OpenCode, Kilo, …) |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `query agent-skills <type> --skills-only` (the self-load) | configured skills, or an empty block | configured skills, or an empty block |
|
|
53
|
+
| `query agent-skills <type>` (orchestrator-side `${AGENT_SKILLS_*}` injection) | configured skills, or an empty block | configured skills, or — when nothing is configured and the agent is installed — the agent's own **persona** (#2454, #4407) |
|
|
54
|
+
|
|
55
|
+
On a non-Claude runtime the plain query can answer with the agent's whole persona
|
|
56
|
+
(about 47 KB for the executor). `--skills-only` is what keeps the self-load from
|
|
57
|
+
reading your own persona back into your context as if it were a skills block. Never
|
|
58
|
+
run the query without it from inside an agent.
|
|
59
|
+
|
|
41
60
|
## What self-load does and does not cover
|
|
42
61
|
|
|
43
62
|
| Skill form | Self-loads? | Notes |
|
|
@@ -144,10 +144,10 @@ gsd_run query commit "docs({phase}-{plan}): complete [plan-name] plan" --files .
|
|
|
144
144
|
</format>
|
|
145
145
|
|
|
146
146
|
<format name="handoff">
|
|
147
|
-
## Handoff (
|
|
147
|
+
## Handoff (Pause)
|
|
148
148
|
|
|
149
149
|
```
|
|
150
|
-
|
|
150
|
+
docs(pause): [phase-name] paused at task [X]/[Y]
|
|
151
151
|
|
|
152
152
|
Current: [task name]
|
|
153
153
|
[If blocked:] Blocked: [reason]
|
|
@@ -156,7 +156,7 @@ Current: [task name]
|
|
|
156
156
|
What to commit:
|
|
157
157
|
|
|
158
158
|
```bash
|
|
159
|
-
gsd_run query commit "
|
|
159
|
+
gsd_run query commit "docs(pause): [phase-name] paused at task [X]/[Y]" --files .planning/
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
</format>
|
|
@@ -20,7 +20,7 @@ this turns out wrong three phases from now, what does undoing it cost?*
|
|
|
20
20
|
|---|---|---|
|
|
21
21
|
| `reversible` | Local and cheap — one file, one function, an implementation swapped behind a stable interface. | `reversible` decisions get no checkpoint and no flag; the task proceeds normally. |
|
|
22
22
|
| `costly` | Undo touches many call sites or needs a coordinated change — a shared interface shape, a cross-module contract, a dependency major bump. | `costly` decisions are flagged in the plan so the reader sees the weight, but this does not block execution. |
|
|
23
|
-
| `one-way` | Undo requires a data migration, breaks a published contract, or cannot be done at all — on-disk/wire format, public API shape, external-service lock-in, a schema other systems already read. | The planner inserts a `checkpoint:decision` **before** the dependent task, so the human confirms the door before the agent walks through it. |
|
|
23
|
+
| `one-way` | Undo requires a data migration, breaks a published contract, or cannot be done at all — on-disk/wire format, public API shape, external-service lock-in, a schema other systems already read. | The planner inserts a `checkpoint:decision` **before** the dependent task, so the human confirms the door before the agent walks through it — unless the human already settled that decision (see *Already settled*). |
|
|
24
24
|
|
|
25
25
|
**When unsure, rate it `reversible`.** The value of this feature is
|
|
26
26
|
*discrimination*. A planner that rates everything `one-way` produces checkpoint
|
|
@@ -101,6 +101,21 @@ the same terms as any other ingested text (ADR-1577,
|
|
|
101
101
|
carrying something that belongs in `<context>`, and long free text is where
|
|
102
102
|
smuggled structure hides.
|
|
103
103
|
|
|
104
|
+
## Already settled
|
|
105
|
+
|
|
106
|
+
A `one-way` decision the human has **already settled** keeps its rating for the
|
|
107
|
+
record and gets **no** new `checkpoint:decision`. It is settled when any of
|
|
108
|
+
these records the human choosing this exact option:
|
|
109
|
+
|
|
110
|
+
- the phase `CONTEXT.md` `<decisions>` block (discuss-phase output);
|
|
111
|
+
- an ADR ingested with `--ingest`;
|
|
112
|
+
- a `checkpoint:decision` earlier in this phase's plans that was already answered.
|
|
113
|
+
|
|
114
|
+
Only a **newly surfaced** `one-way` decision — one nothing above records — earns a
|
|
115
|
+
fresh checkpoint before the implementing task. If you cannot point at the record,
|
|
116
|
+
treat the decision as newly surfaced. `gsd-planner` and this reference state the
|
|
117
|
+
same rule; if they ever diverge, this file wins.
|
|
118
|
+
|
|
104
119
|
## Anti-patterns
|
|
105
120
|
|
|
106
121
|
- **Everything is `one-way`.** The most common failure. Re-read the undo cost:
|
|
@@ -111,9 +126,8 @@ the same terms as any other ingested text (ADR-1577,
|
|
|
111
126
|
- **A rationale that restates the rating.** "This is irreversible because it
|
|
112
127
|
cannot be undone" tells the reader nothing. Name the migration, the contract,
|
|
113
128
|
or the dependent system.
|
|
114
|
-
- **Gating a decision already made.**
|
|
115
|
-
|
|
116
|
-
rating for the record; do not insert a checkpoint to re-ask.
|
|
129
|
+
- **Gating a decision already made.** See *Already settled* below: a decision the
|
|
130
|
+
human has already settled does not earn a fresh checkpoint.
|
|
117
131
|
- **Using the gate as a substitute for design.** The checkpoint buys deliberation
|
|
118
132
|
on a door you must walk through. The better move, when available, is to *make
|
|
119
133
|
the decision reversible* — put the format behind a writer seam, version the
|
|
@@ -16,6 +16,24 @@ preferred over `cd <dir> && npm run <script>` — it does not depend on the exec
|
|
|
16
16
|
`prior_verify_commands` is empty and you cannot ground a path, say so in the plan instead of
|
|
17
17
|
guessing one.
|
|
18
18
|
|
|
19
|
+
**Run a count/match gate before it ships (#4927).** A gate you author — or one a `RESEARCH.md`
|
|
20
|
+
hands you — that counts or matches text (`grep -c`, `grep -q`, `rg`, `test -n "$(grep …)"`) is a
|
|
21
|
+
claim about the tree, and two defects are invisible to reading and take one command to expose. You
|
|
22
|
+
have a shell when you author the plan, so run each such gate against the **current tree** and write
|
|
23
|
+
the pre-change result next to it as its baseline (`# baseline at HEAD: 0`):
|
|
24
|
+
|
|
25
|
+
- **It already passes at HEAD.** `grep -c "Agent" .planning/PROJECT.md` returned `6` before any work
|
|
26
|
+
existed, because the word sat in an unrelated row — the gate measures nothing and passes whether
|
|
27
|
+
or not the work happens. Fix the gate: anchor it to the new heading, key, or line the task
|
|
28
|
+
creates, or assert a delta from the recorded baseline.
|
|
29
|
+
- **Its literal can never match the deliverable.** `grep` is case-sensitive, so `grep -c "quickstart"`
|
|
30
|
+
fails on a correct `## Quickstart`. Match the real casing of the text the task writes, or use
|
|
31
|
+
`-i` when casing is not part of the contract.
|
|
32
|
+
|
|
33
|
+
A gate that is meant to prove a *negative* is checked the other way: it must pass after the work
|
|
34
|
+
and its baseline must show the thing was there to remove. Only read-only commands are safe to run
|
|
35
|
+
this way — run `grep`/`rg`/`test` gates; do not execute a gate that builds, installs, or writes.
|
|
36
|
+
|
|
19
37
|
**Root-relative, never absolute (#4767).** The paths in your prompt's `<required_reading>` are
|
|
20
38
|
absolute on purpose — a subagent's cwd may differ from the orchestrator's (#2376). That rule
|
|
21
39
|
covers prompt *inputs* only. Every path in the plan *body* — `<files>`, `<verify>`, `<automated>`,
|
|
@@ -96,11 +96,12 @@ After completion, create SUMMARY.md with:
|
|
|
96
96
|
2. Write test describing expected behavior (from `<behavior>` element)
|
|
97
97
|
3. Run test - it MUST fail **intentionally** (#3770): the TARGET test you named must be the test that fails, on an assertion for the planned behavior. A nonzero exit alone is NOT RED — syntax errors, zero-test discovery, fixture crashes, parser errors, and unrelated assertions are INVALID_RED and must not authorize GREEN.
|
|
98
98
|
4. Persist the RED evidence from the actual command: `{command, exitCode, output, targetTest, targetFile, expected, actual}`. Preserve the real exit status and unmodified report; record the planned expected result and observed actual result. Write the record inside the project directory; the classifier refuses a path outside it. It reads only these fields, so the further fields below stay in the record for review.
|
|
99
|
-
- Select evidence by **report format**, regardless of runner or package manager. The classifier has a shared TAP adapter for Node's built-in test runner and Vitest (`tap` / `tap-flat`), a JUnit XML adapter for Maven Surefire/Failsafe, a swift-testing console adapter for `swift test`,
|
|
99
|
+
- Select evidence by **report format**, regardless of runner or package manager. The classifier has a shared TAP adapter for Node's built-in test runner and Vitest (`tap` / `tap-flat`), a JUnit XML adapter for Maven Surefire/Failsafe, a swift-testing console adapter for `swift test`, a Python `unittest` text adapter, and a `go test` text adapter. Other producers of these formats use the same adapters.
|
|
100
100
|
- TAP: capture the complete reporter output, including its test plan. Node summary comments are optional. The report must start with its TAP output: capture the reporter's own stdout (for npm scripts, `npm run --silent`), not package-manager banners. Use the target test's exact name; when names repeat, use its qualified name as emitted in the report (`tap-flat` emits only the full `file > suite > test` name). Suite/file summary failures are not target assertions. If the reporter is incompatible, rerun the planned target with a supported reporter through the project's test command, capture that real command and result, then submit the rerun's record to the classifier below. Preserve other console output separately rather than editing the report to make it parse.
|
|
101
101
|
- JUnit XML: capture the unchanged target report generated by the actual run. For Maven this is `target/surefire-reports/TEST-*.xml` or `target/failsafe-reports/TEST-*.xml`. Record the report path, run start, and modification time as `reportPath`, `runStartedAt`, and `reportModifiedAt`; require the report to be newer than the run start and verify the named target class or method executed in that run. Name the target as the method, the class (simple or package-qualified), or `<package.Class>#<method>`. Put one complete XML document in `output`, preserving console output alongside it. Missing, stale, or ambiguous reports require STOP and investigation.
|
|
102
|
+
- `go test`: capture the complete console output (`go test ./...` or `-v`; `-json` is not supported) including every package summary line (`FAIL\tpkg\t0.004s`). A failed test is its `--- FAIL: TestName` line; the same test name failing in two packages is ambiguous and needs the package-qualified target `import/path.TestName`. A package that does not compile (`FAIL\tpkg [build failed]`) or load (`[setup failed]`) asserted nothing — the usual cause is a test naming a symbol that does not exist yet — and the whole run is refused as `build_failure`, even if another package genuinely failed. Make the test compile and fail on its assertion, then record the run again.
|
|
102
103
|
- swift-testing and `unittest`: capture the complete console output without color (`NO_COLOR=1`), including the final summary. swift-testing needs one `Test` result line (passed, failed, skipped, or cancelled) for every test its `Test run with N tests` summary counts; it names a test by its quoted display name or, without one, by its function name (`addsNumbers()`), and either name identifies the target; repeated swift-testing names are ambiguous and block GREEN. `unittest` needs one `FAIL:`/`ERROR:` header for every failure or error its `FAILED (...)` line counts; repeated headers from one test's failing subTests count as one failing test, a method name that repeats across classes needs its `module.Class.method` id, and a `unittest.loader._FailedTest` entry is a load failure, never RED.
|
|
103
|
-
- Pass the unchanged record to `gsd_run check tdd-red-evidence <record.json> --raw` and require `RED_EVIDENCE_OK` before GREEN. Every `INVALID_RED` reason (`unexpected_green`, `zero_tests_discovered`, `nonzero_exit_without_test_failure`, `fixture_or_load_failure`, `no_target_test_failure`, `invalid_record`, `unreadable_record`) trips the gate. `invalid_record` includes unsupported or malformed reports; inspect `evidence.report_errors`. An unsupported format requires a supported reporter or a parser adapter before GREEN. Classifier rejection never authorizes a fallback to self-attestation.
|
|
104
|
+
- Pass the unchanged record to `gsd_run check tdd-red-evidence <record.json> --raw` and require `RED_EVIDENCE_OK` before GREEN. Every `INVALID_RED` reason (`unexpected_green`, `zero_tests_discovered`, `nonzero_exit_without_test_failure`, `fixture_or_load_failure`, `no_target_test_failure`, `build_failure`, `invalid_record`, `unreadable_record`) trips the gate. `invalid_record` includes unsupported or malformed reports; inspect `evidence.report_errors`. An unsupported format requires a supported reporter or a parser adapter before GREEN. Classifier rejection never authorizes a fallback to self-attestation.
|
|
104
105
|
- After machine validation, inspect whether the target actually executed and failed on the planned assertion for the intended reason. Zero tests, a missing, skipped, or cancelled target, setup, collection, import, syntax, or fixture faults, unrelated failures, unexpected green, and incomplete or ambiguous evidence block GREEN. Record this semantic assessment as `semanticAssessment` in the RED evidence and in the SUMMARY's RED entry; never fabricate reports, counters, or parser verdicts.
|
|
105
106
|
5. If test passes: feature exists or test is wrong. Investigate.
|
|
106
107
|
6. Commit: `test({phase}-{plan}): add failing test for [feature]`
|
|
@@ -341,7 +342,7 @@ TDD Plans: {count} | Gate violations: {count}
|
|
|
341
342
|
|
|
342
343
|
### What the Review Checks
|
|
343
344
|
|
|
344
|
-
1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order
|
|
345
|
+
1. **Gate sequence:** Each TDD plan has RED → GREEN commits in order. A plan is scored on its own `test(<plan>):` / `feat(<plan>):` commits, with one exception (#4867): a RED-only probe plan is satisfied by a `feat(...)` commit under a plan that declares it in `depends_on`, directly or through a chain. That is the gap-closure shape (probe plan, then the fix plan that depends on it). The plan's own RED is still required, a plan nothing depends on is scored alone, and the report names the plan that supplied the GREEN (`Plan {id} GREEN is supplied by dependent plan {id}.`).
|
|
345
346
|
2. **Test quality:** RED phase tests fail for the right reason (not import errors or syntax)
|
|
346
347
|
3. **Minimal GREEN:** Implementation is minimal — no premature optimization in GREEN phase
|
|
347
348
|
4. **Refactor discipline:** If REFACTOR commit exists, tests still pass
|
|
@@ -39,7 +39,12 @@ fi
|
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The handler returns JSON `{ skipped, blocking: false, total, honored,
|
|
42
|
-
not_honored: [...], message }
|
|
42
|
+
not_honored: [...], message }`, plus `reason` on a skip or a caller error. On a
|
|
43
|
+
caller error about the phase directory (`reason` is `'phase directory not found'`,
|
|
44
|
+
`'phase path is not a directory'`, `'phase directory unreadable'` or
|
|
45
|
+
`'phase directory holds no plans'`), `total` and `honored` are `null` and
|
|
46
|
+
`not_honored` is `[]`; nothing was checked, so there is no count to report.
|
|
47
|
+
A non-zero `DECISION_EXIT` (`69` `UNAVAILABLE`: the gate
|
|
43
48
|
could not read CONTEXT.md or the phase artifacts, #5170) is **not** "no decisions": record
|
|
44
49
|
`Decision coverage: not evaluated (the gate could not read its evidence)` in VERIFICATION.md
|
|
45
50
|
and do not report counts.
|
|
@@ -49,6 +54,8 @@ section) to VERIFICATION.md regardless of outcome — even when all
|
|
|
49
54
|
decisions are honored, recording the count helps reviewers spot drift over
|
|
50
55
|
time. Set `decision_coverage` in the verification result to
|
|
51
56
|
`{honored, total, not_honored: [...]}` so downstream tooling can read it.
|
|
57
|
+
On a caller error that is `{honored: null, total: null, not_honored: []}`;
|
|
58
|
+
pass the nulls through rather than coercing them to `0`.
|
|
52
59
|
|
|
53
60
|
**Status impact:** none. The decision gate does NOT influence the
|
|
54
61
|
`gaps_found` / `human_needed` / `passed` decision tree in Step 9. Its
|
|
@@ -55,7 +55,11 @@ Plans:
|
|
|
55
55
|
Apply the `project_code` prefix (if set in `.planning/config.json`) so the backlog directory name is consistent with all other phase-creation paths:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
|
|
58
|
+
IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
|
|
59
|
+
$ARGUMENTS
|
|
60
|
+
GSD_ARGS_END
|
|
61
|
+
GSD_ARGS=${GSD_ARGS%$'\n'}
|
|
62
|
+
SLUG=$(gsd_run query generate-slug "$GSD_ARGS" --raw)
|
|
59
63
|
PROJECT_CODE=$(gsd_run query config-get project_code --raw 2>/dev/null || echo "")
|
|
60
64
|
PREFIX=$([ -n "$PROJECT_CODE" ] && echo "${PROJECT_CODE}-" || echo "")
|
|
61
65
|
PHASE_DIR=".planning/phases/${PREFIX}${NEXT}-${SLUG}"
|
|
@@ -66,7 +70,11 @@ touch "${PHASE_DIR}/.gitkeep"
|
|
|
66
70
|
## Step 5: Commit
|
|
67
71
|
|
|
68
72
|
```bash
|
|
69
|
-
|
|
73
|
+
IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
|
|
74
|
+
$ARGUMENTS
|
|
75
|
+
GSD_ARGS_END
|
|
76
|
+
GSD_ARGS=${GSD_ARGS%$'\n'}
|
|
77
|
+
gsd_run query commit "docs: add backlog item ${NEXT} — ${GSD_ARGS}" --files .planning/ROADMAP.md "${PHASE_DIR}/.gitkeep"
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
## Step 6: Report
|