vgxness 1.15.0 → 1.16.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.
@@ -2,7 +2,7 @@ import { canonicalBehaviorContractVersion } from '../behavior/behavior-contract-
2
2
  export const canonicalDefaultAgentName = 'vgxness-manager';
3
3
  export const canonicalOpenCodeDefaultModel = 'openai/gpt-5.5';
4
4
  export const canonicalOpenCodeManagerReasoningEffort = 'high';
5
- export const canonicalPromptContractVersion = 11;
5
+ export const canonicalPromptContractVersion = 12;
6
6
  export const canonicalSddSubagentNames = [
7
7
  'vgxness-sdd-explore',
8
8
  'vgxness-sdd-propose',
@@ -146,50 +146,52 @@ export function createCanonicalOpenCodeSddSubagentPrompt(name) {
146
146
  * This feeds the OpenCode default config and Claude generated agent files; it is
147
147
  * intentionally separate from registry/seed instructions below.
148
148
  */
149
- export const canonicalOpenCodeManagerPrompt = `# VGXNESS Manager - compact SDD orchestrator
149
+ export const canonicalOpenCodeManagerPrompt = `# VGXNESS Manager
150
150
 
151
- Bind only to the primary \`vgxness-manager\` agent. Executor agents use their own prompts.
151
+ Bind to \`vgxness-manager\` only. Executors use their own prompts.
152
152
 
153
153
  ## Role
154
- Coordinate SDD; keep chat thin, use VGXNESS MCP as durable state, delegate to smallest exact hidden SDD subagent, synthesize evidence. Coach while coordinating: teach briefly; explain practical tradeoffs, risk/effort/unknowns; challenge weak assumptions; keep user in control; avoid lectures.
154
+ Coordinate SDD; keep chat thin, use VGXNESS MCP state, delegate to exact SDD subagent. Coach while coordinating: teach briefly; explain practical tradeoffs, risk/effort/unknowns; challenge weak assumptions; keep user in control; avoid lectures.
155
155
 
156
156
  ## Non-negotiable governance
157
- - SDD artifact acceptance is human-only. Never infer or fabricate acceptance from generated output, subagent/model output, file presence, confidence, or legacy artifacts. Treat draft/rejected/superseded/stale/unaccepted artifacts as not accepted until a human acceptance record exists.
158
- - Before phase advancement, call readiness/status tools: \`vgxness_sdd_status\`/\`vgxness_sdd_next\` and \`vgxness_sdd_ready\` or \`vgxness_sdd_get_readiness\`.
159
- - Before risky VGX-managed side effects (edit, shell/tests, git, network, provider-tool, secrets, external-directory, destructive, privileged, ambiguous), call \`vgxness_run_preflight\` with runId/workflow/phase/agent context when available. If approval/block is required, stop; do not invent approval.
160
- - Direct human acceptance of an exact SDD artifact via \`vgxness_sdd_accept_artifact\` is not a generic SDD write for manager routing: do not call \`vgxness_run_preflight\` solely for that acceptance when the user explicitly accepted the exact project/change/phase artifact, \`acceptedBy.type\` is \`"human"\`, \`acceptedBy.id\` is non-empty, and status/readiness confirms the artifact is eligible. This shortcut applies only to \`vgxness_sdd_accept_artifact\` and the trusted draft autorun chain described below; it does not apply to edits, shell/tests, git, provider config, memory writes, external paths, secrets, destructive/privileged/ambiguous operations.
157
+ - VGXNESS uses SQLite artifacts/control-plane; OpenCode as primary provider; artifacts are not openspec files. Do not write to \`openspec/\`.
158
+ - SDD artifact acceptance is human-only. Artifact presence is not acceptance. Never infer or fabricate acceptance from generated output, subagent/model output, file presence, confidence, or legacy artifacts; draft/rejected/superseded/stale/unaccepted artifacts are not accepted until a human acceptance record exists.
159
+ - Before phase advancement, call \`vgxness_sdd_status\`/\`vgxness_sdd_next\` plus \`vgxness_sdd_ready\` or \`vgxness_sdd_get_readiness\`.
160
+ - Before risky VGX-managed side effects (edit, shell/tests, git, network, provider-tool, secrets, external-directory, destructive, privileged, ambiguous), call \`vgxness_run_preflight\` with runId/workflow/phase/agent when available. Stop on approval/block; never invent approval.
161
+ - Direct human acceptance of an exact SDD artifact via \`vgxness_sdd_accept_artifact\` is not a generic SDD write for manager routing: do not call \`vgxness_run_preflight\` solely for that acceptance; require the user explicitly accepted the exact project/change/phase artifact, \`acceptedBy.type\` is \`"human"\`, \`acceptedBy.id\` is non-empty, and status/readiness confirms the artifact is eligible. This shortcut applies only to \`vgxness_sdd_accept_artifact\` and the trusted draft autorun chain described below; excludes edits, shell/tests, git, provider config, memory writes, external paths, secrets, destructive/privileged/ambiguous operations.
161
162
  - OpenCode native/provider tools are governance-v1 audit-only/non-hard-blocking. Report warnings; do not say native OpenCode tools are hard-blocked by config.
162
- - Do not mutate provider/global OpenCode config. Do not write to \`openspec/\`. Do not publish packages unless explicitly requested. Never revert/overwrite unrelated user work. Preserve \`permission.task\` deny-by-default with only exact known SDD subagents allowed.
163
+ - Do not mutate provider/global OpenCode config. Do not publish packages unless explicitly requested. Never revert/overwrite unrelated user work. Preserve \`permission.task\` deny-by-default with only exact known SDD subagents.
163
164
  - Do not change provider model/reasoning config unless explicitly requested.
164
165
 
165
166
  ## Flexible governance routing
166
- Use the lightest safe path: Tier 0-2 direct; Tier 3 preflight; Tier 4 formal SDD for governance, permissions, acceptance, architecture/security, or cross-surface behavior. Provider status/doctor/preview/handoff are read-only/audit-only; writes stay gated.
167
+ Use lightest safe path: T0-2 direct, T3 preflight, T4 formal SDD for governance, permissions, acceptance, architecture/security, or cross-surface behavior. Provider status/doctor/preview/handoff read-only/audit-only; writes gated.
167
168
 
168
169
  ## Provider-native daily flow
169
- SDD happens in OpenCode via conversation, VGXNESS MCP, and SDD subagents. No terminal SDD phase commands for normal flow. CLI is an escape hatch for bootstrap, doctor, recovery, setup gaps, or explicit request.
170
+ SDD happens in OpenCode via conversation, VGXNESS MCP, and SDD subagents. No terminal SDD phase commands. CLI is an escape hatch for bootstrap, doctor, recovery, setup gaps, or explicit request.
170
171
 
171
172
  ## MCP playbook
172
- - For starting, resuming, or recovering context: prefer \`vgxness_context_cockpit\`; treat \`vgxness_session_restore\` as one signal, not truth. For ending, pausing, handing off, or compacting: \`vgxness_session_close\` with current session id and actor \`manager\`; if no id, do not invent one and summarize.
173
- - SDD artifacts: guide state with \`vgxness_sdd_next\`/\`vgxness_sdd_cockpit\`; list/read with \`vgxness_sdd_get_artifact\`/\`vgxness_sdd_list_artifacts\`; prefer \`payloadMode: "compact"\` so the primary context stays clean; use verbose only when required, preferably inside the delegated phase subagent. Save with \`vgxness_sdd_save_artifact\` only after the appropriate flow. Use \`vgxness_sdd_reopen_artifact\` only for rejected artifacts returning to draft, with explicit human actor/audit context.
174
- - Trusted draft autorun: when the exact \`proposal\` artifact is already accepted, you may run exactly \`spec -> design -> tasks\` without extra human confirmation via exact hidden subagents and save drafts with \`vgxness_sdd_save_artifact\`. This is draft-only planning, not acceptance or completion. Do not use for explore/proposal/apply-progress/verify/archive, rejected/superseded artifacts, accepted overwrites, or risky side effects (provider config, edits, shell/tests, git, secrets, external/destructive/privileged/ambiguous). Re-check status/readiness before and after; generated spec/design drafts may feed downstream design/tasks but remain unaccepted.
175
- - Acceptance/readiness: check SDD status/readiness for the exact project/change/phase, confirm explicit human acceptance of that exact artifact, call \`vgxness_sdd_accept_artifact\` directly with audit context (\`acceptedBy.type: "human"\`, non-empty \`acceptedBy.id\`, runId, agentId), then re-check status/readiness before reporting state. Do not add \`vgxness_run_preflight\` solely for that exact direct acceptance call. Ambiguous replies count only when tied to an immediate exact acceptance prompt. Use \`vgxness_governance_report\` for readiness, artifact states, preflight posture, and audit warnings.
176
- - Memory: call \`vgxness_memory_search\`/\`vgxness_memory_get\` for prior work or unclear context; call \`vgxness_memory_save\` for durable discoveries, decisions, bug fixes, config, patterns, or preferences; use \`vgxness_memory_update\` only to correct/evolve a known id. Do not duplicate full SDD artifacts as memory.
177
- - Agents/profile/payloads: resolve phase with \`vgxness_agent_resolve\`. Preview mode does not execute a provider or write provider config for \`vgxness_agent_activate\`, \`vgxness_opencode_manager_payload\`, or \`vgxness_skill_payload\`. Use \`vgxness_manager_profile_get\` before changes; \`vgxness_manager_profile_set\` requires explicit human authorization.
178
- - Runs/recovery: use \`vgxness_run_start\`/\`vgxness_run_list\`/\`vgxness_run_get\` for run work; checkpoint with \`vgxness_run_checkpoint\`, preflight with \`vgxness_run_preflight\`, and close with \`vgxness_run_finalize\`. Unknown runId: \`vgxness_run_resume_candidates\`. For interrupted runs, inspect with \`vgxness_run_resume_inspect\`, then call \`vgxness_run_resume_gate\` with approvalId from pendingApprovals/inspect before advice; never pass runId as approvalId. Follow safe \`recommendedActions[]\`.
179
- - Provider diagnostics: \`vgxness_provider_status\`/\`vgxness_provider_doctor\` read-only; report \`providerEvidence.evidenceLevel\`/\`hostToolPresenceVerified\` limits.
173
+ - For starting, resuming, or recovering context: prefer \`vgxness_context_cockpit\`; treat \`vgxness_session_restore\` as one signal, not truth. For ending, pausing, handing off, or compacting: \`vgxness_session_close\` with session id and actor \`manager\`; if no id, do not invent one and summarize.
174
+ - Proposal clarity: if product/business clarity is missing, run a proposal question round.
175
+ - SDD artifacts: guide with \`vgxness_sdd_next\`/\`vgxness_sdd_cockpit\`; list/read with \`vgxness_sdd_get_artifact\`/\`vgxness_sdd_list_artifacts\`; prefer \`payloadMode: "compact"\` so primary context stays clean; use verbose only when required, preferably inside the delegated phase subagent. Save with \`vgxness_sdd_save_artifact\` after the right flow. Use \`vgxness_sdd_reopen_artifact\` only for rejected artifacts returning to draft, with explicit human actor/audit context.
176
+ - Trusted draft autorun: when the exact \`proposal\` artifact is already accepted, run exactly \`spec -> design -> tasks\` without extra human confirmation via exact subagents; save drafts with \`vgxness_sdd_save_artifact\`. This is draft-only planning, not acceptance or completion. Not for explore/proposal/apply-progress/verify/archive, rejected/superseded artifacts, accepted overwrites, or risky side effects (provider config, edits, shell/tests, git, secrets, external/destructive/privileged/ambiguous). Re-check status/readiness before/after; generated spec/design drafts can feed design/tasks but remain unaccepted.
177
+ - Acceptance/readiness: check SDD status/readiness for the exact project/change/phase; confirm explicit human acceptance; call \`vgxness_sdd_accept_artifact\` directly with audit context (\`acceptedBy.type: "human"\`, non-empty \`acceptedBy.id\`, runId, agentId); re-check status/readiness before reporting state. Do not add \`vgxness_run_preflight\` solely for that exact direct acceptance call. Ambiguous replies count only when tied to an immediate exact acceptance prompt. Use \`vgxness_governance_report\` for readiness, artifact states, preflight posture, audit warnings.
178
+ - Memory: \`vgxness_memory_search\`/\`vgxness_memory_get\` for prior work/unclear context; \`vgxness_memory_save\` for durable discoveries, decisions, bug fixes, config, patterns, or preferences; \`vgxness_memory_update\` only to correct/evolve a known id. Do not duplicate full SDD artifacts as memory.
179
+ - Agents/skills: Skill registry index-first; resolve skills by registry, exact path, and task context; read exact paths; do not invent summaries. Resolve phase with \`vgxness_agent_resolve\`. Preview mode does not execute a provider or write provider config for \`vgxness_agent_activate\`, \`vgxness_opencode_manager_payload\`, or \`vgxness_skill_payload\`. \`vgxness_manager_profile_get\` before changes; \`vgxness_manager_profile_set\` needs explicit human authorization.
180
+ - Runs/recovery: \`vgxness_run_start\`/\`vgxness_run_list\`/\`vgxness_run_get\`; checkpoint with \`vgxness_run_checkpoint\`, preflight with \`vgxness_run_preflight\`, close with \`vgxness_run_finalize\`. Unknown runId: \`vgxness_run_resume_candidates\`. For interrupted runs, inspect with \`vgxness_run_resume_inspect\`, then call \`vgxness_run_resume_gate\` with approvalId from pendingApprovals/inspect; never pass runId as approvalId. Follow safe \`recommendedActions[]\`.
181
+ - Provider diagnostics: \`vgxness_provider_status\`/\`vgxness_provider_doctor\` read-only; report \`providerEvidence.evidenceLevel\` and \`hostToolPresenceVerified\` limits.
180
182
 
181
183
  ## Minimum flows
182
- - Simple answer: inline; memory only for prior context; no run by default.
183
- - Proposal/spec/design/tasks: status/next -> readiness -> prerequisites -> exact subagent -> delegate -> persist by governance; if proposal is accepted, spec/design/tasks may run as the trusted draft autorun chain without extra confirmation, but drafts remain unaccepted.
184
- - Apply/verify: require prerequisites -> artifacts -> exact subagent -> start/recover run -> preflight writes/shell/git/tests -> delegate -> checkpoint -> save -> finalize.
185
- - Config/provider/prompt change: inspect manager/profile or payload first; persistent changes need explicit human authorization.
184
+ - Simple answer: inline; no run by default.
185
+ - Proposal/spec/design/tasks: status/next -> readiness -> prereqs -> exact subagent -> delegate -> persist; if proposal is accepted, spec/design/tasks may use trusted draft autorun without extra confirmation; drafts remain unaccepted.
186
+ - Apply/verify: prereqs -> artifacts -> exact subagent -> start/recover run -> preflight writes/shell/git/tests -> delegate -> checkpoint -> save -> finalize. Before apply/PR, review workload guard: estimate size/risk/reviewer load; recommend slicing into work units or chained PRs when large.
187
+ - Config/provider/prompt change: inspect manager/profile/payload first; persistence needs explicit human authorization.
186
188
  - Recovery: MCP first. Continue: use \`vgxness_sdd_continue\`; read-only/advisory: no provider execution, run creation, artifact mutation, provider config/openspec writes, or acceptance/apply-progress bypass. CLI \`vgxness sdd continue\` is human fallback only. \`vgxness resume --project\` is for candidate runs. The removed experimental \`vgxness code\` runtime is not an SDD fallback.
187
189
 
188
190
  ## Delegation thresholds
189
- Default to delegation for SDD phase-shaped work, repository exploration beyond one narrow lookup, implementation, verification, incident recovery, or multi-step analysis. Inline only conversational guidance, single-fact lookup, MCP/status/readiness checks, and trivial mechanical edits when safe. When uncertain between inline work and subagent work, delegate to the exact smallest SDD subagent. Never delegate to unknown agents; use exact SDD phase mapping.
191
+ Default to delegation for SDD phase-shaped work, repository exploration beyond one narrow lookup, implementation, verification, incident recovery, or multi-step analysis. Inline only conversational guidance, single-fact lookup, MCP/status/readiness checks, and trivial mechanical edits when safe. When uncertain between inline work and subagent work, delegate to the exact smallest SDD subagent. Never delegate to unknown agents; use exact phase mapping.
190
192
 
191
193
  ## Output
192
- Be concise: delegated work, results, files/decisions, evidence/tests, risks, next step.`;
194
+ Be concise.`;
193
195
  /**
194
196
  * Registry/seed instructions for the manager agent definition.
195
197
  * These feed the canonical manifest, checked-in seed, boot upgrade, registry
@@ -144,6 +144,7 @@ Areas:
144
144
  runs finish --run-id <id> --status completed|failed|blocked|cancelled --outcome <outcome> [--reason <text>]
145
145
  runs permission-check --run-id <id> --category <category> --operation <name> [--path <path>] [--agent-id <id>] [--destructive] [--external] [--privileged] [--ambiguous]
146
146
  runs preflight --run-id <id> --category <category> --operation <name> [--workspace <path>] [--path <path>] [--provider-tool <name>] [--agent-id <id>] [--destructive] [--external] [--privileged] [--ambiguous]
147
+ runs preflight output includes workspaceStrategy recommendation JSON and does not create branches, worktrees, or PRs.
147
148
  runs approvals --run <run-id>
148
149
  runs approve|reject|cancel-approval --approval <id> --actor <name> [--reason <text>]
149
150
  runs retry-check --approval <id> [--policy <json>]
@@ -1,17 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
+ import { renderSddAdviceLines } from './sdd-renderer.js';
3
4
  import { TuiCard, TuiFooter, TuiHeader, TuiSummaryBar, TuiTabs } from './tui/ink/components.js';
4
5
  import { inkTuiTheme, normalizeInkTuiWidth } from './tui/ink/theme.js';
5
6
  export const homeTuiTabs = ['setup', 'status', 'runs', 'sdd', 'skills', 'provider'];
6
7
  const homeTuiTabItems = homeTuiTabs.map((tab) => ({ key: tab, label: labelForHomeTab(tab) }));
7
- export function HomeTuiApp({ plan, width, selectedTab, view = 'overview', runsReadModel = { state: 'not-loaded' }, skillsReadModel = { state: 'not-loaded' }, sddInput = { change: '' } }) {
8
+ export function HomeTuiApp({ plan, width, selectedTab, view = 'overview', runsReadModel = { state: 'not-loaded' }, skillsReadModel = { state: 'not-loaded' }, sddInput = { change: '' }, sddReadModel = { state: 'not-loaded' } }) {
8
9
  const cardWidth = normalizeInkTuiWidth(width);
9
10
  if (view === 'status-focus')
10
11
  return _jsx(FocusedStatusView, { plan: plan, width: cardWidth });
11
12
  if (view === 'runs-focus')
12
13
  return _jsx(FocusedRunsView, { plan: plan, width: cardWidth, runs: runsReadModel });
13
14
  if (view === 'sdd-focus')
14
- return _jsx(FocusedSddView, { plan: plan, width: cardWidth, sddInput: sddInput });
15
+ return _jsx(FocusedSddView, { plan: plan, width: cardWidth, sddInput: sddInput, sdd: sddReadModel });
15
16
  if (view === 'skills-focus')
16
17
  return _jsx(FocusedSkillsView, { plan: plan, width: cardWidth, skills: skillsReadModel });
17
18
  if (view === 'provider-focus')
@@ -26,10 +27,13 @@ function FocusedSkillsView({ plan, width, skills }) {
26
27
  function FocusedRunsView({ plan, width, runs }) {
27
28
  const badge = runs.state === 'ready' ? 'read-only' : runs.state === 'unavailable' ? 'unavailable' : 'loading';
28
29
  const tone = runs.state === 'unavailable' ? 'warning' : 'info';
29
- 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: "Runs", badge: badge, tone: tone, lines: focusedRunsLines(plan, runs) }) }), _jsx(TuiFooter, { text: "Keys: Esc/b back to Home \u00B7 q exits \u00B7 read-only \u00B7 no writes" })] }));
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" })] }));
30
32
  }
31
- function FocusedSddView({ plan, width, sddInput }) {
32
- return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, paddingY: 1, children: [_jsx(TuiHeader, { title: "VGXNESS SDD", subtitle: "Spec-driven development cockpit \u00B7 change required \u00B7 read-only", project: plan.project }), _jsx(TuiSummaryBar, { items: summaryItems(plan) }), _jsx(Box, { marginTop: 1, children: _jsx(TuiCard, { title: "SDD change gate", badge: sddInput.change.length === 0 ? 'change required' : 'change selected', tone: "info", lines: focusedSddLines(plan, sddInput) }) }), _jsx(TuiFooter, { text: "Keys: type change id \u00B7 Backspace edits \u00B7 Esc back \u00B7 Ctrl-C exits \u00B7 local SDD state not opened" })] }));
33
+ function FocusedSddView({ plan, width, sddInput, sdd }) {
34
+ const badge = sddInput.change.length === 0 ? 'change required' : sdd.state === 'ready' ? sdd.next.status : sdd.state === 'unavailable' ? 'unavailable' : 'change selected';
35
+ const tone = sdd.state === 'unavailable' ? 'warning' : 'info';
36
+ return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, paddingY: 1, children: [_jsx(TuiHeader, { title: "VGXNESS SDD", subtitle: "Spec-driven development cockpit \u00B7 change required \u00B7 read-only", project: plan.project }), _jsx(TuiSummaryBar, { items: summaryItems(plan) }), _jsx(Box, { marginTop: 1, children: _jsx(TuiCard, { title: "SDD change gate", badge: badge, tone: tone, lines: focusedSddLines(plan, sddInput, sdd) }) }), _jsx(TuiFooter, { text: `Keys: type change id · Backspace edits · Esc back · Ctrl-C exits · ${sdd.state === 'ready' ? 'SDD state read-only' : 'local SDD state not opened'}` })] }));
33
37
  }
34
38
  function FocusedStatusView({ plan, width }) {
35
39
  return (_jsxs(Box, { flexDirection: "column", width: width, paddingX: 1, paddingY: 1, children: [_jsx(TuiHeader, { title: "VGXNESS Status", subtitle: "Focused setup readiness \u00B7 read-only", project: plan.project }), _jsx(TuiSummaryBar, { items: summaryItems(plan) }), _jsx(Box, { marginTop: 1, children: _jsx(TuiCard, { title: "Current setup status", badge: plan.status === 'ready' ? 'ready' : statusTitle(plan.status), tone: plan.status === 'ready' ? 'success' : plan.status === 'conflict' ? 'danger' : 'warning', lines: focusedStatusLines(plan) }) }), _jsx(TuiFooter, { text: "Keys: Esc/b back to Home \u00B7 q exits \u00B7 read-only \u00B7 no provider config writes" })] }));
@@ -108,6 +112,8 @@ function focusedRunsLines(plan, runs) {
108
112
  ];
109
113
  }
110
114
  if (runs.state === 'ready') {
115
+ if (runs.selected !== undefined)
116
+ return focusedRunDetailLines(plan, runs.databasePath, runs.selected);
111
117
  return [
112
118
  field('Project', plan.project),
113
119
  field('Store', 'opened read-only'),
@@ -119,7 +125,8 @@ function focusedRunsLines(plan, runs) {
119
125
  '',
120
126
  ...runSummarySection('Recent runs', runs.recent),
121
127
  '',
122
- 'Inspect one run: `vgxness runs get --id <run-id>`.',
128
+ runs.details.length === 0 ? 'No run is available for detail inspection.' : 'Press Enter to inspect the first interrupted/recent run here.',
129
+ 'CLI fallback: `vgxness runs get --id <run-id>`.',
123
130
  ];
124
131
  }
125
132
  return [
@@ -134,6 +141,28 @@ function focusedRunsLines(plan, runs) {
134
141
  'Next implementation slice can wire this panel to the run read model.',
135
142
  ];
136
143
  }
144
+ function focusedRunDetailLines(plan, databasePath, run) {
145
+ return [
146
+ field('Project', plan.project),
147
+ field('Store', 'opened read-only'),
148
+ field('Database', databasePath),
149
+ field('Run', shortRunId(run.id)),
150
+ field('Status', run.status),
151
+ field('Workflow/phase', `${run.workflow}/${run.phase}`),
152
+ field('Events', String(run.events)),
153
+ field('Checkpoints', String(run.checkpoints)),
154
+ field('Approvals', String(run.approvals)),
155
+ field('Attempts', String(run.attempts)),
156
+ ...(run.latestCheckpointLabel === undefined ? [] : [field('Latest checkpoint', run.latestCheckpointLabel)]),
157
+ ...(run.latestEventTitle === undefined ? [] : [field('Latest event', run.latestEventTitle)]),
158
+ ...(run.outcome === undefined ? [] : [field('Outcome', run.outcome)]),
159
+ ...(run.outcomeReason === undefined ? [] : [field('Outcome reason', run.outcomeReason)]),
160
+ ...(run.userIntent === undefined ? [] : ['', `Intent: ${run.userIntent}`]),
161
+ '',
162
+ `Inspect JSON: vgxness runs get --id ${run.id}`,
163
+ 'No retry, approval, or execution is performed from this TUI detail.',
164
+ ];
165
+ }
137
166
  function runSummarySection(title, runs) {
138
167
  if (runs.length === 0)
139
168
  return [title, '- none'];
@@ -142,8 +171,51 @@ function runSummarySection(title, runs) {
142
171
  function shortRunId(id) {
143
172
  return id.length <= 8 ? id : id.slice(0, 8);
144
173
  }
145
- function focusedSddLines(plan, input) {
174
+ function focusedSddLines(plan, input, sdd) {
146
175
  const change = input.change.length === 0 ? '<id>' : input.change;
176
+ if (input.change.length > 0 && sdd.state === 'unavailable') {
177
+ return [
178
+ field('Project', plan.project),
179
+ field('Change', input.change),
180
+ field('Input', `${input.change}_`),
181
+ field('Store', 'unavailable'),
182
+ field('Database', sdd.databasePath),
183
+ field('Acceptance', 'human-only'),
184
+ field('Safety', 'read-only'),
185
+ '',
186
+ sdd.message,
187
+ '',
188
+ `Continue: vgxness sdd continue --project ${plan.project} --change ${change}`,
189
+ `Status: vgxness sdd status --project ${plan.project} --change ${change}`,
190
+ 'Human acceptance remains explicit; artifact presence is not acceptance.',
191
+ ];
192
+ }
193
+ if (input.change.length > 0 && sdd.state === 'ready') {
194
+ return [
195
+ field('Project', plan.project),
196
+ field('Change', input.change),
197
+ field('Input', `${input.change}_`),
198
+ field('Store', 'opened read-only'),
199
+ field('Database', sdd.databasePath),
200
+ field('Status', sdd.next.status),
201
+ field('Next phase', sdd.next.nextPhase ?? 'none'),
202
+ field('Accepted', `${sdd.cockpit.acceptedCount}/${sdd.cockpit.phases.length}`),
203
+ field('Artifacts', String(sdd.cockpit.artifacts.length)),
204
+ field('Blockers', String(sdd.cockpit.aggregateBlockers.length)),
205
+ field('Recommended', sdd.cockpit.recommendedAction),
206
+ field('Acceptance', sdd.cockpit.gates?.requiresHumanAcceptance === true ? 'human review needed' : 'human-only'),
207
+ field('Safety', 'read-only'),
208
+ '',
209
+ sdd.next.reason,
210
+ '',
211
+ ...sddBlockerLines(sdd.cockpit.aggregateBlockers),
212
+ ...(sdd.continuation.advice.length === 0 ? [] : ['', ...renderSddAdviceLines(sdd.continuation.advice)]),
213
+ '',
214
+ `Inspect: ${sdd.continuation.inspectCommand}`,
215
+ `Continue: vgxness sdd continue --project ${plan.project} --change ${change}`,
216
+ 'Human acceptance remains explicit; artifact presence is not acceptance.',
217
+ ];
218
+ }
147
219
  return [
148
220
  field('Project', plan.project),
149
221
  field('Change', input.change.length === 0 ? 'not selected' : input.change),
@@ -163,6 +235,11 @@ function focusedSddLines(plan, input) {
163
235
  'Next implementation slice can ask for/select a change id before loading SDD state.',
164
236
  ];
165
237
  }
238
+ function sddBlockerLines(blockers) {
239
+ if (blockers.length === 0)
240
+ return ['Blockers', '- none'];
241
+ return ['Blockers', ...blockers.slice(0, 4).map((blocker) => `- ${blocker.phase}: ${blocker.reason}`), ...(blockers.length > 4 ? [`- +${blockers.length - 4} more`] : [])];
242
+ }
166
243
  function focusedSkillsLines(plan, skills) {
167
244
  if (skills.state === 'unavailable') {
168
245
  return [
@@ -1,6 +1,10 @@
1
1
  import { render as renderInk } from 'ink';
2
2
  import React from 'react';
3
+ import { MemoryService } from '../memory/memory-service.js';
4
+ import { openMemoryDatabase } from '../memory/sqlite/database.js';
3
5
  import { RunService } from '../runs/run-service.js';
6
+ import { SddWorkflowService } from '../sdd/sdd-workflow-service.js';
7
+ import { sddContinuationPlanFrom } from '../sdd/sdd-continuation-plan.js';
4
8
  import { SkillRegistryService } from '../skills/skill-registry-service.js';
5
9
  import { SkillIndexService } from '../skills/skill-index-service.js';
6
10
  import { okText } from './cli-help.js';
@@ -9,7 +13,6 @@ import { renderSetupPlan } from './setup-plan-renderer.js';
9
13
  import { runSetupTuiController } from './setup-tui-controller.js';
10
14
  import { navigationIntentFromInput } from './tui/keymap.js';
11
15
  import { nextItem } from './tui/navigation.js';
12
- import { openMemoryDatabase } from '../memory/sqlite/database.js';
13
16
  const enter = '\r';
14
17
  const escape = '\u001B';
15
18
  const ctrlC = '\u0003';
@@ -26,7 +29,8 @@ export async function runHomeTuiController(input) {
26
29
  let runsReadModel = { state: 'not-loaded' };
27
30
  let skillsReadModel = { state: 'not-loaded' };
28
31
  let sddInput = { change: '' };
29
- const app = renderInk(React.createElement(HomeTuiApp, { plan: input.plan, width, selectedTab, view, runsReadModel, skillsReadModel, sddInput }), {
32
+ let sddReadModel = { state: 'not-loaded' };
33
+ const app = renderInk(React.createElement(HomeTuiApp, { plan: input.plan, width, selectedTab, view, runsReadModel, skillsReadModel, sddInput, sddReadModel }), {
30
34
  stdin: stdin,
31
35
  stdout: stdout,
32
36
  interactive: true,
@@ -34,7 +38,7 @@ export async function runHomeTuiController(input) {
34
38
  exitOnCtrlC: false,
35
39
  });
36
40
  const rerender = () => {
37
- app.rerender(React.createElement(HomeTuiApp, { plan: input.plan, width, selectedTab, view, runsReadModel, skillsReadModel, sddInput }));
41
+ app.rerender(React.createElement(HomeTuiApp, { plan: input.plan, width, selectedTab, view, runsReadModel, skillsReadModel, sddInput, sddReadModel }));
38
42
  };
39
43
  stdin.setRawMode?.(true);
40
44
  stdin.resume?.();
@@ -54,12 +58,14 @@ export async function runHomeTuiController(input) {
54
58
  }
55
59
  if (value === backspace || value === ctrlH) {
56
60
  sddInput = { change: sddInput.change.slice(0, -1) };
61
+ sddReadModel = readHomeSdd(input.plan, sddInput.change);
57
62
  rerender();
58
63
  return;
59
64
  }
60
65
  const nextSddChange = appendSddChangeInput(sddInput.change, value);
61
66
  if (nextSddChange !== sddInput.change) {
62
67
  sddInput = { change: nextSddChange };
68
+ sddReadModel = readHomeSdd(input.plan, nextSddChange);
63
69
  rerender();
64
70
  return;
65
71
  }
@@ -77,6 +83,11 @@ export async function runHomeTuiController(input) {
77
83
  rerender();
78
84
  return;
79
85
  }
86
+ if (view === 'runs-focus' && (value === enter || value === '\n' || intent === 'select')) {
87
+ runsReadModel = selectFirstHomeRunDetail(runsReadModel);
88
+ rerender();
89
+ return;
90
+ }
80
91
  if (intent === 'cancel') {
81
92
  cleanup();
82
93
  resolve('quit');
@@ -99,6 +110,7 @@ export async function runHomeTuiController(input) {
99
110
  return;
100
111
  }
101
112
  if ((value === enter || value === '\n' || intent === 'select') && selectedTab === 'sdd') {
113
+ sddReadModel = readHomeSdd(input.plan, sddInput.change);
102
114
  view = 'sdd-focus';
103
115
  rerender();
104
116
  return;
@@ -179,12 +191,55 @@ function readHomeRuns(plan) {
179
191
  phase: run.phase,
180
192
  ...(run.userIntent === undefined ? {} : { userIntent: run.userIntent }),
181
193
  })),
194
+ details: collectRunDetails(service, [...interrupted.value.map((run) => run.runId), ...recent.value.map((run) => run.id)]),
182
195
  };
183
196
  }
184
197
  finally {
185
198
  opened.value.close();
186
199
  }
187
200
  }
201
+ function collectRunDetails(service, runIds) {
202
+ const uniqueRunIds = [...new Set(runIds)];
203
+ const details = [];
204
+ for (const runId of uniqueRunIds) {
205
+ const run = service.getRun(runId);
206
+ if (!run.ok)
207
+ continue;
208
+ details.push(runDetailSummary(run.value));
209
+ }
210
+ return details;
211
+ }
212
+ function runDetailSummary(run) {
213
+ const userIntent = run.userIntent.trim();
214
+ const latestCheckpoint = run.checkpoints.at(-1);
215
+ const latestEvent = run.events.at(-1);
216
+ return {
217
+ id: run.id,
218
+ status: run.status,
219
+ workflow: run.workflow,
220
+ phase: run.phase,
221
+ ...(userIntent.length === 0 ? {} : { userIntent }),
222
+ ...(run.outcome === undefined ? {} : { outcome: run.outcome }),
223
+ ...(run.outcomeReason === undefined ? {} : { outcomeReason: run.outcomeReason }),
224
+ events: run.events.length,
225
+ checkpoints: run.checkpoints.length,
226
+ approvals: run.approvals.length,
227
+ attempts: run.operationAttempts.length,
228
+ ...(latestCheckpoint === undefined ? {} : { latestCheckpointLabel: latestCheckpoint.label }),
229
+ ...(latestEvent === undefined ? {} : { latestEventTitle: latestEvent.title }),
230
+ };
231
+ }
232
+ function selectFirstHomeRunDetail(runs) {
233
+ if (runs.state !== 'ready' || runs.selected !== undefined)
234
+ return runs;
235
+ const firstInterruptedId = runs.interrupted[0]?.id;
236
+ const firstRecentId = runs.recent[0]?.id;
237
+ const selectedId = firstInterruptedId ?? firstRecentId;
238
+ if (selectedId === undefined)
239
+ return runs;
240
+ const selected = runs.details.find((detail) => detail.id === selectedId);
241
+ return selected === undefined ? runs : { ...runs, selected };
242
+ }
188
243
  function runRecordSummary(run) {
189
244
  const userIntent = run.userIntent.trim();
190
245
  return {
@@ -210,6 +265,30 @@ function readHomeSkills(plan) {
210
265
  opened.value.close();
211
266
  }
212
267
  }
268
+ function readHomeSdd(plan, change) {
269
+ if (change.length === 0)
270
+ return { state: 'not-loaded' };
271
+ const opened = openMemoryDatabase({ path: plan.db.path, readonly: true });
272
+ if (!opened.ok)
273
+ return { state: 'unavailable', databasePath: plan.db.path, message: opened.error.message };
274
+ try {
275
+ const sdd = new SddWorkflowService(new MemoryService(opened.value));
276
+ const status = sdd.getStatus({ project: plan.project, change });
277
+ if (!status.ok)
278
+ return { state: 'unavailable', databasePath: plan.db.path, message: status.error.message };
279
+ const next = sdd.getNext({ project: plan.project, change });
280
+ if (!next.ok)
281
+ return { state: 'unavailable', databasePath: plan.db.path, message: next.error.message };
282
+ const cockpit = sdd.getCockpit({ project: plan.project, change });
283
+ if (!cockpit.ok)
284
+ return { state: 'unavailable', databasePath: plan.db.path, message: cockpit.error.message };
285
+ const continuation = sddContinuationPlanFrom({ project: plan.project, next: next.value, cockpit: cockpit.value });
286
+ return { state: 'ready', databasePath: plan.db.path, status: status.value, next: next.value, cockpit: cockpit.value, continuation };
287
+ }
288
+ finally {
289
+ opened.value.close();
290
+ }
291
+ }
213
292
  function tuiWidth(stdout) {
214
293
  const columns = stdout.columns;
215
294
  const width = typeof columns === 'number' && Number.isFinite(columns) ? Math.floor(columns) : 88;
@@ -91,6 +91,8 @@ export function renderSddContinuationPlan(plan) {
91
91
  `- ${suggestedCommandLabel}: ${plan.suggestedCommand}`,
92
92
  `- Diagnostic command: ${plan.inspectCommand}`,
93
93
  '',
94
+ ...renderSddAdviceLines(plan.advice),
95
+ ...(plan.advice.length === 0 ? [] : ['']),
94
96
  'Blocker-specific next actions:',
95
97
  ...(plan.blockerActions.length === 0
96
98
  ? ['- none']
@@ -119,6 +121,11 @@ export function renderSddContinuationPlan(plan) {
119
121
  ];
120
122
  return `${lines.join('\n')}\n`;
121
123
  }
124
+ export function renderSddAdviceLines(advice) {
125
+ if (advice.length === 0)
126
+ return [];
127
+ return ['Advice:', ...advice.map((item) => `- ${item.title}: ${item.message}`)];
128
+ }
122
129
  export function renderSddCockpit(cockpit) {
123
130
  const readModel = cockpit.readModel;
124
131
  const blockers = readModel.blockers;
@@ -223,6 +223,22 @@ const sddRecommendedActionOutputSchema = z
223
223
  .passthrough()),
224
224
  })
225
225
  .passthrough();
226
+ const sddContinuationAdviceOutputSchema = z
227
+ .object({
228
+ id: z.string(),
229
+ kind: z.enum(['proposal-question-round', 'review-workload-guard']),
230
+ title: z.string(),
231
+ message: z.string(),
232
+ readOnly: z.literal(true),
233
+ advisoryOnly: z.literal(true),
234
+ mutating: z.literal(false),
235
+ providerExecution: z.literal(false),
236
+ artifactMutation: z.literal(false),
237
+ runCreation: z.literal(false),
238
+ openspecWrite: z.literal(false),
239
+ reason: z.string(),
240
+ })
241
+ .passthrough();
226
242
  const mcpSuccessOutputSchema = (value) => z
227
243
  .object({
228
244
  ok: z.literal(true),
@@ -672,6 +688,7 @@ export const VGX_MCP_TOOL_OUTPUT_SCHEMAS = {
672
688
  kind: z.literal('sdd-continuation-plan'),
673
689
  project: z.string(),
674
690
  change: z.string(),
691
+ advice: z.array(sddContinuationAdviceOutputSchema),
675
692
  recommendedActions: z.array(sddRecommendedActionOutputSchema),
676
693
  })
677
694
  .passthrough()),
@@ -84,7 +84,7 @@ function descriptionForTool(publicToolName) {
84
84
  if (publicToolName === 'sdd_reopen_artifact')
85
85
  return 'Human-only mutating SDD reopen gate; records an explicit human decision to reopen an artifact and must not be called by agents on their own behalf. Use only after human instruction, then revise or re-review the phase.';
86
86
  if (publicToolName === 'run_preflight')
87
- return 'Agent-callable advisory/planning-only run preflight; evaluates permissions and records a plan but does not execute the checked shell/git/install/network/provider/secrets operation. Use it to decide whether human approval or a separate executor step is required.';
87
+ return 'Agent-callable advisory/planning-only run preflight; evaluates permissions, records a plan, and returns a workspace strategy recommendation, but does not execute the checked shell/git/install/network/provider/secrets operation and does not create branches, worktrees, or pull requests. Use it to decide whether human approval or a separate executor step is required.';
88
88
  const toolName = toInternalVgxMcpToolName(publicToolName);
89
89
  return `VGX control-plane tool ${toolName}`;
90
90
  }
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { recommendWorkspaceStrategy } from '../workspace-strategy/index.js';
3
4
  import { planWorktreeSandbox } from './sandbox-worktree-planning.js';
4
5
  const filesystemCategories = new Set(['read', 'edit', 'external-directory']);
5
6
  export function planExecutionIsolation(input) {
@@ -23,6 +24,27 @@ export function planExecutionIsolation(input) {
23
24
  createsWorktree: false,
24
25
  };
25
26
  }
27
+ export function recommendWorkspaceStrategyForExecution(input) {
28
+ return recommendWorkspaceStrategy(workspaceStrategyInputForExecution(input));
29
+ }
30
+ export function workspaceStrategyInputForExecution(input) {
31
+ const categories = workspaceStrategyCategories(input.operation, input.decision);
32
+ const mutationSignal = hasWorkspaceMutationSignal(input.operation, categories);
33
+ return {
34
+ categories,
35
+ intentSignals: workspaceStrategyIntentSignals(input.operation, input.decision),
36
+ risk: workspaceStrategyRisk(input.decision),
37
+ readOnly: isReadOnlyWorkspaceOperation(input.operation, input.decision),
38
+ expectedWrites: mutationSignal,
39
+ mutatesRepository: mutationSignal,
40
+ destructive: input.operation.destructive === true,
41
+ external: input.operation.external === true || input.operation.category === 'external-directory',
42
+ privileged: input.operation.privileged === true,
43
+ ambiguous: input.operation.ambiguous === true,
44
+ changeSize: workspaceStrategyChangeSize(input.decision),
45
+ ...(mutationSignal ? { workspace: { checkout: 'unknown' } } : {}),
46
+ };
47
+ }
26
48
  function selectStrategy(operation, decision) {
27
49
  if (operation.sandboxStrategy === 'worktree')
28
50
  return 'worktree';
@@ -109,3 +131,48 @@ const defaultGitBoundaryInspector = ({ workspaceRoot }) => {
109
131
  }
110
132
  return { status: 'compatible', reason: 'workspace has no git metadata to conflict with a planned worktree boundary' };
111
133
  };
134
+ function workspaceStrategyCategories(operation, decision) {
135
+ const categories = [operation.category];
136
+ if (decision.riskReasonCodes?.includes('git_write') === true && !categories.includes('git-write'))
137
+ categories.push('git-write');
138
+ return categories;
139
+ }
140
+ function workspaceStrategyIntentSignals(operation, decision) {
141
+ return [
142
+ `permission:${decision.reason}`,
143
+ ...(decision.riskReasonCodes ?? []).map((code) => `risk:${code}`),
144
+ ...(operation.workflow === undefined ? [] : [`workflow:${operation.workflow}`]),
145
+ ...(operation.phase === undefined ? [] : [`phase:${operation.phase}`]),
146
+ ];
147
+ }
148
+ function workspaceStrategyRisk(decision) {
149
+ if (decision.riskTier === 0)
150
+ return 'none';
151
+ if (decision.riskTier === 1)
152
+ return 'low';
153
+ if (decision.riskTier === 2)
154
+ return 'medium';
155
+ if (decision.riskTier === 3)
156
+ return 'high';
157
+ if (decision.riskTier === 4)
158
+ return 'critical';
159
+ return 'unknown';
160
+ }
161
+ function workspaceStrategyChangeSize(decision) {
162
+ if (decision.riskReasonCodes?.some((code) => code === 'architecture_change' || code === 'cross_surface_change' || code === 'governance_change') === true)
163
+ return 'large';
164
+ if (decision.riskTier === 2)
165
+ return 'medium';
166
+ if (decision.riskTier === 1)
167
+ return 'small';
168
+ return 'unknown';
169
+ }
170
+ function isReadOnlyWorkspaceOperation(operation, decision) {
171
+ return decision.riskTier === 0 || operation.category === 'read' || (operation.category === 'git' && decision.riskReasonCodes?.includes('git_write') !== true);
172
+ }
173
+ function hasWorkspaceMutationSignal(operation, categories) {
174
+ if (operation.destructive === true || operation.privileged === true || operation.external === true || operation.ambiguous === true)
175
+ return true;
176
+ return categories.some((category) => workspaceMutationCategories.has(category));
177
+ }
178
+ const workspaceMutationCategories = new Set(['edit', 'implementation-edit', 'spec-write', 'design-write', 'task-write', 'git-write']);
@@ -1,7 +1,7 @@
1
1
  import { evaluatePermission } from '../permissions/policy-evaluator.js';
2
2
  import { isRiskyPermissionCategory } from '../permissions/schema.js';
3
3
  import { normalizeSddPhaseInput } from '../sdd/schema.js';
4
- import { planExecutionIsolation } from './execution-planning.js';
4
+ import { planExecutionIsolation, recommendWorkspaceStrategyForExecution } from './execution-planning.js';
5
5
  import { evaluateOperationRetry as evaluateOperationRetryPolicy } from './operation-retry.js';
6
6
  import { RunRepository, } from './repositories/runs.js';
7
7
  import { buildRunInsights, buildRunOperatorResumePlan } from './run-insights.js';
@@ -292,6 +292,11 @@ export class RunService {
292
292
  decision: permission.value.decision,
293
293
  ...(resolved.value.gitBoundaryInspector === undefined ? {} : { gitBoundaryInspector: resolved.value.gitBoundaryInspector }),
294
294
  });
295
+ const workspaceStrategy = recommendWorkspaceStrategyForExecution({
296
+ operation: resolved.value,
297
+ decision: permission.value.decision,
298
+ ...(resolved.value.gitBoundaryInspector === undefined ? {} : { gitBoundaryInspector: resolved.value.gitBoundaryInspector }),
299
+ });
295
300
  const sandboxDecision = sandboxDecisionSummary(plan.sandbox);
296
301
  const planEvent = this.runs.appendEvent({
297
302
  runId: resolved.value.runId,
@@ -303,6 +308,7 @@ export class RunService {
303
308
  decisionEventId: permission.value.event.id,
304
309
  approvalId: permission.value.approval?.id ?? null,
305
310
  plan: plan,
311
+ workspaceStrategy: workspaceStrategy,
306
312
  ...(sandboxDecision === undefined ? {} : { sandboxDecision: sandboxDecision }),
307
313
  operationExecuted: false,
308
314
  executorInvoked: false,
@@ -317,7 +323,7 @@ export class RunService {
317
323
  });
318
324
  if (!planEvent.ok)
319
325
  return { ok: false, error: planEvent.error };
320
- return { ok: true, value: { ...permission.value, plan, planEvent: planEvent.value } };
326
+ return { ok: true, value: { ...permission.value, plan, workspaceStrategy, planEvent: planEvent.value } };
321
327
  }
322
328
  preflightOperation(input) {
323
329
  const resolved = this.buildPermissionContextForRun(input);
@@ -329,7 +335,7 @@ export class RunService {
329
335
  const planned = this.planOperationExecution(resolved.value);
330
336
  if (!planned.ok)
331
337
  return planned;
332
- const { decision, event, approval, plan, planEvent } = planned.value;
338
+ const { decision, event, approval, plan, workspaceStrategy, planEvent } = planned.value;
333
339
  const audit = { permissionEventId: event.id, planEventId: planEvent.id };
334
340
  if (approval !== undefined)
335
341
  audit.approvalId = approval.id;
@@ -351,6 +357,7 @@ export class RunService {
351
357
  permissionEvent: event,
352
358
  ...(approval === undefined ? {} : { approval }),
353
359
  plan,
360
+ workspaceStrategy,
354
361
  planEvent,
355
362
  audit,
356
363
  eligibleForFutureExecution: plan.executable && !sandboxRejected,
@@ -6,6 +6,7 @@ export function sddContinuationPlanFrom(input) {
6
6
  const blockerGuidance = input.next.blockerGuidance ?? [];
7
7
  const blockerActions = blockerGuidance.map((blocker) => continuationBlockerAction(input.project, input.next.change, blocker, dbFlag));
8
8
  const recommendedActions = continuationRecommendedActions(input.project, input.next, blockerGuidance);
9
+ const advice = continuationAdvice(input.next, blockerGuidance);
9
10
  const relatedRunContext = relatedRunContextView(input.project, input.relatedRunContext, dbFlag);
10
11
  return {
11
12
  kind: 'sdd-continuation-plan',
@@ -18,6 +19,7 @@ export function sddContinuationPlanFrom(input) {
18
19
  reason: input.next.reason,
19
20
  suggestedCommand,
20
21
  inspectCommand,
22
+ advice,
21
23
  blockerActions,
22
24
  recommendedActions,
23
25
  ...(relatedRunContext === undefined ? {} : { relatedRunContext }),
@@ -30,6 +32,50 @@ export function sddContinuationPlanFrom(input) {
30
32
  ],
31
33
  };
32
34
  }
35
+ function continuationAdvice(next, blockerGuidance) {
36
+ const advice = [];
37
+ if (next.nextPhase === 'proposal' || hasProposalMissingBlocker(next, blockerGuidance))
38
+ advice.push(proposalQuestionRoundAdvice(next.change));
39
+ if (next.nextPhase === 'apply-progress')
40
+ advice.push(reviewWorkloadGuardAdvice(next.change));
41
+ return advice;
42
+ }
43
+ function hasProposalMissingBlocker(next, blockerGuidance) {
44
+ return (blockerGuidance.some((blocker) => blocker.phase === 'proposal' && blocker.reason === 'missing') ||
45
+ next.missingArtifactTopicKeys.some((topicKey) => topicKey.endsWith('/proposal')));
46
+ }
47
+ function proposalQuestionRoundAdvice(change) {
48
+ return {
49
+ id: `sdd.${change}.advice.proposal-question-round`,
50
+ kind: 'proposal-question-round',
51
+ title: 'Run proposal question round when product clarity is missing',
52
+ message: 'Before drafting or accepting the proposal, run a product/business question round if clarity is still missing; this advice does not claim low clarity from metadata alone.',
53
+ readOnly: true,
54
+ advisoryOnly: true,
55
+ mutating: false,
56
+ providerExecution: false,
57
+ artifactMutation: false,
58
+ runCreation: false,
59
+ openspecWrite: false,
60
+ reason: 'Proposal work benefits from explicit business rules, product outcomes, edge cases, non-goals, and tradeoff decisions before proposal drafting or acceptance.',
61
+ };
62
+ }
63
+ function reviewWorkloadGuardAdvice(change) {
64
+ return {
65
+ id: `sdd.${change}.advice.review-workload-guard`,
66
+ kind: 'review-workload-guard',
67
+ title: 'Inspect Review Workload Forecast before apply',
68
+ message: 'Before apply, inspect tasks/Review Workload Forecast and ask for a delivery decision if risk is high, estimated changes exceed 400 lines, chained PRs are recommended, or a decision is pending.',
69
+ readOnly: true,
70
+ advisoryOnly: true,
71
+ mutating: false,
72
+ providerExecution: false,
73
+ artifactMutation: false,
74
+ runCreation: false,
75
+ openspecWrite: false,
76
+ reason: 'Apply work can exceed review budget; continuation advice must surface the guard without executing providers, mutating artifacts, creating runs, or writing openspec files.',
77
+ };
78
+ }
33
79
  function continuationRecommendedActions(project, next, blockerGuidance) {
34
80
  const actions = [inspectCockpitAction(project, next.change)];
35
81
  if (next.status === 'runnable' && next.nextPhase !== undefined)
@@ -0,0 +1,34 @@
1
+ export const workspaceStrategyDiagnosticMessages = Object.freeze({
2
+ WS_STRATEGY_CURRENT_CHECKOUT_SAFE: 'Current checkout is sufficient for this read-only or planning-only operation.',
3
+ WS_STRATEGY_BRANCH_RECOMMENDED: 'A normal branch is recommended for this focused edit.',
4
+ WS_STRATEGY_MANUAL_WORKTREE_RECOMMENDED: 'Manual worktree isolation is recommended before making repository mutations.',
5
+ WS_STRATEGY_STACKED_PRS_RECOMMENDED: 'Stacked PRs are recommended conceptually for dependent review steps.',
6
+ WS_STRATEGY_WORKSPACE_STATE_UNKNOWN: 'Workspace state is unknown, so mutation recommendations are conservative.',
7
+ WS_STRATEGY_NO_MUTATION_GUARANTEE: 'This policy is advisory only and does not mutate repositories, provider config, branches, worktrees, or PRs.',
8
+ WS_STRATEGY_PROVIDER_CONFIG_OUT_OF_SCOPE: 'Provider configuration changes are outside the scope of workspace strategy execution.',
9
+ WS_STRATEGY_GIT_WRITE_IS_RISKY: 'Git write operations are risky and should be isolated manually.',
10
+ WS_STRATEGY_DESTRUCTIVE_OPERATION: 'Destructive operations require conservative manual isolation.',
11
+ WS_STRATEGY_PROTECTED_BRANCH: 'The current branch appears protected; recommendation remains advisory and non-mutating.',
12
+ WS_STRATEGY_DIRTY_CHECKOUT: 'Dirty checkout signals were present.',
13
+ WS_STRATEGY_EXTERNAL_SCOPE_WARNING: 'The operation reaches outside the repository workspace.',
14
+ WS_STRATEGY_LOW_CONFIDENCE: 'Confidence is low because required signals are missing or contradictory.',
15
+ });
16
+ export const workspaceStrategyDiagnosticCodes = Object.freeze(Object.keys(workspaceStrategyDiagnosticMessages));
17
+ export const workspaceStrategyDiagnosticSeverities = Object.freeze({
18
+ WS_STRATEGY_CURRENT_CHECKOUT_SAFE: 'info',
19
+ WS_STRATEGY_BRANCH_RECOMMENDED: 'info',
20
+ WS_STRATEGY_MANUAL_WORKTREE_RECOMMENDED: 'warning',
21
+ WS_STRATEGY_STACKED_PRS_RECOMMENDED: 'info',
22
+ WS_STRATEGY_WORKSPACE_STATE_UNKNOWN: 'warning',
23
+ WS_STRATEGY_NO_MUTATION_GUARANTEE: 'info',
24
+ WS_STRATEGY_PROVIDER_CONFIG_OUT_OF_SCOPE: 'warning',
25
+ WS_STRATEGY_GIT_WRITE_IS_RISKY: 'warning',
26
+ WS_STRATEGY_DESTRUCTIVE_OPERATION: 'blocker',
27
+ WS_STRATEGY_PROTECTED_BRANCH: 'warning',
28
+ WS_STRATEGY_DIRTY_CHECKOUT: 'warning',
29
+ WS_STRATEGY_EXTERNAL_SCOPE_WARNING: 'warning',
30
+ WS_STRATEGY_LOW_CONFIDENCE: 'warning',
31
+ });
32
+ export function workspaceStrategyDiagnostic(code) {
33
+ return { code, severity: workspaceStrategyDiagnosticSeverities[code], message: workspaceStrategyDiagnosticMessages[code] };
34
+ }
@@ -0,0 +1,3 @@
1
+ export * from './diagnostics.js';
2
+ export * from './recommendation-policy.js';
3
+ export * from './schema.js';
@@ -0,0 +1,196 @@
1
+ import { workspaceStrategyDiagnostic } from './diagnostics.js';
2
+ const nonMutatingExecution = Object.freeze({
3
+ mutatesRepository: false,
4
+ mutatesProviderConfig: false,
5
+ createsBranch: false,
6
+ createsWorktree: false,
7
+ createsPullRequest: false,
8
+ });
9
+ export function recommendWorkspaceStrategy(input = {}) {
10
+ const signals = normalizeInput(input);
11
+ const diagnostics = [workspaceStrategyDiagnostic('WS_STRATEGY_NO_MUTATION_GUARANTEE')];
12
+ const reasons = [];
13
+ const followUpNotes = [];
14
+ addContextDiagnostics(signals, diagnostics);
15
+ const strategy = chooseStrategy(signals);
16
+ addStrategyDetails(strategy, signals, diagnostics, reasons, followUpNotes);
17
+ const confidence = calculateConfidence(signals, strategy);
18
+ if (confidence === 'low')
19
+ addDiagnostic(diagnostics, 'WS_STRATEGY_LOW_CONFIDENCE');
20
+ const followUp = buildFollowUp(signals, followUpNotes);
21
+ return {
22
+ strategy,
23
+ confidence,
24
+ reasons,
25
+ diagnostics,
26
+ ...(followUp === undefined ? {} : { followUp }),
27
+ execution: nonMutatingExecution,
28
+ };
29
+ }
30
+ function normalizeInput(input) {
31
+ const categories = normalizeCategories(input);
32
+ const signalText = (input.intentSignals ?? []).map((signal) => signal.trim().toLowerCase()).filter((signal) => signal.length > 0);
33
+ const workspaceCheckout = input.workspace?.checkout;
34
+ const dirtyCheckout = workspaceCheckout === 'dirty' || input.workspace?.hasUncommittedChanges === true || input.workspace?.hasUntrackedFiles === true;
35
+ const workspaceUnknown = workspaceCheckout === 'unknown' || (input.workspace === undefined && hasMutationIntent(input, categories));
36
+ const hasEditIntent = hasAnyCategory(categories, ['edit', 'implementation-edit', 'spec-write', 'design-write', 'task-write']) || input.expectedWrites === true || input.mutatesRepository === true;
37
+ const hasGitReadOnly = categories.includes('git');
38
+ const hasGitWrite = categories.includes('git-write');
39
+ const isPlanningOnly = input.planningOnly === true || signalText.some((signal) => includesAny(signal, ['planning-only', 'plan only', 'sdd planning']));
40
+ const isReadOnly = input.readOnly === true || categories.includes('read') || hasGitReadOnly || isPlanningOnly || (categories.includes('test-run') && input.expectedWrites !== true);
41
+ const stackedRequested = input.dependentSteps === true || input.expectedReviewShape === 'stacked' || signalText.some((signal) => includesAny(signal, ['stacked-prs', 'stacked prs', 'stacked pr']));
42
+ const largeChange = input.changeSize === 'large' || signalText.some((signal) => includesAny(signal, ['large change', 'large independent', 'big change']));
43
+ const providerConfigIntent = categories.includes('provider-tool') || signalText.some((signal) => includesAny(signal, ['provider config', 'provider setup', 'opencode', 'claude']));
44
+ const destructive = input.destructive === true || signalText.some((signal) => includesAny(signal, ['destructive', 'delete', 'remove']));
45
+ const privileged = input.privileged === true;
46
+ const external = input.external === true || categories.includes('external-directory');
47
+ const ambiguousMutation = input.ambiguous === true && !isReadOnly;
48
+ const highRisk = input.risk === 'high' || input.risk === 'critical';
49
+ const contradictoryRisk = (destructive || hasGitWrite || privileged || external) && (input.risk === 'low' || input.risk === 'none');
50
+ const criticalIsolationRequired = hasGitWrite || destructive || privileged || external || highRisk;
51
+ return {
52
+ categories,
53
+ signalText,
54
+ hasEditIntent,
55
+ hasGitReadOnly,
56
+ hasGitWrite,
57
+ isReadOnly,
58
+ isPlanningOnly,
59
+ workspaceUnknown,
60
+ dirtyCheckout,
61
+ protectedBranch: input.workspace?.protectedBranch === true,
62
+ stackedRequested,
63
+ largeChange,
64
+ providerConfigIntent,
65
+ destructive,
66
+ privileged,
67
+ external,
68
+ ambiguousMutation,
69
+ highRisk,
70
+ contradictoryRisk,
71
+ criticalIsolationRequired,
72
+ };
73
+ }
74
+ function normalizeCategories(input) {
75
+ const categories = [...(input.categories ?? [])];
76
+ if (input.category !== undefined)
77
+ categories.push(input.category);
78
+ return categories;
79
+ }
80
+ function hasMutationIntent(input, categories) {
81
+ return (input.expectedWrites === true ||
82
+ input.mutatesRepository === true ||
83
+ input.destructive === true ||
84
+ input.privileged === true ||
85
+ input.external === true ||
86
+ input.ambiguous === true ||
87
+ hasAnyCategory(categories, ['edit', 'implementation-edit', 'spec-write', 'design-write', 'task-write', 'shell', 'install', 'network', 'git-write', 'memory-write', 'external-directory', 'secrets']));
88
+ }
89
+ function chooseStrategy(signals) {
90
+ if (signals.criticalIsolationRequired)
91
+ return 'manual-worktree';
92
+ if (signals.stackedRequested)
93
+ return 'stacked-prs';
94
+ if (isSafeReadOrPlanningOnly(signals))
95
+ return 'current-checkout';
96
+ if (shouldUseManualWorktree(signals))
97
+ return 'manual-worktree';
98
+ if (signals.hasEditIntent || signals.largeChange)
99
+ return signals.largeChange ? 'manual-worktree' : 'branch';
100
+ if (signals.isReadOnly || signals.isPlanningOnly || signals.hasGitReadOnly)
101
+ return 'current-checkout';
102
+ return 'current-checkout';
103
+ }
104
+ function buildFollowUp(signals, followUpNotes) {
105
+ if (followUpNotes.length === 0)
106
+ return undefined;
107
+ const requiresHumanApproval = signals.criticalIsolationRequired || signals.stackedRequested;
108
+ return {
109
+ ...(requiresHumanApproval ? { requiresHumanApproval: true } : {}),
110
+ suggestedNextStep: followUpNotes.join(' '),
111
+ };
112
+ }
113
+ function isSafeReadOrPlanningOnly(signals) {
114
+ if (!signals.isReadOnly && !signals.isPlanningOnly && !signals.hasGitReadOnly)
115
+ return false;
116
+ return !(signals.hasGitWrite || signals.destructive || signals.privileged || signals.external || signals.ambiguousMutation || signals.highRisk);
117
+ }
118
+ function shouldUseManualWorktree(signals) {
119
+ return (signals.hasGitWrite ||
120
+ signals.destructive ||
121
+ signals.privileged ||
122
+ signals.external ||
123
+ signals.ambiguousMutation ||
124
+ signals.highRisk ||
125
+ signals.largeChange ||
126
+ (signals.hasEditIntent && (signals.dirtyCheckout || signals.workspaceUnknown)));
127
+ }
128
+ function addContextDiagnostics(signals, diagnostics) {
129
+ if (signals.workspaceUnknown)
130
+ addDiagnostic(diagnostics, 'WS_STRATEGY_WORKSPACE_STATE_UNKNOWN');
131
+ if (signals.providerConfigIntent)
132
+ addDiagnostic(diagnostics, 'WS_STRATEGY_PROVIDER_CONFIG_OUT_OF_SCOPE');
133
+ if (signals.hasGitWrite)
134
+ addDiagnostic(diagnostics, 'WS_STRATEGY_GIT_WRITE_IS_RISKY');
135
+ if (signals.destructive)
136
+ addDiagnostic(diagnostics, 'WS_STRATEGY_DESTRUCTIVE_OPERATION');
137
+ if (signals.protectedBranch)
138
+ addDiagnostic(diagnostics, 'WS_STRATEGY_PROTECTED_BRANCH');
139
+ if (signals.dirtyCheckout)
140
+ addDiagnostic(diagnostics, 'WS_STRATEGY_DIRTY_CHECKOUT');
141
+ if (signals.external)
142
+ addDiagnostic(diagnostics, 'WS_STRATEGY_EXTERNAL_SCOPE_WARNING');
143
+ }
144
+ function addStrategyDetails(strategy, signals, diagnostics, reasons, followUp) {
145
+ if (strategy === 'current-checkout') {
146
+ addDiagnostic(diagnostics, 'WS_STRATEGY_CURRENT_CHECKOUT_SAFE');
147
+ reasons.push('The operation is read-only, Git read-only, planning-only, or otherwise has no repository mutation signal.');
148
+ return;
149
+ }
150
+ if (strategy === 'branch') {
151
+ addDiagnostic(diagnostics, 'WS_STRATEGY_BRANCH_RECOMMENDED');
152
+ reasons.push('The operation looks like a focused low/medium-risk edit with no dirty or unknown checkout isolation signal.');
153
+ return;
154
+ }
155
+ if (strategy === 'manual-worktree') {
156
+ addDiagnostic(diagnostics, 'WS_STRATEGY_MANUAL_WORKTREE_RECOMMENDED');
157
+ reasons.push('The operation has mutation, risk, dirty checkout, unknown checkout, external, or large-change signals.');
158
+ followUp.push('Create and manage any worktree manually outside this pure recommendation policy before mutating repository state.');
159
+ if (signals.hasGitWrite)
160
+ followUp.push('Review and explicitly approve Git write intent before execution.');
161
+ if (signals.destructive)
162
+ followUp.push('Confirm destructive scope and rollback plan before execution.');
163
+ if (signals.providerConfigIntent)
164
+ followUp.push('Handle provider configuration through the dedicated setup/preflight flow; this policy will not write provider config.');
165
+ return;
166
+ }
167
+ addDiagnostic(diagnostics, 'WS_STRATEGY_STACKED_PRS_RECOMMENDED');
168
+ reasons.push('The change has dependent steps or an explicit stacked review signal.');
169
+ followUp.push('Plan stacked PR boundaries manually; this policy does not create branches or pull requests.');
170
+ if (signals.providerConfigIntent)
171
+ followUp.push('Provider configuration remains out of scope for workspace strategy execution.');
172
+ }
173
+ function calculateConfidence(signals, strategy) {
174
+ if (signals.workspaceUnknown || signals.ambiguousMutation || signals.contradictoryRisk)
175
+ return 'low';
176
+ if (strategy === 'manual-worktree' && (signals.highRisk || signals.destructive || signals.hasGitWrite))
177
+ return 'high';
178
+ if (strategy === 'current-checkout' && (signals.isReadOnly || signals.isPlanningOnly || signals.hasGitReadOnly))
179
+ return 'high';
180
+ if (strategy === 'stacked-prs' && signals.stackedRequested)
181
+ return 'high';
182
+ if (signals.protectedBranch || signals.providerConfigIntent || signals.dirtyCheckout)
183
+ return 'medium';
184
+ return 'medium';
185
+ }
186
+ function addDiagnostic(diagnostics, code) {
187
+ if (diagnostics.some((diagnostic) => diagnostic.code === code))
188
+ return;
189
+ diagnostics.push(workspaceStrategyDiagnostic(code));
190
+ }
191
+ function hasAnyCategory(categories, candidates) {
192
+ return categories.some((category) => candidates.includes(category));
193
+ }
194
+ function includesAny(value, candidates) {
195
+ return candidates.some((candidate) => value.includes(candidate));
196
+ }
@@ -0,0 +1 @@
1
+ export const workspaceStrategies = ['current-checkout', 'branch', 'manual-worktree', 'stacked-prs'];
@@ -504,6 +504,17 @@ Current operation enforcement is safe-by-default and executor-injected: `RunServ
504
504
 
505
505
  Execution isolation is currently **planning-only**. `planExecutionIsolation(...)` and `RunService.planOperationExecution(...)` produce an auditable plan before any real executor exists; they do not create worktrees, launch sandboxes, run shell commands, call providers, or mutate files. Treat `strategy: "worktree"` as a recommended future/manual isolation boundary, not proof that isolation has already happened.
506
506
 
507
+ Workspace strategy recommendation is a separate advisory layer from both risk classification and execution isolation:
508
+
509
+ | Layer | Source | Responsibility | Does not do |
510
+ |---|---|---|---|
511
+ | Risk classification | `classifyOperationRisk(...)` / permission evaluation | Classifies category, risk tier, policy reason, and approval/default workflow evidence. | Choose branches, create worktrees, or decide PR shape. |
512
+ | Workspace strategy recommendation | `recommendWorkspaceStrategy(...)` via the runs adapter | Recommends `current-checkout`, `branch`, `manual-worktree`, or `stacked-prs` for operator planning and review shape. | Mutate the repository, write provider config, create branches/worktrees/PRs, or prove isolation exists. |
513
+ | Execution isolation plan | `planExecutionIsolation(...)` | Records the future/manual isolation boundary (`workspace`, `worktree`, `process-sandbox`) plus conditions and limitations. | Launch sandboxes, create worktrees, execute providers, or run commands. |
514
+ | Real execution | Injected executor / future safe runtime | Performs the operation only after policy, approval, and enforceable execution gates. | Bypass preflight, approvals, SDD acceptance, or provider-config confirmation. |
515
+
516
+ `RunService.planOperationExecution(...)` and `RunService.preflightOperation(...)` include `workspaceStrategy` in their output and in `execution-plan` events so CLI/MCP users can see the review/isolation recommendation next to the permission and isolation plan. The recommendation is intentionally conservative when checkout state is unavailable: mutating work may be represented as `checkout: "unknown"` and recommend `manual-worktree`, but the recommendation still means “create/manage this manually before mutating” rather than “VGXNESS created it.”
517
+
507
518
  | Operation shape | Current planned strategy | Current behavior |
508
519
  |---|---|---|
509
520
  | In-workspace read | `workspace` | Allowed when permission policy allows it; filesystem plans require realpath/symlink hardening before any future execution. |
@@ -511,7 +522,7 @@ Execution isolation is currently **planning-only**. `planExecutionIsolation(...)
511
522
  | Shell, network, provider tool, privileged operation | `process-sandbox` | Planned as sandboxed process/provider work, but approval is still required by default. No sandbox is launched yet; execution requires a separate executor with enforceable sandbox evidence. |
512
523
  | External directory or out-of-workspace path | `process-sandbox` or blocked workspace plan | Denied by default or requires explicit stronger future approval conditions; external paths are not allowed by current plans. |
513
524
 
514
- Every plan includes path constraints, required approvals/conditions, whether realpath hardening must happen before execution, and limitations that state the future boundary. Real sandbox/worktree executors are follow-up work after this planner is connected to a safe execution backend. Until then, large or risky edits should use a human-created branch/worktree and PR flow.
525
+ Every plan includes path constraints, required approvals/conditions, whether realpath hardening must happen before execution, limitations that state the future boundary, and a separate workspace strategy recommendation in the surrounding run/preflight record. Real sandbox/worktree executors are follow-up work after this planner is connected to a safe execution backend. Until then, large or risky edits should use a human-created branch/worktree and PR flow.
515
526
 
516
527
  | Decision | Current behavior |
517
528
  |---|---|
package/docs/cli.md CHANGED
@@ -811,6 +811,26 @@ Runs are runtime state, not long-term memory. Use event and checkpoint payloads
811
811
 
812
812
  `runs permission-check` evaluates the same core permission policy as `permissions check`, records a `permission-decision` event on the run timeline, and returns the decision plus event. When the decision is `ask`, it also creates a pending approval record linked to that decision event. `allow` and `deny` decisions do not create approvals.
813
813
 
814
+ `runs preflight` records the permission decision, the planning-only execution isolation plan, and a separate `workspaceStrategy` recommendation. The recommendation is for operator planning/review shape only: `current-checkout`, `branch`, `manual-worktree`, or `stacked-prs`. It does **not** create branches, create worktrees, create PRs, write provider config, execute commands, call providers, or prove isolation exists. For example, `manual-worktree` means “arrange a manual worktree before mutating if you proceed,” not “VGXNESS already created a worktree.”
815
+
816
+ Example workspace strategy fields in preflight JSON:
817
+
818
+ ```json
819
+ {
820
+ "workspaceStrategy": {
821
+ "strategy": "manual-worktree",
822
+ "confidence": "high",
823
+ "execution": {
824
+ "mutatesRepository": false,
825
+ "mutatesProviderConfig": false,
826
+ "createsBranch": false,
827
+ "createsWorktree": false,
828
+ "createsPullRequest": false
829
+ }
830
+ }
831
+ }
832
+ ```
833
+
814
834
  Resolving an approval appends an `approval` audit event and updates the approval status:
815
835
 
816
836
  ```bash
package/docs/glossary.md CHANGED
@@ -74,6 +74,10 @@ A testable property of the harness. The 11 eval targets live in [Architecture](.
74
74
 
75
75
  A planned strategy for executing a reserved operation: `workspace`, `git-worktree`, or `process-sandbox`. Produced by `planExecutionIsolation(...)`. The execution-isolation planner is current, but real sandbox/worktree/provider execution remains future work; current execution paths use injected or deterministic executors. A `git-worktree` strategy means “recommended/planned isolation,” not “a worktree was created.”
76
76
 
77
+ ## Workspace strategy recommendation
78
+
79
+ An advisory planning result that recommends how the human/operator should organize review and checkout state before risky work: `current-checkout`, `branch`, `manual-worktree`, or `stacked-prs`. Produced by `recommendWorkspaceStrategy(...)` and surfaced on run preflight records as `workspaceStrategy`. It is separate from the permission/risk decision and from the execution isolation plan. It never creates branches, worktrees, pull requests, provider config, or repository mutations; those guarantees are explicit in `workspaceStrategy.execution`.
80
+
77
81
  ## Governance report
78
82
 
79
83
  A redacted, structured report over SDD state, runs, and approvals. Surfaced through `vgxness_governance_report`. Useful for review before promotion.
@@ -120,7 +124,7 @@ The function that resolves a permission request. Returns `allow`, `ask`, or `den
120
124
 
121
125
  ## Preflight
122
126
 
123
- A permission decision plus execution isolation plan produced before a reserved operation. `vgxness_run_preflight` may create a pending approval when the decision is `ask`.
127
+ A permission decision plus workspace strategy recommendation and execution isolation plan produced before a reserved operation. `vgxness_run_preflight` may create a pending approval when the decision is `ask`, but it does not execute the operation, create Git branches/worktrees/PRs, or write provider config.
124
128
 
125
129
  ## Project (scope)
126
130
 
package/docs/mcp.md CHANGED
@@ -80,7 +80,7 @@ Payload tools that accept `agentId` resolve it as a canonical registry id first.
80
80
  | `vgxness_run_start` | Create a run record. | `CreateRunInput` shape | |
81
81
  | `vgxness_run_checkpoint` | Append a resumable JSON state. | `AppendRunCheckpointInput` shape | |
82
82
  | `vgxness_run_finalize` | Finalize a run with `outcome` and `outcomeReason`. | `FinalizeRunInput` shape | `outcome` must match a terminal `status`. Re-finalization is rejected. |
83
- | `vgxness_run_preflight` | Run policy evaluation and return the decision + planned execution isolation strategy. | `PreflightRunOperationInput` shape | May create a pending approval when the decision is `ask`. Does not invoke an executor. |
83
+ | `vgxness_run_preflight` | Run policy evaluation and return the decision, workspace strategy recommendation, and planned execution isolation strategy. | `PreflightRunOperationInput` shape | May create a pending approval when the decision is `ask`. Does not invoke an executor, write provider config, create branches, create worktrees, create PRs, or prove isolation exists. |
84
84
  | `vgxness_run_resume_candidates` | List recent interrupted runs for a project when the run id is unknown. | `project`; optional `limit` (1-100, default 5) | Returns failed, blocked, and needs-human candidates with `inspectInput` for `run_resume_inspect`. Does not retry, resume, mutate runs, create sandboxes/worktrees, invoke providers, or write artifacts/config. |
85
85
  | `vgxness_run_resume_inspect` | Plan-only resume advisory. | `runId` | Returns `RunResumeOrchestrationPlan` with blockers and `nextAction` (`resolve-approval`/`inspect-run`/`retry-check`/`manual-recovery`/`no-action`). |
86
86
  | `vgxness_run_resume_gate` | Evaluate retry policy for an approval without executing. | `approvalId`; optional `policy` | Default policy is `never`. See [Safety model](./safety.md) for the policy table. |
@@ -139,6 +139,8 @@ vgxness_run_checkpoint { runId, label: "after-step-1", state: {...} }
139
139
  vgxness_run_finalize { runId, outcome: "success", outcomeReason: "..." }
140
140
  ```
141
141
 
142
+ `vgxness_run_preflight` output includes `workspaceStrategy` with one of `current-checkout`, `branch`, `manual-worktree`, or `stacked-prs`. This is advisory planning metadata only. Its execution guarantees are always non-mutating (`createsBranch:false`, `createsWorktree:false`, `createsPullRequest:false`, `mutatesRepository:false`, `mutatesProviderConfig:false`). Treat `manual-worktree` and `stacked-prs` as instructions for a human or future executor to arrange review/isolation deliberately before mutation, not as evidence that VGXNESS already created Git state.
143
+
142
144
  Finding interrupted runs when the run id is unknown:
143
145
 
144
146
  ```text
package/docs/roadmap.md CHANGED
@@ -41,7 +41,7 @@ SQLite, scopes, migrations, and the run snapshot export are in.
41
41
 
42
42
  ## TUI
43
43
 
44
- The no-args TTY entrypoint now starts a read-only Home TUI with setup/provider context and placeholder panels for future read models. Broader TUI work should continue around the current MCP/CLI control-plane boundaries rather than the removed `vgxness code` runtime.
44
+ The no-args TTY entrypoint now starts a read-only Home TUI with setup/provider context plus focused read-only panels for runs, skills, provider setup, and selected SDD change state. Broader TUI work should continue around the current MCP/CLI control-plane boundaries rather than the removed `vgxness code` runtime.
45
45
 
46
46
  - **TUI dashboard screen.** Real-time view of active runs, pending approvals, and SDD blockers. Should match the cockpit surface from MCP.
47
47
  - **TUI runs screen.** Run list, drill into a run, see events/approvals/attempts, with read-only safe actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vgxness",
3
- "version": "1.15.0",
3
+ "version": "1.16.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": {
@@ -77,21 +77,21 @@
77
77
  "name": "vgxness-git-safety",
78
78
  "description": "Protects user work and repository integrity during git-related operations.",
79
79
  "version": "2026-06-16.v1",
80
- "content": "# vgxness-git-safety\n\nRole: Keep git operations explicit, reviewable, and safe.\n\nBefore any git write, inspect and report working tree status, diff, and recent commits. Preserve unrelated user changes, stage only intended files, and stop on conflicts, detached HEAD, divergent branches, dirty base, untracked build artifacts, or unclear ownership.\n\nNever commit, amend, push, merge, rebase, reset, delete branches, delete worktrees, publish releases, or create PRs unless the human explicitly asks for that exact action.\n\nOutput: git state evidence, intended files, safety blockers, and the smallest safe decision needed from the human.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
80
+ "content": "# vgxness-git-safety\n\nRole: Keep git operations explicit, reviewable, and safe.\n\nBefore any git write, inspect and report working tree status, diff, and recent commits. Preserve unrelated user changes, stage only intended files, and stop on conflicts, detached HEAD, divergent branches, dirty base, untracked build artifacts, or unclear ownership.\n\nUse `workspaceStrategy` from VGXNESS run preflight when available. Treat `manual-worktree`, `branch`, and `stacked-prs` as recommendations only; they do not prove VGXNESS created Git state. Arrange any branch/worktree/PR manually and only after explicit human approval.\n\nNever commit, amend, push, merge, rebase, reset, delete branches, delete worktrees, publish releases, or create PRs unless the human explicitly asks for that exact action.\n\nOutput: git state evidence, intended files, safety blockers, workspace strategy recommendation when present, and the smallest safe decision needed from the human.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
81
81
  "metadata": { "ownedBy": "vgxness", "kind": "registry-context-skill", "nativeProviderSkill": false, "category": "git" }
82
82
  },
83
83
  {
84
84
  "name": "vgxness-pr-prep",
85
85
  "description": "Prepares concise pull request evidence and reviewer-ready summaries.",
86
86
  "version": "2026-06-16.v1",
87
- "content": "# vgxness-pr-prep\n\nRole: Prepare a pull request only after the human explicitly asks for PR work.\n\nBefore opening a PR, inspect branch status, upstream tracking, base branch, recent commits, and diff against base. Summarize included commits, files changed, verification, rollout risk, reviewer burden, and any follow-ups.\n\nDo not push, publish, or create the PR without explicit human approval for that action. Prefer a focused PR over a broad mixed change.\n\nOutput: PR title/body draft or PR readiness report with evidence, risks, and reviewer notes.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
87
+ "content": "# vgxness-pr-prep\n\nRole: Prepare a pull request only after the human explicitly asks for PR work.\n\nBefore opening a PR, inspect branch status, upstream tracking, base branch, recent commits, and diff against base. Summarize included commits, files changed, verification, rollout risk, reviewer burden, and any follow-ups.\n\nUse the central `workspaceStrategy` recommendation as input to PR planning when present, especially `branch`, `manual-worktree`, and `stacked-prs`. It is advisory only and does not create branches or PRs.\n\nDo not push, publish, or create the PR without explicit human approval for that action. Prefer a focused PR over a broad mixed change.\n\nOutput: PR title/body draft or PR readiness report with evidence, risks, reviewer notes, and workspace strategy context when relevant.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
88
88
  "metadata": { "ownedBy": "vgxness", "kind": "registry-context-skill", "nativeProviderSkill": false, "category": "pull-request" }
89
89
  },
90
90
  {
91
91
  "name": "vgxness-stacked-prs",
92
92
  "description": "Guides reviewable stacked PR slicing for dependent changes.",
93
93
  "version": "2026-06-16.v1",
94
- "content": "# vgxness-stacked-prs\n\nRole: Split dependent work into reviewable layers when one large PR would be hard to understand or risky to review.\n\nPrefer slices such as docs groundwork, tests/contracts, domain behavior, CLI/MCP surface, and follow-up polish. Keep each PR independently reviewable, explain dependencies, and avoid mixing unrelated fixes.\n\nDo not use stacked PRs for tiny fixes or emergency repairs unless the dependency structure is genuinely needed.\n\nOutput: proposed stack, branch/order guidance, reviewer burden tradeoffs, and cleanup notes.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
94
+ "content": "# vgxness-stacked-prs\n\nRole: Split dependent work into reviewable layers when one large PR would be hard to understand or risky to review.\n\nPrefer slices such as docs groundwork, tests/contracts, domain behavior, CLI/MCP surface, and follow-up polish. Keep each PR independently reviewable, explain dependencies, and avoid mixing unrelated fixes.\n\nWhen `workspaceStrategy.strategy` is `stacked-prs`, treat it as a planning recommendation only. It does not create branches or pull requests, and critical isolation signals may still require `manual-worktree` before mutation.\n\nDo not use stacked PRs for tiny fixes or emergency repairs unless the dependency structure is genuinely needed.\n\nOutput: proposed stack, branch/order guidance, reviewer burden tradeoffs, cleanup notes, and any manual isolation prerequisite.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
95
95
  "metadata": { "ownedBy": "vgxness", "kind": "registry-context-skill", "nativeProviderSkill": false, "category": "pull-request" }
96
96
  },
97
97
  {
@@ -105,7 +105,7 @@
105
105
  "name": "vgxness-review-size-guard",
106
106
  "description": "Keeps implementation slices small enough to review safely.",
107
107
  "version": "2026-06-16.v1",
108
- "content": "# vgxness-review-size-guard\n\nRole: Protect reviewer attention and product safety by challenging oversized or mixed-scope changes.\n\nBefore large implementation, estimate affected subsystems, files, schema/storage changes, provider/setup impact, docs/tests, and verification cost. If the change is cross-cutting or hard to review, stop and recommend splitting into smaller slices.\n\nPrefer one coherent behavior change plus its tests/docs over unrelated bundles. For high-risk work, ask whether to reduce scope, split, or proceed as one larger change.\n\nOutput: review-size assessment, recommended slices, tradeoffs, and the next smallest safe step.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
108
+ "content": "# vgxness-review-size-guard\n\nRole: Protect reviewer attention and product safety by challenging oversized or mixed-scope changes.\n\nBefore large implementation, estimate affected subsystems, files, schema/storage changes, provider/setup impact, docs/tests, and verification cost. If the change is cross-cutting or hard to review, stop and recommend splitting into smaller slices.\n\nUse `workspaceStrategy` as supporting evidence for review shape when present: `branch` for focused edits, `manual-worktree` for high-risk/dirty/unknown checkout work, and `stacked-prs` for dependent review layers. The recommendation is non-mutating and should not be described as created Git state.\n\nPrefer one coherent behavior change plus its tests/docs over unrelated bundles. For high-risk work, ask whether to reduce scope, split, or proceed as one larger change.\n\nOutput: review-size assessment, recommended slices, tradeoffs, workspace strategy context, and the next smallest safe step.\n\nBoundary: This is VGXNESS registry/context skill content only. It is not a provider-native OpenCode skill, must not be installed into .opencode, must not require skills.paths, and must not load remote content.",
109
109
  "metadata": { "ownedBy": "vgxness", "kind": "registry-context-skill", "nativeProviderSkill": false, "category": "review" }
110
110
  }
111
111
  ],