@opengsd/gsd-core 1.17.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 -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-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/install.js +38 -2
- package/commands/gsd/code-review.md +8 -0
- package/commands/gsd/pause-work.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +18 -11
- 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 +70 -64
- 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 +6 -4
- package/gsd-core/bin/lib/commands.cjs +5 -1
- 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-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/init.cjs +11 -4
- package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
- 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/phase.cjs +120 -11
- 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 +47 -2
- package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
- package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
- package/gsd-core/bin/lib/smart-entry.cjs +24 -16
- package/gsd-core/bin/lib/state-document.cjs +37 -3
- package/gsd-core/bin/lib/state-transition.cjs +31 -10
- package/gsd-core/bin/lib/state.cjs +90 -60
- 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/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 +5 -1
- 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/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 +37 -6
- 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 +5 -1
- 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-cursor-subagent-start.js +2 -1
- package/hooks/dist/gsd-secret-read-guard.js +72 -14
- package/hooks/dist/gsd-validate-commit.sh +27 -5
- package/hooks/gsd-agent-isolation-guard.js +3 -1
- package/hooks/gsd-cursor-subagent-start.js +2 -1
- package/hooks/gsd-secret-read-guard.js +72 -14
- 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/lib/macos-conformance-tier.generated.cjs +9 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
- package/scripts/lint-phase-arg-assignment.cjs +2 -2
- package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
- package/scripts/release-tarball-smoke.cjs +1 -0
- 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
|
@@ -14,6 +14,7 @@ exports.isUnfilledFieldValue = isUnfilledFieldValue;
|
|
|
14
14
|
exports.leadingCalendarDate = leadingCalendarDate;
|
|
15
15
|
exports.isRealCalendarDate = isRealCalendarDate;
|
|
16
16
|
exports.stateFieldContinuation = stateFieldContinuation;
|
|
17
|
+
exports.fieldContinuationEnd = fieldContinuationEnd;
|
|
17
18
|
exports.stateExtractField = stateExtractField;
|
|
18
19
|
exports.stateFieldValue = stateFieldValue;
|
|
19
20
|
exports.stateCurrentPositionSlice = stateCurrentPositionSlice;
|
|
@@ -378,7 +379,16 @@ function stateFieldContinuation(content, fieldName) {
|
|
|
378
379
|
if (!rest.startsWith('\n'))
|
|
379
380
|
return null; // end of file: nothing follows
|
|
380
381
|
const lines = rest.slice(1).split('\n').map((line) => line.replace(/\r$/, ''));
|
|
381
|
-
const continuation =
|
|
382
|
+
const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
|
|
383
|
+
return continuation.length ? continuation.join(' ') : null;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* How many of the lines directly below a single-line field belong to its value: the scan
|
|
387
|
+
* `stateFieldContinuation` reports on, shared so a writer that replaces the value removes exactly
|
|
388
|
+
* the lines a validator would call its continuation (#4945). `lines` are CR-free.
|
|
389
|
+
*/
|
|
390
|
+
function continuationLineCount(lines) {
|
|
391
|
+
let count = 0;
|
|
382
392
|
for (let i = 0; i < lines.length; i++) {
|
|
383
393
|
const line = lines[i];
|
|
384
394
|
if (!line.trim())
|
|
@@ -391,9 +401,33 @@ function stateFieldContinuation(content, fieldName) {
|
|
|
391
401
|
// title, so stop before consuming it rather than after.
|
|
392
402
|
if (i + 1 < lines.length && SETEXT_UNDERLINE_RE.test(lines[i + 1]))
|
|
393
403
|
break;
|
|
394
|
-
|
|
404
|
+
count++;
|
|
395
405
|
}
|
|
396
|
-
return
|
|
406
|
+
return count;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The index just past a single-line field's value AND the continuation lines that wrap it
|
|
410
|
+
* (#4945). `valueEnd` is where the field's own line ends (before its line terminator). Replacing
|
|
411
|
+
* `content.slice(valueEnd, result)` with nothing removes every continuation line and the
|
|
412
|
+
* terminator in front of each, so the terminator after the last one stays. Returns `valueEnd`
|
|
413
|
+
* when nothing continues the value.
|
|
414
|
+
*/
|
|
415
|
+
function fieldContinuationEnd(content, valueEnd) {
|
|
416
|
+
const tail = content.slice(valueEnd);
|
|
417
|
+
const first = /^\r?\n/.exec(tail);
|
|
418
|
+
if (!first)
|
|
419
|
+
return valueEnd;
|
|
420
|
+
const rawLines = tail.slice(first[0].length).split('\n');
|
|
421
|
+
const count = continuationLineCount(rawLines.map((line) => line.replace(/\r$/, '')));
|
|
422
|
+
if (count === 0)
|
|
423
|
+
return valueEnd;
|
|
424
|
+
let removed = first[0].length + (count - 1);
|
|
425
|
+
for (let i = 0; i < count; i++)
|
|
426
|
+
removed += rawLines[i].length;
|
|
427
|
+
// The terminator after the last continuation line stays; on a CRLF document that includes its CR.
|
|
428
|
+
if (rawLines[count - 1].endsWith('\r'))
|
|
429
|
+
removed -= 1;
|
|
430
|
+
return valueEnd + removed;
|
|
397
431
|
}
|
|
398
432
|
function stateExtractField(content, fieldName) {
|
|
399
433
|
const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
|
|
@@ -922,6 +922,8 @@ function transitionCore(content, intent, deps) {
|
|
|
922
922
|
*/
|
|
923
923
|
function beginPhaseCore(content, intent, deps) {
|
|
924
924
|
const updated = [];
|
|
925
|
+
// #4945: a re-entry whose earlier plans already have summaries starts at the first plan still to run.
|
|
926
|
+
const startPlan = intent.startPlan && intent.startPlan > 0 ? Math.floor(intent.startPlan) : 1;
|
|
925
927
|
// #1255: body-field replacements operate on body only (frontmatter stripped),
|
|
926
928
|
// not on the full content. The YAML `status:` key matches `^Status:\s*`
|
|
927
929
|
// before the body pipe-table row if full content is passed.
|
|
@@ -977,7 +979,7 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
977
979
|
if (intent.phaseName) {
|
|
978
980
|
tryField('Current Phase Name', intent.phaseName);
|
|
979
981
|
}
|
|
980
|
-
tryField('Current Plan',
|
|
982
|
+
tryField('Current Plan', String(startPlan));
|
|
981
983
|
if (intent.planCount) {
|
|
982
984
|
tryField('Total Plans in Phase', String(intent.planCount));
|
|
983
985
|
}
|
|
@@ -996,14 +998,21 @@ function beginPhaseCore(content, intent, deps) {
|
|
|
996
998
|
// end-of-line (inert here since `.` never crosses line terminators
|
|
997
999
|
// without `/s`, which is not set).
|
|
998
1000
|
const focusPattern = /^([ \t]*\*\*Current focus:\*\*[ \t]*)(.*)$/im;
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
+
const focusMatch = focusPattern.exec(body);
|
|
1002
|
+
if (focusMatch) {
|
|
1003
|
+
// #4945: a value an author wrapped onto further lines is replaced whole, so its tail is not
|
|
1004
|
+
// left behind under the new value. The lines that count are the ones `state validate` already
|
|
1005
|
+
// reports as a field's continuation (stops at a blank line, a heading, a list item, a table
|
|
1006
|
+
// row, a fence or a sibling field). An empty first line has no value to continue.
|
|
1007
|
+
const valueEnd = focusMatch.index + focusMatch[0].length;
|
|
1008
|
+
const end = focusMatch[2].trim() === '' ? valueEnd : (0, state_document_cjs_2.fieldContinuationEnd)(body, valueEnd);
|
|
1009
|
+
body = body.slice(0, focusMatch.index) + focusMatch[1] + focusLabel + body.slice(end);
|
|
1001
1010
|
updated.push('Current focus');
|
|
1002
1011
|
}
|
|
1003
1012
|
// ## Current Position section mutation (#1104, #1365).
|
|
1004
1013
|
// `locateCurrentPosition` (fence-aware, tokenizeHeadings-based) locates
|
|
1005
1014
|
// the section; mirrors state.cts:2261-2324 byte-for-behaviour.
|
|
1006
|
-
body = mutateCurrentPositionFirstTime(body, intent, today, updated);
|
|
1015
|
+
body = mutateCurrentPositionFirstTime(body, { ...intent, startPlan }, today, updated);
|
|
1007
1016
|
}
|
|
1008
1017
|
else {
|
|
1009
1018
|
// Resume path: only update Last activity timestamp in Current Position
|
|
@@ -1107,7 +1116,7 @@ function mutateCurrentPositionFirstTime(body, intent, today, updated) {
|
|
|
1107
1116
|
sectionBody = replaced;
|
|
1108
1117
|
}
|
|
1109
1118
|
// Plan line.
|
|
1110
|
-
const planValue =
|
|
1119
|
+
const planValue = `${intent.startPlan} of ${intent.planCount || '?'}`;
|
|
1111
1120
|
if (/^Plan:/m.test(sectionBody)) {
|
|
1112
1121
|
sectionBody = sectionBody.replace(/^Plan:.*$/m, `Plan: ${planValue}`);
|
|
1113
1122
|
}
|
|
@@ -1798,15 +1807,27 @@ function completePhaseCore(content, intent, deps) {
|
|
|
1798
1807
|
// Only runs when a Completed Phases field exists (the existing guard).
|
|
1799
1808
|
const completedRaw = (0, state_document_cjs_1.stateExtractField)(body, 'Completed Phases');
|
|
1800
1809
|
if (completedRaw !== null) {
|
|
1801
|
-
|
|
1810
|
+
const recordedCompleted = parseInt(completedRaw, 10);
|
|
1811
|
+
let newCompleted = recordedCompleted;
|
|
1802
1812
|
let derivedTotalPhases = null;
|
|
1803
1813
|
const roadmapContent = deps.roadmapProvider ? deps.roadmapProvider() : null;
|
|
1804
1814
|
if (roadmapContent) {
|
|
1805
1815
|
const derived = (0, phase_lifecycle_cjs_1.deriveProgressFromRoadmap)(roadmapContent);
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1816
|
+
// #4901: completed counters only ratchet up, like the frontmatter twin
|
|
1817
|
+
// (applyAuthoritativeProgressSubkeys, completedOnlyRaise). A derivation
|
|
1818
|
+
// that lands BELOW the recorded counter read a narrower scope than the
|
|
1819
|
+
// counter spans (e.g. the first `## Progress` section of a multi-milestone
|
|
1820
|
+
// roadmap), so its total is just as suspect: keep the recorded counter and
|
|
1821
|
+
// fall back to the recorded Total Phases for the percent below.
|
|
1822
|
+
const derivedLowered = derived.completedPhases !== null
|
|
1823
|
+
&& Number.isFinite(recordedCompleted)
|
|
1824
|
+
&& derived.completedPhases < recordedCompleted;
|
|
1825
|
+
if (!derivedLowered) {
|
|
1826
|
+
if (derived.completedPhases !== null)
|
|
1827
|
+
newCompleted = derived.completedPhases;
|
|
1828
|
+
if (derived.totalPhases !== null)
|
|
1829
|
+
derivedTotalPhases = derived.totalPhases;
|
|
1830
|
+
}
|
|
1810
1831
|
}
|
|
1811
1832
|
// #3057 B9: only mark 'Completed Phases' updated when the text actually
|
|
1812
1833
|
// changed. `stateReplaceField` returns the full (re-)substituted content
|
|
@@ -14,11 +14,14 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
14
14
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
15
15
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
16
|
const ioMod = require("./io.cjs");
|
|
17
|
-
const { output, error, declineNoOp, formatDiagnosticToken } = ioMod;
|
|
17
|
+
const { output, error, declineNoOp, formatDiagnosticToken, ERROR_REASON } = ioMod;
|
|
18
18
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
19
19
|
const cliExitModule = require("./cli-exit.cjs");
|
|
20
20
|
const { ExitError } = cliExitModule;
|
|
21
21
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
|
+
const capabilityLedgerMod = require("./capability-ledger.cjs");
|
|
23
|
+
const { readSmallRegularFile } = capabilityLedgerMod;
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
22
25
|
const stateContract = require("./state-contract.cjs");
|
|
23
26
|
const { publishStateContract } = stateContract;
|
|
24
27
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
@@ -316,19 +319,51 @@ function cmdStateGet(cwd, section, raw) {
|
|
|
316
319
|
output({ error: `Section or field "${section}" not found` }, raw, '');
|
|
317
320
|
}
|
|
318
321
|
}
|
|
319
|
-
|
|
322
|
+
/**
|
|
323
|
+
* #4926: cap on a `--<flag>-file` text input. Twice the 128 KiB one argv string
|
|
324
|
+
* can carry on Linux, so the file form never takes less than the inline form,
|
|
325
|
+
* and the entry echoed back on stdout stays small.
|
|
326
|
+
*/
|
|
327
|
+
const TEXT_FILE_MAX_BYTES = 256 * 1024;
|
|
328
|
+
/**
|
|
329
|
+
* Resolve a text argument that may instead be supplied as a file (`--<flag>`
|
|
330
|
+
* or `--<flag>-file`).
|
|
331
|
+
*
|
|
332
|
+
* #4926: the file is a read-once INPUT — its contents are copied into
|
|
333
|
+
* STATE.md and the path itself is never written, recorded, or resolved
|
|
334
|
+
* again — so it is not a project-write resource and is not confined to the
|
|
335
|
+
* project root. A scratch file from an agent's temp directory is accepted.
|
|
336
|
+
* A relative path still resolves against the project root.
|
|
337
|
+
*
|
|
338
|
+
* A file that cannot be read is a caller error and goes through the fault
|
|
339
|
+
* path — stderr, non-zero exit (USAGE), `--json-errors` envelope — rather than
|
|
340
|
+
* an `{ added: false }` payload at exit 0 that a shell caller gating on `$?`
|
|
341
|
+
* reads as success while nothing was written.
|
|
342
|
+
*
|
|
343
|
+
* An unconfined path is hostile input, so the read goes through
|
|
344
|
+
* `readSmallRegularFile`: a FIFO, a device (`/dev/zero`, a piped `/dev/stdin`)
|
|
345
|
+
* or a directory is refused without blocking, and a file over
|
|
346
|
+
* TEXT_FILE_MAX_BYTES is refused without being read.
|
|
347
|
+
*/
|
|
348
|
+
function readTextArgOrFile(cwd, value, filePath, flag) {
|
|
320
349
|
if (!filePath)
|
|
321
350
|
return value;
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const { assertWithinRoot, PathAcceptance } = require('./security.cjs');
|
|
325
|
-
const contained = assertWithinRoot(filePath, cwd, `${label} path`, PathAcceptance.AbsoluteInsideRoot);
|
|
351
|
+
const refuse = (why) => error(`--${flag}-file could not be read: ${formatDiagnosticToken(filePath)}${why ? ` (${why})` : ''}`, ERROR_REASON.USAGE);
|
|
352
|
+
let content;
|
|
326
353
|
try {
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
catch {
|
|
330
|
-
throw new Error(`${label} file not found: ${filePath}`);
|
|
354
|
+
content = readSmallRegularFile(node_path_1.default.resolve(cwd, filePath), TEXT_FILE_MAX_BYTES);
|
|
331
355
|
}
|
|
356
|
+
catch (err) {
|
|
357
|
+
const code = err.code;
|
|
358
|
+
if (code === 'EFBIG')
|
|
359
|
+
return refuse(`larger than ${TEXT_FILE_MAX_BYTES} bytes`);
|
|
360
|
+
if (code === 'EISDIR' || code === 'ENXIO')
|
|
361
|
+
return refuse('not a regular file');
|
|
362
|
+
return refuse(code);
|
|
363
|
+
}
|
|
364
|
+
if (content === null)
|
|
365
|
+
return refuse('ENOENT');
|
|
366
|
+
return content.trimEnd();
|
|
332
367
|
}
|
|
333
368
|
function cmdStatePatch(cwd, patches, raw) {
|
|
334
369
|
// Validate all field names before processing
|
|
@@ -1224,25 +1259,19 @@ function cmdStateUpdateProgress(cwd, raw) {
|
|
|
1224
1259
|
}
|
|
1225
1260
|
function cmdStateAddDecision(cwd, options, raw) {
|
|
1226
1261
|
const statePath = planningPaths(cwd).state;
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1262
|
+
// #4926: every refusal from this verb goes through the fault path (non-zero
|
|
1263
|
+
// exit), so a caller gating on `$?` never reads success while nothing was written.
|
|
1264
|
+
if (!node_fs_1.default.existsSync(statePath))
|
|
1265
|
+
return error('STATE.md not found', ERROR_REASON.USAGE);
|
|
1231
1266
|
const { phase, summary, summary_file, rationale, rationale_file } = options;
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1242
|
-
if (!summaryText) {
|
|
1243
|
-
output({ error: 'summary required' }, raw, undefined);
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1267
|
+
const summaryText = readTextArgOrFile(cwd, summary, summary_file, 'summary');
|
|
1268
|
+
// The rationale is optional: blank, inline or file, means "no rationale".
|
|
1269
|
+
const rationaleInput = readTextArgOrFile(cwd, rationale || '', rationale_file, 'rationale') || '';
|
|
1270
|
+
const rationaleText = rationaleInput.trim() ? rationaleInput : '';
|
|
1271
|
+
// A blank inline value is refused the same as a blank file (the file form is
|
|
1272
|
+
// trimEnd()ed by readTextArgOrFile, the inline form is not).
|
|
1273
|
+
if (!summaryText || !summaryText.trim())
|
|
1274
|
+
return error('summary required — pass a non-blank --summary or --summary-file', ERROR_REASON.USAGE);
|
|
1246
1275
|
// #3231/#3481: `--phase` omitted → resolve from the STATE.md being written, via
|
|
1247
1276
|
// the canonical ladder `state prune` uses. A decision entry is a permanent
|
|
1248
1277
|
// record, so a literal `[Phase ?]` written while `current_phase` sat three
|
|
@@ -1310,23 +1339,14 @@ function cmdStateAddDecision(cwd, options, raw) {
|
|
|
1310
1339
|
}
|
|
1311
1340
|
function cmdStateAddBlocker(cwd, text, raw) {
|
|
1312
1341
|
const statePath = planningPaths(cwd).state;
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1342
|
+
// #4926: every refusal from this verb goes through the fault path (non-zero
|
|
1343
|
+
// exit), so a caller gating on `$?` never reads success while nothing was written.
|
|
1344
|
+
if (!node_fs_1.default.existsSync(statePath))
|
|
1345
|
+
return error('STATE.md not found', ERROR_REASON.USAGE);
|
|
1317
1346
|
const blockerOptions = typeof text === 'object' && text !== null ? text : { text: text };
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
catch (err) {
|
|
1323
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
1324
|
-
return;
|
|
1325
|
-
}
|
|
1326
|
-
if (!blockerText) {
|
|
1327
|
-
output({ error: 'text required' }, raw, undefined);
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1347
|
+
const blockerText = readTextArgOrFile(cwd, blockerOptions.text, blockerOptions.text_file, 'text');
|
|
1348
|
+
if (!blockerText || !blockerText.trim())
|
|
1349
|
+
return error('text required — pass a non-blank --text or --text-file', ERROR_REASON.USAGE);
|
|
1330
1350
|
const entry = `- ${blockerText}`;
|
|
1331
1351
|
let _added = false;
|
|
1332
1352
|
let created = false;
|
|
@@ -1379,25 +1399,16 @@ function cmdStateAddBlocker(cwd, text, raw) {
|
|
|
1379
1399
|
}
|
|
1380
1400
|
function cmdStateAddRoadmapEvolution(cwd, options, raw) {
|
|
1381
1401
|
const statePath = planningPaths(cwd).state;
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1402
|
+
// #4926: every refusal from this verb goes through the fault path (non-zero
|
|
1403
|
+
// exit), so a caller gating on `$?` never reads success while nothing was written.
|
|
1404
|
+
if (!node_fs_1.default.existsSync(statePath))
|
|
1405
|
+
return error('STATE.md not found', ERROR_REASON.USAGE);
|
|
1386
1406
|
const { phase, action, after, note, note_file, urgent } = options;
|
|
1387
|
-
|
|
1388
|
-
try {
|
|
1389
|
-
noteText = readTextArgOrFile(cwd, note, note_file, 'note');
|
|
1390
|
-
}
|
|
1391
|
-
catch (err) {
|
|
1392
|
-
output({ added: false, reason: err.message }, raw, 'false');
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1407
|
+
const noteText = readTextArgOrFile(cwd, note, note_file, 'note');
|
|
1395
1408
|
// Reject missing / empty / whitespace-only notes — an evolution entry with no
|
|
1396
1409
|
// narrative is meaningless and would corrupt the section with a dangling bullet.
|
|
1397
|
-
if (!noteText || !noteText.trim())
|
|
1398
|
-
|
|
1399
|
-
return;
|
|
1400
|
-
}
|
|
1410
|
+
if (!noteText || !noteText.trim())
|
|
1411
|
+
return error('note required — pass a non-blank --note or --note-file', ERROR_REASON.USAGE);
|
|
1401
1412
|
// Flatten line breaks so the entry is always a single Markdown bullet. The
|
|
1402
1413
|
// dedupe + rendering contract is line-oriented; a multiline --note-file would
|
|
1403
1414
|
// otherwise spill continuation lines outside the bullet and defeat dedupe.
|
|
@@ -5097,6 +5108,23 @@ function cmdStateJson(cwd, raw) {
|
|
|
5097
5108
|
preferNewerLastActivity(existingFm, built);
|
|
5098
5109
|
output(built, raw, JSON.stringify(built, null, 2));
|
|
5099
5110
|
}
|
|
5111
|
+
/**
|
|
5112
|
+
* 1-based position of the first plan with no summary, when SOME of the phase's plans already have
|
|
5113
|
+
* one (a gap-closure re-entry: plans 1..17 are done and 18 and 19 are the gap plans, #4945).
|
|
5114
|
+
* `null` when no plan is summarized yet, when every plan is, or when the phase directory cannot be
|
|
5115
|
+
* found: the phase then starts at plan 1, exactly as before. Plans and summaries are paired by the
|
|
5116
|
+
* phase locator's own rule, so this agrees with `init` and `phase-plan-index`.
|
|
5117
|
+
*/
|
|
5118
|
+
function firstPlanToRun(cwd, phaseNumber) {
|
|
5119
|
+
const located = phaseLocatorMod.findPhaseInternal(cwd, String(phaseNumber));
|
|
5120
|
+
if (!located || !located.found)
|
|
5121
|
+
return null;
|
|
5122
|
+
const { plans, incomplete_plans: incomplete } = located;
|
|
5123
|
+
if (incomplete.length === 0 || incomplete.length === plans.length)
|
|
5124
|
+
return null;
|
|
5125
|
+
const index = plans.indexOf(incomplete[0]);
|
|
5126
|
+
return index === -1 ? null : index + 1;
|
|
5127
|
+
}
|
|
5100
5128
|
/**
|
|
5101
5129
|
* Update STATE.md when a new phase begins execution.
|
|
5102
5130
|
* Updates body text fields (Current focus, Status, Last Activity, Current Position)
|
|
@@ -5144,6 +5172,8 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
|
|
|
5144
5172
|
phaseNumber: phaseNumber,
|
|
5145
5173
|
phaseName: phaseName ?? null,
|
|
5146
5174
|
planCount: planCount ?? null,
|
|
5175
|
+
// #4945: a gap-closure re-entry starts at the first plan still to run, not at plan 1.
|
|
5176
|
+
startPlan: firstPlanToRun(cwd, phaseNumber),
|
|
5147
5177
|
};
|
|
5148
5178
|
const deps = {
|
|
5149
5179
|
clock: clock_cjs_1.realClock,
|
|
@@ -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
|
+
}
|
|
@@ -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
|