vgxness 1.9.2 → 1.9.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.
Files changed (39) hide show
  1. package/README.md +9 -4
  2. package/dist/agents/agent-resolver.js +33 -3
  3. package/dist/agents/canonical-agent-manifest.js +39 -18
  4. package/dist/agents/canonical-agent-projection.js +31 -4
  5. package/dist/cli/cli-help.js +14 -3
  6. package/dist/cli/commands/index.js +1 -0
  7. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +8 -0
  8. package/dist/cli/commands/memory-sdd-dispatcher.js +71 -5
  9. package/dist/cli/commands/status-dispatcher.js +130 -0
  10. package/dist/cli/commands/workflow-dispatcher.js +11 -5
  11. package/dist/cli/dispatcher.js +9 -1
  12. package/dist/cli/product-resume-renderer.js +32 -0
  13. package/dist/cli/product-status-renderer.js +74 -0
  14. package/dist/cli/sdd-renderer.js +80 -3
  15. package/dist/code/cli/code-command.js +7 -4
  16. package/dist/code/reporting/summary.js +4 -1
  17. package/dist/code/runtime/code-runtime.js +27 -4
  18. package/dist/code/runtime/sdd-context.js +18 -2
  19. package/dist/governance/governance-report-builder.js +18 -7
  20. package/dist/mcp/claude-code-agent-config.js +10 -4
  21. package/dist/mcp/control-plane.js +56 -0
  22. package/dist/mcp/provider-status.js +86 -81
  23. package/dist/mcp/schema.js +25 -7
  24. package/dist/mcp/stdio-server.js +4 -0
  25. package/dist/mcp/validation.js +39 -3
  26. package/dist/resume/product-resume.js +166 -0
  27. package/dist/runs/repositories/runs.js +12 -1
  28. package/dist/runs/run-service.js +62 -5
  29. package/dist/sdd/schema.js +8 -0
  30. package/dist/sdd/sdd-continuation-plan.js +81 -0
  31. package/dist/sdd/sdd-workflow-service.js +103 -16
  32. package/dist/skills/skill-resolver.js +21 -4
  33. package/dist/status/product-status.js +117 -0
  34. package/docs/architecture.md +1 -1
  35. package/docs/cli.md +33 -8
  36. package/docs/code-runtime.md +3 -0
  37. package/docs/glossary.md +1 -1
  38. package/docs/mcp.md +18 -4
  39. package/package.json +1 -1
@@ -0,0 +1,130 @@
1
+ import { MemoryService } from '../../memory/memory-service.js';
2
+ import { openMemoryDatabase } from '../../memory/sqlite/database.js';
3
+ import { SddWorkflowService } from '../../sdd/sdd-workflow-service.js';
4
+ import { buildProductStatus } from '../../status/product-status.js';
5
+ import { RunService } from '../../runs/run-service.js';
6
+ import { buildProductResume } from '../../resume/product-resume.js';
7
+ import { databasePathFor, optionalStringFlag, requiredFlag } from '../cli-flags.js';
8
+ import { jsonResult, resultFailure } from '../cli-helpers.js';
9
+ import { okText, usageFailure } from '../cli-help.js';
10
+ import { renderProductResume } from '../product-resume-renderer.js';
11
+ import { buildProductNext, renderProductNext, renderProductStatus } from '../product-status-renderer.js';
12
+ export function runProductStatusCommand(parsed, environment) {
13
+ const extraPositionals = rejectExtraPositionals(parsed, 'status');
14
+ if (extraPositionals !== undefined)
15
+ return extraPositionals;
16
+ const status = buildProductStatusForCli(parsed, environment);
17
+ if (!status.ok)
18
+ return status.value;
19
+ return parsed.flags.json === true ? jsonResult({ ok: true, value: status.value }) : okText(renderProductStatus(status.value));
20
+ }
21
+ export function runProductNextCommand(parsed, environment) {
22
+ const extraPositionals = rejectExtraPositionals(parsed, 'next');
23
+ if (extraPositionals !== undefined)
24
+ return extraPositionals;
25
+ const status = buildProductStatusForCli(parsed, environment);
26
+ if (!status.ok)
27
+ return status.value;
28
+ const next = buildProductNext(status.value);
29
+ return parsed.flags.json === true ? jsonResult({ ok: true, value: next }) : okText(renderProductNext(next));
30
+ }
31
+ export function runProductResumeCommand(parsed, environment) {
32
+ const extraPositionals = rejectExtraPositionals(parsed, 'resume');
33
+ if (extraPositionals !== undefined)
34
+ return extraPositionals;
35
+ const resume = buildProductResumeForCli(parsed, environment);
36
+ if (!resume.ok)
37
+ return resume.value;
38
+ return parsed.flags.json === true ? jsonResult({ ok: true, value: resume.value }) : okText(renderProductResume(resume.value));
39
+ }
40
+ function rejectExtraPositionals(parsed, command) {
41
+ const extra = parsed.positionals.slice(1);
42
+ return extra.length === 0 ? undefined : usageFailure(`${command} does not accept positional arguments: ${extra.join(' ')}`);
43
+ }
44
+ function buildProductStatusForCli(parsed, environment) {
45
+ const projectFlag = parsed.flags.project === undefined ? { ok: true, value: undefined } : requiredFlag(parsed.flags, 'project');
46
+ if (!projectFlag.ok)
47
+ return { ok: false, value: resultFailure(projectFlag) };
48
+ const changeFlag = parsed.flags.change === undefined ? { ok: true, value: undefined } : requiredFlag(parsed.flags, 'change');
49
+ if (!changeFlag.ok)
50
+ return { ok: false, value: resultFailure(changeFlag) };
51
+ const project = projectFlag.value;
52
+ const change = changeFlag.value;
53
+ if (change === undefined) {
54
+ const status = buildProductStatus({ cwd: environment.cwd, ...(project === undefined ? {} : { project }) });
55
+ return { ok: true, value: status };
56
+ }
57
+ const selectedDatabasePath = databasePathFor(parsed.flags, environment);
58
+ if (!selectedDatabasePath.ok)
59
+ return { ok: false, value: resultFailure(selectedDatabasePath) };
60
+ const explicitDatabasePath = optionalStringFlag(parsed.flags, 'db');
61
+ const opened = openMemoryDatabase({ path: selectedDatabasePath.value, readonly: true });
62
+ if (!opened.ok) {
63
+ const status = buildProductStatus({
64
+ cwd: environment.cwd,
65
+ ...(project === undefined ? {} : { project }),
66
+ change,
67
+ databasePath: selectedDatabasePath.value,
68
+ databaseError: opened.error.message,
69
+ });
70
+ return { ok: true, value: status };
71
+ }
72
+ try {
73
+ const status = buildProductStatus({
74
+ cwd: environment.cwd,
75
+ ...(project === undefined ? {} : { project }),
76
+ change,
77
+ databasePath: selectedDatabasePath.value,
78
+ ...(explicitDatabasePath === undefined ? {} : { explicitDatabasePath }),
79
+ sdd: new SddWorkflowService(new MemoryService(opened.value)),
80
+ runs: new RunService(opened.value),
81
+ });
82
+ return { ok: true, value: status };
83
+ }
84
+ finally {
85
+ opened.value.close();
86
+ }
87
+ }
88
+ function buildProductResumeForCli(parsed, environment) {
89
+ const projectFlag = parsed.flags.project === undefined ? { ok: true, value: undefined } : requiredFlag(parsed.flags, 'project');
90
+ if (!projectFlag.ok)
91
+ return { ok: false, value: resultFailure(projectFlag) };
92
+ const runIdFlag = parsed.flags['run-id'] === undefined ? { ok: true, value: undefined } : requiredFlag(parsed.flags, 'run-id');
93
+ if (!runIdFlag.ok)
94
+ return { ok: false, value: resultFailure(runIdFlag) };
95
+ const project = projectFlag.value;
96
+ const runId = runIdFlag.value;
97
+ if (runId === undefined && project === undefined) {
98
+ const resume = buildProductResume({ cwd: environment.cwd, ...(project === undefined ? {} : { project }) });
99
+ return { ok: true, value: resume };
100
+ }
101
+ const selectedDatabasePath = databasePathFor(parsed.flags, environment);
102
+ if (!selectedDatabasePath.ok)
103
+ return { ok: false, value: resultFailure(selectedDatabasePath) };
104
+ const opened = openMemoryDatabase({ path: selectedDatabasePath.value, readonly: true });
105
+ if (!opened.ok) {
106
+ const visibleDatabasePath = runId === undefined && parsed.flags.db === undefined ? undefined : selectedDatabasePath.value;
107
+ const resume = buildProductResume({
108
+ cwd: environment.cwd,
109
+ ...(project === undefined ? {} : { project }),
110
+ ...(runId === undefined ? {} : { runId }),
111
+ ...(visibleDatabasePath === undefined ? {} : { databasePath: visibleDatabasePath }),
112
+ databaseError: opened.error.message,
113
+ });
114
+ return { ok: true, value: resume };
115
+ }
116
+ try {
117
+ const flaggedDatabasePath = parsed.flags.db === undefined ? undefined : selectedDatabasePath.value;
118
+ const resume = buildProductResume({
119
+ cwd: environment.cwd,
120
+ ...(project === undefined ? {} : { project }),
121
+ ...(runId === undefined ? {} : { runId }),
122
+ ...(flaggedDatabasePath === undefined ? {} : { databasePath: flaggedDatabasePath }),
123
+ runs: new RunService(opened.value),
124
+ });
125
+ return { ok: true, value: resume };
126
+ }
127
+ finally {
128
+ opened.value.close();
129
+ }
130
+ }
@@ -1,7 +1,8 @@
1
1
  import { AgentRegistryService } from '../../agents/agent-registry-service.js';
2
+ import { canonicalOpenCodeDefaultModel } from '../../agents/canonical-agent-manifest.js';
2
3
  import { createNaturalLanguagePlan } from '../../orchestrator/natural-language-planner.js';
3
4
  import { RunService } from '../../runs/run-service.js';
4
- import { isSddPhase, sddPhases } from '../../sdd/schema.js';
5
+ import { normalizeSddPhaseInput, sddPhases } from '../../sdd/schema.js';
5
6
  import { CommandAllowlistAdapter, commandAllowlistIds } from '../../workflows/command-allowlist-adapter.js';
6
7
  import { GuardedProviderWorkflowExecutor, operationMetadataForWorkflowExecution, SafeNonDispatchingWorkflowExecutor, workflowExecutorSafety, } from '../../workflows/workflow-executor.js';
7
8
  import { getWorkflowDefinition, listWorkflows } from '../../workflows/workflow-registry.js';
@@ -248,11 +249,16 @@ export function runWorkflowRunCommand(workflow, parsed, database) {
248
249
  const selected = getWorkflowDefinition(workflow);
249
250
  const planner = createNaturalLanguagePlan({ project, intent: intent.value });
250
251
  const recommended = getWorkflowDefinition(planner.workflow);
251
- const phase = optionalStringFlag(parsed.flags, 'phase') ?? selected.defaultPhase;
252
- if (selected.id === 'sdd' && !isSddPhase(phase))
253
- return resultFailure(validationFailure(`Unknown SDD phase: ${phase}. Expected one of: ${sddPhases.join(', ')}`));
252
+ const phaseInput = optionalStringFlag(parsed.flags, 'phase') ?? selected.defaultPhase;
253
+ let phase = phaseInput;
254
+ if (selected.id === 'sdd') {
255
+ const canonicalPhase = normalizeSddPhaseInput(phaseInput);
256
+ if (canonicalPhase === undefined)
257
+ return resultFailure(validationFailure(`Unknown SDD phase: ${phaseInput}. Expected one of: ${sddPhases.join(', ')}`));
258
+ phase = canonicalPhase;
259
+ }
254
260
  const providerAdapter = optionalStringFlag(parsed.flags, 'provider-adapter') ?? 'opencode';
255
- const model = optionalStringFlag(parsed.flags, 'model') ?? 'openai/gpt-5.5';
261
+ const model = optionalStringFlag(parsed.flags, 'model') ?? canonicalOpenCodeDefaultModel;
256
262
  const registry = new AgentRegistryService(database);
257
263
  const explicitAgentId = optionalStringFlag(parsed.flags, 'agent-id');
258
264
  const selectedAgent = resolveWorkflowRunAgent({
@@ -5,7 +5,7 @@ import { databasePathFor, parseArgs, requiredFlag } from './cli-flags.js';
5
5
  import { okText, usageFailure, visibleHelpText } from './cli-help.js';
6
6
  import { openCliDatabase, resultFailure } from './cli-helpers.js';
7
7
  import { runBootAgentSeedUpgrade } from '../agents/boot-upgrade.js';
8
- import { runAgentCommand, runApprovalsCommand, runCodeCliCommand, runDefaultInteractiveEntrypoint, runDoctorAliasCommand, runInitCommand, runMcpDoctorCommand, runMcpInstallCommand, runMcpSetupCommand, runMemoryCommand, runMemoryImportCommand, runOpenCodeCommand, runOrchestratorCommand, runPermissionsCommand, runRunsCommand, runSddCommand, runSetupApplyCommand, runSetupLifecycleCommand, runSetupPlanCommand, runSetupRollbackCommand, runSkillCommand, runSubagentCommand, runVerificationPlanCommand, runVerificationReportCommand, runWorkflowExecuteCommand, runWorkflowPreviewCommand, runWorkflowRunCommand, } from './commands/index.js';
8
+ import { runAgentCommand, runApprovalsCommand, runCodeCliCommand, runDefaultInteractiveEntrypoint, runDoctorAliasCommand, runInitCommand, runMcpDoctorCommand, runMcpInstallCommand, runMcpSetupCommand, runMemoryCommand, runMemoryImportCommand, runOpenCodeCommand, runOrchestratorCommand, runPermissionsCommand, runProductNextCommand, runProductResumeCommand, runRunsCommand, runSddCommand, runSetupApplyCommand, runSetupLifecycleCommand, runSetupPlanCommand, runSetupRollbackCommand, runProductStatusCommand, runSkillCommand, runSubagentCommand, runVerificationPlanCommand, runVerificationReportCommand, runWorkflowExecuteCommand, runWorkflowPreviewCommand, runWorkflowRunCommand, } from './commands/index.js';
9
9
  const _promptBuffers = new WeakMap();
10
10
  const require = createRequire(import.meta.url);
11
11
  const packageJson = require('../../package.json');
@@ -17,6 +17,12 @@ export function dispatchCli(argv, environment) {
17
17
  const [area, command] = parsed.positionals;
18
18
  if (!area || area === 'help' || area === '--help' || area === '-h')
19
19
  return okText(visibleHelpText());
20
+ if (area === 'status')
21
+ return runProductStatusCommand(parsed, environment);
22
+ if (area === 'next')
23
+ return runProductNextCommand(parsed, environment);
24
+ if (area === 'resume')
25
+ return runProductResumeCommand(parsed, environment);
20
26
  if (area === 'init')
21
27
  return runSetupPlanCommand(parsed, environment);
22
28
  if (area === 'doctor')
@@ -192,10 +198,12 @@ function validateCommand(area, command) {
192
198
  command === 'execute' ||
193
199
  command === 'status' ||
194
200
  command === 'next' ||
201
+ command === 'continue' ||
195
202
  command === 'cockpit' ||
196
203
  command === 'ready' ||
197
204
  command === 'save-artifact' ||
198
205
  command === 'accept-artifact' ||
206
+ command === 'reopen-artifact' ||
199
207
  command === 'get-artifact' ||
200
208
  command === 'list-artifacts' ||
201
209
  command === 'export' ||
@@ -0,0 +1,32 @@
1
+ export function renderProductResume(resume) {
2
+ const lines = [
3
+ 'Resume',
4
+ ...resume.resume.map((line) => `- ${line}`),
5
+ '',
6
+ 'Why',
7
+ ...resume.why.map((line) => `- ${line}`),
8
+ '',
9
+ 'Blockers',
10
+ ...(resume.blockers.length === 0 ? ['- none'] : resume.blockers.map((line) => `- ${line}`)),
11
+ ...(resume.candidateRuns.length === 0
12
+ ? []
13
+ : [
14
+ '',
15
+ 'Candidate runs',
16
+ ...resume.candidateRuns.flatMap((run) => [
17
+ `- ${run.runId} [${run.status}] ${run.workflow}/${run.phase}`,
18
+ ...(run.userIntent === undefined ? [] : [` Intent: ${run.userIntent}`]),
19
+ ` Latest checkpoint: ${run.latestCheckpointLabel ?? 'none'}`,
20
+ ` Resume command: ${run.command}`,
21
+ ]),
22
+ ]),
23
+ '',
24
+ 'Command',
25
+ `- ${resume.command}`,
26
+ ...(resume.manualNextCommands.length === 0 ? [] : ['', 'Related commands', ...resume.manualNextCommands.map((line) => `- ${line}`)]),
27
+ '',
28
+ 'Safety',
29
+ ...resume.safety.map((line) => `- ${line}`),
30
+ ];
31
+ return `${lines.join('\n')}\n`;
32
+ }
@@ -0,0 +1,74 @@
1
+ export function renderProductStatus(status) {
2
+ const lines = [
3
+ 'Status',
4
+ ...status.status.map((line) => `- ${line}`),
5
+ '',
6
+ 'Blockers',
7
+ ...status.blockers.map((line) => `- ${line}`),
8
+ '',
9
+ 'Next',
10
+ ...status.next.map((line) => `- ${line}`),
11
+ '',
12
+ 'Command',
13
+ `- ${status.command}`,
14
+ '',
15
+ ...(status.relatedRunContext === undefined ? [] : relatedRunContextLines(status.relatedRunContext, true)),
16
+ 'Safety',
17
+ ...status.safety.map((line) => `- ${line}`),
18
+ ];
19
+ return `${lines.join('\n')}\n`;
20
+ }
21
+ export function buildProductNext(status) {
22
+ const blockers = status.blockers.filter((line) => line !== 'none');
23
+ const blocked = status.sddNextStatus === undefined ? blockers.length > 0 : status.sddNextStatus === 'blocked';
24
+ const visibleBlockers = blocked ? blockers : [];
25
+ const command = blocked ? buildBlockedNextCommand(status) : status.command;
26
+ return {
27
+ version: 1,
28
+ kind: 'product-next',
29
+ project: status.project,
30
+ ...(status.change === undefined ? {} : { change: status.change }),
31
+ blocked,
32
+ next: status.next,
33
+ why: status.status,
34
+ blockers: visibleBlockers,
35
+ command,
36
+ ...(status.relatedRunContext === undefined ? {} : { relatedRunContext: status.relatedRunContext }),
37
+ safety: status.safety,
38
+ };
39
+ }
40
+ function buildBlockedNextCommand(status) {
41
+ const change = status.change === undefined ? '<change>' : status.change;
42
+ const dbHint = status.command.includes('--db <path>') ? ' --db <path>' : '';
43
+ return `vgxness next --project ${status.project.value} --change ${change}${dbHint}`;
44
+ }
45
+ export function renderProductNext(next) {
46
+ const lines = [
47
+ 'Next',
48
+ ...next.next.map((line) => `- ${line}`),
49
+ '',
50
+ 'Why',
51
+ ...next.why.map((line) => `- ${line}`),
52
+ ...(next.blockers.length === 0 ? [] : ['', 'Blockers', ...next.blockers.map((line) => `- ${line}`)]),
53
+ '',
54
+ 'Command',
55
+ `- ${next.command}`,
56
+ '',
57
+ ...(next.relatedRunContext === undefined ? [] : relatedRunContextLines(next.relatedRunContext, true)),
58
+ 'Safety',
59
+ ...next.safety.map((line) => `- ${line}`),
60
+ ];
61
+ return `${lines.join('\n')}\n`;
62
+ }
63
+ function relatedRunContextLines(relatedRunContext, includeTrailingBlank) {
64
+ return [
65
+ 'Related interrupted run:',
66
+ `- Run ID: ${relatedRunContext.runId}`,
67
+ `- Status: ${relatedRunContext.status}`,
68
+ `- Workflow/phase: ${relatedRunContext.workflow}/${relatedRunContext.phase}`,
69
+ `- Latest checkpoint: ${relatedRunContext.latestCheckpointLabel ?? 'none'}`,
70
+ `- Recommendation: ${relatedRunContext.recommendation}`,
71
+ `- Resume command: ${relatedRunContext.resumeCommand}`,
72
+ ...(includeTrailingBlank ? [''] : []),
73
+ ];
74
+ }
@@ -1,4 +1,5 @@
1
- import { normalizeSddArtifact, sddPhases } from '../sdd/schema.js';
1
+ import { normalizeSddArtifact, sddPhases, } from '../sdd/schema.js';
2
+ export { sddContinuationPlanFrom } from '../sdd/sdd-continuation-plan.js';
2
3
  export function renderSddStatus(input) {
3
4
  const missing = input.status.phases.filter((phase) => !phase.present).map((phase) => phase.topicKey);
4
5
  const blockers = input.status.phases.filter((phase) => phase.present && phase.accepted !== true);
@@ -34,6 +35,7 @@ export function renderSddStatus(input) {
34
35
  }
35
36
  export function renderSddNext(input) {
36
37
  const blockers = input.decision.blockedPrerequisites ?? [];
38
+ const guidance = input.decision.blockerGuidance ?? [];
37
39
  const phase = input.decision.nextPhase ?? 'none';
38
40
  const commandGuidance = input.decision.status === 'runnable' && input.decision.nextPhase !== undefined
39
41
  ? `vgxness code sdd ${input.decision.change} ${input.decision.nextPhase} --save-artifact`
@@ -47,17 +49,70 @@ export function renderSddNext(input) {
47
49
  `Reason: ${input.decision.reason}`,
48
50
  '',
49
51
  'Blockers:',
50
- ...(blockers.length === 0 ? ['- none'] : blockers.map((blocker) => `- ${blocker.phase}: ${blocker.reason} at ${blocker.topicKey}`)),
52
+ ...(blockers.length === 0
53
+ ? ['- none']
54
+ : blockers.map((blocker) => `- ${blocker.phase}: ${blocker.reason} at ${blocker.topicKey}${blocker.action === undefined ? '' : `; action=${blocker.action}`}`)),
51
55
  '',
52
56
  'Missing topic keys:',
53
57
  ...(input.decision.missingArtifactTopicKeys.length === 0 ? ['- none'] : input.decision.missingArtifactTopicKeys.map((topicKey) => `- ${topicKey}`)),
54
58
  '',
59
+ 'Missing prerequisite topic keys:',
60
+ ...((input.decision.missingPrerequisiteTopicKeys ?? []).length === 0
61
+ ? ['- none']
62
+ : (input.decision.missingPrerequisiteTopicKeys ?? []).map((topicKey) => `- ${topicKey}`)),
63
+ '',
64
+ 'Next actions:',
65
+ ...(guidance.length === 0 ? ['- none'] : guidance.map((item) => `- ${item.phase}: ${item.action}`)),
66
+ '',
55
67
  `Recommended action: ${input.decision.recommendedAction}`,
56
68
  `Command: ${commandGuidance}`,
57
69
  `JSON: vgxness sdd next --project ${input.project} --change ${input.decision.change} --json`,
58
70
  ];
59
71
  return `${lines.join('\n')}\n`;
60
72
  }
73
+ export function renderSddContinuationPlan(plan) {
74
+ const lines = [
75
+ 'SDD Continue (read-only)',
76
+ `Project: ${plan.project}`,
77
+ `Change: ${plan.change}`,
78
+ `Status: ${plan.status}`,
79
+ `Next phase: ${plan.nextPhase ?? 'none'}`,
80
+ `Actionable phase: ${plan.actionablePhase ?? 'none'}`,
81
+ `Reason: ${plan.reason}`,
82
+ '',
83
+ 'Recommended plan:',
84
+ `- ${plan.recommendedAction}`,
85
+ `- Suggested command: ${plan.suggestedCommand}`,
86
+ `- Inspect command: ${plan.inspectCommand}`,
87
+ '',
88
+ 'Blocker-specific next actions:',
89
+ ...(plan.blockerActions.length === 0
90
+ ? ['- none']
91
+ : plan.blockerActions.map((item) => {
92
+ const draftRun = item.draftRunCommand === undefined ? '' : `; draftRunCommand=${item.draftRunCommand}`;
93
+ const warning = item.warning === undefined ? '' : `; warning=${item.warning}`;
94
+ return `- ${item.phase}: ${item.action}; command=${item.command}${draftRun}${warning}`;
95
+ })),
96
+ '',
97
+ ...(plan.relatedRunContext === undefined
98
+ ? []
99
+ : [
100
+ 'Related interrupted run:',
101
+ `- Run ID: ${plan.relatedRunContext.runId}`,
102
+ `- Status: ${plan.relatedRunContext.status}`,
103
+ `- Workflow/phase: ${plan.relatedRunContext.workflow}/${plan.relatedRunContext.phase}`,
104
+ `- Latest checkpoint: ${plan.relatedRunContext.latestCheckpointLabel ?? 'none'}`,
105
+ `- Recommendation: ${plan.relatedRunContext.recommendation}`,
106
+ `- Resume command: ${plan.relatedRunContext.resumeCommand}`,
107
+ '',
108
+ ]),
109
+ 'Safety:',
110
+ ...plan.safety.map((line) => `- ${line}`),
111
+ '',
112
+ `JSON: vgxness sdd continue --project ${plan.project} --change ${plan.change} --json${continuationDbFlag(plan.explicitDatabasePath)}`,
113
+ ];
114
+ return `${lines.join('\n')}\n`;
115
+ }
61
116
  export function renderSddCockpit(cockpit) {
62
117
  const blockers = cockpit.aggregateBlockers;
63
118
  const lines = [
@@ -71,13 +126,18 @@ export function renderSddCockpit(cockpit) {
71
126
  `Legacy artifacts: ${cockpit.legacyCount}`,
72
127
  '',
73
128
  'Aggregate blockers:',
74
- ...(blockers.length === 0 ? ['- none'] : blockers.map((blocker) => `- ${blocker.kind}: ${blocker.phase} at ${blocker.topicKey} — ${blocker.reason}`)),
129
+ ...(blockers.length === 0
130
+ ? ['- none']
131
+ : blockers.map((blocker) => `- ${blocker.kind}: ${blocker.phase} at ${blocker.topicKey} - ${blocker.reason}${blocker.action === undefined ? '' : `; action=${blocker.action}`}`)),
75
132
  '',
76
133
  `Inspect: ${cockpit.inspectCommand}`,
77
134
  `Cockpit JSON: ${cockpit.inspectCommand}`,
78
135
  ];
79
136
  return `${lines.join('\n')}\n`;
80
137
  }
138
+ function continuationDbFlag(explicitDatabasePath) {
139
+ return explicitDatabasePath === undefined ? '' : ` --db ${explicitDatabasePath}`;
140
+ }
81
141
  export function renderSddArtifactAccepted(input) {
82
142
  const lines = [
83
143
  'SDD Artifact Accepted',
@@ -93,6 +153,23 @@ export function renderSddArtifactAccepted(input) {
93
153
  ];
94
154
  return `${lines.join('\n')}\n`;
95
155
  }
156
+ export function renderSddArtifactReopened(input) {
157
+ const lines = [
158
+ 'SDD Artifact Reopened',
159
+ `- Project: ${input.project}`,
160
+ `- Change: ${input.change}`,
161
+ `- Phase: ${input.phase}`,
162
+ `- Status: ${input.status}`,
163
+ `- Topic key: ${input.topicKey}`,
164
+ `- Artifact ID: ${input.artifactId}`,
165
+ `- Reopened by: ${input.reopenedBy.displayName} (${input.reopenedBy.id})`,
166
+ `- Reopened at: ${input.reopenedAt}`,
167
+ ...(input.note === undefined ? [] : [`- Note: ${input.note}`]),
168
+ `Next step: update the ${input.phase} artifact, then request human acceptance again.`,
169
+ `JSON: vgxness sdd reopen-artifact --project ${input.project} --change ${input.change} --phase ${input.phase} --actor ${input.reopenedBy.id} --json`,
170
+ ];
171
+ return `${lines.join('\n')}\n`;
172
+ }
96
173
  export function renderSddArtifactList(input) {
97
174
  const artifactsByPhase = new Map(input.artifacts.map((artifact) => [artifact.phase, artifact]));
98
175
  const hasArtifacts = input.artifacts.length > 0;
@@ -1,3 +1,4 @@
1
+ import { normalizeSddPhaseInput } from '../../sdd/schema.js';
1
2
  import { resolveCodeConfig } from '../config/defaults.js';
2
3
  import { CodeProviderRegistry } from '../providers/provider-registry.js';
3
4
  import { exitCodeForCodeStatus, renderHumanCodeSummary } from '../reporting/summary.js';
@@ -31,11 +32,12 @@ export async function runCodeCommand(input) {
31
32
  };
32
33
  const intent = input.command === 'sdd' ? `Run SDD ${input.args[1] ?? ''} for ${input.args[0] ?? ''}`.trim() : input.args.join(' ').trim();
33
34
  const changeId = input.command === 'sdd' ? input.args[0] : undefined;
34
- const phase = input.command === 'sdd' ? input.args[1] : undefined;
35
- if (input.command === 'sdd' && (changeId === undefined || phase === undefined))
35
+ const phaseInput = input.command === 'sdd' ? input.args[1] : undefined;
36
+ const phase = phaseInput === undefined ? undefined : normalizeSddPhaseInput(phaseInput);
37
+ if (input.command === 'sdd' && (changeId === undefined || phaseInput === undefined))
36
38
  return { exitCode: 1, stdout: '', stderr: 'Missing sdd change and phase\n' };
37
- if (input.command === 'sdd' && phase !== undefined && !isSddCodePhase(phase))
38
- return { exitCode: 1, stdout: '', stderr: `Unknown SDD phase: ${phase}\n` };
39
+ if (input.command === 'sdd' && phaseInput !== undefined && phase === undefined)
40
+ return { exitCode: 1, stdout: '', stderr: `Unknown SDD phase: ${phaseInput}\n` };
39
41
  if (!intent)
40
42
  return {
41
43
  exitCode: 1,
@@ -77,6 +79,7 @@ export async function runCodeCommand(input) {
77
79
  cwd: input.cwd,
78
80
  ...(changeId === undefined ? {} : { changeId }),
79
81
  ...(phase !== undefined && isSddCodePhase(phase) ? { phase } : {}),
82
+ ...(input.sddRuntimeMode === undefined ? {} : { sddRuntimeMode: input.sddRuntimeMode }),
80
83
  ...(input.persistArtifact === undefined ? {} : { persistArtifact: input.persistArtifact }),
81
84
  ...(input.sddGateway === undefined ? {} : { sddGateway: input.sddGateway }),
82
85
  ...(input.memoryGateway === undefined ? {} : { memoryGateway: input.memoryGateway }),
@@ -21,7 +21,10 @@ export function renderHumanCodeSummary(summary) {
21
21
  ...(summary.sdd === undefined
22
22
  ? []
23
23
  : [
24
- `SDD: ${summary.sdd.changeId}/${summary.sdd.phase} saved=${summary.sdd.artifactSaved ? 'yes' : 'no'}`,
24
+ `SDD: ${summary.sdd.changeId}/${summary.sdd.phase} mode=${summary.sdd.sddRuntimeMode ?? 'strict'} saved=${summary.sdd.artifactSaved ? 'yes' : 'no'}`,
25
+ ...(summary.sdd.sddRuntimeMode === 'draft-run'
26
+ ? ['SDD draft-run note: draft prerequisites are used for planning only; human acceptance is still required; apply-progress remains gated.']
27
+ : []),
25
28
  `Next SDD step: ${summary.sdd.nextRecommendation}`,
26
29
  ]),
27
30
  `Risks: ${summary.risks.length === 0 ? 'none reported' : summary.risks.join('; ')}`,
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
+ import { normalizeSddPhaseInput } from '../../sdd/schema.js';
2
3
  import { resolveCodeConfig } from '../config/defaults.js';
3
4
  import { buildCodePrompt } from '../prompts/prompt-builder.js';
4
5
  import { CodeProviderError } from '../providers/provider-adapter.js';
@@ -30,7 +31,13 @@ export class CodeRuntime {
30
31
  if (input.changeId === undefined || input.phase === undefined || !isSddCodePhase(input.phase))
31
32
  throw new Error('SDD mode requires a valid changeId and phase');
32
33
  const gateway = input.sddGateway ?? new InMemorySddGateway();
33
- sddContext = await loadSddContext({ gateway, project, changeId: input.changeId, phase: input.phase });
34
+ sddContext = await loadSddContext({
35
+ gateway,
36
+ project,
37
+ changeId: input.changeId,
38
+ phase: input.phase,
39
+ ...(input.sddRuntimeMode === undefined ? {} : { runtimeMode: input.sddRuntimeMode }),
40
+ });
34
41
  }
35
42
  const availableDefinitions = input.mode === 'sdd' && input.phase !== undefined
36
43
  ? sddToolDefinitions({ phase: input.phase, persist: input.persistArtifact === true })
@@ -117,6 +124,7 @@ export class CodeRuntime {
117
124
  ...(diagnostics === undefined ? {} : { diagnostics: diagnostics }),
118
125
  },
119
126
  ...(sddContext === undefined ? {} : { changeId: sddContext.changeId, sddPhase: sddContext.phase }),
127
+ ...(sddContext === undefined ? {} : { sddRuntimeMode: sddContext.runtimeMode }),
120
128
  });
121
129
  if (sddContext !== undefined) {
122
130
  await this.checkpoint(run.id, session, 'sdd-context-loaded', {
@@ -480,7 +488,15 @@ export class CodeRuntime {
480
488
  memory,
481
489
  ...(sddContext === undefined
482
490
  ? {}
483
- : { sdd: { changeId: sddContext.changeId, phase: sddContext.phase, artifactSaved, nextRecommendation: sddContext.next.recommendedAction } }),
491
+ : {
492
+ sdd: {
493
+ changeId: sddContext.changeId,
494
+ phase: sddContext.phase,
495
+ ...(sddContext.runtimeMode === undefined ? {} : { sddRuntimeMode: sddContext.runtimeMode }),
496
+ artifactSaved,
497
+ nextRecommendation: sddContext.next.recommendedAction,
498
+ },
499
+ }),
484
500
  };
485
501
  const summary = config.transcriptMode === 'off' ? baseSummary : { ...baseSummary, transcript: buildTranscript(config.transcriptMode, session.checkpoints, toolResults) };
486
502
  if (sddContext !== undefined)
@@ -533,7 +549,13 @@ export class CodeRuntime {
533
549
  verification: { status: 'not-run', reason: 'SDD phase blocked before provider execution.' },
534
550
  filesChanged: [],
535
551
  risks: [answer],
536
- sdd: { changeId: sddContext.changeId, phase: sddContext.phase, artifactSaved: false, nextRecommendation: sddContext.next.recommendedAction },
552
+ sdd: {
553
+ changeId: sddContext.changeId,
554
+ phase: sddContext.phase,
555
+ ...(sddContext.runtimeMode === undefined ? {} : { sddRuntimeMode: sddContext.runtimeMode }),
556
+ artifactSaved: false,
557
+ nextRecommendation: sddContext.next.recommendedAction,
558
+ },
537
559
  };
538
560
  await this.runGateway.finalize(runId, { status: 'blocked', outcome: 'blocked', outcomeReason: answer });
539
561
  return summary;
@@ -641,7 +663,8 @@ export class CodeRuntime {
641
663
  changedFiles: [],
642
664
  };
643
665
  if (call.name === 'sdd_read_artifact') {
644
- const phase = typeof call.input.phase === 'string' && isSddCodePhase(call.input.phase) ? call.input.phase : context.phase;
666
+ const requestedPhase = typeof call.input.phase === 'string' ? normalizeSddPhaseInput(call.input.phase) : undefined;
667
+ const phase = requestedPhase !== undefined && isSddCodePhase(requestedPhase) ? requestedPhase : context.phase;
645
668
  const artifact = await gateway.getArtifact({ project, change: context.changeId, phase });
646
669
  return {
647
670
  result: { toolCallId: call.id, toolName: call.name, ok: true, output: { phase, artifact }, metadata: { capability, truncated: false } },
@@ -10,6 +10,7 @@ export function shellEnabledForSddPhase(phase) {
10
10
  return phase === 'apply-progress' || phase === 'verify';
11
11
  }
12
12
  export async function loadSddContext(input) {
13
+ const runtimeMode = input.runtimeMode ?? 'strict';
13
14
  const [status, next] = await Promise.all([
14
15
  input.gateway.getStatus({ project: input.project, change: input.changeId }),
15
16
  input.gateway.getNextPhase({ project: input.project, change: input.changeId }),
@@ -22,7 +23,7 @@ export async function loadSddContext(input) {
22
23
  if (artifact !== null)
23
24
  artifacts.push(toContextArtifact(input.project, input.changeId, phase, phase === input.phase ? 'active' : 'accepted', artifact.content, phaseStatus));
24
25
  }
25
- return { changeId: input.changeId, phase: input.phase, status, next, artifacts };
26
+ return { changeId: input.changeId, phase: input.phase, runtimeMode, status, next, artifacts };
26
27
  }
27
28
  function toContextArtifact(project, change, phase, role, content, phaseStatus) {
28
29
  const bytes = Buffer.byteLength(content, 'utf8');
@@ -98,6 +99,8 @@ export function validateSddReadiness(context) {
98
99
  recommendedAction: `Accept or restore prerequisites before running ${context.phase}.`,
99
100
  },
100
101
  };
102
+ if (isDraftRun(context) && isPlanningPhase(context.phase))
103
+ return { ok: true };
101
104
  if (present)
102
105
  return { ok: true };
103
106
  if (context.status.nextReadyPhase === context.phase)
@@ -123,10 +126,23 @@ export function activeBlockedPrerequisites(context) {
123
126
  return [{ phase, topicKey, reason: 'missing' }];
124
127
  if (status.accepted === true && status.legacy !== true)
125
128
  return [];
126
- const reason = status.legacy === true ? 'legacy' : status.state === 'rejected' || status.state === 'superseded' ? status.state : 'draft';
129
+ if (status.legacy === true)
130
+ return [{ phase, topicKey, reason: 'legacy', ...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }) }];
131
+ if (draftPrerequisiteAllowed(context, status.state ?? 'draft'))
132
+ return [];
133
+ const reason = status.state === 'rejected' || status.state === 'superseded' ? status.state : 'draft';
127
134
  return [{ phase, topicKey, reason, ...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }) }];
128
135
  });
129
136
  }
137
+ function draftPrerequisiteAllowed(context, state) {
138
+ return isDraftRun(context) && isPlanningPhase(context.phase) && state === 'draft';
139
+ }
140
+ function isDraftRun(context) {
141
+ return context.runtimeMode === 'draft-run';
142
+ }
143
+ function isPlanningPhase(phase) {
144
+ return phase === 'explore' || phase === 'proposal' || phase === 'spec' || phase === 'design' || phase === 'tasks';
145
+ }
130
146
  function acceptedArtifactPhases(active) {
131
147
  return prerequisitePhases(active);
132
148
  }