vgxness 1.9.2 → 1.9.4
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 -6
- package/dist/agents/agent-resolver.js +33 -3
- package/dist/agents/canonical-agent-manifest.js +68 -21
- package/dist/agents/canonical-agent-projection.js +46 -4
- package/dist/cli/cli-help.js +14 -3
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +8 -0
- package/dist/cli/commands/mcp-dispatcher.js +49 -18
- package/dist/cli/commands/memory-sdd-dispatcher.js +71 -5
- package/dist/cli/commands/setup-dispatcher.js +22 -10
- package/dist/cli/commands/status-dispatcher.js +130 -0
- package/dist/cli/commands/workflow-dispatcher.js +11 -5
- package/dist/cli/dispatcher.js +9 -1
- package/dist/cli/product-resume-renderer.js +32 -0
- package/dist/cli/product-status-renderer.js +81 -0
- package/dist/cli/sdd-renderer.js +90 -7
- package/dist/cli/tui/main-menu/main-menu-read-model.js +8 -8
- package/dist/cli/tui/setup/setup-tui-services.js +27 -10
- package/dist/code/cli/code-command.js +7 -4
- package/dist/code/reporting/summary.js +4 -1
- package/dist/code/runtime/code-runtime.js +27 -4
- package/dist/code/runtime/sdd-context.js +18 -2
- package/dist/governance/governance-report-builder.js +18 -7
- package/dist/mcp/claude-code-agent-config.js +10 -4
- package/dist/mcp/client-install-opencode-contract.js +2 -2
- package/dist/mcp/client-install-opencode.js +10 -6
- package/dist/mcp/control-plane.js +56 -0
- package/dist/mcp/opencode-default-agent-config.js +7 -4
- package/dist/mcp/provider-status.js +86 -81
- package/dist/mcp/schema.js +25 -7
- package/dist/mcp/stdio-server.js +4 -0
- package/dist/mcp/validation.js +39 -3
- package/dist/resume/product-resume.js +166 -0
- package/dist/runs/repositories/runs.js +12 -1
- package/dist/runs/run-service.js +62 -5
- package/dist/sdd/schema.js +8 -0
- package/dist/sdd/sdd-continuation-plan.js +81 -0
- package/dist/sdd/sdd-workflow-service.js +103 -16
- package/dist/skills/skill-resolver.js +21 -4
- package/dist/status/product-status.js +121 -0
- package/docs/architecture.md +1 -1
- package/docs/cli.md +40 -12
- package/docs/code-runtime.md +3 -0
- package/docs/glossary.md +1 -1
- package/docs/mcp.md +18 -4
- package/package.json +1 -1
package/dist/runs/run-service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluatePermission } from '../permissions/policy-evaluator.js';
|
|
2
2
|
import { isRiskyPermissionCategory } from '../permissions/schema.js';
|
|
3
|
+
import { normalizeSddPhaseInput } from '../sdd/schema.js';
|
|
3
4
|
import { planExecutionIsolation } from './execution-planning.js';
|
|
4
5
|
import { evaluateOperationRetry as evaluateOperationRetryPolicy } from './operation-retry.js';
|
|
5
6
|
import { RunRepository, } from './repositories/runs.js';
|
|
@@ -28,7 +29,7 @@ export class RunService {
|
|
|
28
29
|
this.runs = new RunRepository(database);
|
|
29
30
|
}
|
|
30
31
|
createRun(input) {
|
|
31
|
-
return this.runs.create(input);
|
|
32
|
+
return this.runs.create({ ...input, phase: canonicalRunPhase(input.workflow, input.phase) ?? input.phase });
|
|
32
33
|
}
|
|
33
34
|
getRun(id) {
|
|
34
35
|
return this.runs.getDetails(id);
|
|
@@ -44,6 +45,53 @@ export class RunService {
|
|
|
44
45
|
listRuns(filters = {}) {
|
|
45
46
|
return this.runs.list(filters);
|
|
46
47
|
}
|
|
48
|
+
listRecentInterruptedRuns(input) {
|
|
49
|
+
const runs = this.runs.list({ project: input.project, statuses: ['failed', 'blocked', 'needs-human'], limit: input.limit ?? 5 });
|
|
50
|
+
if (!runs.ok)
|
|
51
|
+
return runs;
|
|
52
|
+
const candidates = [];
|
|
53
|
+
for (const run of runs.value) {
|
|
54
|
+
const details = this.runs.getDetails(run.id);
|
|
55
|
+
if (!details.ok)
|
|
56
|
+
return details;
|
|
57
|
+
const latestCheckpoint = details.value.checkpoints.at(-1);
|
|
58
|
+
const userIntent = details.value.userIntent.trim();
|
|
59
|
+
candidates.push({
|
|
60
|
+
runId: details.value.id,
|
|
61
|
+
status: details.value.status,
|
|
62
|
+
workflow: details.value.workflow,
|
|
63
|
+
phase: details.value.phase,
|
|
64
|
+
...(userIntent.length === 0 ? {} : { userIntent }),
|
|
65
|
+
...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, value: candidates };
|
|
69
|
+
}
|
|
70
|
+
findRelatedInterruptedSddRun(input) {
|
|
71
|
+
const runs = this.runs.list({ project: input.project, statuses: ['failed', 'blocked', 'needs-human'], limit: input.limit ?? 25 });
|
|
72
|
+
if (!runs.ok)
|
|
73
|
+
return runs;
|
|
74
|
+
for (const run of runs.value) {
|
|
75
|
+
const details = this.runs.getDetails(run.id);
|
|
76
|
+
if (!details.ok)
|
|
77
|
+
return details;
|
|
78
|
+
const hasMatchingCheckpoint = details.value.checkpoints.some((checkpoint) => checkpointHasChangeId(checkpoint.state, input.change));
|
|
79
|
+
if (!hasMatchingCheckpoint)
|
|
80
|
+
continue;
|
|
81
|
+
const latestCheckpoint = details.value.checkpoints.at(-1);
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
value: {
|
|
85
|
+
runId: details.value.id,
|
|
86
|
+
status: details.value.status,
|
|
87
|
+
workflow: details.value.workflow,
|
|
88
|
+
phase: details.value.phase,
|
|
89
|
+
...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, value: undefined };
|
|
94
|
+
}
|
|
47
95
|
appendEvent(input) {
|
|
48
96
|
return this.runs.appendEvent(input);
|
|
49
97
|
}
|
|
@@ -695,10 +743,11 @@ export class RunService {
|
|
|
695
743
|
const run = this.runs.getById(input.runId);
|
|
696
744
|
if (!run.ok)
|
|
697
745
|
return { ok: false, error: run.error };
|
|
698
|
-
const
|
|
746
|
+
const workflow = input.workflow ?? run.value.workflow;
|
|
747
|
+
const phase = canonicalRunPhase(workflow, input.phase) ?? canonicalRunPhase(run.value.workflow, run.value.phase);
|
|
699
748
|
const merged = {
|
|
700
749
|
...input,
|
|
701
|
-
workflow
|
|
750
|
+
workflow,
|
|
702
751
|
...(phase === undefined ? {} : { phase }),
|
|
703
752
|
agentId: input.agentId ?? input.agent?.id ?? run.value.selectedAgentId,
|
|
704
753
|
};
|
|
@@ -799,6 +848,9 @@ export class RunService {
|
|
|
799
848
|
return { ok: true, value: resolved[0] };
|
|
800
849
|
}
|
|
801
850
|
}
|
|
851
|
+
function checkpointHasChangeId(state, change) {
|
|
852
|
+
return typeof state === 'object' && state !== null && !Array.isArray(state) && state.changeId === change;
|
|
853
|
+
}
|
|
802
854
|
function preflightOutcome(decision) {
|
|
803
855
|
if (decision.decision === 'allow')
|
|
804
856
|
return 'allowed';
|
|
@@ -883,10 +935,15 @@ function resumeGateManualNextCommands(approval, details, operation) {
|
|
|
883
935
|
commands.push(`Review operation manually: ${operation.category} ${operation.operation}`);
|
|
884
936
|
return commands;
|
|
885
937
|
}
|
|
886
|
-
function canonicalRunPhase(phase) {
|
|
938
|
+
function canonicalRunPhase(workflow, phase) {
|
|
887
939
|
if (phase === undefined || phase.trim().length === 0)
|
|
888
940
|
return undefined;
|
|
889
|
-
|
|
941
|
+
if (!isSddWorkflow(workflow))
|
|
942
|
+
return phase;
|
|
943
|
+
return normalizeSddPhaseInput(phase) ?? phase;
|
|
944
|
+
}
|
|
945
|
+
function isSddWorkflow(workflow) {
|
|
946
|
+
return workflow?.trim().toLowerCase() === 'sdd';
|
|
890
947
|
}
|
|
891
948
|
function operationMetadata(input) {
|
|
892
949
|
const metadata = { category: input.category, name: input.operation };
|
package/dist/sdd/schema.js
CHANGED
|
@@ -98,6 +98,14 @@ export const sddPrerequisites = {
|
|
|
98
98
|
export function isSddPhase(value) {
|
|
99
99
|
return sddPhases.includes(value);
|
|
100
100
|
}
|
|
101
|
+
export function normalizeSddPhaseInput(value) {
|
|
102
|
+
const normalized = value.trim();
|
|
103
|
+
if (normalized === 'apply')
|
|
104
|
+
return 'apply-progress';
|
|
105
|
+
if (isSddPhase(normalized))
|
|
106
|
+
return normalized;
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
101
109
|
export function sddTopicKey(change, phase) {
|
|
102
110
|
return `sdd/${change}/${phase}`;
|
|
103
111
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function sddContinuationPlanFrom(input) {
|
|
2
|
+
const nextPhase = input.next.nextPhase;
|
|
3
|
+
const dbFlag = continuationDbFlag(input.explicitDatabasePath);
|
|
4
|
+
const inspectCommand = `vgxness sdd cockpit --project ${input.project} --change ${input.next.change} --json${dbFlag}`;
|
|
5
|
+
const suggestedCommand = suggestedContinuationCommand(input.project, input.next, inspectCommand, dbFlag);
|
|
6
|
+
const blockerGuidance = input.next.blockerGuidance ?? [];
|
|
7
|
+
const blockerActions = blockerGuidance.map((blocker) => continuationBlockerAction(input.project, input.next.change, input.next.nextPhase, blocker, dbFlag));
|
|
8
|
+
const hasDraftRunAction = blockerActions.some((action) => action.draftRunCommand !== undefined);
|
|
9
|
+
const relatedRunContext = relatedRunContextView(input.project, input.relatedRunContext, dbFlag);
|
|
10
|
+
return {
|
|
11
|
+
kind: 'sdd-continuation-plan',
|
|
12
|
+
project: input.project,
|
|
13
|
+
change: input.next.change,
|
|
14
|
+
status: input.next.status,
|
|
15
|
+
...(nextPhase === undefined ? {} : { nextPhase }),
|
|
16
|
+
...(input.cockpit.actionablePhase === undefined ? {} : { actionablePhase: input.cockpit.actionablePhase }),
|
|
17
|
+
recommendedAction: input.cockpit.recommendedAction,
|
|
18
|
+
reason: input.next.reason,
|
|
19
|
+
suggestedCommand,
|
|
20
|
+
inspectCommand,
|
|
21
|
+
blockerActions,
|
|
22
|
+
...(relatedRunContext === undefined ? {} : { relatedRunContext }),
|
|
23
|
+
...(input.explicitDatabasePath === undefined ? {} : { explicitDatabasePath: input.explicitDatabasePath }),
|
|
24
|
+
safety: [
|
|
25
|
+
'Read-only planner: this command does not execute providers or mutate SDD artifacts, runs, provider config, or openspec/ files.',
|
|
26
|
+
'Human acceptance remains explicit; artifact presence is not treated as acceptance.',
|
|
27
|
+
...(hasDraftRunAction ? [draftRunWarning()] : []),
|
|
28
|
+
'No openspec/ files are created or written.',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function relatedRunContextView(project, relatedRunContext, dbFlag) {
|
|
33
|
+
if (relatedRunContext === undefined)
|
|
34
|
+
return undefined;
|
|
35
|
+
return {
|
|
36
|
+
...relatedRunContext,
|
|
37
|
+
recommendation: 'Related interrupted run found by checkpoint changeId; inspect or resume it before starting duplicate SDD work. Advisory only.',
|
|
38
|
+
resumeCommand: `vgxness resume --project ${project} --run-id ${relatedRunContext.runId}${dbFlag}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function suggestedContinuationCommand(project, next, fallbackInspectCommand, dbFlag) {
|
|
42
|
+
if (next.status === 'runnable' && next.nextPhase !== undefined)
|
|
43
|
+
return runPhaseCommand(project, next.change, next.nextPhase, dbFlag);
|
|
44
|
+
return fallbackInspectCommand;
|
|
45
|
+
}
|
|
46
|
+
function continuationBlockerAction(project, change, blockedPhase, blocker, dbFlag) {
|
|
47
|
+
const draftRunCommand = blocker.reason === 'draft' && isDraftRunPlanningPhase(blockedPhase) ? draftRunPhaseCommand(project, change, blockedPhase, dbFlag) : undefined;
|
|
48
|
+
return {
|
|
49
|
+
phase: blocker.phase,
|
|
50
|
+
topicKey: blocker.topicKey,
|
|
51
|
+
reason: blocker.reason,
|
|
52
|
+
action: blocker.action,
|
|
53
|
+
command: blockerCommand(project, change, blocker, dbFlag),
|
|
54
|
+
...(draftRunCommand === undefined ? {} : { draftRunCommand, warning: draftRunWarning() }),
|
|
55
|
+
...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function blockerCommand(project, change, blocker, dbFlag) {
|
|
59
|
+
if (blocker.reason === 'missing')
|
|
60
|
+
return runPhaseCommand(project, change, blocker.phase, dbFlag);
|
|
61
|
+
if (blocker.reason === 'draft' || blocker.reason === 'legacy')
|
|
62
|
+
return `vgxness sdd accept-artifact --project ${project} --change ${change} --phase ${blocker.phase} --actor <human-id>${dbFlag}`;
|
|
63
|
+
if (blocker.reason === 'rejected')
|
|
64
|
+
return `vgxness sdd reopen-artifact --project ${project} --change ${change} --phase ${blocker.phase} --actor <human-id>${dbFlag}`;
|
|
65
|
+
return `vgxness sdd get-artifact --project ${project} --change ${change} --phase ${blocker.phase} --json${dbFlag}`;
|
|
66
|
+
}
|
|
67
|
+
function runPhaseCommand(project, change, phase, dbFlag) {
|
|
68
|
+
return `vgxness code sdd ${change} ${phase} --project ${project} --save-artifact${dbFlag}`;
|
|
69
|
+
}
|
|
70
|
+
function draftRunPhaseCommand(project, change, phase, dbFlag) {
|
|
71
|
+
return `vgxness code sdd ${change} ${phase} --project ${project} --draft-run --save-artifact${dbFlag}`;
|
|
72
|
+
}
|
|
73
|
+
function isDraftRunPlanningPhase(phase) {
|
|
74
|
+
return phase === 'explore' || phase === 'proposal' || phase === 'spec' || phase === 'design' || phase === 'tasks';
|
|
75
|
+
}
|
|
76
|
+
function draftRunWarning() {
|
|
77
|
+
return 'Draft-run is planning-only; human acceptance is still required; apply-progress remains gated.';
|
|
78
|
+
}
|
|
79
|
+
function continuationDbFlag(explicitDatabasePath) {
|
|
80
|
+
return explicitDatabasePath === undefined ? '' : ` --db ${explicitDatabasePath}`;
|
|
81
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { summarizePayloadContent } from '../payload/payload-summary.js';
|
|
2
|
-
import {
|
|
2
|
+
import { normalizeSddArtifact, normalizeSddPhaseInput, sddPhases, sddPrerequisites, sddTopicKey, } from './schema.js';
|
|
3
3
|
const defaultContext = { actor: 'sdd-workflow-service' };
|
|
4
4
|
const validChangePattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
5
|
function readAutoAdvanceFromEnv() {
|
|
@@ -103,7 +103,7 @@ export class SddWorkflowService {
|
|
|
103
103
|
const artifacts = cockpitPhases.map((phase) => phase.artifact).filter((artifact) => artifact !== undefined);
|
|
104
104
|
const aggregateBlockers = dedupeCockpitBlockers([
|
|
105
105
|
...cockpitPhases.flatMap((phase) => phase.blockers),
|
|
106
|
-
...next.
|
|
106
|
+
...(next.missingPrerequisiteTopicKeys ?? []).map((topicKey) => blockerFromTopicKey(validated.value.change, topicKey)),
|
|
107
107
|
...(next.blockedPrerequisites ?? []).map((blocker) => ({
|
|
108
108
|
kind: 'readiness',
|
|
109
109
|
phase: blocker.phase,
|
|
@@ -314,9 +314,10 @@ export class SddWorkflowService {
|
|
|
314
314
|
const validated = validateProjectAndChange(input.project, input.change);
|
|
315
315
|
if (!validated.ok)
|
|
316
316
|
return validated;
|
|
317
|
-
|
|
317
|
+
const phase = normalizeSddPhaseInput(input.phase);
|
|
318
|
+
if (phase === undefined)
|
|
318
319
|
return validationFailure(`Unknown SDD phase: ${input.phase}`);
|
|
319
|
-
return { ok: true, value: { ...validated.value, phase
|
|
320
|
+
return { ok: true, value: { ...validated.value, phase } };
|
|
320
321
|
}
|
|
321
322
|
getPhaseStatuses(project, change) {
|
|
322
323
|
const snapshot = this.loadPhaseSnapshot(project, change);
|
|
@@ -367,9 +368,11 @@ export function nextDecisionFromStatuses(change, phases, options = {}) {
|
|
|
367
368
|
status: 'blocked',
|
|
368
369
|
nextPhase: blockedPresentPhase.phase,
|
|
369
370
|
missingArtifactTopicKeys: readiness.missingArtifactTopicKeys,
|
|
371
|
+
missingPrerequisiteTopicKeys: readiness.missingPrerequisiteTopicKeys ?? [],
|
|
370
372
|
blockedPrerequisites: blockers,
|
|
373
|
+
blockerGuidance: readiness.blockerGuidance ?? [],
|
|
371
374
|
reason: `${blockedPresentPhase.phase} is blocked by prerequisite artifacts that are not accepted: ${formatBlockers(blockers)}.`,
|
|
372
|
-
recommendedAction:
|
|
375
|
+
recommendedAction: blockedRecommendedAction(readiness, 'continuing'),
|
|
373
376
|
};
|
|
374
377
|
}
|
|
375
378
|
const nextMissingPhase = phases.find((status) => !status.present)?.phase;
|
|
@@ -381,6 +384,7 @@ export function nextDecisionFromStatuses(change, phases, options = {}) {
|
|
|
381
384
|
status: 'blocked',
|
|
382
385
|
nextPhase: firstUnacceptedPhase.phase,
|
|
383
386
|
missingArtifactTopicKeys: [firstUnacceptedPhase.topicKey],
|
|
387
|
+
missingPrerequisiteTopicKeys: [],
|
|
384
388
|
blockedPrerequisites: [
|
|
385
389
|
{
|
|
386
390
|
phase: firstUnacceptedPhase.phase,
|
|
@@ -389,8 +393,9 @@ export function nextDecisionFromStatuses(change, phases, options = {}) {
|
|
|
389
393
|
...(firstUnacceptedPhase.artifactId === undefined ? {} : { artifactId: firstUnacceptedPhase.artifactId }),
|
|
390
394
|
},
|
|
391
395
|
],
|
|
396
|
+
blockerGuidance: [blockerGuidanceForPhaseStatus(firstUnacceptedPhase)],
|
|
392
397
|
reason: `${firstUnacceptedPhase.phase} is present but ${firstUnacceptedPhase.state ?? 'draft'}; accepted governance status is required before the change is complete.`,
|
|
393
|
-
recommendedAction:
|
|
398
|
+
recommendedAction: `${blockerActionForStatus(firstUnacceptedPhase)} before archiving this SDD change.`,
|
|
394
399
|
};
|
|
395
400
|
}
|
|
396
401
|
return {
|
|
@@ -410,9 +415,11 @@ export function nextDecisionFromStatuses(change, phases, options = {}) {
|
|
|
410
415
|
status: 'blocked',
|
|
411
416
|
nextPhase: nextMissingPhase,
|
|
412
417
|
missingArtifactTopicKeys: readiness.missingArtifactTopicKeys,
|
|
418
|
+
missingPrerequisiteTopicKeys: readiness.missingPrerequisiteTopicKeys ?? [],
|
|
413
419
|
blockedPrerequisites: blockers,
|
|
420
|
+
blockerGuidance: readiness.blockerGuidance ?? [],
|
|
414
421
|
reason: `${nextMissingPhase} is blocked by prerequisite artifacts that are not accepted: ${formatBlockers(blockers)}.`,
|
|
415
|
-
recommendedAction:
|
|
422
|
+
recommendedAction: blockedRecommendedAction(readiness, `running ${nextMissingPhase}`),
|
|
416
423
|
};
|
|
417
424
|
}
|
|
418
425
|
return {
|
|
@@ -439,7 +446,9 @@ function statusFromPhases(change, phases, options = {}) {
|
|
|
439
446
|
function getReadinessFromStatuses(change, phase, phases, options = {}) {
|
|
440
447
|
const satisfiedPrerequisites = [];
|
|
441
448
|
const missingArtifactTopicKeys = [];
|
|
449
|
+
const missingPrerequisiteTopicKeys = [];
|
|
442
450
|
const blockedPrerequisites = [];
|
|
451
|
+
const blockerGuidance = [];
|
|
443
452
|
const useAutoAdvance = options.autoAdvance === true;
|
|
444
453
|
for (const prerequisite of sddPrerequisites[phase]) {
|
|
445
454
|
const status = phases.find((candidate) => candidate.phase === prerequisite);
|
|
@@ -449,19 +458,26 @@ function getReadinessFromStatuses(change, phase, phases, options = {}) {
|
|
|
449
458
|
continue;
|
|
450
459
|
}
|
|
451
460
|
const topicKey = status?.topicKey ?? sddTopicKey(change, prerequisite);
|
|
461
|
+
const reason = status === undefined ? 'missing' : blockerReasonForStatus(status);
|
|
452
462
|
missingArtifactTopicKeys.push(topicKey);
|
|
453
|
-
|
|
463
|
+
if (reason === 'missing')
|
|
464
|
+
missingPrerequisiteTopicKeys.push(topicKey);
|
|
465
|
+
const blocker = {
|
|
454
466
|
phase: prerequisite,
|
|
455
467
|
topicKey,
|
|
456
|
-
reason
|
|
468
|
+
reason,
|
|
457
469
|
...(status?.artifactId === undefined ? {} : { artifactId: status.artifactId }),
|
|
458
|
-
}
|
|
470
|
+
};
|
|
471
|
+
blockedPrerequisites.push(blocker);
|
|
472
|
+
blockerGuidance.push(blockerGuidanceForBlocker(blocker, phase));
|
|
459
473
|
}
|
|
460
474
|
return {
|
|
461
475
|
ready: blockedPrerequisites.length === 0,
|
|
462
476
|
satisfiedPrerequisites,
|
|
463
477
|
missingArtifactTopicKeys,
|
|
478
|
+
missingPrerequisiteTopicKeys,
|
|
464
479
|
blockedPrerequisites,
|
|
480
|
+
blockerGuidance,
|
|
465
481
|
};
|
|
466
482
|
}
|
|
467
483
|
function compactArtifactProjection(artifact, change, phase, readiness) {
|
|
@@ -518,13 +534,20 @@ function compactGovernanceArtifact(artifact) {
|
|
|
518
534
|
function cockpitBlockersForPhase(status, readiness) {
|
|
519
535
|
const blockers = [];
|
|
520
536
|
if (!status.present)
|
|
521
|
-
blockers.push({
|
|
537
|
+
blockers.push({
|
|
538
|
+
kind: 'missing-topic-key',
|
|
539
|
+
phase: status.phase,
|
|
540
|
+
topicKey: status.topicKey,
|
|
541
|
+
reason: 'Canonical SDD artifact is missing.',
|
|
542
|
+
action: blockerActionForBlocker({ phase: status.phase, topicKey: status.topicKey, reason: 'missing' }),
|
|
543
|
+
});
|
|
522
544
|
if (status.present && status.accepted !== true)
|
|
523
545
|
blockers.push({
|
|
524
546
|
kind: 'unaccepted-phase',
|
|
525
547
|
phase: status.phase,
|
|
526
548
|
topicKey: status.topicKey,
|
|
527
549
|
reason: `${status.phase} is ${status.state ?? 'draft'}; explicit human acceptance is required.`,
|
|
550
|
+
action: blockerActionForStatus(status),
|
|
528
551
|
...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }),
|
|
529
552
|
});
|
|
530
553
|
if (status.legacy === true)
|
|
@@ -533,6 +556,7 @@ function cockpitBlockersForPhase(status, readiness) {
|
|
|
533
556
|
phase: status.phase,
|
|
534
557
|
topicKey: status.topicKey,
|
|
535
558
|
reason: 'Artifact has legacy or missing governance metadata and is treated as not accepted.',
|
|
559
|
+
action: blockerActionForStatus(status),
|
|
536
560
|
...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }),
|
|
537
561
|
});
|
|
538
562
|
for (const blocker of readiness.blockedPrerequisites ?? []) {
|
|
@@ -541,6 +565,7 @@ function cockpitBlockersForPhase(status, readiness) {
|
|
|
541
565
|
phase: blocker.phase,
|
|
542
566
|
topicKey: blocker.topicKey,
|
|
543
567
|
reason: blocker.reason,
|
|
568
|
+
action: blockerActionForBlocker(blocker),
|
|
544
569
|
...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
|
|
545
570
|
});
|
|
546
571
|
}
|
|
@@ -548,7 +573,13 @@ function cockpitBlockersForPhase(status, readiness) {
|
|
|
548
573
|
}
|
|
549
574
|
function blockerFromTopicKey(change, topicKey) {
|
|
550
575
|
const phase = phaseFromTopicKey(change, topicKey);
|
|
551
|
-
return {
|
|
576
|
+
return {
|
|
577
|
+
kind: 'missing-topic-key',
|
|
578
|
+
phase,
|
|
579
|
+
topicKey,
|
|
580
|
+
reason: 'Required SDD topic key is missing or not accepted.',
|
|
581
|
+
action: blockerActionForBlocker({ phase, topicKey, reason: 'missing' }),
|
|
582
|
+
};
|
|
552
583
|
}
|
|
553
584
|
function dedupeCockpitBlockers(blockers) {
|
|
554
585
|
const seen = new Set();
|
|
@@ -567,16 +598,72 @@ function cockpitRecommendedAction(next, blockers) {
|
|
|
567
598
|
return next.recommendedAction;
|
|
568
599
|
const legacy = blockers.find((blocker) => blocker.kind === 'legacy-artifact');
|
|
569
600
|
if (legacy !== undefined)
|
|
570
|
-
return `
|
|
601
|
+
return legacy.action ?? `Inspect and explicitly accept or replace legacy artifact ${legacy.topicKey}.`;
|
|
571
602
|
const unaccepted = blockers.find((blocker) => blocker.kind === 'unaccepted-phase');
|
|
572
603
|
if (unaccepted !== undefined)
|
|
573
|
-
return `Accept or replace ${unaccepted.topicKey} before continuing.`;
|
|
604
|
+
return unaccepted.action ?? `Accept or replace ${unaccepted.topicKey} before continuing.`;
|
|
574
605
|
return next.recommendedAction;
|
|
575
606
|
}
|
|
607
|
+
function blockedRecommendedAction(readiness, context) {
|
|
608
|
+
const firstGuidance = readiness.blockerGuidance?.[0];
|
|
609
|
+
if (firstGuidance !== undefined)
|
|
610
|
+
return `${firstGuidance.action} before ${context}.`;
|
|
611
|
+
const firstBlocker = readiness.blockedPrerequisites?.[0];
|
|
612
|
+
if (firstBlocker !== undefined)
|
|
613
|
+
return `${blockerActionForBlocker(firstBlocker)} before ${context}.`;
|
|
614
|
+
return `Resolve prerequisite blockers before ${context}.`;
|
|
615
|
+
}
|
|
616
|
+
function blockerGuidanceForPhaseStatus(status) {
|
|
617
|
+
return blockerGuidanceForBlocker({
|
|
618
|
+
phase: status.phase,
|
|
619
|
+
topicKey: status.topicKey,
|
|
620
|
+
reason: blockerReasonForStatus(status),
|
|
621
|
+
...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
function blockerGuidanceForBlocker(blocker, blockedPhase) {
|
|
625
|
+
return {
|
|
626
|
+
phase: blocker.phase,
|
|
627
|
+
topicKey: blocker.topicKey,
|
|
628
|
+
reason: blocker.reason,
|
|
629
|
+
action: blockerActionForBlocker(blocker, blockedPhase),
|
|
630
|
+
...(blocker.artifactId === undefined ? {} : { artifactId: blocker.artifactId }),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function blockerActionForStatus(status) {
|
|
634
|
+
return blockerActionForBlocker({
|
|
635
|
+
phase: status.phase,
|
|
636
|
+
topicKey: status.topicKey,
|
|
637
|
+
reason: blockerReasonForStatus(status),
|
|
638
|
+
...(status.artifactId === undefined ? {} : { artifactId: status.artifactId }),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function blockerActionForBlocker(blocker, blockedPhase) {
|
|
642
|
+
switch (blocker.reason) {
|
|
643
|
+
case 'missing':
|
|
644
|
+
return `Run or create the ${blocker.phase} SDD phase artifact at ${blocker.topicKey}`;
|
|
645
|
+
case 'draft':
|
|
646
|
+
if (blockedPhase !== undefined && isDraftRunPlanningPhase(blockedPhase))
|
|
647
|
+
return `Accept ${blocker.topicKey} if it is ready, or intentionally continue planning ${blockedPhase} with --draft-run; draft-run is planning-only, human acceptance is still required, and apply-progress remains gated`;
|
|
648
|
+
return `Accept ${blocker.topicKey}, or continue and re-run the ${blocker.phase} phase if the draft is incomplete`;
|
|
649
|
+
case 'accepted':
|
|
650
|
+
return `Inspect ${blocker.topicKey}; it is marked accepted but still appears in blocker data`;
|
|
651
|
+
case 'legacy':
|
|
652
|
+
return `Inspect ${blocker.topicKey} and explicitly accept or replace the legacy artifact`;
|
|
653
|
+
case 'rejected':
|
|
654
|
+
return `Reopen ${blocker.topicKey} for revision, or replace it with a new accepted artifact`;
|
|
655
|
+
case 'superseded':
|
|
656
|
+
return `Use the current replacement for ${blocker.topicKey}, or create and accept a new canonical ${blocker.phase} artifact`;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function isDraftRunPlanningPhase(phase) {
|
|
660
|
+
return phase === 'explore' || phase === 'proposal' || phase === 'spec' || phase === 'design' || phase === 'tasks';
|
|
661
|
+
}
|
|
576
662
|
function phaseFromTopicKey(change, topicKey) {
|
|
577
663
|
const suffix = topicKey.slice(`sdd/${change}/`.length);
|
|
578
|
-
|
|
579
|
-
|
|
664
|
+
const phase = normalizeSddPhaseInput(suffix);
|
|
665
|
+
if (phase !== undefined)
|
|
666
|
+
return phase;
|
|
580
667
|
return 'explore';
|
|
581
668
|
}
|
|
582
669
|
function blockerReasonForStatus(status) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeSddPhaseInput } from '../sdd/schema.js';
|
|
1
2
|
export class SkillResolver {
|
|
2
3
|
skills;
|
|
3
4
|
agents;
|
|
@@ -113,10 +114,13 @@ export class SkillResolver {
|
|
|
113
114
|
targets.push({ targetType: agent.mode, targetKey: agent.name });
|
|
114
115
|
targets.push({ targetType: agent.mode, targetKey: `${agent.project}/${agent.scope}/${agent.name}` });
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const phases = phaseTargetKeys(input.workflow, input.phase);
|
|
118
|
+
if (input.workflow !== undefined) {
|
|
119
|
+
for (const phase of phases)
|
|
120
|
+
targets.push({ targetType: 'workflow-phase', targetKey: `${input.workflow}:${phase}` });
|
|
121
|
+
}
|
|
122
|
+
for (const phase of phases)
|
|
123
|
+
targets.push({ targetType: 'workflow-phase', targetKey: phase });
|
|
120
124
|
if (input.providerAdapter !== undefined)
|
|
121
125
|
targets.push({ targetType: 'provider-adapter', targetKey: input.providerAdapter });
|
|
122
126
|
return dedupeTargets(targets);
|
|
@@ -212,6 +216,19 @@ function dedupeTargets(targets) {
|
|
|
212
216
|
return true;
|
|
213
217
|
});
|
|
214
218
|
}
|
|
219
|
+
function phaseTargetKeys(workflow, phase) {
|
|
220
|
+
if (phase === undefined)
|
|
221
|
+
return [];
|
|
222
|
+
if (!isSddWorkflow(workflow))
|
|
223
|
+
return [phase];
|
|
224
|
+
const normalized = normalizeSddPhaseInput(phase);
|
|
225
|
+
if (normalized === 'apply-progress')
|
|
226
|
+
return ['apply-progress', 'apply'];
|
|
227
|
+
return normalized === undefined || normalized === phase ? [phase] : [normalized, phase];
|
|
228
|
+
}
|
|
229
|
+
function isSddWorkflow(workflow) {
|
|
230
|
+
return workflow?.trim().toLowerCase() === 'sdd';
|
|
231
|
+
}
|
|
215
232
|
function stringMetadata(value) {
|
|
216
233
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
217
234
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path';
|
|
2
|
+
export function buildProductStatus(input) {
|
|
3
|
+
const project = resolveProject(input);
|
|
4
|
+
const baseSafety = [
|
|
5
|
+
'Read-only cockpit: does not execute providers, edit files, write provider config, or mutate SDD artifacts.',
|
|
6
|
+
'Human acceptance remains explicit; saved artifacts do not imply acceptance.',
|
|
7
|
+
];
|
|
8
|
+
if (input.change === undefined) {
|
|
9
|
+
return {
|
|
10
|
+
version: 1,
|
|
11
|
+
kind: 'product-status',
|
|
12
|
+
project,
|
|
13
|
+
status: [`Project: ${project.value} (${project.source === 'flag' ? 'from --project' : 'inferred from cwd'})`, 'Change: not selected'],
|
|
14
|
+
blockers: ['No --change was provided, so VGXNESS cannot inspect SDD phase state yet.'],
|
|
15
|
+
next: ['Choose the active change id, then re-run status for the SDD cockpit view.'],
|
|
16
|
+
command: `vgxness status --project ${project.value} --change <change>`,
|
|
17
|
+
safety: ['Did not open the local memory store because no change was selected.', ...baseSafety],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (input.databaseError !== undefined) {
|
|
21
|
+
return blockedStatus({
|
|
22
|
+
project,
|
|
23
|
+
change: input.change,
|
|
24
|
+
blocker: `Unable to read local memory store${input.databasePath === undefined ? '' : ` at ${input.databasePath}`}: ${input.databaseError}`,
|
|
25
|
+
next: 'Check --db, VGXNESS_DB_PATH, or run setup/status commands with the installed Bun runtime.',
|
|
26
|
+
command: `vgxness status --project ${project.value} --change ${input.change} --db <path>`,
|
|
27
|
+
safety: baseSafety,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (input.sdd === undefined) {
|
|
31
|
+
return blockedStatus({
|
|
32
|
+
project,
|
|
33
|
+
change: input.change,
|
|
34
|
+
blocker: 'SDD cockpit service is not available for this status request.',
|
|
35
|
+
next: 'Use the installed CLI or provide a readable local memory store.',
|
|
36
|
+
command: `vgxness sdd cockpit --project ${project.value} --change ${input.change}`,
|
|
37
|
+
safety: baseSafety,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const cockpit = input.sdd.getCockpit({ project: project.value, change: input.change });
|
|
41
|
+
if (!cockpit.ok) {
|
|
42
|
+
return blockedStatus({
|
|
43
|
+
project,
|
|
44
|
+
change: input.change,
|
|
45
|
+
blocker: cockpit.error.message,
|
|
46
|
+
next: 'Review the project/change flags and local memory store, then re-run status.',
|
|
47
|
+
command: `vgxness sdd cockpit --project ${project.value} --change ${input.change}`,
|
|
48
|
+
safety: baseSafety,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const relatedRunContext = findRelatedRunContext({
|
|
52
|
+
project: project.value,
|
|
53
|
+
change: input.change,
|
|
54
|
+
...(input.runs === undefined ? {} : { runs: input.runs }),
|
|
55
|
+
...(input.explicitDatabasePath === undefined ? {} : { explicitDatabasePath: input.explicitDatabasePath }),
|
|
56
|
+
});
|
|
57
|
+
return fromSddCockpit(cockpit.value, project, baseSafety, relatedRunContext);
|
|
58
|
+
}
|
|
59
|
+
function resolveProject(input) {
|
|
60
|
+
if (input.project !== undefined && input.project.trim().length > 0)
|
|
61
|
+
return { value: input.project.trim(), source: 'flag' };
|
|
62
|
+
const directoryName = basename(resolve(input.cwd));
|
|
63
|
+
return { value: directoryName.length > 0 ? directoryName : 'unknown-project', source: 'cwd' };
|
|
64
|
+
}
|
|
65
|
+
function fromSddCockpit(cockpit, project, baseSafety, relatedRunContext) {
|
|
66
|
+
const nextLabel = cockpit.next.nextPhase === undefined ? cockpit.next.status : `${cockpit.next.status} / ${cockpit.next.nextPhase}`;
|
|
67
|
+
const command = cockpit.next.status === 'runnable' && cockpit.next.nextPhase !== undefined
|
|
68
|
+
? `vgxness code sdd ${cockpit.change} ${cockpit.next.nextPhase} --project ${cockpit.project} --save-artifact`
|
|
69
|
+
: `vgxness sdd cockpit --project ${cockpit.project} --change ${cockpit.change} --json`;
|
|
70
|
+
return {
|
|
71
|
+
version: 1,
|
|
72
|
+
kind: 'product-status',
|
|
73
|
+
project,
|
|
74
|
+
change: cockpit.change,
|
|
75
|
+
status: [
|
|
76
|
+
`Project: ${cockpit.project}`,
|
|
77
|
+
`Change: ${cockpit.change}`,
|
|
78
|
+
`Next: ${nextLabel}`,
|
|
79
|
+
`Accepted: ${cockpit.acceptedCount}/${cockpit.phases.length}`,
|
|
80
|
+
`Legacy artifacts: ${cockpit.legacyCount}`,
|
|
81
|
+
],
|
|
82
|
+
blockers: cockpit.aggregateBlockers.length === 0
|
|
83
|
+
? ['none']
|
|
84
|
+
: cockpit.aggregateBlockers.map((blocker) => `${blocker.phase}: ${blocker.reason} at ${blocker.topicKey}${blocker.action === undefined ? '' : `; action=${blocker.action}`}`),
|
|
85
|
+
next: [
|
|
86
|
+
cockpit.next.status === 'runnable' && cockpit.next.nextPhase !== undefined
|
|
87
|
+
? `Continue the ${cockpit.next.nextPhase} phase in OpenCode using VGXNESS MCP and hidden SDD subagents.`
|
|
88
|
+
: cockpit.recommendedAction,
|
|
89
|
+
],
|
|
90
|
+
command,
|
|
91
|
+
sddNextStatus: cockpit.next.status,
|
|
92
|
+
...(relatedRunContext === undefined ? {} : { relatedRunContext }),
|
|
93
|
+
safety: baseSafety,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function findRelatedRunContext(input) {
|
|
97
|
+
if (input.runs === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const relatedRun = input.runs.findRelatedInterruptedSddRun({ project: input.project, change: input.change });
|
|
100
|
+
if (!relatedRun.ok || relatedRun.value === undefined)
|
|
101
|
+
return undefined;
|
|
102
|
+
const dbFlag = input.explicitDatabasePath === undefined ? '' : ` --db ${input.explicitDatabasePath}`;
|
|
103
|
+
return {
|
|
104
|
+
...relatedRun.value,
|
|
105
|
+
recommendation: 'Related interrupted run found by checkpoint changeId; inspect or resume it before starting duplicate SDD work. Advisory only.',
|
|
106
|
+
resumeCommand: `vgxness resume --project ${input.project} --run-id ${relatedRun.value.runId}${dbFlag}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function blockedStatus(input) {
|
|
110
|
+
return {
|
|
111
|
+
version: 1,
|
|
112
|
+
kind: 'product-status',
|
|
113
|
+
project: input.project,
|
|
114
|
+
change: input.change,
|
|
115
|
+
status: [`Project: ${input.project.value}`, `Change: ${input.change}`, 'Status: blocked'],
|
|
116
|
+
blockers: [input.blocker],
|
|
117
|
+
next: [input.next],
|
|
118
|
+
command: input.command,
|
|
119
|
+
safety: input.safety,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/docs/architecture.md
CHANGED
|
@@ -625,7 +625,7 @@ The code runtime layers a second, finer-grained decision on top of the policy ev
|
|
|
625
625
|
|
|
626
626
|
CLI surface groups are documented in [CLI reference](./cli.md). The plural form is canonical — singular shortcuts are not added.
|
|
627
627
|
|
|
628
|
-
MCP tools mirror the same core services for agent use. The full, current list of
|
|
628
|
+
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 and TUI are human/operator control surfaces. MCP is the agent-facing control surface. Provider integrations and the code runtime sit on the execution plane.
|
|
629
629
|
|
|
630
630
|
## Evaluation strategy
|
|
631
631
|
|