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.
- package/README.md +15 -5
- package/dist/agents/agent-activation-service.js +13 -4
- package/dist/agents/agent-registry-service.js +8 -2
- package/dist/agents/agent-resolver.js +33 -3
- package/dist/agents/agent-seed-upgrade-service.js +231 -0
- package/dist/agents/boot-upgrade.js +59 -0
- package/dist/agents/canonical-agent-manifest.js +39 -18
- package/dist/agents/canonical-agent-projection.js +38 -4
- package/dist/agents/manager-profile-overlay-service.js +14 -0
- package/dist/agents/repositories/agent-seed-history.js +128 -0
- package/dist/cli/cli-help.js +14 -3
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +8 -0
- package/dist/cli/commands/mcp-dispatcher.js +7 -0
- package/dist/cli/commands/memory-sdd-dispatcher.js +71 -5
- package/dist/cli/commands/status-dispatcher.js +130 -0
- package/dist/cli/commands/workflow-dispatcher.js +11 -5
- package/dist/cli/dispatcher.js +11 -1
- package/dist/cli/product-resume-renderer.js +32 -0
- package/dist/cli/product-status-renderer.js +74 -0
- package/dist/cli/sdd-renderer.js +80 -3
- package/dist/code/cli/code-command.js +7 -4
- package/dist/code/reporting/summary.js +4 -1
- package/dist/code/runtime/code-runtime.js +27 -4
- package/dist/code/runtime/sdd-context.js +18 -2
- package/dist/governance/governance-report-builder.js +18 -7
- package/dist/mcp/claude-code-agent-config.js +10 -4
- package/dist/mcp/client-install-claude-code-contract.js +19 -4
- package/dist/mcp/client-install-claude-code.js +2 -2
- package/dist/mcp/control-plane.js +78 -5
- package/dist/mcp/provider-status.js +89 -88
- package/dist/mcp/schema.js +42 -8
- package/dist/mcp/stdio-server.js +6 -0
- package/dist/mcp/validation.js +77 -5
- package/dist/memory/sqlite/migrations/016_agent_seed_history.sql +15 -0
- package/dist/resume/product-resume.js +166 -0
- package/dist/runs/repositories/runs.js +12 -1
- package/dist/runs/run-service.js +62 -5
- package/dist/runs/schema.js +4 -0
- package/dist/sdd/schema.js +20 -0
- package/dist/sdd/sdd-continuation-plan.js +81 -0
- package/dist/sdd/sdd-workflow-service.js +146 -18
- package/dist/skills/skill-resolver.js +21 -4
- package/dist/status/product-status.js +117 -0
- package/docs/architecture.md +9 -1
- package/docs/cli.md +38 -13
- package/docs/code-runtime.md +3 -0
- package/docs/contributing.md +1 -1
- package/docs/glossary.md +2 -2
- package/docs/mcp.md +20 -6
- package/docs/project-health-audit-v1.9.1.md +126 -0
- package/docs/providers.md +4 -4
- package/docs/safety.md +1 -1
- package/package.json +1 -1
package/dist/mcp/validation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isRiskyPermissionCategory, permissionCategories } from '../permissions/schema.js';
|
|
2
2
|
import { parseOperationRetryPolicy } from '../runs/operation-retry.js';
|
|
3
|
-
import {
|
|
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', '
|
|
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', '
|
|
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
|
-
|
|
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:
|
|
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(
|
|
90
|
+
.all(queryParameters);
|
|
80
91
|
return ok(rows.map(mapRun));
|
|
81
92
|
}
|
|
82
93
|
catch (cause) {
|
package/dist/runs/run-service.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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 };
|
package/dist/runs/schema.js
CHANGED
package/dist/sdd/schema.js
CHANGED
|
@@ -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
|
+
}
|