let-them-talk 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3 -1
- package/README.md +158 -592
- package/SECURITY.md +3 -3
- package/USAGE.md +151 -0
- package/agent-contracts.js +447 -0
- package/api-agents.js +760 -0
- package/autonomy/decision-v2.js +380 -0
- package/autonomy/watchdog-policy.js +572 -0
- package/cli.js +454 -298
- package/conversation-templates/autonomous-feature.json +83 -22
- package/conversation-templates/code-review.json +69 -21
- package/conversation-templates/debug-squad.json +69 -21
- package/conversation-templates/feature-build.json +69 -21
- package/conversation-templates/research-write.json +69 -21
- package/dashboard.html +3148 -174
- package/dashboard.js +823 -786
- package/data-dir.js +58 -0
- package/docs/architecture/branch-semantics.md +157 -0
- package/docs/architecture/canonical-event-schema.md +88 -0
- package/docs/architecture/markdown-workspace.md +183 -0
- package/docs/architecture/runtime-contract.md +459 -0
- package/docs/architecture/runtime-migration-hardening.md +64 -0
- package/events/hooks.js +154 -0
- package/events/log.js +457 -0
- package/events/replay.js +33 -0
- package/events/schema.js +432 -0
- package/managed-team-integration.js +261 -0
- package/office/agents.js +704 -597
- package/office/animation.js +1 -1
- package/office/assets/arcade-cabinet.js +141 -0
- package/office/assets/archway.js +77 -0
- package/office/assets/bar-counter.js +91 -0
- package/office/assets/bar-stool.js +71 -0
- package/office/assets/beanbag.js +64 -0
- package/office/assets/bench.js +99 -0
- package/office/assets/bollard.js +87 -0
- package/office/assets/cactus.js +100 -0
- package/office/assets/carpet-tile.js +46 -0
- package/office/assets/chair.js +123 -0
- package/office/assets/chandelier.js +107 -0
- package/office/assets/coffee-machine.js +95 -0
- package/office/assets/coffee-table.js +81 -0
- package/office/assets/column.js +95 -0
- package/office/assets/desk-lamp.js +102 -0
- package/office/assets/desk.js +76 -0
- package/office/assets/dining-table.js +105 -0
- package/office/assets/door.js +70 -0
- package/office/assets/dual-monitor.js +72 -0
- package/office/assets/fence.js +76 -0
- package/office/assets/filing-cabinet.js +111 -0
- package/office/assets/floor-lamp.js +69 -0
- package/office/assets/floor-tile.js +54 -0
- package/office/assets/flower-pot.js +76 -0
- package/office/assets/foosball.js +95 -0
- package/office/assets/fridge.js +99 -0
- package/office/assets/gaming-chair.js +154 -0
- package/office/assets/gaming-desk.js +105 -0
- package/office/assets/glass-door.js +72 -0
- package/office/assets/glass-wall.js +64 -0
- package/office/assets/half-wall.js +49 -0
- package/office/assets/hanging-plant.js +112 -0
- package/office/assets/index.js +151 -0
- package/office/assets/indoor-tree.js +90 -0
- package/office/assets/l-sofa.js +153 -0
- package/office/assets/marble-floor.js +64 -0
- package/office/assets/materials.js +40 -0
- package/office/assets/meeting-table.js +88 -0
- package/office/assets/microwave.js +94 -0
- package/office/assets/monitor.js +67 -0
- package/office/assets/neon-strip.js +73 -0
- package/office/assets/painting.js +84 -0
- package/office/assets/palm-tree.js +108 -0
- package/office/assets/pc-tower.js +91 -0
- package/office/assets/pendant-light.js +67 -0
- package/office/assets/ping-pong.js +114 -0
- package/office/assets/plant.js +72 -0
- package/office/assets/planter-box.js +95 -0
- package/office/assets/pool-table.js +94 -0
- package/office/assets/printer.js +113 -0
- package/office/assets/reception-desk.js +133 -0
- package/office/assets/rug.js +78 -0
- package/office/assets/sculpture.js +85 -0
- package/office/assets/server-rack.js +98 -0
- package/office/assets/sink.js +109 -0
- package/office/assets/sofa.js +106 -0
- package/office/assets/speaker.js +83 -0
- package/office/assets/spotlight.js +83 -0
- package/office/assets/street-lamp.js +97 -0
- package/office/assets/trash-can.js +83 -0
- package/office/assets/treadmill.js +126 -0
- package/office/assets/trophy.js +89 -0
- package/office/assets/tv-screen.js +79 -0
- package/office/assets/vase.js +84 -0
- package/office/assets/wall-clock.js +84 -0
- package/office/assets/wall.js +53 -0
- package/office/assets/water-cooler.js +146 -0
- package/office/assets/whiteboard.js +115 -0
- package/office/assets.js +3 -431
- package/office/builder.js +791 -355
- package/office/campus-env.js +1012 -1119
- package/office/environment.js +2 -0
- package/office/gallery.js +997 -0
- package/office/index.js +165 -61
- package/office/navigation.js +173 -152
- package/office/player.js +178 -68
- package/office/robot-character.js +272 -0
- package/office/spectator-camera.js +33 -10
- package/office/state.js +2 -0
- package/office/world-save.js +35 -4
- package/package.json +57 -3
- package/providers/comfyui.js +383 -0
- package/providers/dalle.js +79 -0
- package/providers/gemini.js +181 -0
- package/providers/ollama.js +184 -0
- package/providers/replicate.js +115 -0
- package/providers/zai.js +183 -0
- package/runtime-descriptor.js +270 -0
- package/scripts/check-agent-contract-advisory.js +132 -0
- package/scripts/check-api-agent-parity.js +277 -0
- package/scripts/check-autonomy-v2-decision.js +207 -0
- package/scripts/check-autonomy-v2-execution.js +588 -0
- package/scripts/check-autonomy-v2-watchdog.js +224 -0
- package/scripts/check-branch-fork-snapshot.js +337 -0
- package/scripts/check-branch-isolation.js +787 -0
- package/scripts/check-branch-semantics.js +139 -0
- package/scripts/check-dashboard-control-plane.js +1304 -0
- package/scripts/check-docs-onboarding.js +490 -0
- package/scripts/check-event-schema.js +276 -0
- package/scripts/check-evidence-completion.js +239 -0
- package/scripts/check-invariants.js +992 -0
- package/scripts/check-lifecycle-hooks.js +525 -0
- package/scripts/check-managed-team-integration.js +166 -0
- package/scripts/check-markdown-workspace-export.js +548 -0
- package/scripts/check-markdown-workspace-safety.js +347 -0
- package/scripts/check-markdown-workspace.js +136 -0
- package/scripts/check-message-replay.js +429 -0
- package/scripts/check-migration-hardening.js +300 -0
- package/scripts/check-performance-indexing.js +272 -0
- package/scripts/check-provider-capabilities.js +316 -0
- package/scripts/check-runtime-contract.js +109 -0
- package/scripts/check-session-aware-context.js +172 -0
- package/scripts/check-session-lifecycle.js +210 -0
- package/scripts/export-markdown-workspace.js +84 -0
- package/scripts/fixtures/message-replay/clean.jsonl +2 -0
- package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
- package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
- package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
- package/scripts/migrate-legacy-to-canonical.js +201 -0
- package/scripts/run-verification-suite.js +242 -0
- package/scripts/sync-packaged-docs.js +69 -0
- package/server.js +9546 -7216
- package/state/agents.js +161 -0
- package/state/canonical.js +3068 -0
- package/state/dashboard-queries.js +441 -0
- package/state/evidence.js +56 -0
- package/state/io.js +69 -0
- package/state/markdown-workspace.js +951 -0
- package/state/messages.js +669 -0
- package/state/sessions.js +683 -0
- package/state/tasks-workflows.js +92 -0
- package/templates/debate.json +2 -2
- package/templates/managed.json +4 -4
- package/templates/pair.json +2 -2
- package/templates/review.json +2 -2
- package/templates/team.json +3 -3
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const SERVER_FILE = path.resolve(__dirname, '..', 'server.js');
|
|
8
|
+
const PACKAGE_FILE = path.resolve(__dirname, '..', 'package.json');
|
|
9
|
+
const SUITE_FILE = path.resolve(__dirname, 'run-verification-suite.js');
|
|
10
|
+
|
|
11
|
+
const { createCanonicalState } = require(path.resolve(__dirname, '..', 'state', 'canonical.js'));
|
|
12
|
+
const {
|
|
13
|
+
evaluateAutonomyCandidate,
|
|
14
|
+
resolveAgentDecisionContext,
|
|
15
|
+
selectAutonomyDecisionCandidate,
|
|
16
|
+
} = require(path.resolve(__dirname, '..', 'autonomy', 'decision-v2.js'));
|
|
17
|
+
const {
|
|
18
|
+
classifyStalledWorkflowStepPolicy,
|
|
19
|
+
planStalledStepOwnershipChange,
|
|
20
|
+
} = require(path.resolve(__dirname, '..', 'autonomy', 'watchdog-policy.js'));
|
|
21
|
+
const { resolveAgentContract } = require(path.resolve(__dirname, '..', 'agent-contracts.js'));
|
|
22
|
+
|
|
23
|
+
function fail(lines, exitCode = 1) {
|
|
24
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
25
|
+
process.exit(exitCode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assert(condition, message, problems) {
|
|
29
|
+
if (!condition) problems.push(message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function extractBlock(source, startAnchor, endAnchor) {
|
|
33
|
+
const startIndex = source.indexOf(startAnchor);
|
|
34
|
+
if (startIndex === -1) return '';
|
|
35
|
+
const endIndex = endAnchor ? source.indexOf(endAnchor, startIndex + startAnchor.length) : source.length;
|
|
36
|
+
if (endIndex === -1) return source.slice(startIndex);
|
|
37
|
+
return source.slice(startIndex, endIndex);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJson(filePath, fallback) {
|
|
41
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
42
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readJsonl(filePath) {
|
|
46
|
+
if (!fs.existsSync(filePath)) return [];
|
|
47
|
+
const raw = fs.readFileSync(filePath, 'utf8').trim();
|
|
48
|
+
if (!raw) return [];
|
|
49
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeSessionSummary(sessionId, branchId, state = 'active', stale = false) {
|
|
53
|
+
return {
|
|
54
|
+
session_id: sessionId,
|
|
55
|
+
branch_id: branchId,
|
|
56
|
+
state,
|
|
57
|
+
stale,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildContext(params) {
|
|
62
|
+
return resolveAgentDecisionContext({
|
|
63
|
+
agentName: params.agentName,
|
|
64
|
+
branchId: params.branchId,
|
|
65
|
+
sessionSummary: params.sessionSummary || null,
|
|
66
|
+
contract: resolveAgentContract({
|
|
67
|
+
role: params.role || '',
|
|
68
|
+
archetype: params.archetype,
|
|
69
|
+
skills: params.skills,
|
|
70
|
+
contract_mode: params.contractMode,
|
|
71
|
+
}),
|
|
72
|
+
agentRecord: {
|
|
73
|
+
runtime_type: params.runtimeType || 'cli',
|
|
74
|
+
provider_id: params.providerId || null,
|
|
75
|
+
model_id: params.modelId || null,
|
|
76
|
+
capabilities: params.capabilities || null,
|
|
77
|
+
},
|
|
78
|
+
availableSkills: params.availableSkills || params.skills || [],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readWorkflow(canonicalState, branchName, workflowId) {
|
|
83
|
+
const workflows = canonicalState.listWorkflows({ branch: branchName });
|
|
84
|
+
return Array.isArray(workflows)
|
|
85
|
+
? workflows.find((entry) => entry && entry.id === workflowId) || null
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getBranchEventsFile(dataDir, branchName) {
|
|
90
|
+
return path.join(dataDir, 'runtime', 'branches', branchName, 'events.jsonl');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildStepCandidate(workflow, step, sessionSummary) {
|
|
94
|
+
return {
|
|
95
|
+
id: `step_${workflow.id}_${step.id}`,
|
|
96
|
+
order: 10,
|
|
97
|
+
target: {
|
|
98
|
+
work_type: 'workflow_step',
|
|
99
|
+
title: step.description,
|
|
100
|
+
description: workflow.name,
|
|
101
|
+
assigned: true,
|
|
102
|
+
assignment_priority: step.status === 'in_progress' ? 'active' : 'assigned',
|
|
103
|
+
required_capabilities: step.required_capabilities || null,
|
|
104
|
+
preferred_capabilities: step.preferred_capabilities || null,
|
|
105
|
+
session_summary: sessionSummary || null,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function main() {
|
|
111
|
+
const problems = [];
|
|
112
|
+
const serverSource = fs.readFileSync(SERVER_FILE, 'utf8');
|
|
113
|
+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_FILE, 'utf8'));
|
|
114
|
+
const suiteSource = fs.readFileSync(SUITE_FILE, 'utf8');
|
|
115
|
+
const watchdogBlock = extractBlock(serverSource, 'function watchdogCheck() {', '// --- Monitor Agent: system health check ---');
|
|
116
|
+
const activeStepBlock = extractBlock(serverSource, 'function findMyActiveWorkflowStep() {', 'function findReadySteps(workflow) {');
|
|
117
|
+
const upcomingStepBlock = extractBlock(serverSource, 'function findUpcomingStepsForMe() {', 'async function listenWithTimeout(timeoutMs) {');
|
|
118
|
+
|
|
119
|
+
assert(serverSource.includes('planStalledStepOwnershipChange'), 'server.js must load the bounded stalled-step ownership-change planner.', problems);
|
|
120
|
+
assert(watchdogBlock.includes('planStalledStepOwnershipChange({'), 'watchdogCheck() must derive any stalled-step ownership change from the shared policy helper.', problems);
|
|
121
|
+
assert(watchdogBlock.includes('canonicalState.reassignWorkflowStep({'), 'watchdogCheck() must apply policy-approved stalled-step ownership changes through canonical state.', problems);
|
|
122
|
+
assert(watchdogBlock.includes('clearPolicySignal: true'), 'Policy-approved stalled-step ownership changes must clear the previous watchdog policy signal.', problems);
|
|
123
|
+
assert(watchdogBlock.includes("clearSignalFields: ['watchdog_pinged_at', 'watchdog_escalated_at']"), 'Policy-approved stalled-step ownership changes must clear prior watchdog timestamps.', problems);
|
|
124
|
+
assert(watchdogBlock.includes('restartStartedAt: reassignedAt'), 'Policy-approved stalled-step ownership changes must restart the in-progress timer for the new owner.', problems);
|
|
125
|
+
assert(activeStepBlock.includes('workflowMatchesActiveBranch(wf)'), 'findMyActiveWorkflowStep() must stay scoped to the current branch-local workflow view.', problems);
|
|
126
|
+
assert(upcomingStepBlock.includes('workflowMatchesActiveBranch(wf)'), 'findUpcomingStepsForMe() must stay scoped to the current branch-local workflow view.', problems);
|
|
127
|
+
|
|
128
|
+
assert(packageJson.scripts['verify:invariants:autonomy-v2-execution'] === 'node scripts/check-autonomy-v2-execution.js', 'package.json must expose the autonomy-v2 execution validator in verify:invariants.', problems);
|
|
129
|
+
assert(packageJson.scripts['verify:invariants'].includes('verify:invariants:autonomy-v2-execution'), 'package.json verify:invariants must include the autonomy-v2 execution validator.', problems);
|
|
130
|
+
assert(suiteSource.includes("label: 'autonomy-v2-execution'"), 'run-verification-suite.js smoke coverage must include autonomy-v2 execution validation.', problems);
|
|
131
|
+
assert(suiteSource.includes("args: ['scripts/check-autonomy-v2-execution.js']"), 'run-verification-suite.js smoke coverage must execute the autonomy-v2 execution validator.', problems);
|
|
132
|
+
|
|
133
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'letthemtalk-task12c-'));
|
|
134
|
+
const dataDir = path.join(tempRoot, '.agent-bridge');
|
|
135
|
+
const canonicalState = createCanonicalState({ dataDir, processPid: 4242 });
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const branchHealthy = 'feature_autonomy_v2';
|
|
139
|
+
const healthyWorkflow = {
|
|
140
|
+
id: 'wf_autonomy_v2_branch',
|
|
141
|
+
name: 'Branch-local autonomy-v2 workflow',
|
|
142
|
+
status: 'active',
|
|
143
|
+
autonomous: true,
|
|
144
|
+
parallel: false,
|
|
145
|
+
created_by: 'lead',
|
|
146
|
+
created_at: '2026-04-16T12:00:00.000Z',
|
|
147
|
+
updated_at: '2026-04-16T12:00:00.000Z',
|
|
148
|
+
steps: [
|
|
149
|
+
{
|
|
150
|
+
id: 1,
|
|
151
|
+
description: 'Implement branch-local autonomy slice',
|
|
152
|
+
assignee: 'alpha',
|
|
153
|
+
depends_on: [],
|
|
154
|
+
status: 'in_progress',
|
|
155
|
+
started_at: '2026-04-16T12:01:00.000Z',
|
|
156
|
+
completed_at: null,
|
|
157
|
+
notes: '',
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 2,
|
|
161
|
+
description: 'Review the branch-local autonomy slice',
|
|
162
|
+
assignee: 'beta',
|
|
163
|
+
depends_on: [1],
|
|
164
|
+
status: 'pending',
|
|
165
|
+
started_at: null,
|
|
166
|
+
completed_at: null,
|
|
167
|
+
notes: '',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
canonicalState.createWorkflow({
|
|
172
|
+
workflow: healthyWorkflow,
|
|
173
|
+
actor: 'lead',
|
|
174
|
+
branch: branchHealthy,
|
|
175
|
+
sessionId: 'sess_lead_feature',
|
|
176
|
+
correlationId: healthyWorkflow.id,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const alphaContext = buildContext({
|
|
180
|
+
agentName: 'alpha',
|
|
181
|
+
branchId: branchHealthy,
|
|
182
|
+
sessionSummary: makeSessionSummary('sess_alpha_feature', branchHealthy),
|
|
183
|
+
archetype: 'implementer',
|
|
184
|
+
skills: ['backend'],
|
|
185
|
+
contractMode: 'strict',
|
|
186
|
+
runtimeType: 'cli',
|
|
187
|
+
});
|
|
188
|
+
const alphaSelection = selectAutonomyDecisionCandidate([
|
|
189
|
+
buildStepCandidate(healthyWorkflow, healthyWorkflow.steps[0], makeSessionSummary('sess_alpha_feature', branchHealthy)),
|
|
190
|
+
], alphaContext);
|
|
191
|
+
assert(alphaSelection && alphaSelection.id === 'step_wf_autonomy_v2_branch_1', 'Healthy autonomy-v2 branch execution must select the active step for the branch-local owner.', problems);
|
|
192
|
+
|
|
193
|
+
const healthyAdvance = canonicalState.advanceWorkflow({
|
|
194
|
+
workflowId: healthyWorkflow.id,
|
|
195
|
+
actor: 'alpha',
|
|
196
|
+
branch: branchHealthy,
|
|
197
|
+
sessionId: 'sess_alpha_feature',
|
|
198
|
+
commandId: 'cmd_autonomy_v2_branch',
|
|
199
|
+
correlationId: healthyWorkflow.id,
|
|
200
|
+
at: '2026-04-16T12:05:00.000Z',
|
|
201
|
+
sourceTool: 'check-autonomy-v2-execution',
|
|
202
|
+
expectedAssignee: 'alpha',
|
|
203
|
+
evidence: {
|
|
204
|
+
summary: 'Implemented the branch-local autonomy slice.',
|
|
205
|
+
verification: 'Deterministic fixture advance executed cleanly.',
|
|
206
|
+
files_changed: ['agent-bridge/server.js'],
|
|
207
|
+
confidence: 96,
|
|
208
|
+
learnings: 'Branch-local autonomy steps advance cleanly through canonical workflow state.',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
assert(healthyAdvance && healthyAdvance.success, 'Healthy autonomy-v2 branch execution must advance through canonical workflow state.', problems);
|
|
212
|
+
|
|
213
|
+
const healthyWorkflowAfter = readWorkflow(canonicalState, branchHealthy, healthyWorkflow.id);
|
|
214
|
+
const healthyFeatureEvents = readJsonl(getBranchEventsFile(dataDir, branchHealthy));
|
|
215
|
+
const mainEvents = readJsonl(getBranchEventsFile(dataDir, 'main'));
|
|
216
|
+
assert(healthyWorkflowAfter && healthyWorkflowAfter.branch_id === branchHealthy, 'Autonomy-v2 workflows created on a branch must persist their branch_id explicitly.', problems);
|
|
217
|
+
assert(healthyWorkflowAfter && healthyWorkflowAfter.steps[0].status === 'done', 'Healthy autonomy-v2 execution must mark the completed branch-local step done.', problems);
|
|
218
|
+
assert(healthyWorkflowAfter && healthyWorkflowAfter.steps[1].status === 'in_progress', 'Healthy autonomy-v2 execution must start the next branch-local step deterministically.', problems);
|
|
219
|
+
assert(healthyWorkflowAfter && healthyWorkflowAfter.steps[1].assignee === 'beta', 'Healthy autonomy-v2 execution must preserve the explicit next-step assignee.', problems);
|
|
220
|
+
assert(healthyFeatureEvents.some((event) => event.type === 'workflow.step_completed' && event.payload && event.payload.step_id === 1), 'Healthy autonomy-v2 execution must emit branch-local workflow.step_completed events.', problems);
|
|
221
|
+
assert(healthyFeatureEvents.some((event) => event.type === 'workflow.step_started' && event.payload && event.payload.step_id === 2), 'Healthy autonomy-v2 execution must emit branch-local workflow.step_started events for the next step.', problems);
|
|
222
|
+
assert(mainEvents.length === 0, 'Healthy autonomy-v2 branch execution must not leak workflow events into main.', problems);
|
|
223
|
+
|
|
224
|
+
const branchRecovery = 'feature_recovery_v2';
|
|
225
|
+
const recoveryWorkflow = {
|
|
226
|
+
id: 'wf_autonomy_v2_recovery',
|
|
227
|
+
name: 'Unavailable owner recovery workflow',
|
|
228
|
+
status: 'active',
|
|
229
|
+
autonomous: true,
|
|
230
|
+
parallel: false,
|
|
231
|
+
created_by: 'lead',
|
|
232
|
+
created_at: '2026-04-16T11:00:00.000Z',
|
|
233
|
+
updated_at: '2026-04-16T11:00:00.000Z',
|
|
234
|
+
steps: [
|
|
235
|
+
{
|
|
236
|
+
id: 1,
|
|
237
|
+
description: 'Render release poster',
|
|
238
|
+
assignee: 'stalled_owner',
|
|
239
|
+
depends_on: [],
|
|
240
|
+
status: 'in_progress',
|
|
241
|
+
started_at: '2026-04-16T11:05:00.000Z',
|
|
242
|
+
completed_at: null,
|
|
243
|
+
notes: '',
|
|
244
|
+
required_capabilities: ['image_generation'],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
};
|
|
248
|
+
canonicalState.createWorkflow({
|
|
249
|
+
workflow: recoveryWorkflow,
|
|
250
|
+
actor: 'lead',
|
|
251
|
+
branch: branchRecovery,
|
|
252
|
+
sessionId: 'sess_lead_recovery',
|
|
253
|
+
correlationId: recoveryWorkflow.id,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const ownerContext = buildContext({
|
|
257
|
+
agentName: 'stalled_owner',
|
|
258
|
+
branchId: branchRecovery,
|
|
259
|
+
sessionSummary: makeSessionSummary('sess_stalled_owner', branchRecovery, 'interrupted', true),
|
|
260
|
+
archetype: 'implementer',
|
|
261
|
+
skills: ['media'],
|
|
262
|
+
contractMode: 'strict',
|
|
263
|
+
runtimeType: 'cli',
|
|
264
|
+
});
|
|
265
|
+
const stalledPolicy = classifyStalledWorkflowStepPolicy({
|
|
266
|
+
target: {
|
|
267
|
+
work_type: 'workflow_step',
|
|
268
|
+
title: 'Render release poster',
|
|
269
|
+
description: recoveryWorkflow.name,
|
|
270
|
+
assigned: true,
|
|
271
|
+
required_capabilities: ['image_generation'],
|
|
272
|
+
},
|
|
273
|
+
context: ownerContext,
|
|
274
|
+
ownerAlive: false,
|
|
275
|
+
idleMs: 0,
|
|
276
|
+
stepAgeMs: 1900000,
|
|
277
|
+
resumeContext: {
|
|
278
|
+
dependency_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_dep_recovery' } } }],
|
|
279
|
+
recent_evidence: [{ evidence: { evidence_ref: { evidence_id: 'ev_recent_recovery' } } }],
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
assert(stalledPolicy.signal === 'escalate', 'Unavailable stalled-step policy must escalate explicitly before ownership can move.', problems);
|
|
283
|
+
assert(stalledPolicy.classification === 'step_owner_unavailable', 'Unavailable stalled-step policy must classify owner-unavailable recovery explicitly.', problems);
|
|
284
|
+
|
|
285
|
+
canonicalState.setWorkflowStepPolicySignal({
|
|
286
|
+
workflowId: recoveryWorkflow.id,
|
|
287
|
+
stepId: 1,
|
|
288
|
+
expectedAssignee: 'stalled_owner',
|
|
289
|
+
signalAtField: 'watchdog_escalated_at',
|
|
290
|
+
policySignal: {
|
|
291
|
+
source: 'watchdog',
|
|
292
|
+
classification: stalledPolicy.classification,
|
|
293
|
+
summary: stalledPolicy.summary,
|
|
294
|
+
},
|
|
295
|
+
at: '2026-04-16T12:10:00.000Z',
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const replacementContexts = {
|
|
299
|
+
replacement_good: buildContext({
|
|
300
|
+
agentName: 'replacement_good',
|
|
301
|
+
branchId: branchRecovery,
|
|
302
|
+
sessionSummary: makeSessionSummary('sess_replacement_good', branchRecovery),
|
|
303
|
+
archetype: 'implementer',
|
|
304
|
+
skills: ['media'],
|
|
305
|
+
contractMode: 'strict',
|
|
306
|
+
runtimeType: 'api',
|
|
307
|
+
providerId: 'gemini',
|
|
308
|
+
modelId: 'gemini-image',
|
|
309
|
+
capabilities: ['image_generation'],
|
|
310
|
+
}),
|
|
311
|
+
replacement_bad_contract: buildContext({
|
|
312
|
+
agentName: 'replacement_bad_contract',
|
|
313
|
+
branchId: branchRecovery,
|
|
314
|
+
sessionSummary: makeSessionSummary('sess_bad_contract', branchRecovery),
|
|
315
|
+
archetype: 'advisor',
|
|
316
|
+
skills: ['strategy'],
|
|
317
|
+
contractMode: 'strict',
|
|
318
|
+
runtimeType: 'api',
|
|
319
|
+
providerId: 'gemini',
|
|
320
|
+
modelId: 'gemini-image',
|
|
321
|
+
capabilities: ['image_generation'],
|
|
322
|
+
}),
|
|
323
|
+
replacement_bad_capability: buildContext({
|
|
324
|
+
agentName: 'replacement_bad_capability',
|
|
325
|
+
branchId: branchRecovery,
|
|
326
|
+
sessionSummary: makeSessionSummary('sess_bad_capability', branchRecovery),
|
|
327
|
+
archetype: 'implementer',
|
|
328
|
+
skills: ['media'],
|
|
329
|
+
contractMode: 'strict',
|
|
330
|
+
runtimeType: 'cli',
|
|
331
|
+
}),
|
|
332
|
+
off_branch_image: buildContext({
|
|
333
|
+
agentName: 'off_branch_image',
|
|
334
|
+
branchId: 'main',
|
|
335
|
+
sessionSummary: makeSessionSummary('sess_off_branch', 'main'),
|
|
336
|
+
archetype: 'implementer',
|
|
337
|
+
skills: ['media'],
|
|
338
|
+
contractMode: 'strict',
|
|
339
|
+
runtimeType: 'api',
|
|
340
|
+
providerId: 'gemini',
|
|
341
|
+
modelId: 'gemini-image',
|
|
342
|
+
capabilities: ['image_generation'],
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const ownershipChange = planStalledStepOwnershipChange({
|
|
347
|
+
branchId: branchRecovery,
|
|
348
|
+
currentAssignee: 'stalled_owner',
|
|
349
|
+
watchdogAgentName: 'watchdog',
|
|
350
|
+
policy: stalledPolicy,
|
|
351
|
+
target: {
|
|
352
|
+
work_type: 'workflow_step',
|
|
353
|
+
title: 'Render release poster',
|
|
354
|
+
description: recoveryWorkflow.name,
|
|
355
|
+
assigned: true,
|
|
356
|
+
required_capabilities: ['image_generation'],
|
|
357
|
+
},
|
|
358
|
+
agents: {
|
|
359
|
+
stalled_owner: { branch: branchRecovery },
|
|
360
|
+
replacement_good: { branch: branchRecovery },
|
|
361
|
+
replacement_bad_contract: { branch: branchRecovery },
|
|
362
|
+
replacement_bad_capability: { branch: branchRecovery },
|
|
363
|
+
off_branch_image: { branch: 'main' },
|
|
364
|
+
watchdog: { branch: branchRecovery },
|
|
365
|
+
},
|
|
366
|
+
resolveContext: (agentName) => replacementContexts[agentName] || ownerContext,
|
|
367
|
+
isAgentAlive: (agentName) => agentName !== 'stalled_owner',
|
|
368
|
+
});
|
|
369
|
+
assert(ownershipChange.allowed === true, 'Owner-unavailable stalled-step recovery must allow a bounded ownership transfer when one same-branch replacement passes policy checks.', problems);
|
|
370
|
+
assert(ownershipChange.new_assignee === 'replacement_good', 'Bounded stalled-step ownership transfer must choose the best same-branch admissible replacement deterministically.', problems);
|
|
371
|
+
assert(ownershipChange.rejected_candidates.some((entry) => entry.agent_name === 'off_branch_image' && entry.reason === 'branch_mismatch'), 'Bounded stalled-step ownership transfer must reject off-branch replacements.', problems);
|
|
372
|
+
assert(ownershipChange.rejected_candidates.some((entry) => entry.agent_name === 'replacement_bad_contract' && entry.reason === 'strict_contract_mismatch'), 'Bounded stalled-step ownership transfer must fail closed on strict contract mismatches.', problems);
|
|
373
|
+
assert(ownershipChange.rejected_candidates.some((entry) => entry.agent_name === 'replacement_bad_capability' && entry.reason === 'required_capability_unavailable'), 'Bounded stalled-step ownership transfer must fail closed on capability mismatches.', problems);
|
|
374
|
+
|
|
375
|
+
const reassigned = canonicalState.reassignWorkflowStep({
|
|
376
|
+
workflowId: recoveryWorkflow.id,
|
|
377
|
+
stepId: 1,
|
|
378
|
+
newAssignee: ownershipChange.new_assignee,
|
|
379
|
+
actor: 'watchdog',
|
|
380
|
+
branch: branchRecovery,
|
|
381
|
+
expectedAssignee: 'stalled_owner',
|
|
382
|
+
clearPolicySignal: true,
|
|
383
|
+
clearSignalFields: ['watchdog_pinged_at', 'watchdog_escalated_at'],
|
|
384
|
+
restartStartedAt: '2026-04-16T12:11:00.000Z',
|
|
385
|
+
at: '2026-04-16T12:11:00.000Z',
|
|
386
|
+
});
|
|
387
|
+
assert(reassigned && reassigned.success, 'Policy-approved stalled-step ownership transfer must apply through canonical workflow reassignment.', problems);
|
|
388
|
+
|
|
389
|
+
const recoveryWorkflowAfter = readWorkflow(canonicalState, branchRecovery, recoveryWorkflow.id);
|
|
390
|
+
const recoveryStepAfter = recoveryWorkflowAfter && recoveryWorkflowAfter.steps ? recoveryWorkflowAfter.steps[0] : null;
|
|
391
|
+
const recoveryEvents = readJsonl(getBranchEventsFile(dataDir, branchRecovery));
|
|
392
|
+
assert(recoveryStepAfter && recoveryStepAfter.assignee === 'replacement_good', 'Policy-approved stalled-step ownership transfer must update the workflow assignee.', problems);
|
|
393
|
+
assert(recoveryStepAfter && recoveryStepAfter.started_at === '2026-04-16T12:11:00.000Z', 'Policy-approved stalled-step ownership transfer must restart the in-progress timer for the new owner.', problems);
|
|
394
|
+
assert(recoveryStepAfter && !Object.prototype.hasOwnProperty.call(recoveryStepAfter, 'policy_signal'), 'Policy-approved stalled-step ownership transfer must clear the prior watchdog policy signal.', problems);
|
|
395
|
+
assert(recoveryStepAfter && !Object.prototype.hasOwnProperty.call(recoveryStepAfter, 'watchdog_escalated_at'), 'Policy-approved stalled-step ownership transfer must clear the prior watchdog escalation timestamp.', problems);
|
|
396
|
+
assert(recoveryEvents.some((event) => event.type === 'workflow.step_reassigned' && event.payload && event.payload.new_assignee === 'replacement_good'), 'Policy-approved stalled-step ownership transfer must emit workflow.step_reassigned canonically.', problems);
|
|
397
|
+
|
|
398
|
+
const branchMixedProviders = 'feature_mixed_provider_v2';
|
|
399
|
+
const mixedProviderWorkflow = {
|
|
400
|
+
id: 'wf_autonomy_v2_mixed_provider',
|
|
401
|
+
name: 'Mixed-provider autonomy-v2 workflow',
|
|
402
|
+
status: 'active',
|
|
403
|
+
autonomous: true,
|
|
404
|
+
parallel: false,
|
|
405
|
+
created_by: 'lead',
|
|
406
|
+
created_at: '2026-04-16T13:00:00.000Z',
|
|
407
|
+
updated_at: '2026-04-16T13:00:00.000Z',
|
|
408
|
+
steps: [
|
|
409
|
+
{
|
|
410
|
+
id: 1,
|
|
411
|
+
description: 'Generate launch poster',
|
|
412
|
+
assignee: 'image_bot',
|
|
413
|
+
depends_on: [],
|
|
414
|
+
status: 'in_progress',
|
|
415
|
+
started_at: '2026-04-16T13:01:00.000Z',
|
|
416
|
+
completed_at: null,
|
|
417
|
+
notes: '',
|
|
418
|
+
required_capabilities: ['image_generation'],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 2,
|
|
422
|
+
description: 'Write launch summary',
|
|
423
|
+
assignee: 'writer_bot',
|
|
424
|
+
depends_on: [1],
|
|
425
|
+
status: 'pending',
|
|
426
|
+
started_at: null,
|
|
427
|
+
completed_at: null,
|
|
428
|
+
notes: '',
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
};
|
|
432
|
+
canonicalState.createWorkflow({
|
|
433
|
+
workflow: mixedProviderWorkflow,
|
|
434
|
+
actor: 'lead',
|
|
435
|
+
branch: branchMixedProviders,
|
|
436
|
+
sessionId: 'sess_mixed_provider_lead',
|
|
437
|
+
correlationId: mixedProviderWorkflow.id,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const imageBotContext = buildContext({
|
|
441
|
+
agentName: 'image_bot',
|
|
442
|
+
branchId: branchMixedProviders,
|
|
443
|
+
sessionSummary: makeSessionSummary('sess_image_bot', branchMixedProviders),
|
|
444
|
+
archetype: 'implementer',
|
|
445
|
+
skills: ['media'],
|
|
446
|
+
contractMode: 'strict',
|
|
447
|
+
runtimeType: 'api',
|
|
448
|
+
providerId: 'gemini',
|
|
449
|
+
modelId: 'gemini-image',
|
|
450
|
+
capabilities: ['image_generation'],
|
|
451
|
+
});
|
|
452
|
+
const writerBotContext = buildContext({
|
|
453
|
+
agentName: 'writer_bot',
|
|
454
|
+
branchId: branchMixedProviders,
|
|
455
|
+
sessionSummary: makeSessionSummary('sess_writer_bot', branchMixedProviders),
|
|
456
|
+
archetype: 'implementer',
|
|
457
|
+
skills: ['writing'],
|
|
458
|
+
contractMode: 'strict',
|
|
459
|
+
runtimeType: 'cli',
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const mixedProviderSelection = selectAutonomyDecisionCandidate([
|
|
463
|
+
buildStepCandidate(mixedProviderWorkflow, mixedProviderWorkflow.steps[0], makeSessionSummary('sess_image_bot', branchMixedProviders)),
|
|
464
|
+
], imageBotContext);
|
|
465
|
+
assert(mixedProviderSelection && mixedProviderSelection.id === 'step_wf_autonomy_v2_mixed_provider_1', 'Mixed-provider autonomy-v2 execution must select the explicit image-capable branch-local owner for the media step.', problems);
|
|
466
|
+
|
|
467
|
+
const mixedProviderAdvance = canonicalState.advanceWorkflow({
|
|
468
|
+
workflowId: mixedProviderWorkflow.id,
|
|
469
|
+
actor: 'image_bot',
|
|
470
|
+
branch: branchMixedProviders,
|
|
471
|
+
sessionId: 'sess_image_bot',
|
|
472
|
+
commandId: 'cmd_mixed_provider_step_1',
|
|
473
|
+
correlationId: mixedProviderWorkflow.id,
|
|
474
|
+
at: '2026-04-16T13:06:00.000Z',
|
|
475
|
+
sourceTool: 'check-autonomy-v2-execution',
|
|
476
|
+
expectedAssignee: 'image_bot',
|
|
477
|
+
evidence: {
|
|
478
|
+
summary: 'Generated the launch poster.',
|
|
479
|
+
verification: 'Mixed-provider branch-local fixture advanced successfully.',
|
|
480
|
+
files_changed: ['agent-bridge/autonomy/decision-v2.js'],
|
|
481
|
+
confidence: 94,
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
assert(mixedProviderAdvance && mixedProviderAdvance.success, 'Mixed-provider autonomy-v2 execution must advance the first provider-specific step.', problems);
|
|
485
|
+
|
|
486
|
+
const mixedProviderWorkflowAfter = readWorkflow(canonicalState, branchMixedProviders, mixedProviderWorkflow.id);
|
|
487
|
+
const writerStep = mixedProviderWorkflowAfter && mixedProviderWorkflowAfter.steps
|
|
488
|
+
? mixedProviderWorkflowAfter.steps.find((entry) => entry.id === 2) || null
|
|
489
|
+
: null;
|
|
490
|
+
const writerSelection = writerStep
|
|
491
|
+
? selectAutonomyDecisionCandidate([
|
|
492
|
+
buildStepCandidate(mixedProviderWorkflowAfter, writerStep, makeSessionSummary('sess_writer_bot', branchMixedProviders)),
|
|
493
|
+
], writerBotContext)
|
|
494
|
+
: null;
|
|
495
|
+
const mixedProviderEvents = readJsonl(getBranchEventsFile(dataDir, branchMixedProviders));
|
|
496
|
+
assert(writerStep && writerStep.status === 'in_progress', 'Mixed-provider autonomy-v2 execution must start the branch-local follow-up step after the provider-specific step completes.', problems);
|
|
497
|
+
assert(writerSelection && writerSelection.id === 'step_wf_autonomy_v2_mixed_provider_2', 'Mixed-provider autonomy-v2 execution must hand branch-local follow-up work to the next explicitly assigned provider/runtime owner.', problems);
|
|
498
|
+
assert(mixedProviderEvents.some((event) => event.type === 'workflow.step_completed' && event.payload && event.payload.workflow_id === mixedProviderWorkflow.id), 'Mixed-provider autonomy-v2 execution must record branch-local completion events for the provider-specific workflow.', problems);
|
|
499
|
+
|
|
500
|
+
const strictAdvisorContext = buildContext({
|
|
501
|
+
agentName: 'strategy_bot',
|
|
502
|
+
branchId: 'feature_contract_v2',
|
|
503
|
+
sessionSummary: makeSessionSummary('sess_strategy_bot', 'feature_contract_v2'),
|
|
504
|
+
archetype: 'advisor',
|
|
505
|
+
skills: ['analysis', 'strategy'],
|
|
506
|
+
contractMode: 'strict',
|
|
507
|
+
runtimeType: 'cli',
|
|
508
|
+
});
|
|
509
|
+
const strictMismatchEvaluation = evaluateAutonomyCandidate({
|
|
510
|
+
target: {
|
|
511
|
+
work_type: 'task',
|
|
512
|
+
title: 'Implement dashboard drag and drop',
|
|
513
|
+
description: 'Write code for the dashboard interaction.',
|
|
514
|
+
},
|
|
515
|
+
}, strictAdvisorContext);
|
|
516
|
+
const strictNeutralEvaluation = evaluateAutonomyCandidate({
|
|
517
|
+
target: {
|
|
518
|
+
work_type: 'review',
|
|
519
|
+
title: 'Review the launch checklist',
|
|
520
|
+
description: 'Check the release notes for typos.',
|
|
521
|
+
},
|
|
522
|
+
}, strictAdvisorContext);
|
|
523
|
+
const strictAssignedEvaluation = evaluateAutonomyCandidate({
|
|
524
|
+
target: {
|
|
525
|
+
work_type: 'task',
|
|
526
|
+
title: 'Implement dashboard drag and drop',
|
|
527
|
+
description: 'Write code for the dashboard interaction.',
|
|
528
|
+
assigned: true,
|
|
529
|
+
assignment_priority: 'assigned',
|
|
530
|
+
},
|
|
531
|
+
}, strictAdvisorContext);
|
|
532
|
+
const strictSelection = selectAutonomyDecisionCandidate([
|
|
533
|
+
{
|
|
534
|
+
id: 'blocked_implementation_task',
|
|
535
|
+
order: 10,
|
|
536
|
+
target: {
|
|
537
|
+
work_type: 'task',
|
|
538
|
+
title: 'Implement dashboard drag and drop',
|
|
539
|
+
description: 'Write code for the dashboard interaction.',
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
id: 'aligned_help_request',
|
|
544
|
+
order: 20,
|
|
545
|
+
target: {
|
|
546
|
+
work_type: 'help_teammate',
|
|
547
|
+
title: 'Help debug a launch blocker',
|
|
548
|
+
description: 'Investigate why the verification suite is blocked.',
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
], strictAdvisorContext);
|
|
552
|
+
const strictNoSelection = selectAutonomyDecisionCandidate([
|
|
553
|
+
{
|
|
554
|
+
id: 'only_blocked_implementation',
|
|
555
|
+
order: 10,
|
|
556
|
+
target: {
|
|
557
|
+
work_type: 'task',
|
|
558
|
+
title: 'Implement dashboard drag and drop',
|
|
559
|
+
description: 'Write code for the dashboard interaction.',
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
], strictAdvisorContext);
|
|
563
|
+
|
|
564
|
+
assert(strictMismatchEvaluation.admissible === false, 'Strict contract mode must fail closed on mismatched unowned work in autonomy-v2 selection.', problems);
|
|
565
|
+
assert(strictMismatchEvaluation.contract_admissibility && strictMismatchEvaluation.contract_admissibility.reason === 'strict_contract_mismatch', 'Strict contract mode must classify mismatched unowned work explicitly.', problems);
|
|
566
|
+
assert(strictNeutralEvaluation.admissible === false, 'Strict contract mode must also fail closed when unowned work has no positive contract-fit signal.', problems);
|
|
567
|
+
assert(strictNeutralEvaluation.contract_admissibility && strictNeutralEvaluation.contract_admissibility.reason === 'strict_contract_no_positive_fit', 'Strict contract mode must classify neutral unowned work explicitly when it is blocked.', problems);
|
|
568
|
+
assert(strictAssignedEvaluation.admissible === true, 'Assigned-work precedence must remain intact even when strict contract mode marks the work as a weaker fit.', problems);
|
|
569
|
+
assert(strictSelection && strictSelection.id === 'aligned_help_request', 'Stronger autonomy-v2 contract-aware selection must prefer aligned work over fail-closed mismatched work.', problems);
|
|
570
|
+
assert(strictNoSelection === null, 'Stronger autonomy-v2 contract-aware selection must return no candidate when every available item is blocked by strict contract mismatch.', problems);
|
|
571
|
+
} finally {
|
|
572
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (problems.length > 0) {
|
|
576
|
+
fail(['Autonomy-v2 execution validation failed.', ...problems.map((problem) => `- ${problem}`)]);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
console.log([
|
|
580
|
+
'Autonomy-v2 execution validation passed.',
|
|
581
|
+
'- Healthy branch-local autonomous workflows advance deterministically and emit branch-scoped evidence/workflow events.',
|
|
582
|
+
'- Owner-unavailable stalled steps can change ownership only through an explicit policy-approved, same-branch, fail-closed recovery path.',
|
|
583
|
+
'- Mixed-provider workflows stay branch-local while advancing between explicit provider/runtime owners.',
|
|
584
|
+
'- Strict contract-aware autonomy-v2 selection now fail-closes mismatched unowned work while preserving assigned-work precedence.',
|
|
585
|
+
].join('\n'));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
main();
|