vgxness 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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 v1.14.x has a current project health audit describing the implemented CLI/MCP/SDD control plane, OpenCode-first workflow, release-readiness checks, and the remaining execution/recovery gaps. See [Project health audit v1.14.x](./docs/project-health-audit-v1.14.x.md) for the current system snapshot; [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.
11
+ VGXNESS is currently versioned from `package.json` (`1.16.0`). 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
 
@@ -256,7 +256,7 @@ Remove any user-global OpenCode/Claude config entries and local/global VGXNESS d
256
256
  ## More docs
257
257
 
258
258
  - [CLI reference](./docs/cli.md)
259
- - [Project health audit v1.14.x](./docs/project-health-audit-v1.14.x.md)
259
+ - [Project health audit v1.14.x](./docs/project-health-audit-v1.14.x.md) — historical snapshot
260
260
  - [Project health audit v1.10.x](./docs/project-health-audit-v1.10.x.md)
261
261
  - [Project health audit v1.9.1](./docs/project-health-audit-v1.9.1.md)
262
262
  - [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) {
@@ -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 opens first run detail \u00B7 Esc/b back to Home \u00B7 q exits \u00B7 read-only \u00B7 no writes" })] }));
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
- 'Use `vgxness resume --project <name>` to inspect interrupted runs from a Bun-backed runtime.',
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 resume --project <name>` to inspect interrupted runs.',
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
- 'Next implementation slice can wire this panel to the run read model.',
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', String(run.approvals)),
155
- field('Attempts', String(run.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,31 @@ 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}`,
163
- 'No retry, approval, or execution is performed from this TUI detail.',
168
+ 'Safety: read-only/no writes/no retry/approval/execution from this TUI detail.',
164
169
  ];
165
170
  }
166
- function runSummarySection(title, runs) {
171
+ function runSummarySection(title, runs, details = []) {
167
172
  if (runs.length === 0)
168
173
  return [title, '- none'];
169
- return [title, ...runs.map((run) => `- ${shortRunId(run.id)} ${run.status} ${run.workflow}/${run.phase}${run.userIntent === undefined ? '' : ` — ${run.userIntent}`}`)];
174
+ return [title, ...runs.map((run) => runSummaryLine(run, details.find((detail) => detail.id === run.id)))];
175
+ }
176
+ function runSummaryLine(run, detail) {
177
+ const base = `- ${shortRunId(run.id)} ${run.status} ${run.workflow}/${run.phase}${run.userIntent === undefined ? '' : ` — ${run.userIntent}`}`;
178
+ if (detail === undefined)
179
+ return base;
180
+ return [
181
+ base,
182
+ `approvals ${detail.approvals} (${formatApprovalStatusCounts(detail.approvalStatusCounts)})`,
183
+ `attempts ${detail.attempts} (${formatAttemptStatusCounts(detail.attemptStatusCounts)})`,
184
+ `latest checkpoint ${detail.latestCheckpointLabel ?? 'none'}`,
185
+ `latest event ${detail.latestEventTitle ?? 'none'}`,
186
+ ].join(' · ');
187
+ }
188
+ function formatApprovalStatusCounts(counts) {
189
+ return [`pending=${counts.pending}`, `approved=${counts.approved}`, `rejected=${counts.rejected}`, `cancelled=${counts.cancelled}`].join(' ');
190
+ }
191
+ function formatAttemptStatusCounts(counts) {
192
+ return [`reserved=${counts.reserved}`, `succeeded=${counts.succeeded}`, `failed=${counts.failed}`, `abandoned=${counts.abandoned}`].join(' ');
170
193
  }
171
194
  function shortRunId(id) {
172
195
  return id.length <= 8 ? id : id.slice(0, 8);
@@ -287,7 +310,7 @@ function skillSummarySection(skills) {
287
310
  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
311
  }
289
312
  function runsLines() {
290
- return ['Run dashboard is not loaded in this first Home TUI slice.', '', 'Use `vgxness status` or `vgxness resume --project <name>` for current CLI diagnostics.', 'Next slice can wire this panel to the run read model.'];
313
+ 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
314
  }
292
315
  function sddLines() {
293
316
  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, mapMemoryResult(services.sdd.getCockpit(validated.input), buildSddCockpitSurfaceResponse));
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)
@@ -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: operationMetadata(operation),
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: operationMetadata(operation),
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: true,
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
  }
@@ -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 (!capabilities.sandboxEnforceable || !capabilities.processExecutionEnforceable)
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.',
@@ -1,6 +1,6 @@
1
1
  # vgxness Architecture
2
2
 
3
- > **Scope:** this document describes the current v1.14.x architecture as it is actually built. It is the source of truth for how the product works today, with the latest health snapshot captured in [Project health audit v1.14.x](./project-health-audit-v1.14.x.md). 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.
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 41 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.
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 41 MCP tools exposed to agents.
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 41 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).
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 the current system snapshot.
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 reporta 41 MCP tools. | Implementado |
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 actual reporta 41 MCP tools y un inventario distinto de tests según exploració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 the current system snapshot.
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 documentation snapshot.
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. A formal `import` path through the artifact portability service would help, but it should not silently convert external artifacts into accepted SDD phases.
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
- - **`import` path for SDD artifacts.** The `ArtifactPortabilityService` exports; an import path that re-creates acceptance records with explicit human re-acceptance is planned.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "CLI and MCP control plane for guided AI-agent workflows, SDD, memory, and OpenCode setup.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {