@pellux/goodvibes-tui 0.19.82 → 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 CHANGED
@@ -4,6 +4,11 @@ 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
+
7
12
  ## [0.19.82] — 2026-05-09
8
13
 
9
14
  ### Changes
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.82-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.83-blue.svg)](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.82",
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",
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: {
@@ -267,7 +267,7 @@ export class ProjectPlanningPanel extends BasePanel {
267
267
  if (question.whyItMatters) {
268
268
  lines.push(...buildBodyText(width, `Why this matters: ${question.whyItMatters}`, C, C.dim));
269
269
  }
270
- if (question.recommendedAnswer) {
270
+ if (question.recommendedAnswer && !this.isGenericRecommendation(question.recommendedAnswer)) {
271
271
  lines.push(...buildBodyText(width, `Recommendation: ${question.recommendedAnswer}`, C, C.good));
272
272
  }
273
273
  lines.push(...buildBodyText(
@@ -321,7 +321,7 @@ export class ProjectPlanningPanel extends BasePanel {
321
321
  if (evaluation.nextQuestion.whyItMatters) {
322
322
  lines.push(...buildBodyText(width, `Why it matters: ${evaluation.nextQuestion.whyItMatters}`, C, C.dim));
323
323
  }
324
- if (evaluation.nextQuestion.recommendedAnswer) {
324
+ if (evaluation.nextQuestion.recommendedAnswer && !this.isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
325
325
  lines.push(...buildBodyText(width, `Recommended answer: ${evaluation.nextQuestion.recommendedAnswer}`, C, C.good));
326
326
  }
327
327
  }
@@ -469,6 +469,8 @@ export class ProjectPlanningPanel extends BasePanel {
469
469
  const actions: PlanningAnswerAction[] = [];
470
470
  const prompt = question.prompt.toLowerCase();
471
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');
472
474
  const isApprovalQuestion = prompt.includes('approved') || prompt.includes('approve') || prompt.includes('execution');
473
475
  if (isApprovalQuestion) {
474
476
  actions.push({
@@ -479,7 +481,31 @@ export class ProjectPlanningPanel extends BasePanel {
479
481
  kind: 'approve',
480
482
  });
481
483
  }
482
- if (question.recommendedAnswer?.trim()) {
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)) {
483
509
  actions.push({
484
510
  id: 'recommended',
485
511
  label: 'Use recommended answer',
@@ -517,6 +543,13 @@ export class ProjectPlanningPanel extends BasePanel {
517
543
  return actions;
518
544
  }
519
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
+
520
553
  private submitSelectedAction(question: ProjectPlanningQuestion, actions: readonly PlanningAnswerAction[]): void {
521
554
  const action = actions[this.clampActionIndex(actions.length)];
522
555
  if (!action || action.disabled || !action.answer.trim()) {
@@ -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.push({
147
- ...options.answeredQuestion,
148
- status: 'answered',
149
- answer: prompt,
150
- answeredAt: now,
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
- return {
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(
@@ -238,6 +496,20 @@ export class ProjectPlanningCoordinator {
238
496
  ].join('\n');
239
497
  }
240
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
+
241
513
  private isActive(state: ProjectPlanningState | null): boolean {
242
514
  return state?.metadata?.['active'] === true && state.executionApproved !== true;
243
515
  }
@@ -253,4 +525,17 @@ export class ProjectPlanningCoordinator {
253
525
  private isApproval(text: string): boolean {
254
526
  return APPROVAL_PATTERNS.some((pattern) => pattern.test(text));
255
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
+ }
256
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,
@@ -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.82';
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;