gentle-pi 2.7.0 → 3.0.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/README.md +24 -6
- package/assets/agents/gentle-ai-worker.md +5 -1
- package/assets/agents/sdd-apply.md +9 -7
- package/assets/agents/sdd-archive.md +42 -23
- package/assets/agents/sdd-proposal.md +2 -2
- package/assets/agents/sdd-remediate.md +4 -4
- package/assets/agents/sdd-research.md +20 -48
- package/assets/agents/sdd-tasks.md +5 -5
- package/assets/agents/sdd-verify.md +6 -28
- package/assets/chains/sdd-full.chain.md +4 -22
- package/assets/chains/sdd-verify.chain.md +3 -12
- package/assets/orchestrator-delegation.md +33 -3
- package/assets/orchestrator-memory.md +20 -7
- package/assets/orchestrator.md +5 -3
- package/assets/sdd-orchestrator-workflow.md +25 -58
- package/assets/support/sdd-status-contract.md +9 -12
- package/docs/gentle-shell.md +14 -4
- package/docs/readme-reference.md +171 -32
- package/extensions/codegraph-tools.ts +2 -0
- package/extensions/gentle-agents.ts +281 -361
- package/extensions/gentle-ai.ts +588 -117
- package/extensions/gentle-shell.ts +74 -30
- package/extensions/pi-pretty.ts +63 -14
- package/extensions/quiet-tools.ts +1 -2
- package/extensions/startup-banner.ts +10 -9
- package/lib/agent-home.ts +8 -0
- package/lib/agent-profile-pin.ts +336 -0
- package/lib/agent-profiles.ts +28 -8
- package/lib/agents-config.ts +24 -2
- package/lib/agents-history.ts +3 -97
- package/lib/agents-keys.ts +27 -0
- package/lib/agents-protocol.ts +2 -15
- package/lib/agents-runner.ts +67 -111
- package/lib/agents-session-transport.ts +691 -0
- package/lib/command-palette-catalog.ts +87 -0
- package/lib/command-palette.ts +346 -0
- package/lib/native-choice-list.ts +5 -0
- package/lib/native-review-cli.ts +10 -97
- package/lib/review-publication-gate.ts +11 -1
- package/lib/review-repository.ts +1 -1
- package/lib/review-snapshot.ts +1 -0
- package/lib/review-transaction.ts +4 -2
- package/lib/sdd-preflight.ts +2 -1
- package/lib/sdd-research-capabilities.ts +18 -152
- package/lib/sdd-status.ts +7 -779
- package/lib/session-change-capture.ts +2 -1
- package/lib/session-changes.ts +8 -1
- package/lib/shell-bar.ts +21 -12
- package/lib/shell-card.ts +8 -12
- package/lib/shell-changes.ts +3 -2
- package/lib/shell-prompt.ts +25 -8
- package/lib/shell-sidebar-banner.ts +2 -2
- package/lib/shell-sidebar-layout.ts +5 -2
- package/lib/windows-session-transport.ts +877 -0
- package/package.json +3 -3
- package/runtime/native-review-cli.mjs +9 -96
- package/runtime/windows-session-transport.ps1 +791 -0
- package/scripts/test-packed-runner.mjs +1668 -20
- package/scripts/verify-package-files.mjs +0 -1
- package/tests/agent-home.test.ts +52 -0
- package/tests/agent-profiles.test.ts +30 -1
- package/tests/agents-config.test.ts +44 -0
- package/tests/agents-history.test.ts +12 -24
- package/tests/agents-runner.test.ts +307 -58
- package/tests/agents-session-transport-process.test.ts +249 -0
- package/tests/agents-session-transport.test.ts +823 -0
- package/tests/artifact-language.test.ts +10 -7
- package/tests/command-palette.test.ts +378 -0
- package/tests/delegated-key-learnings-contract.test.ts +2 -2
- package/tests/fixtures/agents-session-transport-process.mjs +108 -0
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +54 -0
- package/tests/fixtures/windows-session-bootstrap.ps1 +129 -0
- package/tests/fixtures/windows-session-compile.ps1 +110 -0
- package/tests/gentle-agents.test.ts +849 -356
- package/tests/gentle-ai.test.ts +472 -4
- package/tests/gentle-shell.test.ts +201 -8
- package/tests/native-choice-list.test.ts +13 -0
- package/tests/native-review-cli.test.ts +0 -33
- package/tests/odd-routing-contract.test.ts +208 -0
- package/tests/orchestrator-budget.test.ts +17 -2
- package/tests/package-manifest.test.ts +115 -27
- package/tests/persona-single-channel.test.ts +3 -3
- package/tests/pi-pretty.test.ts +45 -0
- package/tests/profile-pin.test.ts +370 -0
- package/tests/quiet-tool-rendering.test.ts +32 -5
- package/tests/review-contract-prompt.test.ts +9 -0
- package/tests/review-controller.test.ts +0 -44
- package/tests/review-session-standing-permission-ipc.test.ts +427 -13
- package/tests/runtime-harness.mjs +4 -4
- package/tests/sdd-agent-tools.test.ts +15 -36
- package/tests/sdd-archive-replay.test.ts +82 -0
- package/tests/sdd-classical-continuation.test.ts +74 -0
- package/tests/sdd-execution-routing-contract.test.ts +18 -2
- package/tests/sdd-managed-runtime-settlement.test.ts +42 -330
- package/tests/sdd-native-managed-uptake.test.ts +11 -21
- package/tests/sdd-no-attempts-contract.test.ts +15 -0
- package/tests/sdd-odd-integration.test.ts +33 -0
- package/tests/sdd-optional-research.test.ts +124 -0
- package/tests/sdd-planning-routing-contract.test.ts +1 -1
- package/tests/sdd-preflight-rpc-input.test.ts +125 -0
- package/tests/sdd-preflight.test.ts +1 -1
- package/tests/sdd-research-capabilities.test.ts +20 -162
- package/tests/sdd-selection-transport.test.ts +180 -88
- package/tests/sdd-status.test.ts +5 -778
- package/tests/sdd-task-truth.test.ts +43 -0
- package/tests/session-change-capture.test.ts +20 -2
- package/tests/session-changes.test.ts +11 -0
- package/tests/shell-bar.test.ts +21 -0
- package/tests/shell-card.test.ts +8 -6
- package/tests/shell-changes.test.ts +8 -0
- package/tests/shell-prompt.test.ts +41 -7
- package/tests/shell-sidebar-banner.test.ts +4 -4
- package/tests/shell-sidebar-layout.test.ts +97 -13
- package/tests/startup-banner.test.ts +55 -2
- package/tests/windows-hidden-processes.test.ts +303 -0
- package/tests/windows-session-bootstrap.test.ts +1772 -0
- package/tests/windows-session-compile.test.ts +170 -0
- package/tests/windows-session-transport.test.ts +754 -0
- package/assets/agents/sdd-sync.md +0 -146
- package/lib/openspec-guardrails.ts +0 -99
- package/tests/native-sdd-attempt-authority.test.ts +0 -240
- package/tests/openspec-guardrails.test.ts +0 -71
package/lib/sdd-status.ts
CHANGED
|
@@ -1,798 +1,26 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, join, relative, resolve } from "node:path";
|
|
3
1
|
import type { NativeSddStatusV2 } from "./native-review-cli.ts";
|
|
4
|
-
import {
|
|
5
|
-
detectActiveDomainCollisions,
|
|
6
|
-
detectLegacyFlatSpec,
|
|
7
|
-
type DomainCollision,
|
|
8
|
-
} from "./openspec-guardrails.ts";
|
|
9
2
|
|
|
10
|
-
//
|
|
11
|
-
// mode was called "both" here; "hybrid" is the provider's name for the same
|
|
12
|
-
// thing, and normalizeSddArtifactStore keeps already-persisted "both" loading.
|
|
3
|
+
// Keep persisted legacy "both" normalization in sdd-preflight; native owns status.
|
|
13
4
|
export type SddArtifactStore = "openspec" | "engram" | "hybrid" | "none";
|
|
14
|
-
export type
|
|
15
|
-
export type DependencyState = "blocked" | "ready" | "all_done" | "not_applicable";
|
|
16
|
-
export type ApplyState = "blocked" | "ready" | "all_done" | "not_applicable";
|
|
17
|
-
export type SddPhase = "apply" | "verify" | "sync" | "archive";
|
|
18
|
-
export type SddNextRecommended =
|
|
19
|
-
| `sdd-${"propose" | "spec" | "design" | "tasks" | SddPhase}`
|
|
20
|
-
| "fix-task-ownership-marker"
|
|
21
|
-
| "archived"
|
|
22
|
-
| "resolve-via-engram"
|
|
23
|
-
| "blocked";
|
|
5
|
+
export type SddPhase = "apply" | "verify" | "archive";
|
|
24
6
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
specs: string[];
|
|
28
|
-
design: string[];
|
|
29
|
-
tasks: string[];
|
|
30
|
-
applyProgress: string[];
|
|
31
|
-
verifyReport: string[];
|
|
32
|
-
syncReport: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface SddTaskProgress {
|
|
36
|
-
total: number;
|
|
37
|
-
complete: number;
|
|
38
|
-
remaining: number;
|
|
39
|
-
unchecked: string[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const SDD_TASK_OWNER = {
|
|
43
|
-
IMPLEMENTATION: "implementation",
|
|
44
|
-
PARENT: "parent",
|
|
45
|
-
} as const;
|
|
46
|
-
|
|
47
|
-
type SddTaskOwner = (typeof SDD_TASK_OWNER)[keyof typeof SDD_TASK_OWNER];
|
|
48
|
-
|
|
49
|
-
interface SddTaskAccounting {
|
|
50
|
-
implementation: SddTaskProgress;
|
|
51
|
-
parent: SddTaskProgress;
|
|
52
|
-
errors: string[];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const EMPTY_TASK_PROGRESS: SddTaskProgress = {
|
|
56
|
-
total: 0,
|
|
57
|
-
complete: 0,
|
|
58
|
-
remaining: 0,
|
|
59
|
-
unchecked: [],
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export interface SddActionContext {
|
|
63
|
-
mode: "repo-local";
|
|
64
|
-
workspaceRoot: string;
|
|
65
|
-
allowedEditRoots: string[];
|
|
66
|
-
warnings: string[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface SddDomainCollisionReport {
|
|
70
|
-
domain: string;
|
|
71
|
-
changes: DomainCollision[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface SddPhaseInstructions {
|
|
75
|
-
apply: string[];
|
|
76
|
-
verify: string[];
|
|
77
|
-
sync: string[];
|
|
78
|
-
archive: string[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface SddRelationships {
|
|
82
|
-
dependsOn: string[];
|
|
83
|
-
supersedes: string[];
|
|
84
|
-
amends: string[];
|
|
85
|
-
conflictsWith: string[];
|
|
86
|
-
sameDomainActiveChanges: SddDomainCollisionReport[];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface SddStatus {
|
|
90
|
-
schemaName: "gentle-pi.sdd-status";
|
|
91
|
-
schemaVersion: 1;
|
|
92
|
-
changeName: string | null;
|
|
93
|
-
artifactStore: SddArtifactStore;
|
|
94
|
-
planningHome: { root: string; changesDir: string };
|
|
95
|
-
changeRoot: string | null;
|
|
96
|
-
artifactPaths: SddArtifactPaths;
|
|
97
|
-
contextFiles: SddArtifactPaths;
|
|
98
|
-
artifacts: Record<keyof SddArtifactPaths, ArtifactState>;
|
|
99
|
-
/** Implementation-owned progress; malformed markers remain unresolved here. */
|
|
100
|
-
taskProgress: SddTaskProgress;
|
|
101
|
-
/** Visible parent/orchestrator lifecycle actions, excluded from apply completion. */
|
|
102
|
-
deferredParentActions: SddTaskProgress;
|
|
103
|
-
/** Stable diagnostics for malformed ownership markers. */
|
|
104
|
-
taskArtifactErrors: string[];
|
|
105
|
-
applyState: ApplyState;
|
|
106
|
-
dependencies: Record<SddPhase, DependencyState>;
|
|
107
|
-
actionContext: SddActionContext;
|
|
108
|
-
relationships: SddRelationships;
|
|
109
|
-
collisions: SddDomainCollisionReport[];
|
|
110
|
-
legacyFlatSpec?: { path: string; hasDomainSpecs: boolean };
|
|
111
|
-
/**
|
|
112
|
-
* Positive terminal projection: the change was archived. `path` is the
|
|
113
|
-
* repo-relative archive folder (openspec/changes/archive/YYYY-MM-DD-<change>).
|
|
114
|
-
* When set, `nextRecommended` is "archived" and no further phase is recommended.
|
|
115
|
-
*/
|
|
116
|
-
archived?: { path: string };
|
|
117
|
-
nextRecommended: SddNextRecommended;
|
|
118
|
-
instructions?: SddPhaseInstructions;
|
|
119
|
-
blockedReasons: string[];
|
|
120
|
-
/**
|
|
121
|
-
* True when the native status engine is not authoritative for the selected
|
|
122
|
-
* artifact store (engram, none, or both without an openspec/ directory).
|
|
123
|
-
* When true, `dependencies`, `applyState`, and `blockedReasons` must not be
|
|
124
|
-
* treated as real blockers — resolve readiness from Engram instead.
|
|
125
|
-
* Defaults to false on all authoritative (openspec / both-with-disk) paths.
|
|
126
|
-
*/
|
|
127
|
-
isNonAuthoritative: boolean;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface ResolveSddStatusOptions {
|
|
131
|
-
cwd: string;
|
|
132
|
-
changeName?: string;
|
|
133
|
-
includeInstructions?: boolean;
|
|
134
|
-
workspaceRoot?: string;
|
|
135
|
-
artifactStore?: SddArtifactStore;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const EMPTY_PATHS: SddArtifactPaths = {
|
|
139
|
-
proposal: [],
|
|
140
|
-
specs: [],
|
|
141
|
-
design: [],
|
|
142
|
-
tasks: [],
|
|
143
|
-
applyProgress: [],
|
|
144
|
-
verifyReport: [],
|
|
145
|
-
syncReport: [],
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
function safeDirectories(path: string): string[] {
|
|
149
|
-
try {
|
|
150
|
-
return readdirSync(path)
|
|
151
|
-
.filter((entry) => {
|
|
152
|
-
try {
|
|
153
|
-
return statSync(join(path, entry)).isDirectory();
|
|
154
|
-
} catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
})
|
|
158
|
-
.sort();
|
|
159
|
-
} catch {
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function safeRead(path: string): string {
|
|
165
|
-
try {
|
|
166
|
-
return readFileSync(path, "utf8");
|
|
167
|
-
} catch {
|
|
168
|
-
return "";
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function hasContent(path: string): boolean {
|
|
173
|
-
return existsSync(path) && safeRead(path).trim().length > 0;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function singleFileState(paths: string[]): ArtifactState {
|
|
177
|
-
if (paths.length === 0) return "missing";
|
|
178
|
-
return paths.some((path) => !hasContent(path)) ? "partial" : "done";
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function multiFileState(paths: string[], partial = false): ArtifactState {
|
|
182
|
-
if (paths.length === 0) return partial ? "partial" : "missing";
|
|
183
|
-
return paths.some((path) => !hasContent(path)) || partial ? "partial" : "done";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function findSpecFiles(specsDir: string): string[] {
|
|
187
|
-
const files: string[] = [];
|
|
188
|
-
function walk(dir: string): void {
|
|
189
|
-
for (const entry of safeDirectories(dir)) {
|
|
190
|
-
const path = join(dir, entry);
|
|
191
|
-
const specPath = join(path, "spec.md");
|
|
192
|
-
if (existsSync(specPath)) files.push(specPath);
|
|
193
|
-
walk(path);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
walk(specsDir);
|
|
197
|
-
return files.sort();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function domainFromSpecPath(changeRoot: string, specPath: string): string | undefined {
|
|
201
|
-
const rel = relative(join(changeRoot, "specs"), specPath).split(/[\\/]/);
|
|
202
|
-
return rel.length >= 2 && rel.at(-1) === "spec.md" ? rel.slice(0, -1).join("/") : undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function taskProgress(lines: string[]): SddTaskProgress {
|
|
206
|
-
const unchecked = lines.filter((line) => !/^\s*- \[[xX]\]/.test(line));
|
|
207
|
-
return {
|
|
208
|
-
total: lines.length,
|
|
209
|
-
complete: lines.length - unchecked.length,
|
|
210
|
-
remaining: unchecked.length,
|
|
211
|
-
unchecked: unchecked.map((line) => line.trim()),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function countTasks(tasksPath: string | undefined): SddTaskAccounting {
|
|
216
|
-
if (!tasksPath || !existsSync(tasksPath)) {
|
|
217
|
-
return { implementation: { ...EMPTY_TASK_PROGRESS }, parent: { ...EMPTY_TASK_PROGRESS }, errors: [] };
|
|
218
|
-
}
|
|
219
|
-
const implementation: string[] = [];
|
|
220
|
-
const malformed: string[] = [];
|
|
221
|
-
const parent: string[] = [];
|
|
222
|
-
const errors: string[] = [];
|
|
223
|
-
for (const rawLine of safeRead(tasksPath).split(/\r?\n/)) {
|
|
224
|
-
const line = rawLine.trimEnd();
|
|
225
|
-
if (!/^\s*- \[(?: |x|X)\]/.test(line)) continue;
|
|
226
|
-
const markers = line.match(/<!--\s*sdd-owner\s*:[\s\S]*?-->/gi) ?? [];
|
|
227
|
-
const hasOwnerToken = /sdd-owner/i.test(line);
|
|
228
|
-
const canonical = /<!-- sdd-owner: (implementation|parent) -->\s*$/.exec(line);
|
|
229
|
-
const owner: SddTaskOwner | undefined = canonical?.[1] as SddTaskOwner | undefined;
|
|
230
|
-
if (!hasOwnerToken) {
|
|
231
|
-
implementation.push(line);
|
|
232
|
-
} else if (markers.length === 1 && canonical && canonical[0] === markers[0]) {
|
|
233
|
-
(owner === SDD_TASK_OWNER.PARENT ? parent : implementation).push(line);
|
|
234
|
-
} else {
|
|
235
|
-
malformed.push(line);
|
|
236
|
-
errors.push(`Malformed task ownership marker: ${line.trim()}`);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
const implementationProgress = taskProgress(implementation);
|
|
240
|
-
return {
|
|
241
|
-
implementation: {
|
|
242
|
-
total: implementationProgress.total + malformed.length,
|
|
243
|
-
complete: implementationProgress.complete,
|
|
244
|
-
remaining: implementationProgress.remaining + malformed.length,
|
|
245
|
-
unchecked: [...implementationProgress.unchecked, ...malformed.map((line) => line.trim())],
|
|
246
|
-
},
|
|
247
|
-
parent: taskProgress(parent),
|
|
248
|
-
errors,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function reportIsClearlyPassing(path: string | undefined): boolean {
|
|
253
|
-
if (!path || !hasContent(path)) return false;
|
|
254
|
-
const text = safeRead(path);
|
|
255
|
-
const hasBlocker = /(^|\b)(FAIL|FAILED|BLOCKED|CRITICAL|PENDING|TODO)(\b|:)|verification blockers?|not\s+(?:pass|passed|passing|successful|complete|completed)|(?:pass|passed|success|successful|complete|completed)\s*:\s*no\b/i.test(text);
|
|
256
|
-
const hasPassSignal = text
|
|
257
|
-
.split(/\r?\n/)
|
|
258
|
-
.map((line) => line.trim())
|
|
259
|
-
.some((line) =>
|
|
260
|
-
/^(?:(?:status|verdict|result|verification|sync|final(?:\s+verdict)?)\s*:\s*)?(?:PASS|PASSED|SUCCESS|SUCCESSFUL)$/i.test(line) ||
|
|
261
|
-
/^all checks passed\.?$/i.test(line) ||
|
|
262
|
-
/^ready for archive\.?$/i.test(line) ||
|
|
263
|
-
/^sync completed?\.?$/i.test(line),
|
|
264
|
-
);
|
|
265
|
-
return hasPassSignal && !hasBlocker;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Planning routes repair the first incomplete prerequisite; diagnostics remain separate.
|
|
269
|
-
function planningRecommendation(artifacts: SddStatus["artifacts"], taskTotal: number): SddNextRecommended {
|
|
270
|
-
if (artifacts.proposal !== "done") return "sdd-propose";
|
|
271
|
-
if (artifacts.specs !== "done") return "sdd-spec";
|
|
272
|
-
if (artifacts.design !== "done") return "sdd-design";
|
|
273
|
-
if (artifacts.tasks !== "done" || taskTotal === 0) return "sdd-tasks";
|
|
274
|
-
return "blocked";
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function emptyStatus(cwd: string, changeName: string | null, blockedReasons: string[], artifactStore: SddArtifactStore = "openspec", isNonAuthoritative = false): SddStatus {
|
|
278
|
-
const root = resolve(cwd);
|
|
279
|
-
const changesDir = join(root, "openspec", "changes");
|
|
280
|
-
const actionContext: SddActionContext = {
|
|
281
|
-
mode: "repo-local",
|
|
282
|
-
workspaceRoot: root,
|
|
283
|
-
allowedEditRoots: [root],
|
|
284
|
-
warnings: [],
|
|
285
|
-
};
|
|
286
|
-
return {
|
|
287
|
-
schemaName: "gentle-pi.sdd-status",
|
|
288
|
-
schemaVersion: 1,
|
|
289
|
-
changeName,
|
|
290
|
-
artifactStore,
|
|
291
|
-
planningHome: { root, changesDir },
|
|
292
|
-
changeRoot: null,
|
|
293
|
-
artifactPaths: { ...EMPTY_PATHS },
|
|
294
|
-
contextFiles: { ...EMPTY_PATHS },
|
|
295
|
-
artifacts: {
|
|
296
|
-
proposal: "missing",
|
|
297
|
-
specs: "missing",
|
|
298
|
-
design: "missing",
|
|
299
|
-
tasks: "missing",
|
|
300
|
-
applyProgress: "missing",
|
|
301
|
-
verifyReport: "missing",
|
|
302
|
-
syncReport: "missing",
|
|
303
|
-
},
|
|
304
|
-
taskProgress: { ...EMPTY_TASK_PROGRESS },
|
|
305
|
-
deferredParentActions: { ...EMPTY_TASK_PROGRESS },
|
|
306
|
-
taskArtifactErrors: [],
|
|
307
|
-
applyState: "blocked",
|
|
308
|
-
dependencies: { apply: "blocked", verify: "blocked", sync: "blocked", archive: "blocked" },
|
|
309
|
-
actionContext,
|
|
310
|
-
relationships: {
|
|
311
|
-
dependsOn: [],
|
|
312
|
-
supersedes: [],
|
|
313
|
-
amends: [],
|
|
314
|
-
conflictsWith: [],
|
|
315
|
-
sameDomainActiveChanges: [],
|
|
316
|
-
},
|
|
317
|
-
collisions: [],
|
|
318
|
-
nextRecommended: "blocked",
|
|
319
|
-
blockedReasons,
|
|
320
|
-
isNonAuthoritative,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Find the newest archive entry for a change. Archive folders are named
|
|
326
|
-
* `YYYY-MM-DD-<change>`; only an exact `-<change>` suffix after a full date
|
|
327
|
-
* prefix matches (change "foo-bar" never matches "2026-01-01-foo-bar-baz").
|
|
328
|
-
* safeDirectories sorts lexicographically, so the last match is the newest date.
|
|
329
|
-
*/
|
|
330
|
-
function findArchivedChangeEntry(root: string, changeName: string): string | undefined {
|
|
331
|
-
return safeDirectories(join(root, "openspec", "changes", "archive"))
|
|
332
|
-
.filter(
|
|
333
|
-
(entry) =>
|
|
334
|
-
/^\d{4}-\d{2}-\d{2}-$/.test(entry.slice(0, 11)) && entry.slice(11) === changeName,
|
|
335
|
-
)
|
|
336
|
-
.at(-1);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/** Positive terminal projection for a change whose folder moved to the archive. */
|
|
340
|
-
function archivedStatus(cwd: string, changeName: string, archiveEntry: string, artifactStore: SddArtifactStore): SddStatus {
|
|
341
|
-
const status = emptyStatus(cwd, changeName, [], artifactStore);
|
|
342
|
-
status.applyState = "all_done";
|
|
343
|
-
status.dependencies = { apply: "all_done", verify: "all_done", sync: "all_done", archive: "all_done" };
|
|
344
|
-
status.archived = { path: join("openspec", "changes", "archive", archiveEntry) };
|
|
345
|
-
status.nextRecommended = "archived";
|
|
346
|
-
return status;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function listActiveOpenSpecChanges(cwd: string): string[] {
|
|
350
|
-
return safeDirectories(join(cwd, "openspec", "changes")).filter(
|
|
351
|
-
(change) => change !== "archive",
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function renderPhaseInstructions(status: SddStatus): SddPhaseInstructions {
|
|
356
|
-
const change = status.changeName ?? "<unresolved>";
|
|
357
|
-
if (status.applyState === "not_applicable") {
|
|
358
|
-
return {
|
|
359
|
-
apply: ["Readiness is resolved from Engram; per-phase instructions not applicable."],
|
|
360
|
-
verify: ["Readiness is resolved from Engram; per-phase instructions not applicable."],
|
|
361
|
-
sync: ["Readiness is resolved from Engram; per-phase instructions not applicable."],
|
|
362
|
-
archive: ["Readiness is resolved from Engram; per-phase instructions not applicable."],
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
return {
|
|
366
|
-
apply: [
|
|
367
|
-
`Change: ${change}`,
|
|
368
|
-
`State: ${status.dependencies.apply}`,
|
|
369
|
-
status.applyState === "all_done"
|
|
370
|
-
? "All implementation tasks are checked complete; do not edit."
|
|
371
|
-
: "Implement only unchecked implementation-owned tasks from the tasks artifact.",
|
|
372
|
-
`Implementation tasks: ${status.taskProgress.complete}/${status.taskProgress.total} complete`,
|
|
373
|
-
"Update persisted task checkboxes for implementation-owned tasks immediately after completing each task.",
|
|
374
|
-
...status.taskProgress.unchecked.map((line) => `Remaining implementation task: ${line}`),
|
|
375
|
-
`Deferred parent lifecycle actions: ${status.deferredParentActions.complete}/${status.deferredParentActions.total} complete`,
|
|
376
|
-
...status.deferredParentActions.unchecked.map((line) => `Deferred parent action: ${line}`),
|
|
377
|
-
...status.taskArtifactErrors.map((error) => `Task artifact error: ${error}`),
|
|
378
|
-
],
|
|
379
|
-
verify: [
|
|
380
|
-
`Change: ${change}`,
|
|
381
|
-
`State: ${status.dependencies.verify}`,
|
|
382
|
-
"Verify task completion, spec coverage, implementation correctness, design coherence, and tests when available.",
|
|
383
|
-
"Unchecked implementation tasks are CRITICAL archive blockers.",
|
|
384
|
-
...status.taskProgress.unchecked.map((line) => `Unchecked blocker: ${line}`),
|
|
385
|
-
],
|
|
386
|
-
sync: [
|
|
387
|
-
`Change: ${change}`,
|
|
388
|
-
`State: ${status.dependencies.sync}`,
|
|
389
|
-
"Sync delta specs into openspec/specs only after verification is clean.",
|
|
390
|
-
...status.artifactPaths.specs.map((path) => `Delta spec: ${path}`),
|
|
391
|
-
...status.collisions.flatMap((collision) =>
|
|
392
|
-
collision.changes.map((item) =>
|
|
393
|
-
`Same-domain collision: ${collision.domain} also touched by ${item.change} (${item.path})`,
|
|
394
|
-
),
|
|
395
|
-
),
|
|
396
|
-
],
|
|
397
|
-
archive: [
|
|
398
|
-
`Change: ${change}`,
|
|
399
|
-
`State: ${status.dependencies.archive}`,
|
|
400
|
-
"Archive only after clean verify, completed sync, and zero unchecked implementation tasks.",
|
|
401
|
-
"CRITICAL verification issues have no override.",
|
|
402
|
-
status.changeRoot
|
|
403
|
-
? `Archive target: ${join(status.planningHome.changesDir, "archive", `YYYY-MM-DD-${change}`)}`
|
|
404
|
-
: "Archive target unavailable until change is resolved.",
|
|
405
|
-
],
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Build the single canonical non-authoritative SddStatus.
|
|
411
|
-
* All non-authoritative return sites must call this instead of constructing by hand.
|
|
412
|
-
*/
|
|
413
|
-
function nonAuthoritativeStatus(cwd: string, changeName: string | null, store: SddArtifactStore, includeInstructions?: boolean): SddStatus {
|
|
414
|
-
const root = resolve(cwd);
|
|
415
|
-
const actionContext: SddActionContext = {
|
|
416
|
-
mode: "repo-local",
|
|
417
|
-
workspaceRoot: root,
|
|
418
|
-
allowedEditRoots: [root],
|
|
419
|
-
warnings: [],
|
|
420
|
-
};
|
|
421
|
-
const status: SddStatus = {
|
|
422
|
-
schemaName: "gentle-pi.sdd-status",
|
|
423
|
-
schemaVersion: 1,
|
|
424
|
-
changeName,
|
|
425
|
-
artifactStore: store,
|
|
426
|
-
planningHome: { root, changesDir: "" },
|
|
427
|
-
changeRoot: null,
|
|
428
|
-
artifactPaths: { ...EMPTY_PATHS },
|
|
429
|
-
contextFiles: { ...EMPTY_PATHS },
|
|
430
|
-
artifacts: {
|
|
431
|
-
proposal: "missing",
|
|
432
|
-
specs: "missing",
|
|
433
|
-
design: "missing",
|
|
434
|
-
tasks: "missing",
|
|
435
|
-
applyProgress: "missing",
|
|
436
|
-
verifyReport: "missing",
|
|
437
|
-
syncReport: "missing",
|
|
438
|
-
},
|
|
439
|
-
taskProgress: { ...EMPTY_TASK_PROGRESS },
|
|
440
|
-
deferredParentActions: { ...EMPTY_TASK_PROGRESS },
|
|
441
|
-
taskArtifactErrors: [],
|
|
442
|
-
applyState: "not_applicable",
|
|
443
|
-
dependencies: { apply: "not_applicable", verify: "not_applicable", sync: "not_applicable", archive: "not_applicable" },
|
|
444
|
-
actionContext,
|
|
445
|
-
relationships: {
|
|
446
|
-
dependsOn: [],
|
|
447
|
-
supersedes: [],
|
|
448
|
-
amends: [],
|
|
449
|
-
conflictsWith: [],
|
|
450
|
-
sameDomainActiveChanges: [],
|
|
451
|
-
},
|
|
452
|
-
collisions: [],
|
|
453
|
-
nextRecommended: "resolve-via-engram",
|
|
454
|
-
blockedReasons: [],
|
|
455
|
-
isNonAuthoritative: true,
|
|
456
|
-
};
|
|
457
|
-
if (includeInstructions) status.instructions = renderPhaseInstructions(status);
|
|
458
|
-
return status;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
export function resolveSddStatus(options: ResolveSddStatusOptions): SddStatus {
|
|
462
|
-
// Safety net: when the store is unknown (undefined) and there is no openspec/ directory
|
|
463
|
-
// on disk, don't emit the openspec "no changes / blocked" status — it would be a false
|
|
464
|
-
// block for an engram or none session that hasn't been identified yet. Treat it as
|
|
465
|
-
// non-authoritative instead. A genuine openspec session will have the directory.
|
|
466
|
-
const hasOpenSpecDir = existsSync(join(resolve(options.cwd), "openspec"));
|
|
467
|
-
const store: SddArtifactStore =
|
|
468
|
-
options.artifactStore ?? (hasOpenSpecDir ? "openspec" : "none");
|
|
469
|
-
|
|
470
|
-
// Single decision point: non-authoritative when the disk engine cannot resolve authoritatively.
|
|
471
|
-
// Cases:
|
|
472
|
-
// - store engram or none: always non-authoritative (no disk backing)
|
|
473
|
-
// - store both, no openspec/ dir: non-authoritative (no disk to scan)
|
|
474
|
-
// The both-with-openspec cases are handled below after listing active changes.
|
|
475
|
-
if (store === "engram" || store === "none" || (store === "hybrid" && !hasOpenSpecDir)) {
|
|
476
|
-
const changeName = options.changeName?.trim() || null;
|
|
477
|
-
return nonAuthoritativeStatus(options.cwd, changeName, store, options.includeInstructions);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const root = resolve(options.cwd);
|
|
481
|
-
const changesDir = join(root, "openspec", "changes");
|
|
482
|
-
const activeChanges = listActiveOpenSpecChanges(root);
|
|
483
|
-
let changeName = options.changeName?.trim() || "";
|
|
484
|
-
const blockedReasons: string[] = [];
|
|
485
|
-
|
|
486
|
-
if (!changeName) {
|
|
487
|
-
if (activeChanges.length === 1) {
|
|
488
|
-
changeName = activeChanges[0];
|
|
489
|
-
} else if (activeChanges.length === 0) {
|
|
490
|
-
// store both + openspec/ present + zero active changes + no changeName:
|
|
491
|
-
// The change may live only in Engram — non-authoritative, not a false block.
|
|
492
|
-
// Pure openspec with zero changes is a real block (run sdd-new).
|
|
493
|
-
if (store === "hybrid") {
|
|
494
|
-
return nonAuthoritativeStatus(options.cwd, null, store, options.includeInstructions);
|
|
495
|
-
}
|
|
496
|
-
return emptyStatus(root, null, ["No active SDD changes found."], store);
|
|
497
|
-
} else {
|
|
498
|
-
// Multiple active changes and no changeName: legit selection prompt (changes DO exist
|
|
499
|
-
// on disk). Keep the existing authoritative ambiguous-selection behavior for both stores.
|
|
500
|
-
return emptyStatus(root, null, [
|
|
501
|
-
`Change selection is ambiguous: ${activeChanges.join(", ")}.`,
|
|
502
|
-
], store);
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (!activeChanges.includes(changeName)) {
|
|
507
|
-
// store both + openspec/ present + named change NOT found on disk:
|
|
508
|
-
// The change may live only in Engram — non-authoritative.
|
|
509
|
-
// Pure openspec still blocks (legit "run sdd-new").
|
|
510
|
-
if (store === "hybrid") {
|
|
511
|
-
return nonAuthoritativeStatus(options.cwd, changeName, store, options.includeInstructions);
|
|
512
|
-
}
|
|
513
|
-
// Issue #535: an absent active folder may mean the change was archived.
|
|
514
|
-
// Project explicit completion instead of a false "run sdd-new" block.
|
|
515
|
-
const archiveEntry = findArchivedChangeEntry(root, changeName);
|
|
516
|
-
if (archiveEntry) {
|
|
517
|
-
return archivedStatus(root, changeName, archiveEntry, store);
|
|
518
|
-
}
|
|
519
|
-
return emptyStatus(root, changeName, [`Active change not found: ${changeName}.`], store);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const changeRoot = join(changesDir, changeName);
|
|
523
|
-
const proposal = join(changeRoot, "proposal.md");
|
|
524
|
-
const design = join(changeRoot, "design.md");
|
|
525
|
-
const tasks = join(changeRoot, "tasks.md");
|
|
526
|
-
const applyProgress = join(changeRoot, "apply-progress.md");
|
|
527
|
-
const verifyReport = join(changeRoot, "verify-report.md");
|
|
528
|
-
const syncReport = join(changeRoot, "sync-report.md");
|
|
529
|
-
const specFiles = findSpecFiles(join(changeRoot, "specs"));
|
|
530
|
-
const legacyFlatSpec = detectLegacyFlatSpec(root, changeName);
|
|
531
|
-
const flatOnly = Boolean(legacyFlatSpec && specFiles.length === 0);
|
|
532
|
-
|
|
533
|
-
const artifactPaths: SddArtifactPaths = {
|
|
534
|
-
proposal: existsSync(proposal) ? [proposal] : [],
|
|
535
|
-
specs: specFiles,
|
|
536
|
-
design: existsSync(design) ? [design] : [],
|
|
537
|
-
tasks: existsSync(tasks) ? [tasks] : [],
|
|
538
|
-
applyProgress: existsSync(applyProgress) ? [applyProgress] : [],
|
|
539
|
-
verifyReport: existsSync(verifyReport) ? [verifyReport] : [],
|
|
540
|
-
syncReport: existsSync(syncReport) ? [syncReport] : [],
|
|
541
|
-
};
|
|
542
|
-
const artifacts = {
|
|
543
|
-
proposal: singleFileState(artifactPaths.proposal),
|
|
544
|
-
specs: multiFileState(artifactPaths.specs, flatOnly),
|
|
545
|
-
design: singleFileState(artifactPaths.design),
|
|
546
|
-
tasks: singleFileState(artifactPaths.tasks),
|
|
547
|
-
applyProgress: singleFileState(artifactPaths.applyProgress),
|
|
548
|
-
verifyReport: singleFileState(artifactPaths.verifyReport),
|
|
549
|
-
syncReport: singleFileState(artifactPaths.syncReport),
|
|
550
|
-
} satisfies SddStatus["artifacts"];
|
|
551
|
-
const taskAccounting = countTasks(artifactPaths.tasks[0]);
|
|
552
|
-
const taskProgress = taskAccounting.implementation;
|
|
553
|
-
const actionContext: SddActionContext = {
|
|
554
|
-
mode: "repo-local",
|
|
555
|
-
workspaceRoot: options.workspaceRoot ? resolve(options.workspaceRoot) : root,
|
|
556
|
-
allowedEditRoots: [options.workspaceRoot ? resolve(options.workspaceRoot) : root],
|
|
557
|
-
warnings: [],
|
|
558
|
-
};
|
|
559
|
-
|
|
560
|
-
const collisions = specFiles
|
|
561
|
-
.map((path) => domainFromSpecPath(changeRoot, path))
|
|
562
|
-
.filter((domain): domain is string => Boolean(domain))
|
|
563
|
-
.map((domain) => ({
|
|
564
|
-
domain,
|
|
565
|
-
changes: detectActiveDomainCollisions(root, changeName, domain).sort((a, b) =>
|
|
566
|
-
a.change.localeCompare(b.change),
|
|
567
|
-
),
|
|
568
|
-
}))
|
|
569
|
-
.filter((collision) => collision.changes.length > 0);
|
|
570
|
-
|
|
571
|
-
if (artifacts.proposal === "missing") blockedReasons.push("proposal.md is missing.");
|
|
572
|
-
if (artifacts.proposal === "partial") blockedReasons.push("proposal.md is empty or partial.");
|
|
573
|
-
if (artifacts.specs !== "done") blockedReasons.push("domain specs are missing or partial.");
|
|
574
|
-
if (artifacts.design === "missing") blockedReasons.push("design.md is missing.");
|
|
575
|
-
if (artifacts.design === "partial") blockedReasons.push("design.md is empty or partial.");
|
|
576
|
-
if (artifacts.tasks === "missing") blockedReasons.push("tasks.md is missing.");
|
|
577
|
-
if (artifacts.tasks === "partial") blockedReasons.push("tasks.md is empty or partial.");
|
|
578
|
-
if (artifacts.tasks === "done" && taskProgress.total === 0) {
|
|
579
|
-
blockedReasons.push("tasks.md has no implementation task checkboxes.");
|
|
580
|
-
}
|
|
581
|
-
blockedReasons.push(...taskAccounting.errors);
|
|
582
|
-
if (flatOnly && legacyFlatSpec) {
|
|
583
|
-
blockedReasons.push(`Legacy flat spec is present without domain specs: ${legacyFlatSpec.path}.`);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const coreArtifactsReady = artifacts.proposal === "done" && artifacts.specs === "done" && artifacts.design === "done" && artifacts.tasks === "done" && taskProgress.total > 0 && !flatOnly;
|
|
587
|
-
const taskArtifactBlocked = taskAccounting.errors.length > 0;
|
|
588
|
-
const applyState: ApplyState = !coreArtifactsReady || taskArtifactBlocked
|
|
589
|
-
? "blocked"
|
|
590
|
-
: taskProgress.remaining === 0
|
|
591
|
-
? "all_done"
|
|
592
|
-
: "ready";
|
|
593
|
-
const verifyClean = reportIsClearlyPassing(artifactPaths.verifyReport[0]);
|
|
594
|
-
const syncClean = reportIsClearlyPassing(artifactPaths.syncReport[0]);
|
|
595
|
-
const syncPrerequisitesReady = coreArtifactsReady && verifyClean && collisions.length === 0 && !flatOnly;
|
|
596
|
-
const syncState: DependencyState = syncPrerequisitesReady
|
|
597
|
-
? syncClean
|
|
598
|
-
? "all_done"
|
|
599
|
-
: "ready"
|
|
600
|
-
: "blocked";
|
|
601
|
-
const verifyState: DependencyState = verifyClean
|
|
602
|
-
? "all_done"
|
|
603
|
-
: artifacts.tasks === "done" && taskProgress.total > 0 && (artifacts.applyProgress === "done" || applyState === "all_done")
|
|
604
|
-
? "ready"
|
|
605
|
-
: "blocked";
|
|
606
|
-
const dependencies: SddStatus["dependencies"] = {
|
|
607
|
-
apply: applyState === "blocked" ? "blocked" : applyState,
|
|
608
|
-
verify: verifyState,
|
|
609
|
-
sync: syncState,
|
|
610
|
-
archive: coreArtifactsReady && verifyClean && syncClean && taskProgress.remaining === 0 && !taskArtifactBlocked ? "ready" : "blocked",
|
|
611
|
-
};
|
|
612
|
-
const archiveReady = dependencies.archive === "ready";
|
|
613
|
-
const nextRecommended: SddNextRecommended = taskArtifactBlocked
|
|
614
|
-
? "fix-task-ownership-marker"
|
|
615
|
-
: dependencies.apply === "ready"
|
|
616
|
-
? "sdd-apply"
|
|
617
|
-
: dependencies.verify === "ready"
|
|
618
|
-
? "sdd-verify"
|
|
619
|
-
: dependencies.sync === "ready"
|
|
620
|
-
? "sdd-sync"
|
|
621
|
-
: archiveReady
|
|
622
|
-
? "sdd-archive"
|
|
623
|
-
: planningRecommendation(artifacts, taskProgress.total);
|
|
624
|
-
|
|
625
|
-
const status: SddStatus = {
|
|
626
|
-
schemaName: "gentle-pi.sdd-status",
|
|
627
|
-
schemaVersion: 1,
|
|
628
|
-
changeName,
|
|
629
|
-
artifactStore: store,
|
|
630
|
-
planningHome: { root, changesDir },
|
|
631
|
-
changeRoot,
|
|
632
|
-
artifactPaths,
|
|
633
|
-
contextFiles: artifactPaths,
|
|
634
|
-
artifacts,
|
|
635
|
-
taskProgress,
|
|
636
|
-
deferredParentActions: taskAccounting.parent,
|
|
637
|
-
taskArtifactErrors: taskAccounting.errors,
|
|
638
|
-
applyState,
|
|
639
|
-
dependencies,
|
|
640
|
-
actionContext,
|
|
641
|
-
relationships: {
|
|
642
|
-
dependsOn: [],
|
|
643
|
-
supersedes: [],
|
|
644
|
-
amends: [],
|
|
645
|
-
conflictsWith: [],
|
|
646
|
-
sameDomainActiveChanges: collisions,
|
|
647
|
-
},
|
|
648
|
-
collisions,
|
|
649
|
-
legacyFlatSpec: legacyFlatSpec
|
|
650
|
-
? { path: legacyFlatSpec.path, hasDomainSpecs: specFiles.length > 0 }
|
|
651
|
-
: undefined,
|
|
652
|
-
nextRecommended,
|
|
653
|
-
blockedReasons,
|
|
654
|
-
isNonAuthoritative: false,
|
|
655
|
-
};
|
|
656
|
-
if (options.includeInstructions) status.instructions = renderPhaseInstructions(status);
|
|
657
|
-
return status;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
export function isNonAuthoritativeStatus(status: SddStatus): boolean {
|
|
661
|
-
return status.isNonAuthoritative;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
export function renderNativeSddPhasePrompt(status: SddStatus | NativeSddStatusV2, phase?: SddPhase | "remediate"): string {
|
|
665
|
-
const native = status.schemaName === "gentle-ai.sdd-status";
|
|
666
|
-
let selectedInstructions: readonly string[] | undefined;
|
|
667
|
-
if (phase) {
|
|
668
|
-
selectedInstructions = native
|
|
669
|
-
? phase === "sync" ? undefined : status.phaseInstructions?.[phase]
|
|
670
|
-
: phase === "remediate" ? undefined : status.instructions?.[phase];
|
|
671
|
-
}
|
|
672
|
-
const isNonAuthoritative = !native && isNonAuthoritativeStatus(status);
|
|
673
|
-
const authorityLine = isNonAuthoritative
|
|
674
|
-
? `This status is non-authoritative (artifact store: ${status.artifactStore}). The orchestrator must resolve readiness from Engram instead.`
|
|
675
|
-
: "The parent/orchestrator resolved this status deterministically. Treat it as authoritative over prompt inference.";
|
|
676
|
-
const blockLine = isNonAuthoritative
|
|
677
|
-
? `Do not block phase work based on this status — resolve readiness from Engram using the injected Engram memory read tools on the change topic keys (sdd/{change}/proposal, sdd/{change}/spec, sdd/{change}/design, sdd/{change}/tasks, etc.) instead.`
|
|
678
|
-
: "Do not run phase work when this status marks the phase blocked; return the blockers instead.";
|
|
7
|
+
export function renderNativeSddPhasePrompt(status: NativeSddStatusV2, phase?: SddPhase | "remediate"): string {
|
|
8
|
+
const instructions = phase ? status.phaseInstructions?.[phase] : undefined;
|
|
679
9
|
return [
|
|
680
10
|
"## Native SDD Status Engine",
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
...(phase &&
|
|
684
|
-
? ["", `### ${phase} instructions`, ...selectedInstructions.map((line) => `- ${line}`)]
|
|
685
|
-
: []),
|
|
686
|
-
"",
|
|
687
|
-
"```json",
|
|
688
|
-
JSON.stringify(status, null, 2),
|
|
689
|
-
"```",
|
|
690
|
-
].join("\n");
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
export function renderSddDispatcherMarkdown(status: SddStatus): string {
|
|
694
|
-
const isNonAuthoritative = isNonAuthoritativeStatus(status);
|
|
695
|
-
const statusSection = isNonAuthoritative
|
|
696
|
-
? [
|
|
697
|
-
"### Non-authoritative store — resolve via Engram",
|
|
698
|
-
`This status is non-authoritative (artifact store: ${status.artifactStore}).`,
|
|
699
|
-
"Resolve readiness directly from Engram using the injected Engram memory read tools on the change topic keys:",
|
|
700
|
-
`- sdd/${status.changeName ?? "<change>"}/proposal`,
|
|
701
|
-
`- sdd/${status.changeName ?? "<change>"}/spec`,
|
|
702
|
-
`- sdd/${status.changeName ?? "<change>"}/design`,
|
|
703
|
-
`- sdd/${status.changeName ?? "<change>"}/tasks`,
|
|
704
|
-
`- sdd/${status.changeName ?? "<change>"}/apply-progress (if present)`,
|
|
705
|
-
`- sdd/${status.changeName ?? "<change>"}/verify-report (if present)`,
|
|
706
|
-
"Do not treat blockedReasons or dependency states from this status as real blockers.",
|
|
707
|
-
].join("\n")
|
|
708
|
-
: status.blockedReasons.length > 0
|
|
709
|
-
? ["### Blocked", ...status.blockedReasons.map((reason) => `- ${reason}`)].join("\n")
|
|
710
|
-
: "### Ready\nThe next phase may be delegated with the attached status JSON and phase instructions.";
|
|
711
|
-
// For non-authoritative status, skip the unsafe SddPhase cast on nextRecommended
|
|
712
|
-
const instructionsSection = isNonAuthoritative
|
|
713
|
-
? []
|
|
714
|
-
: (status.instructions?.[status.nextRecommended.replace(/^sdd-/, "") as SddPhase] ?? []).map(
|
|
715
|
-
(line) => `- ${line}`,
|
|
716
|
-
);
|
|
717
|
-
return [
|
|
718
|
-
`## Native SDD Dispatcher: ${status.changeName ?? "unresolved"}`,
|
|
11
|
+
"Native status is the authoritative, read-only projection. Never re-derive readiness from artifacts or replace its recommended action.",
|
|
12
|
+
"Run only an admitted phase; report blocked dependencies and preserve every native blocker. Explicit optional verification does not change the recommended action.",
|
|
13
|
+
...(phase && instructions ? ["", `### ${phase} instructions`, ...instructions.map((line) => `- ${line}`)] : []),
|
|
719
14
|
"",
|
|
720
|
-
`nextPhase: ${status.nextRecommended}`,
|
|
721
|
-
`apply: ${status.dependencies.apply}`,
|
|
722
|
-
`verify: ${status.dependencies.verify}`,
|
|
723
|
-
`sync: ${status.dependencies.sync}`,
|
|
724
|
-
`archive: ${status.dependencies.archive}`,
|
|
725
|
-
"",
|
|
726
|
-
statusSection,
|
|
727
|
-
"",
|
|
728
|
-
...(instructionsSection.length > 0
|
|
729
|
-
? ["### Instructions for next phase", ...instructionsSection, ""]
|
|
730
|
-
: []),
|
|
731
|
-
"### Status JSON",
|
|
732
15
|
"```json",
|
|
733
16
|
JSON.stringify(status, null, 2),
|
|
734
17
|
"```",
|
|
735
18
|
].join("\n");
|
|
736
19
|
}
|
|
737
20
|
|
|
738
|
-
export function renderSddStatusMarkdown(status: SddStatus): string {
|
|
739
|
-
const title = status.changeName ?? "unresolved";
|
|
740
|
-
const lines = [
|
|
741
|
-
`## SDD Status: ${title}`,
|
|
742
|
-
"",
|
|
743
|
-
`schema: ${status.schemaName}@${status.schemaVersion}`,
|
|
744
|
-
`store: ${status.artifactStore}`,
|
|
745
|
-
`root: ${status.planningHome.root}`,
|
|
746
|
-
`next: ${status.nextRecommended}`,
|
|
747
|
-
"",
|
|
748
|
-
"### Implementation tasks",
|
|
749
|
-
`- complete: ${status.taskProgress.complete}/${status.taskProgress.total}`,
|
|
750
|
-
`- remaining: ${status.taskProgress.remaining}`,
|
|
751
|
-
...status.taskProgress.unchecked.map((line) => `- unchecked: ${line}`),
|
|
752
|
-
"",
|
|
753
|
-
"### Deferred parent lifecycle actions",
|
|
754
|
-
`- complete: ${status.deferredParentActions.complete}/${status.deferredParentActions.total}`,
|
|
755
|
-
`- remaining: ${status.deferredParentActions.remaining}`,
|
|
756
|
-
...status.deferredParentActions.unchecked.map((line) => `- deferred: ${line}`),
|
|
757
|
-
...status.taskArtifactErrors.map((error) => `- task artifact error: ${error}`),
|
|
758
|
-
"",
|
|
759
|
-
"### Dependencies",
|
|
760
|
-
...Object.entries(status.dependencies).map(([phase, state]) => `- ${phase}: ${state}`),
|
|
761
|
-
];
|
|
762
|
-
if (status.collisions.length > 0) {
|
|
763
|
-
lines.push("", "### Same-domain active changes");
|
|
764
|
-
for (const collision of status.collisions) {
|
|
765
|
-
lines.push(
|
|
766
|
-
`- ${collision.domain}: ${collision.changes.map((item) => item.change).join(", ")}`,
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
if (status.blockedReasons.length > 0) {
|
|
771
|
-
lines.push("", "### Blockers", ...status.blockedReasons.map((reason) => `- ${reason}`));
|
|
772
|
-
}
|
|
773
|
-
lines.push("", "### JSON", "```json", JSON.stringify(status, null, 2), "```");
|
|
774
|
-
return lines.join("\n");
|
|
775
|
-
}
|
|
776
|
-
|
|
777
21
|
export function parseSddStatusCommandArgs(args: string): { changeName?: string; json: boolean } {
|
|
778
22
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
779
23
|
const json = parts.includes("--json");
|
|
780
24
|
const changeName = parts.find((part) => part !== "--json");
|
|
781
25
|
return { changeName, json };
|
|
782
26
|
}
|
|
783
|
-
|
|
784
|
-
export function sddStatusSeverity(status: SddStatus): "info" | "warning" {
|
|
785
|
-
// Non-authoritative status has no real blockers — always info
|
|
786
|
-
if (isNonAuthoritativeStatus(status)) return "info";
|
|
787
|
-
return status.blockedReasons.length > 0 || Object.values(status.dependencies).includes("blocked")
|
|
788
|
-
? "warning"
|
|
789
|
-
: "info";
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
export function summarizeSddStatusForTitle(status: SddStatus): string {
|
|
793
|
-
return `${status.changeName ?? "unresolved"}: ${status.nextRecommended} (${status.taskProgress.complete}/${status.taskProgress.total} tasks)`;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
export function activeChangeLabel(cwd: string): string {
|
|
797
|
-
return basename(resolve(cwd));
|
|
798
|
-
}
|