@pellux/goodvibes-sdk 0.33.36 → 0.33.38
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/dist/client-auth/control-plane-auth-snapshot.d.ts +23 -0
- package/dist/client-auth/control-plane-auth-snapshot.d.ts.map +1 -1
- package/dist/client-auth/oauth-types.d.ts +18 -0
- package/dist/client-auth/oauth-types.d.ts.map +1 -1
- package/dist/client-auth/types.d.ts +26 -0
- package/dist/client-auth/types.d.ts.map +1 -1
- package/dist/client.d.ts +22 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/contracts/artifacts/operator-contract.json +84 -10
- package/dist/events/knowledge.d.ts +31 -0
- package/dist/events/knowledge.d.ts.map +1 -1
- package/dist/events/tasks.d.ts +58 -0
- package/dist/events/tasks.d.ts.map +1 -1
- package/dist/events/transport.d.ts +60 -0
- package/dist/events/transport.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-config.d.ts +6 -0
- package/dist/platform/agents/wrfc-config.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-config.js +6 -0
- package/dist/platform/agents/wrfc-controller.d.ts +74 -0
- package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-controller.js +368 -9
- package/dist/platform/agents/wrfc-reporting.d.ts +60 -0
- package/dist/platform/agents/wrfc-reporting.d.ts.map +1 -1
- package/dist/platform/agents/wrfc-reporting.js +131 -7
- package/dist/platform/agents/wrfc-types.d.ts +12 -0
- package/dist/platform/agents/wrfc-types.d.ts.map +1 -1
- package/dist/platform/config/manager.d.ts +11 -0
- package/dist/platform/config/manager.d.ts.map +1 -1
- package/dist/platform/config/manager.js +17 -1
- package/dist/platform/config/schema-domain-core.d.ts +2 -0
- package/dist/platform/config/schema-domain-core.d.ts.map +1 -1
- package/dist/platform/config/schema-domain-core.js +17 -0
- package/dist/platform/config/schema-shared.d.ts +6 -0
- package/dist/platform/config/schema-shared.d.ts.map +1 -1
- package/dist/platform/config/schema-types.d.ts +9 -2
- package/dist/platform/config/schema-types.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-control-automation.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-control-automation.js +10 -4
- package/dist/platform/control-plane/method-catalog-knowledge.js +4 -4
- package/dist/platform/control-plane/method-catalog-runtime.d.ts.map +1 -1
- package/dist/platform/control-plane/method-catalog-runtime.js +13 -0
- package/dist/platform/control-plane/routes/automation.js +2 -2
- package/dist/platform/control-plane/routes/operator.d.ts.map +1 -1
- package/dist/platform/control-plane/routes/operator.js +2 -0
- package/dist/platform/control-plane/routes/sessions.js +1 -1
- package/dist/platform/daemon/http/openai-compatible-routes.d.ts.map +1 -1
- package/dist/platform/daemon/http/openai-compatible-routes.js +4 -12
- package/dist/platform/daemon/http/router.d.ts.map +1 -1
- package/dist/platform/daemon/http/router.js +15 -51
- package/dist/platform/daemon/http-listener.d.ts +17 -0
- package/dist/platform/daemon/http-listener.d.ts.map +1 -1
- package/dist/platform/daemon/http-listener.js +186 -7
- package/dist/platform/knowledge/knowledge-api.d.ts +1 -1
- package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
- package/dist/platform/knowledge/knowledge-api.js +1 -1
- package/dist/platform/runtime/store/helpers/reducers/lifecycle.d.ts.map +1 -1
- package/dist/platform/runtime/store/helpers/reducers/lifecycle.js +4 -0
- package/dist/platform/runtime/store/helpers/reducers/sync.d.ts.map +1 -1
- package/dist/platform/runtime/store/helpers/reducers/sync.js +4 -0
- package/dist/platform/security/http-auth.d.ts.map +1 -1
- package/dist/platform/security/http-auth.js +11 -4
- package/dist/platform/security/user-auth.d.ts +98 -2
- package/dist/platform/security/user-auth.d.ts.map +1 -1
- package/dist/platform/security/user-auth.js +195 -18
- package/dist/platform/sessions/manager.d.ts +19 -2
- package/dist/platform/sessions/manager.d.ts.map +1 -1
- package/dist/platform/sessions/manager.js +43 -6
- package/dist/platform/state/memory-registry.d.ts +1 -1
- package/dist/platform/state/memory-registry.d.ts.map +1 -1
- package/dist/platform/state/memory-registry.js +2 -2
- package/dist/platform/state/memory-store.d.ts +1 -1
- package/dist/platform/state/memory-store.d.ts.map +1 -1
- package/dist/platform/state/memory-store.js +2 -2
- package/dist/platform/version.js +1 -1
- package/package.json +9 -9
|
@@ -9,6 +9,11 @@ import type { RuntimeEventBus } from '../runtime/events/index.js';
|
|
|
9
9
|
import type { ProjectWorkPlanTaskCreateInput, ProjectWorkPlanTaskUpdateInput } from '../knowledge/project-planning/index.js';
|
|
10
10
|
import { type AgentManagerLike } from './wrfc-config.js';
|
|
11
11
|
export { extractScoreFromText, extractPassedFromText, extractIssuesFromText } from './wrfc-reporting.js';
|
|
12
|
+
/**
|
|
13
|
+
* Schema version for the serialized WRFC chain envelope.
|
|
14
|
+
* Increment when the WrfcChain shape changes in an incompatible way.
|
|
15
|
+
*/
|
|
16
|
+
export declare const CURRENT_WRFC_CHAIN_SCHEMA_VERSION = 1;
|
|
12
17
|
type WrfcWorktreeOps = Pick<AgentWorktree, 'merge' | 'cleanup'> & Partial<Pick<AgentWorktree, 'commitWorkingTree' | 'currentHead'>>;
|
|
13
18
|
type WrfcWorkPlanService = {
|
|
14
19
|
createWorkPlanTask(input: ProjectWorkPlanTaskCreateInput): Promise<unknown>;
|
|
@@ -22,6 +27,9 @@ export declare class WrfcController {
|
|
|
22
27
|
private readonly sessionId;
|
|
23
28
|
private readonly workmap;
|
|
24
29
|
private readonly projectRoot;
|
|
30
|
+
private readonly skipClaimVerification;
|
|
31
|
+
/** Cached at construction time: whether projectRoot existed on disk when this controller was created. */
|
|
32
|
+
private readonly projectRootExistedAtStartup;
|
|
25
33
|
private runtimeBus;
|
|
26
34
|
private readonly messageBus;
|
|
27
35
|
private planManager;
|
|
@@ -31,6 +39,10 @@ export declare class WrfcController {
|
|
|
31
39
|
private readonly selectChildRoute;
|
|
32
40
|
private workPlanService;
|
|
33
41
|
private readonly workPlanTaskQueues;
|
|
42
|
+
/** Tracks last-seen timestamp per agent for watchdog timeout. */
|
|
43
|
+
private readonly agentLastSeen;
|
|
44
|
+
/** Active watchdog timer handle, if any. */
|
|
45
|
+
private watchdogTimer;
|
|
34
46
|
constructor(runtimeBus: RuntimeEventBus, messageBus: Pick<AgentMessageBus, 'registerAgent'>, deps: {
|
|
35
47
|
readonly agentManager: AgentManagerLike;
|
|
36
48
|
readonly configManager: Pick<ConfigManager, 'get' | 'getCategory'>;
|
|
@@ -38,6 +50,13 @@ export declare class WrfcController {
|
|
|
38
50
|
readonly surfaceRoot?: string | undefined;
|
|
39
51
|
readonly createWorktree?: (() => WrfcWorktreeOps) | undefined;
|
|
40
52
|
readonly selectChildRoute?: WrfcChildRouteSelector | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* When true, skip verifyEngineerClaims for both engineer and fixer completions.
|
|
55
|
+
* Use ONLY in test harnesses where projectRoot is a synthetic path without real files.
|
|
56
|
+
* Production code must NEVER set this flag — it disables the phantom-work guard.
|
|
57
|
+
* Prefer the environment-driven skip (nonexistent projectRoot) where possible.
|
|
58
|
+
*/
|
|
59
|
+
readonly skipClaimVerification?: boolean;
|
|
41
60
|
});
|
|
42
61
|
createChain(ownerRecord: AgentRecord): WrfcChain;
|
|
43
62
|
getSessionId(): string;
|
|
@@ -49,6 +68,34 @@ export declare class WrfcController {
|
|
|
49
68
|
listChains(): WrfcChain[];
|
|
50
69
|
resumeChain(chainId: string): boolean;
|
|
51
70
|
resumeAllActiveChains(): number;
|
|
71
|
+
/**
|
|
72
|
+
* Item 3: Serialize a chain to a JSON string for durable storage.
|
|
73
|
+
* Returns null if the chain does not exist.
|
|
74
|
+
*/
|
|
75
|
+
serializeChain(chainId: string): string | null;
|
|
76
|
+
/**
|
|
77
|
+
* Item 3: Deserialize a chain from a JSON string.
|
|
78
|
+
* Returns null if the JSON is invalid, the required fields are missing, or
|
|
79
|
+
* the schema version is newer than this runtime supports.
|
|
80
|
+
*
|
|
81
|
+
* Schema versioning:
|
|
82
|
+
* - Missing schemaVersion (v0/legacy): accepted for back-compat — the JSON
|
|
83
|
+
* is the raw chain object directly.
|
|
84
|
+
* - schemaVersion === CURRENT_WRFC_CHAIN_SCHEMA_VERSION (1): unwrap { schemaVersion, chain }.
|
|
85
|
+
* - schemaVersion > CURRENT_WRFC_CHAIN_SCHEMA_VERSION: rejected — fail closed.
|
|
86
|
+
*/
|
|
87
|
+
deserializeChain(json: string): WrfcChain | null;
|
|
88
|
+
/**
|
|
89
|
+
* Item 3: Import a deserialized chain into this controller instance.
|
|
90
|
+
* After importing, call resumeChain(chain.id) to continue from recorded state.
|
|
91
|
+
*
|
|
92
|
+
* Refuses to overwrite a non-terminal chain (state is not 'passed' or 'failed')
|
|
93
|
+
* to prevent accidental clobber of live chains. Use force=true to override.
|
|
94
|
+
* Always overwrites terminal chains (idempotent replay is safe for completed work).
|
|
95
|
+
*
|
|
96
|
+
* Returns true if the chain was imported, false if refused.
|
|
97
|
+
*/
|
|
98
|
+
importChain(chain: WrfcChain, force?: boolean): boolean;
|
|
52
99
|
dispose(): void;
|
|
53
100
|
private transition;
|
|
54
101
|
private applyWrfcAgentMetadata;
|
|
@@ -56,6 +103,33 @@ export declare class WrfcController {
|
|
|
56
103
|
private ownerProgress;
|
|
57
104
|
private wrfcPhaseOrder;
|
|
58
105
|
private setupListeners;
|
|
106
|
+
/**
|
|
107
|
+
* Start (or restart) the watchdog timer based on current config.
|
|
108
|
+
* Called once on setup and whenever the timeout config may have changed.
|
|
109
|
+
*/
|
|
110
|
+
private resetWatchdog;
|
|
111
|
+
/** Tick: fail any chain whose active child agent has been silent longer than timeoutMs. */
|
|
112
|
+
private tickWatchdog;
|
|
113
|
+
/**
|
|
114
|
+
* Returns true when verifyEngineerClaims should be skipped for a completion event.
|
|
115
|
+
*
|
|
116
|
+
* Skip conditions (applied uniformly to engineer AND fixer completions):
|
|
117
|
+
* 1. `this.skipClaimVerification` is true — explicit opt-out for test harnesses that
|
|
118
|
+
* use real /tmp paths (directories that exist on disk) where files are never actually
|
|
119
|
+
* written to disk by agents. Use this when the environment-driven skip does not apply.
|
|
120
|
+
* 2. `!this.projectRootExistedAtStartup` — environment-driven skip: projectRoot did not
|
|
121
|
+
* exist on disk when this controller was constructed. Cached at construction time because
|
|
122
|
+
* the WrfcWorkmap mkdir's the directory tree on the first appendOwnerDecision call,
|
|
123
|
+
* making a late-bound existsSync check unreliable (would always return true after that).
|
|
124
|
+
* Preferred mechanism; harnesses should use nonexistent projectRoot paths when feasible.
|
|
125
|
+
*
|
|
126
|
+
* PRODUCTION INVARIANT: In any real GoodVibes session the projectRoot is the cloned
|
|
127
|
+
* repo root which always exists at startup. Both skip conditions are false in production;
|
|
128
|
+
* claim verification always runs for both engineer and fixer completions.
|
|
129
|
+
*/
|
|
130
|
+
private shouldSkipClaimVerification;
|
|
131
|
+
/** Returns the single currently-active child agent ID for a chain, if deterministic. */
|
|
132
|
+
private activeChildAgentId;
|
|
59
133
|
private onAgentComplete;
|
|
60
134
|
private onAgentFailed;
|
|
61
135
|
private onAgentCancelled;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrfc-controller.d.ts","sourceRoot":"","sources":["../../../src/platform/agents/wrfc-controller.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"wrfc-controller.d.ts","sourceRoot":"","sources":["../../../src/platform/agents/wrfc-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|