@pellux/goodvibes-sdk 0.33.37 → 0.34.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.
Files changed (83) hide show
  1. package/dist/client-auth/control-plane-auth-snapshot.d.ts +23 -0
  2. package/dist/client-auth/control-plane-auth-snapshot.d.ts.map +1 -1
  3. package/dist/client-auth/oauth-types.d.ts +18 -0
  4. package/dist/client-auth/oauth-types.d.ts.map +1 -1
  5. package/dist/client-auth/types.d.ts +26 -0
  6. package/dist/client-auth/types.d.ts.map +1 -1
  7. package/dist/client.d.ts +22 -0
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/contracts/artifacts/operator-contract.json +1463 -10
  10. package/dist/events/knowledge.d.ts +31 -0
  11. package/dist/events/knowledge.d.ts.map +1 -1
  12. package/dist/events/tasks.d.ts +58 -0
  13. package/dist/events/tasks.d.ts.map +1 -1
  14. package/dist/events/transport.d.ts +60 -0
  15. package/dist/events/transport.d.ts.map +1 -1
  16. package/dist/platform/agents/wrfc-config.d.ts +6 -0
  17. package/dist/platform/agents/wrfc-config.d.ts.map +1 -1
  18. package/dist/platform/agents/wrfc-config.js +6 -0
  19. package/dist/platform/agents/wrfc-controller.d.ts +74 -0
  20. package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
  21. package/dist/platform/agents/wrfc-controller.js +368 -9
  22. package/dist/platform/agents/wrfc-reporting.d.ts +60 -0
  23. package/dist/platform/agents/wrfc-reporting.d.ts.map +1 -1
  24. package/dist/platform/agents/wrfc-reporting.js +131 -7
  25. package/dist/platform/agents/wrfc-types.d.ts +12 -0
  26. package/dist/platform/agents/wrfc-types.d.ts.map +1 -1
  27. package/dist/platform/config/manager.d.ts +11 -0
  28. package/dist/platform/config/manager.d.ts.map +1 -1
  29. package/dist/platform/config/manager.js +17 -1
  30. package/dist/platform/config/schema-domain-core.d.ts +2 -0
  31. package/dist/platform/config/schema-domain-core.d.ts.map +1 -1
  32. package/dist/platform/config/schema-domain-core.js +17 -0
  33. package/dist/platform/config/schema-shared.d.ts +6 -0
  34. package/dist/platform/config/schema-shared.d.ts.map +1 -1
  35. package/dist/platform/config/schema-types.d.ts +9 -2
  36. package/dist/platform/config/schema-types.d.ts.map +1 -1
  37. package/dist/platform/control-plane/method-catalog-calendar.d.ts +9 -0
  38. package/dist/platform/control-plane/method-catalog-calendar.d.ts.map +1 -0
  39. package/dist/platform/control-plane/method-catalog-calendar.js +117 -0
  40. package/dist/platform/control-plane/method-catalog-channels.d.ts.map +1 -1
  41. package/dist/platform/control-plane/method-catalog-channels.js +174 -0
  42. package/dist/platform/control-plane/method-catalog-control-automation.d.ts.map +1 -1
  43. package/dist/platform/control-plane/method-catalog-control-automation.js +10 -4
  44. package/dist/platform/control-plane/method-catalog-email.d.ts +9 -0
  45. package/dist/platform/control-plane/method-catalog-email.d.ts.map +1 -0
  46. package/dist/platform/control-plane/method-catalog-email.js +101 -0
  47. package/dist/platform/control-plane/method-catalog-knowledge.js +4 -4
  48. package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
  49. package/dist/platform/control-plane/method-catalog-runtime.js +13 -0
  50. package/dist/platform/control-plane/method-catalog.d.ts.map +1 -1
  51. package/dist/platform/control-plane/method-catalog.js +4 -0
  52. package/dist/platform/control-plane/routes/automation.js +2 -2
  53. package/dist/platform/control-plane/routes/operator.d.ts.map +1 -1
  54. package/dist/platform/control-plane/routes/operator.js +2 -0
  55. package/dist/platform/control-plane/routes/sessions.js +1 -1
  56. package/dist/platform/daemon/http/router.d.ts.map +1 -1
  57. package/dist/platform/daemon/http/router.js +12 -2
  58. package/dist/platform/daemon/http-listener.d.ts +17 -0
  59. package/dist/platform/daemon/http-listener.d.ts.map +1 -1
  60. package/dist/platform/daemon/http-listener.js +186 -7
  61. package/dist/platform/knowledge/knowledge-api.d.ts +1 -1
  62. package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
  63. package/dist/platform/knowledge/knowledge-api.js +1 -1
  64. package/dist/platform/runtime/store/helpers/reducers/lifecycle.d.ts.map +1 -1
  65. package/dist/platform/runtime/store/helpers/reducers/lifecycle.js +4 -0
  66. package/dist/platform/runtime/store/helpers/reducers/sync.d.ts.map +1 -1
  67. package/dist/platform/runtime/store/helpers/reducers/sync.js +4 -0
  68. package/dist/platform/security/http-auth.d.ts.map +1 -1
  69. package/dist/platform/security/http-auth.js +11 -4
  70. package/dist/platform/security/user-auth.d.ts +98 -2
  71. package/dist/platform/security/user-auth.d.ts.map +1 -1
  72. package/dist/platform/security/user-auth.js +195 -18
  73. package/dist/platform/sessions/manager.d.ts +19 -2
  74. package/dist/platform/sessions/manager.d.ts.map +1 -1
  75. package/dist/platform/sessions/manager.js +43 -6
  76. package/dist/platform/state/memory-registry.d.ts +1 -1
  77. package/dist/platform/state/memory-registry.d.ts.map +1 -1
  78. package/dist/platform/state/memory-registry.js +2 -2
  79. package/dist/platform/state/memory-store.d.ts +1 -1
  80. package/dist/platform/state/memory-store.d.ts.map +1 -1
  81. package/dist/platform/state/memory-store.js +2 -2
  82. package/dist/platform/version.js +1 -1
  83. package/package.json +9 -9
@@ -2,18 +2,23 @@ import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { AgentMessageBus } from './message-bus.js';
4
4
  import {} from './completion-report.js';
5
- import { buildGateFailureTask, buildFixTask, buildReviewTask, parseEngineerCompletionReport, parseReviewerCompletionReport, } from './wrfc-reporting.js';
5
+ import { buildGateFailureTask, buildFixTask, buildReviewTask, parseEngineerCompletionReport, parseReviewerCompletionReport, verifyEngineerClaims, } from './wrfc-reporting.js';
6
6
  import { WrfcWorkmap } from './wrfc-workmap.js';
7
7
  import { AgentWorktree } from './worktree.js';
8
8
  import { completePlanItemsForAgent } from './wrfc-plan-sync.js';
9
9
  import { logger } from '../utils/logger.js';
10
10
  import { summarizeError } from '../utils/error-display.js';
11
11
  import { emitAgentCompleted, emitAgentFailed, emitAgentProgress, emitAgentRunning, emitWorkflowChainFailed, emitWorkflowFixAttempted, emitWorkflowReviewCompleted, } from '../runtime/emitters/index.js';
12
- import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, } from './wrfc-config.js';
12
+ import { getWrfcAutoCommit, getWrfcMaxFixAttempts, getWrfcScoreThreshold, getWrfcAgentHeartbeatTimeoutMs, } from './wrfc-config.js';
13
13
  import { buildEngineerConstraintAddendum, } from './wrfc-prompt-addenda.js';
14
14
  import { completeWrfcOrchestrationNode, createWrfcWorkflowContext, emitWrfcAutoCommitted, emitWrfcCascadeAbort, emitWrfcChainCreated, emitWrfcChainPassed, emitWrfcConstraintsEnumerated, emitWrfcGraphCreated, emitWrfcStateChanged, failWrfcOrchestrationNode, startWrfcOrchestrationNode, } from './wrfc-runtime-events.js';
15
15
  import { runWrfcGateChecks } from './wrfc-gate-runtime.js';
16
16
  export { extractScoreFromText, extractPassedFromText, extractIssuesFromText } from './wrfc-reporting.js';
17
+ /**
18
+ * Schema version for the serialized WRFC chain envelope.
19
+ * Increment when the WrfcChain shape changes in an incompatible way.
20
+ */
21
+ export const CURRENT_WRFC_CHAIN_SCHEMA_VERSION = 1;
17
22
  const VALID_TRANSITIONS = {
18
23
  pending: ['engineering'],
19
24
  engineering: ['integrating', 'reviewing', 'failed'],
@@ -34,6 +39,9 @@ export class WrfcController {
34
39
  sessionId;
35
40
  workmap;
36
41
  projectRoot;
42
+ skipClaimVerification;
43
+ /** Cached at construction time: whether projectRoot existed on disk when this controller was created. */
44
+ projectRootExistedAtStartup;
37
45
  runtimeBus;
38
46
  messageBus;
39
47
  planManager = null;
@@ -43,12 +51,20 @@ export class WrfcController {
43
51
  selectChildRoute;
44
52
  workPlanService = null;
45
53
  workPlanTaskQueues = new Map();
54
+ /** Tracks last-seen timestamp per agent for watchdog timeout. */
55
+ agentLastSeen = new Map();
56
+ /** Active watchdog timer handle, if any. */
57
+ watchdogTimer = null;
46
58
  constructor(runtimeBus, messageBus, deps) {
47
59
  this.runtimeBus = runtimeBus;
48
60
  this.messageBus = messageBus;
49
61
  this.agentManager = deps.agentManager;
50
62
  this.configManager = deps.configManager;
51
63
  this.projectRoot = deps.projectRoot;
64
+ this.skipClaimVerification = deps.skipClaimVerification ?? false;
65
+ // Cache existsSync at construction time — the workmap will mkdir under projectRoot
66
+ // during the first appendOwnerDecision, so checking later would always return true.
67
+ this.projectRootExistedAtStartup = existsSync(deps.projectRoot);
52
68
  this.createWorktree = deps.createWorktree ?? (() => new AgentWorktree(this.projectRoot));
53
69
  this.selectChildRoute = deps.selectChildRoute ?? null;
54
70
  this.sessionId = crypto.randomUUID().slice(0, 8);
@@ -102,10 +118,6 @@ export class WrfcController {
102
118
  this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain already has an active child agent');
103
119
  return true;
104
120
  }
105
- if (chain.state !== 'pending') {
106
- this.appendOwnerDecision(chain, 'resume_skipped', `WRFC chain state ${chain.state} cannot be resumed without an active phase result`);
107
- return true;
108
- }
109
121
  if (this.activeChainCount >= MAX_ACTIVE_CHAINS) {
110
122
  if (!this.chainQueue.some((queued) => queued.record.id === chain.ownerAgentId)) {
111
123
  const owner = this.agentManager.getStatus(chain.ownerAgentId);
@@ -115,6 +127,62 @@ export class WrfcController {
115
127
  this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain is queued because active chain capacity is full');
116
128
  return true;
117
129
  }
130
+ // Item 3: Resume interrupted chains from their recorded state.
131
+ if (chain.state === 'reviewing') {
132
+ // Re-spawn reviewer using the last engineer report on record.
133
+ const report = chain.engineerReport;
134
+ if (report) {
135
+ this.appendOwnerDecision(chain, 'resume_started', `WRFC owner re-spawning reviewer for interrupted chain (was reviewing)`);
136
+ // MIN-6: Re-inject phantom-work synthetic issue on resume if claimsVerified was false,
137
+ // so the flag is not silently laundered by the startReview synthetic-issue clear.
138
+ // NOTE: We intentionally only re-inject for claimsVerified===false (kind='unverified').
139
+ // The 'unverifiable_no_claims' path leaves claimsVerified===undefined; if that chain
140
+ // was interrupted mid-review, the reviewer already received the synthetic issue in
141
+ // its task text during startReview — it does not need re-injection on resume.
142
+ if (chain.claimsVerified === false) {
143
+ chain.syntheticIssues ??= [];
144
+ const alreadyInjected = chain.syntheticIssues.some((issue) => issue.description.includes('Claimed work not found on disk') ||
145
+ issue.description.includes('No work claimed and no git diff'));
146
+ if (!alreadyInjected) {
147
+ chain.syntheticIssues.push({
148
+ severity: 'critical',
149
+ description: 'Claimed work not found on disk (re-injected on resume): claimsVerified=false from prior engineering phase',
150
+ });
151
+ }
152
+ }
153
+ // Reset to engineering so transition() accepts reviewing.
154
+ chain.state = 'engineering';
155
+ this.startReview(chain, report);
156
+ return true;
157
+ }
158
+ this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain in reviewing state but no engineer report found for re-review');
159
+ return true;
160
+ }
161
+ if (chain.state === 'fixing') {
162
+ // Re-spawn fixer using the last reviewer report.
163
+ const reviewerReport = chain.reviewerReport;
164
+ if (reviewerReport) {
165
+ this.appendOwnerDecision(chain, 'resume_started', `WRFC owner re-spawning fixer for interrupted chain (was fixing)`);
166
+ // Reset to reviewing so transition() will allow fixing.
167
+ chain.state = 'reviewing';
168
+ this.startFix(chain, reviewerReport);
169
+ return true;
170
+ }
171
+ this.appendOwnerDecision(chain, 'resume_skipped', 'WRFC chain in fixing state but no reviewer report found for re-fix');
172
+ return true;
173
+ }
174
+ if (chain.state === 'awaiting_gates') {
175
+ this.appendOwnerDecision(chain, 'resume_started', `WRFC owner re-running gates for interrupted chain (was awaiting_gates)`);
176
+ this.checkAndRunGatesForAll().catch((error) => {
177
+ logger.error('WrfcController.resumeChain: gate phase error', { chainId: chain.id, error: summarizeError(error) });
178
+ this.failChain(chain, `Gate phase error during resume: ${summarizeError(error)}`);
179
+ });
180
+ return true;
181
+ }
182
+ if (chain.state !== 'pending') {
183
+ this.appendOwnerDecision(chain, 'resume_skipped', `WRFC chain state ${chain.state} cannot be resumed`);
184
+ return true;
185
+ }
118
186
  this.appendOwnerDecision(chain, 'resume_started', 'WRFC owner resumed pending chain');
119
187
  this.startEngineeringChain(chain, false);
120
188
  return true;
@@ -127,10 +195,110 @@ export class WrfcController {
127
195
  }
128
196
  return resumed;
129
197
  }
198
+ /**
199
+ * Item 3: Serialize a chain to a JSON string for durable storage.
200
+ * Returns null if the chain does not exist.
201
+ */
202
+ serializeChain(chainId) {
203
+ const chain = this.chains.get(chainId);
204
+ if (!chain)
205
+ return null;
206
+ try {
207
+ return JSON.stringify({ schemaVersion: CURRENT_WRFC_CHAIN_SCHEMA_VERSION, chain });
208
+ }
209
+ catch (error) {
210
+ logger.error('WrfcController.serializeChain: JSON serialization failed', { chainId, error: summarizeError(error) });
211
+ return null;
212
+ }
213
+ }
214
+ /**
215
+ * Item 3: Deserialize a chain from a JSON string.
216
+ * Returns null if the JSON is invalid, the required fields are missing, or
217
+ * the schema version is newer than this runtime supports.
218
+ *
219
+ * Schema versioning:
220
+ * - Missing schemaVersion (v0/legacy): accepted for back-compat — the JSON
221
+ * is the raw chain object directly.
222
+ * - schemaVersion === CURRENT_WRFC_CHAIN_SCHEMA_VERSION (1): unwrap { schemaVersion, chain }.
223
+ * - schemaVersion > CURRENT_WRFC_CHAIN_SCHEMA_VERSION: rejected — fail closed.
224
+ */
225
+ deserializeChain(json) {
226
+ let raw;
227
+ try {
228
+ raw = JSON.parse(json);
229
+ }
230
+ catch (error) {
231
+ logger.error('WrfcController.deserializeChain: JSON parse failed', { error: summarizeError(error) });
232
+ return null;
233
+ }
234
+ // Unwrap schema-versioned envelope or treat as legacy (v0) raw chain.
235
+ let candidate;
236
+ if (raw !== null
237
+ && typeof raw === 'object'
238
+ && 'schemaVersion' in raw
239
+ && typeof raw.schemaVersion === 'number') {
240
+ const version = raw.schemaVersion;
241
+ if (version > CURRENT_WRFC_CHAIN_SCHEMA_VERSION) {
242
+ logger.error('WrfcController.deserializeChain: future schemaVersion rejected — upgrade runtime to read this payload', {
243
+ schemaVersion: version,
244
+ supportedVersion: CURRENT_WRFC_CHAIN_SCHEMA_VERSION,
245
+ });
246
+ return null;
247
+ }
248
+ // version <= current: unwrap the chain field.
249
+ candidate = raw.chain;
250
+ }
251
+ else {
252
+ // Legacy v0: the JSON payload IS the chain directly.
253
+ candidate = raw;
254
+ }
255
+ // Structural validation of required fields.
256
+ if (!candidate
257
+ || typeof candidate !== 'object'
258
+ || !('id' in candidate) || typeof candidate.id !== 'string'
259
+ || !('state' in candidate) || typeof candidate.state !== 'string'
260
+ || !('ownerAgentId' in candidate) || typeof candidate.ownerAgentId !== 'string'
261
+ || !('task' in candidate) || typeof candidate.task !== 'string') {
262
+ logger.warn('WrfcController.deserializeChain: invalid chain JSON — missing required fields (id, state, ownerAgentId, task)');
263
+ return null;
264
+ }
265
+ return candidate;
266
+ }
267
+ /**
268
+ * Item 3: Import a deserialized chain into this controller instance.
269
+ * After importing, call resumeChain(chain.id) to continue from recorded state.
270
+ *
271
+ * Refuses to overwrite a non-terminal chain (state is not 'passed' or 'failed')
272
+ * to prevent accidental clobber of live chains. Use force=true to override.
273
+ * Always overwrites terminal chains (idempotent replay is safe for completed work).
274
+ *
275
+ * Returns true if the chain was imported, false if refused.
276
+ */
277
+ importChain(chain, force = false) {
278
+ const existing = this.chains.get(chain.id);
279
+ if (existing && existing.state !== 'passed' && existing.state !== 'failed' && !force) {
280
+ logger.warn('WrfcController.importChain: refused — existing chain is non-terminal; use force=true to overwrite', {
281
+ chainId: chain.id,
282
+ existingState: existing.state,
283
+ });
284
+ return false;
285
+ }
286
+ this.chains.set(chain.id, chain);
287
+ logger.info('WrfcController.importChain: chain imported', {
288
+ chainId: chain.id,
289
+ state: chain.state,
290
+ overwroteExisting: existing !== undefined,
291
+ });
292
+ return true;
293
+ }
130
294
  dispose() {
131
295
  for (const unsub of this.unsubscribers)
132
296
  unsub();
133
297
  this.unsubscribers = [];
298
+ if (this.watchdogTimer !== null) {
299
+ clearInterval(this.watchdogTimer);
300
+ this.watchdogTimer = null;
301
+ }
134
302
  }
135
303
  transition(chain, to) {
136
304
  const allowed = VALID_TRANSITIONS[chain.state];
@@ -237,6 +405,7 @@ export class WrfcController {
237
405
  }
238
406
  setupListeners() {
239
407
  const unsubComplete = this.runtimeBus.on('AGENT_COMPLETED', ({ payload }) => {
408
+ this.agentLastSeen.set(payload.agentId, Date.now());
240
409
  this.onAgentComplete(payload.agentId).catch((error) => {
241
410
  logger.error('WrfcController.onAgentComplete unhandled error', {
242
411
  agentId: payload.agentId,
@@ -245,12 +414,104 @@ export class WrfcController {
245
414
  });
246
415
  });
247
416
  const unsubError = this.runtimeBus.on('AGENT_FAILED', ({ payload }) => {
417
+ this.agentLastSeen.set(payload.agentId, Date.now());
248
418
  this.onAgentFailed(payload.agentId, payload.error);
249
419
  });
250
420
  const unsubCancelled = this.runtimeBus.on('AGENT_CANCELLED', ({ payload }) => {
421
+ this.agentLastSeen.set(payload.agentId, Date.now());
251
422
  this.onAgentCancelled(payload.agentId, payload.reason);
252
423
  });
253
- this.unsubscribers.push(unsubComplete, unsubError, unsubCancelled);
424
+ const unsubRunning = this.runtimeBus.on('AGENT_RUNNING', ({ payload }) => {
425
+ this.agentLastSeen.set(payload.agentId, Date.now());
426
+ });
427
+ const unsubStreamDelta = this.runtimeBus.on('AGENT_STREAM_DELTA', ({ payload }) => {
428
+ // MAJ-3: Reset agentLastSeen on streaming output so watchdog does not
429
+ // time out a streaming-only agent that emits no PROGRESS events.
430
+ this.agentLastSeen.set(payload.agentId, Date.now());
431
+ });
432
+ const unsubProgress = this.runtimeBus.on('AGENT_PROGRESS', ({ payload }) => {
433
+ this.agentLastSeen.set(payload.agentId, Date.now());
434
+ });
435
+ this.unsubscribers.push(unsubComplete, unsubError, unsubCancelled, unsubRunning, unsubProgress, unsubStreamDelta);
436
+ this.resetWatchdog();
437
+ }
438
+ /**
439
+ * Start (or restart) the watchdog timer based on current config.
440
+ * Called once on setup and whenever the timeout config may have changed.
441
+ */
442
+ resetWatchdog() {
443
+ if (this.watchdogTimer !== null) {
444
+ clearInterval(this.watchdogTimer);
445
+ this.watchdogTimer = null;
446
+ }
447
+ const timeoutMs = getWrfcAgentHeartbeatTimeoutMs(this.configManager);
448
+ if (timeoutMs <= 0)
449
+ return;
450
+ // Check at 1/4 the timeout interval, but no less than 50ms (for testability)
451
+ // and no more than 5 seconds (to avoid excessive polling in production).
452
+ const intervalMs = Math.min(5_000, Math.max(50, Math.floor(timeoutMs / 4)));
453
+ this.watchdogTimer = setInterval(() => {
454
+ this.tickWatchdog(timeoutMs);
455
+ }, intervalMs);
456
+ this.watchdogTimer.unref?.();
457
+ }
458
+ /** Tick: fail any chain whose active child agent has been silent longer than timeoutMs. */
459
+ tickWatchdog(timeoutMs) {
460
+ const now = Date.now();
461
+ for (const chain of this.chains.values()) {
462
+ if (chain.state === 'passed' || chain.state === 'failed' || chain.state === 'pending')
463
+ continue;
464
+ // Find the active child agent for this chain.
465
+ const activeAgentId = this.activeChildAgentId(chain);
466
+ if (!activeAgentId)
467
+ continue;
468
+ const record = this.agentManager.getStatus(activeAgentId);
469
+ if (!record || (record.status !== 'running' && record.status !== 'pending'))
470
+ continue;
471
+ const lastSeen = this.agentLastSeen.get(activeAgentId) ?? record.startedAt ?? now;
472
+ const silentMs = now - lastSeen;
473
+ if (silentMs >= timeoutMs) {
474
+ logger.error('WrfcController.watchdog: agent silent, failing chain', {
475
+ chainId: chain.id,
476
+ agentId: activeAgentId,
477
+ silentMs,
478
+ timeoutMs,
479
+ });
480
+ this.failChain(chain, `Agent ${activeAgentId} went silent for ${Math.round(silentMs / 1000)}s (timeout: ${Math.round(timeoutMs / 1000)}s)`);
481
+ }
482
+ }
483
+ }
484
+ /**
485
+ * Returns true when verifyEngineerClaims should be skipped for a completion event.
486
+ *
487
+ * Skip conditions (applied uniformly to engineer AND fixer completions):
488
+ * 1. `this.skipClaimVerification` is true — explicit opt-out for test harnesses that
489
+ * use real /tmp paths (directories that exist on disk) where files are never actually
490
+ * written to disk by agents. Use this when the environment-driven skip does not apply.
491
+ * 2. `!this.projectRootExistedAtStartup` — environment-driven skip: projectRoot did not
492
+ * exist on disk when this controller was constructed. Cached at construction time because
493
+ * the WrfcWorkmap mkdir's the directory tree on the first appendOwnerDecision call,
494
+ * making a late-bound existsSync check unreliable (would always return true after that).
495
+ * Preferred mechanism; harnesses should use nonexistent projectRoot paths when feasible.
496
+ *
497
+ * PRODUCTION INVARIANT: In any real GoodVibes session the projectRoot is the cloned
498
+ * repo root which always exists at startup. Both skip conditions are false in production;
499
+ * claim verification always runs for both engineer and fixer completions.
500
+ */
501
+ shouldSkipClaimVerification() {
502
+ return this.skipClaimVerification || !this.projectRootExistedAtStartup;
503
+ }
504
+ /** Returns the single currently-active child agent ID for a chain, if deterministic. */
505
+ activeChildAgentId(chain) {
506
+ if (chain.state === 'reviewing')
507
+ return chain.reviewerAgentId ?? null;
508
+ if (chain.state === 'fixing')
509
+ return chain.fixerAgentId ?? null;
510
+ if (chain.state === 'engineering')
511
+ return chain.engineerAgentId ?? null;
512
+ if (chain.state === 'integrating')
513
+ return chain.integratorAgentId ?? null;
514
+ return null;
254
515
  }
255
516
  async onAgentComplete(agentId) {
256
517
  const chain = this.findChainByAgentId(agentId);
@@ -406,7 +667,8 @@ export class WrfcController {
406
667
  });
407
668
  }
408
669
  const { constraintsSatisfied, constraintsTotal, unsatisfiedConstraintIds, constraintFailure, } = constraintEvaluation;
409
- const passed = review.score >= threshold && !constraintFailure;
670
+ // MIN-4: claimsVerified===false is a mechanical block — cannot pass review regardless of score.
671
+ const passed = review.score >= threshold && !constraintFailure && chain.claimsVerified !== false;
410
672
  this.completeCurrentNode(chain, `Score ${review.score}/10${passed ? ' passed' : ' needs fixes'}`);
411
673
  emitWorkflowReviewCompleted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
412
674
  chainId: chain.id,
@@ -1158,6 +1420,62 @@ export class WrfcController {
1158
1420
  // The authoritative constraint list is NOT overwritten — chain.constraints remains the
1159
1421
  // original enumeration from the initial engineer turn.
1160
1422
  }
1423
+ // Item 2 / MAJ-1 / MAJ-9: Verify claims before handing off to reviewer.
1424
+ // Runs for BOTH the initial engineer pass (chain.state === 'engineering') AND fixer
1425
+ // re-runs (chain.state === 'fixing'). A lying fixer that claims files it did not write
1426
+ // is the same phantom-work pattern as a lying engineer — the same tri-state logic applies.
1427
+ //
1428
+ // Skip conditions (see shouldSkipClaimVerification for full rationale):
1429
+ // - Explicit opt-out (skipClaimVerification constructor flag) — harness use only.
1430
+ // - Environment-driven: projectRoot does not exist on disk (existsSync false).
1431
+ //
1432
+ // Tri-state kind logic (same for engineer and fixer passes):
1433
+ // 'files_verified' → claimsVerified=true, no synthetic issue.
1434
+ // 'git_corroborated' → claimsVerified=true, no synthetic issue.
1435
+ // 'verified_empty' → claimsVerified=true, no synthetic issue (git confirms real work, no file list required).
1436
+ // 'unverifiable_no_claims' → claimsVerified=undefined, inject advisory synthetic issue.
1437
+ // NOTE: claimsVerified is left undefined (not false) because we cannot confirm
1438
+ // work WAS done, but we also cannot confirm it WASN't. The synthetic issue
1439
+ // is the enforcement mechanism — the mechanical MIN-4 gate is NOT applied.
1440
+ // For fixers, a zero-claims completion is MORE suspicious (a fix round
1441
+ // by definition follows concrete reviewer findings) — but we keep the same
1442
+ // advisory contract for consistency; the reviewer sees the synthetic issue.
1443
+ // 'unverified' → claimsVerified=false, inject synthetic issue; MIN-4 gate will block pass.
1444
+ if (!this.shouldSkipClaimVerification()) {
1445
+ const claimVerification = verifyEngineerClaims(reportForReview, this.projectRoot);
1446
+ if (claimVerification.kind === 'unverifiable_no_claims') {
1447
+ // Leave chain.claimsVerified as undefined — not a confirmed false, but suspicious.
1448
+ const agentClass = chain.state === 'fixing' ? 'fixer' : 'engineer';
1449
+ logger.warn(`WrfcController: ${agentClass} sent success prose with no claims and no git diff — suspected phantom work`, {
1450
+ chainId: chain.id,
1451
+ kind: claimVerification.kind,
1452
+ summary: claimVerification.summary,
1453
+ });
1454
+ chain.syntheticIssues ??= [];
1455
+ chain.syntheticIssues.push({
1456
+ severity: 'critical',
1457
+ description: `No work claimed and no git diff detected — suspected phantom work: ${claimVerification.summary}`,
1458
+ });
1459
+ }
1460
+ else {
1461
+ chain.claimsVerified = claimVerification.verified;
1462
+ if (!claimVerification.verified) {
1463
+ // kind === 'unverified': claims present but missing on disk and no git corroboration.
1464
+ const agentClass = chain.state === 'fixing' ? 'fixer' : 'engineer';
1465
+ logger.warn(`WrfcController: ${agentClass} claim verification failed — phantom work detected`, {
1466
+ chainId: chain.id,
1467
+ kind: claimVerification.kind,
1468
+ summary: claimVerification.summary,
1469
+ missingPaths: claimVerification.missingPaths,
1470
+ });
1471
+ chain.syntheticIssues ??= [];
1472
+ chain.syntheticIssues.push({
1473
+ severity: 'critical',
1474
+ description: `Claimed work not found on disk: ${claimVerification.summary}`,
1475
+ });
1476
+ }
1477
+ }
1478
+ }
1161
1479
  this.startReview(chain, reportForReview);
1162
1480
  }
1163
1481
  buildSubtaskEngineerTask(chain, subtask) {
@@ -1286,6 +1604,46 @@ export class WrfcController {
1286
1604
  subtask.syntheticIssues.push({ severity: 'critical', description });
1287
1605
  }
1288
1606
  subtask.engineerReport = reportForReview;
1607
+ // Item 2 / MAJ-1: Verify engineer claims for compound subtask before handing off to reviewer.
1608
+ // Tri-state kind logic (mirrors handleEngineerCompletion):
1609
+ // 'files_verified' → claimsVerified=true, no synthetic issue.
1610
+ // 'git_corroborated' → claimsVerified=true, no synthetic issue.
1611
+ // 'verified_empty' → claimsVerified=true, no synthetic issue.
1612
+ // 'unverifiable_no_claims' → claimsVerified=undefined, inject advisory synthetic issue only (no MIN-4 mechanical block).
1613
+ // 'unverified' → claimsVerified=false, inject synthetic issue; MIN-4 gate blocks pass.
1614
+ const subtaskClaimVerification = verifyEngineerClaims(reportForReview, this.projectRoot);
1615
+ if (subtaskClaimVerification.kind === 'unverifiable_no_claims') {
1616
+ // Leave subtask.claimsVerified as undefined — suspicious but not a confirmed false.
1617
+ logger.warn('WrfcController: compound subtask engineer sent success prose with no claims and no git diff — suspected phantom work', {
1618
+ chainId: chain.id,
1619
+ subtaskId: subtask.id,
1620
+ kind: subtaskClaimVerification.kind,
1621
+ summary: subtaskClaimVerification.summary,
1622
+ });
1623
+ subtask.syntheticIssues ??= [];
1624
+ subtask.syntheticIssues.push({
1625
+ severity: 'critical',
1626
+ description: `No work claimed and no git diff detected for subtask ${subtask.id} — suspected phantom work: ${subtaskClaimVerification.summary}`,
1627
+ });
1628
+ }
1629
+ else {
1630
+ subtask.claimsVerified = subtaskClaimVerification.verified;
1631
+ if (!subtaskClaimVerification.verified) {
1632
+ // kind === 'unverified': claims present but missing on disk and no git corroboration.
1633
+ logger.warn('WrfcController: compound engineer claim verification failed — phantom work detected', {
1634
+ chainId: chain.id,
1635
+ subtaskId: subtask.id,
1636
+ kind: subtaskClaimVerification.kind,
1637
+ summary: subtaskClaimVerification.summary,
1638
+ missingPaths: subtaskClaimVerification.missingPaths,
1639
+ });
1640
+ subtask.syntheticIssues ??= [];
1641
+ subtask.syntheticIssues.push({
1642
+ severity: 'critical',
1643
+ description: `Claimed work not found on disk (${subtask.id}): ${subtaskClaimVerification.summary}`,
1644
+ });
1645
+ }
1646
+ }
1289
1647
  this.startCompoundSubtaskReview(chain, subtask, reportForReview);
1290
1648
  }
1291
1649
  startCompoundSubtaskReview(chain, subtask, report) {
@@ -1320,7 +1678,8 @@ export class WrfcController {
1320
1678
  async processCompoundSubtaskReview(chain, subtask, review) {
1321
1679
  const threshold = getWrfcScoreThreshold(this.configManager);
1322
1680
  const constraintEvaluation = this.evaluateSubtaskConstraints(subtask, review);
1323
- const passed = review.score >= threshold && !constraintEvaluation.constraintFailure;
1681
+ // MIN-4: claimsVerified===false is a mechanical block on compound subtasks too.
1682
+ const passed = review.score >= threshold && !constraintEvaluation.constraintFailure && subtask.claimsVerified !== false;
1324
1683
  this.completeSubtaskNode(chain, subtask, `Score ${review.score}/10${passed ? ' passed' : ' needs fixes'}`);
1325
1684
  emitWorkflowReviewCompleted(this.runtimeBus, createWrfcWorkflowContext(this.sessionId, chain.id), {
1326
1685
  chainId: chain.id,
@@ -4,9 +4,69 @@ type ReviewableCompletionReport = CompletionReport & {
4
4
  reviewableOutput?: string | undefined;
5
5
  };
6
6
  export declare function extractScoreFromText(text: string): number | null;
7
+ /**
8
+ * Determines whether a review verdict passes.
9
+ *
10
+ * Score >= threshold is a NECESSARY condition. Prose language ("passed",
11
+ * "approved") is treated as confirmation only — it can never elevate a
12
+ * sub-threshold score to a pass verdict.
13
+ *
14
+ * This is intentionally fail-closed: if the score is below threshold,
15
+ * the result is always false regardless of what the reviewer wrote.
16
+ */
7
17
  export declare function extractPassedFromText(text: string, score: number, threshold: number): boolean;
8
18
  export declare function extractIssuesFromText(text: string): ReviewerReport['issues'];
9
19
  export declare function parseEngineerCompletionReport(rawOutput: string, _template?: string): ReviewableCompletionReport;
20
+ /**
21
+ * Discriminator for claim verification outcome:
22
+ * - 'files_verified': claims present and all found on disk.
23
+ * - 'git_corroborated': claims present, some missing on disk, but git diff shows changes.
24
+ * - 'verified_empty': no claims made but git diff shows changes (engineer did real work without listing files).
25
+ * - 'unverifiable_no_claims': no claims AND no git diff — suspicious; treated as phantom work.
26
+ * - 'unverified': claims present but not found on disk and git shows no changes.
27
+ */
28
+ export type ClaimVerificationKind = 'files_verified' | 'git_corroborated' | 'verified_empty' | 'unverifiable_no_claims' | 'unverified';
29
+ /** Per-file result for claim verification. */
30
+ export interface ClaimVerificationResult {
31
+ /** All paths claimed as created, modified, or deleted. */
32
+ claimedPaths: string[];
33
+ /** Paths that exist on disk (for created/modified claims). */
34
+ foundPaths: string[];
35
+ /** Paths that were claimed but not found on disk. */
36
+ missingPaths: string[];
37
+ /** Whether git diff/status shows any changes since the engineer started. */
38
+ gitDiffDetected: boolean | null;
39
+ /**
40
+ * Tri-state discriminator. Use this instead of the bare `verified` boolean
41
+ * to distinguish 'unverifiable_no_claims' (suspicious) from 'verified_empty'
42
+ * (legit no-file work with a git diff). Controllers must treat 'unverifiable_no_claims'
43
+ * as phantom work and inject a synthetic issue.
44
+ */
45
+ kind: ClaimVerificationKind;
46
+ /**
47
+ * Convenience: true iff kind is NOT 'unverified' or 'unverifiable_no_claims'.
48
+ * NOTE: Callers should use `kind` directly when deciding whether to set `chain.claimsVerified`.
49
+ * In particular, `unverifiable_no_claims` returns `verified: false` here but the controller
50
+ * intentionally leaves `chain.claimsVerified` as `undefined` (not `false`) because suspicion
51
+ * cannot be confirmed. Do NOT blindly propagate `result.verified` into chain state.
52
+ */
53
+ verified: boolean;
54
+ /** Human-readable summary of what was and wasn't found. */
55
+ summary: string;
56
+ }
57
+ /**
58
+ * Verifies that an engineer's self-reported work actually materialised on disk.
59
+ *
60
+ * Strategy:
61
+ * 1. Stat every path claimed in filesCreated/filesModified.
62
+ * 2. If any claimed paths are missing, check git diff/status as a fallback
63
+ * (the engineer may have written to a path not literally listed).
64
+ * 3. If no paths were claimed at all, fall through to git as the sole signal.
65
+ *
66
+ * This is intentionally lenient about the git check — a non-empty diff is
67
+ * treated as corroborating evidence even when individual file stats fail.
68
+ */
69
+ export declare function verifyEngineerClaims(report: CompletionReport, projectRoot: string): ClaimVerificationResult;
10
70
  export declare function parseReviewerCompletionReport(chainId: string, rawOutput: string, threshold: number): ReviewerReport;
11
71
  export declare function buildReviewTask(chainId: string, originalTask: string, report: ReviewableCompletionReport, threshold: number, constraints?: Constraint[]): string;
12
72
  export declare function buildFixTask(chainId: string, originalTask: string, review: ReviewerReport, threshold: number, fixAttempts: number, constraints?: Constraint[], constraintFindings?: ConstraintFinding[]): string;
@@ -1 +1 @@
1
- {"version":3,"file":"wrfc-reporting.d.ts","sourceRoot":"","sources":["../../../src/platform/agents/wrfc-reporting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAkB,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG9H,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAQzD,KAAK,0BAA0B,GAAG,gBAAgB,GAAG;IACnD,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBhE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAK7F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAa5E;AAED,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,0BAA0B,CAkB/G;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,cAAc,CA6BhB;AAID,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,0BAA0B,EAClC,SAAS,EAAE,MAAM,EACjB,WAAW,GAAE,UAAU,EAAO,GAC7B,MAAM,CAuDR;AAiGD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,GAAE,UAAU,EAAO,EAC9B,kBAAkB,GAAE,iBAAiB,EAAO,GAC3C,MAAM,CAiER;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,SAAS,iBAAiB,EAAE,EACzC,WAAW,GAAE,SAAS,UAAU,EAAO,GACtC,MAAM,CAuCR"}
1
+ {"version":3,"file":"wrfc-reporting.d.ts","sourceRoot":"","sources":["../../../src/platform/agents/wrfc-reporting.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,iBAAiB,EAAkB,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG9H,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAQzD,KAAK,0BAA0B,GAAG,gBAAgB,GAAG;IACnD,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuBhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAO7F;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAAC,QAAQ,CAAC,CAa5E;AAED,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,0BAA0B,CAkB/G;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,qBAAqB,GAC7B,gBAAgB,GAChB,kBAAkB,GAClB,gBAAgB,GAChB,wBAAwB,GACxB,YAAY,CAAC;AAEjB,8CAA8C;AAC9C,MAAM,WAAW,uBAAuB;IACtC,0DAA0D;IAC1D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,qDAAqD;IACrD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,4EAA4E;IAC5E,eAAe,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC;;;;;OAKG;IACH,IAAI,EAAE,qBAAqB,CAAC;IAC5B;;;;;;OAMG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,gBAAgB,EACxB,WAAW,EAAE,MAAM,GAClB,uBAAuB,CAmGzB;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,cAAc,CA6BhB;AAID,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,0BAA0B,EAClC,SAAS,EAAE,MAAM,EACjB,WAAW,GAAE,UAAU,EAAO,GAC7B,MAAM,CAuDR;AAiGD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,GAAE,UAAU,EAAO,EAC9B,kBAAkB,GAAE,iBAAiB,EAAO,GAC3C,MAAM,CAiER;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,SAAS,iBAAiB,EAAE,EACzC,WAAW,GAAE,SAAS,UAAU,EAAO,GACtC,MAAM,CAuCR"}