@pellux/goodvibes-tui 1.13.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- package/src/runtime/render-scheduler.ts +0 -90
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// workstream-attempts.ts — the /workstream attempts surface for best-of-N.
|
|
3
|
+
//
|
|
4
|
+
// The engine runs a best-of-N work item as N sibling attempts in isolated
|
|
5
|
+
// worktrees and HOLDS the merge, exposing the candidates for a winner pick
|
|
6
|
+
// (orchestration engine.listHeldMergeGroups / proposeAttemptWinner /
|
|
7
|
+
// pickAttemptWinner — the same seam the fleet.attempts.* gateway verbs use).
|
|
8
|
+
// This module drives that seam from the /workstream command:
|
|
9
|
+
// • list — the held-merge groups awaiting a pick, with candidates.
|
|
10
|
+
// • diff <c> — one candidate's worktree diff in the existing DiffPanel.
|
|
11
|
+
// • judge — the OPTIONAL model proposal, clearly labelled a model
|
|
12
|
+
// judgment (scoredBy 'model'), with its reasons; never a pick.
|
|
13
|
+
// • pick <c> — behind a DiffPanel confirm; merges the winner and lets
|
|
14
|
+
// the engine clean the losers, then renders the receipt.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
AttemptCandidate,
|
|
19
|
+
AttemptJudgment,
|
|
20
|
+
AttemptPickResult,
|
|
21
|
+
HeldMergeGroup,
|
|
22
|
+
} from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
23
|
+
import { AttemptError } from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
24
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
25
|
+
import type { CommandContext } from '../command-registry.ts';
|
|
26
|
+
import type { WorkstreamCommandService } from '../../runtime/workstream-services.ts';
|
|
27
|
+
import { requirePanelManager } from './runtime-services.ts';
|
|
28
|
+
import { type AttemptGraphItem, renderDependentHoldLines } from './workstream-attempt-dependents.ts';
|
|
29
|
+
|
|
30
|
+
/** Resolve a workstream's items for dependent-hold display; null-safe against a stubbed engine. */
|
|
31
|
+
type ResolveWorkstreamItems = (workstreamId: string) => readonly AttemptGraphItem[] | null;
|
|
32
|
+
|
|
33
|
+
function shortId(id: string): string {
|
|
34
|
+
return id.length > 10 ? `${id.slice(0, 10)}…` : id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Resolve a group by exact id or unambiguous id prefix. */
|
|
38
|
+
function resolveGroup(groups: readonly HeldMergeGroup[], ref: string): HeldMergeGroup | { error: string } {
|
|
39
|
+
const exact = groups.find((g) => g.groupId === ref);
|
|
40
|
+
if (exact) return exact;
|
|
41
|
+
const prefix = groups.filter((g) => g.groupId.startsWith(ref));
|
|
42
|
+
if (prefix.length === 1) return prefix[0]!;
|
|
43
|
+
if (prefix.length > 1) return { error: `Group ref "${ref}" is ambiguous (${prefix.length} matches). Use the full id.` };
|
|
44
|
+
return { error: `No held-merge group found: ${ref}. Run /workstream attempts list.` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve a candidate within a group by 1-based attempt number or itemId (prefix). */
|
|
48
|
+
function resolveCandidate(group: HeldMergeGroup, ref: string): AttemptCandidate | { error: string } {
|
|
49
|
+
if (/^\d+$/.test(ref)) {
|
|
50
|
+
const cand = group.candidates.find((c) => c.attemptIndex + 1 === Number(ref));
|
|
51
|
+
return cand ?? { error: `No attempt #${ref} in group ${shortId(group.groupId)} (1–${group.candidates.length}).` };
|
|
52
|
+
}
|
|
53
|
+
const byId = group.candidates.filter((c) => c.itemId === ref || c.itemId.startsWith(ref));
|
|
54
|
+
if (byId.length === 1) return byId[0]!;
|
|
55
|
+
if (byId.length > 1) return { error: `Candidate ref "${ref}" is ambiguous. Use the attempt number instead.` };
|
|
56
|
+
return { error: `No candidate "${ref}" in group ${shortId(group.groupId)}.` };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function candidateLine(c: AttemptCandidate): string {
|
|
60
|
+
const files = c.diff ? `${c.diff.files.length} file(s)` : 'no diff';
|
|
61
|
+
const state = c.state === 'held-merge' ? 'held (pick-ready)' : `failed${c.failureReason ? `: ${c.failureReason}` : ''}`;
|
|
62
|
+
return ` ${c.attemptIndex + 1}. [${state}] ${c.title} — ${files}, item ${shortId(c.itemId)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderGroupList(groups: readonly HeldMergeGroup[], resolveItems?: ResolveWorkstreamItems): string {
|
|
66
|
+
if (groups.length === 0) {
|
|
67
|
+
return 'No best-of-N groups awaiting a pick. A worktree-isolated item with attempts>1 appears here once its siblings finish.';
|
|
68
|
+
}
|
|
69
|
+
const lines: string[] = ['Best-of-N groups (held for a winner pick):'];
|
|
70
|
+
for (const g of groups) {
|
|
71
|
+
lines.push(` ${shortId(g.groupId)} — "${g.sourceTitle}" (workstream ${shortId(g.workstreamId)}) ${g.ready ? 'READY' : 'still running'}${g.autoAccept ? ', auto-accept' : ''}`);
|
|
72
|
+
for (const c of g.candidates) lines.push(candidateLine(c));
|
|
73
|
+
if (g.judgment?.proposedWinnerItemId) {
|
|
74
|
+
lines.push(` judge proposal: item ${shortId(g.judgment.proposedWinnerItemId)} (model, advisory)`);
|
|
75
|
+
}
|
|
76
|
+
// Non-leaf best-of-N: name the dependents this group holds, so a group that
|
|
77
|
+
// gates downstream work reads as such (leaf groups add nothing here).
|
|
78
|
+
const items = resolveItems?.(g.workstreamId);
|
|
79
|
+
if (items) {
|
|
80
|
+
lines.push(...renderDependentHoldLines(items, g.candidates.map((c) => c.itemId)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
lines.push('Compare: /workstream attempts diff <group> <#> Judge: /workstream attempts judge <group> Pick: /workstream attempts pick <group> <#>');
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The model proposal, CLEARLY labelled a model judgment (never ground truth), with reasons. */
|
|
88
|
+
function renderJudgment(group: HeldMergeGroup, judgment: AttemptJudgment): string {
|
|
89
|
+
const lines: string[] = [`Judge proposal for group ${shortId(group.groupId)} — "${group.sourceTitle}"`];
|
|
90
|
+
lines.push(' MODEL PROPOSAL (scoredBy: model) — advisory only; a human still confirms the pick.');
|
|
91
|
+
if (judgment.proposedWinnerItemId) {
|
|
92
|
+
const cand = group.candidates.find((c) => c.itemId === judgment.proposedWinnerItemId);
|
|
93
|
+
lines.push(` Proposed winner: ${cand ? `attempt ${cand.attemptIndex + 1} — ${cand.title}` : `item ${shortId(judgment.proposedWinnerItemId)}`}`);
|
|
94
|
+
} else {
|
|
95
|
+
lines.push(' Proposed winner: none — the judge declined to choose.');
|
|
96
|
+
}
|
|
97
|
+
if (judgment.model) lines.push(` Model: ${judgment.model}`);
|
|
98
|
+
if (judgment.reasons.length > 0) {
|
|
99
|
+
lines.push(' Reasons:');
|
|
100
|
+
for (const r of judgment.reasons) lines.push(` - ${r}`);
|
|
101
|
+
}
|
|
102
|
+
if (judgment.proposedWinnerItemId) {
|
|
103
|
+
const cand = group.candidates.find((c) => c.itemId === judgment.proposedWinnerItemId);
|
|
104
|
+
lines.push(` Accept it with: /workstream attempts pick ${shortId(group.groupId)} ${cand ? cand.attemptIndex + 1 : judgment.proposedWinnerItemId}`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderPickReceipt(result: AttemptPickResult): string {
|
|
110
|
+
const losers = result.loserItemIds.length;
|
|
111
|
+
return [
|
|
112
|
+
`[Workstream] Winner picked for group ${shortId(result.groupId)}${result.auto ? ' (auto-accepted by judge)' : ''}.`,
|
|
113
|
+
` Winner item ${shortId(result.winnerItemId)} merged through the integration lane.`,
|
|
114
|
+
` ${losers} loser worktree(s) cleaned by the engine.`,
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Handle `/workstream attempts …`. Returns true when it consumed the subcommand
|
|
120
|
+
* (so the main handler can stop), false when `attempts` was not the subcommand.
|
|
121
|
+
*/
|
|
122
|
+
export async function handleAttemptsSubcommand(ctx: CommandContext, service: WorkstreamCommandService, args: readonly string[]): Promise<boolean> {
|
|
123
|
+
if (args[0] !== 'attempts') return false;
|
|
124
|
+
const action = args[1] ?? 'list';
|
|
125
|
+
|
|
126
|
+
let groups: HeldMergeGroup[];
|
|
127
|
+
try {
|
|
128
|
+
groups = await service.engine.listHeldMergeGroups();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
ctx.print(`Could not read best-of-N groups: ${summarizeError(err)}`);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === 'list') {
|
|
135
|
+
const resolveItems: ResolveWorkstreamItems = (workstreamId) => {
|
|
136
|
+
try {
|
|
137
|
+
return (service.engine.getWorkstream(workstreamId)?.items as readonly AttemptGraphItem[] | undefined) ?? null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
ctx.print(renderGroupList(groups, resolveItems));
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const groupRef = args[2];
|
|
147
|
+
if (!groupRef) {
|
|
148
|
+
ctx.print(`Usage: /workstream attempts ${action} <group> ${action === 'list' ? '' : '<#>'}`.trim());
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const group = resolveGroup(groups, groupRef);
|
|
152
|
+
if ('error' in group) {
|
|
153
|
+
ctx.print(group.error);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (action === 'judge') {
|
|
158
|
+
try {
|
|
159
|
+
const judgment = await service.engine.proposeAttemptWinner(group.groupId);
|
|
160
|
+
ctx.print(renderJudgment(group, judgment));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
ctx.print(err instanceof AttemptError
|
|
163
|
+
? `No judge available: ${err.message}. Configure a judge model to get a proposal; you can still pick manually.`
|
|
164
|
+
: `Judge failed: ${summarizeError(err)}`);
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (action === 'diff') {
|
|
170
|
+
const candRef = args[3];
|
|
171
|
+
if (!candRef) {
|
|
172
|
+
ctx.print(`Candidates in group ${shortId(group.groupId)}:\n${group.candidates.map(candidateLine).join('\n')}\nView one: /workstream attempts diff ${shortId(group.groupId)} <#>`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
const cand = resolveCandidate(group, candRef);
|
|
176
|
+
if ('error' in cand) { ctx.print(cand.error); return true; }
|
|
177
|
+
if (!cand.diff || !cand.diff.unifiedDiff.trim()) {
|
|
178
|
+
ctx.print(`Attempt ${cand.attemptIndex + 1} has no viewable diff (${cand.state === 'failed' ? 'it failed' : 'its worktree was already cleaned'}).`);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
const { DiffPanel } = await import('../../panels/diff-panel.ts');
|
|
182
|
+
const pm = requirePanelManager(ctx);
|
|
183
|
+
let panel = pm.getAllOpen().find((p) => p.id === 'diff');
|
|
184
|
+
if (!panel) { try { panel = pm.open('diff'); } catch { ctx.print('Could not open the diff panel.'); return true; } }
|
|
185
|
+
pm.activateById('diff');
|
|
186
|
+
if (!pm.isVisible()) pm.show();
|
|
187
|
+
ctx.focusPanels?.();
|
|
188
|
+
(panel as InstanceType<typeof DiffPanel>).loadRawDiff(cand.diff.unifiedDiff);
|
|
189
|
+
ctx.print(`Showing attempt ${cand.attemptIndex + 1} ("${cand.title}") diff in the diff panel.`);
|
|
190
|
+
ctx.renderRequest();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (action === 'pick') {
|
|
195
|
+
const candRef = args[3];
|
|
196
|
+
if (!candRef) { ctx.print(`Usage: /workstream attempts pick ${shortId(group.groupId)} <#>`); return true; }
|
|
197
|
+
if (!group.ready) { ctx.print(`Group ${shortId(group.groupId)} is not ready — its siblings are still running. Wait until every attempt is terminal.`); return true; }
|
|
198
|
+
const cand = resolveCandidate(group, candRef);
|
|
199
|
+
if ('error' in cand) { ctx.print(cand.error); return true; }
|
|
200
|
+
if (cand.state !== 'held-merge') { ctx.print(`Attempt ${cand.attemptIndex + 1} failed — only a held (pick-ready) candidate can win.`); return true; }
|
|
201
|
+
|
|
202
|
+
const { DiffPanel } = await import('../../panels/diff-panel.ts');
|
|
203
|
+
const pm = requirePanelManager(ctx);
|
|
204
|
+
let panel = pm.getAllOpen().find((p) => p.id === 'diff');
|
|
205
|
+
if (!panel) { try { panel = pm.open('diff'); } catch { ctx.print('Could not open the diff panel to confirm.'); return true; } }
|
|
206
|
+
pm.activateById('diff');
|
|
207
|
+
if (!pm.isVisible()) pm.show();
|
|
208
|
+
ctx.focusPanels?.();
|
|
209
|
+
const diffPanel = panel as InstanceType<typeof DiffPanel>;
|
|
210
|
+
if (cand.diff?.unifiedDiff.trim()) diffPanel.loadRawDiff(cand.diff.unifiedDiff);
|
|
211
|
+
else diffPanel.showDiff(cand.title, '@@ pick @@\n (no diff to preview for this candidate)');
|
|
212
|
+
|
|
213
|
+
diffPanel.confirmOverlay.arm({
|
|
214
|
+
id: `${group.groupId}:${cand.itemId}`,
|
|
215
|
+
label: `Pick attempt ${cand.attemptIndex + 1} ("${cand.title}") — merge it, clean the ${group.candidates.length - 1} other worktree(s)`,
|
|
216
|
+
verb: 'Pick',
|
|
217
|
+
onConfirm: async () => {
|
|
218
|
+
try {
|
|
219
|
+
const result = await service.engine.pickAttemptWinner(group.groupId, cand.itemId);
|
|
220
|
+
pm.close('diff');
|
|
221
|
+
ctx.focusPrompt?.();
|
|
222
|
+
ctx.print(renderPickReceipt(result));
|
|
223
|
+
} catch (err) {
|
|
224
|
+
pm.close('diff');
|
|
225
|
+
ctx.focusPrompt?.();
|
|
226
|
+
ctx.print(err instanceof AttemptError ? `Pick refused: ${err.message}` : `Pick failed: ${summarizeError(err)}`);
|
|
227
|
+
}
|
|
228
|
+
ctx.renderRequest();
|
|
229
|
+
},
|
|
230
|
+
onCancel: () => {
|
|
231
|
+
pm.close('diff');
|
|
232
|
+
ctx.focusPrompt?.();
|
|
233
|
+
ctx.print('Pick cancelled — nothing merged, no worktree cleaned.');
|
|
234
|
+
ctx.renderRequest();
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
ctx.print(`Confirm the pick in the diff panel: Enter/y to merge attempt ${cand.attemptIndex + 1} and clean the losers, n/Esc to cancel.`);
|
|
238
|
+
ctx.renderRequest();
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
ctx.print(`Unknown attempts action "${action}". Use: list | diff <group> <#> | judge <group> | pick <group> <#>.`);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
// planning-runtime.ts), NOT a panel — a multi-phase proposal is too rich for
|
|
12
12
|
// a one-line confirm overlay, and Pillar-3 doctrine keeps work visible in the
|
|
13
13
|
// transcript. create -> approve -> launch is a real three-step flow (edit and
|
|
14
|
-
// cancel apply to the pending proposal too)
|
|
14
|
+
// cancel apply to the pending proposal too). The transcript IS the plan-review
|
|
15
|
+
// gate: between create and approve the proposal is reshapable in place —
|
|
16
|
+
// edit-item rewrites one item's brief, remove-item drops an item (and unlinks
|
|
17
|
+
// it from siblings' dependencies), move-item reorders authoring order, and
|
|
18
|
+
// each re-renders the whole proposal and clears any prior approval, so nothing
|
|
19
|
+
// launches until the reshaped plan is explicitly re-approved. The engine's own createWorkstream
|
|
15
20
|
// immediately materializes a real, ticking-eligible Workstream with no
|
|
16
21
|
// pre-creation "draft" concept, so approval happens on a TUI-held draft
|
|
17
22
|
// (workstream-services.ts's WorkstreamDraft) before anything is created in
|
|
@@ -22,7 +27,10 @@
|
|
|
22
27
|
import { AdaptivePlanner } from '@pellux/goodvibes-sdk/platform/core';
|
|
23
28
|
import type { CreateWorkstreamInput, PhaseKind, PhaseRole, WorkItem, WorkItemState, Workstream, WorkstreamIsolation } from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
24
29
|
import type { WorkstreamCommandService, WorkstreamDraft, WorkstreamDraftProvenance } from '../../runtime/workstream-services.ts';
|
|
30
|
+
import { validateAttempts } from '../../runtime/workstream-attempts-validation.ts';
|
|
31
|
+
import { handleAttemptsSubcommand } from './workstream-attempts.ts';
|
|
25
32
|
import type { CommandContext, CommandRegistry } from '../command-registry.ts';
|
|
33
|
+
import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
|
|
26
34
|
|
|
27
35
|
// ---------------------------------------------------------------------------
|
|
28
36
|
// Formatting helpers
|
|
@@ -53,6 +61,47 @@ function templateForPhaseKind(kind: PhaseKind): string {
|
|
|
53
61
|
return kind === 'review' || kind === 'gate' ? 'general' : 'engineer';
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Pre-fan-out quota consultation for /workstream launch. Assesses whether the
|
|
66
|
+
* draft's work-item count (the fan-out this launch is about to create, worst
|
|
67
|
+
* case — items can run concurrently within a phase) is likely to exhaust the
|
|
68
|
+
* active provider's quota window, grounded in observed rate-limit signals
|
|
69
|
+
* (quota.fanout.get). Returns a printable warning + evidence when the daemon
|
|
70
|
+
* reports 'likely-exhausts'; returns null for 'unlikely'/'unknown' (no
|
|
71
|
+
* evidence of risk is not itself evidence of safety, so those verdicts never
|
|
72
|
+
* block) and whenever the check itself can't run (daemon unavailable/
|
|
73
|
+
* unreachable, or no active provider) — an infra gap in the quota check is
|
|
74
|
+
* not grounds to block launching work the operator already approved.
|
|
75
|
+
*/
|
|
76
|
+
async function checkFanoutQuotaWarning(ctx: CommandContext, draft: WorkstreamDraft): Promise<string | null> {
|
|
77
|
+
const provider = ctx.session.runtime.provider;
|
|
78
|
+
if (!provider) return null;
|
|
79
|
+
const rpc = getOperatorRpc(ctx);
|
|
80
|
+
if (!rpc.available) return null;
|
|
81
|
+
const agentCount = Math.max(1, draft.proposal.workItems.length);
|
|
82
|
+
const assessment = await rpc.sdk.operator.invoke('quota.fanout.get', { provider, agentCount }).catch((error: unknown) => {
|
|
83
|
+
ctx.print(`[workstream launch] quota check could not run: ${describeOperatorRpcError(error)}`);
|
|
84
|
+
return null;
|
|
85
|
+
});
|
|
86
|
+
if (!assessment || assessment.verdict !== 'likely-exhausts') return null;
|
|
87
|
+
const ev = assessment.evidence;
|
|
88
|
+
const evidenceParts = [
|
|
89
|
+
`requested=${ev.requestedAgents}`,
|
|
90
|
+
`recentRateLimitCount=${ev.recentRateLimitCount}`,
|
|
91
|
+
ev.activeCooldownMs !== undefined ? `activeCooldownMs=${ev.activeCooldownMs}` : null,
|
|
92
|
+
ev.observedRemaining !== undefined ? `observedRemaining=${ev.observedRemaining}` : null,
|
|
93
|
+
ev.observedLimit !== undefined ? `observedLimit=${ev.observedLimit}` : null,
|
|
94
|
+
].filter((part): part is string => part !== null);
|
|
95
|
+
return [
|
|
96
|
+
`[workstream launch] WARNING — ${provider} likely exhausts its quota window for ${agentCount} agent(s) fanning out.`,
|
|
97
|
+
` reason: ${assessment.reason}`,
|
|
98
|
+
` evidence: ${evidenceParts.join(' ')}`,
|
|
99
|
+
'',
|
|
100
|
+
`Launch anyway: /workstream launch ${draft.id} --force`,
|
|
101
|
+
'Cancel: do nothing — the approved proposal stays ready to launch later.',
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
|
|
56
105
|
/**
|
|
57
106
|
* phase.role for a 'custom' phase is the free-text description passed to
|
|
58
107
|
* `/workstream insert-phase` — it is purely COSMETIC. Neither
|
|
@@ -142,11 +191,21 @@ function formatItemDependencyNote(item: WorkItem, ws: Workstream): string {
|
|
|
142
191
|
*/
|
|
143
192
|
function formatDraftItems(spec: CreateWorkstreamInput): string[] {
|
|
144
193
|
const titleById = new Map(spec.items.map((it) => [it.id ?? it.title, it.title] as const));
|
|
145
|
-
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
spec.items.forEach((it, i) => {
|
|
146
196
|
const deps = it.dependsOn ?? [];
|
|
147
197
|
const after = deps.length > 0 ? ` (after: ${deps.map((d) => titleById.get(d) ?? d).join(', ')})` : '';
|
|
148
|
-
|
|
198
|
+
const attempts = (it.attempts ?? 1) > 1 ? ` [best-of-${it.attempts}${it.autoAcceptWinner ? ', auto-accept winner' : ''}]` : '';
|
|
199
|
+
lines.push(` ${i + 1}. ${it.title}${after}${attempts}`);
|
|
200
|
+
// Show the brief (the instructions the item's agent runs) only when it says
|
|
201
|
+
// something the title doesn't — an edited brief (via /workstream edit-item)
|
|
202
|
+
// must be visible on the review surface, but a decomposition whose title and
|
|
203
|
+
// brief coincide would just repeat itself.
|
|
204
|
+
if (it.task.trim() && it.task.trim() !== it.title.trim()) {
|
|
205
|
+
lines.push(` brief: ${it.task.trim()}`);
|
|
206
|
+
}
|
|
149
207
|
});
|
|
208
|
+
return lines;
|
|
150
209
|
}
|
|
151
210
|
|
|
152
211
|
function summarizeItemStates(ws: Workstream): string {
|
|
@@ -204,18 +263,31 @@ function renderDraftProposal(draft: WorkstreamDraft): string {
|
|
|
204
263
|
});
|
|
205
264
|
lines.push('Work items (ordinal order):');
|
|
206
265
|
lines.push(...formatDraftItems(draft.spec));
|
|
266
|
+
// Best-of-N plan validation (workstream-attempts-validation.ts): surface the
|
|
267
|
+
// worktree/stable-id constraint breaks here so a violating plan is visible
|
|
268
|
+
// before approve, and blocked at launch. Non-leaf best-of-N (items with
|
|
269
|
+
// dependents) is allowed. Notes (e.g. the engine's attempts cap) are advisory.
|
|
270
|
+
const attemptsCheck = validateAttempts(draft.spec);
|
|
271
|
+
if (attemptsCheck.violations.length > 0) {
|
|
272
|
+
lines.push('Best-of-N: plan is INVALID and cannot launch until fixed:');
|
|
273
|
+
for (const v of attemptsCheck.violations) lines.push(` ✗ ${v}`);
|
|
274
|
+
} else if (attemptsCheck.hasAttempts) {
|
|
275
|
+
lines.push('Best-of-N: worktree + stable-id constraints satisfied — winners are chosen via /workstream attempts pick, and any dependents wait for the winner to be picked and merged.');
|
|
276
|
+
}
|
|
277
|
+
for (const n of attemptsCheck.notes) lines.push(` note: ${n}`);
|
|
207
278
|
lines.push(
|
|
208
279
|
draft.approved
|
|
209
280
|
? `Approved. Launch with: /workstream launch ${draft.id}`
|
|
210
281
|
: `Approve with: /workstream approve ${draft.id}`,
|
|
211
282
|
);
|
|
212
|
-
lines.push(`
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
283
|
+
lines.push(`Reshape: /workstream edit-item ${draft.id} <#> <brief...> | remove-item ${draft.id} <#> | move-item ${draft.id} <#> <pos>`);
|
|
284
|
+
lines.push(`Re-decompose from a new goal: /workstream edit ${draft.id} <new task...> Discard: /workstream cancel ${draft.id}`);
|
|
285
|
+
// Honest durability note (see workstream-services.ts's REALITY-WINS doc):
|
|
286
|
+
// the engine has no pre-launch draft concept, but the TUI journals this
|
|
287
|
+
// proposal to disk (.goodvibes/orchestration/drafts/) and reloads it at
|
|
288
|
+
// startup, so it survives a restart and is here to launch afterward — its
|
|
289
|
+
// approval state included.
|
|
290
|
+
lines.push('Note: saved to .goodvibes/orchestration/drafts/ — survives a restart until you launch or cancel it.');
|
|
219
291
|
return lines.join('\n');
|
|
220
292
|
}
|
|
221
293
|
|
|
@@ -285,18 +357,16 @@ function resolveWorkstream(service: WorkstreamCommandService, ref: string): Work
|
|
|
285
357
|
}
|
|
286
358
|
|
|
287
359
|
/**
|
|
288
|
-
* approve/edit/launch all fail this way when `id` doesn't resolve to a
|
|
289
|
-
* draft.
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
* service is
|
|
294
|
-
*
|
|
360
|
+
* approve/edit/launch/*-item all fail this way when `id` doesn't resolve to a
|
|
361
|
+
* held draft. Drafts are now journaled to disk and reloaded at startup (see
|
|
362
|
+
* renderDraftProposal's note and workstream-services.ts's REALITY-WINS doc), so
|
|
363
|
+
* a restart no longer silently wipes them — a missing id is a typo or a
|
|
364
|
+
* stale/already-launched proposal, and the plain message is the honest guess.
|
|
365
|
+
* `service` is kept in the signature so callers stay uniform and a future
|
|
366
|
+
* "did you mean <closest id>?" hint has somewhere to live.
|
|
295
367
|
*/
|
|
296
|
-
function draftNotFoundMessage(
|
|
297
|
-
|
|
298
|
-
if (service.listDrafts().length > 0) return base;
|
|
299
|
-
return `${base} (proposals aren't saved — a TUI restart since you created it would explain this; recreate it with /workstream create)`;
|
|
368
|
+
function draftNotFoundMessage(_service: WorkstreamCommandService, id: string): string {
|
|
369
|
+
return `No pending proposal found: ${id}`;
|
|
300
370
|
}
|
|
301
371
|
|
|
302
372
|
// ---------------------------------------------------------------------------
|
|
@@ -307,8 +377,8 @@ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): vo
|
|
|
307
377
|
registry.register({
|
|
308
378
|
name: 'workstream',
|
|
309
379
|
description: 'Author and oversee multi-phase agent workstreams (orchestration engine)',
|
|
310
|
-
usage: 'create [--isolation shared|worktree] <task...> | list | status [id] | insert-phase <id> <description...> | approve <id> | edit <id> [--isolation shared|worktree] <task...> | launch <id> | cancel <id>',
|
|
311
|
-
argsHint: 'create [--isolation worktree] <task> | list | status [id] | approve | edit | launch | cancel',
|
|
380
|
+
usage: 'create [--isolation shared|worktree] <task...> | list | status [id] | insert-phase <id> <description...> | edit-item <id> <item#> <brief...> | remove-item <id> <item#> | move-item <id> <item#> <pos> | approve <id> | edit <id> [--isolation shared|worktree] <task...> | launch <id> [--force] | cancel <id> | attempts list|diff|judge|pick',
|
|
381
|
+
argsHint: 'create [--isolation worktree] <task> | list | status [id] | edit-item | remove-item | move-item | approve | edit | launch | cancel | attempts',
|
|
312
382
|
handler: async (args, ctx: CommandContext) => {
|
|
313
383
|
const service = ctx.session.workstreamEngine;
|
|
314
384
|
if (!service) {
|
|
@@ -318,6 +388,22 @@ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): vo
|
|
|
318
388
|
|
|
319
389
|
const sub = args[0];
|
|
320
390
|
|
|
391
|
+
// Best-of-N surface: /workstream attempts list|diff|judge|pick (workstream-attempts.ts).
|
|
392
|
+
if (await handleAttemptsSubcommand(ctx, service, args)) return;
|
|
393
|
+
|
|
394
|
+
/** Thread editItem/removeItem/moveItem's tri-state result to the transcript: a missing draft gets the restart-aware not-found message, a bad reference/argument gets its honest reason, and a success re-renders the whole reshaped proposal so the review surface always shows the current plan. */
|
|
395
|
+
const printItemEdit = (id: string, res: WorkstreamDraft | { error: string } | undefined): void => {
|
|
396
|
+
if (res === undefined) {
|
|
397
|
+
ctx.print(draftNotFoundMessage(service, id));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if ('error' in res) {
|
|
401
|
+
ctx.print(res.error);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
ctx.print(renderDraftProposal(res));
|
|
405
|
+
};
|
|
406
|
+
|
|
321
407
|
if (!sub || sub === 'list') {
|
|
322
408
|
ctx.print(renderWorkstreamList(service));
|
|
323
409
|
return;
|
|
@@ -419,10 +505,47 @@ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): vo
|
|
|
419
505
|
return;
|
|
420
506
|
}
|
|
421
507
|
|
|
422
|
-
if (sub === '
|
|
508
|
+
if (sub === 'edit-item') {
|
|
423
509
|
const id = args[1];
|
|
510
|
+
const itemRef = args[2];
|
|
511
|
+
const brief = args.slice(3).join(' ').trim();
|
|
512
|
+
if (!id || !itemRef || !brief) {
|
|
513
|
+
ctx.print('Usage: /workstream edit-item <id> <item#> <new brief...>');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
printItemEdit(id, service.editItem(id, itemRef, brief));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (sub === 'remove-item') {
|
|
521
|
+
const id = args[1];
|
|
522
|
+
const itemRef = args[2];
|
|
523
|
+
if (!id || !itemRef) {
|
|
524
|
+
ctx.print('Usage: /workstream remove-item <id> <item#>');
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
printItemEdit(id, service.removeItem(id, itemRef));
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (sub === 'move-item') {
|
|
532
|
+
const id = args[1];
|
|
533
|
+
const itemRef = args[2];
|
|
534
|
+
const posRaw = args[3];
|
|
535
|
+
const position = Number(posRaw);
|
|
536
|
+
if (!id || !itemRef || posRaw === undefined || !Number.isInteger(position)) {
|
|
537
|
+
ctx.print('Usage: /workstream move-item <id> <item#> <new-position#>');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
printItemEdit(id, service.moveItem(id, itemRef, position));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (sub === 'launch') {
|
|
545
|
+
const force = args.includes('--force');
|
|
546
|
+
const id = args.find((arg, index) => index > 0 && arg !== '--force');
|
|
424
547
|
if (!id) {
|
|
425
|
-
ctx.print('Usage: /workstream launch <id>');
|
|
548
|
+
ctx.print('Usage: /workstream launch <id> [--force]');
|
|
426
549
|
return;
|
|
427
550
|
}
|
|
428
551
|
const draft = service.getDraft(id);
|
|
@@ -434,6 +557,18 @@ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): vo
|
|
|
434
557
|
ctx.print(`Proposal ${id} is not approved yet. Run /workstream approve ${id} first.`);
|
|
435
558
|
return;
|
|
436
559
|
}
|
|
560
|
+
const attemptsCheck = validateAttempts(draft.spec);
|
|
561
|
+
if (attemptsCheck.violations.length > 0) {
|
|
562
|
+
ctx.print(`Cannot launch ${id} — best-of-N plan constraints are violated:\n${attemptsCheck.violations.map((v) => ` - ${v}`).join('\n')}\nFix the plan (or drop the attempts) and re-approve.`);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!force) {
|
|
566
|
+
const quotaWarning = await checkFanoutQuotaWarning(ctx, draft);
|
|
567
|
+
if (quotaWarning) {
|
|
568
|
+
ctx.print(quotaWarning);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
437
572
|
const result = service.launchDraft(id);
|
|
438
573
|
if (!result) {
|
|
439
574
|
ctx.print(`Could not launch ${id}.`);
|
|
@@ -478,9 +613,12 @@ export function registerWorkstreamRuntimeCommands(registry: CommandRegistry): vo
|
|
|
478
613
|
+ ' /workstream list\n'
|
|
479
614
|
+ ' /workstream status [id]\n'
|
|
480
615
|
+ ' /workstream insert-phase <id> <description...>\n'
|
|
616
|
+
+ ' /workstream edit-item <id> <item#> <new brief...>\n'
|
|
617
|
+
+ ' /workstream remove-item <id> <item#>\n'
|
|
618
|
+
+ ' /workstream move-item <id> <item#> <new-position#>\n'
|
|
481
619
|
+ ' /workstream approve <id>\n'
|
|
482
620
|
+ ' /workstream edit <id> [--isolation shared|worktree] <new task...>\n'
|
|
483
|
-
+ ' /workstream launch <id
|
|
621
|
+
+ ' /workstream launch <id> [--force]\n'
|
|
484
622
|
+ ' /workstream cancel <id>',
|
|
485
623
|
);
|
|
486
624
|
},
|
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
import { getPersistedWorktreeMeta, reviewWorktreeAttachments, summarizeWorktreeOwnership } from '@/runtime/index.ts';
|
|
2
|
+
import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
|
|
2
3
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
3
4
|
import { openCommandPanel, requireShellPaths } from './runtime-services.ts';
|
|
5
|
+
import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
|
|
6
|
+
|
|
7
|
+
/** Compact per-row setup-state tag for /worktree review — absent when setup has never run; failure stands out. */
|
|
8
|
+
export function formatSetupTag(setup: ManagedWorktreeMeta['setup']): string {
|
|
9
|
+
if (!setup) return '';
|
|
10
|
+
if (setup.state === 'failed') return ' setup:FAILED';
|
|
11
|
+
if (setup.state === 'succeeded') return ' setup:ok';
|
|
12
|
+
return ' setup:skipped';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Full setup-state block for /worktree inspect — the failing step + captured output when failed. */
|
|
16
|
+
export function formatSetupDetail(setup: ManagedWorktreeMeta['setup']): string[] {
|
|
17
|
+
if (!setup) return [' setup: never run'];
|
|
18
|
+
if (setup.state !== 'failed') return [` setup: ${setup.state}`];
|
|
19
|
+
const failingStep = setup.steps.find((step) => !step.ok);
|
|
20
|
+
return [
|
|
21
|
+
` setup: FAILED — ${setup.error ?? 'unknown error'}`,
|
|
22
|
+
...(failingStep
|
|
23
|
+
? [
|
|
24
|
+
` failing step (${failingStep.kind}): ${failingStep.label}${failingStep.exitCode !== undefined ? ` (exit ${failingStep.exitCode})` : ''}`,
|
|
25
|
+
...(failingStep.output ? [` output: ${failingStep.output.slice(0, 2000)}`] : []),
|
|
26
|
+
]
|
|
27
|
+
: []),
|
|
28
|
+
];
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
export function registerWorktreeRuntimeCommands(registry: CommandRegistry): void {
|
|
6
32
|
registry.register({
|
|
7
33
|
name: 'worktree',
|
|
8
34
|
aliases: ['worktrees'],
|
|
9
35
|
description: 'Review and manage orchestrator-owned git worktrees',
|
|
10
|
-
usage: '[review|panel|inspect <path>|attach <path> <session|task> <id>|session <id>|task <id>|recover <session|task> <id>|pause <path>|resume <path>|keep <path>|discard <path>|cleanup <path>]',
|
|
36
|
+
usage: '[review|panel|inspect <path>|setup <path>|attach <path> <session|task> <id>|session <id>|task <id>|recover <session|task> <id>|pause <path>|resume <path>|keep <path>|discard <path>|cleanup <path>]',
|
|
11
37
|
async handler(args, ctx) {
|
|
12
38
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
13
39
|
const runtime = ctx.workspace.worktreeRegistry;
|
|
@@ -32,6 +58,7 @@ export function registerWorktreeRuntimeCommands(registry: CommandRegistry): void
|
|
|
32
58
|
return;
|
|
33
59
|
}
|
|
34
60
|
const nextSteps = [
|
|
61
|
+
record.setup?.state === 'failed' ? `/worktree setup ${record.path}` : null,
|
|
35
62
|
record.state === 'paused' ? `/worktree resume ${record.path}` : null,
|
|
36
63
|
record.state === 'discard' || record.state === 'pending-cleanup' ? `/worktree cleanup ${record.path}` : null,
|
|
37
64
|
record.state === 'kept' ? `/worktree keep ${record.path}` : null,
|
|
@@ -47,10 +74,37 @@ export function registerWorktreeRuntimeCommands(registry: CommandRegistry): void
|
|
|
47
74
|
` session: ${record.sessionId ?? 'n/a'}`,
|
|
48
75
|
` task: ${record.taskId ?? 'n/a'}`,
|
|
49
76
|
` updated: ${new Date(record.updatedAt).toLocaleString()}`,
|
|
77
|
+
...formatSetupDetail(record.setup),
|
|
50
78
|
...(nextSteps.length > 0 ? [' next:', ...nextSteps.map((step) => ` ${step}`)] : [' next: /worktree review']),
|
|
51
79
|
].join('\n'));
|
|
52
80
|
return;
|
|
53
81
|
}
|
|
82
|
+
if (sub === 'setup') {
|
|
83
|
+
const path = args[1];
|
|
84
|
+
if (!path) {
|
|
85
|
+
ctx.print('Usage: /worktree setup <path>');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const rpc = getOperatorRpc(ctx);
|
|
89
|
+
if (!rpc.available) {
|
|
90
|
+
ctx.print(`[worktree setup] ${rpc.reason}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
ctx.print(`[worktree setup] Re-running cold-start setup for ${path}...`);
|
|
94
|
+
try {
|
|
95
|
+
const { setup } = await rpc.sdk.operator.invoke('worktrees.setup.run', { path });
|
|
96
|
+
if (setup.state === 'skipped') {
|
|
97
|
+
ctx.print('[worktree setup] skipped — no setup commands or carry-over globs are configured.');
|
|
98
|
+
} else if (setup.state === 'succeeded') {
|
|
99
|
+
ctx.print(`[worktree setup] succeeded (${setup.steps.length} step(s)).`);
|
|
100
|
+
} else {
|
|
101
|
+
ctx.print(formatSetupDetail(setup).join('\n'));
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
ctx.print(`[worktree setup] round-trip request failed: ${describeOperatorRpcError(error)}`);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
54
108
|
if (sub === 'attach') {
|
|
55
109
|
const path = args[1];
|
|
56
110
|
const targetKind = args[2];
|
|
@@ -128,7 +182,7 @@ export function registerWorktreeRuntimeCommands(registry: CommandRegistry): void
|
|
|
128
182
|
` session attached: ${summary.sessionAttached} task attached: ${summary.taskAttached}`,
|
|
129
183
|
` agent owned: ${summary.agentOwned} orchestrator owned: ${summary.orchestratorOwned} manual: ${summary.manualOwned}`,
|
|
130
184
|
...rows.map((row) => (
|
|
131
|
-
` ${row.kind.padEnd(12)} ${row.state.padEnd(15)} ${row.branch.padEnd(22)} session=${(row.sessionId ?? '-').padEnd(12)} task=${(row.taskId ?? '-').padEnd(12)} ${row.path}`
|
|
185
|
+
` ${row.kind.padEnd(12)} ${row.state.padEnd(15)} ${row.branch.padEnd(22)} session=${(row.sessionId ?? '-').padEnd(12)} task=${(row.taskId ?? '-').padEnd(12)} ${row.path}${formatSetupTag(row.setup)}`
|
|
132
186
|
)),
|
|
133
187
|
].join('\n')
|
|
134
188
|
: 'Worktree Review\n No worktrees discovered.');
|