vgxness 1.16.0 → 1.17.1
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 +12 -3
- package/dist/agents/canonical-agent-manifest.js +7 -0
- package/dist/cli/home-tui-app.js +35 -11
- package/dist/cli/home-tui-controller.js +18 -0
- package/dist/mcp/control-plane.js +10 -1
- package/dist/runs/run-service.js +234 -4
- package/dist/runs/sandbox-process-execution.js +7 -1
- package/dist/sdd/cockpit-read-model.js +6 -5
- package/dist/skills/personal-skills.js +38 -0
- package/dist/skills/skill-index-service.js +12 -1
- package/dist/skills/skill-resolver.js +11 -1
- package/dist/skills/skill-status-service.js +10 -4
- package/dist/workflows/command-allowlist-adapter.js +100 -0
- package/dist/workflows/workflow-executor.js +7 -1
- package/docs/architecture.md +2 -2
- package/docs/cli.md +1 -1
- package/docs/glossary.md +1 -1
- package/docs/project-health-audit-v1.10.x.md +3 -3
- package/docs/project-health-audit-v1.14.x.md +2 -0
- package/docs/project-health-audit-v1.9.1.md +1 -1
- package/docs/providers.md +1 -1
- package/docs/roadmap.md +2 -2
- package/docs/sdd-flow.md +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ This package is proprietary software. The npm package ships inspectable JavaScri
|
|
|
8
8
|
|
|
9
9
|
OpenCode is the primary supported provider. Claude setup support is secondary. VGX-managed OpenCode and Claude provider configuration is user-global only; provider config writes require explicit CLI confirmation.
|
|
10
10
|
|
|
11
|
-
VGXNESS
|
|
11
|
+
VGXNESS is currently versioned from `package.json` (`1.17.1`). The latest full project health audit is the historical [v1.14.x snapshot](./docs/project-health-audit-v1.14.x.md), which documents the implemented CLI/MCP/SDD control plane, OpenCode-first workflow, release-readiness checks, and the remaining execution/recovery gaps at that point in time. [v1.10.x](./docs/project-health-audit-v1.10.x.md) and [v1.9.1](./docs/project-health-audit-v1.9.1.md) remain historical validation evidence for those releases.
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
14
14
|
|
|
@@ -165,6 +165,15 @@ vgxness setup apply --yes
|
|
|
165
165
|
|
|
166
166
|
The daily SDD happy path is OpenCode conversation with the installed VGXNESS MCP server and hidden SDD subagents. Use CLI commands for bootstrap, setup, diagnostics, recovery, fallback, and scripting: `status`, `next`, and `sdd continue` inspect state and print read-only continuation guidance; `resume` helps inspect interrupted work. After a draft is ready, `sdd accept-artifact` records explicit human acceptance, and `sdd reopen-artifact` returns rejected artifacts to draft.
|
|
167
167
|
|
|
168
|
+
Copy this prompt into OpenCode for normal daily SDD work:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Continue VGXNESS SDD for project <project> and change <change>.
|
|
172
|
+
Use the VGXNESS MCP tools to inspect status/readiness/artifacts, then delegate phase work to the exact hidden SDD subagent when needed.
|
|
173
|
+
Save draft artifacts when appropriate and report evidence, tests, blockers, and the next human decision.
|
|
174
|
+
Do not accept or reopen artifacts for me; ask me before any human-only, provider-config, git, destructive, external, install, or secret-sensitive action.
|
|
175
|
+
```
|
|
176
|
+
|
|
168
177
|
## Code runtime status
|
|
169
178
|
|
|
170
179
|
The experimental `vgxness code` runtime and principal OpenTUI code surface have been removed temporarily because they were copied OpenCode-like runtime work. Continue SDD work in OpenCode through VGXNESS MCP, and use the safe CLI commands above for diagnostics and recovery.
|
|
@@ -179,7 +188,7 @@ The experimental `vgxness code` runtime and principal OpenTUI code surface have
|
|
|
179
188
|
- `vgxness sdd accept-artifact` records explicit human-only acceptance; saving a draft never implies acceptance.
|
|
180
189
|
- `vgxness sdd reopen-artifact` is the explicit path from a rejected artifact back to draft before revision.
|
|
181
190
|
- OpenCode is the primary supported provider; other providers are preview/manual extension points.
|
|
182
|
-
- VGXNESS exposes
|
|
191
|
+
- VGXNESS exposes 42 typed MCP tools across SDD, memory, sessions, agents, skills, runs, providers, and verification. See [MCP tools](./docs/mcp.md).
|
|
183
192
|
|
|
184
193
|
## Principal entrypoint
|
|
185
194
|
|
|
@@ -256,7 +265,7 @@ Remove any user-global OpenCode/Claude config entries and local/global VGXNESS d
|
|
|
256
265
|
## More docs
|
|
257
266
|
|
|
258
267
|
- [CLI reference](./docs/cli.md)
|
|
259
|
-
- [Project health audit v1.14.x](./docs/project-health-audit-v1.14.x.md)
|
|
268
|
+
- [Project health audit v1.14.x](./docs/project-health-audit-v1.14.x.md) — historical snapshot
|
|
260
269
|
- [Project health audit v1.10.x](./docs/project-health-audit-v1.10.x.md)
|
|
261
270
|
- [Project health audit v1.9.1](./docs/project-health-audit-v1.9.1.md)
|
|
262
271
|
- [Architecture](./docs/architecture.md)
|
|
@@ -131,8 +131,15 @@ export function createCanonicalOpenCodeSddMcpToolPermissions() {
|
|
|
131
131
|
vgxness_sdd_get_artifact: 'allow',
|
|
132
132
|
vgxness_sdd_list_artifacts: 'allow',
|
|
133
133
|
vgxness_sdd_cockpit: 'allow',
|
|
134
|
+
vgxness_context_cockpit: 'allow',
|
|
134
135
|
vgxness_agent_resolve: 'allow',
|
|
135
136
|
vgxness_sdd_save_artifact: 'allow',
|
|
137
|
+
vgxness_memory_search: 'allow',
|
|
138
|
+
vgxness_memory_get: 'allow',
|
|
139
|
+
vgxness_memory_save: 'allow',
|
|
140
|
+
vgxness_memory_update: 'allow',
|
|
141
|
+
vgxness_skill_payload: 'allow',
|
|
142
|
+
vgxness_run_preflight: 'allow',
|
|
136
143
|
};
|
|
137
144
|
}
|
|
138
145
|
export function createCanonicalOpenCodeSddSubagentPrompt(name) {
|
package/dist/cli/home-tui-app.js
CHANGED
|
@@ -28,7 +28,7 @@ function FocusedRunsView({ plan, width, runs }) {
|
|
|
28
28
|
const badge = runs.state === 'ready' ? 'read-only' : runs.state === 'unavailable' ? 'unavailable' : 'loading';
|
|
29
29
|
const tone = runs.state === 'unavailable' ? 'warning' : 'info';
|
|
30
30
|
const title = runs.state === 'ready' && runs.selected !== undefined ? 'Run detail' : 'Runs';
|
|
31
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, paddingY: 1, children: [_jsx(TuiHeader, { title: "VGXNESS Runs", subtitle: "Run recovery cockpit \u00B7 read-only", project: plan.project }), _jsx(TuiSummaryBar, { items: summaryItems(plan) }), _jsx(Box, { marginTop: 1, children: _jsx(TuiCard, { title: title, badge: badge, tone: tone, lines: focusedRunsLines(plan, runs) }) }), _jsx(TuiFooter, { text: "Keys: Enter
|
|
31
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, paddingY: 1, children: [_jsx(TuiHeader, { title: "VGXNESS Runs", subtitle: "Run recovery cockpit \u00B7 read-only", project: plan.project }), _jsx(TuiSummaryBar, { items: summaryItems(plan) }), _jsx(Box, { marginTop: 1, children: _jsx(TuiCard, { title: title, badge: badge, tone: tone, lines: focusedRunsLines(plan, runs) }) }), _jsx(TuiFooter, { text: "Keys: Enter inspects first run detail \u00B7 Esc/b back to Home \u00B7 q exits \u00B7 read-only \u00B7 no writes \u00B7 no retry/approval/execution" })] }));
|
|
32
32
|
}
|
|
33
33
|
function FocusedSddView({ plan, width, sddInput, sdd }) {
|
|
34
34
|
const badge = sddInput.change.length === 0 ? 'change required' : sdd.state === 'ready' ? sdd.next.status : sdd.state === 'unavailable' ? 'unavailable' : 'change selected';
|
|
@@ -107,8 +107,9 @@ function focusedRunsLines(plan, runs) {
|
|
|
107
107
|
'',
|
|
108
108
|
runs.message,
|
|
109
109
|
'',
|
|
110
|
-
'
|
|
110
|
+
'Interrupted runs can be inspected with `vgxness runs list --project <name> --status needs-human` from a Bun-backed runtime.',
|
|
111
111
|
'Use `vgxness runs list --project <name> --status needs-human` for actionable blockers.',
|
|
112
|
+
'Safety: read-only/no writes/no retry/approval/execution.',
|
|
112
113
|
];
|
|
113
114
|
}
|
|
114
115
|
if (runs.state === 'ready') {
|
|
@@ -121,12 +122,13 @@ function focusedRunsLines(plan, runs) {
|
|
|
121
122
|
field('Recent runs', String(runs.recent.length)),
|
|
122
123
|
field('Interrupted', String(runs.interrupted.length)),
|
|
123
124
|
'',
|
|
124
|
-
...runSummarySection('Interrupted runs', runs.interrupted),
|
|
125
|
+
...runSummarySection('Interrupted runs', runs.interrupted, runs.details),
|
|
125
126
|
'',
|
|
126
|
-
...runSummarySection('Recent runs', runs.recent),
|
|
127
|
+
...runSummarySection('Recent runs', runs.recent, runs.details),
|
|
127
128
|
'',
|
|
128
129
|
runs.details.length === 0 ? 'No run is available for detail inspection.' : 'Press Enter to inspect the first interrupted/recent run here.',
|
|
129
130
|
'CLI fallback: `vgxness runs get --id <run-id>`.',
|
|
131
|
+
'Safety: read-only/no writes/no retry/approval/execution.',
|
|
130
132
|
];
|
|
131
133
|
}
|
|
132
134
|
return [
|
|
@@ -135,12 +137,15 @@ function focusedRunsLines(plan, runs) {
|
|
|
135
137
|
field('Safety', 'read-only placeholder'),
|
|
136
138
|
'',
|
|
137
139
|
'This panel does not open the local SQLite store yet.',
|
|
138
|
-
'Use `vgxness
|
|
140
|
+
'Use `vgxness runs list --project <name> --status needs-human` to inspect interrupted runs.',
|
|
139
141
|
'Use `vgxness runs list --project <name> --status needs-human` for actionable blockers.',
|
|
140
142
|
'',
|
|
141
|
-
'
|
|
143
|
+
'This panel is read-only/no writes/no retry/approval/execution.',
|
|
142
144
|
];
|
|
143
145
|
}
|
|
146
|
+
export function renderHomeRunsReadOnlyLines(plan, runs) {
|
|
147
|
+
return focusedRunsLines(plan, runs);
|
|
148
|
+
}
|
|
144
149
|
function focusedRunDetailLines(plan, databasePath, run) {
|
|
145
150
|
return [
|
|
146
151
|
field('Project', plan.project),
|
|
@@ -151,8 +156,8 @@ function focusedRunDetailLines(plan, databasePath, run) {
|
|
|
151
156
|
field('Workflow/phase', `${run.workflow}/${run.phase}`),
|
|
152
157
|
field('Events', String(run.events)),
|
|
153
158
|
field('Checkpoints', String(run.checkpoints)),
|
|
154
|
-
field('Approvals',
|
|
155
|
-
field('Attempts',
|
|
159
|
+
field('Approvals', `${run.approvals} (${formatApprovalStatusCounts(run.approvalStatusCounts)})`),
|
|
160
|
+
field('Attempts', `${run.attempts} (${formatAttemptStatusCounts(run.attemptStatusCounts)})`),
|
|
156
161
|
...(run.latestCheckpointLabel === undefined ? [] : [field('Latest checkpoint', run.latestCheckpointLabel)]),
|
|
157
162
|
...(run.latestEventTitle === undefined ? [] : [field('Latest event', run.latestEventTitle)]),
|
|
158
163
|
...(run.outcome === undefined ? [] : [field('Outcome', run.outcome)]),
|
|
@@ -160,13 +165,32 @@ function focusedRunDetailLines(plan, databasePath, run) {
|
|
|
160
165
|
...(run.userIntent === undefined ? [] : ['', `Intent: ${run.userIntent}`]),
|
|
161
166
|
'',
|
|
162
167
|
`Inspect JSON: vgxness runs get --id ${run.id}`,
|
|
168
|
+
'Safety: read-only/no writes/no retry/approval/execution.',
|
|
163
169
|
'No retry, approval, or execution is performed from this TUI detail.',
|
|
164
170
|
];
|
|
165
171
|
}
|
|
166
|
-
function runSummarySection(title, runs) {
|
|
172
|
+
function runSummarySection(title, runs, details = []) {
|
|
167
173
|
if (runs.length === 0)
|
|
168
174
|
return [title, '- none'];
|
|
169
|
-
return [title, ...runs.map((run) =>
|
|
175
|
+
return [title, ...runs.map((run) => runSummaryLine(run, details.find((detail) => detail.id === run.id)))];
|
|
176
|
+
}
|
|
177
|
+
function runSummaryLine(run, detail) {
|
|
178
|
+
const base = `- ${shortRunId(run.id)} ${run.status} ${run.workflow}/${run.phase}${run.userIntent === undefined ? '' : ` — ${run.userIntent}`}`;
|
|
179
|
+
if (detail === undefined)
|
|
180
|
+
return base;
|
|
181
|
+
return [
|
|
182
|
+
base,
|
|
183
|
+
`approvals ${detail.approvals} (${formatApprovalStatusCounts(detail.approvalStatusCounts)})`,
|
|
184
|
+
`attempts ${detail.attempts} (${formatAttemptStatusCounts(detail.attemptStatusCounts)})`,
|
|
185
|
+
`latest checkpoint ${detail.latestCheckpointLabel ?? 'none'}`,
|
|
186
|
+
`latest event ${detail.latestEventTitle ?? 'none'}`,
|
|
187
|
+
].join(' · ');
|
|
188
|
+
}
|
|
189
|
+
function formatApprovalStatusCounts(counts) {
|
|
190
|
+
return [`pending=${counts.pending}`, `approved=${counts.approved}`, `rejected=${counts.rejected}`, `cancelled=${counts.cancelled}`].join(' ');
|
|
191
|
+
}
|
|
192
|
+
function formatAttemptStatusCounts(counts) {
|
|
193
|
+
return [`reserved=${counts.reserved}`, `succeeded=${counts.succeeded}`, `failed=${counts.failed}`, `abandoned=${counts.abandoned}`].join(' ');
|
|
170
194
|
}
|
|
171
195
|
function shortRunId(id) {
|
|
172
196
|
return id.length <= 8 ? id : id.slice(0, 8);
|
|
@@ -287,7 +311,7 @@ function skillSummarySection(skills) {
|
|
|
287
311
|
return ['Indexed skills', ...skills.slice(0, 8).map((skill) => `- ${skill.name} ${skill.status}${skill.activeVersion === null ? '' : ` @ ${skill.activeVersion}`}`), ...(skills.length > 8 ? [`- +${skills.length - 8} more`] : [])];
|
|
288
312
|
}
|
|
289
313
|
function runsLines() {
|
|
290
|
-
return ['Run dashboard is not loaded in this
|
|
314
|
+
return ['Run dashboard is not loaded in this Home TUI overview.', '', 'Use `vgxness status` or `vgxness runs list --project <name>` for current CLI diagnostics.', 'Focused Runs is read-only/no writes/no retry/approval/execution.'];
|
|
291
315
|
}
|
|
292
316
|
function sddLines() {
|
|
293
317
|
return [
|
|
@@ -224,11 +224,29 @@ function runDetailSummary(run) {
|
|
|
224
224
|
events: run.events.length,
|
|
225
225
|
checkpoints: run.checkpoints.length,
|
|
226
226
|
approvals: run.approvals.length,
|
|
227
|
+
approvalStatusCounts: countApprovalStatuses(run.approvals.map((approval) => approval.status)),
|
|
227
228
|
attempts: run.operationAttempts.length,
|
|
229
|
+
attemptStatusCounts: countAttemptStatuses(run.operationAttempts.map((attempt) => attempt.status)),
|
|
228
230
|
...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
|
|
229
231
|
...(latestEvent === undefined ? {} : { latestEventTitle: latestEvent.title }),
|
|
230
232
|
};
|
|
231
233
|
}
|
|
234
|
+
function countApprovalStatuses(statuses) {
|
|
235
|
+
return {
|
|
236
|
+
pending: statuses.filter((status) => status === 'pending').length,
|
|
237
|
+
approved: statuses.filter((status) => status === 'approved').length,
|
|
238
|
+
rejected: statuses.filter((status) => status === 'rejected').length,
|
|
239
|
+
cancelled: statuses.filter((status) => status === 'cancelled').length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function countAttemptStatuses(statuses) {
|
|
243
|
+
return {
|
|
244
|
+
reserved: statuses.filter((status) => status === 'reserved').length,
|
|
245
|
+
succeeded: statuses.filter((status) => status === 'succeeded').length,
|
|
246
|
+
failed: statuses.filter((status) => status === 'failed').length,
|
|
247
|
+
abandoned: statuses.filter((status) => status === 'abandoned').length,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
232
250
|
function selectFirstHomeRunDetail(runs) {
|
|
233
251
|
if (runs.state !== 'ready' || runs.selected !== undefined)
|
|
234
252
|
return runs;
|
|
@@ -46,7 +46,7 @@ export function callVgxTool(call, services) {
|
|
|
46
46
|
case 'vgxness_sdd_next':
|
|
47
47
|
return toEnvelope(validated.tool, services.sdd.getNext(validated.input));
|
|
48
48
|
case 'vgxness_sdd_cockpit':
|
|
49
|
-
return toEnvelope(validated.tool,
|
|
49
|
+
return toEnvelope(validated.tool, sddCockpitSurfaceResponse(validated.input, services));
|
|
50
50
|
case 'vgxness_sdd_continue':
|
|
51
51
|
return sddContinueEnvelope(validated.input, services);
|
|
52
52
|
case 'vgxness_governance_report':
|
|
@@ -267,6 +267,15 @@ function sddContinueEnvelope(input, services) {
|
|
|
267
267
|
...(relatedRun.value === undefined ? {} : { relatedRunContext: relatedRun.value }),
|
|
268
268
|
}));
|
|
269
269
|
}
|
|
270
|
+
function sddCockpitSurfaceResponse(input, services) {
|
|
271
|
+
const cockpit = services.sdd.getCockpit(input);
|
|
272
|
+
if (!cockpit.ok)
|
|
273
|
+
return cockpit;
|
|
274
|
+
const operationalEvidence = services.runs.getSddOperationalEvidence({ project: input.project, change: input.change });
|
|
275
|
+
if (!operationalEvidence.ok)
|
|
276
|
+
return { ok: true, value: buildSddCockpitSurfaceResponse(cockpit.value) };
|
|
277
|
+
return { ok: true, value: buildSddCockpitSurfaceResponse(cockpit.value, { operationalEvidenceByPhase: operationalEvidence.value }) };
|
|
278
|
+
}
|
|
270
279
|
export function createVgxMcpControlPlane(options = {}) {
|
|
271
280
|
const databasePath = resolveControlPlaneDatabasePath(options);
|
|
272
281
|
if (!databasePath.ok)
|
package/dist/runs/run-service.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { evaluatePermission } from '../permissions/policy-evaluator.js';
|
|
2
3
|
import { isRiskyPermissionCategory } from '../permissions/schema.js';
|
|
3
4
|
import { normalizeSddPhaseInput } from '../sdd/schema.js';
|
|
4
5
|
import { planExecutionIsolation, recommendWorkspaceStrategyForExecution } from './execution-planning.js';
|
|
5
6
|
import { evaluateOperationRetry as evaluateOperationRetryPolicy } from './operation-retry.js';
|
|
7
|
+
import { AllowlistedApplyProgressCommandExecutor } from '../workflows/command-allowlist-adapter.js';
|
|
6
8
|
import { RunRepository, } from './repositories/runs.js';
|
|
7
9
|
import { buildRunInsights, buildRunOperatorResumePlan } from './run-insights.js';
|
|
8
10
|
const preflightSafety = {
|
|
@@ -45,6 +47,51 @@ export class RunService {
|
|
|
45
47
|
listRuns(filters = {}) {
|
|
46
48
|
return this.runs.list(filters);
|
|
47
49
|
}
|
|
50
|
+
getSddOperationalEvidence(input) {
|
|
51
|
+
const runs = this.runs.list({ project: input.project, limit: input.limit ?? 50 });
|
|
52
|
+
if (!runs.ok)
|
|
53
|
+
return runs;
|
|
54
|
+
const evidenceByPhase = {};
|
|
55
|
+
for (const run of runs.value) {
|
|
56
|
+
if (!isSddWorkflow(run.workflow))
|
|
57
|
+
continue;
|
|
58
|
+
const phase = normalizeSddPhaseInput(run.phase);
|
|
59
|
+
if (phase === undefined || evidenceByPhase[phase] !== undefined)
|
|
60
|
+
continue;
|
|
61
|
+
const details = this.runs.getDetails(run.id);
|
|
62
|
+
if (!details.ok)
|
|
63
|
+
return details;
|
|
64
|
+
const matchingCheckpoints = details.value.checkpoints.filter((checkpoint) => checkpointHasChangeId(checkpoint.state, input.change));
|
|
65
|
+
if (matchingCheckpoints.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const latestMatchingCheckpoint = matchingCheckpoints.at(-1);
|
|
68
|
+
evidenceByPhase[phase] = {
|
|
69
|
+
latestRun: {
|
|
70
|
+
id: details.value.id,
|
|
71
|
+
status: details.value.status,
|
|
72
|
+
workflow: details.value.workflow,
|
|
73
|
+
phase: details.value.phase,
|
|
74
|
+
...(details.value.outcome === undefined ? {} : { outcome: details.value.outcome }),
|
|
75
|
+
createdAt: details.value.createdAt,
|
|
76
|
+
updatedAt: details.value.updatedAt,
|
|
77
|
+
...(details.value.completedAt === undefined ? {} : { completedAt: details.value.completedAt }),
|
|
78
|
+
},
|
|
79
|
+
approvalCounts: countByStatus(details.value.approvals.map((approval) => approval.status)),
|
|
80
|
+
attemptCounts: countByStatus(details.value.operationAttempts.map((attempt) => attempt.status)),
|
|
81
|
+
...(latestMatchingCheckpoint === undefined
|
|
82
|
+
? {}
|
|
83
|
+
: {
|
|
84
|
+
latestCheckpoint: {
|
|
85
|
+
id: latestMatchingCheckpoint.id,
|
|
86
|
+
label: latestMatchingCheckpoint.label,
|
|
87
|
+
sequence: latestMatchingCheckpoint.sequence,
|
|
88
|
+
createdAt: latestMatchingCheckpoint.createdAt,
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, value: evidenceByPhase };
|
|
94
|
+
}
|
|
48
95
|
listRecentInterruptedRuns(input) {
|
|
49
96
|
const runs = this.runs.list({ project: input.project, statuses: ['failed', 'blocked', 'needs-human'], limit: input.limit ?? 5 });
|
|
50
97
|
if (!runs.ok)
|
|
@@ -234,6 +281,7 @@ export class RunService {
|
|
|
234
281
|
const operation = operationFromPendingExecution(pendingExecutionEvent.payload);
|
|
235
282
|
if (operation === undefined)
|
|
236
283
|
return validationFailure('Pending operation metadata is incomplete and cannot be retried');
|
|
284
|
+
const replayGuard = replayGuardForApprovedOperation(approval.value, permissionEvent, pendingExecutionEvent, operation);
|
|
237
285
|
const attempts = details.value.operationAttempts.filter((attempt) => attempt.approvalId === approval.value.id);
|
|
238
286
|
const retryDecision = evaluateOperationRetryPolicy(input.policy === undefined ? { attempts } : { attempts, policy: input.policy });
|
|
239
287
|
if (!retryDecision.allowed || retryDecision.reasonCode !== 'status_allowed_by_policy')
|
|
@@ -245,7 +293,7 @@ export class RunService {
|
|
|
245
293
|
pendingExecutionEventId: pendingExecutionEvent.id,
|
|
246
294
|
category: operation.category,
|
|
247
295
|
operation: operation.operation,
|
|
248
|
-
operationMetadata:
|
|
296
|
+
operationMetadata: operationMetadataWithReplayGuard(operation, replayGuard),
|
|
249
297
|
executorName: input.executorName ?? 'retry-admission',
|
|
250
298
|
});
|
|
251
299
|
if (!reserved.ok)
|
|
@@ -267,6 +315,8 @@ export class RunService {
|
|
|
267
315
|
retryReason: retryDecision.reason,
|
|
268
316
|
evaluatedAttemptCount: retryDecision.evaluatedAttemptCount,
|
|
269
317
|
retryableStatuses: [...retryDecision.retryableStatuses],
|
|
318
|
+
replayGuard,
|
|
319
|
+
operationFingerprint: replayGuard.operationFingerprint,
|
|
270
320
|
executorInvoked: false,
|
|
271
321
|
operationExecuted: false,
|
|
272
322
|
}),
|
|
@@ -385,6 +435,10 @@ export class RunService {
|
|
|
385
435
|
const operation = operationFromPendingExecution(pendingExecutionEvent.payload);
|
|
386
436
|
if (operation === undefined)
|
|
387
437
|
return validationFailure('Pending operation metadata is incomplete and cannot be resumed');
|
|
438
|
+
const replayGuard = replayGuardForApprovedOperation(approval.value, permissionEvent, pendingExecutionEvent, operation);
|
|
439
|
+
const replayValidation = validateReplayGuard(pendingExecutionEvent, replayGuard, input.expectedOperationFingerprint);
|
|
440
|
+
if (!replayValidation.ok)
|
|
441
|
+
return replayValidation;
|
|
388
442
|
const retry = this.evaluateOperationRetry({ approvalId: approval.value.id });
|
|
389
443
|
if (!retry.ok)
|
|
390
444
|
return retry;
|
|
@@ -397,7 +451,7 @@ export class RunService {
|
|
|
397
451
|
pendingExecutionEventId: pendingExecutionEvent.id,
|
|
398
452
|
category: operation.category,
|
|
399
453
|
operation: operation.operation,
|
|
400
|
-
operationMetadata:
|
|
454
|
+
operationMetadata: operationMetadataWithReplayGuard(operation, replayGuard),
|
|
401
455
|
executorName: input.executor.name,
|
|
402
456
|
});
|
|
403
457
|
if (!reserved.ok)
|
|
@@ -420,6 +474,8 @@ export class RunService {
|
|
|
420
474
|
originalDecision: 'ask',
|
|
421
475
|
approvalStatus: 'approved',
|
|
422
476
|
policyReevaluated: false,
|
|
477
|
+
replayGuard,
|
|
478
|
+
operationFingerprint: replayGuard.operationFingerprint,
|
|
423
479
|
resumedFromApprovalId: approval.value.id,
|
|
424
480
|
executorInvoked: true,
|
|
425
481
|
error,
|
|
@@ -430,6 +486,9 @@ export class RunService {
|
|
|
430
486
|
if (!executionEvent.ok)
|
|
431
487
|
return { ok: false, error: executionEvent.error };
|
|
432
488
|
const linkedAttempt = this.runs.attachAttemptResultEvent(attempt.value.id, executionEvent.value.id);
|
|
489
|
+
const evidence = appendApplyProgressAllowlistEvidenceIfNeeded(this.runs, approval.value.id, permissionEvent, pendingExecutionEvent, linkedAttempt.ok ? linkedAttempt.value : attempt.value, executionEvent.value, replayGuard.operationFingerprint, input.executor.name, operation, undefined, error);
|
|
490
|
+
if (!evidence.ok)
|
|
491
|
+
return { ok: false, error: evidence.error };
|
|
433
492
|
return {
|
|
434
493
|
ok: true,
|
|
435
494
|
value: {
|
|
@@ -439,6 +498,7 @@ export class RunService {
|
|
|
439
498
|
attempt: linkedAttempt.ok ? linkedAttempt.value : attempt.value,
|
|
440
499
|
executionEvent: executionEvent.value,
|
|
441
500
|
status: 'failed',
|
|
501
|
+
...(evidence.value === undefined ? {} : evidence.value),
|
|
442
502
|
},
|
|
443
503
|
};
|
|
444
504
|
}
|
|
@@ -458,6 +518,8 @@ export class RunService {
|
|
|
458
518
|
originalDecision: 'ask',
|
|
459
519
|
approvalStatus: 'approved',
|
|
460
520
|
policyReevaluated: false,
|
|
521
|
+
replayGuard,
|
|
522
|
+
operationFingerprint: replayGuard.operationFingerprint,
|
|
461
523
|
resumedFromApprovalId: approval.value.id,
|
|
462
524
|
executorInvoked: true,
|
|
463
525
|
output: output ?? null,
|
|
@@ -468,6 +530,9 @@ export class RunService {
|
|
|
468
530
|
if (!executionEvent.ok)
|
|
469
531
|
return { ok: false, error: executionEvent.error };
|
|
470
532
|
const linkedAttempt = this.runs.attachAttemptResultEvent(attempt.value.id, executionEvent.value.id);
|
|
533
|
+
const evidence = appendApplyProgressAllowlistEvidenceIfNeeded(this.runs, approval.value.id, permissionEvent, pendingExecutionEvent, linkedAttempt.ok ? linkedAttempt.value : attempt.value, executionEvent.value, replayGuard.operationFingerprint, input.executor.name, operation, output, undefined);
|
|
534
|
+
if (!evidence.ok)
|
|
535
|
+
return { ok: false, error: evidence.error };
|
|
471
536
|
return {
|
|
472
537
|
ok: true,
|
|
473
538
|
value: {
|
|
@@ -478,9 +543,17 @@ export class RunService {
|
|
|
478
543
|
executionEvent: executionEvent.value,
|
|
479
544
|
status: 'succeeded',
|
|
480
545
|
...(output === undefined ? {} : { output }),
|
|
546
|
+
...(evidence.value === undefined ? {} : evidence.value),
|
|
481
547
|
},
|
|
482
548
|
};
|
|
483
549
|
}
|
|
550
|
+
resumeApprovedAllowlistedApplyProgressCommand(input) {
|
|
551
|
+
return this.resumeApprovedOperation({
|
|
552
|
+
approvalId: input.approvalId,
|
|
553
|
+
executor: new AllowlistedApplyProgressCommandExecutor(input.executorOptions),
|
|
554
|
+
...(input.expectedOperationFingerprint === undefined ? {} : { expectedOperationFingerprint: input.expectedOperationFingerprint }),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
484
557
|
executeOperation(input) {
|
|
485
558
|
const permission = this.evaluatePermissionForRun({ runId: input.runId, ...input.operation, reusePendingApproval: true });
|
|
486
559
|
if (!permission.ok)
|
|
@@ -541,6 +614,7 @@ export class RunService {
|
|
|
541
614
|
: { ok: false, error: executionEvent.error };
|
|
542
615
|
}
|
|
543
616
|
if (permission.value.decision.decision === 'ask') {
|
|
617
|
+
const replayGuard = permission.value.approval === undefined ? undefined : replayGuardForApprovedOperation(permission.value.approval, permission.value.event, undefined, input.operation);
|
|
544
618
|
const executionEvent = this.runs.appendEvent({
|
|
545
619
|
runId: input.runId,
|
|
546
620
|
kind: 'operation-execution',
|
|
@@ -551,6 +625,7 @@ export class RunService {
|
|
|
551
625
|
decision: 'ask',
|
|
552
626
|
reason: permission.value.decision.reason,
|
|
553
627
|
message: permission.value.decision.message,
|
|
628
|
+
...(replayGuard === undefined ? {} : { replayGuard, operationFingerprint: replayGuard.operationFingerprint }),
|
|
554
629
|
executorInvoked: false,
|
|
555
630
|
operationExecuted: false,
|
|
556
631
|
}),
|
|
@@ -855,8 +930,49 @@ export class RunService {
|
|
|
855
930
|
return { ok: true, value: resolved[0] };
|
|
856
931
|
}
|
|
857
932
|
}
|
|
933
|
+
export function calculateStableOperationFingerprint(input) {
|
|
934
|
+
const canonical = {
|
|
935
|
+
runId: input.runId,
|
|
936
|
+
approvalId: input.approvalId,
|
|
937
|
+
category: input.category,
|
|
938
|
+
operation: input.operation,
|
|
939
|
+
};
|
|
940
|
+
if (input.phase !== undefined)
|
|
941
|
+
canonical.phase = input.phase;
|
|
942
|
+
if (input.agentId !== undefined)
|
|
943
|
+
canonical.agentId = input.agentId;
|
|
944
|
+
if (input.workspaceRoot !== undefined)
|
|
945
|
+
canonical.workspaceRoot = input.workspaceRoot;
|
|
946
|
+
if (input.targetPath !== undefined)
|
|
947
|
+
canonical.targetPath = input.targetPath;
|
|
948
|
+
if (input.input !== undefined)
|
|
949
|
+
canonical.input = input.input;
|
|
950
|
+
return `sha256:${createHash('sha256').update(stableJson(canonical)).digest('hex')}`;
|
|
951
|
+
}
|
|
858
952
|
function checkpointHasChangeId(state, change) {
|
|
859
|
-
return typeof state === 'object' && state !== null && !Array.isArray(state) && state.changeId === change;
|
|
953
|
+
return typeof state === 'object' && state !== null && !Array.isArray(state) && (state.changeId === change || state.change === change);
|
|
954
|
+
}
|
|
955
|
+
function countByStatus(statuses) {
|
|
956
|
+
const counts = { total: statuses.length };
|
|
957
|
+
for (const status of statuses) {
|
|
958
|
+
if (status === 'pending')
|
|
959
|
+
counts.pending = (counts.pending ?? 0) + 1;
|
|
960
|
+
else if (status === 'approved')
|
|
961
|
+
counts.approved = (counts.approved ?? 0) + 1;
|
|
962
|
+
else if (status === 'rejected')
|
|
963
|
+
counts.rejected = (counts.rejected ?? 0) + 1;
|
|
964
|
+
else if (status === 'cancelled')
|
|
965
|
+
counts.cancelled = (counts.cancelled ?? 0) + 1;
|
|
966
|
+
else if (status === 'reserved')
|
|
967
|
+
counts.reserved = (counts.reserved ?? 0) + 1;
|
|
968
|
+
else if (status === 'succeeded')
|
|
969
|
+
counts.succeeded = (counts.succeeded ?? 0) + 1;
|
|
970
|
+
else if (status === 'failed')
|
|
971
|
+
counts.failed = (counts.failed ?? 0) + 1;
|
|
972
|
+
else if (status === 'abandoned')
|
|
973
|
+
counts.abandoned = (counts.abandoned ?? 0) + 1;
|
|
974
|
+
}
|
|
975
|
+
return counts;
|
|
860
976
|
}
|
|
861
977
|
function preflightOutcome(decision) {
|
|
862
978
|
if (decision.decision === 'allow')
|
|
@@ -886,6 +1002,54 @@ function workflowConflictDecision(input, runWorkflow) {
|
|
|
886
1002
|
auditEvidence: [`runWorkflow:${runWorkflow}`, `explicitWorkflow:${input.workflow}`],
|
|
887
1003
|
};
|
|
888
1004
|
}
|
|
1005
|
+
function appendApplyProgressAllowlistEvidenceIfNeeded(runs, approvalId, permissionEvent, pendingExecutionEvent, attempt, executionEvent, operationFingerprint, executorName, operation, output, error) {
|
|
1006
|
+
if (!isApplyProgressAllowlistedCommandOperation(operation))
|
|
1007
|
+
return { ok: true, value: undefined };
|
|
1008
|
+
const base = {
|
|
1009
|
+
kind: 'apply-progress-operation',
|
|
1010
|
+
operationKind: 'allowlisted-command',
|
|
1011
|
+
runId: attempt.runId,
|
|
1012
|
+
approvalId,
|
|
1013
|
+
attemptId: attempt.id,
|
|
1014
|
+
pendingExecutionEventId: pendingExecutionEvent.id,
|
|
1015
|
+
permissionEventId: permissionEvent.id,
|
|
1016
|
+
executionEventId: executionEvent.id,
|
|
1017
|
+
operationFingerprint,
|
|
1018
|
+
executor: executorName,
|
|
1019
|
+
operation: operationMetadata(operation),
|
|
1020
|
+
status: attempt.status,
|
|
1021
|
+
output: output ?? null,
|
|
1022
|
+
error: error ?? null,
|
|
1023
|
+
};
|
|
1024
|
+
const evidence = runs.appendEvent({
|
|
1025
|
+
runId: attempt.runId,
|
|
1026
|
+
kind: 'evidence',
|
|
1027
|
+
title: `Apply-progress allowlisted command ${attempt.status}: ${operation.operation}`,
|
|
1028
|
+
payload: base,
|
|
1029
|
+
relatedType: 'operation-attempt',
|
|
1030
|
+
relatedId: attempt.id,
|
|
1031
|
+
});
|
|
1032
|
+
if (!evidence.ok)
|
|
1033
|
+
return { ok: false, error: evidence.error };
|
|
1034
|
+
const checkpoint = runs.appendCheckpoint({
|
|
1035
|
+
runId: attempt.runId,
|
|
1036
|
+
label: 'apply-progress-allowlisted-command-executed',
|
|
1037
|
+
state: { ...base, evidenceEventId: evidence.value.id },
|
|
1038
|
+
});
|
|
1039
|
+
if (!checkpoint.ok)
|
|
1040
|
+
return { ok: false, error: checkpoint.error };
|
|
1041
|
+
return { ok: true, value: { checkpoint: checkpoint.value, evidenceEvent: evidence.value } };
|
|
1042
|
+
}
|
|
1043
|
+
function isApplyProgressAllowlistedCommandOperation(operation) {
|
|
1044
|
+
if (operation.input === undefined)
|
|
1045
|
+
return false;
|
|
1046
|
+
if (!isObject(operation.input))
|
|
1047
|
+
return false;
|
|
1048
|
+
return (operation.phase === 'apply-progress' &&
|
|
1049
|
+
operation.input.kind === 'apply-progress-operation' &&
|
|
1050
|
+
operation.input.operationKind === 'allowlisted-command' &&
|
|
1051
|
+
/^command-allowlist:[A-Za-z0-9._/-]+$/u.test(operation.operation));
|
|
1052
|
+
}
|
|
889
1053
|
function executionPayload(operation, executorName, status, extra) {
|
|
890
1054
|
return {
|
|
891
1055
|
status,
|
|
@@ -896,6 +1060,65 @@ function executionPayload(operation, executorName, status, extra) {
|
|
|
896
1060
|
...extra,
|
|
897
1061
|
};
|
|
898
1062
|
}
|
|
1063
|
+
function operationMetadataWithReplayGuard(operation, replayGuard) {
|
|
1064
|
+
const metadata = operationMetadata(operation);
|
|
1065
|
+
if (!isObject(metadata))
|
|
1066
|
+
return metadata;
|
|
1067
|
+
return { ...metadata, replayGuard };
|
|
1068
|
+
}
|
|
1069
|
+
function replayGuardForApprovedOperation(approval, permissionEvent, pendingExecutionEvent, operation) {
|
|
1070
|
+
const fingerprintInput = fingerprintInputForApprovedOperation(approval, permissionEvent, operation);
|
|
1071
|
+
const operationFingerprint = calculateStableOperationFingerprint(fingerprintInput);
|
|
1072
|
+
return {
|
|
1073
|
+
version: 1,
|
|
1074
|
+
operationFingerprint,
|
|
1075
|
+
fingerprintAlgorithm: 'sha256:stable-json:v1',
|
|
1076
|
+
expectedPendingExecutionEventId: pendingExecutionEvent?.id ?? null,
|
|
1077
|
+
associated: fingerprintInput,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
function fingerprintInputForApprovedOperation(approval, permissionEvent, operation) {
|
|
1081
|
+
const permissionPayload = isObject(permissionEvent.payload) ? permissionEvent.payload : {};
|
|
1082
|
+
const requestedOperation = permissionPayload.requestedOperation !== undefined && isObject(permissionPayload.requestedOperation) ? permissionPayload.requestedOperation : {};
|
|
1083
|
+
const agent = permissionPayload.agent !== undefined && isObject(permissionPayload.agent) ? permissionPayload.agent : undefined;
|
|
1084
|
+
const phase = stringField(operation.phase) ?? stringField(requestedOperation.phase) ?? stringField(permissionPayload.phase);
|
|
1085
|
+
const agentId = stringField(operation.agentId) ?? stringField(requestedOperation.agentId) ?? stringField(agent?.id);
|
|
1086
|
+
const workspaceRoot = stringField(operation.workspaceRoot) ?? stringField(requestedOperation.workspaceRoot);
|
|
1087
|
+
const targetPath = stringField(operation.targetPath) ?? stringField(requestedOperation.targetPath);
|
|
1088
|
+
const fingerprintInput = {
|
|
1089
|
+
runId: approval.runId,
|
|
1090
|
+
approvalId: approval.id,
|
|
1091
|
+
category: operation.category,
|
|
1092
|
+
operation: operation.operation,
|
|
1093
|
+
};
|
|
1094
|
+
if (phase !== undefined)
|
|
1095
|
+
fingerprintInput.phase = phase;
|
|
1096
|
+
if (agentId !== undefined)
|
|
1097
|
+
fingerprintInput.agentId = agentId;
|
|
1098
|
+
if (workspaceRoot !== undefined)
|
|
1099
|
+
fingerprintInput.workspaceRoot = workspaceRoot;
|
|
1100
|
+
if (targetPath !== undefined)
|
|
1101
|
+
fingerprintInput.targetPath = targetPath;
|
|
1102
|
+
if (operation.input !== undefined)
|
|
1103
|
+
fingerprintInput.input = operation.input;
|
|
1104
|
+
return fingerprintInput;
|
|
1105
|
+
}
|
|
1106
|
+
function validateReplayGuard(pendingExecutionEvent, replayGuard, expectedOperationFingerprint) {
|
|
1107
|
+
const actualFingerprint = replayGuard.operationFingerprint;
|
|
1108
|
+
if (expectedOperationFingerprint !== undefined && expectedOperationFingerprint !== actualFingerprint) {
|
|
1109
|
+
return validationFailure(`Operation fingerprint mismatch: expected ${expectedOperationFingerprint}, calculated ${actualFingerprint}`);
|
|
1110
|
+
}
|
|
1111
|
+
if (isObject(pendingExecutionEvent.payload)) {
|
|
1112
|
+
const stored = typeof pendingExecutionEvent.payload.operationFingerprint === 'string' ? pendingExecutionEvent.payload.operationFingerprint : undefined;
|
|
1113
|
+
const storedReplayGuard = pendingExecutionEvent.payload.replayGuard !== undefined && isObject(pendingExecutionEvent.payload.replayGuard) ? pendingExecutionEvent.payload.replayGuard : undefined;
|
|
1114
|
+
const storedReplayFingerprint = typeof storedReplayGuard?.operationFingerprint === 'string' ? storedReplayGuard.operationFingerprint : undefined;
|
|
1115
|
+
const storedFingerprint = stored ?? storedReplayFingerprint;
|
|
1116
|
+
if (storedFingerprint !== undefined && storedFingerprint !== actualFingerprint) {
|
|
1117
|
+
return validationFailure(`Stored operation fingerprint mismatch: stored ${storedFingerprint}, calculated ${actualFingerprint}`);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return { ok: true, value: undefined };
|
|
1121
|
+
}
|
|
899
1122
|
function summarizeOperation(operation) {
|
|
900
1123
|
return {
|
|
901
1124
|
category: operation.category,
|
|
@@ -1085,6 +1308,10 @@ function operationFromPendingExecution(payload) {
|
|
|
1085
1308
|
operation.targetPath = requested.targetPath;
|
|
1086
1309
|
if (typeof requested.providerToolName === 'string')
|
|
1087
1310
|
operation.providerToolName = requested.providerToolName;
|
|
1311
|
+
if (typeof requested.phase === 'string')
|
|
1312
|
+
operation.phase = requested.phase;
|
|
1313
|
+
if (typeof requested.agentId === 'string')
|
|
1314
|
+
operation.agentId = requested.agentId;
|
|
1088
1315
|
if (typeof requested.destructive === 'boolean')
|
|
1089
1316
|
operation.destructive = requested.destructive;
|
|
1090
1317
|
if (typeof requested.external === 'boolean')
|
|
@@ -1095,10 +1322,13 @@ function operationFromPendingExecution(payload) {
|
|
|
1095
1322
|
operation.ambiguous = requested.ambiguous;
|
|
1096
1323
|
if ('input' in requested)
|
|
1097
1324
|
operation.input = requested.input;
|
|
1098
|
-
if ('input' in payload)
|
|
1325
|
+
if ('input' in payload && payload.input !== null)
|
|
1099
1326
|
operation.input = payload.input;
|
|
1100
1327
|
return operation;
|
|
1101
1328
|
}
|
|
1329
|
+
function stringField(value) {
|
|
1330
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
1331
|
+
}
|
|
1102
1332
|
function isObject(value) {
|
|
1103
1333
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1104
1334
|
}
|
|
@@ -160,10 +160,12 @@ function acceptedEvidence(workspaceRoot, cwd, checks) {
|
|
|
160
160
|
return {
|
|
161
161
|
strategy: 'bounded-process',
|
|
162
162
|
enforceable: true,
|
|
163
|
+
createsSandbox: false,
|
|
164
|
+
createsWorktree: false,
|
|
163
165
|
workspaceRoot,
|
|
164
166
|
cwd,
|
|
165
167
|
capabilities: {
|
|
166
|
-
sandboxEnforceable:
|
|
168
|
+
sandboxEnforceable: false,
|
|
167
169
|
processExecutionEnforceable: true,
|
|
168
170
|
workspaceBoundaryEnforced: true,
|
|
169
171
|
providerConfigWritesBlocked: true,
|
|
@@ -172,10 +174,14 @@ function acceptedEvidence(workspaceRoot, cwd, checks) {
|
|
|
172
174
|
limitedEnvironment: true,
|
|
173
175
|
timeoutEnforced: true,
|
|
174
176
|
outputCapEnforced: true,
|
|
177
|
+
createsSandbox: false,
|
|
178
|
+
createsWorktree: false,
|
|
175
179
|
filesystemIsolation: false,
|
|
176
180
|
networkIsolation: false,
|
|
177
181
|
},
|
|
178
182
|
limitations: [
|
|
183
|
+
'bounded-process does not create an OS-level sandbox',
|
|
184
|
+
'bounded-process does not create or manage a git worktree',
|
|
179
185
|
'bounded-process enforces local process launch constraints only; it is not an OS-level filesystem sandbox',
|
|
180
186
|
'bounded-process does not prove network isolation',
|
|
181
187
|
],
|
|
@@ -17,11 +17,11 @@ export class SddCockpitReadModelService {
|
|
|
17
17
|
return { ok: true, value: buildSddCockpitSurfaceResponse(cockpit.value) };
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
export function buildSddCockpitSurfaceResponse(cockpit) {
|
|
21
|
-
return { ...cockpit, readModel: buildSddCockpitReadModel(cockpit) };
|
|
20
|
+
export function buildSddCockpitSurfaceResponse(cockpit, options = {}) {
|
|
21
|
+
return { ...cockpit, readModel: buildSddCockpitReadModel(cockpit, options) };
|
|
22
22
|
}
|
|
23
|
-
export function buildSddCockpitReadModel(cockpit) {
|
|
24
|
-
const phases = cockpit.phases.map((phase) => toPhaseReadModel(phase));
|
|
23
|
+
export function buildSddCockpitReadModel(cockpit, options = {}) {
|
|
24
|
+
const phases = cockpit.phases.map((phase) => toPhaseReadModel(phase, options.operationalEvidenceByPhase?.[phase.phase]));
|
|
25
25
|
const blockers = toReadBlockers(cockpit);
|
|
26
26
|
const nextAction = chooseNextAction(cockpit, phases);
|
|
27
27
|
return {
|
|
@@ -36,7 +36,7 @@ export function buildSddCockpitReadModel(cockpit) {
|
|
|
36
36
|
contentIncluded: false,
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
-
function toPhaseReadModel(phase) {
|
|
39
|
+
function toPhaseReadModel(phase, operationalEvidence) {
|
|
40
40
|
const status = artifactReadStatus(phase);
|
|
41
41
|
const contentAvailable = phase.present && phase.artifact !== undefined;
|
|
42
42
|
const reasons = phase.blockers.map((blocker) => blocker.action ?? blocker.reason);
|
|
@@ -70,6 +70,7 @@ function toPhaseReadModel(phase) {
|
|
|
70
70
|
reasons,
|
|
71
71
|
},
|
|
72
72
|
...(phase.gates === undefined ? {} : { gates: phase.gates }),
|
|
73
|
+
...(operationalEvidence === undefined ? {} : { operationalEvidence }),
|
|
73
74
|
guidance: guidanceForPhase(phase, status, canAccept),
|
|
74
75
|
};
|
|
75
76
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const GLOBAL_PERSONAL_SKILLS_PROJECT = "__personal__";
|
|
2
|
+
export const skillLookupContexts = (project, scope) => {
|
|
3
|
+
const contexts = [{ project, scope }];
|
|
4
|
+
if (scope !== "personal")
|
|
5
|
+
contexts.push({ project, scope: "personal" });
|
|
6
|
+
if (project !== GLOBAL_PERSONAL_SKILLS_PROJECT)
|
|
7
|
+
contexts.push({
|
|
8
|
+
project: GLOBAL_PERSONAL_SKILLS_PROJECT,
|
|
9
|
+
scope: "personal",
|
|
10
|
+
});
|
|
11
|
+
return dedupeContexts(contexts);
|
|
12
|
+
};
|
|
13
|
+
export const personalIndexContexts = (project, scope) => {
|
|
14
|
+
if (scope !== "personal")
|
|
15
|
+
return [{ project, scope }];
|
|
16
|
+
return skillLookupContexts(project, "personal");
|
|
17
|
+
};
|
|
18
|
+
export const dedupeSkillSummariesByName = (skills) => {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const deduped = [];
|
|
21
|
+
for (const skill of skills) {
|
|
22
|
+
if (seen.has(skill.name))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(skill.name);
|
|
25
|
+
deduped.push(skill);
|
|
26
|
+
}
|
|
27
|
+
return deduped;
|
|
28
|
+
};
|
|
29
|
+
export const dedupeContexts = (contexts) => {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
return contexts.filter((context) => {
|
|
32
|
+
const key = `${context.project}:${context.scope}`;
|
|
33
|
+
if (seen.has(key))
|
|
34
|
+
return false;
|
|
35
|
+
seen.add(key);
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { dedupeSkillSummariesByName, personalIndexContexts } from './personal-skills.js';
|
|
1
2
|
export class SkillIndexService {
|
|
2
3
|
registry;
|
|
3
4
|
constructor(registry) {
|
|
4
5
|
this.registry = registry;
|
|
5
6
|
}
|
|
6
7
|
getIndex(input) {
|
|
7
|
-
const listed = this.
|
|
8
|
+
const listed = this.listSkills(input);
|
|
8
9
|
if (!listed.ok)
|
|
9
10
|
return listed;
|
|
10
11
|
const skills = [];
|
|
@@ -41,6 +42,16 @@ export class SkillIndexService {
|
|
|
41
42
|
},
|
|
42
43
|
};
|
|
43
44
|
}
|
|
45
|
+
listSkills(input) {
|
|
46
|
+
const skills = [];
|
|
47
|
+
for (const context of personalIndexContexts(input.project, input.scope)) {
|
|
48
|
+
const listed = this.registry.listSkills({ project: context.project, scope: context.scope });
|
|
49
|
+
if (!listed.ok)
|
|
50
|
+
return listed;
|
|
51
|
+
skills.push(...listed.value);
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, value: dedupeSkillSummariesByName(skills) };
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
function indexSkill(details) {
|
|
46
57
|
const currentVersion = details.currentVersion;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveAgentSelector } from '../agents/agent-selector-resolver.js';
|
|
2
2
|
import { normalizeSddPhaseInput } from '../sdd/schema.js';
|
|
3
|
+
import { skillLookupContexts } from './personal-skills.js';
|
|
3
4
|
export class SkillResolver {
|
|
4
5
|
skills;
|
|
5
6
|
agents;
|
|
@@ -163,7 +164,16 @@ export class SkillResolver {
|
|
|
163
164
|
return validationFailure('Resolved skill candidate has neither skill id nor name');
|
|
164
165
|
if (project === undefined)
|
|
165
166
|
return validationFailure(`Skill ${candidate.name} needs a project context`);
|
|
166
|
-
|
|
167
|
+
let lastMissing;
|
|
168
|
+
for (const context of skillLookupContexts(project, scope)) {
|
|
169
|
+
const result = this.skills.getByName(context.project, context.scope, candidate.name);
|
|
170
|
+
if (result.ok)
|
|
171
|
+
return result;
|
|
172
|
+
if (result.error.code !== 'not_found')
|
|
173
|
+
return result;
|
|
174
|
+
lastMissing = result;
|
|
175
|
+
}
|
|
176
|
+
return lastMissing ?? this.skills.getByName(project, scope, candidate.name);
|
|
167
177
|
}
|
|
168
178
|
resolveVersion(skill, candidateVersionId) {
|
|
169
179
|
const versionId = candidateVersionId ?? skill.currentVersionId;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeSkillSummariesByName, personalIndexContexts } from './personal-skills.js';
|
|
1
2
|
export class SkillStatusService {
|
|
2
3
|
skills;
|
|
3
4
|
agents;
|
|
@@ -81,11 +82,16 @@ export class SkillStatusService {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
skillRegistry(project, scope) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const listedSkills = [];
|
|
86
|
+
for (const context of personalIndexContexts(project, scope)) {
|
|
87
|
+
const listed = this.skills.listSkills({ project: context.project, scope: context.scope });
|
|
88
|
+
if (!listed.ok)
|
|
89
|
+
return listed;
|
|
90
|
+
listedSkills.push(...listed.value);
|
|
91
|
+
}
|
|
92
|
+
const summaries = dedupeSkillSummariesByName(listedSkills);
|
|
87
93
|
const items = [];
|
|
88
|
-
for (const skill of
|
|
94
|
+
for (const skill of summaries) {
|
|
89
95
|
const details = this.skills.getSkillDetails(skill.id);
|
|
90
96
|
if (!details.ok)
|
|
91
97
|
return details;
|
|
@@ -49,6 +49,9 @@ export class CommandAllowlistAdapter {
|
|
|
49
49
|
this.plan = plan.value;
|
|
50
50
|
}
|
|
51
51
|
dispatch(_request) {
|
|
52
|
+
return this.executeAllowlistedCommand();
|
|
53
|
+
}
|
|
54
|
+
executeAllowlistedCommand() {
|
|
52
55
|
const result = this.runner(this.request);
|
|
53
56
|
if (!result.ok)
|
|
54
57
|
throw new Error(`${result.error.code}: ${result.error.message}`);
|
|
@@ -86,6 +89,103 @@ export class CommandAllowlistAdapter {
|
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
}
|
|
92
|
+
export class AllowlistedApplyProgressCommandExecutor {
|
|
93
|
+
options;
|
|
94
|
+
name = 'allowlisted-apply-progress-command-executor';
|
|
95
|
+
constructor(options = {}) {
|
|
96
|
+
this.options = options;
|
|
97
|
+
}
|
|
98
|
+
execute(request) {
|
|
99
|
+
const contract = applyProgressCommandContract(request.operation.input);
|
|
100
|
+
if (!contract.ok)
|
|
101
|
+
return contract;
|
|
102
|
+
const operationCommandId = commandIdFromOperation(request.operation.operation);
|
|
103
|
+
if (operationCommandId === undefined)
|
|
104
|
+
return validationFailure('Allowlisted apply-progress execution requires operation command-allowlist:<id>.');
|
|
105
|
+
if (operationCommandId !== contract.value.commandId)
|
|
106
|
+
return validationFailure(`Allowlisted command mismatch: operation ${operationCommandId}, input ${contract.value.commandId}.`);
|
|
107
|
+
const workspaceRoot = this.options.workspaceRoot ?? request.operation.workspaceRoot ?? contract.value.workspaceRoot;
|
|
108
|
+
if (workspaceRoot === undefined || workspaceRoot.trim().length === 0)
|
|
109
|
+
return validationFailure('Allowlisted command execution requires a workspaceRoot.');
|
|
110
|
+
try {
|
|
111
|
+
const adapterOptions = {
|
|
112
|
+
commandId: contract.value.commandId,
|
|
113
|
+
workspaceRoot,
|
|
114
|
+
targetPaths: this.options.targetPaths ?? contract.value.targetPaths,
|
|
115
|
+
};
|
|
116
|
+
const cwd = this.options.cwd ?? contract.value.cwd;
|
|
117
|
+
if (cwd !== undefined)
|
|
118
|
+
adapterOptions.cwd = cwd;
|
|
119
|
+
if (this.options.allowlist !== undefined)
|
|
120
|
+
adapterOptions.allowlist = this.options.allowlist;
|
|
121
|
+
if (this.options.runner !== undefined)
|
|
122
|
+
adapterOptions.runner = this.options.runner;
|
|
123
|
+
const adapter = new CommandAllowlistAdapter({
|
|
124
|
+
...adapterOptions,
|
|
125
|
+
});
|
|
126
|
+
const result = adapter.executeAllowlistedCommand();
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
value: {
|
|
130
|
+
output: {
|
|
131
|
+
kind: 'apply-progress-operation-result',
|
|
132
|
+
operationKind: 'allowlisted-command',
|
|
133
|
+
runId: request.runId,
|
|
134
|
+
commandId: contract.value.commandId,
|
|
135
|
+
argv: [adapter.plan.request.command, ...adapter.plan.request.argv],
|
|
136
|
+
executor: this.name,
|
|
137
|
+
status: result.failure === undefined ? 'succeeded' : 'failed',
|
|
138
|
+
audit: result.audit ?? null,
|
|
139
|
+
command: result.output ?? null,
|
|
140
|
+
failure: result.failure ?? null,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return validationFailure(error instanceof Error ? error.message : 'Allowlisted command adapter rejected the request.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
89
150
|
export function commandAllowlistIds(allowlist = defaultCommandAllowlist) {
|
|
90
151
|
return Object.keys(allowlist).sort();
|
|
91
152
|
}
|
|
153
|
+
function commandIdFromOperation(operation) {
|
|
154
|
+
const match = /^command-allowlist:([A-Za-z0-9._/-]+)$/u.exec(operation);
|
|
155
|
+
return match?.[1];
|
|
156
|
+
}
|
|
157
|
+
function applyProgressCommandContract(input) {
|
|
158
|
+
if (!isObject(input))
|
|
159
|
+
return validationFailure('Allowlisted apply-progress execution requires structured input.');
|
|
160
|
+
if (input.kind !== 'apply-progress-operation')
|
|
161
|
+
return validationFailure('Allowlisted apply-progress execution requires kind apply-progress-operation.');
|
|
162
|
+
if (input.operationKind !== 'allowlisted-command')
|
|
163
|
+
return validationFailure('Allowlisted apply-progress execution requires operationKind allowlisted-command.');
|
|
164
|
+
if (typeof input.shell === 'string' || typeof input.commandString === 'string')
|
|
165
|
+
return validationFailure('Shell strings are rejected; use commandId with fixed allowlisted argv.');
|
|
166
|
+
if (typeof input.providerToolName === 'string' || input.providerDispatch === true)
|
|
167
|
+
return validationFailure('Provider dispatch is rejected on the allowlisted command execution path.');
|
|
168
|
+
const commandId = typeof input.commandId === 'string' ? input.commandId : typeof input.command === 'string' ? input.command : undefined;
|
|
169
|
+
if (commandId === undefined || commandId.trim().length === 0)
|
|
170
|
+
return validationFailure('Allowlisted command input requires commandId.');
|
|
171
|
+
const workspaceRoot = typeof input.workspaceRoot === 'string' ? input.workspaceRoot : undefined;
|
|
172
|
+
const cwd = typeof input.cwd === 'string' ? input.cwd : undefined;
|
|
173
|
+
const rawTargets = input.targetPaths;
|
|
174
|
+
if (rawTargets !== undefined && (!Array.isArray(rawTargets) || !rawTargets.every((item) => typeof item === 'string')))
|
|
175
|
+
return validationFailure('Allowlisted command targetPaths must be an array of strings.');
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
value: {
|
|
179
|
+
commandId,
|
|
180
|
+
...(workspaceRoot === undefined ? {} : { workspaceRoot }),
|
|
181
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
182
|
+
targetPaths: rawTargets === undefined ? [] : rawTargets,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function isObject(value) {
|
|
187
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
188
|
+
}
|
|
189
|
+
function validationFailure(message) {
|
|
190
|
+
return { ok: false, error: { code: 'validation_failed', message } };
|
|
191
|
+
}
|
|
@@ -265,7 +265,13 @@ export class GuardedProviderWorkflowExecutor {
|
|
|
265
265
|
message: 'Workspace boundary constraints are present but not enforceable in this runtime.',
|
|
266
266
|
nextAction: 'Enable realpath/workspace boundary enforcement before dispatch.',
|
|
267
267
|
};
|
|
268
|
-
if (
|
|
268
|
+
if (boundedCommandAdapter && !capabilities.processExecutionEnforceable)
|
|
269
|
+
return {
|
|
270
|
+
reason: 'process-execution-not-enforceable',
|
|
271
|
+
message: 'Bounded command dispatch requires enforceable process execution constraints.',
|
|
272
|
+
nextAction: 'Build accepted bounded-process evidence before running an allowlisted command adapter.',
|
|
273
|
+
};
|
|
274
|
+
if (!boundedCommandAdapter && (!capabilities.sandboxEnforceable || !capabilities.processExecutionEnforceable))
|
|
269
275
|
return {
|
|
270
276
|
reason: 'sandbox-not-enforceable',
|
|
271
277
|
message: 'Sandbox/process execution capability is not enforceable; provider dispatch is refused.',
|
package/docs/architecture.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vgxness Architecture
|
|
2
2
|
|
|
3
|
-
> **Scope:** this document describes the current
|
|
3
|
+
> **Scope:** this document describes the current architecture as it is actually built. It is the source of truth for how the product works today; the latest full health snapshot is the historical [Project health audit v1.14.x](./project-health-audit-v1.14.x.md), while the package version is sourced from `package.json`. Planned work that is not yet shipped lives in [Roadmap](./roadmap.md); historical planning context that no longer reflects reality has been retired. Where this doc disagrees with code, code wins — file a doc-sync task against the relevant module.
|
|
4
4
|
|
|
5
5
|
`vgxness` is a local-first, provider-agnostic, Gentle-AI-like harness for agentic development. Its core architecture separates the product domain from provider-specific tooling so agents, skills, memory, SDD workflows, runs, and traces can work across OpenCode, Claude Code, and future adapters such as Pi.
|
|
6
6
|
|
|
@@ -637,7 +637,7 @@ The experimental code runtime layer was removed. Current risky side effects are
|
|
|
637
637
|
|
|
638
638
|
CLI surface groups are documented in [CLI reference](./cli.md). The plural form is canonical — singular shortcuts are not added.
|
|
639
639
|
|
|
640
|
-
MCP tools mirror the same core services for agent use. The full, current list of
|
|
640
|
+
MCP tools mirror the same core services for agent use. The full, current list of 42 tools is in [MCP tools](./mcp.md) and `SUPPORTED_VGX_MCP_TOOL_NAMES` (`src/mcp/schema.ts`); treat that array as the source of truth. The CLI is the human/operator control surface. MCP is the agent-facing control surface.
|
|
641
641
|
|
|
642
642
|
## Evaluation strategy
|
|
643
643
|
|
package/docs/cli.md
CHANGED
|
@@ -963,7 +963,7 @@ For more on schema, scopes, and lifecycle, see [Storage](./storage.md).
|
|
|
963
963
|
## See also
|
|
964
964
|
|
|
965
965
|
- [Architecture](./architecture.md) — current-state architecture and core domain model.
|
|
966
|
-
- [MCP tools](./mcp.md) — full reference for the
|
|
966
|
+
- [MCP tools](./mcp.md) — full reference for the 42 MCP tools exposed to agents.
|
|
967
967
|
- [Safety model](./safety.md) — permission categories, approval flow, redactors, and runtime gates.
|
|
968
968
|
- [Storage](./storage.md) — SQLite schema, scopes, and lifecycle.
|
|
969
969
|
- [Providers](./providers.md) — adapter contract and how to add a new provider.
|
package/docs/glossary.md
CHANGED
|
@@ -84,7 +84,7 @@ A redacted, structured report over SDD state, runs, and approvals. Surfaced thro
|
|
|
84
84
|
|
|
85
85
|
## MCP
|
|
86
86
|
|
|
87
|
-
Model Context Protocol. The agent-facing transport. VGXNESS exposes
|
|
87
|
+
Model Context Protocol. The agent-facing transport. VGXNESS exposes 42 typed tools over stdio through `vgxness mcp start`. The tool list lives in `SUPPORTED_VGX_MCP_TOOL_NAMES` (`src/mcp/schema.ts`) and is documented in [MCP tools](./mcp.md).
|
|
88
88
|
|
|
89
89
|
## Memory observation
|
|
90
90
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auditoría de salud del proyecto VGXNESS v1.10.x
|
|
2
2
|
|
|
3
|
-
> Historical snapshot: this document records the v1.10.x health review and should not be treated as current status. See [Project health audit v1.14.x](./project-health-audit-v1.14.x.md) for
|
|
3
|
+
> Historical snapshot: this document records the v1.10.x health review and should not be treated as current status. See [Project health audit v1.14.x](./project-health-audit-v1.14.x.md) for a newer historical release-readiness snapshot.
|
|
4
4
|
|
|
5
5
|
## Resumen ejecutivo
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ Esta auditoría es una foto de salud v1.10.x basada en inspección read-only del
|
|
|
17
17
|
| Package | `package.json` declara `version: 1.10.1` y describe el producto como CLI/MCP control plane. | Implementado |
|
|
18
18
|
| Runtime/tooling | `package.json` usa Bun para runtime instalado y verificación (`bun run verify`, `verify:bun-sqlite`, `package:bun:evidence`). | Implementado |
|
|
19
19
|
| CLI | `src/cli/dispatcher.ts` centraliza comandos para status, next, resume, setup, agents, skills, SDD, runs, permissions, MCP y verification. | Implementado |
|
|
20
|
-
| MCP | `src/mcp/stdio-server.ts` y `src/mcp/schema.ts` exponen herramientas tipadas; el estado observado
|
|
20
|
+
| MCP | `src/mcp/stdio-server.ts` y `src/mcp/schema.ts` exponen herramientas tipadas; el estado observado durante v1.10.x reportó 41 MCP tools. | Implementado |
|
|
21
21
|
| SDD | `src/sdd/sdd-workflow-service.ts` y `docs/architecture.md` definen `explore → proposal → spec → design → tasks → apply-progress → verify → archive`. | Implementado |
|
|
22
22
|
| Storage | `src/memory/sqlite/*` y migraciones en `src/memory/sqlite/migrations/*.sql`; runtime real depende de `bun:sqlite`. | Implementado |
|
|
23
23
|
| Providers | OpenCode es primario; Claude está soportado/guarded; Antigravity/custom son extensiones futuras/placeholders. | Mixto |
|
|
@@ -76,7 +76,7 @@ Hay documentación amplia (`README.md`, `docs/architecture.md`, `docs/cli.md`, `
|
|
|
76
76
|
- `package.json` está en `1.10.1`.
|
|
77
77
|
- `README.md` y `docs/architecture.md` ya apuntan a esta auditoría v1.10.x como snapshot actual.
|
|
78
78
|
- Las referencias v1.5.1/v1.9.1 que quedan están clasificadas como historia en PRD o en la auditoría versionada v1.9.1.
|
|
79
|
-
- La auditoría histórica `v1.9.1` registra 38 MCP tools y 106 test files; la inspección
|
|
79
|
+
- La auditoría histórica `v1.9.1` registra 38 MCP tools y 106 test files; la inspección de v1.10.x reportó 41 MCP tools y un inventario distinto de tests según exploración.
|
|
80
80
|
|
|
81
81
|
## Riesgos principales
|
|
82
82
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Auditoría de salud del proyecto VGXNESS v1.14.x
|
|
2
2
|
|
|
3
|
+
> Historical snapshot: this document records v1.14.x release-readiness evidence and should not be treated as current package status. The current package version is sourced from `package.json`.
|
|
4
|
+
|
|
3
5
|
## Resumen ejecutivo
|
|
4
6
|
|
|
5
7
|
VGXNESS v1.14.x está avanzado como **CLI/MCP control plane local-first** para SDD, memoria, runs, permisos, agentes, skills y setup de OpenCode. La base de producto es sólida: el flujo SDD canónico existe como estado SQLite-backed, OpenCode es el provider primario, los drafts y la aceptación humana están separados, y la ruta oficial de verificación es Bun-first.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auditoría de salud del proyecto VGXNESS v1.9.1
|
|
2
2
|
|
|
3
|
-
> Historical snapshot: this document records validated v1.9.1 evidence and should not be treated as current status. See [Project health audit v1.14.x](./project-health-audit-v1.14.x.md) for
|
|
3
|
+
> Historical snapshot: this document records validated v1.9.1 evidence and should not be treated as current status. See [Project health audit v1.14.x](./project-health-audit-v1.14.x.md) for a newer historical release-readiness snapshot.
|
|
4
4
|
|
|
5
5
|
## Resumen v1.9.1
|
|
6
6
|
|
package/docs/providers.md
CHANGED
|
@@ -4,7 +4,7 @@ VGXNESS is provider-agnostic at the core: the registry stores provider-neutral d
|
|
|
4
4
|
|
|
5
5
|
## Status
|
|
6
6
|
|
|
7
|
-
Current as of the v1.14.x
|
|
7
|
+
Current as of the `1.16.0` package documentation snapshot. The latest full health audit remains the historical v1.14.x snapshot.
|
|
8
8
|
|
|
9
9
|
| Provider | Control plane | Workspace runtime | Notes |
|
|
10
10
|
|---|---|---|---|
|
package/docs/roadmap.md
CHANGED
|
@@ -21,7 +21,7 @@ The hard acceptance gate and the cockpit blockers are in. The remaining work is
|
|
|
21
21
|
|
|
22
22
|
- **Verification evidence linked to the cockpit.** The cockpit currently returns blockers; surfacing the per-phase verification evidence (pass/fail/skipped counts, last-run timestamps) would make "why is this blocked" answers more useful.
|
|
23
23
|
- **Per-phase model/profile routing in the cockpit.** The manager profile overlay exists. The cockpit should recommend a model for the next phase based on the active overlay.
|
|
24
|
-
- **Migration of `openspec/`-style workflows.** Some users bring artifacts from other tools.
|
|
24
|
+
- **Migration of `openspec/`-style workflows.** Some users bring artifacts from other tools. The artifact portability service and CLI can now plan/import SDD packages as drafts; remaining work is better migration UX, validation guidance, and explicit human re-acceptance flows so external artifacts are never silently converted into accepted SDD phases.
|
|
25
25
|
|
|
26
26
|
## Skills and agents
|
|
27
27
|
|
|
@@ -35,7 +35,7 @@ The registries, version model, payloads, and improvement-proposal lifecycle are
|
|
|
35
35
|
|
|
36
36
|
SQLite, scopes, migrations, and the run snapshot export are in.
|
|
37
37
|
|
|
38
|
-
-
|
|
38
|
+
- **SDD artifact import hardening.** The `ArtifactPortabilityService` and CLI now support import/plan flows that save artifacts as drafts. Remaining work is UX and policy hardening around overwrite review, provenance, and explicit human re-acceptance after import.
|
|
39
39
|
- **Sensitive-data redaction during export.** `src/export/redaction.ts` exists. Wiring it as a default into the export path and a CLI flag (`--redact`) is the next step.
|
|
40
40
|
- **Database upgrade tooling.** Forward-only migrations work; downgrade requires a snapshot. A small CLI helper for "is my DB on the latest migration?" would be useful for support.
|
|
41
41
|
|
package/docs/sdd-flow.md
CHANGED
|
@@ -59,6 +59,15 @@ agent_resolve
|
|
|
59
59
|
agent_activate
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
Copyable OpenCode prompt for the normal daily path:
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
Continue VGXNESS SDD for project <project> and change <change>.
|
|
66
|
+
Use the VGXNESS MCP tools to inspect status/readiness/artifacts, then delegate phase work to the exact hidden SDD subagent when needed.
|
|
67
|
+
Save draft artifacts when appropriate and report evidence, tests, blockers, and the next human decision.
|
|
68
|
+
Do not accept or reopen artifacts for me; ask me before any human-only, provider-config, git, destructive, external, install, or secret-sensitive action.
|
|
69
|
+
```
|
|
70
|
+
|
|
62
71
|
The CLI equivalents are useful for setup, diagnostics, recovery, and scripting:
|
|
63
72
|
|
|
64
73
|
```bash
|
|
@@ -111,7 +120,7 @@ Only a human acceptance decision should advance governance-gated downstream work
|
|
|
111
120
|
CLI example:
|
|
112
121
|
|
|
113
122
|
```bash
|
|
114
|
-
vgxness sdd accept-artifact --project <project> --change <change> --phase explore
|
|
123
|
+
vgxness sdd accept-artifact --project <project> --change <change> --phase explore --actor <human-id>
|
|
115
124
|
```
|
|
116
125
|
|
|
117
126
|
## 4. `proposal`: choose the product direction
|