gsdd-cli 0.25.0 → 0.27.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 +98 -548
- package/bin/adapters/claude.mjs +18 -8
- package/bin/adapters/opencode.mjs +3 -3
- package/bin/gsdd.mjs +13 -30
- package/bin/lib/closeout-report.mjs +32 -6
- package/bin/lib/control-map.mjs +94 -19
- package/bin/lib/global-install.mjs +629 -0
- package/bin/lib/global-manifest.mjs +122 -0
- package/bin/lib/health.mjs +2 -1
- package/bin/lib/init-flow.mjs +3 -0
- package/bin/lib/init-prompts.mjs +6 -4
- package/bin/lib/init-runtime.mjs +28 -2
- package/bin/lib/lifecycle-preflight.mjs +211 -5
- package/bin/lib/models.mjs +136 -5
- package/bin/lib/next.mjs +834 -0
- package/bin/lib/work-context.mjs +760 -0
- package/bin/lib/workflows.mjs +27 -0
- package/distilled/README.md +30 -3
- package/distilled/templates/agents.block.md +1 -1
- package/distilled/templates/agents.md +0 -1
- package/distilled/templates/codebase/architecture.md +0 -1
- package/distilled/templates/codebase/concerns.md +0 -1
- package/distilled/templates/codebase/conventions.md +0 -1
- package/distilled/templates/codebase/stack.md +0 -1
- package/distilled/templates/roadmap.md +0 -1
- package/distilled/templates/spec.md +0 -1
- package/distilled/workflows/new-project.md +1 -1
- package/docs/RUNTIME-SUPPORT.md +23 -3
- package/docs/USER-GUIDE.md +43 -4
- package/docs/VERIFICATION-DISCIPLINE.md +3 -1
- package/package.json +2 -2
package/bin/lib/next.mjs
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { output, parseFlagValue } from './cli-utils.mjs';
|
|
4
|
+
import { buildControlMap } from './control-map.mjs';
|
|
5
|
+
import {
|
|
6
|
+
NEXT_STATES,
|
|
7
|
+
addOpenQuestion,
|
|
8
|
+
answerQuestion,
|
|
9
|
+
captureDogfoodFinding,
|
|
10
|
+
ensureWorkStructure,
|
|
11
|
+
getWorkPaths,
|
|
12
|
+
inspectWorkContext,
|
|
13
|
+
readJsonIfExists,
|
|
14
|
+
rebuildGraphIndex,
|
|
15
|
+
recordDecision,
|
|
16
|
+
} from './work-context.mjs';
|
|
17
|
+
|
|
18
|
+
const NEXT_USAGE = [
|
|
19
|
+
'Usage:',
|
|
20
|
+
' gsdd next [--json] [--format auto|json|human]',
|
|
21
|
+
' gsdd next --init [--json] [--format auto|json|human]',
|
|
22
|
+
' gsdd next graph rebuild [--json] [--format auto|json|human]',
|
|
23
|
+
' gsdd next question add --id <id> --prompt <text> [--default <text>] [--gate <type>] [--blocking <true|false>] [--replace] [--json]',
|
|
24
|
+
' gsdd next question answer --id <id> --answer <text> [--json]',
|
|
25
|
+
' gsdd next decision record --id <id> --title <text> --body <text> [--supersedes <id>] [--privacy <public|repo|local_only|secret_risk>] [--replace] [--json]',
|
|
26
|
+
' gsdd next dogfood capture --id <id> --title <text> --body <text> [--backlog <pointer>] [--replace] [--json]',
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
function normalizeSlashes(value) {
|
|
30
|
+
return String(value || '').replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function removeFlags(args, flags) {
|
|
34
|
+
return args.filter((arg, index) => {
|
|
35
|
+
if (flags.includes(arg)) return false;
|
|
36
|
+
if (index > 0 && flags.includes(args[index - 1])) return false;
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function boolFlagValue(value, fallback = true) {
|
|
42
|
+
if (value === null || value === undefined) return fallback;
|
|
43
|
+
if (String(value).toLowerCase() === 'false') return false;
|
|
44
|
+
if (String(value).toLowerCase() === 'true') return true;
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function outputMode(args) {
|
|
49
|
+
if (args.includes('--json')) return 'json';
|
|
50
|
+
const format = parseFlagValue(args, '--format');
|
|
51
|
+
const value = format.value || 'auto';
|
|
52
|
+
if (!['auto', 'json', 'human'].includes(value)) {
|
|
53
|
+
throw new Error('Usage: gsdd next [--json] [--format auto|json|human]');
|
|
54
|
+
}
|
|
55
|
+
if (value === 'json') return 'json';
|
|
56
|
+
if (value === 'human') return 'human';
|
|
57
|
+
return process.stdout.isTTY ? 'human' : 'json';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function packet(overrides) {
|
|
61
|
+
const state = overrides.state || 'blocked';
|
|
62
|
+
if (!NEXT_STATES.includes(state)) throw new Error(`unsupported next state: ${state}`);
|
|
63
|
+
return {
|
|
64
|
+
schema_version: 1,
|
|
65
|
+
operation: 'next',
|
|
66
|
+
state,
|
|
67
|
+
reason: overrides.reason || '',
|
|
68
|
+
confidence: overrides.confidence || 'medium',
|
|
69
|
+
next_command: overrides.next_command || null,
|
|
70
|
+
next_action: overrides.next_action || null,
|
|
71
|
+
requires_user: Boolean(overrides.requires_user),
|
|
72
|
+
questions: overrides.questions || [],
|
|
73
|
+
constraints: overrides.constraints || [],
|
|
74
|
+
evidence_required: overrides.evidence_required || [],
|
|
75
|
+
artifacts_to_read: overrides.artifacts_to_read || [],
|
|
76
|
+
artifacts_to_write: overrides.artifacts_to_write || [],
|
|
77
|
+
error_code: overrides.error_code || null,
|
|
78
|
+
repair_action: overrides.repair_action || null,
|
|
79
|
+
repair_targets: overrides.repair_targets || [],
|
|
80
|
+
repo_warnings: overrides.repo_warnings || [],
|
|
81
|
+
privacy_notes: overrides.privacy_notes || [],
|
|
82
|
+
inputs_considered: overrides.inputs_considered || [],
|
|
83
|
+
inputs_skipped: overrides.inputs_skipped || [],
|
|
84
|
+
trace_refs: overrides.trace_refs || [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function cliAction(argv, description) {
|
|
89
|
+
return {
|
|
90
|
+
type: 'cli_command',
|
|
91
|
+
command: ['gsdd', ...argv].join(' '),
|
|
92
|
+
argv,
|
|
93
|
+
description,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function workflowAction(skillId, description) {
|
|
98
|
+
return {
|
|
99
|
+
type: 'workflow_skill',
|
|
100
|
+
skill_id: skillId,
|
|
101
|
+
description,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function manualReviewAction(targets, description) {
|
|
106
|
+
return {
|
|
107
|
+
type: 'manual_review',
|
|
108
|
+
targets,
|
|
109
|
+
description,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function userQuestionAction(questionIds, description) {
|
|
114
|
+
return {
|
|
115
|
+
type: 'user_question',
|
|
116
|
+
question_ids: questionIds,
|
|
117
|
+
description,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function summarizeControlMap(cwd) {
|
|
122
|
+
try {
|
|
123
|
+
return buildControlMap({
|
|
124
|
+
workspaceRoot: cwd,
|
|
125
|
+
planningDir: join(cwd, '.planning'),
|
|
126
|
+
});
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
operation: 'control-map',
|
|
130
|
+
error: error.message,
|
|
131
|
+
risks: [{ code: 'control_map_failed', severity: 'warn', message: error.message }],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function readManifestStatus(context) {
|
|
137
|
+
if (!context.evidence.ok) {
|
|
138
|
+
return {
|
|
139
|
+
manifest: null,
|
|
140
|
+
error: context.evidence.error,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return { manifest: context.evidence.value || {}, error: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function repoWarningsFromControlMap(controlMap) {
|
|
147
|
+
return (controlMap.risks || [])
|
|
148
|
+
.filter((risk) => risk.code === 'canonical_dirty')
|
|
149
|
+
.map((risk) => risk.message);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function routeFromStateObject(stateValue) {
|
|
153
|
+
const state = stateValue || {};
|
|
154
|
+
const workflow = state.workflow || state.milestone || state;
|
|
155
|
+
if (workflow.status === 'paused') return { state: 'pause', reason: 'Local `.work/state.json` marks work as paused.' };
|
|
156
|
+
if (workflow.status === 'blocked') return { state: 'blocked', reason: 'Local `.work/state.json` marks work as blocked.' };
|
|
157
|
+
if (workflow.human_gate && workflow.human_gate.approved !== true) {
|
|
158
|
+
return {
|
|
159
|
+
state: 'ask_user',
|
|
160
|
+
reason: workflow.human_gate.reason || 'A recorded human gate requires approval before continuing.',
|
|
161
|
+
questions: workflow.human_gate.question ? [workflow.human_gate] : [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (workflow.plan?.approved === true && workflow.execution?.status !== 'complete') {
|
|
165
|
+
return { state: 'execute', reason: 'A plan is approved and execution is not complete.' };
|
|
166
|
+
}
|
|
167
|
+
if (workflow.execution?.status === 'complete' && workflow.verification?.status !== 'passed') {
|
|
168
|
+
return { state: 'verify', reason: 'Execution is complete and verification has not passed.' };
|
|
169
|
+
}
|
|
170
|
+
if (workflow.verification?.status === 'gaps_found' || workflow.audit?.status === 'gaps_found') {
|
|
171
|
+
return { state: 'fix_gaps', reason: 'Verification or audit recorded gaps.' };
|
|
172
|
+
}
|
|
173
|
+
if (workflow.verification?.status === 'passed' && workflow.audit?.status !== 'passed') {
|
|
174
|
+
return { state: 'audit', reason: 'Verification passed and milestone audit has not passed.' };
|
|
175
|
+
}
|
|
176
|
+
if (workflow.audit?.status === 'passed' && workflow.dogfood?.status !== 'captured') {
|
|
177
|
+
return { state: 'dogfood', reason: 'Audit passed and no dogfood finding has been captured.' };
|
|
178
|
+
}
|
|
179
|
+
if (workflow.audit?.status === 'passed' && workflow.dogfood?.status === 'captured') {
|
|
180
|
+
if (workflow.completion_approved === true) {
|
|
181
|
+
return { state: 'complete', reason: 'Audit passed, dogfood was captured, and completion was approved.' };
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
state: 'ask_user',
|
|
185
|
+
reason: 'Milestone appears complete, but declaring completion is a human gate.',
|
|
186
|
+
questions: [{
|
|
187
|
+
id: 'completion-approval',
|
|
188
|
+
question: 'Approve declaring this milestone complete?',
|
|
189
|
+
default: 'No automatic completion; review audit evidence first.',
|
|
190
|
+
gate: 'completion',
|
|
191
|
+
blocking: true,
|
|
192
|
+
}],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (workflow.current_state && workflow.current_state !== 'plan' && NEXT_STATES.includes(workflow.current_state)) {
|
|
196
|
+
if (workflow.current_state === 'complete' && workflow.completion_approved !== true) {
|
|
197
|
+
return {
|
|
198
|
+
state: 'ask_user',
|
|
199
|
+
reason: 'Recorded state requests completion, but declaring completion is a human gate.',
|
|
200
|
+
questions: [{
|
|
201
|
+
id: 'completion-approval',
|
|
202
|
+
question: 'Approve declaring this milestone complete?',
|
|
203
|
+
default: 'No automatic completion; review audit evidence first.',
|
|
204
|
+
gate: 'completion',
|
|
205
|
+
blocking: true,
|
|
206
|
+
}],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return { state: workflow.current_state, reason: `Local \`.work/state.json\` requests ${workflow.current_state}.` };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function routeNext(ctx) {
|
|
215
|
+
const context = inspectWorkContext(ctx.cwd);
|
|
216
|
+
const controlMap = summarizeControlMap(ctx.cwd);
|
|
217
|
+
const inputsConsidered = ['repo truth: control-map'];
|
|
218
|
+
const inputsSkipped = [];
|
|
219
|
+
const traceRefs = [];
|
|
220
|
+
const constraints = [
|
|
221
|
+
'`gsdd next` v1 is read-only unless an explicit mutating subcommand is used.',
|
|
222
|
+
'No raw transcript ingestion, hosted memory, SQLite/vector DB, MCP memory server, or browser-provider implementation in this milestone.',
|
|
223
|
+
];
|
|
224
|
+
const privacyNotes = [
|
|
225
|
+
'Mutable `.work` runtime state is local-only by default.',
|
|
226
|
+
'Raw transcript ingestion is disabled by default.',
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
if (!context.has_goal) {
|
|
230
|
+
return packet({
|
|
231
|
+
state: 'ask_user',
|
|
232
|
+
reason: 'No `.work/goal.md` continuity contract exists yet.',
|
|
233
|
+
confidence: 'high',
|
|
234
|
+
next_command: 'gsdd next --init',
|
|
235
|
+
next_action: cliAction(['next', '--init'], 'Bootstrap `.work` explicitly after user approval.'),
|
|
236
|
+
requires_user: true,
|
|
237
|
+
questions: [{
|
|
238
|
+
id: 'work-bootstrap',
|
|
239
|
+
question: 'Initialize `.work/` as the canonical Workspine continuity root?',
|
|
240
|
+
default: 'Yes: run `gsdd next --init`.',
|
|
241
|
+
gate: 'bootstrap',
|
|
242
|
+
blocking: true,
|
|
243
|
+
}],
|
|
244
|
+
constraints,
|
|
245
|
+
artifacts_to_write: ['.work/goal.md', '.work/state.json', '.work/graph/events.jsonl', '.work/questions/open.json', '.work/evidence/manifest.json'],
|
|
246
|
+
privacy_notes: privacyNotes,
|
|
247
|
+
inputs_considered: inputsConsidered,
|
|
248
|
+
inputs_skipped: ['.work files: missing'],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
inputsConsidered.push('.work/goal.md');
|
|
253
|
+
if (context.state.exists) inputsConsidered.push('.work/state.json');
|
|
254
|
+
else inputsSkipped.push('.work/state.json: missing');
|
|
255
|
+
if (context.questions.ok && context.questions.exists) inputsConsidered.push('.work/questions/open.json');
|
|
256
|
+
else if (context.questions.ok) inputsSkipped.push('.work/questions/open.json: missing');
|
|
257
|
+
else inputsSkipped.push(`.work/questions/open.json: ${context.questions.error}`);
|
|
258
|
+
if (context.evidence.exists) inputsConsidered.push('.work/evidence/manifest.json');
|
|
259
|
+
else inputsSkipped.push('.work/evidence/manifest.json: missing');
|
|
260
|
+
if (context.graph.events.length > 0) {
|
|
261
|
+
inputsConsidered.push('.work/graph/events.jsonl');
|
|
262
|
+
traceRefs.push(...context.graph.events.slice(-5).map((event) => event.id));
|
|
263
|
+
} else {
|
|
264
|
+
inputsSkipped.push('.work/graph/events.jsonl: no events');
|
|
265
|
+
}
|
|
266
|
+
if (context.focus_exists) inputsConsidered.push('.work/focus/current.md');
|
|
267
|
+
else inputsSkipped.push('.work/focus/current.md: missing');
|
|
268
|
+
if (context.handoff_exists) inputsConsidered.push('.work/handoff/current.md');
|
|
269
|
+
else inputsSkipped.push('.work/handoff/current.md: missing');
|
|
270
|
+
|
|
271
|
+
if (context.decisions.length > 0) inputsConsidered.push('.work/decisions/*.md');
|
|
272
|
+
if (context.dogfood.length > 0) inputsConsidered.push('.work/dogfood/*.md');
|
|
273
|
+
if (context.milestone?.has_milestone) inputsConsidered.push('.work/milestone/MILESTONE.md');
|
|
274
|
+
else inputsSkipped.push('.work/milestone/MILESTONE.md: missing');
|
|
275
|
+
if (context.milestone?.has_roadmap) inputsConsidered.push('.work/milestone/ROADMAP.md');
|
|
276
|
+
else inputsSkipped.push('.work/milestone/ROADMAP.md: missing');
|
|
277
|
+
if (context.milestone?.has_audit) inputsConsidered.push('.work/milestone/AUDIT.md');
|
|
278
|
+
else inputsSkipped.push('.work/milestone/AUDIT.md: missing');
|
|
279
|
+
if (context.milestone?.phase_packet_count > 0) inputsConsidered.push('.work/milestone/phases/*');
|
|
280
|
+
|
|
281
|
+
if (context.graph.invalid.length > 0) {
|
|
282
|
+
return packet({
|
|
283
|
+
state: 'blocked',
|
|
284
|
+
reason: 'The continuity graph contains invalid events; routing would be unsafe.',
|
|
285
|
+
confidence: 'high',
|
|
286
|
+
next_command: 'gsdd next graph rebuild',
|
|
287
|
+
next_action: cliAction(['next', 'graph', 'rebuild'], 'Rebuild the deterministic graph index after malformed event lines are fixed.'),
|
|
288
|
+
requires_user: false,
|
|
289
|
+
constraints,
|
|
290
|
+
evidence_required: ['Fix or remove malformed graph event lines, then rebuild the index.'],
|
|
291
|
+
artifacts_to_read: ['.work/graph/events.jsonl'],
|
|
292
|
+
artifacts_to_write: ['.work/graph/index.json'],
|
|
293
|
+
privacy_notes: privacyNotes,
|
|
294
|
+
inputs_considered: inputsConsidered,
|
|
295
|
+
inputs_skipped: inputsSkipped,
|
|
296
|
+
trace_refs: traceRefs,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!context.state.ok) {
|
|
301
|
+
return packet({
|
|
302
|
+
state: 'blocked',
|
|
303
|
+
reason: '`.work/state.json` is unparseable.',
|
|
304
|
+
confidence: 'high',
|
|
305
|
+
next_command: null,
|
|
306
|
+
next_action: manualReviewAction(['.work/state.json'], 'Repair or replace malformed Workspine state JSON.'),
|
|
307
|
+
requires_user: false,
|
|
308
|
+
error_code: 'work_state_unparseable',
|
|
309
|
+
repair_action: 'manual_review',
|
|
310
|
+
repair_targets: ['.work/state.json'],
|
|
311
|
+
constraints,
|
|
312
|
+
artifacts_to_read: ['.work/state.json'],
|
|
313
|
+
privacy_notes: privacyNotes,
|
|
314
|
+
inputs_considered: inputsConsidered,
|
|
315
|
+
inputs_skipped: inputsSkipped,
|
|
316
|
+
trace_refs: traceRefs,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!context.questions.ok) {
|
|
321
|
+
return packet({
|
|
322
|
+
state: 'blocked',
|
|
323
|
+
reason: '`.work/questions/open.json` is unparseable.',
|
|
324
|
+
confidence: 'high',
|
|
325
|
+
next_command: null,
|
|
326
|
+
next_action: manualReviewAction(['.work/questions/open.json'], 'Repair or replace malformed open-question JSON.'),
|
|
327
|
+
requires_user: false,
|
|
328
|
+
error_code: 'open_questions_unparseable',
|
|
329
|
+
repair_action: 'manual_review',
|
|
330
|
+
repair_targets: ['.work/questions/open.json'],
|
|
331
|
+
constraints,
|
|
332
|
+
artifacts_to_read: ['.work/questions/open.json'],
|
|
333
|
+
privacy_notes: privacyNotes,
|
|
334
|
+
inputs_considered: inputsConsidered,
|
|
335
|
+
inputs_skipped: inputsSkipped,
|
|
336
|
+
trace_refs: traceRefs,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const blockingQuestions = context.questions.questions.filter((question) => question.blocking !== false);
|
|
341
|
+
if (blockingQuestions.length > 0) {
|
|
342
|
+
return packet({
|
|
343
|
+
state: 'ask_user',
|
|
344
|
+
reason: 'There are unresolved blocking questions in `.work/questions/open.json`.',
|
|
345
|
+
confidence: 'high',
|
|
346
|
+
next_command: 'gsdd next question answer --id <id> --answer <text>',
|
|
347
|
+
next_action: cliAction(['next', 'question', 'answer', '--id', '<id>', '--answer', '<text>'], 'Answer the blocking question after the user decides.'),
|
|
348
|
+
requires_user: true,
|
|
349
|
+
questions: blockingQuestions,
|
|
350
|
+
constraints,
|
|
351
|
+
artifacts_to_read: ['.work/questions/open.json'],
|
|
352
|
+
artifacts_to_write: ['.work/questions/answered.jsonl', '.work/graph/events.jsonl'],
|
|
353
|
+
privacy_notes: privacyNotes,
|
|
354
|
+
inputs_considered: inputsConsidered,
|
|
355
|
+
inputs_skipped: inputsSkipped,
|
|
356
|
+
trace_refs: traceRefs,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { manifest, error: manifestError } = readManifestStatus(context);
|
|
361
|
+
if (manifestError) {
|
|
362
|
+
return packet({
|
|
363
|
+
state: 'blocked',
|
|
364
|
+
reason: '`.work/evidence/manifest.json` is unparseable.',
|
|
365
|
+
confidence: 'high',
|
|
366
|
+
next_command: null,
|
|
367
|
+
next_action: manualReviewAction(['.work/evidence/manifest.json'], 'Repair or replace malformed evidence manifest JSON.'),
|
|
368
|
+
requires_user: false,
|
|
369
|
+
error_code: 'evidence_manifest_unparseable',
|
|
370
|
+
repair_action: 'manual_review',
|
|
371
|
+
repair_targets: ['.work/evidence/manifest.json'],
|
|
372
|
+
constraints,
|
|
373
|
+
artifacts_to_read: ['.work/evidence/manifest.json'],
|
|
374
|
+
privacy_notes: privacyNotes,
|
|
375
|
+
inputs_considered: inputsConsidered,
|
|
376
|
+
inputs_skipped: inputsSkipped,
|
|
377
|
+
trace_refs: traceRefs,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const trustGate = findTrustGate(manifest);
|
|
382
|
+
if (trustGate) {
|
|
383
|
+
return packet({
|
|
384
|
+
state: 'ask_user',
|
|
385
|
+
reason: trustGate.reason,
|
|
386
|
+
confidence: 'high',
|
|
387
|
+
next_command: null,
|
|
388
|
+
next_action: userQuestionAction([trustGate.id], 'Resolve the trust-boundary approval before continuing.'),
|
|
389
|
+
requires_user: true,
|
|
390
|
+
questions: [trustGate],
|
|
391
|
+
constraints,
|
|
392
|
+
privacy_notes: privacyNotes,
|
|
393
|
+
inputs_considered: inputsConsidered,
|
|
394
|
+
inputs_skipped: inputsSkipped,
|
|
395
|
+
trace_refs: traceRefs,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const stateRoute = routeFromStateObject(context.state.value);
|
|
400
|
+
if (stateRoute) {
|
|
401
|
+
return enrichRoute(stateRoute, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (manifest?.verification?.status === 'gaps_found' || manifest?.audit?.status === 'gaps_found') {
|
|
405
|
+
return enrichRoute({ state: 'fix_gaps', reason: 'Evidence manifest records verification or audit gaps.' }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (manifest?.audit?.status === 'passed' && context.dogfood.length === 0) {
|
|
409
|
+
return enrichRoute({ state: 'dogfood', reason: 'Evidence manifest records a passed audit and no dogfood finding exists.' }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (manifest?.audit?.status === 'passed' && context.dogfood.length > 0) {
|
|
413
|
+
return enrichRoute({
|
|
414
|
+
state: 'ask_user',
|
|
415
|
+
reason: 'Audit passed and dogfood was captured, but declaring completion is a human gate.',
|
|
416
|
+
questions: [{
|
|
417
|
+
id: 'completion-approval',
|
|
418
|
+
question: 'Approve declaring this milestone complete?',
|
|
419
|
+
default: 'No automatic completion; review audit evidence first.',
|
|
420
|
+
gate: 'completion',
|
|
421
|
+
blocking: true,
|
|
422
|
+
}],
|
|
423
|
+
}, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (hasUnverifiedSummaries(context.planning.phases)) {
|
|
427
|
+
return enrichRoute({ state: 'verify', reason: 'Legacy `.planning` phase summaries exist without matching verification reports.' }, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const workMilestoneRoute = routeFromWorkMilestone(context, manifest);
|
|
431
|
+
if (workMilestoneRoute) {
|
|
432
|
+
return enrichRoute(workMilestoneRoute, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const legacyComplete = context.planning.has_spec && context.planning.has_roadmap && context.planning.has_milestones;
|
|
436
|
+
if (!legacyComplete) {
|
|
437
|
+
return packet({
|
|
438
|
+
state: 'plan',
|
|
439
|
+
reason: '`.work/goal.md` exists, but canonical `.planning` lifecycle truth is incomplete; create or refresh the Workspine-native plan from `.work`.',
|
|
440
|
+
confidence: context.planning.exists ? 'medium' : 'high',
|
|
441
|
+
next_command: 'gsdd-plan',
|
|
442
|
+
next_action: workflowAction('gsdd-plan', 'Plan the Workspine-native milestone from `.work` truth.'),
|
|
443
|
+
requires_user: false,
|
|
444
|
+
constraints: [
|
|
445
|
+
...constraints,
|
|
446
|
+
'Do not infer normal `.planning` milestone progress when SPEC.md, ROADMAP.md, or MILESTONES.md are missing.',
|
|
447
|
+
],
|
|
448
|
+
evidence_required: ['Plan must map `.work/goal.md` requirements to implementation and verification artifacts.'],
|
|
449
|
+
artifacts_to_read: ['.work/goal.md', '.work/research/2026-06-20-long-term-agent-harness-consistency.md'],
|
|
450
|
+
artifacts_to_write: ['.work/focus/current.md', '.work/graph/events.jsonl'],
|
|
451
|
+
privacy_notes: privacyNotes,
|
|
452
|
+
inputs_considered: inputsConsidered,
|
|
453
|
+
inputs_skipped: [
|
|
454
|
+
...inputsSkipped,
|
|
455
|
+
!context.planning.has_spec ? '.planning/SPEC.md: missing' : null,
|
|
456
|
+
!context.planning.has_roadmap ? '.planning/ROADMAP.md: missing' : null,
|
|
457
|
+
!context.planning.has_milestones ? '.planning/MILESTONES.md: missing' : null,
|
|
458
|
+
].filter(Boolean),
|
|
459
|
+
trace_refs: traceRefs,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return packet({
|
|
464
|
+
state: 'plan',
|
|
465
|
+
reason: 'Continuity contract and legacy lifecycle artifacts are present; plan the next approved work slice.',
|
|
466
|
+
confidence: 'medium',
|
|
467
|
+
next_command: 'gsdd-plan',
|
|
468
|
+
next_action: workflowAction('gsdd-plan', 'Plan the next approved work slice.'),
|
|
469
|
+
requires_user: false,
|
|
470
|
+
constraints,
|
|
471
|
+
artifacts_to_read: ['.work/goal.md', '.planning/SPEC.md', '.planning/ROADMAP.md', '.planning/MILESTONES.md'],
|
|
472
|
+
artifacts_to_write: ['.work/focus/current.md'],
|
|
473
|
+
privacy_notes: privacyNotes,
|
|
474
|
+
inputs_considered: inputsConsidered,
|
|
475
|
+
inputs_skipped: inputsSkipped,
|
|
476
|
+
trace_refs: traceRefs,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function routeFromWorkMilestone(context, manifest) {
|
|
481
|
+
const milestone = context.milestone;
|
|
482
|
+
if (!milestone?.exists) return null;
|
|
483
|
+
if (milestone.roadmap_all_complete && !milestone.audit_passed) {
|
|
484
|
+
return {
|
|
485
|
+
state: 'audit',
|
|
486
|
+
reason: 'Workspine-native `.work/milestone` roadmap is complete and needs milestone audit.',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if (milestone.audit_passed && context.dogfood.length === 0 && manifest?.dogfood?.status !== 'captured') {
|
|
490
|
+
return {
|
|
491
|
+
state: 'dogfood',
|
|
492
|
+
reason: 'Workspine-native `.work/milestone` audit passed and no dogfood finding has been captured.',
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (milestone.audit_passed && (context.dogfood.length > 0 || manifest?.dogfood?.status === 'captured')) {
|
|
496
|
+
return {
|
|
497
|
+
state: 'ask_user',
|
|
498
|
+
reason: 'Workspine-native milestone audit passed and dogfood exists, but declaring completion is a human gate.',
|
|
499
|
+
next_command: 'gsdd-complete-milestone',
|
|
500
|
+
next_action: manualReviewAction(['.work/milestone/AUDIT.md', '.work/milestone/ROADMAP.md', '.work/evidence/manifest.json'], 'Review closure evidence with the user before running completion workflow.'),
|
|
501
|
+
questions: [{
|
|
502
|
+
id: 'completion-approval',
|
|
503
|
+
question: 'Approve declaring this Workspine-native milestone complete?',
|
|
504
|
+
default: 'No automatic completion; review `.work/milestone/AUDIT.md` and evidence first.',
|
|
505
|
+
gate: 'completion',
|
|
506
|
+
blocking: true,
|
|
507
|
+
}],
|
|
508
|
+
evidence_required: ['Human approval after reviewing `.work/milestone/AUDIT.md` and the scoped closure limits.'],
|
|
509
|
+
artifacts_to_read: ['.work/milestone/AUDIT.md', '.work/milestone/ROADMAP.md', '.work/evidence/manifest.json'],
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (milestone.has_roadmap && milestone.phase_count > 0) {
|
|
513
|
+
return {
|
|
514
|
+
state: 'verify',
|
|
515
|
+
reason: 'Workspine-native `.work/milestone` phase packets exist and should be verified before closure.',
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function enrichRoute(route, { context, controlMap, constraints, privacyNotes, inputsConsidered, inputsSkipped, traceRefs }) {
|
|
522
|
+
const commands = {
|
|
523
|
+
execute: workflowAction('gsdd-execute', 'Execute the approved Workspine plan.'),
|
|
524
|
+
verify: workflowAction('gsdd-verify', 'Verify executed artifacts against the plan.'),
|
|
525
|
+
audit: workflowAction('gsdd-audit-milestone', 'Audit milestone-level integration and closure evidence.'),
|
|
526
|
+
fix_gaps: workflowAction('gsdd-plan-milestone-gaps', 'Plan gap-fix work from audit or verification findings.'),
|
|
527
|
+
dogfood: cliAction(['next', 'dogfood', 'capture', '--id', '<id>', '--title', '<text>', '--body', '<text>'], 'Capture one bounded local dogfood finding.'),
|
|
528
|
+
pause: manualReviewAction(['.work/handoff/current.md'], 'Update handoff before pausing.'),
|
|
529
|
+
blocked: null,
|
|
530
|
+
ask_user: null,
|
|
531
|
+
complete: null,
|
|
532
|
+
};
|
|
533
|
+
const nextAction = route.next_action || commands[route.state] || null;
|
|
534
|
+
return packet({
|
|
535
|
+
state: route.state,
|
|
536
|
+
reason: route.reason,
|
|
537
|
+
confidence: route.state === 'blocked' || route.state === 'ask_user' ? 'high' : 'medium',
|
|
538
|
+
next_command: route.next_command || actionToLegacyCommand(nextAction),
|
|
539
|
+
next_action: nextAction,
|
|
540
|
+
requires_user: route.state === 'ask_user',
|
|
541
|
+
questions: route.questions || [],
|
|
542
|
+
constraints,
|
|
543
|
+
evidence_required: route.evidence_required || (route.state === 'verify' ? ['Verification report proving executed artifacts satisfy the plan.'] : []),
|
|
544
|
+
artifacts_to_read: route.artifacts_to_read || defaultReadArtifacts(route.state, context),
|
|
545
|
+
artifacts_to_write: route.artifacts_to_write || defaultWriteArtifacts(route.state),
|
|
546
|
+
repo_warnings: repoWarningsFromControlMap(controlMap),
|
|
547
|
+
privacy_notes: privacyNotes,
|
|
548
|
+
inputs_considered: inputsConsidered,
|
|
549
|
+
inputs_skipped: inputsSkipped,
|
|
550
|
+
trace_refs: traceRefs,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function actionToLegacyCommand(action) {
|
|
555
|
+
if (!action) return null;
|
|
556
|
+
if (action.type === 'cli_command') return action.command;
|
|
557
|
+
if (action.type === 'workflow_skill') return action.skill_id;
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function defaultReadArtifacts(state, context) {
|
|
562
|
+
if (state === 'execute') return ['.work/goal.md', '.work/focus/current.md'];
|
|
563
|
+
if (state === 'verify') return context.milestone?.has_roadmap
|
|
564
|
+
? ['.work/goal.md', '.work/milestone/ROADMAP.md', '.work/milestone/phases/*/*-VERIFY.md']
|
|
565
|
+
: ['.work/goal.md', '.work/evidence/manifest.json', '.planning/phases/*/*-SUMMARY.md'];
|
|
566
|
+
if (state === 'audit') return context.milestone?.has_roadmap
|
|
567
|
+
? ['.work/goal.md', '.work/milestone/ROADMAP.md', '.work/milestone/phases/*/*-VERIFY.md']
|
|
568
|
+
: ['.work/goal.md', '.work/evidence/manifest.json', '.planning/phases/**/*-VERIFICATION.md'];
|
|
569
|
+
if (state === 'fix_gaps') return ['.work/evidence/manifest.json'];
|
|
570
|
+
if (state === 'dogfood') return ['.work/goal.md', '.work/evidence/manifest.json'];
|
|
571
|
+
if (state === 'pause') return ['.work/handoff/current.md'];
|
|
572
|
+
if (state === 'complete') return ['.work/goal.md', '.work/evidence/manifest.json', '.work/dogfood/'];
|
|
573
|
+
if (state === 'ask_user') return context.questions.questions.length > 0 ? ['.work/questions/open.json'] : ['.work/state.json'];
|
|
574
|
+
return ['.work/goal.md'];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function defaultWriteArtifacts(state) {
|
|
578
|
+
if (state === 'verify') return ['.work/evidence/manifest.json'];
|
|
579
|
+
if (state === 'audit') return ['.work/evidence/manifest.json'];
|
|
580
|
+
if (state === 'fix_gaps') return ['.work/focus/current.md', '.work/graph/events.jsonl'];
|
|
581
|
+
if (state === 'dogfood') return ['.work/dogfood/*.md', '.work/graph/events.jsonl'];
|
|
582
|
+
if (state === 'pause') return ['.work/handoff/current.md'];
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function hasUnverifiedSummaries(phases) {
|
|
587
|
+
return phases.some((phase) => phase.summaries.length > 0 && phase.verifications.length === 0);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function findTrustGate(manifest) {
|
|
591
|
+
const gates = Array.isArray(manifest?.trust_gates) ? manifest.trust_gates : [];
|
|
592
|
+
const gate = gates.find((entry) => entry && entry.approved !== true);
|
|
593
|
+
if (!gate) return null;
|
|
594
|
+
return {
|
|
595
|
+
id: gate.id || 'trust-boundary',
|
|
596
|
+
question: gate.question || 'Approve crossing this trust boundary?',
|
|
597
|
+
default: gate.default || 'Do not proceed without explicit approval.',
|
|
598
|
+
gate: gate.gate || 'trust',
|
|
599
|
+
blocking: true,
|
|
600
|
+
reason: gate.reason || 'A trust boundary requires explicit approval.',
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function printHuman(packetValue) {
|
|
605
|
+
console.log(`gsdd next: ${packetValue.state}`);
|
|
606
|
+
console.log(`Why: ${packetValue.reason}`);
|
|
607
|
+
if (packetValue.next_action) console.log(`Next: ${renderAction(packetValue.next_action)}`);
|
|
608
|
+
else if (packetValue.next_command) console.log(`Next: ${packetValue.next_command}`);
|
|
609
|
+
if (packetValue.requires_user) console.log('Approval: required');
|
|
610
|
+
else console.log('Approval: not required');
|
|
611
|
+
if (packetValue.questions.length > 0) {
|
|
612
|
+
console.log('\nQuestions:');
|
|
613
|
+
for (const question of packetValue.questions) {
|
|
614
|
+
console.log(`- ${question.id}: ${question.question || question.prompt}`);
|
|
615
|
+
if (question.default) console.log(` Default: ${question.default}`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (packetValue.constraints.length > 0) {
|
|
619
|
+
console.log('\nConstraints:');
|
|
620
|
+
for (const constraint of packetValue.constraints) console.log(`- ${constraint}`);
|
|
621
|
+
}
|
|
622
|
+
if (packetValue.evidence_required.length > 0) {
|
|
623
|
+
console.log('\nEvidence required:');
|
|
624
|
+
for (const item of packetValue.evidence_required) console.log(`- ${item}`);
|
|
625
|
+
}
|
|
626
|
+
if (packetValue.repo_warnings.length > 0) {
|
|
627
|
+
console.log('\nRepo risk:');
|
|
628
|
+
for (const item of packetValue.repo_warnings) console.log(`- ${item}`);
|
|
629
|
+
}
|
|
630
|
+
if (packetValue.inputs_skipped.length > 0) {
|
|
631
|
+
console.log('\nSkipped inputs:');
|
|
632
|
+
for (const item of packetValue.inputs_skipped) console.log(`- ${item}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renderAction(action) {
|
|
637
|
+
if (action.type === 'cli_command') return action.command;
|
|
638
|
+
if (action.type === 'workflow_skill') return action.skill_id;
|
|
639
|
+
if (action.type === 'manual_review') return `review ${action.targets.join(', ')}`;
|
|
640
|
+
if (action.type === 'user_question') return `answer ${action.question_ids.join(', ')}`;
|
|
641
|
+
return action.description || action.type;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function requireFlag(args, name, usage = NEXT_USAGE) {
|
|
645
|
+
const parsed = parseFlagValue(args, name);
|
|
646
|
+
if (parsed.invalid || !parsed.value) {
|
|
647
|
+
const error = new Error(usage);
|
|
648
|
+
error.usage = true;
|
|
649
|
+
throw error;
|
|
650
|
+
}
|
|
651
|
+
return parsed.value;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function createCmdNext(ctx) {
|
|
655
|
+
return function cmdNext(...args) {
|
|
656
|
+
const jsonMode = outputMode(args) === 'json';
|
|
657
|
+
const workPaths = getWorkPaths(ctx.cwd);
|
|
658
|
+
try {
|
|
659
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
660
|
+
console.log(NEXT_USAGE);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (args.includes('--init')) {
|
|
665
|
+
const result = ensureWorkStructure(ctx.cwd);
|
|
666
|
+
const index = rebuildGraphIndex(result.paths.workDir, { write: true });
|
|
667
|
+
const response = {
|
|
668
|
+
schema_version: 1,
|
|
669
|
+
operation: 'next init',
|
|
670
|
+
status: 'ok',
|
|
671
|
+
changed: result.created.length > 0,
|
|
672
|
+
created: result.created,
|
|
673
|
+
work_dir: '.work',
|
|
674
|
+
index: {
|
|
675
|
+
event_count: index.event_count,
|
|
676
|
+
invalid_event_count: index.invalid_event_count,
|
|
677
|
+
},
|
|
678
|
+
next: routeNext(ctx),
|
|
679
|
+
};
|
|
680
|
+
if (jsonMode) output(response);
|
|
681
|
+
else {
|
|
682
|
+
console.log(`Initialized .work (${result.created.length} created).`);
|
|
683
|
+
printHuman(response.next);
|
|
684
|
+
}
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (args[0] === 'graph' && args[1] === 'rebuild') {
|
|
689
|
+
if (!existsSync(workPaths.workDir)) throw new Error('No `.work/` directory found. Run `gsdd next --init` first.');
|
|
690
|
+
const index = rebuildGraphIndex(workPaths.workDir, { write: true });
|
|
691
|
+
const response = {
|
|
692
|
+
schema_version: 1,
|
|
693
|
+
operation: 'next graph rebuild',
|
|
694
|
+
status: index.invalid_event_count > 0 ? 'invalid_events' : 'ok',
|
|
695
|
+
index,
|
|
696
|
+
};
|
|
697
|
+
if (jsonMode) output(response);
|
|
698
|
+
else console.log(`Rebuilt .work graph index (${index.event_count} events, ${index.invalid_event_count} invalid).`);
|
|
699
|
+
if (index.invalid_event_count > 0) process.exitCode = 1;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (args[0] === 'question') {
|
|
704
|
+
handleQuestion(ctx, args.slice(1), jsonMode);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (args[0] === 'decision') {
|
|
709
|
+
handleDecision(ctx, args.slice(1), jsonMode);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (args[0] === 'dogfood') {
|
|
714
|
+
handleDogfood(ctx, args.slice(1), jsonMode);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const filtered = removeFlags(args, ['--json', '--format']);
|
|
719
|
+
if (filtered.length > 0) {
|
|
720
|
+
console.error(NEXT_USAGE);
|
|
721
|
+
process.exitCode = 1;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const result = routeNext(ctx);
|
|
726
|
+
if (jsonMode) output(result);
|
|
727
|
+
else printHuman(result);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
const response = {
|
|
730
|
+
schema_version: 1,
|
|
731
|
+
operation: 'next',
|
|
732
|
+
status: 'error',
|
|
733
|
+
error: error.message,
|
|
734
|
+
};
|
|
735
|
+
if (jsonMode) output(response);
|
|
736
|
+
else console.error(error.usage ? error.message : `gsdd next failed: ${error.message}`);
|
|
737
|
+
process.exitCode = 1;
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function handleQuestion(ctx, args, jsonMode) {
|
|
743
|
+
if (!existsSync(getWorkPaths(ctx.cwd).workDir)) throw new Error('No `.work/` directory found. Run `gsdd next --init` first.');
|
|
744
|
+
if (args[0] === 'add') {
|
|
745
|
+
const id = requireFlag(args, '--id');
|
|
746
|
+
const question = requireFlag(args, '--prompt');
|
|
747
|
+
const defaultValue = parseFlagValue(args, '--default').value;
|
|
748
|
+
const gate = parseFlagValue(args, '--gate').value || 'product';
|
|
749
|
+
const blocking = boolFlagValue(parseFlagValue(args, '--blocking').value, true);
|
|
750
|
+
const replace = args.includes('--replace');
|
|
751
|
+
const result = addOpenQuestion(getWorkPaths(ctx.cwd).workDir, {
|
|
752
|
+
id,
|
|
753
|
+
question,
|
|
754
|
+
default: defaultValue,
|
|
755
|
+
gate,
|
|
756
|
+
blocking,
|
|
757
|
+
}, { replace });
|
|
758
|
+
const response = {
|
|
759
|
+
schema_version: 1,
|
|
760
|
+
operation: 'next question add',
|
|
761
|
+
status: result.status,
|
|
762
|
+
question: result.question,
|
|
763
|
+
graph_event_id: result.event?.id || null,
|
|
764
|
+
graph_event_ids: result.events.map((event) => event.id),
|
|
765
|
+
};
|
|
766
|
+
if (jsonMode) output(response);
|
|
767
|
+
else console.log(`Added question ${result.question.id}.`);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (args[0] === 'answer') {
|
|
771
|
+
const id = requireFlag(args, '--id');
|
|
772
|
+
const answer = requireFlag(args, '--answer');
|
|
773
|
+
const result = answerQuestion(getWorkPaths(ctx.cwd).workDir, id, answer);
|
|
774
|
+
const response = {
|
|
775
|
+
schema_version: 1,
|
|
776
|
+
operation: 'next question answer',
|
|
777
|
+
status: result.already_applied ? 'already_applied' : result.found ? 'ok' : 'not_found',
|
|
778
|
+
question: result.question,
|
|
779
|
+
graph_event_id: result.event?.id || null,
|
|
780
|
+
graph_event_ids: result.events?.map((event) => event.id) || [],
|
|
781
|
+
};
|
|
782
|
+
if (jsonMode) output(response);
|
|
783
|
+
else console.log(result.found ? `Answered question ${id}.` : `Question ${id} not found.`);
|
|
784
|
+
if (!result.found) process.exitCode = 1;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
throw new Error(NEXT_USAGE);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function handleDecision(ctx, args, jsonMode) {
|
|
791
|
+
if (!existsSync(getWorkPaths(ctx.cwd).workDir)) throw new Error('No `.work/` directory found. Run `gsdd next --init` first.');
|
|
792
|
+
if (args[0] !== 'record') throw new Error(NEXT_USAGE);
|
|
793
|
+
const id = requireFlag(args, '--id');
|
|
794
|
+
const title = requireFlag(args, '--title');
|
|
795
|
+
const body = requireFlag(args, '--body');
|
|
796
|
+
const supersedes = parseFlagValue(args, '--supersedes').value;
|
|
797
|
+
const privacy = parseFlagValue(args, '--privacy').value || 'repo';
|
|
798
|
+
const replace = args.includes('--replace');
|
|
799
|
+
const result = recordDecision(getWorkPaths(ctx.cwd).workDir, { id, title, body, supersedes, privacy }, { replace });
|
|
800
|
+
const response = {
|
|
801
|
+
schema_version: 1,
|
|
802
|
+
operation: 'next decision record',
|
|
803
|
+
status: result.status,
|
|
804
|
+
decision: { id: result.id, path: normalizeSlashes(result.path) },
|
|
805
|
+
graph_event_id: result.event?.id || null,
|
|
806
|
+
graph_event_ids: result.events.map((event) => event.id),
|
|
807
|
+
};
|
|
808
|
+
if (jsonMode) output(response);
|
|
809
|
+
else console.log(`Recorded decision ${result.id}.`);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function handleDogfood(ctx, args, jsonMode) {
|
|
813
|
+
if (!existsSync(getWorkPaths(ctx.cwd).workDir)) throw new Error('No `.work/` directory found. Run `gsdd next --init` first.');
|
|
814
|
+
if (args[0] !== 'capture') throw new Error(NEXT_USAGE);
|
|
815
|
+
const id = requireFlag(args, '--id');
|
|
816
|
+
const title = requireFlag(args, '--title');
|
|
817
|
+
const body = requireFlag(args, '--body');
|
|
818
|
+
const backlog = parseFlagValue(args, '--backlog').value;
|
|
819
|
+
const replace = args.includes('--replace');
|
|
820
|
+
const result = captureDogfoodFinding(getWorkPaths(ctx.cwd).workDir, { id, title, body, backlog }, { replace });
|
|
821
|
+
const response = {
|
|
822
|
+
schema_version: 1,
|
|
823
|
+
operation: 'next dogfood capture',
|
|
824
|
+
status: result.status,
|
|
825
|
+
finding: { id: result.id, path: normalizeSlashes(result.path) },
|
|
826
|
+
graph_event_id: result.event?.id || null,
|
|
827
|
+
};
|
|
828
|
+
if (jsonMode) output(response);
|
|
829
|
+
else console.log(`Captured dogfood finding ${result.id}.`);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function cmdNext(...args) {
|
|
833
|
+
return createCmdNext({ cwd: process.cwd() })(...args);
|
|
834
|
+
}
|