vgxness 1.9.1 → 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 (54) hide show
  1. package/README.md +15 -5
  2. package/dist/agents/agent-activation-service.js +13 -4
  3. package/dist/agents/agent-registry-service.js +8 -2
  4. package/dist/agents/agent-resolver.js +33 -3
  5. package/dist/agents/agent-seed-upgrade-service.js +231 -0
  6. package/dist/agents/boot-upgrade.js +59 -0
  7. package/dist/agents/canonical-agent-manifest.js +39 -18
  8. package/dist/agents/canonical-agent-projection.js +38 -4
  9. package/dist/agents/manager-profile-overlay-service.js +14 -0
  10. package/dist/agents/repositories/agent-seed-history.js +128 -0
  11. package/dist/cli/cli-help.js +14 -3
  12. package/dist/cli/commands/index.js +1 -0
  13. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +8 -0
  14. package/dist/cli/commands/mcp-dispatcher.js +7 -0
  15. package/dist/cli/commands/memory-sdd-dispatcher.js +71 -5
  16. package/dist/cli/commands/status-dispatcher.js +130 -0
  17. package/dist/cli/commands/workflow-dispatcher.js +11 -5
  18. package/dist/cli/dispatcher.js +11 -1
  19. package/dist/cli/product-resume-renderer.js +32 -0
  20. package/dist/cli/product-status-renderer.js +74 -0
  21. package/dist/cli/sdd-renderer.js +80 -3
  22. package/dist/code/cli/code-command.js +7 -4
  23. package/dist/code/reporting/summary.js +4 -1
  24. package/dist/code/runtime/code-runtime.js +27 -4
  25. package/dist/code/runtime/sdd-context.js +18 -2
  26. package/dist/governance/governance-report-builder.js +18 -7
  27. package/dist/mcp/claude-code-agent-config.js +10 -4
  28. package/dist/mcp/client-install-claude-code-contract.js +19 -4
  29. package/dist/mcp/client-install-claude-code.js +2 -2
  30. package/dist/mcp/control-plane.js +78 -5
  31. package/dist/mcp/provider-status.js +89 -88
  32. package/dist/mcp/schema.js +42 -8
  33. package/dist/mcp/stdio-server.js +6 -0
  34. package/dist/mcp/validation.js +77 -5
  35. package/dist/memory/sqlite/migrations/016_agent_seed_history.sql +15 -0
  36. package/dist/resume/product-resume.js +166 -0
  37. package/dist/runs/repositories/runs.js +12 -1
  38. package/dist/runs/run-service.js +62 -5
  39. package/dist/runs/schema.js +4 -0
  40. package/dist/sdd/schema.js +20 -0
  41. package/dist/sdd/sdd-continuation-plan.js +81 -0
  42. package/dist/sdd/sdd-workflow-service.js +146 -18
  43. package/dist/skills/skill-resolver.js +21 -4
  44. package/dist/status/product-status.js +117 -0
  45. package/docs/architecture.md +9 -1
  46. package/docs/cli.md +38 -13
  47. package/docs/code-runtime.md +3 -0
  48. package/docs/contributing.md +1 -1
  49. package/docs/glossary.md +2 -2
  50. package/docs/mcp.md +20 -6
  51. package/docs/project-health-audit-v1.9.1.md +126 -0
  52. package/docs/providers.md +4 -4
  53. package/docs/safety.md +1 -1
  54. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import { isRiskyPermissionCategory, permissionCategories } from '../permissions/schema.js';
2
2
  import { parseOperationRetryPolicy } from '../runs/operation-retry.js';
3
- import { isSddPhase, sddPhases } from '../sdd/schema.js';
3
+ import { normalizeSddPhaseInput, sddPhases } from '../sdd/schema.js';
4
4
  import { workflowIds } from '../workflows/schema.js';
5
5
  import { supportedTypeMessage, verificationChangeTypes } from '../verification/index.js';
6
6
  import { errorEnvelope, isVgxMcpToolName, } from './schema.js';
@@ -42,6 +42,8 @@ export function validateVgxMcpToolCall(call) {
42
42
  return validationSuccess(tool.value, validateSddSaveArtifactInput(input, tool.value));
43
43
  case 'vgxness_sdd_accept_artifact':
44
44
  return validationSuccess(tool.value, validateSddAcceptArtifactInput(input, tool.value));
45
+ case 'vgxness_sdd_reopen_artifact':
46
+ return validationSuccess(tool.value, validateSddReopenArtifactInput(input, tool.value));
45
47
  case 'vgxness_sdd_get_artifact':
46
48
  return validationSuccess(tool.value, validateSddGetArtifactInput(input, tool.value));
47
49
  case 'vgxness_sdd_list_artifacts':
@@ -50,6 +52,8 @@ export function validateVgxMcpToolCall(call) {
50
52
  return validationSuccess(tool.value, validateSddNextInput(input, tool.value));
51
53
  case 'vgxness_sdd_cockpit':
52
54
  return validationSuccess(tool.value, validateSddCockpitInput(input, tool.value));
55
+ case 'vgxness_sdd_continue':
56
+ return validationSuccess(tool.value, validateSddContinueInput(input, tool.value));
53
57
  case 'vgxness_governance_report':
54
58
  return validationSuccess(tool.value, validateGovernanceReportInput(input, tool.value));
55
59
  case 'vgxness_memory_save':
@@ -96,6 +100,8 @@ export function validateVgxMcpToolCall(call) {
96
100
  return validationSuccess(tool.value, validateRunCheckpointInput(input, tool.value));
97
101
  case 'vgxness_run_finalize':
98
102
  return validationSuccess(tool.value, validateRunFinalizeInput(input, tool.value));
103
+ case 'vgxness_run_resume_candidates':
104
+ return validationSuccess(tool.value, validateRunResumeCandidatesInput(input, tool.value));
99
105
  case 'vgxness_run_resume_inspect':
100
106
  return validationSuccess(tool.value, validateRunResumeInspectInput(input, tool.value));
101
107
  case 'vgxness_run_resume_gate':
@@ -238,6 +244,21 @@ function validateSddCockpitInput(input, tool) {
238
244
  return record;
239
245
  return readProjectAndChange(record.value, tool);
240
246
  }
247
+ function validateSddContinueInput(input, tool) {
248
+ const record = inputRecord(input, tool, ['project', 'change', 'payloadMode']);
249
+ if (!record.ok)
250
+ return record;
251
+ const base = readProjectAndChange(record.value, tool);
252
+ if (!base.ok)
253
+ return base;
254
+ const payloadMode = readOptionalOneOf(record.value, 'payloadMode', payloadModes, tool);
255
+ if (!payloadMode.ok)
256
+ return payloadMode;
257
+ const result = { ...base.value };
258
+ if (payloadMode.value !== undefined)
259
+ result.payloadMode = payloadMode.value;
260
+ return { ok: true, value: result };
261
+ }
241
262
  function validateContextCockpitInput(input, tool) {
242
263
  const record = inputRecord(input, tool, ['project', 'change', 'directory', 'limit', 'level']);
243
264
  if (!record.ok)
@@ -312,7 +333,7 @@ function validateSddSaveArtifactInput(input, tool) {
312
333
  return { ok: true, value: { ...base.value, phase: phase.value, content: content.value } };
313
334
  }
314
335
  function validateSddAcceptArtifactInput(input, tool) {
315
- const record = inputRecord(input, tool, ['project', 'change', 'phase', 'acceptedBy', 'acceptedAt', 'note', 'notes', 'rationale', 'runId', 'agentId']);
336
+ const record = inputRecord(input, tool, ['project', 'change', 'phase', 'acceptedBy', 'acceptedAt', 'note', 'runId', 'agentId']);
316
337
  if (!record.ok)
317
338
  return record;
318
339
  const base = readProjectAndChange(record.value, tool);
@@ -340,7 +361,41 @@ function validateSddAcceptArtifactInput(input, tool) {
340
361
  if (displayName.value !== undefined)
341
362
  acceptedBy.displayName = displayName.value;
342
363
  const result = { ...base.value, phase: phase.value, acceptedBy };
343
- const copied = copyOptionalLimitedStrings(result, record.value, tool, ['acceptedAt', 'note', 'notes', 'rationale', 'runId', 'agentId'], undefined);
364
+ const copied = copyOptionalLimitedStrings(result, record.value, tool, ['acceptedAt', 'note', 'runId', 'agentId'], undefined);
365
+ if (!copied.ok)
366
+ return copied;
367
+ return { ok: true, value: result };
368
+ }
369
+ function validateSddReopenArtifactInput(input, tool) {
370
+ const record = inputRecord(input, tool, ['project', 'change', 'phase', 'reopenedBy', 'reopenedAt', 'note', 'runId', 'agentId']);
371
+ if (!record.ok)
372
+ return record;
373
+ const base = readProjectAndChange(record.value, tool);
374
+ if (!base.ok)
375
+ return base;
376
+ const phase = readPhase(record.value, tool);
377
+ if (!phase.ok)
378
+ return phase;
379
+ const reopenedByRecord = asRecord(record.value.reopenedBy, tool, 'reopenedBy is required');
380
+ if (!reopenedByRecord.ok)
381
+ return reopenedByRecord;
382
+ const reopenedByExtra = Object.keys(reopenedByRecord.value).find((key) => !['type', 'id', 'displayName'].includes(key));
383
+ if (reopenedByExtra !== undefined)
384
+ return validationFailure(`Unexpected field: reopenedBy.${reopenedByExtra}`, tool);
385
+ const type = readRequiredOneOf(reopenedByRecord.value, 'type', ['human'], tool);
386
+ if (!type.ok)
387
+ return type;
388
+ const id = readNonEmptyString(reopenedByRecord.value, 'id', tool);
389
+ if (!id.ok)
390
+ return id;
391
+ const displayName = readOptionalNonEmptyString(reopenedByRecord.value, 'displayName', tool);
392
+ if (!displayName.ok)
393
+ return displayName;
394
+ const reopenedBy = { type: type.value, id: id.value };
395
+ if (displayName.value !== undefined)
396
+ reopenedBy.displayName = displayName.value;
397
+ const result = { ...base.value, phase: phase.value, reopenedBy };
398
+ const copied = copyOptionalLimitedStrings(result, record.value, tool, ['reopenedAt', 'note', 'runId', 'agentId'], undefined);
344
399
  if (!copied.ok)
345
400
  return copied;
346
401
  return { ok: true, value: result };
@@ -935,6 +990,22 @@ function validateRunFinalizeInput(input, tool) {
935
990
  return copied;
936
991
  return { ok: true, value: result };
937
992
  }
993
+ function validateRunResumeCandidatesInput(input, tool) {
994
+ const record = inputRecord(input, tool, ['project', 'limit']);
995
+ if (!record.ok)
996
+ return record;
997
+ const project = readNonEmptyString(record.value, 'project', tool);
998
+ if (!project.ok)
999
+ return project;
1000
+ const result = { project: project.value };
1001
+ if (record.value.limit !== undefined) {
1002
+ const limit = readBoundedLimit(record.value, tool);
1003
+ if (!limit.ok)
1004
+ return limit;
1005
+ result.limit = limit.value;
1006
+ }
1007
+ return { ok: true, value: result };
1008
+ }
938
1009
  function validateRunResumeInspectInput(input, tool) {
939
1010
  const record = inputRecord(input, tool, ['runId']);
940
1011
  if (!record.ok)
@@ -1017,9 +1088,10 @@ function readPhase(record, tool) {
1017
1088
  const phase = readNonEmptyString(record, 'phase', tool);
1018
1089
  if (!phase.ok)
1019
1090
  return phase;
1020
- if (!isSddPhase(phase.value))
1091
+ const normalized = normalizeSddPhaseInput(phase.value);
1092
+ if (normalized === undefined)
1021
1093
  return validationFailure(`phase must be one of: ${sddPhases.join(', ')}`, tool);
1022
- return { ok: true, value: phase.value };
1094
+ return { ok: true, value: normalized };
1023
1095
  }
1024
1096
  function copyOptionalStrings(target, record, tool, fields) {
1025
1097
  for (const field of fields) {
@@ -0,0 +1,15 @@
1
+ CREATE TABLE agent_seed_history (
2
+ id TEXT PRIMARY KEY,
3
+ applied_at TEXT NOT NULL,
4
+ from_version INTEGER,
5
+ to_version INTEGER NOT NULL,
6
+ agent_name TEXT NOT NULL,
7
+ project TEXT NOT NULL,
8
+ scope TEXT NOT NULL,
9
+ outcome TEXT NOT NULL,
10
+ reason TEXT,
11
+ source TEXT NOT NULL
12
+ );
13
+
14
+ CREATE INDEX idx_agent_seed_history_applied_at ON agent_seed_history(applied_at);
15
+ CREATE INDEX idx_agent_seed_history_project_scope_name ON agent_seed_history(project, scope, agent_name);
@@ -0,0 +1,166 @@
1
+ import { basename, resolve } from 'node:path';
2
+ const candidateLimit = 5;
3
+ const baseSafety = [
4
+ 'Read-only resume cockpit: does not execute providers, retry operations, edit files, write provider config, create sandboxes, or create worktrees.',
5
+ 'This command only helps inspect interrupted work; any continuation remains an explicit human decision.',
6
+ ];
7
+ export function buildProductResume(input) {
8
+ const initialProject = resolveProject(input);
9
+ if (input.runId === undefined) {
10
+ return buildOrientationResume(input, initialProject);
11
+ }
12
+ if (input.databaseError !== undefined) {
13
+ return blockedResume({
14
+ project: initialProject,
15
+ runId: input.runId,
16
+ blocker: `Unable to read local memory store${input.databasePath === undefined ? '' : ` at ${input.databasePath}`}: ${input.databaseError}`,
17
+ why: 'Run inspection requires a readable local memory store.',
18
+ command: `vgxness resume --project ${initialProject.value} --run-id ${input.runId} --db <path>`,
19
+ safety: baseSafety,
20
+ });
21
+ }
22
+ if (input.runs?.getRunOperatorResumePlan === undefined) {
23
+ return blockedResume({
24
+ project: initialProject,
25
+ runId: input.runId,
26
+ blocker: 'Run resume inspection service is not available for this request.',
27
+ why: 'Use the installed CLI or provide a readable local memory store.',
28
+ command: `vgxness resume --project ${initialProject.value} --run-id ${input.runId}`,
29
+ safety: baseSafety,
30
+ });
31
+ }
32
+ const inspected = input.runs.getRunOperatorResumePlan(input.runId);
33
+ if (!inspected.ok) {
34
+ return blockedResume({
35
+ project: initialProject,
36
+ runId: input.runId,
37
+ blocker: inspected.error.message,
38
+ why: 'VGXNESS could not find or inspect the requested run.',
39
+ command: `vgxness runs list --project ${initialProject.value}`,
40
+ safety: baseSafety,
41
+ });
42
+ }
43
+ return fromRunOperatorResumePlan(inspected.value, initialProject, input.databasePath !== undefined);
44
+ }
45
+ function buildOrientationResume(input, project) {
46
+ const dbHint = input.databasePath === undefined ? '' : ` --db ${input.databasePath}`;
47
+ const command = `vgxness runs list --project ${project.value} --status failed${dbHint}`;
48
+ const manualNextCommands = [
49
+ `vgxness runs list --project ${project.value} --status failed${dbHint}`,
50
+ `vgxness runs list --project ${project.value} --status blocked${dbHint}`,
51
+ `vgxness runs list --project ${project.value} --status needs-human${dbHint}`,
52
+ `vgxness resume --project ${project.value} --run-id <id>${dbHint}`,
53
+ ];
54
+ if (input.databaseError !== undefined) {
55
+ return {
56
+ version: 1,
57
+ kind: 'product-resume',
58
+ mode: 'orientation',
59
+ project,
60
+ resumable: false,
61
+ resume: ['Find an interrupted run, then inspect it before deciding whether to continue manually.'],
62
+ why: ['No --run-id was provided, so VGXNESS cannot inspect checkpoint, approval, or blocker state for a selected run yet.'],
63
+ blockers: [`Unable to read local memory store${input.databasePath === undefined ? '' : ` at ${input.databasePath}`}: ${input.databaseError}`],
64
+ command,
65
+ manualNextCommands,
66
+ safety: baseSafety,
67
+ candidateRuns: [],
68
+ };
69
+ }
70
+ const listed = input.runs?.listRecentInterruptedRuns?.({ project: project.value, limit: candidateLimit });
71
+ if (listed !== undefined && !listed.ok) {
72
+ return {
73
+ version: 1,
74
+ kind: 'product-resume',
75
+ mode: 'orientation',
76
+ project,
77
+ resumable: false,
78
+ resume: ['Find an interrupted run, then inspect it before deciding whether to continue manually.'],
79
+ why: ['No --run-id was provided, so VGXNESS cannot inspect checkpoint, approval, or blocker state for a selected run yet.'],
80
+ blockers: [listed.error.message],
81
+ command,
82
+ manualNextCommands,
83
+ safety: baseSafety,
84
+ candidateRuns: [],
85
+ };
86
+ }
87
+ const candidateRuns = (listed?.value ?? []).map((candidate) => ({
88
+ ...candidate,
89
+ ...(candidate.userIntent === undefined ? {} : { userIntent: shorten(candidate.userIntent, 72) }),
90
+ command: `vgxness resume --project ${project.value} --run-id ${candidate.runId}${dbHint}`,
91
+ }));
92
+ const why = listed === undefined
93
+ ? ['No --run-id was provided, so VGXNESS cannot inspect checkpoint, approval, or blocker state for a selected run yet.']
94
+ : ['No --run-id was provided, so VGXNESS is showing recent failed, blocked, or needs-human runs for this project.'];
95
+ return {
96
+ version: 1,
97
+ kind: 'product-resume',
98
+ mode: 'orientation',
99
+ project,
100
+ resumable: false,
101
+ resume: candidateRuns.length === 0
102
+ ? ['Find an interrupted run, then inspect it before deciding whether to continue manually.']
103
+ : ['Recent interrupted runs were found. Inspect one before deciding whether to continue manually.'],
104
+ why,
105
+ blockers: [],
106
+ command,
107
+ manualNextCommands,
108
+ safety: [listed === undefined ? 'Did not open the local memory store because no project-specific run lookup was requested.' : 'Opened the local memory store read-only to list recent interrupted runs.', ...baseSafety],
109
+ candidateRuns,
110
+ };
111
+ }
112
+ function resolveProject(input) {
113
+ if (input.project !== undefined && input.project.trim().length > 0)
114
+ return { value: input.project.trim(), source: 'flag' };
115
+ const directoryName = basename(resolve(input.cwd));
116
+ return { value: directoryName.length > 0 ? directoryName : 'unknown-project', source: 'cwd' };
117
+ }
118
+ function fromRunOperatorResumePlan(inspect, fallbackProject, includeDbHint) {
119
+ const project = { value: inspect.run.project || fallbackProject.value, source: 'run' };
120
+ const blockerLines = inspect.blockers.map((blocker) => (blocker.relatedId === undefined ? blocker.message : `${blocker.message} (${blocker.relatedId})`));
121
+ const latestCheckpoint = inspect.latestCheckpoint === null ? 'none' : `${inspect.latestCheckpoint.label} (${inspect.latestCheckpoint.id})`;
122
+ const dbHint = includeDbHint ? ' --db <path>' : '';
123
+ return {
124
+ version: 1,
125
+ kind: 'product-resume',
126
+ mode: 'inspect',
127
+ project,
128
+ runId: inspect.run.id,
129
+ resumable: inspect.resumePlan.resumable,
130
+ resume: [inspect.resumePlan.recommendedAction],
131
+ why: [
132
+ `Run: ${inspect.run.id}`,
133
+ `Status: ${inspect.run.status}`,
134
+ `Workflow: ${inspect.run.workflow} / ${inspect.run.phase}`,
135
+ `Latest checkpoint: ${latestCheckpoint}`,
136
+ inspect.debug.summary,
137
+ ],
138
+ blockers: blockerLines,
139
+ command: `vgxness resume --project ${project.value} --run-id ${inspect.run.id}${dbHint}`,
140
+ manualNextCommands: inspect.manualNextCommands,
141
+ safety: baseSafety,
142
+ candidateRuns: [],
143
+ inspect,
144
+ };
145
+ }
146
+ function blockedResume(input) {
147
+ return {
148
+ version: 1,
149
+ kind: 'product-resume',
150
+ mode: 'inspect',
151
+ project: input.project,
152
+ runId: input.runId,
153
+ resumable: false,
154
+ resume: ['Resolve the blocker, then inspect the run again before deciding whether to continue manually.'],
155
+ why: [`Run: ${input.runId}`, input.why],
156
+ blockers: [input.blocker],
157
+ command: input.command,
158
+ manualNextCommands: [input.command],
159
+ safety: input.safety,
160
+ candidateRuns: [],
161
+ };
162
+ }
163
+ function shorten(value, maxLength) {
164
+ const normalized = value.replace(/\s+/g, ' ').trim();
165
+ return normalized.length <= maxLength ? normalized : `${normalized.slice(0, maxLength - 3)}...`;
166
+ }
@@ -70,13 +70,24 @@ export class RunRepository {
70
70
  where.push('status=@status');
71
71
  parameters.status = filters.status;
72
72
  }
73
+ if (filters.statuses !== undefined && filters.statuses.length > 0) {
74
+ const statusPlaceholders = filters.statuses.map((_, index) => `@status${index}`);
75
+ where.push(`status IN (${statusPlaceholders.join(', ')})`);
76
+ for (const [index, status] of filters.statuses.entries())
77
+ parameters[`status${index}`] = status;
78
+ }
79
+ const limitClause = filters.limit === undefined ? '' : 'LIMIT @limit';
80
+ const queryParameters = { ...parameters };
81
+ if (filters.limit !== undefined)
82
+ queryParameters.limit = filters.limit;
73
83
  const rows = this.db.connection
74
84
  .prepare(`
75
85
  SELECT * FROM runs
76
86
  ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
77
87
  ORDER BY created_at DESC
88
+ ${limitClause}
78
89
  `)
79
- .all(parameters);
90
+ .all(queryParameters);
80
91
  return ok(rows.map(mapRun));
81
92
  }
82
93
  catch (cause) {
@@ -1,5 +1,6 @@
1
1
  import { evaluatePermission } from '../permissions/policy-evaluator.js';
2
2
  import { isRiskyPermissionCategory } from '../permissions/schema.js';
3
+ import { normalizeSddPhaseInput } from '../sdd/schema.js';
3
4
  import { planExecutionIsolation } from './execution-planning.js';
4
5
  import { evaluateOperationRetry as evaluateOperationRetryPolicy } from './operation-retry.js';
5
6
  import { RunRepository, } from './repositories/runs.js';
@@ -28,7 +29,7 @@ export class RunService {
28
29
  this.runs = new RunRepository(database);
29
30
  }
30
31
  createRun(input) {
31
- return this.runs.create(input);
32
+ return this.runs.create({ ...input, phase: canonicalRunPhase(input.workflow, input.phase) ?? input.phase });
32
33
  }
33
34
  getRun(id) {
34
35
  return this.runs.getDetails(id);
@@ -44,6 +45,53 @@ export class RunService {
44
45
  listRuns(filters = {}) {
45
46
  return this.runs.list(filters);
46
47
  }
48
+ listRecentInterruptedRuns(input) {
49
+ const runs = this.runs.list({ project: input.project, statuses: ['failed', 'blocked', 'needs-human'], limit: input.limit ?? 5 });
50
+ if (!runs.ok)
51
+ return runs;
52
+ const candidates = [];
53
+ for (const run of runs.value) {
54
+ const details = this.runs.getDetails(run.id);
55
+ if (!details.ok)
56
+ return details;
57
+ const latestCheckpoint = details.value.checkpoints.at(-1);
58
+ const userIntent = details.value.userIntent.trim();
59
+ candidates.push({
60
+ runId: details.value.id,
61
+ status: details.value.status,
62
+ workflow: details.value.workflow,
63
+ phase: details.value.phase,
64
+ ...(userIntent.length === 0 ? {} : { userIntent }),
65
+ ...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
66
+ });
67
+ }
68
+ return { ok: true, value: candidates };
69
+ }
70
+ findRelatedInterruptedSddRun(input) {
71
+ const runs = this.runs.list({ project: input.project, statuses: ['failed', 'blocked', 'needs-human'], limit: input.limit ?? 25 });
72
+ if (!runs.ok)
73
+ return runs;
74
+ for (const run of runs.value) {
75
+ const details = this.runs.getDetails(run.id);
76
+ if (!details.ok)
77
+ return details;
78
+ const hasMatchingCheckpoint = details.value.checkpoints.some((checkpoint) => checkpointHasChangeId(checkpoint.state, input.change));
79
+ if (!hasMatchingCheckpoint)
80
+ continue;
81
+ const latestCheckpoint = details.value.checkpoints.at(-1);
82
+ return {
83
+ ok: true,
84
+ value: {
85
+ runId: details.value.id,
86
+ status: details.value.status,
87
+ workflow: details.value.workflow,
88
+ phase: details.value.phase,
89
+ ...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
90
+ },
91
+ };
92
+ }
93
+ return { ok: true, value: undefined };
94
+ }
47
95
  appendEvent(input) {
48
96
  return this.runs.appendEvent(input);
49
97
  }
@@ -695,10 +743,11 @@ export class RunService {
695
743
  const run = this.runs.getById(input.runId);
696
744
  if (!run.ok)
697
745
  return { ok: false, error: run.error };
698
- const phase = input.phase ?? canonicalRunPhase(run.value.phase);
746
+ const workflow = input.workflow ?? run.value.workflow;
747
+ const phase = canonicalRunPhase(workflow, input.phase) ?? canonicalRunPhase(run.value.workflow, run.value.phase);
699
748
  const merged = {
700
749
  ...input,
701
- workflow: input.workflow ?? run.value.workflow,
750
+ workflow,
702
751
  ...(phase === undefined ? {} : { phase }),
703
752
  agentId: input.agentId ?? input.agent?.id ?? run.value.selectedAgentId,
704
753
  };
@@ -799,6 +848,9 @@ export class RunService {
799
848
  return { ok: true, value: resolved[0] };
800
849
  }
801
850
  }
851
+ function checkpointHasChangeId(state, change) {
852
+ return typeof state === 'object' && state !== null && !Array.isArray(state) && state.changeId === change;
853
+ }
802
854
  function preflightOutcome(decision) {
803
855
  if (decision.decision === 'allow')
804
856
  return 'allowed';
@@ -883,10 +935,15 @@ function resumeGateManualNextCommands(approval, details, operation) {
883
935
  commands.push(`Review operation manually: ${operation.category} ${operation.operation}`);
884
936
  return commands;
885
937
  }
886
- function canonicalRunPhase(phase) {
938
+ function canonicalRunPhase(workflow, phase) {
887
939
  if (phase === undefined || phase.trim().length === 0)
888
940
  return undefined;
889
- return phase === 'apply' ? 'apply-progress' : phase;
941
+ if (!isSddWorkflow(workflow))
942
+ return phase;
943
+ return normalizeSddPhaseInput(phase) ?? phase;
944
+ }
945
+ function isSddWorkflow(workflow) {
946
+ return workflow?.trim().toLowerCase() === 'sdd';
890
947
  }
891
948
  function operationMetadata(input) {
892
949
  const metadata = { category: input.category, name: input.operation };
@@ -1 +1,5 @@
1
+ export const TERMINAL_RUN_STATUSES = ['completed', 'failed', 'blocked', 'cancelled'];
2
+ export function isTerminalRunStatus(status) {
3
+ return TERMINAL_RUN_STATUSES.includes(status);
4
+ }
1
5
  export const runSnapshotPackageKind = 'vgxness.run-snapshot-package';
@@ -30,10 +30,22 @@ export const SddArtifactAcceptanceRecordSchema = z
30
30
  note: z.string().optional(),
31
31
  })
32
32
  .strict();
33
+ export const SddArtifactReopenRecordSchema = z
34
+ .object({
35
+ actor: z.object({
36
+ type: z.literal('human'),
37
+ id: z.string().min(1),
38
+ displayName: z.string().min(1).optional(),
39
+ }),
40
+ reopenedAt: z.string().min(1),
41
+ note: z.string().optional(),
42
+ })
43
+ .strict();
33
44
  export const SddArtifactMetadataSchema = z
34
45
  .object({
35
46
  status: SddArtifactStatusSchema,
36
47
  acceptance: SddArtifactAcceptanceRecordSchema.optional(),
48
+ reopen: SddArtifactReopenRecordSchema.optional(),
37
49
  supersededByTopicKey: z.string().min(1).optional(),
38
50
  updatedAt: z.string().min(1).optional(),
39
51
  })
@@ -86,6 +98,14 @@ export const sddPrerequisites = {
86
98
  export function isSddPhase(value) {
87
99
  return sddPhases.includes(value);
88
100
  }
101
+ export function normalizeSddPhaseInput(value) {
102
+ const normalized = value.trim();
103
+ if (normalized === 'apply')
104
+ return 'apply-progress';
105
+ if (isSddPhase(normalized))
106
+ return normalized;
107
+ return undefined;
108
+ }
89
109
  export function sddTopicKey(change, phase) {
90
110
  return `sdd/${change}/${phase}`;
91
111
  }
@@ -0,0 +1,81 @@
1
+ export function sddContinuationPlanFrom(input) {
2
+ const nextPhase = input.next.nextPhase;
3
+ const dbFlag = continuationDbFlag(input.explicitDatabasePath);
4
+ const inspectCommand = `vgxness sdd cockpit --project ${input.project} --change ${input.next.change} --json${dbFlag}`;
5
+ const suggestedCommand = suggestedContinuationCommand(input.project, input.next, inspectCommand, dbFlag);
6
+ const blockerGuidance = input.next.blockerGuidance ?? [];
7
+ const blockerActions = blockerGuidance.map((blocker) => continuationBlockerAction(input.project, input.next.change, input.next.nextPhase, blocker, dbFlag));
8
+ const hasDraftRunAction = blockerActions.some((action) => action.draftRunCommand !== undefined);
9
+ const relatedRunContext = relatedRunContextView(input.project, input.relatedRunContext, dbFlag);
10
+ return {
11
+ kind: 'sdd-continuation-plan',
12
+ project: input.project,
13
+ change: input.next.change,
14
+ status: input.next.status,
15
+ ...(nextPhase === undefined ? {} : { nextPhase }),
16
+ ...(input.cockpit.actionablePhase === undefined ? {} : { actionablePhase: input.cockpit.actionablePhase }),
17
+ recommendedAction: input.cockpit.recommendedAction,
18
+ reason: input.next.reason,
19
+ suggestedCommand,
20
+ inspectCommand,
21
+ blockerActions,
22
+ ...(relatedRunContext === undefined ? {} : { relatedRunContext }),
23
+ ...(input.explicitDatabasePath === undefined ? {} : { explicitDatabasePath: input.explicitDatabasePath }),
24
+ safety: [
25
+ 'Read-only planner: this command does not execute providers or mutate SDD artifacts, runs, provider config, or openspec/ files.',
26
+ 'Human acceptance remains explicit; artifact presence is not treated as acceptance.',
27
+ ...(hasDraftRunAction ? [draftRunWarning()] : []),
28
+ 'No openspec/ files are created or written.',
29
+ ],
30
+ };
31
+ }
32
+ function relatedRunContextView(project, relatedRunContext, dbFlag) {
33
+ if (relatedRunContext === undefined)
34
+ return undefined;
35
+ return {
36
+ ...relatedRunContext,
37
+ recommendation: 'Related interrupted run found by checkpoint changeId; inspect or resume it before starting duplicate SDD work. Advisory only.',
38
+ resumeCommand: `vgxness resume --project ${project} --run-id ${relatedRunContext.runId}${dbFlag}`,
39
+ };
40
+ }
41
+ function suggestedContinuationCommand(project, next, fallbackInspectCommand, dbFlag) {
42
+ if (next.status === 'runnable' && next.nextPhase !== undefined)
43
+ return runPhaseCommand(project, next.change, next.nextPhase, dbFlag);
44
+ return fallbackInspectCommand;
45
+ }
46
+ function continuationBlockerAction(project, change, blockedPhase, blocker, dbFlag) {
47
+ const draftRunCommand = blocker.reason === 'draft' && isDraftRunPlanningPhase(blockedPhase) ? draftRunPhaseCommand(project, change, blockedPhase, dbFlag) : undefined;
48
+ return {
49
+ phase: blocker.phase,
50
+ topicKey: blocker.topicKey,
51
+ reason: blocker.reason,
52
+ action: blocker.action,
53
+ command: blockerCommand(project, change, blocker, dbFlag),
54
+ ...(draftRunCommand === undefined ? {} : { draftRunCommand, warning: draftRunWarning() }),
55
+ ...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
56
+ };
57
+ }
58
+ function blockerCommand(project, change, blocker, dbFlag) {
59
+ if (blocker.reason === 'missing')
60
+ return runPhaseCommand(project, change, blocker.phase, dbFlag);
61
+ if (blocker.reason === 'draft' || blocker.reason === 'legacy')
62
+ return `vgxness sdd accept-artifact --project ${project} --change ${change} --phase ${blocker.phase} --actor <human-id>${dbFlag}`;
63
+ if (blocker.reason === 'rejected')
64
+ return `vgxness sdd reopen-artifact --project ${project} --change ${change} --phase ${blocker.phase} --actor <human-id>${dbFlag}`;
65
+ return `vgxness sdd get-artifact --project ${project} --change ${change} --phase ${blocker.phase} --json${dbFlag}`;
66
+ }
67
+ function runPhaseCommand(project, change, phase, dbFlag) {
68
+ return `vgxness code sdd ${change} ${phase} --project ${project} --save-artifact${dbFlag}`;
69
+ }
70
+ function draftRunPhaseCommand(project, change, phase, dbFlag) {
71
+ return `vgxness code sdd ${change} ${phase} --project ${project} --draft-run --save-artifact${dbFlag}`;
72
+ }
73
+ function isDraftRunPlanningPhase(phase) {
74
+ return phase === 'explore' || phase === 'proposal' || phase === 'spec' || phase === 'design' || phase === 'tasks';
75
+ }
76
+ function draftRunWarning() {
77
+ return 'Draft-run is planning-only; human acceptance is still required; apply-progress remains gated.';
78
+ }
79
+ function continuationDbFlag(explicitDatabasePath) {
80
+ return explicitDatabasePath === undefined ? '' : ` --db ${explicitDatabasePath}`;
81
+ }