@pellux/goodvibes-tui 0.19.81 → 0.19.83
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 +10 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/input/commands/planning-runtime.ts +65 -6
- package/src/main.ts +5 -0
- package/src/panels/builtin/agent.ts +1 -0
- package/src/panels/builtin/shared.ts +2 -0
- package/src/panels/panel-manager.ts +6 -1
- package/src/panels/project-planning-panel.ts +272 -1
- package/src/planning/project-planning-coordinator.ts +301 -9
- package/src/runtime/bootstrap-core.ts +2 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/tools/wrfc-agent-guard.ts +83 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.19.83] — 2026-05-09
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
- debd3a93 Fix planning loop and WRFC root spawning
|
|
11
|
+
|
|
12
|
+
## [0.19.82] — 2026-05-09
|
|
13
|
+
|
|
14
|
+
### Changes
|
|
15
|
+
- e13b0885 Update SDK and repair planning flow
|
|
16
|
+
|
|
7
17
|
## [0.19.81] — 2026-05-08
|
|
8
18
|
|
|
9
19
|
### Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.83",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
98
98
|
"@ast-grep/napi": "^0.42.0",
|
|
99
99
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
100
|
-
"@pellux/goodvibes-sdk": "0.33.
|
|
100
|
+
"@pellux/goodvibes-sdk": "0.33.19",
|
|
101
101
|
"bash-language-server": "^5.6.0",
|
|
102
102
|
"fuse.js": "^7.1.0",
|
|
103
103
|
"graphql": "^16.13.2",
|
|
@@ -1,6 +1,52 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectPlanningEvaluation,
|
|
3
|
+
ProjectPlanningQuestion,
|
|
4
|
+
ProjectPlanningService,
|
|
5
|
+
ProjectPlanningState,
|
|
6
|
+
} from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
1
7
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
2
8
|
import { requirePlanManager, requireSessionLineageTracker } from './runtime-services.ts';
|
|
3
9
|
|
|
10
|
+
function recordNextQuestion(
|
|
11
|
+
state: Partial<ProjectPlanningState>,
|
|
12
|
+
question: ProjectPlanningQuestion | undefined,
|
|
13
|
+
): Partial<ProjectPlanningState> {
|
|
14
|
+
if (!question) return state;
|
|
15
|
+
const answered = new Set((state.answeredQuestions ?? []).map((entry) => entry.id));
|
|
16
|
+
if (answered.has(question.id)) return state;
|
|
17
|
+
const openQuestions = [...(state.openQuestions ?? [])];
|
|
18
|
+
const existingIndex = openQuestions.findIndex((entry) => entry.id === question.id);
|
|
19
|
+
const normalized = { ...question, status: question.status ?? 'open' } satisfies ProjectPlanningQuestion;
|
|
20
|
+
if (existingIndex >= 0) openQuestions[existingIndex] = normalized;
|
|
21
|
+
else openQuestions.unshift(normalized);
|
|
22
|
+
return { ...state, openQuestions };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function persistEvaluatedNextQuestion(
|
|
26
|
+
service: ProjectPlanningService,
|
|
27
|
+
projectId: string,
|
|
28
|
+
state: ProjectPlanningState,
|
|
29
|
+
evaluation: ProjectPlanningEvaluation,
|
|
30
|
+
): Promise<{ state: ProjectPlanningState; evaluation: ProjectPlanningEvaluation }> {
|
|
31
|
+
if (!evaluation.nextQuestion) return { state, evaluation };
|
|
32
|
+
if (state.openQuestions.some((question) => question.id === evaluation.nextQuestion?.id)) {
|
|
33
|
+
return { state, evaluation };
|
|
34
|
+
}
|
|
35
|
+
const withQuestion = recordNextQuestion(evaluation.state ?? state, evaluation.nextQuestion);
|
|
36
|
+
const saved = await service.upsertState({ projectId, state: withQuestion });
|
|
37
|
+
const nextState = saved.state ?? state;
|
|
38
|
+
const nextEvaluation = await service.evaluate({ projectId, state: nextState });
|
|
39
|
+
return { state: nextState, evaluation: nextEvaluation };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatNextQuestion(question: ProjectPlanningQuestion | undefined): string {
|
|
43
|
+
if (!question) return 'No next question recorded.';
|
|
44
|
+
const lines = [`Next question: ${question.prompt}`];
|
|
45
|
+
if (question.recommendedAnswer) lines.push(`Recommended answer: ${question.recommendedAnswer}`);
|
|
46
|
+
lines.push('Answer in the prompt, or focus the Planning panel to choose/type an answer.');
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
|
|
4
50
|
export function registerPlanningRuntimeCommands(registry: CommandRegistry): void {
|
|
5
51
|
registry.register({
|
|
6
52
|
name: 'plan',
|
|
@@ -25,17 +71,24 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
25
71
|
|
|
26
72
|
if (args.length === 0) {
|
|
27
73
|
if (projectPlanningService && projectId) {
|
|
28
|
-
const [status,
|
|
74
|
+
const [status, stateResult] = await Promise.all([
|
|
29
75
|
projectPlanningService.status({ projectId }),
|
|
30
|
-
projectPlanningService.
|
|
76
|
+
projectPlanningService.getState({ projectId }),
|
|
31
77
|
]);
|
|
78
|
+
const initialEvaluation = await projectPlanningService.evaluate({
|
|
79
|
+
projectId,
|
|
80
|
+
...(stateResult.state ? { state: stateResult.state } : {}),
|
|
81
|
+
});
|
|
82
|
+
const { evaluation } = stateResult.state
|
|
83
|
+
? await persistEvaluatedNextQuestion(projectPlanningService, projectId, stateResult.state, initialEvaluation)
|
|
84
|
+
: { evaluation: initialEvaluation };
|
|
32
85
|
openProjectPlanningPanel();
|
|
33
86
|
ctx.print(
|
|
34
87
|
`Project planning: ${evaluation.readiness}\n` +
|
|
35
88
|
`Project: ${status.projectId}\n` +
|
|
36
89
|
`Knowledge space: ${status.knowledgeSpaceId}\n` +
|
|
37
90
|
`Artifacts: ${status.counts.states} state, ${status.counts.decisions} decisions, ${status.counts.languageArtifacts} language\n` +
|
|
38
|
-
(evaluation.nextQuestion
|
|
91
|
+
formatNextQuestion(evaluation.nextQuestion),
|
|
39
92
|
);
|
|
40
93
|
return;
|
|
41
94
|
}
|
|
@@ -132,14 +185,20 @@ export function registerPlanningRuntimeCommands(registry: CommandRegistry): void
|
|
|
132
185
|
},
|
|
133
186
|
},
|
|
134
187
|
});
|
|
135
|
-
const
|
|
188
|
+
const initialEvaluation = await projectPlanningService.evaluate({
|
|
189
|
+
projectId,
|
|
190
|
+
...(result.state ? { state: result.state } : {}),
|
|
191
|
+
});
|
|
192
|
+
const { state, evaluation } = result.state
|
|
193
|
+
? await persistEvaluatedNextQuestion(projectPlanningService, projectId, result.state, initialEvaluation)
|
|
194
|
+
: { state: result.state, evaluation: initialEvaluation };
|
|
136
195
|
sessionLineageTracker.setOriginalTask(taskDescription.slice(0, 200));
|
|
137
196
|
openProjectPlanningPanel();
|
|
138
197
|
|
|
139
198
|
ctx.print(
|
|
140
|
-
`Project planning seeded: "${
|
|
199
|
+
`Project planning seeded: "${state?.goal ?? taskDescription}"\n` +
|
|
141
200
|
`Readiness: ${evaluation.readiness}\n` +
|
|
142
|
-
(evaluation.nextQuestion
|
|
201
|
+
formatNextQuestion(evaluation.nextQuestion),
|
|
143
202
|
);
|
|
144
203
|
},
|
|
145
204
|
});
|
package/src/main.ts
CHANGED
|
@@ -311,6 +311,11 @@ async function main() {
|
|
|
311
311
|
try {
|
|
312
312
|
const planning = await projectPlanningCoordinator.prepareTurn(processedText);
|
|
313
313
|
if (planning) {
|
|
314
|
+
if (planning.handledLocally) {
|
|
315
|
+
systemMessageRouter.high(planning.statusMessage);
|
|
316
|
+
render();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
314
319
|
conversation.addSystemMessage(planning.systemMessage);
|
|
315
320
|
inputOptions = {
|
|
316
321
|
origin: {
|
|
@@ -90,6 +90,7 @@ export function registerAgentPanels(manager: PanelManager, deps: ResolvedBuiltin
|
|
|
90
90
|
service: deps.projectPlanningService,
|
|
91
91
|
projectId: deps.projectPlanningProjectId,
|
|
92
92
|
requestRender: deps.requestRender,
|
|
93
|
+
submitAnswer: deps.submitPlanningAnswer,
|
|
93
94
|
}),
|
|
94
95
|
});
|
|
95
96
|
|
|
@@ -55,6 +55,8 @@ export interface BuiltinPanelDeps {
|
|
|
55
55
|
resumeSession?: (sessionId: string) => void;
|
|
56
56
|
/** Request a shell repaint directly rather than routing through a retired event path. */
|
|
57
57
|
requestRender?: () => void;
|
|
58
|
+
/** Submit a Planning panel answer through the normal TUI chat/planning coordinator path. */
|
|
59
|
+
submitPlanningAnswer?: (answer: string) => void;
|
|
58
60
|
/** ForensicsRegistry for the Forensics panel. */
|
|
59
61
|
forensicsRegistry?: import('@/runtime/index.ts').ForensicsRegistry;
|
|
60
62
|
/** EvalRegistry for the Eval panel. */
|
|
@@ -553,7 +553,12 @@ export class PanelManager {
|
|
|
553
553
|
private _activateByIdInPane(panelId: string, which: 'top' | 'bottom'): void {
|
|
554
554
|
const p = this._getPane(which);
|
|
555
555
|
const index = p.panels.findIndex(panel => panel.id === panelId);
|
|
556
|
-
if (index
|
|
556
|
+
if (index < 0) return;
|
|
557
|
+
if (index === p.activeIndex) {
|
|
558
|
+
p.panels[index]?.onActivate();
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (index !== p.activeIndex) {
|
|
557
562
|
const oldPanel = p.panels[p.activeIndex];
|
|
558
563
|
if (oldPanel) oldPanel.onDeactivate();
|
|
559
564
|
p.activeIndex = index;
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ProjectPlanningDecision,
|
|
3
3
|
ProjectPlanningEvaluation,
|
|
4
4
|
ProjectPlanningLanguageArtifact,
|
|
5
|
+
ProjectPlanningQuestion,
|
|
5
6
|
ProjectPlanningService,
|
|
6
7
|
ProjectPlanningState,
|
|
7
8
|
ProjectPlanningStatus,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
buildEmptyState,
|
|
14
15
|
buildKeyValueLine,
|
|
15
16
|
buildPanelLine,
|
|
17
|
+
buildPanelListRow,
|
|
16
18
|
buildPanelWorkspace,
|
|
17
19
|
DEFAULT_PANEL_PALETTE,
|
|
18
20
|
extendPalette,
|
|
@@ -39,21 +41,35 @@ export interface ProjectPlanningPanelOptions {
|
|
|
39
41
|
readonly service: ProjectPlanningService;
|
|
40
42
|
readonly projectId: string;
|
|
41
43
|
readonly requestRender?: () => void;
|
|
44
|
+
readonly submitAnswer?: (answer: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PlanningAnswerAction {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
readonly label: string;
|
|
50
|
+
readonly detail: string;
|
|
51
|
+
readonly answer: string;
|
|
52
|
+
readonly kind?: 'answer' | 'approve';
|
|
53
|
+
readonly disabled?: boolean;
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
export class ProjectPlanningPanel extends BasePanel {
|
|
45
57
|
private readonly service: ProjectPlanningService;
|
|
46
58
|
private readonly projectId: string;
|
|
47
59
|
private readonly requestRender: () => void;
|
|
60
|
+
private readonly submitAnswer: ((answer: string) => void) | undefined;
|
|
48
61
|
private snapshot: ProjectPlanningPanelSnapshot | null = null;
|
|
49
62
|
private loading = false;
|
|
50
63
|
private scrollOffset = 0;
|
|
64
|
+
private selectedActionIndex = 0;
|
|
65
|
+
private draftAnswer = '';
|
|
51
66
|
|
|
52
67
|
public constructor(options: ProjectPlanningPanelOptions) {
|
|
53
68
|
super('project-planning', 'Planning', 'P', 'agent');
|
|
54
69
|
this.service = options.service;
|
|
55
70
|
this.projectId = options.projectId;
|
|
56
71
|
this.requestRender = options.requestRender ?? (() => {});
|
|
72
|
+
this.submitAnswer = options.submitAnswer;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
public override onActivate(): void {
|
|
@@ -62,6 +78,58 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
public handleInput(key: string): boolean {
|
|
81
|
+
if (this.lastError !== null) this.clearError();
|
|
82
|
+
const question = this.getCurrentQuestion();
|
|
83
|
+
if (question) {
|
|
84
|
+
const actions = this.getAnswerActions(question);
|
|
85
|
+
this.selectedActionIndex = this.clampActionIndex(actions.length);
|
|
86
|
+
if (key === 'up') {
|
|
87
|
+
this.selectedActionIndex = Math.max(0, this.selectedActionIndex - 1);
|
|
88
|
+
this.markDirty();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (key === 'down') {
|
|
92
|
+
this.selectedActionIndex = Math.min(Math.max(0, actions.length - 1), this.selectedActionIndex + 1);
|
|
93
|
+
this.markDirty();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (key === 'enter' || key === 'return') {
|
|
97
|
+
this.submitSelectedAction(question, actions);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (key === 'backspace') {
|
|
101
|
+
this.draftAnswer = this.draftAnswer.slice(0, -1);
|
|
102
|
+
this.markDirty();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (key === 'delete') {
|
|
106
|
+
this.draftAnswer = '';
|
|
107
|
+
this.markDirty();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (key === 'space') {
|
|
111
|
+
this.draftAnswer += ' ';
|
|
112
|
+
this.markDirty();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (key === 'pageup') {
|
|
116
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 6);
|
|
117
|
+
this.markDirty();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
if (key === 'pagedown') {
|
|
121
|
+
this.scrollOffset += 6;
|
|
122
|
+
this.markDirty();
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (this.isPrintableKey(key)) {
|
|
126
|
+
this.draftAnswer += key;
|
|
127
|
+
this.markDirty();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
65
133
|
if (key === 'r' || key === 'R') {
|
|
66
134
|
this.refresh(true);
|
|
67
135
|
return true;
|
|
@@ -121,6 +189,8 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
121
189
|
});
|
|
122
190
|
} else {
|
|
123
191
|
sections.push(this.buildStateSection(width, state, evaluation));
|
|
192
|
+
const question = this.getCurrentQuestion();
|
|
193
|
+
if (question) sections.push(this.buildQuestionSection(width, question));
|
|
124
194
|
sections.push(this.buildGapsSection(width, evaluation));
|
|
125
195
|
sections.push(this.buildTasksSection(width, state));
|
|
126
196
|
sections.push(this.buildDecisionsSection(width, state, decisions));
|
|
@@ -161,6 +231,21 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
161
231
|
}
|
|
162
232
|
|
|
163
233
|
private footerLines(width: number): Line[] {
|
|
234
|
+
const hasQuestion = this.getCurrentQuestion() !== null;
|
|
235
|
+
if (hasQuestion) {
|
|
236
|
+
return [
|
|
237
|
+
buildPanelLine(width, [
|
|
238
|
+
[' Up/Down', C.info],
|
|
239
|
+
[' choose answer ', C.dim],
|
|
240
|
+
['type', C.info],
|
|
241
|
+
[' draft ', C.dim],
|
|
242
|
+
['Backspace/Delete', C.info],
|
|
243
|
+
[' edit ', C.dim],
|
|
244
|
+
['Enter', C.info],
|
|
245
|
+
[' submit Esc close panel focus', C.dim],
|
|
246
|
+
]),
|
|
247
|
+
];
|
|
248
|
+
}
|
|
164
249
|
return [
|
|
165
250
|
buildPanelLine(width, [
|
|
166
251
|
[' Up/Down', C.info],
|
|
@@ -173,6 +258,41 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
173
258
|
];
|
|
174
259
|
}
|
|
175
260
|
|
|
261
|
+
private buildQuestionSection(width: number, question: ProjectPlanningQuestion): PanelWorkspaceSection {
|
|
262
|
+
const actions = this.getAnswerActions(question);
|
|
263
|
+
this.selectedActionIndex = this.clampActionIndex(actions.length);
|
|
264
|
+
const lines: Line[] = [
|
|
265
|
+
...buildBodyText(width, question.prompt, C, C.planning),
|
|
266
|
+
];
|
|
267
|
+
if (question.whyItMatters) {
|
|
268
|
+
lines.push(...buildBodyText(width, `Why this matters: ${question.whyItMatters}`, C, C.dim));
|
|
269
|
+
}
|
|
270
|
+
if (question.recommendedAnswer && !this.isGenericRecommendation(question.recommendedAnswer)) {
|
|
271
|
+
lines.push(...buildBodyText(width, `Recommendation: ${question.recommendedAnswer}`, C, C.good));
|
|
272
|
+
}
|
|
273
|
+
lines.push(...buildBodyText(
|
|
274
|
+
width,
|
|
275
|
+
`Typed answer: ${this.draftAnswer || '(type here while this panel is focused)'}`,
|
|
276
|
+
C,
|
|
277
|
+
this.draftAnswer ? C.value : C.dim,
|
|
278
|
+
));
|
|
279
|
+
lines.push(buildPanelLine(width, [[
|
|
280
|
+
' Select an answer below or type your own. Enter sends it through the normal planning chat path.',
|
|
281
|
+
C.dim,
|
|
282
|
+
]]));
|
|
283
|
+
actions.forEach((action, index) => {
|
|
284
|
+
const selected = index === this.selectedActionIndex;
|
|
285
|
+
lines.push(buildPanelListRow(width, [
|
|
286
|
+
{ text: action.label, fg: action.disabled ? C.dim : C.value, bold: selected },
|
|
287
|
+
{ text: ` ${action.detail}`, fg: C.dim },
|
|
288
|
+
], C, {
|
|
289
|
+
selected,
|
|
290
|
+
marker: selected ? '▶' : ' ',
|
|
291
|
+
}));
|
|
292
|
+
});
|
|
293
|
+
return { title: 'Answer Current Question', lines };
|
|
294
|
+
}
|
|
295
|
+
|
|
176
296
|
private buildStateSection(
|
|
177
297
|
width: number,
|
|
178
298
|
state: ProjectPlanningState,
|
|
@@ -201,7 +321,7 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
201
321
|
if (evaluation.nextQuestion.whyItMatters) {
|
|
202
322
|
lines.push(...buildBodyText(width, `Why it matters: ${evaluation.nextQuestion.whyItMatters}`, C, C.dim));
|
|
203
323
|
}
|
|
204
|
-
if (evaluation.nextQuestion.recommendedAnswer) {
|
|
324
|
+
if (evaluation.nextQuestion.recommendedAnswer && !this.isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
|
|
205
325
|
lines.push(...buildBodyText(width, `Recommended answer: ${evaluation.nextQuestion.recommendedAnswer}`, C, C.good));
|
|
206
326
|
}
|
|
207
327
|
}
|
|
@@ -339,6 +459,157 @@ export class ProjectPlanningPanel extends BasePanel {
|
|
|
339
459
|
})();
|
|
340
460
|
}
|
|
341
461
|
|
|
462
|
+
private getCurrentQuestion(): ProjectPlanningQuestion | null {
|
|
463
|
+
const state = this.snapshot?.state;
|
|
464
|
+
const open = state?.openQuestions.find((question) => (question.status ?? 'open') === 'open');
|
|
465
|
+
return open ?? this.snapshot?.evaluation?.nextQuestion ?? null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private getAnswerActions(question: ProjectPlanningQuestion): PlanningAnswerAction[] {
|
|
469
|
+
const actions: PlanningAnswerAction[] = [];
|
|
470
|
+
const prompt = question.prompt.toLowerCase();
|
|
471
|
+
const isScopeQuestion = prompt.includes('scope') || prompt.includes('in or out');
|
|
472
|
+
const isTaskQuestion = prompt.includes('task') || prompt.includes('dependency') || prompt.includes('work breakdown');
|
|
473
|
+
const isVerificationQuestion = prompt.includes('verification') || prompt.includes('test') || prompt.includes('prove');
|
|
474
|
+
const isApprovalQuestion = prompt.includes('approved') || prompt.includes('approve') || prompt.includes('execution');
|
|
475
|
+
if (isApprovalQuestion) {
|
|
476
|
+
actions.push({
|
|
477
|
+
id: 'approve-execution',
|
|
478
|
+
label: 'Approve execution',
|
|
479
|
+
detail: 'Mark this plan approved so execution may proceed.',
|
|
480
|
+
answer: 'Approve this planning state for execution.',
|
|
481
|
+
kind: 'approve',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (isScopeQuestion) {
|
|
485
|
+
actions.push({
|
|
486
|
+
id: 'scope-focused-first-pass',
|
|
487
|
+
label: 'Use focused first-pass scope',
|
|
488
|
+
detail: 'Fill a concrete end-to-end scope for this goal and keep unrelated work out.',
|
|
489
|
+
answer: 'Use a focused first-pass scope for this goal.',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (isTaskQuestion) {
|
|
493
|
+
actions.push({
|
|
494
|
+
id: 'tasks-default-breakdown',
|
|
495
|
+
label: 'Create default task breakdown',
|
|
496
|
+
detail: 'Create inspect, implement, wire, and verify tasks with dependencies.',
|
|
497
|
+
answer: 'Create the default task breakdown for this goal.',
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (isVerificationQuestion) {
|
|
501
|
+
actions.push({
|
|
502
|
+
id: 'verification-default-gates',
|
|
503
|
+
label: 'Use standard verification gates',
|
|
504
|
+
detail: 'Require focused regression coverage, typecheck/build validation, and a runtime smoke where feasible.',
|
|
505
|
+
answer: 'Use standard verification gates for this goal.',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (question.recommendedAnswer?.trim() && !this.isGenericRecommendation(question.recommendedAnswer)) {
|
|
509
|
+
actions.push({
|
|
510
|
+
id: 'recommended',
|
|
511
|
+
label: 'Use recommended answer',
|
|
512
|
+
detail: this.compact(question.recommendedAnswer),
|
|
513
|
+
answer: question.recommendedAnswer,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
if (isScopeQuestion) {
|
|
517
|
+
actions.push({
|
|
518
|
+
id: 'scope-end-to-end',
|
|
519
|
+
label: 'End-to-end required scope',
|
|
520
|
+
detail: 'Let the plan include every component needed to make this work, but avoid unrelated cleanup.',
|
|
521
|
+
answer: 'Scope is everything required to make the requested outcome work end-to-end. Include TUI, daemon composition, configuration, docs, and tests if they are required. Do not include unrelated cleanup or broad refactors unless they are necessary for this task.',
|
|
522
|
+
});
|
|
523
|
+
actions.push({
|
|
524
|
+
id: 'scope-tui-first',
|
|
525
|
+
label: 'TUI-first scope',
|
|
526
|
+
detail: 'Fix TUI behavior here; report SDK blockers instead of patching around SDK-owned bugs.',
|
|
527
|
+
answer: 'Scope is TUI-owned behavior first. If a blocker is SDK-owned, report the exact SDK contract/runtime issue instead of patching around it in the TUI. Include daemon composition only where the TUI owns the wiring.',
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
actions.push({
|
|
531
|
+
id: 'ask-narrower',
|
|
532
|
+
label: 'I am not sure yet',
|
|
533
|
+
detail: 'Break this into smaller concrete choices with examples and a recommended default.',
|
|
534
|
+
answer: `I do not know enough to answer "${question.prompt}" as asked. Break it into smaller concrete questions with 2-4 specific choices, explain the tradeoffs, recommend a default, and ask me the first one.`,
|
|
535
|
+
});
|
|
536
|
+
actions.push({
|
|
537
|
+
id: 'custom',
|
|
538
|
+
label: 'Submit typed answer',
|
|
539
|
+
detail: this.draftAnswer ? this.compact(this.draftAnswer) : 'Type an answer first; this row becomes the custom answer.',
|
|
540
|
+
answer: this.draftAnswer.trim(),
|
|
541
|
+
disabled: !this.draftAnswer.trim(),
|
|
542
|
+
});
|
|
543
|
+
return actions;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private isGenericRecommendation(value: string): boolean {
|
|
547
|
+
return /\bdefine the first-pass scope\b/i.test(value)
|
|
548
|
+
|| /\bcreate task records\b/i.test(value)
|
|
549
|
+
|| /\brecord concrete tests\b/i.test(value)
|
|
550
|
+
|| /\bseparate out-of-scope work\b/i.test(value);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private submitSelectedAction(question: ProjectPlanningQuestion, actions: readonly PlanningAnswerAction[]): void {
|
|
554
|
+
const action = actions[this.clampActionIndex(actions.length)];
|
|
555
|
+
if (!action || action.disabled || !action.answer.trim()) {
|
|
556
|
+
this.setError('Type an answer or choose a non-empty answer option.');
|
|
557
|
+
this.requestRender();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (action.kind === 'approve') {
|
|
561
|
+
this.approveExecution();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (!this.submitAnswer) {
|
|
565
|
+
this.setError('Planning answer submission is not wired in this runtime.');
|
|
566
|
+
this.requestRender();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
void (async () => {
|
|
570
|
+
try {
|
|
571
|
+
await this.persistQuestionIfNeeded(question);
|
|
572
|
+
this.draftAnswer = '';
|
|
573
|
+
this.submitAnswer?.(action.answer.trim());
|
|
574
|
+
this.refresh(true);
|
|
575
|
+
this.registerTimer(setTimeout(() => this.refresh(true), 250));
|
|
576
|
+
} catch (err) {
|
|
577
|
+
this.setError(err instanceof Error ? err.message : String(err));
|
|
578
|
+
this.requestRender();
|
|
579
|
+
}
|
|
580
|
+
})();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private async persistQuestionIfNeeded(question: ProjectPlanningQuestion): Promise<void> {
|
|
584
|
+
const state = this.snapshot?.state;
|
|
585
|
+
if (!state) return;
|
|
586
|
+
if (state.openQuestions.some((entry) => entry.id === question.id)) return;
|
|
587
|
+
await this.service.upsertState({
|
|
588
|
+
projectId: this.projectId,
|
|
589
|
+
state: {
|
|
590
|
+
...state,
|
|
591
|
+
openQuestions: [
|
|
592
|
+
{ ...question, status: question.status ?? 'open' },
|
|
593
|
+
...state.openQuestions,
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private clampActionIndex(count: number): number {
|
|
600
|
+
if (count <= 0) return 0;
|
|
601
|
+
return Math.max(0, Math.min(count - 1, this.selectedActionIndex));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private isPrintableKey(key: string): boolean {
|
|
605
|
+
return key.length === 1 && key >= ' ';
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private compact(text: string): string {
|
|
609
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
610
|
+
return normalized.length > 86 ? `${normalized.slice(0, 83)}...` : normalized;
|
|
611
|
+
}
|
|
612
|
+
|
|
342
613
|
private approveExecution(): void {
|
|
343
614
|
const state = this.snapshot?.state;
|
|
344
615
|
if (!state) {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ProjectPlanningAgentAssignment,
|
|
3
|
+
ProjectPlanningDependency,
|
|
2
4
|
ProjectPlanningEvaluation,
|
|
3
5
|
ProjectPlanningQuestion,
|
|
4
6
|
ProjectPlanningService,
|
|
5
7
|
ProjectPlanningState,
|
|
8
|
+
ProjectPlanningTask,
|
|
9
|
+
ProjectPlanningVerificationGate,
|
|
6
10
|
} from '@pellux/goodvibes-sdk/platform/knowledge';
|
|
7
11
|
|
|
8
12
|
export interface ProjectPlanningCoordinatorOptions {
|
|
@@ -18,6 +22,8 @@ export interface ProjectPlanningTurnPreparation {
|
|
|
18
22
|
readonly systemMessage: string;
|
|
19
23
|
readonly state: ProjectPlanningState;
|
|
20
24
|
readonly evaluation: ProjectPlanningEvaluation;
|
|
25
|
+
readonly handledLocally: boolean;
|
|
26
|
+
readonly statusMessage: string;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
const PLANNING_INTENT_PATTERNS: readonly RegExp[] = [
|
|
@@ -42,6 +48,24 @@ const APPROVAL_PATTERNS: readonly RegExp[] = [
|
|
|
42
48
|
/\b(go ahead|execute this plan|start execution|ready to execute)\b/i,
|
|
43
49
|
];
|
|
44
50
|
|
|
51
|
+
const ACCEPT_DEFAULT_PATTERNS: readonly RegExp[] = [
|
|
52
|
+
/^(ok|okay|yes|y|yep|yeah|sure|fine|default|continue|go|go ahead|let'?s go|looks good|sounds good)\.?$/i,
|
|
53
|
+
/\bok\b.*\blet'?s go\b/i,
|
|
54
|
+
/\blet'?s go\b/i,
|
|
55
|
+
/\b(use|accept|take) (the )?(default|recommended|recommendation)\b/i,
|
|
56
|
+
/\b(create|use) (the )?default task breakdown\b/i,
|
|
57
|
+
/\bfocused first-pass scope\b/i,
|
|
58
|
+
/\bstandard verification gates?\b/i,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const GENERIC_RECOMMENDATION_PATTERNS: readonly RegExp[] = [
|
|
62
|
+
/\bdefine the first-pass scope\b/i,
|
|
63
|
+
/\bseparate out-of-scope work\b/i,
|
|
64
|
+
/\bcreate task records\b/i,
|
|
65
|
+
/\brecord concrete tests\b/i,
|
|
66
|
+
/\bverification gates?\b/i,
|
|
67
|
+
];
|
|
68
|
+
|
|
45
69
|
export function hasProjectPlanningIntent(text: string): boolean {
|
|
46
70
|
const trimmed = text.trim();
|
|
47
71
|
if (!trimmed || trimmed.startsWith('/')) return false;
|
|
@@ -125,6 +149,8 @@ export class ProjectPlanningCoordinator {
|
|
|
125
149
|
systemMessage: this.buildSystemMessage(state, evaluation),
|
|
126
150
|
state,
|
|
127
151
|
evaluation,
|
|
152
|
+
handledLocally: true,
|
|
153
|
+
statusMessage: this.buildStatusMessage(state, evaluation),
|
|
128
154
|
};
|
|
129
155
|
}
|
|
130
156
|
|
|
@@ -143,12 +169,14 @@ export class ProjectPlanningCoordinator {
|
|
|
143
169
|
if (options.answeredQuestion) {
|
|
144
170
|
const idx = openQuestions.findIndex((question) => question.id === options.answeredQuestion?.id);
|
|
145
171
|
if (idx >= 0) openQuestions.splice(idx, 1);
|
|
146
|
-
answeredQuestions.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
172
|
+
if (!answeredQuestions.some((question) => question.id === options.answeredQuestion?.id && question.answer === prompt)) {
|
|
173
|
+
answeredQuestions.push({
|
|
174
|
+
...options.answeredQuestion,
|
|
175
|
+
status: 'answered',
|
|
176
|
+
answer: prompt,
|
|
177
|
+
answeredAt: now,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
const knownContext = new Set(existing?.knownContext ?? []);
|
|
@@ -157,7 +185,7 @@ export class ProjectPlanningCoordinator {
|
|
|
157
185
|
knownContext.add(`Latest planning request: ${prompt}`);
|
|
158
186
|
}
|
|
159
187
|
|
|
160
|
-
|
|
188
|
+
const draft: Partial<ProjectPlanningState> = {
|
|
161
189
|
...(existing ?? {}),
|
|
162
190
|
projectId: this.projectId,
|
|
163
191
|
goal: existing?.goal?.trim() ? existing.goal : prompt,
|
|
@@ -173,6 +201,236 @@ export class ProjectPlanningCoordinator {
|
|
|
173
201
|
lastPromptAt: now,
|
|
174
202
|
},
|
|
175
203
|
};
|
|
204
|
+
return this.applyPlanningAnswer(draft, prompt, options.answeredQuestion, {
|
|
205
|
+
startsPlanning: options.startsPlanning,
|
|
206
|
+
approved: options.approved,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private applyPlanningAnswer(
|
|
211
|
+
state: Partial<ProjectPlanningState>,
|
|
212
|
+
prompt: string,
|
|
213
|
+
question: ProjectPlanningQuestion | null,
|
|
214
|
+
options: {
|
|
215
|
+
readonly startsPlanning: boolean;
|
|
216
|
+
readonly approved: boolean;
|
|
217
|
+
},
|
|
218
|
+
): Partial<ProjectPlanningState> {
|
|
219
|
+
if (!question) return state;
|
|
220
|
+
|
|
221
|
+
const acceptDefault = this.acceptsDefault(prompt);
|
|
222
|
+
const questionId = question.id.toLowerCase();
|
|
223
|
+
const questionText = question.prompt.toLowerCase();
|
|
224
|
+
const existingGoal = (state.goal ?? prompt).trim();
|
|
225
|
+
const generic = acceptDefault || this.isGenericPlanningRecommendation(prompt);
|
|
226
|
+
let next: Partial<ProjectPlanningState> = { ...state };
|
|
227
|
+
|
|
228
|
+
if (questionId.includes('scope') || questionText.includes('scope') || questionText.includes('in or out')) {
|
|
229
|
+
next = {
|
|
230
|
+
...next,
|
|
231
|
+
scope: generic ? this.defaultScope(existingGoal) : this.normalizeScopeAnswer(existingGoal, prompt),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
questionId.includes('task') ||
|
|
237
|
+
questionText.includes('task') ||
|
|
238
|
+
questionId.includes('dependency') ||
|
|
239
|
+
questionText.includes('dependency') ||
|
|
240
|
+
questionId.includes('verification') ||
|
|
241
|
+
questionText.includes('verification') ||
|
|
242
|
+
(acceptDefault && this.hasStructuralPlanningGaps(next))
|
|
243
|
+
) {
|
|
244
|
+
next = this.withDefaultExecutionPlan(next, existingGoal);
|
|
245
|
+
if (!generic && !questionId.includes('scope')) {
|
|
246
|
+
next = this.addKnownContext(next, `Planning answer: ${prompt}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (questionId.includes('approve') || questionText.includes('approve') || questionText.includes('execution')) {
|
|
251
|
+
next = {
|
|
252
|
+
...next,
|
|
253
|
+
executionApproved: true,
|
|
254
|
+
metadata: {
|
|
255
|
+
...(next.metadata ?? {}),
|
|
256
|
+
approvedAt: this.now(),
|
|
257
|
+
approvedFrom: 'planning-answer',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (acceptDefault && this.isGoAhead(prompt)) {
|
|
263
|
+
next = this.withDefaultExecutionPlan({
|
|
264
|
+
...next,
|
|
265
|
+
scope: next.scope ?? this.defaultScope(existingGoal),
|
|
266
|
+
executionApproved: true,
|
|
267
|
+
metadata: {
|
|
268
|
+
...(next.metadata ?? {}),
|
|
269
|
+
approvedAt: this.now(),
|
|
270
|
+
approvedFrom: 'planning-go-ahead',
|
|
271
|
+
},
|
|
272
|
+
}, existingGoal);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return next;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private normalizeScopeAnswer(goal: string, answer: string): string {
|
|
279
|
+
const trimmed = answer.trim();
|
|
280
|
+
if (!trimmed) return this.defaultScope(goal);
|
|
281
|
+
if (/^scope\s+is\b/i.test(trimmed)) return trimmed;
|
|
282
|
+
return `Scope for "${goal}": ${trimmed}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private defaultScope(goal: string): string {
|
|
286
|
+
const cleanGoal = goal.trim() || 'the requested change';
|
|
287
|
+
return [
|
|
288
|
+
`First pass: make "${cleanGoal}" work end-to-end for the primary local TUI workflow.`,
|
|
289
|
+
'Include the minimum TUI, daemon wiring, configuration persistence, documentation, and verification required for the feature to actually work.',
|
|
290
|
+
'Exclude unrelated cleanup, broad refactors, polish-only changes, third-party integrations, and advanced distributed behavior unless they directly block the primary workflow.',
|
|
291
|
+
].join(' ');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private withDefaultExecutionPlan(
|
|
295
|
+
state: Partial<ProjectPlanningState>,
|
|
296
|
+
goal: string,
|
|
297
|
+
): Partial<ProjectPlanningState> {
|
|
298
|
+
const tasks = state.tasks && state.tasks.length > 0 ? [...state.tasks] : this.defaultTasks(goal);
|
|
299
|
+
const verificationGates = state.verificationGates && state.verificationGates.length > 0
|
|
300
|
+
? [...state.verificationGates]
|
|
301
|
+
: this.defaultVerificationGates(goal);
|
|
302
|
+
const dependencies = state.dependencies && state.dependencies.length > 0
|
|
303
|
+
? [...state.dependencies]
|
|
304
|
+
: this.defaultDependencies();
|
|
305
|
+
const agentAssignments = state.agentAssignments && state.agentAssignments.length > 0
|
|
306
|
+
? [...state.agentAssignments]
|
|
307
|
+
: this.defaultAgentAssignments();
|
|
308
|
+
return {
|
|
309
|
+
...state,
|
|
310
|
+
scope: state.scope ?? this.defaultScope(goal),
|
|
311
|
+
tasks,
|
|
312
|
+
verificationGates,
|
|
313
|
+
dependencies,
|
|
314
|
+
agentAssignments,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private defaultTasks(goal: string): ProjectPlanningTask[] {
|
|
319
|
+
const cleanGoal = goal.trim() || 'requested change';
|
|
320
|
+
return [
|
|
321
|
+
{
|
|
322
|
+
id: 'inspect-current-flow',
|
|
323
|
+
title: `Inspect the current paths for ${cleanGoal}`,
|
|
324
|
+
why: 'Planning must start from the actual code and runtime behavior, not assumptions.',
|
|
325
|
+
status: 'pending',
|
|
326
|
+
verification: ['Identify the relevant files, commands, config keys, and runtime path before editing.'],
|
|
327
|
+
canRunConcurrently: false,
|
|
328
|
+
needsReview: false,
|
|
329
|
+
recommendedAgent: 'explorer',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: 'implement-core-behavior',
|
|
333
|
+
title: `Implement the core ${cleanGoal} behavior`,
|
|
334
|
+
why: 'This is the minimum product behavior required for the requested outcome to work.',
|
|
335
|
+
status: 'pending',
|
|
336
|
+
dependencies: ['inspect-current-flow'],
|
|
337
|
+
verification: ['Focused tests cover the changed behavior and fail without the implementation.'],
|
|
338
|
+
canRunConcurrently: false,
|
|
339
|
+
needsReview: true,
|
|
340
|
+
recommendedAgent: 'worker',
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'wire-user-surface',
|
|
344
|
+
title: `Wire the user-facing path for ${cleanGoal}`,
|
|
345
|
+
why: 'The feature must be reachable through the intended TUI/daemon/config surface, not just internal code.',
|
|
346
|
+
status: 'pending',
|
|
347
|
+
dependencies: ['implement-core-behavior'],
|
|
348
|
+
verification: ['A command, panel, route, or setting path exercises the behavior from the user-facing entry point.'],
|
|
349
|
+
canRunConcurrently: false,
|
|
350
|
+
needsReview: true,
|
|
351
|
+
recommendedAgent: 'worker',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: 'verify-release-readiness',
|
|
355
|
+
title: `Verify ${cleanGoal} end-to-end`,
|
|
356
|
+
why: 'The plan is not complete until the user-facing path and regression tests prove it works.',
|
|
357
|
+
status: 'pending',
|
|
358
|
+
dependencies: ['implement-core-behavior', 'wire-user-surface'],
|
|
359
|
+
verification: ['Run focused tests plus the relevant type/build/smoke checks for the touched area.'],
|
|
360
|
+
canRunConcurrently: false,
|
|
361
|
+
needsReview: false,
|
|
362
|
+
recommendedAgent: 'none',
|
|
363
|
+
},
|
|
364
|
+
];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private defaultDependencies(): ProjectPlanningDependency[] {
|
|
368
|
+
return [
|
|
369
|
+
{ fromTaskId: 'inspect-current-flow', toTaskId: 'implement-core-behavior', reason: 'Implementation depends on knowing the current code path.' },
|
|
370
|
+
{ fromTaskId: 'implement-core-behavior', toTaskId: 'wire-user-surface', reason: 'The user-facing surface should call the implemented behavior.' },
|
|
371
|
+
{ fromTaskId: 'implement-core-behavior', toTaskId: 'verify-release-readiness', reason: 'Verification needs the implementation in place.' },
|
|
372
|
+
{ fromTaskId: 'wire-user-surface', toTaskId: 'verify-release-readiness', reason: 'Verification must include the reachable user path.' },
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private defaultVerificationGates(goal: string): ProjectPlanningVerificationGate[] {
|
|
377
|
+
const cleanGoal = goal.trim() || 'requested change';
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
id: 'focused-regression',
|
|
381
|
+
description: `Focused regression coverage proves "${cleanGoal}" works and prevents the observed failure from returning.`,
|
|
382
|
+
status: 'pending',
|
|
383
|
+
required: true,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: 'typecheck',
|
|
387
|
+
description: 'TypeScript/build validation passes for the touched code.',
|
|
388
|
+
command: 'bunx tsc --noEmit',
|
|
389
|
+
status: 'pending',
|
|
390
|
+
required: true,
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: 'runtime-smoke',
|
|
394
|
+
description: 'A user-facing runtime smoke exercises the changed path when feasible.',
|
|
395
|
+
status: 'pending',
|
|
396
|
+
required: true,
|
|
397
|
+
},
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private defaultAgentAssignments(): ProjectPlanningAgentAssignment[] {
|
|
402
|
+
return [
|
|
403
|
+
{
|
|
404
|
+
taskId: 'inspect-current-flow',
|
|
405
|
+
agentType: 'explorer',
|
|
406
|
+
expectedOutput: 'Concrete files, state transitions, and failure path that must change.',
|
|
407
|
+
canRunConcurrently: false,
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
taskId: 'implement-core-behavior',
|
|
411
|
+
agentType: 'worker',
|
|
412
|
+
expectedOutput: 'Patch implementing the core behavior with focused tests.',
|
|
413
|
+
verification: 'Review the patch against the original request and regression test.',
|
|
414
|
+
canRunConcurrently: false,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
taskId: 'wire-user-surface',
|
|
418
|
+
agentType: 'worker',
|
|
419
|
+
expectedOutput: 'Patch wiring the behavior through the intended user-facing surface.',
|
|
420
|
+
verification: 'Verify the UI/command/route actually exercises the new behavior.',
|
|
421
|
+
canRunConcurrently: false,
|
|
422
|
+
},
|
|
423
|
+
];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private addKnownContext(state: Partial<ProjectPlanningState>, entry: string): Partial<ProjectPlanningState> {
|
|
427
|
+
const knownContext = new Set(state.knownContext ?? []);
|
|
428
|
+
knownContext.add(entry);
|
|
429
|
+
return { ...state, knownContext: [...knownContext] };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private hasStructuralPlanningGaps(state: Partial<ProjectPlanningState>): boolean {
|
|
433
|
+
return !state.scope || (state.tasks?.length ?? 0) === 0 || (state.verificationGates?.length ?? 0) === 0;
|
|
176
434
|
}
|
|
177
435
|
|
|
178
436
|
private recordNextQuestion(
|
|
@@ -205,12 +463,16 @@ export class ProjectPlanningCoordinator {
|
|
|
205
463
|
const tasks = state.tasks
|
|
206
464
|
.map((task) => `- ${task.id}: ${task.title}`)
|
|
207
465
|
.join('\n') || '- none recorded yet';
|
|
466
|
+
const recentAnswers = state.answeredQuestions
|
|
467
|
+
.slice(-3)
|
|
468
|
+
.map((question) => `- ${question.prompt}\n Answer: ${question.answer ?? '(no answer recorded)'}`)
|
|
469
|
+
.join('\n') || '- none recorded yet';
|
|
208
470
|
|
|
209
471
|
return [
|
|
210
472
|
'TUI-owned project planning loop is active for this turn.',
|
|
211
473
|
'Do not execute code changes, spawn agents, or claim implementation is complete unless the user explicitly approves execution after the plan is structurally ready.',
|
|
212
474
|
'Be relentless and thorough: challenge vague wording, inspect relevant context before proposing execution, and ask exactly one focused question when information is missing.',
|
|
213
|
-
'
|
|
475
|
+
'Do not ask broad questions like "what is in scope?" without examples. Break broad planning gaps into concrete choices, explain tradeoffs, and recommend a default the user can accept or correct.',
|
|
214
476
|
'',
|
|
215
477
|
`Project id: ${this.projectId}`,
|
|
216
478
|
`Knowledge space: ${state.knowledgeSpaceId}`,
|
|
@@ -222,15 +484,32 @@ export class ProjectPlanningCoordinator {
|
|
|
222
484
|
'Readiness gaps:',
|
|
223
485
|
gaps,
|
|
224
486
|
'',
|
|
487
|
+
'Recent answered planning questions:',
|
|
488
|
+
recentAnswers,
|
|
489
|
+
'',
|
|
225
490
|
'Recorded tasks:',
|
|
226
491
|
tasks,
|
|
227
492
|
'',
|
|
228
493
|
nextQuestion
|
|
229
|
-
? `
|
|
494
|
+
? `Resolve this next planning gap. If this wording is broad, turn it into a smaller concrete multiple-choice question before asking: ${nextQuestion.prompt}`
|
|
230
495
|
: 'If the plan is structurally ready, summarize the plan and ask for explicit execution approval. Do not start execution yourself.',
|
|
231
496
|
].join('\n');
|
|
232
497
|
}
|
|
233
498
|
|
|
499
|
+
private buildStatusMessage(
|
|
500
|
+
state: ProjectPlanningState,
|
|
501
|
+
evaluation: ProjectPlanningEvaluation,
|
|
502
|
+
): string {
|
|
503
|
+
const nextQuestion = evaluation.nextQuestion?.prompt;
|
|
504
|
+
const taskCount = state.tasks.length;
|
|
505
|
+
const gateCount = state.verificationGates.length;
|
|
506
|
+
const approved = state.executionApproved ? 'approved' : 'not approved';
|
|
507
|
+
if (evaluation.readiness === 'executable') {
|
|
508
|
+
return `[Planning] Updated plan: ${taskCount} task(s), ${gateCount} verification gate(s), execution ${approved}.`;
|
|
509
|
+
}
|
|
510
|
+
return `[Planning] Updated plan: ${taskCount} task(s), ${gateCount} verification gate(s). Next: ${nextQuestion ?? 'review the plan.'}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
234
513
|
private isActive(state: ProjectPlanningState | null): boolean {
|
|
235
514
|
return state?.metadata?.['active'] === true && state.executionApproved !== true;
|
|
236
515
|
}
|
|
@@ -246,4 +525,17 @@ export class ProjectPlanningCoordinator {
|
|
|
246
525
|
private isApproval(text: string): boolean {
|
|
247
526
|
return APPROVAL_PATTERNS.some((pattern) => pattern.test(text));
|
|
248
527
|
}
|
|
528
|
+
|
|
529
|
+
private acceptsDefault(text: string): boolean {
|
|
530
|
+
const trimmed = text.trim();
|
|
531
|
+
return ACCEPT_DEFAULT_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private isGoAhead(text: string): boolean {
|
|
535
|
+
return /\b(go|go ahead|let'?s go|execute|start|approved?|approval granted)\b/i.test(text.trim());
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private isGenericPlanningRecommendation(text: string): boolean {
|
|
539
|
+
return GENERIC_RECOMMENDATION_PATTERNS.some((pattern) => pattern.test(text));
|
|
540
|
+
}
|
|
249
541
|
}
|
|
@@ -29,6 +29,7 @@ import { registerBootstrapHookBridge } from '@/runtime/index.ts';
|
|
|
29
29
|
import { registerBootstrapRuntimeEvents } from '@/runtime/index.ts';
|
|
30
30
|
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
31
31
|
import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
|
|
32
|
+
import { installWrfcAgentToolGuard } from '../tools/wrfc-agent-guard.ts';
|
|
32
33
|
|
|
33
34
|
export interface BootstrapCoreState {
|
|
34
35
|
readonly userSessionId: string;
|
|
@@ -221,6 +222,7 @@ export async function initializeBootstrapCore(
|
|
|
221
222
|
overflowHandler: services.overflowHandler,
|
|
222
223
|
changeTracker: services.sessionChangeTracker,
|
|
223
224
|
});
|
|
225
|
+
installWrfcAgentToolGuard(toolRegistry);
|
|
224
226
|
services.agentOrchestrator.setDependencies({
|
|
225
227
|
surfaceRoot: 'tui',
|
|
226
228
|
fileCache,
|
|
@@ -103,6 +103,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
103
103
|
providerRegistry: services.providerRegistry,
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
let commandContextRef: CommandContext | null = null;
|
|
106
107
|
registerBuiltinPanels(services.panelManager, {
|
|
107
108
|
configManager,
|
|
108
109
|
getOrchestratorUsage: () => orchestrator.usage as { input: number; output: number; cacheRead: number; cacheWrite: number; model?: string },
|
|
@@ -113,6 +114,12 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
113
114
|
getCtxWindow: () => services.providerRegistry.getContextWindowForModel(services.providerRegistry.getCurrentModel()),
|
|
114
115
|
resumeSession,
|
|
115
116
|
requestRender,
|
|
117
|
+
submitPlanningAnswer: (answer) => {
|
|
118
|
+
if (!commandContextRef?.submitInput) {
|
|
119
|
+
throw new Error('Planning answer submission is not wired yet.');
|
|
120
|
+
}
|
|
121
|
+
commandContextRef.submitInput(answer);
|
|
122
|
+
},
|
|
116
123
|
forensicsRegistry,
|
|
117
124
|
policyRuntimeState,
|
|
118
125
|
approvalBroker: services.approvalBroker,
|
|
@@ -243,6 +250,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
243
250
|
completeModelSelectionSideEffect,
|
|
244
251
|
componentHealthMonitor: services.componentHealthMonitor,
|
|
245
252
|
});
|
|
253
|
+
commandContextRef = commandContext;
|
|
246
254
|
|
|
247
255
|
const gitStatusProvider = new GitStatusProvider(services.workingDirectory);
|
|
248
256
|
const lastGitInfoRef = { value: undefined as GitHeaderInfo | undefined };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Tool } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
+
import type { ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
|
|
3
|
+
|
|
4
|
+
type AgentToolArgs = {
|
|
5
|
+
readonly mode?: unknown;
|
|
6
|
+
readonly template?: unknown;
|
|
7
|
+
readonly reviewMode?: unknown;
|
|
8
|
+
readonly dangerously_disable_wrfc?: unknown;
|
|
9
|
+
readonly tasks?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type AgentTaskArgs = {
|
|
13
|
+
readonly template?: unknown;
|
|
14
|
+
readonly reviewMode?: unknown;
|
|
15
|
+
readonly dangerously_disable_wrfc?: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const OWNER_BLOCKED_TEMPLATES = new Set(['reviewer', 'review', 'verifier', 'tester', 'test']);
|
|
19
|
+
|
|
20
|
+
export function installWrfcAgentToolGuard(registry: ToolRegistry): void {
|
|
21
|
+
const agentTool = registry.list().find((tool) => tool.definition.name === 'agent');
|
|
22
|
+
if (!agentTool) throw new Error('WRFC agent guard could not find the agent tool.');
|
|
23
|
+
wrapWrfcAgentTool(agentTool);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function wrapWrfcAgentTool(tool: Tool): void {
|
|
27
|
+
const originalExecute = tool.execute.bind(tool);
|
|
28
|
+
tool.execute = async (args) => {
|
|
29
|
+
const denial = validateWrfcAgentToolInvocation(args as AgentToolArgs);
|
|
30
|
+
if (denial) return { success: false, error: denial };
|
|
31
|
+
return originalExecute(args);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateWrfcAgentToolInvocation(args: AgentToolArgs): string | null {
|
|
36
|
+
if (args.mode === 'spawn') {
|
|
37
|
+
if (isWrfcEnabled(args, args) && isBlockedOwnerTemplate(args.template)) {
|
|
38
|
+
return [
|
|
39
|
+
'WRFC spawn blocked: a WRFC root task must be an owner/engineer task, not a reviewer/verifier/tester task.',
|
|
40
|
+
'Spawn one engineer/general owner with reviewMode:"wrfc"; WRFC creates reviewer and fixer agents only after owner output exists.',
|
|
41
|
+
].join(' ');
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args.mode !== 'batch-spawn') return null;
|
|
47
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks.filter(isRecord) : [];
|
|
48
|
+
const wrfcTasks = tasks.filter((task) => isWrfcEnabled(task, args));
|
|
49
|
+
if (wrfcTasks.length === 0) return null;
|
|
50
|
+
|
|
51
|
+
if (tasks.length !== 1 || wrfcTasks.length !== 1) {
|
|
52
|
+
return [
|
|
53
|
+
'WRFC batch-spawn blocked: WRFC must start as exactly one owner task.',
|
|
54
|
+
'Do not batch an engineer with reviewer/verifier/tester tasks.',
|
|
55
|
+
'Spawn one engineer/general owner with reviewMode:"wrfc"; the WRFC controller creates review/fix agents after the owner deliverable exists.',
|
|
56
|
+
].join(' ');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const [task] = wrfcTasks;
|
|
60
|
+
if (isBlockedOwnerTemplate(task.template ?? args.template)) {
|
|
61
|
+
return [
|
|
62
|
+
'WRFC batch-spawn blocked: the single WRFC task must be an owner/engineer task, not a reviewer/verifier/tester task.',
|
|
63
|
+
'Use template:"engineer" or template:"general" with reviewMode:"wrfc".',
|
|
64
|
+
].join(' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRecord(value: unknown): value is AgentTaskArgs {
|
|
71
|
+
return Boolean(value) && typeof value === 'object';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isWrfcEnabled(task: AgentTaskArgs, root: AgentToolArgs): boolean {
|
|
75
|
+
const disabled = task.dangerously_disable_wrfc === true || root.dangerously_disable_wrfc === true;
|
|
76
|
+
if (disabled) return false;
|
|
77
|
+
return task.reviewMode === 'wrfc' || root.reviewMode === 'wrfc';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isBlockedOwnerTemplate(value: unknown): boolean {
|
|
81
|
+
if (typeof value !== 'string') return false;
|
|
82
|
+
return OWNER_BLOCKED_TEMPLATES.has(value.trim().toLowerCase());
|
|
83
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.19.
|
|
9
|
+
let _version = '0.19.83';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|