@opengsd/gsd-core 1.7.0-rc.1 → 1.7.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +711 -0
- package/GEMINI.md +25 -23
- package/README.md +3 -3
- package/agents/gsd-advisor-researcher.md +3 -1
- package/agents/gsd-ai-researcher.md +2 -2
- package/agents/gsd-assumptions-analyzer.md +2 -0
- package/agents/gsd-code-fixer.md +3 -1
- package/agents/gsd-code-reviewer.md +2 -0
- package/agents/gsd-codebase-mapper.md +2 -0
- package/agents/gsd-debug-session-manager.md +1 -1
- package/agents/gsd-debugger.md +3 -1
- package/agents/gsd-doc-writer.md +2 -0
- package/agents/gsd-domain-researcher.md +1 -1
- package/agents/gsd-eval-auditor.md +3 -1
- package/agents/gsd-executor.md +12 -9
- package/agents/gsd-integration-checker.md +2 -0
- package/agents/gsd-intel-updater.md +1 -1
- package/agents/gsd-mempalace-curator.md +1 -1
- package/agents/gsd-nyquist-auditor.md +2 -0
- package/agents/gsd-phase-researcher.md +4 -2
- package/agents/gsd-plan-checker.md +3 -1
- package/agents/gsd-planner.md +4 -2
- package/agents/gsd-project-researcher.md +4 -2
- package/agents/gsd-research-synthesizer.md +3 -1
- package/agents/gsd-roadmapper.md +2 -0
- package/agents/gsd-security-auditor.md +2 -0
- package/agents/gsd-ui-auditor.md +2 -0
- package/agents/gsd-ui-checker.md +2 -0
- package/agents/gsd-ui-researcher.md +4 -2
- package/agents/gsd-verifier.md +6 -4
- package/bin/install.js +226 -459
- package/commands/gsd/mempalace-capture.md +1 -1
- package/commands/gsd/mempalace-recall.md +9 -5
- package/commands/gsd/next.md +29 -0
- package/commands/gsd/ns-workflow.md +5 -2
- package/gsd-core/bin/ensure-runtime-build.cjs +246 -0
- package/gsd-core/bin/gsd-tools.cjs +43 -8
- package/gsd-core/bin/lib/capability-loader.cjs +27 -9
- package/gsd-core/bin/lib/capability-registry.cjs +209 -205
- package/gsd-core/bin/lib/capability-source.cjs +22 -7
- package/gsd-core/bin/lib/capability-validator.cjs +24 -2
- package/gsd-core/bin/lib/check-command-router.cjs +111 -1
- package/gsd-core/bin/lib/clusters.cjs +1 -0
- package/gsd-core/bin/lib/commands.cjs +2 -1
- package/gsd-core/bin/lib/config.cjs +19 -1
- package/gsd-core/bin/lib/core-utils.cjs +41 -0
- package/gsd-core/bin/lib/external-job.cjs +287 -0
- package/gsd-core/bin/lib/frontmatter.cjs +53 -6
- package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +151 -0
- package/gsd-core/bin/lib/graphify.cjs +41 -9
- package/gsd-core/bin/lib/handshake-serialized.cjs +70 -0
- package/gsd-core/bin/lib/host-integration-sdk.cjs +53 -0
- package/gsd-core/bin/lib/host-integration.cjs +61 -0
- package/gsd-core/bin/lib/init.cjs +34 -6
- package/gsd-core/bin/lib/milestone.cjs +56 -11
- package/gsd-core/bin/lib/phase-id.cjs +18 -0
- package/gsd-core/bin/lib/phase.cjs +51 -29
- package/gsd-core/bin/lib/phases-command-router.cjs +4 -3
- package/gsd-core/bin/lib/plan-scan.cjs +9 -1
- package/gsd-core/bin/lib/probe-core.cjs +44 -4
- package/gsd-core/bin/lib/profile-output.cjs +1 -1
- package/gsd-core/bin/lib/prohibition-enforcement.cjs +21 -19
- package/gsd-core/bin/lib/roadmap-command-router.cjs +3 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +21 -11
- package/gsd-core/bin/lib/roadmap.cjs +45 -22
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +18 -13
- package/gsd-core/bin/lib/runtime-name-policy.cjs +8 -10
- package/gsd-core/bin/lib/shell-command-projection.cjs +12 -8
- package/gsd-core/bin/lib/smart-entry.cjs +521 -0
- package/gsd-core/bin/lib/state-transition.cjs +15 -0
- package/gsd-core/bin/lib/state.cjs +27 -8
- package/gsd-core/bin/lib/surface.cjs +18 -9
- package/gsd-core/bin/lib/update-context.cjs +0 -5
- package/gsd-core/bin/lib/validate.cjs +2 -1
- package/gsd-core/bin/lib/verify.cjs +6 -4
- package/gsd-core/bin/lib/workstream-inventory-builder.cjs +12 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +28 -0
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/bin/shared/model-catalog.json +8 -13
- package/gsd-core/bin/shared/runtime-aliases.manifest.json +0 -5
- package/gsd-core/references/agent-skills-bootstrap.md +60 -0
- package/gsd-core/references/gates.md +1 -1
- package/gsd-core/references/model-profiles.md +27 -0
- package/gsd-core/references/planning-config.md +1 -1
- package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
- package/gsd-core/workflows/add-backlog.md +1 -1
- package/gsd-core/workflows/add-phase.md +1 -1
- package/gsd-core/workflows/add-tests.md +1 -1
- package/gsd-core/workflows/add-todo.md +1 -1
- package/gsd-core/workflows/ai-integration-phase.md +1 -1
- package/gsd-core/workflows/audit-fix.md +1 -1
- package/gsd-core/workflows/audit-milestone.md +1 -1
- package/gsd-core/workflows/audit-uat.md +1 -1
- package/gsd-core/workflows/autonomous.md +23 -25
- package/gsd-core/workflows/check-todos.md +1 -1
- package/gsd-core/workflows/cleanup.md +1 -1
- package/gsd-core/workflows/code-review-fix.md +1 -1
- package/gsd-core/workflows/code-review.md +1 -1
- package/gsd-core/workflows/complete-milestone.md +7 -11
- package/gsd-core/workflows/debug.md +1 -1
- package/gsd-core/workflows/diagnose-issues.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/auto.md +1 -1
- package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
- package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
- package/gsd-core/workflows/discuss-phase.md +2 -2
- package/gsd-core/workflows/do.md +1 -1
- package/gsd-core/workflows/docs-update.md +1 -1
- package/gsd-core/workflows/edit-phase.md +1 -1
- package/gsd-core/workflows/eval-review.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
- package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +1 -1
- package/gsd-core/workflows/execute-phase.md +5 -6
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/explore.md +1 -1
- package/gsd-core/workflows/extract-learnings.md +1 -1
- package/gsd-core/workflows/forensics.md +4 -4
- package/gsd-core/workflows/graduation.md +1 -1
- package/gsd-core/workflows/health.md +1 -1
- package/gsd-core/workflows/help/modes/full.md +15 -1
- package/gsd-core/workflows/help/modes/topic.md +1 -0
- package/gsd-core/workflows/import.md +1 -1
- package/gsd-core/workflows/ingest-docs.md +1 -1
- package/gsd-core/workflows/insert-phase.md +1 -1
- package/gsd-core/workflows/list-seeds.md +1 -1
- package/gsd-core/workflows/list-workspaces.md +1 -1
- package/gsd-core/workflows/manager.md +1 -1
- package/gsd-core/workflows/map-codebase.md +1 -1
- package/gsd-core/workflows/milestone-summary.md +4 -4
- package/gsd-core/workflows/mvp-phase.md +1 -1
- package/gsd-core/workflows/new-milestone.md +7 -1
- package/gsd-core/workflows/new-project.md +1 -1
- package/gsd-core/workflows/new-workspace.md +1 -1
- package/gsd-core/workflows/next.md +1 -1
- package/gsd-core/workflows/pause-work.md +1 -1
- package/gsd-core/workflows/plan-milestone-gaps.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +42 -0
- package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +102 -0
- package/gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md +23 -0
- package/gsd-core/workflows/plan-phase.md +12 -160
- package/gsd-core/workflows/plan-review-convergence.md +1 -1
- package/gsd-core/workflows/plant-seed.md +1 -1
- package/gsd-core/workflows/pr-branch.md +1 -1
- package/gsd-core/workflows/profile-user.md +1 -1
- package/gsd-core/workflows/progress.md +1 -1
- package/gsd-core/workflows/quick.md +1 -1
- package/gsd-core/workflows/reapply-patches.md +1 -1
- package/gsd-core/workflows/remove-phase.md +1 -1
- package/gsd-core/workflows/remove-workspace.md +1 -1
- package/gsd-core/workflows/resume-project.md +1 -1
- package/gsd-core/workflows/review.md +50 -9
- package/gsd-core/workflows/scan.md +1 -1
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +13 -11
- package/gsd-core/workflows/settings-integrations.md +1 -1
- package/gsd-core/workflows/settings.md +1 -1
- package/gsd-core/workflows/ship.md +1 -1
- package/gsd-core/workflows/sketch-wrap-up.md +1 -1
- package/gsd-core/workflows/sketch.md +1 -1
- package/gsd-core/workflows/smart-entry.md +120 -0
- package/gsd-core/workflows/spec-phase.md +8 -6
- package/gsd-core/workflows/spike-wrap-up.md +1 -1
- package/gsd-core/workflows/spike.md +1 -1
- package/gsd-core/workflows/stats.md +1 -1
- package/gsd-core/workflows/thread.md +1 -1
- package/gsd-core/workflows/transition.md +1 -1
- package/gsd-core/workflows/ui-phase.md +1 -1
- package/gsd-core/workflows/ui-review.md +1 -1
- package/gsd-core/workflows/ultraplan-phase.md +1 -1
- package/gsd-core/workflows/update.md +3 -6
- package/gsd-core/workflows/validate-phase.md +1 -1
- package/gsd-core/workflows/verify-phase.md +1 -1
- package/gsd-core/workflows/verify-work.md +63 -6
- package/package.json +5 -5
- package/scripts/ci-test-scope.cjs +19 -1
- package/scripts/generate-package-identity.cjs +27 -2
- package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -148
- package/scripts/lint-regression-test-names.allowlist.json +1 -273
- package/scripts/lint-test-file-count.allowlist.json +7 -91
- package/scripts/research-profiles.cjs +6 -6
- package/scripts/run-tests.cjs +67 -4
- package/scripts/slurm-adapter.cjs +269 -0
- package/scripts/sync-manifest-versions.cjs +67 -14
- package/skills/gsd-mempalace-capture/SKILL.md +1 -1
- package/skills/gsd-mempalace-recall/SKILL.md +9 -5
- package/skills/gsd-next/SKILL.md +30 -0
- package/skills/gsd-ns-workflow/SKILL.md +4 -1
- package/skills/gsd-review/SKILL.md +6 -0
- package/gemini-extension.json +0 -6
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Smart Entry — state-aware situation classifier for the /gsd front door.
|
|
4
|
+
*
|
|
5
|
+
* Reads project + workflow state (`.planning/STATE.md`, ROADMAP.md, git) and
|
|
6
|
+
* classifies the user's situation into one of a small set of enumerated values,
|
|
7
|
+
* each carrying a recommended next action and a menu of alternatives. Pure
|
|
8
|
+
* detection → classification; no side effects, no writes.
|
|
9
|
+
*
|
|
10
|
+
* Surfaced as `gsd-tools smart-entry [--json]`. The markdown `/gsd` command +
|
|
11
|
+
* workflow shells out to `smart-entry --json`, renders an AskUserQuestion menu
|
|
12
|
+
* (with a --text numbered-list fallback for non-Claude runtimes), and dispatches
|
|
13
|
+
* the chosen action to an existing slash command. See
|
|
14
|
+
* docs/superpowers/specs/2026-06-27-gsd-smart-entry-design.md and
|
|
15
|
+
* docs/adr/1787-gsd-next-smart-entry.md.
|
|
16
|
+
*
|
|
17
|
+
* Relationship to `/gsd:progress --next`: this classifier is a *menu* front door,
|
|
18
|
+
* not a second router. For in-project forward motion (planning → executing →
|
|
19
|
+
* verify-pending) the recommended action delegates to `/gsd:progress --next`
|
|
20
|
+
* (workflows/next.md) — the single, gated advancement engine (Route 0 resume-
|
|
21
|
+
* incomplete-phase invariant + Gates 1-3). smart-entry never re-derives forward
|
|
22
|
+
* routing itself; a standalone advancement command that bypassed those gates is
|
|
23
|
+
* exactly what got the old flat `/gsd-next` removed (#3054). Its distinct value is
|
|
24
|
+
* the states `--next` cannot reach: pre-project (no-project), remediation (paused,
|
|
25
|
+
* blocked, verify-failed), and lifecycle exits (idle-stranded, complete).
|
|
26
|
+
*
|
|
27
|
+
* Design note: this is the gsd-core analog of gsd-pi's `showSmartEntry` branch
|
|
28
|
+
* tree, redesigned for gsd-core's `.planning/` phase loop. gsd-pi's
|
|
29
|
+
* milestone/slice/task model does not exist here; the situation table is the
|
|
30
|
+
* translation, not a port.
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SITUATIONS = void 0;
|
|
37
|
+
exports.detectSignals = detectSignals;
|
|
38
|
+
exports.classify = classify;
|
|
39
|
+
exports.actionsFor = actionsFor;
|
|
40
|
+
exports.classifyProject = classifyProject;
|
|
41
|
+
exports.runSmartEntry = runSmartEntry;
|
|
42
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
43
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
44
|
+
const node_child_process_1 = require("node:child_process");
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const ioMod = require("./io.cjs");
|
|
47
|
+
const { output } = ioMod;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
49
|
+
const planningWorkspace = require("./planning-workspace.cjs");
|
|
50
|
+
const { planningPaths } = planningWorkspace;
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const frontmatter = require("./frontmatter.cjs");
|
|
53
|
+
const { extractFrontmatter } = frontmatter;
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
55
|
+
const stateDocument = require("./state-document.cjs");
|
|
56
|
+
const { stateExtractField } = stateDocument;
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
const phaseId = require("./phase-id.cjs");
|
|
59
|
+
const { comparePhaseNum, extractPhaseToken, normalizePhaseName, phaseTokenMatches } = phaseId;
|
|
60
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Staleness threshold for idle-stranded detection. A clean tree whose last
|
|
63
|
+
* activity is older than this (with non-complete status) is considered stranded
|
|
64
|
+
* committed-but-unshipped work. Hardcoded for v1; configurable later.
|
|
65
|
+
*/
|
|
66
|
+
const IDLE_STALE_MS = 72 * 60 * 60 * 1000; // 72h
|
|
67
|
+
/** @internal a single source of truth so the structure test can assert coverage. */
|
|
68
|
+
exports.SITUATIONS = Object.freeze([
|
|
69
|
+
'no-project',
|
|
70
|
+
'paused',
|
|
71
|
+
'blocked',
|
|
72
|
+
'verify-failed',
|
|
73
|
+
'needs-first-phase',
|
|
74
|
+
'planning',
|
|
75
|
+
'executing',
|
|
76
|
+
'verify-pending',
|
|
77
|
+
'idle-stranded',
|
|
78
|
+
'complete',
|
|
79
|
+
'unknown',
|
|
80
|
+
]);
|
|
81
|
+
// ─── Detection ─────────────────────────────────────────────────────────────────
|
|
82
|
+
/** Frontmatter scalar helper: prefer YAML frontmatter, fall back to body field. */
|
|
83
|
+
function fmScalar(fm, body, key, bodyField) {
|
|
84
|
+
const v = fm[key];
|
|
85
|
+
if (typeof v === 'string' && v.trim())
|
|
86
|
+
return v.trim();
|
|
87
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
88
|
+
return String(v);
|
|
89
|
+
return stateExtractField(body, bodyField);
|
|
90
|
+
}
|
|
91
|
+
/** Read a scalar value from a nested frontmatter object (e.g. progress.total_phases). */
|
|
92
|
+
function fmScalarKey(obj, key) {
|
|
93
|
+
if (!obj || typeof obj !== 'object')
|
|
94
|
+
return null;
|
|
95
|
+
const v = obj[key];
|
|
96
|
+
if (typeof v === 'string' && v.trim())
|
|
97
|
+
return v.trim();
|
|
98
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
99
|
+
return String(v);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
function parseIntOrNull(s) {
|
|
103
|
+
if (s === null)
|
|
104
|
+
return null;
|
|
105
|
+
const cleaned = s.replace('%', '').trim();
|
|
106
|
+
const n = Number.parseInt(cleaned, 10);
|
|
107
|
+
return Number.isFinite(n) ? n : null;
|
|
108
|
+
}
|
|
109
|
+
function phaseTokenFromDirName(name) {
|
|
110
|
+
const token = extractPhaseToken(name);
|
|
111
|
+
return /^\d+(?:[A-Z])?(?:\.\d+)*(?:-|$)/i.test(token) ? token : null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a `last_activity` value that may be an ISO date or a free-form string
|
|
115
|
+
* into an epoch-ms timestamp. Returns null when unparseable.
|
|
116
|
+
*/
|
|
117
|
+
function parseActivityTimestamp(raw) {
|
|
118
|
+
if (!raw)
|
|
119
|
+
return null;
|
|
120
|
+
const ms = Date.parse(raw);
|
|
121
|
+
return Number.isNaN(ms) ? null : ms;
|
|
122
|
+
}
|
|
123
|
+
/** Read-only git signals. Any git error is swallowed → "no git signal". */
|
|
124
|
+
function readGitSignals(cwd) {
|
|
125
|
+
const run = (args) => {
|
|
126
|
+
try {
|
|
127
|
+
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
128
|
+
cwd,
|
|
129
|
+
encoding: 'utf-8',
|
|
130
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
// Cheap "is this a git repo" probe that also doubles as the dirty check.
|
|
140
|
+
const status = run(['status', '--porcelain']);
|
|
141
|
+
if (status === '' && !run(['rev-parse', '--is-inside-work-tree']).trim()) {
|
|
142
|
+
return { has_git: false, dirty: false, unpushed: false };
|
|
143
|
+
}
|
|
144
|
+
const dirty = status.trim().length > 0;
|
|
145
|
+
// `git log @{u}..HEAD` lists commits on this branch not on its upstream.
|
|
146
|
+
// Non-empty ⇒ unpushed. Errors (no upstream, detached HEAD) ⇒ not unpushed.
|
|
147
|
+
const unpushed = run(['log', '@{u}..HEAD', '--oneline']).trim().length > 0;
|
|
148
|
+
return { has_git: true, dirty, unpushed };
|
|
149
|
+
}
|
|
150
|
+
/** Leading numeric phase token from a STATE.md scalar or body `Phase:` value. */
|
|
151
|
+
function phaseTokenFromState(raw) {
|
|
152
|
+
if (!raw?.trim())
|
|
153
|
+
return null;
|
|
154
|
+
const match = raw.trim().match(/^(\d+(?:[A-Z])?(?:\.\d+)*)/i);
|
|
155
|
+
return match ? match[1] : null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Detect whether the current phase's verify report indicates failure. The
|
|
159
|
+
* canonical signal is a phase summary/verify artifact whose STATUS: marker reads
|
|
160
|
+
* blocked, failed, or fail. Scoped to STATE.md's current phase so a leftover or
|
|
161
|
+
* newer phase tree cannot skew routing. Returns true only on a clear positive signal.
|
|
162
|
+
*/
|
|
163
|
+
function detectVerifyFailed(cwd, currentPhaseRaw) {
|
|
164
|
+
const phasesDir = node_path_1.default.join(planningPaths(cwd).phases);
|
|
165
|
+
let entries = [];
|
|
166
|
+
try {
|
|
167
|
+
entries = node_fs_1.default.readdirSync(phasesDir)
|
|
168
|
+
.map((name) => ({ name, phaseToken: phaseTokenFromDirName(name) }))
|
|
169
|
+
.filter((entry) => entry.phaseToken !== null)
|
|
170
|
+
.sort((a, b) => comparePhaseNum(a.phaseToken, b.phaseToken) || a.name.localeCompare(b.name))
|
|
171
|
+
.map((entry) => entry.name);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
if (entries.length === 0)
|
|
177
|
+
return false;
|
|
178
|
+
const phaseToken = phaseTokenFromState(currentPhaseRaw);
|
|
179
|
+
let targetDir;
|
|
180
|
+
if (phaseToken) {
|
|
181
|
+
const normalized = normalizePhaseName(phaseToken);
|
|
182
|
+
targetDir = entries.find((name) => phaseTokenMatches(name, normalized));
|
|
183
|
+
if (!targetDir)
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// No current phase in state — fall back to the highest-numbered phase dir.
|
|
188
|
+
targetDir = entries[entries.length - 1];
|
|
189
|
+
}
|
|
190
|
+
const latestDir = node_path_1.default.join(phasesDir, targetDir);
|
|
191
|
+
let files = [];
|
|
192
|
+
try {
|
|
193
|
+
files = node_fs_1.default.readdirSync(latestDir);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const candidates = files.filter((f) => /summary|verif(?:y|ication)|uat/i.test(f));
|
|
199
|
+
for (const name of candidates) {
|
|
200
|
+
let content = '';
|
|
201
|
+
try {
|
|
202
|
+
content = node_fs_1.default.readFileSync(node_path_1.default.join(latestDir, name), 'utf-8');
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const statusMatch = content.match(/STATUS:\s*([A-Za-z_-]+)/i);
|
|
208
|
+
if (statusMatch && /\b(blocked|fail(ed)?)\b/i.test(statusMatch[1])) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
/** Parse `.planning/STATE.md` (frontmatter + body) into the raw signals we need. */
|
|
215
|
+
function readStateFile(statePath) {
|
|
216
|
+
let content;
|
|
217
|
+
try {
|
|
218
|
+
content = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const fm = extractFrontmatter(content);
|
|
224
|
+
const body = content.replace(/^---[\s\S]*?---\s*/, '');
|
|
225
|
+
return { fm, body };
|
|
226
|
+
}
|
|
227
|
+
/** Collect the full signal set from disk for `cwd`. Exported for unit tests. */
|
|
228
|
+
function detectSignals(cwd, now = Date.now) {
|
|
229
|
+
const paths = planningPaths(cwd);
|
|
230
|
+
const hasPlanning = node_fs_1.default.existsSync(paths.planning);
|
|
231
|
+
const hasRoadmap = node_fs_1.default.existsSync(paths.roadmap);
|
|
232
|
+
const git = readGitSignals(cwd);
|
|
233
|
+
const empty = {
|
|
234
|
+
current_phase: null,
|
|
235
|
+
total_phases: null,
|
|
236
|
+
status: '',
|
|
237
|
+
progress: null,
|
|
238
|
+
has_planning: hasPlanning,
|
|
239
|
+
has_roadmap: hasRoadmap,
|
|
240
|
+
git_dirty: git.dirty,
|
|
241
|
+
git_unpushed: git.unpushed,
|
|
242
|
+
paused: false,
|
|
243
|
+
blockers: [],
|
|
244
|
+
has_git: git.has_git,
|
|
245
|
+
verify_failed: false,
|
|
246
|
+
stale_activity: false,
|
|
247
|
+
};
|
|
248
|
+
if (!hasPlanning)
|
|
249
|
+
return empty;
|
|
250
|
+
const parsed = readStateFile(paths.state);
|
|
251
|
+
if (!parsed)
|
|
252
|
+
return empty;
|
|
253
|
+
const { fm, body } = parsed;
|
|
254
|
+
// STATE.md has two lineages of schemas (see state.cjs parseProsePhaseField):
|
|
255
|
+
// - scalar frontmatter: `current_phase: 3`, `total_phases: 5`
|
|
256
|
+
// - nested frontmatter: `progress: { total_phases: 5, percent: 40 }`
|
|
257
|
+
// - body prose: `Phase: 3`, `**Status:** verifying`, `Total Phases: 5`
|
|
258
|
+
// Read each field across every form, scalar-first then nested then body, so
|
|
259
|
+
// the classifier works on real STATE.md files written by current GSD.
|
|
260
|
+
const statusRaw = fmScalar(fm, body, 'status', 'Status');
|
|
261
|
+
const pausedAtRaw = fmScalar(fm, body, 'paused_at', 'Paused At');
|
|
262
|
+
const lastActivityRaw = fmScalar(fm, body, 'last_activity', 'Last Activity');
|
|
263
|
+
// current_phase: scalar fm → nested (none) → body "Current Phase" → body "Phase".
|
|
264
|
+
// The body `Phase:` field is the canonical location in prose-form STATE.md
|
|
265
|
+
// (e.g. "Phase: 3" or "Phase: 3 — ui-review"); parse the leading number.
|
|
266
|
+
const currentPhaseRaw = fmScalar(fm, body, 'current_phase', 'Current Phase') ??
|
|
267
|
+
stateExtractField(body, 'Phase');
|
|
268
|
+
// total_phases & percent: nested `progress:` object takes precedence in the
|
|
269
|
+
// nested schema; scalar fm / body fields cover the flat schema.
|
|
270
|
+
const progressFm = typeof fm.progress === 'object' ? fm.progress : null;
|
|
271
|
+
const totalPhasesRaw = fmScalarKey(progressFm, 'total_phases') ??
|
|
272
|
+
fmScalar(fm, body, 'total_phases', 'Total Phases');
|
|
273
|
+
const progressRaw = fmScalarKey(progressFm, 'percent') ??
|
|
274
|
+
fmScalar(fm, body, 'progress', 'Progress');
|
|
275
|
+
// Blockers list: `- <text>` items under a `## Blockers` heading.
|
|
276
|
+
const blockers = [];
|
|
277
|
+
const blockersMatch = body.match(/##\s*Blockers\s*\n([\s\S]*?)(?=\n##|$)/i); // allow-adhoc-markdown: read-only blockers section-collect in smart-entry.cts; mirrors state.cts (#1372), pending collectSection migration
|
|
278
|
+
if (blockersMatch) {
|
|
279
|
+
const items = blockersMatch[1].match(/^-\s+(.+)$/gm) || [];
|
|
280
|
+
for (const item of items)
|
|
281
|
+
blockers.push(item.replace(/^-\s+/, '').trim());
|
|
282
|
+
}
|
|
283
|
+
const paused = Boolean(pausedAtRaw && pausedAtRaw.trim());
|
|
284
|
+
// Stale = no recorded activity for IDLE_STALE_MS. Used only by idle-stranded.
|
|
285
|
+
// Computed here (with the clock seam) so the pure classify() stays a function
|
|
286
|
+
// of (signals, staleActivity) and detectSignals owns all disk reads.
|
|
287
|
+
const lastActivityMs = parseActivityTimestamp(lastActivityRaw);
|
|
288
|
+
const staleActivity = lastActivityMs !== null && now() - lastActivityMs > IDLE_STALE_MS;
|
|
289
|
+
// Verify-failed may be signalled either by STATE.md status or by a failed
|
|
290
|
+
// STATUS: marker on the current phase's summary/verify artifact.
|
|
291
|
+
const verifyFailed = /\bverify-fail(ed)?|verification-fail|uat-fail\b/i.test(statusRaw || '') ||
|
|
292
|
+
detectVerifyFailed(cwd, currentPhaseRaw);
|
|
293
|
+
return {
|
|
294
|
+
current_phase: parseIntOrNull(currentPhaseRaw),
|
|
295
|
+
total_phases: parseIntOrNull(totalPhasesRaw),
|
|
296
|
+
status: (statusRaw || '').toLowerCase(),
|
|
297
|
+
progress: parseIntOrNull(progressRaw),
|
|
298
|
+
has_planning: hasPlanning,
|
|
299
|
+
has_roadmap: hasRoadmap,
|
|
300
|
+
git_dirty: git.dirty,
|
|
301
|
+
git_unpushed: git.unpushed,
|
|
302
|
+
paused,
|
|
303
|
+
blockers,
|
|
304
|
+
has_git: git.has_git,
|
|
305
|
+
verify_failed: verifyFailed,
|
|
306
|
+
stale_activity: staleActivity,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// ─── Situation classification ─────────────────────────────────────────────────
|
|
310
|
+
/** True when the workflow has fully completed all phases. */
|
|
311
|
+
function isComplete(s) {
|
|
312
|
+
if (s.total_phases === null || s.current_phase === null)
|
|
313
|
+
return false;
|
|
314
|
+
return s.current_phase >= s.total_phases && /\bcomplete(d)?|done|shipped\b/i.test(s.status);
|
|
315
|
+
}
|
|
316
|
+
/** Idle-stranded: clean tree, committed work not shipped, optionally stale. */
|
|
317
|
+
function isIdleStranded(s) {
|
|
318
|
+
if (/\bcomplete(d)?|done|shipped|paused\b/i.test(s.status))
|
|
319
|
+
return false;
|
|
320
|
+
if (s.git_dirty)
|
|
321
|
+
return false;
|
|
322
|
+
// Unpushed commits are the strongest signal. Otherwise require staleness.
|
|
323
|
+
return s.git_unpushed || s.stale_activity;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Classify signals into a situation. Priority order matters — the first
|
|
327
|
+
* matching predicate wins (e.g. paused beats blocked). Exported for unit tests.
|
|
328
|
+
* Pure function of signals (staleness + verify-failed are precomputed on signals).
|
|
329
|
+
*/
|
|
330
|
+
function classify(s) {
|
|
331
|
+
if (!s.has_planning)
|
|
332
|
+
return 'no-project';
|
|
333
|
+
if (s.paused)
|
|
334
|
+
return 'paused';
|
|
335
|
+
if (s.blockers.length > 0)
|
|
336
|
+
return 'blocked';
|
|
337
|
+
if (s.verify_failed)
|
|
338
|
+
return 'verify-failed';
|
|
339
|
+
if (s.total_phases === null || s.total_phases <= 0 || !s.has_roadmap)
|
|
340
|
+
return 'needs-first-phase';
|
|
341
|
+
if (isComplete(s))
|
|
342
|
+
return 'complete';
|
|
343
|
+
if (/\bplanning|planned\b/i.test(s.status))
|
|
344
|
+
return 'planning';
|
|
345
|
+
if (/\bexecut(e|ing)|active|in.progress|building\b/i.test(s.status))
|
|
346
|
+
return 'executing';
|
|
347
|
+
if (/\bverify|review|needs.review|pending.review\b/i.test(s.status))
|
|
348
|
+
return 'verify-pending';
|
|
349
|
+
if (isIdleStranded(s))
|
|
350
|
+
return 'idle-stranded';
|
|
351
|
+
return 'unknown';
|
|
352
|
+
}
|
|
353
|
+
// ─── Action sets ──────────────────────────────────────────────────────────────
|
|
354
|
+
const action = (id, label, command, recommended = false) => ({ id, label, command, recommended });
|
|
355
|
+
function rec(id, label, command) {
|
|
356
|
+
return action(id, label, command, true);
|
|
357
|
+
}
|
|
358
|
+
/** Per-situation ordered action list; recommended action first. */
|
|
359
|
+
function actionsFor(situation, s) {
|
|
360
|
+
const phaseN = s.current_phase ?? '';
|
|
361
|
+
const execLabel = phaseN === '' ? 'Continue executing' : `Continue executing phase ${phaseN}`;
|
|
362
|
+
switch (situation) {
|
|
363
|
+
case 'no-project':
|
|
364
|
+
return [
|
|
365
|
+
rec('new-project', 'Start a new project', '/gsd:new-project'),
|
|
366
|
+
action('map-codebase', 'Map an existing codebase', '/gsd:map-codebase'),
|
|
367
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
368
|
+
action('help', 'Show help', '/gsd:help'),
|
|
369
|
+
];
|
|
370
|
+
case 'paused':
|
|
371
|
+
return [
|
|
372
|
+
rec('resume-work', 'Resume work', '/gsd:resume-work'),
|
|
373
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
374
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
375
|
+
action('help', 'Show help', '/gsd:help'),
|
|
376
|
+
];
|
|
377
|
+
case 'blocked':
|
|
378
|
+
return [
|
|
379
|
+
rec('debug', 'Debug the blocker', '/gsd:debug'),
|
|
380
|
+
action('verify-work', 'Verify current work', '/gsd:verify-work'),
|
|
381
|
+
action('capture', 'Capture a note', '/gsd:capture'),
|
|
382
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
383
|
+
];
|
|
384
|
+
case 'verify-failed':
|
|
385
|
+
return [
|
|
386
|
+
rec('verify-work', 'Re-verify work', '/gsd:verify-work'),
|
|
387
|
+
action('debug', 'Debug the failure', '/gsd:debug'),
|
|
388
|
+
action('code-review', 'Review recent work', '/gsd:code-review'),
|
|
389
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
390
|
+
];
|
|
391
|
+
case 'needs-first-phase':
|
|
392
|
+
return [
|
|
393
|
+
rec('discuss-phase', 'Discuss the first phase', '/gsd:discuss-phase'),
|
|
394
|
+
action('plan-phase', 'Plan phase 1', '/gsd:plan-phase'),
|
|
395
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
396
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
397
|
+
];
|
|
398
|
+
case 'planning':
|
|
399
|
+
// Forward motion → delegate to the single gated engine (see 'executing').
|
|
400
|
+
return [
|
|
401
|
+
rec('progress-next', `Advance to the next step (plan phase ${phaseN || 1})`, '/gsd:progress --next'),
|
|
402
|
+
action('plan-phase', `Plan phase ${phaseN || 1}`, '/gsd:plan-phase'),
|
|
403
|
+
action('discuss-phase', 'Discuss before planning', '/gsd:discuss-phase'),
|
|
404
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
405
|
+
];
|
|
406
|
+
case 'executing':
|
|
407
|
+
// In-project forward motion delegates to the single gated engine
|
|
408
|
+
// (/gsd:progress --next → workflows/next.md). Its Route 0 resume-incomplete
|
|
409
|
+
// -phase invariant + Gates 1-3 must not be bypassed by dispatching a raw
|
|
410
|
+
// /gsd:execute-phase here — that divergence is why the old flat /gsd-next
|
|
411
|
+
// was removed (#3054). Direct execute stays as an explicit secondary choice.
|
|
412
|
+
return [
|
|
413
|
+
rec('progress-next', 'Advance to the next step', '/gsd:progress --next'),
|
|
414
|
+
action('execute-phase', execLabel, '/gsd:execute-phase'),
|
|
415
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
416
|
+
action('code-review', 'Review recent work', '/gsd:code-review'),
|
|
417
|
+
];
|
|
418
|
+
case 'verify-pending':
|
|
419
|
+
// Forward motion → delegate to the single gated engine (see 'executing').
|
|
420
|
+
return [
|
|
421
|
+
rec('progress-next', 'Advance to the next step (verify)', '/gsd:progress --next'),
|
|
422
|
+
action('verify-work', 'Verify work', '/gsd:verify-work'),
|
|
423
|
+
action('code-review', 'Review recent work', '/gsd:code-review'),
|
|
424
|
+
action('ship', 'Ship completed work', '/gsd:ship'),
|
|
425
|
+
];
|
|
426
|
+
case 'idle-stranded':
|
|
427
|
+
return [
|
|
428
|
+
rec('ship', 'Ship committed work', '/gsd:ship'),
|
|
429
|
+
action('complete-milestone', 'Complete the milestone', '/gsd:complete-milestone'),
|
|
430
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
431
|
+
action('capture', 'Capture a note', '/gsd:capture'),
|
|
432
|
+
];
|
|
433
|
+
case 'complete':
|
|
434
|
+
return [
|
|
435
|
+
rec('new-milestone', 'Start a new milestone', '/gsd:new-milestone'),
|
|
436
|
+
action('extract-learnings', 'Extract learnings', '/gsd:extract-learnings'),
|
|
437
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
438
|
+
action('progress', 'Show progress', '/gsd:progress'),
|
|
439
|
+
];
|
|
440
|
+
case 'unknown':
|
|
441
|
+
default:
|
|
442
|
+
return [
|
|
443
|
+
rec('progress', 'Show progress', '/gsd:progress'),
|
|
444
|
+
action('progress-next', 'Advance to the next step', '/gsd:progress --next'),
|
|
445
|
+
action('quick', 'Quick task', '/gsd:quick'),
|
|
446
|
+
action('help', 'Show help', '/gsd:help'),
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ─── Summary line ─────────────────────────────────────────────────────────────
|
|
451
|
+
function buildSummary(situation, s) {
|
|
452
|
+
switch (situation) {
|
|
453
|
+
case 'no-project':
|
|
454
|
+
return 'No project yet — start fresh or map an existing codebase';
|
|
455
|
+
case 'paused':
|
|
456
|
+
return 'Work is paused — pick up where you left off';
|
|
457
|
+
case 'blocked':
|
|
458
|
+
return `Blocked${s.blockers.length ? ` · ${s.blockers.length} blocker(s)` : ''} — resolve before continuing`;
|
|
459
|
+
case 'verify-failed':
|
|
460
|
+
return 'Recent verification failed — re-verify or debug';
|
|
461
|
+
case 'needs-first-phase':
|
|
462
|
+
return 'Project initialized — plan your first phase';
|
|
463
|
+
case 'planning':
|
|
464
|
+
return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'} — needs a plan`;
|
|
465
|
+
case 'executing':
|
|
466
|
+
return progressLine('executing', s);
|
|
467
|
+
case 'verify-pending':
|
|
468
|
+
return progressLine('ready to verify', s);
|
|
469
|
+
case 'idle-stranded':
|
|
470
|
+
return 'Committed work not shipped — time to ship';
|
|
471
|
+
case 'complete':
|
|
472
|
+
return 'All phases complete — start a new milestone';
|
|
473
|
+
case 'unknown':
|
|
474
|
+
default:
|
|
475
|
+
return 'Unsure of state — showing progress';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function progressLine(tail, s) {
|
|
479
|
+
const parts = [];
|
|
480
|
+
if (s.current_phase !== null && s.total_phases !== null) {
|
|
481
|
+
parts.push(`Phase ${s.current_phase} of ${s.total_phases}`);
|
|
482
|
+
}
|
|
483
|
+
else if (s.current_phase !== null) {
|
|
484
|
+
parts.push(`Phase ${s.current_phase}`);
|
|
485
|
+
}
|
|
486
|
+
if (s.progress !== null)
|
|
487
|
+
parts.push(`${s.progress}%`);
|
|
488
|
+
parts.push(tail);
|
|
489
|
+
return parts.join(' · ');
|
|
490
|
+
}
|
|
491
|
+
// ─── Orchestration ─────────────────────────────────────────────────────────────
|
|
492
|
+
/**
|
|
493
|
+
* Run the full detect → classify → assemble pipeline. Pure: no stdout, no
|
|
494
|
+
* writes. Exported for direct unit testing and reuse. The clock seam drives
|
|
495
|
+
* staleness detection inside detectSignals.
|
|
496
|
+
*/
|
|
497
|
+
function classifyProject(cwd, now = Date.now) {
|
|
498
|
+
const signals = detectSignals(cwd, now);
|
|
499
|
+
const situation = classify(signals);
|
|
500
|
+
const actions = actionsFor(situation, signals);
|
|
501
|
+
const recommended = actions.find((a) => a.recommended)?.id ?? 'progress';
|
|
502
|
+
const summary = buildSummary(situation, signals);
|
|
503
|
+
return { situation, recommended, summary, signals, actions };
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* `gsd-tools smart-entry` entry point. `--json` emits machine JSON; default
|
|
507
|
+
* emits a human-readable summary line. Never throws for a missing `.planning/`
|
|
508
|
+
* (returns `no-project`); other internal failures surface a typed error via
|
|
509
|
+
* `output()` so the workflow can fall back to `/gsd:progress`.
|
|
510
|
+
*/
|
|
511
|
+
function runSmartEntry(cwd, args, raw) {
|
|
512
|
+
const json = args.includes('--json');
|
|
513
|
+
const result = classifyProject(cwd);
|
|
514
|
+
if (json) {
|
|
515
|
+
output(result, raw, undefined);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Human mode: a compact one-liner plus the recommended action.
|
|
519
|
+
const human = `${result.summary}\nRecommended: ${result.recommended} → ${result.actions.find((a) => a.id === result.recommended)?.command ?? '/gsd:progress'}`;
|
|
520
|
+
output(null, true, human);
|
|
521
|
+
}
|
|
@@ -19,6 +19,7 @@ exports.STATE_MD_SECTIONS = exports.FIELD_CLASSIFICATION = void 0;
|
|
|
19
19
|
exports.getFieldClassification = getFieldClassification;
|
|
20
20
|
exports.applyStatePreservation = applyStatePreservation;
|
|
21
21
|
exports.transitionCore = transitionCore;
|
|
22
|
+
exports.sliceCurrentPositionSection = sliceCurrentPositionSection;
|
|
22
23
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
24
|
const frontmatter = require("./frontmatter.cjs");
|
|
24
25
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
@@ -319,6 +320,20 @@ function locateCurrentPosition(body) {
|
|
|
319
320
|
}
|
|
320
321
|
return { start, end };
|
|
321
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Return the body text of the `## Current Position` section, or `null` when it
|
|
325
|
+
* is absent. Reuses the fence-aware `locateCurrentPosition` locator (ADR-1372).
|
|
326
|
+
*
|
|
327
|
+
* Exposed so callers that must read a position field (e.g. `cmdStatePrune`,
|
|
328
|
+
* #1776) can scope extraction to the canonical section instead of the whole
|
|
329
|
+
* document — where `stateExtractField`'s pipe-table fallback could otherwise
|
|
330
|
+
* latch onto an unrelated `| Phase | N |` row elsewhere in STATE.md. This
|
|
331
|
+
* scopes the *caller*; the shared extractor is left broad for every other use.
|
|
332
|
+
*/
|
|
333
|
+
function sliceCurrentPositionSection(body) {
|
|
334
|
+
const span = locateCurrentPosition(body);
|
|
335
|
+
return span === null ? null : body.slice(span.start, span.end);
|
|
336
|
+
}
|
|
322
337
|
/**
|
|
323
338
|
* First-time ## Current Position mutation: update Phase / Plan / Status /
|
|
324
339
|
* Last activity lines. Mirrors state.cts:2261-2324 byte-for-behaviour
|
|
@@ -35,7 +35,7 @@ const { extractFrontmatter, reconstructFrontmatter } = frontmatter;
|
|
|
35
35
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
36
36
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
37
37
|
const stateTransitionMod = require("./state-transition.cjs");
|
|
38
|
-
const { transitionCore, applyStatePreservation } = stateTransitionMod;
|
|
38
|
+
const { transitionCore, applyStatePreservation, sliceCurrentPositionSection } = stateTransitionMod;
|
|
39
39
|
const state_document_cjs_1 = require("./state-document.cjs");
|
|
40
40
|
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
41
41
|
const STATE_PROGRESS_RESYNC_FIELDS = new Set([
|
|
@@ -1293,7 +1293,8 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|
|
1293
1293
|
// truth for total_phases (#549).
|
|
1294
1294
|
let roadmapPhaseCount = 0;
|
|
1295
1295
|
if (roadmapScope !== null) {
|
|
1296
|
-
|
|
1296
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
1297
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
1297
1298
|
let m;
|
|
1298
1299
|
while ((m = phaseHeadingPattern.exec(roadmapScope)) !== null) {
|
|
1299
1300
|
// Only count tokens that contain at least one digit — excludes
|
|
@@ -2245,7 +2246,8 @@ function cmdStateSync(cwd, options, raw) {
|
|
|
2245
2246
|
try {
|
|
2246
2247
|
let roadmapPhaseCount = 0;
|
|
2247
2248
|
if (syncRoadmapScope !== null) {
|
|
2248
|
-
|
|
2249
|
+
// #1729: `(?:\s*\([^)\n]*\))?` tolerates a pre-colon ( ) tag (literal mirror of OPTIONAL_PHASE_TAG_SOURCE).
|
|
2250
|
+
const phaseHeadingPattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)(?:\s*\([^)\n]*\))?\s*:/gi;
|
|
2249
2251
|
let m;
|
|
2250
2252
|
while ((m = phaseHeadingPattern.exec(syncRoadmapScope)) !== null) {
|
|
2251
2253
|
// Only count tokens that contain at least one digit — excludes
|
|
@@ -2321,12 +2323,29 @@ function cmdStatePrune(cwd, options, raw) {
|
|
|
2321
2323
|
}
|
|
2322
2324
|
const keepRecent = parseInt(String(options.keepRecent), 10) || 3;
|
|
2323
2325
|
const dryRun = !!options.dryRun;
|
|
2324
|
-
//
|
|
2325
|
-
// `Current Phase
|
|
2326
|
-
//
|
|
2327
|
-
//
|
|
2326
|
+
// Resolve the current phase via the same canonical chain buildStateFrontmatter
|
|
2327
|
+
// uses (frontmatter `current_phase` → `Current Phase` field → prose `Phase: X
|
|
2328
|
+
// of Y`), so prune engages on template-conformant STATE.md instead of bailing
|
|
2329
|
+
// "Only 0 phases" (#1760).
|
|
2330
|
+
// #1776: scope ONLY the prose `Phase:` term to the canonical `## Current
|
|
2331
|
+
// Position` section. Over the whole body, `stateExtractField`'s pipe-table
|
|
2332
|
+
// fallback matches any `| Phase | N |` row (e.g. a historical verification
|
|
2333
|
+
// table), resolving a stale phase and computing a wrong cutoff. Frontmatter and
|
|
2334
|
+
// the explicit `Current Phase` field are unambiguous, so they stay document-wide;
|
|
2335
|
+
// the shared extractor is not narrowed for any other caller.
|
|
2328
2336
|
const rawState = node_fs_1.default.readFileSync(statePath, 'utf-8');
|
|
2329
|
-
const
|
|
2337
|
+
const fm = extractFrontmatter(rawState);
|
|
2338
|
+
const body = stripFrontmatter(rawState);
|
|
2339
|
+
// Mirror buildStateFrontmatter's fmScalar: only string/number/boolean
|
|
2340
|
+
// frontmatter scalars are usable (an object/array `current_phase` is ignored,
|
|
2341
|
+
// which also avoids a base-to-string on a non-primitive).
|
|
2342
|
+
const fmRawPhase = fm.current_phase;
|
|
2343
|
+
const fmCurrentPhase = typeof fmRawPhase === 'string' ? (fmRawPhase.trim() || null)
|
|
2344
|
+
: typeof fmRawPhase === 'number' || typeof fmRawPhase === 'boolean' ? String(fmRawPhase)
|
|
2345
|
+
: null;
|
|
2346
|
+
const positionSection = sliceCurrentPositionSection(body);
|
|
2347
|
+
const prosePhase = positionSection !== null ? parseProsePhaseField((0, state_document_cjs_1.stateExtractField)(positionSection, 'Phase')).phase : null;
|
|
2348
|
+
const currentPhaseRaw = fmCurrentPhase ?? (0, state_document_cjs_1.stateExtractField)(body, 'Current Phase') ?? prosePhase;
|
|
2330
2349
|
const currentPhase = parseInt(String(currentPhaseRaw), 10) || 0;
|
|
2331
2350
|
const cutoff = currentPhase - keepRecent;
|
|
2332
2351
|
if (cutoff <= 0) {
|
|
@@ -466,16 +466,25 @@ function _syncGsdDir(stagedDir, destDir, kind, manifest) {
|
|
|
466
466
|
// - agents: only gsd-* are GSD-owned
|
|
467
467
|
// - flat command dirs: only `${kindPrefix}`-prefixed are GSD-owned
|
|
468
468
|
// - namespaced command dirs: the whole dir is GSD-owned
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
469
|
+
//
|
|
470
|
+
// Manifest gate (#2018): when the manifest is empty/absent (e.g. an unresolvable
|
|
471
|
+
// install source root yields an empty staged dir), the staged set is untrustworthy.
|
|
472
|
+
// Skills are guarded by pruneSkillDirs' manifest-membership check; agents must be
|
|
473
|
+
// guarded here — skip the prune loop entirely so an empty manifest never deletes
|
|
474
|
+
// every gsd-* agent. Copying (above) still runs so genuinely new agents are added.
|
|
475
|
+
const shouldPruneAgents = !(kindName === 'agents' && (!manifest || manifest.size === 0));
|
|
476
|
+
if (shouldPruneAgents) {
|
|
477
|
+
for (const file of node_fs_1.default.readdirSync(destDir).filter(f => f.endsWith('.md'))) {
|
|
478
|
+
if (kindName === 'agents' && !file.startsWith('gsd-'))
|
|
479
|
+
continue;
|
|
480
|
+
if (kindName === 'commands' && !namespacedByDir && kindPrefix && !file.startsWith(kindPrefix))
|
|
481
|
+
continue;
|
|
482
|
+
if (!stagedDestNames.has(file)) {
|
|
483
|
+
try {
|
|
484
|
+
node_fs_1.default.unlinkSync(node_path_1.default.join(destDir, file));
|
|
485
|
+
}
|
|
486
|
+
catch { /* ignore */ }
|
|
477
487
|
}
|
|
478
|
-
catch { /* ignore */ }
|
|
479
488
|
}
|
|
480
489
|
}
|
|
481
490
|
}
|