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.
Files changed (166) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/README.md +158 -592
  3. package/SECURITY.md +3 -3
  4. package/USAGE.md +151 -0
  5. package/agent-contracts.js +447 -0
  6. package/api-agents.js +760 -0
  7. package/autonomy/decision-v2.js +380 -0
  8. package/autonomy/watchdog-policy.js +572 -0
  9. package/cli.js +454 -298
  10. package/conversation-templates/autonomous-feature.json +83 -22
  11. package/conversation-templates/code-review.json +69 -21
  12. package/conversation-templates/debug-squad.json +69 -21
  13. package/conversation-templates/feature-build.json +69 -21
  14. package/conversation-templates/research-write.json +69 -21
  15. package/dashboard.html +3148 -174
  16. package/dashboard.js +823 -786
  17. package/data-dir.js +58 -0
  18. package/docs/architecture/branch-semantics.md +157 -0
  19. package/docs/architecture/canonical-event-schema.md +88 -0
  20. package/docs/architecture/markdown-workspace.md +183 -0
  21. package/docs/architecture/runtime-contract.md +459 -0
  22. package/docs/architecture/runtime-migration-hardening.md +64 -0
  23. package/events/hooks.js +154 -0
  24. package/events/log.js +457 -0
  25. package/events/replay.js +33 -0
  26. package/events/schema.js +432 -0
  27. package/managed-team-integration.js +261 -0
  28. package/office/agents.js +704 -597
  29. package/office/animation.js +1 -1
  30. package/office/assets/arcade-cabinet.js +141 -0
  31. package/office/assets/archway.js +77 -0
  32. package/office/assets/bar-counter.js +91 -0
  33. package/office/assets/bar-stool.js +71 -0
  34. package/office/assets/beanbag.js +64 -0
  35. package/office/assets/bench.js +99 -0
  36. package/office/assets/bollard.js +87 -0
  37. package/office/assets/cactus.js +100 -0
  38. package/office/assets/carpet-tile.js +46 -0
  39. package/office/assets/chair.js +123 -0
  40. package/office/assets/chandelier.js +107 -0
  41. package/office/assets/coffee-machine.js +95 -0
  42. package/office/assets/coffee-table.js +81 -0
  43. package/office/assets/column.js +95 -0
  44. package/office/assets/desk-lamp.js +102 -0
  45. package/office/assets/desk.js +76 -0
  46. package/office/assets/dining-table.js +105 -0
  47. package/office/assets/door.js +70 -0
  48. package/office/assets/dual-monitor.js +72 -0
  49. package/office/assets/fence.js +76 -0
  50. package/office/assets/filing-cabinet.js +111 -0
  51. package/office/assets/floor-lamp.js +69 -0
  52. package/office/assets/floor-tile.js +54 -0
  53. package/office/assets/flower-pot.js +76 -0
  54. package/office/assets/foosball.js +95 -0
  55. package/office/assets/fridge.js +99 -0
  56. package/office/assets/gaming-chair.js +154 -0
  57. package/office/assets/gaming-desk.js +105 -0
  58. package/office/assets/glass-door.js +72 -0
  59. package/office/assets/glass-wall.js +64 -0
  60. package/office/assets/half-wall.js +49 -0
  61. package/office/assets/hanging-plant.js +112 -0
  62. package/office/assets/index.js +151 -0
  63. package/office/assets/indoor-tree.js +90 -0
  64. package/office/assets/l-sofa.js +153 -0
  65. package/office/assets/marble-floor.js +64 -0
  66. package/office/assets/materials.js +40 -0
  67. package/office/assets/meeting-table.js +88 -0
  68. package/office/assets/microwave.js +94 -0
  69. package/office/assets/monitor.js +67 -0
  70. package/office/assets/neon-strip.js +73 -0
  71. package/office/assets/painting.js +84 -0
  72. package/office/assets/palm-tree.js +108 -0
  73. package/office/assets/pc-tower.js +91 -0
  74. package/office/assets/pendant-light.js +67 -0
  75. package/office/assets/ping-pong.js +114 -0
  76. package/office/assets/plant.js +72 -0
  77. package/office/assets/planter-box.js +95 -0
  78. package/office/assets/pool-table.js +94 -0
  79. package/office/assets/printer.js +113 -0
  80. package/office/assets/reception-desk.js +133 -0
  81. package/office/assets/rug.js +78 -0
  82. package/office/assets/sculpture.js +85 -0
  83. package/office/assets/server-rack.js +98 -0
  84. package/office/assets/sink.js +109 -0
  85. package/office/assets/sofa.js +106 -0
  86. package/office/assets/speaker.js +83 -0
  87. package/office/assets/spotlight.js +83 -0
  88. package/office/assets/street-lamp.js +97 -0
  89. package/office/assets/trash-can.js +83 -0
  90. package/office/assets/treadmill.js +126 -0
  91. package/office/assets/trophy.js +89 -0
  92. package/office/assets/tv-screen.js +79 -0
  93. package/office/assets/vase.js +84 -0
  94. package/office/assets/wall-clock.js +84 -0
  95. package/office/assets/wall.js +53 -0
  96. package/office/assets/water-cooler.js +146 -0
  97. package/office/assets/whiteboard.js +115 -0
  98. package/office/assets.js +3 -431
  99. package/office/builder.js +791 -355
  100. package/office/campus-env.js +1012 -1119
  101. package/office/environment.js +2 -0
  102. package/office/gallery.js +997 -0
  103. package/office/index.js +165 -61
  104. package/office/navigation.js +173 -152
  105. package/office/player.js +178 -68
  106. package/office/robot-character.js +272 -0
  107. package/office/spectator-camera.js +33 -10
  108. package/office/state.js +2 -0
  109. package/office/world-save.js +35 -4
  110. package/package.json +57 -3
  111. package/providers/comfyui.js +383 -0
  112. package/providers/dalle.js +79 -0
  113. package/providers/gemini.js +181 -0
  114. package/providers/ollama.js +184 -0
  115. package/providers/replicate.js +115 -0
  116. package/providers/zai.js +183 -0
  117. package/runtime-descriptor.js +270 -0
  118. package/scripts/check-agent-contract-advisory.js +132 -0
  119. package/scripts/check-api-agent-parity.js +277 -0
  120. package/scripts/check-autonomy-v2-decision.js +207 -0
  121. package/scripts/check-autonomy-v2-execution.js +588 -0
  122. package/scripts/check-autonomy-v2-watchdog.js +224 -0
  123. package/scripts/check-branch-fork-snapshot.js +337 -0
  124. package/scripts/check-branch-isolation.js +787 -0
  125. package/scripts/check-branch-semantics.js +139 -0
  126. package/scripts/check-dashboard-control-plane.js +1304 -0
  127. package/scripts/check-docs-onboarding.js +490 -0
  128. package/scripts/check-event-schema.js +276 -0
  129. package/scripts/check-evidence-completion.js +239 -0
  130. package/scripts/check-invariants.js +992 -0
  131. package/scripts/check-lifecycle-hooks.js +525 -0
  132. package/scripts/check-managed-team-integration.js +166 -0
  133. package/scripts/check-markdown-workspace-export.js +548 -0
  134. package/scripts/check-markdown-workspace-safety.js +347 -0
  135. package/scripts/check-markdown-workspace.js +136 -0
  136. package/scripts/check-message-replay.js +429 -0
  137. package/scripts/check-migration-hardening.js +300 -0
  138. package/scripts/check-performance-indexing.js +272 -0
  139. package/scripts/check-provider-capabilities.js +316 -0
  140. package/scripts/check-runtime-contract.js +109 -0
  141. package/scripts/check-session-aware-context.js +172 -0
  142. package/scripts/check-session-lifecycle.js +210 -0
  143. package/scripts/export-markdown-workspace.js +84 -0
  144. package/scripts/fixtures/message-replay/clean.jsonl +2 -0
  145. package/scripts/fixtures/message-replay/corrupt-correction-payload.jsonl +1 -0
  146. package/scripts/fixtures/message-replay/corrupt-jsonl.jsonl +1 -0
  147. package/scripts/fixtures/message-replay/corrupt-payload.jsonl +1 -0
  148. package/scripts/fixtures/message-replay/out-of-order.jsonl +2 -0
  149. package/scripts/migrate-legacy-to-canonical.js +201 -0
  150. package/scripts/run-verification-suite.js +242 -0
  151. package/scripts/sync-packaged-docs.js +69 -0
  152. package/server.js +9546 -7216
  153. package/state/agents.js +161 -0
  154. package/state/canonical.js +3068 -0
  155. package/state/dashboard-queries.js +441 -0
  156. package/state/evidence.js +56 -0
  157. package/state/io.js +69 -0
  158. package/state/markdown-workspace.js +951 -0
  159. package/state/messages.js +669 -0
  160. package/state/sessions.js +683 -0
  161. package/state/tasks-workflows.js +92 -0
  162. package/templates/debate.json +2 -2
  163. package/templates/managed.json +4 -4
  164. package/templates/pair.json +2 -2
  165. package/templates/review.json +2 -2
  166. package/templates/team.json +3 -3
@@ -0,0 +1,572 @@
1
+ const { analyzeContractFit } = require('../agent-contracts');
2
+ const { analyzeCapabilityFit, evaluateAutonomyCandidate } = require('./decision-v2');
3
+
4
+ const WATCHDOG_POLICY_THRESHOLDS = Object.freeze({
5
+ idle_nudge_ms: 120000,
6
+ idle_hard_nudge_ms: 300000,
7
+ dead_claim_release_ms: 600000,
8
+ blocked_escalation_ms: 300000,
9
+ step_ping_ms: 900000,
10
+ step_escalation_ms: 1800000,
11
+ retry_escalation_attempts: 2,
12
+ retry_blocked_permanent_attempts: 3,
13
+ });
14
+
15
+ function normalizeText(value) {
16
+ if (typeof value !== 'string') return null;
17
+ const trimmed = value.trim();
18
+ return trimmed ? trimmed : null;
19
+ }
20
+
21
+ function resolveThresholds(overrides = {}) {
22
+ return {
23
+ ...WATCHDOG_POLICY_THRESHOLDS,
24
+ ...(overrides && typeof overrides === 'object' ? overrides : {}),
25
+ };
26
+ }
27
+
28
+ function cloneJson(value) {
29
+ return value == null ? value : JSON.parse(JSON.stringify(value));
30
+ }
31
+
32
+ function serializeAdvisory(advisory) {
33
+ if (!advisory || typeof advisory !== 'object') return null;
34
+ return cloneJson(advisory);
35
+ }
36
+
37
+ function resolveOwnerState(context = {}, ownerAlive = true, idleMs = 0, thresholds = WATCHDOG_POLICY_THRESHOLDS) {
38
+ const sessionSummary = context && context.session_summary && typeof context.session_summary === 'object'
39
+ ? context.session_summary
40
+ : null;
41
+
42
+ if (!ownerAlive) return 'dead';
43
+ if (sessionSummary && sessionSummary.state && sessionSummary.state !== 'active') {
44
+ return `session_${sessionSummary.state}`;
45
+ }
46
+ if (sessionSummary && sessionSummary.stale) return 'session_stale';
47
+ if (idleMs >= thresholds.idle_hard_nudge_ms) return 'idle_hard';
48
+ if (idleMs >= thresholds.idle_nudge_ms) return 'idle';
49
+ return 'active';
50
+ }
51
+
52
+ function buildWorkTarget(params = {}) {
53
+ return {
54
+ work_type: params.work_type || 'task',
55
+ title: normalizeText(params.title) || '',
56
+ description: normalizeText(params.description) || '',
57
+ assigned: !!params.assigned,
58
+ required_capabilities: Array.isArray(params.required_capabilities) ? [...params.required_capabilities] : [],
59
+ preferred_capabilities: Array.isArray(params.preferred_capabilities) ? [...params.preferred_capabilities] : [],
60
+ };
61
+ }
62
+
63
+ function compareOwnershipCandidates(left, right) {
64
+ const leftScore = left && left.evaluation ? left.evaluation.score || 0 : 0;
65
+ const rightScore = right && right.evaluation ? right.evaluation.score || 0 : 0;
66
+ if (rightScore !== leftScore) return rightScore - leftScore;
67
+
68
+ const leftName = left && left.agent_name ? left.agent_name : '';
69
+ const rightName = right && right.agent_name ? right.agent_name : '';
70
+ return leftName.localeCompare(rightName);
71
+ }
72
+
73
+ function canTransferStalledStepOwnership(policy) {
74
+ return !!(
75
+ policy
76
+ && policy.signal === 'escalate'
77
+ && policy.classification === 'step_owner_unavailable'
78
+ );
79
+ }
80
+
81
+ function planStalledStepOwnershipChange(params = {}) {
82
+ const branchId = normalizeText(params.branchId) || 'main';
83
+ const currentAssignee = normalizeText(params.currentAssignee) || null;
84
+ const watchdogAgentName = normalizeText(params.watchdogAgentName) || null;
85
+ const policy = params.policy && typeof params.policy === 'object' ? params.policy : null;
86
+ const target = buildWorkTarget(params.target && typeof params.target === 'object' ? params.target : params);
87
+ const selectionTarget = {
88
+ ...target,
89
+ assigned: false,
90
+ assignment_priority: 'none',
91
+ };
92
+ const agents = params.agents && typeof params.agents === 'object' ? params.agents : {};
93
+ const resolveContext = typeof params.resolveContext === 'function' ? params.resolveContext : (() => ({}));
94
+ const isAgentAlive = typeof params.isAgentAlive === 'function'
95
+ ? params.isAgentAlive
96
+ : ((_name, agent) => !!agent);
97
+ const rejectedCandidates = [];
98
+ const eligibleCandidates = [];
99
+
100
+ if (!canTransferStalledStepOwnership(policy)) {
101
+ return {
102
+ allowed: false,
103
+ classification: 'ownership_change_blocked',
104
+ reason: 'policy_disallows_transfer',
105
+ branch_id: branchId,
106
+ current_assignee: currentAssignee,
107
+ new_assignee: null,
108
+ policy: cloneJson(policy),
109
+ eligible_candidates: [],
110
+ rejected_candidates: [],
111
+ summary: 'Ownership transfer stays blocked until the stalled-step policy explicitly marks the owner unavailable.',
112
+ };
113
+ }
114
+
115
+ for (const [agentName, agentRecord] of Object.entries(agents)) {
116
+ if (!agentRecord) continue;
117
+
118
+ if (agentName === currentAssignee) {
119
+ rejectedCandidates.push({ agent_name: agentName, reason: 'current_owner' });
120
+ continue;
121
+ }
122
+ if (watchdogAgentName && agentName === watchdogAgentName) {
123
+ rejectedCandidates.push({ agent_name: agentName, reason: 'watchdog_agent' });
124
+ continue;
125
+ }
126
+
127
+ const agentBranch = normalizeText(agentRecord.branch) || branchId;
128
+ if (agentBranch !== branchId) {
129
+ rejectedCandidates.push({ agent_name: agentName, reason: 'branch_mismatch', branch_id: agentBranch });
130
+ continue;
131
+ }
132
+
133
+ if (!isAgentAlive(agentName, agentRecord)) {
134
+ rejectedCandidates.push({ agent_name: agentName, reason: 'agent_unavailable' });
135
+ continue;
136
+ }
137
+
138
+ const evaluation = evaluateAutonomyCandidate({ target: selectionTarget }, resolveContext(agentName, branchId));
139
+ if (!evaluation.capability_advisory || !evaluation.capability_advisory.admissible) {
140
+ rejectedCandidates.push({
141
+ agent_name: agentName,
142
+ reason: evaluation.capability_advisory && evaluation.capability_advisory.status === 'blocked'
143
+ ? 'required_capability_unavailable'
144
+ : 'capability_mismatch',
145
+ });
146
+ continue;
147
+ }
148
+ if (evaluation.contract_admissibility && evaluation.contract_admissibility.admissible === false) {
149
+ rejectedCandidates.push({
150
+ agent_name: agentName,
151
+ reason: evaluation.contract_admissibility.reason || 'contract_mismatch',
152
+ });
153
+ continue;
154
+ }
155
+
156
+ eligibleCandidates.push({
157
+ agent_name: agentName,
158
+ branch_id: agentBranch,
159
+ evaluation,
160
+ });
161
+ }
162
+
163
+ eligibleCandidates.sort(compareOwnershipCandidates);
164
+ const selected = eligibleCandidates[0] || null;
165
+
166
+ if (!selected) {
167
+ return {
168
+ allowed: false,
169
+ classification: 'ownership_change_blocked',
170
+ reason: 'no_eligible_replacement',
171
+ branch_id: branchId,
172
+ current_assignee: currentAssignee,
173
+ new_assignee: null,
174
+ policy: cloneJson(policy),
175
+ eligible_candidates: [],
176
+ rejected_candidates: rejectedCandidates,
177
+ summary: 'Ownership transfer stayed blocked because no same-branch replacement satisfied the current capability and strict-contract gates.',
178
+ };
179
+ }
180
+
181
+ return {
182
+ allowed: true,
183
+ classification: 'ownership_change_allowed',
184
+ reason: 'owner_unavailable_policy',
185
+ branch_id: branchId,
186
+ current_assignee: currentAssignee,
187
+ new_assignee: selected.agent_name,
188
+ policy: cloneJson(policy),
189
+ selected_evaluation: {
190
+ score: selected.evaluation.score,
191
+ contract_status: selected.evaluation.contract_advisory ? selected.evaluation.contract_advisory.status : 'neutral',
192
+ capability_status: selected.evaluation.capability_advisory ? selected.evaluation.capability_advisory.status : 'neutral',
193
+ },
194
+ eligible_candidates: eligibleCandidates.map((entry) => ({
195
+ agent_name: entry.agent_name,
196
+ score: entry.evaluation.score,
197
+ contract_status: entry.evaluation.contract_advisory ? entry.evaluation.contract_advisory.status : 'neutral',
198
+ capability_status: entry.evaluation.capability_advisory ? entry.evaluation.capability_advisory.status : 'neutral',
199
+ })),
200
+ rejected_candidates: rejectedCandidates,
201
+ summary: `Explicit owner-unavailable policy allows moving this workflow step from ${currentAssignee || 'unassigned'} to ${selected.agent_name} on branch ${branchId}.`,
202
+ };
203
+ }
204
+
205
+ function buildRetrySummary(state, attemptCount, maxAttempts, ownerState, contractAdvisory, capabilityAdvisory) {
206
+ const fragments = [];
207
+ fragments.push(`retry ${attemptCount}/${maxAttempts}`);
208
+ if (ownerState !== 'active') fragments.push(`owner ${ownerState.replace(/_/g, ' ')}`);
209
+ if (contractAdvisory && contractAdvisory.status && contractAdvisory.status !== 'neutral') {
210
+ fragments.push(`contract ${contractAdvisory.status}`);
211
+ }
212
+ if (capabilityAdvisory && capabilityAdvisory.status && capabilityAdvisory.status !== 'neutral') {
213
+ fragments.push(`capability ${capabilityAdvisory.status}`);
214
+ }
215
+
216
+ if (state === 'blocked_permanent') {
217
+ return `Retry policy hit its bounded limit (${fragments.join(', ')}). Escalate for human/team review instead of silently reassigning ownership.`;
218
+ }
219
+ if (state === 'escalate') {
220
+ return `Retry policy requests an explicit help signal (${fragments.join(', ')}). Keep ownership stable until a deliberate reassignment decision is made.`;
221
+ }
222
+ return `Retry remains within bounds (${fragments.join(', ')}).`;
223
+ }
224
+
225
+ function classifyRetryPolicy(params = {}) {
226
+ const thresholds = resolveThresholds(params.thresholds);
227
+ const target = params.target && typeof params.target === 'object' ? params.target : buildWorkTarget(params);
228
+ const context = params.context && typeof params.context === 'object' ? params.context : {};
229
+ const attemptCount = Number.isFinite(params.attemptCount) ? params.attemptCount : 0;
230
+ const ownerAlive = params.ownerAlive !== false;
231
+ const idleMs = Number.isFinite(params.idleMs) ? params.idleMs : 0;
232
+ const ownerState = resolveOwnerState(context, ownerAlive, idleMs, thresholds);
233
+ const contractAdvisory = analyzeContractFit(context.contract, target);
234
+ const capabilityAdvisory = analyzeCapabilityFit(context.runtime, target);
235
+ const reasons = [];
236
+
237
+ if (attemptCount >= thresholds.retry_blocked_permanent_attempts) {
238
+ reasons.push('retry_limit_reached');
239
+ } else if (attemptCount >= thresholds.retry_escalation_attempts) {
240
+ reasons.push('multiple_failed_attempts');
241
+ }
242
+ if (ownerState !== 'active') reasons.push(`owner_${ownerState}`);
243
+ if (contractAdvisory && contractAdvisory.status === 'mismatch') reasons.push('contract_mismatch');
244
+ if (capabilityAdvisory.status === 'blocked') reasons.push('required_capability_unavailable');
245
+ else if (capabilityAdvisory.status === 'mismatch') reasons.push('capability_mismatch');
246
+
247
+ let state = 'continue';
248
+ if (attemptCount >= thresholds.retry_blocked_permanent_attempts) {
249
+ state = 'blocked_permanent';
250
+ } else if (attemptCount >= thresholds.retry_escalation_attempts || ownerState !== 'active') {
251
+ state = 'escalate';
252
+ }
253
+
254
+ return {
255
+ state,
256
+ classification: `retry_${state}`,
257
+ attempt_count: attemptCount,
258
+ max_attempts: thresholds.retry_blocked_permanent_attempts,
259
+ escalation_attempts: thresholds.retry_escalation_attempts,
260
+ owner_state: ownerState,
261
+ session_summary: cloneJson(context.session_summary || null),
262
+ contract_advisory: serializeAdvisory(contractAdvisory),
263
+ capability_advisory: serializeAdvisory(capabilityAdvisory),
264
+ reasons,
265
+ summary: buildRetrySummary(
266
+ state,
267
+ attemptCount,
268
+ thresholds.retry_blocked_permanent_attempts,
269
+ ownerState,
270
+ contractAdvisory,
271
+ capabilityAdvisory
272
+ ),
273
+ };
274
+ }
275
+
276
+ function classifyBlockedTaskPolicy(params = {}) {
277
+ const thresholds = resolveThresholds(params.thresholds);
278
+ const blockedAgeMs = Number.isFinite(params.blockedAgeMs) ? params.blockedAgeMs : 0;
279
+ const blockedMinutes = Math.max(1, Math.round(blockedAgeMs / 60000));
280
+ const retryPolicy = classifyRetryPolicy({
281
+ thresholds,
282
+ target: params.target,
283
+ context: params.context,
284
+ attemptCount: params.attemptCount,
285
+ ownerAlive: params.ownerAlive,
286
+ idleMs: params.idleMs,
287
+ });
288
+
289
+ let classification = 'blocked_waiting';
290
+ let signal = 'none';
291
+ if (retryPolicy.state === 'blocked_permanent') {
292
+ classification = 'blocked_permanent_candidate';
293
+ signal = 'escalate';
294
+ } else if (blockedAgeMs >= thresholds.blocked_escalation_ms || retryPolicy.state === 'escalate') {
295
+ classification = retryPolicy.owner_state !== 'active'
296
+ ? 'blocked_owner_unavailable'
297
+ : 'blocked_escalation_candidate';
298
+ signal = 'escalate';
299
+ }
300
+
301
+ return {
302
+ ...retryPolicy,
303
+ classification,
304
+ signal,
305
+ blocked_age_ms: blockedAgeMs,
306
+ blocked_minutes: blockedMinutes,
307
+ summary: signal === 'escalate'
308
+ ? `Blocked task needs an explicit escalation after ${blockedMinutes} minute(s). ${retryPolicy.summary}`
309
+ : `Blocked task remains within the bounded waiting window after ${blockedMinutes} minute(s).`,
310
+ };
311
+ }
312
+
313
+ function classifyStalledWorkflowStepPolicy(params = {}) {
314
+ const thresholds = resolveThresholds(params.thresholds);
315
+ const target = params.target && typeof params.target === 'object' ? params.target : buildWorkTarget(params);
316
+ const context = params.context && typeof params.context === 'object' ? params.context : {};
317
+ const stepAgeMs = Number.isFinite(params.stepAgeMs) ? params.stepAgeMs : 0;
318
+ const stepMinutes = Math.max(1, Math.round(stepAgeMs / 60000));
319
+ const ownerAlive = params.ownerAlive !== false;
320
+ const idleMs = Number.isFinite(params.idleMs) ? params.idleMs : 0;
321
+ const ownerState = resolveOwnerState(context, ownerAlive, idleMs, thresholds);
322
+ const contractAdvisory = analyzeContractFit(context.contract, target);
323
+ const capabilityAdvisory = analyzeCapabilityFit(context.runtime, target);
324
+ const resumeContext = params.resumeContext && typeof params.resumeContext === 'object' ? params.resumeContext : {};
325
+ const dependencyEvidenceCount = Array.isArray(resumeContext.dependency_evidence) ? resumeContext.dependency_evidence.length : 0;
326
+ const recentEvidenceCount = Array.isArray(resumeContext.recent_evidence) ? resumeContext.recent_evidence.length : 0;
327
+ const reasons = [];
328
+
329
+ let classification = 'step_healthy';
330
+ let signal = 'none';
331
+ if (ownerState !== 'active') {
332
+ classification = 'step_owner_unavailable';
333
+ signal = 'escalate';
334
+ reasons.push(`owner_${ownerState}`);
335
+ } else if (stepAgeMs >= thresholds.step_escalation_ms) {
336
+ classification = 'step_stalled_escalation';
337
+ signal = 'escalate';
338
+ reasons.push('duration_limit_exceeded');
339
+ } else if (stepAgeMs >= thresholds.step_ping_ms) {
340
+ classification = 'step_stalled_checkin';
341
+ signal = 'checkin';
342
+ reasons.push('duration_checkin_due');
343
+ }
344
+
345
+ if (contractAdvisory && contractAdvisory.status === 'mismatch') reasons.push('contract_mismatch');
346
+ if (capabilityAdvisory.status === 'blocked') reasons.push('required_capability_unavailable');
347
+ else if (capabilityAdvisory.status === 'mismatch') reasons.push('capability_mismatch');
348
+
349
+ const summary = signal === 'escalate'
350
+ ? `Workflow step needs explicit escalation after ${stepMinutes} minute(s). Owner ${ownerState.replace(/_/g, ' ')}.`
351
+ : signal === 'checkin'
352
+ ? `Workflow step needs a bounded status check after ${stepMinutes} minute(s).`
353
+ : `Workflow step is within the current watchdog window after ${stepMinutes} minute(s).`;
354
+
355
+ return {
356
+ classification,
357
+ signal,
358
+ owner_state: ownerState,
359
+ step_age_ms: stepAgeMs,
360
+ step_minutes: stepMinutes,
361
+ session_summary: cloneJson(context.session_summary || null),
362
+ contract_advisory: serializeAdvisory(contractAdvisory),
363
+ capability_advisory: serializeAdvisory(capabilityAdvisory),
364
+ dependency_evidence_count: dependencyEvidenceCount,
365
+ recent_evidence_count: recentEvidenceCount,
366
+ reasons,
367
+ summary,
368
+ };
369
+ }
370
+
371
+ function canReleaseUnavailableClaim(ownerState) {
372
+ return ownerState === 'dead'
373
+ || ownerState === 'session_interrupted'
374
+ || ownerState === 'session_failed'
375
+ || ownerState === 'session_abandoned';
376
+ }
377
+
378
+ function planWatchdogActions(params = {}) {
379
+ const thresholds = resolveThresholds(params.thresholds);
380
+ const nowMs = Number.isFinite(params.nowMs) ? params.nowMs : Date.now();
381
+ const agents = params.agents && typeof params.agents === 'object' ? params.agents : {};
382
+ const tasks = Array.isArray(params.tasks) ? params.tasks : [];
383
+ const workflows = Array.isArray(params.workflows) ? params.workflows : [];
384
+ const resolveContext = typeof params.resolveContext === 'function' ? params.resolveContext : (() => ({}));
385
+ const resolveStepResumeContext = typeof params.resolveStepResumeContext === 'function' ? params.resolveStepResumeContext : (() => null);
386
+ const isAgentAlive = typeof params.isAgentAlive === 'function'
387
+ ? params.isAgentAlive
388
+ : ((_name, agent) => !!agent);
389
+ const actions = [];
390
+
391
+ for (const [name, agent] of Object.entries(agents)) {
392
+ if (!agent || name === params.watchdogAgentName) continue;
393
+ if (!isAgentAlive(name, agent)) continue;
394
+
395
+ const lastActivityMs = Date.parse(agent.last_activity || '') || 0;
396
+ const idleMs = Math.max(0, nowMs - lastActivityMs);
397
+ const context = resolveContext(name, agent.branch || params.branchId || 'main');
398
+
399
+ if (idleMs >= thresholds.idle_hard_nudge_ms && !agent.watchdog_hard_nudged) {
400
+ actions.push({
401
+ kind: 'nudge_idle_hard',
402
+ agentName: name,
403
+ branchId: agent.branch || params.branchId || 'main',
404
+ idleMs,
405
+ policy: {
406
+ classification: 'agent_idle_hard',
407
+ owner_state: resolveOwnerState(context, true, idleMs, thresholds),
408
+ session_summary: cloneJson(context.session_summary || null),
409
+ summary: `Agent has been idle for ${Math.round(idleMs / 60000)} minute(s) and needs an explicit get_work() reminder.`,
410
+ },
411
+ });
412
+ continue;
413
+ }
414
+
415
+ if (idleMs >= thresholds.idle_nudge_ms && !agent.watchdog_nudged) {
416
+ actions.push({
417
+ kind: 'nudge_idle',
418
+ agentName: name,
419
+ branchId: agent.branch || params.branchId || 'main',
420
+ idleMs,
421
+ policy: {
422
+ classification: 'agent_idle',
423
+ owner_state: resolveOwnerState(context, true, idleMs, thresholds),
424
+ session_summary: cloneJson(context.session_summary || null),
425
+ summary: `Agent has been idle for ${Math.round(idleMs / 60000)} minute(s) and should refresh work ownership explicitly.`,
426
+ },
427
+ });
428
+ }
429
+ }
430
+
431
+ for (const task of tasks) {
432
+ if (!task || !task.id) continue;
433
+ const branchId = task.branch_id || (task.assignee && agents[task.assignee] && agents[task.assignee].branch) || params.branchId || 'main';
434
+ const assignee = normalizeText(task.assignee);
435
+ const ownerAlive = assignee ? isAgentAlive(assignee, agents[assignee] || null) : true;
436
+ const lastActivityMs = assignee && agents[assignee] ? (Date.parse(agents[assignee].last_activity || '') || 0) : 0;
437
+ const idleMs = assignee ? Math.max(0, nowMs - lastActivityMs) : 0;
438
+ const context = assignee ? resolveContext(assignee, branchId) : {};
439
+ const target = buildWorkTarget({
440
+ work_type: 'task',
441
+ title: task.title,
442
+ description: task.description,
443
+ assigned: !!assignee,
444
+ required_capabilities: task.required_capabilities,
445
+ preferred_capabilities: task.preferred_capabilities,
446
+ });
447
+ const retryPolicy = classifyRetryPolicy({
448
+ thresholds,
449
+ target,
450
+ context,
451
+ attemptCount: Array.isArray(task.attempt_agents) ? task.attempt_agents.length : 0,
452
+ ownerAlive,
453
+ idleMs,
454
+ });
455
+
456
+ if (task.status === 'in_progress' && assignee && canReleaseUnavailableClaim(retryPolicy.owner_state)) {
457
+ actions.push({
458
+ kind: 'release_task_claim',
459
+ branchId,
460
+ taskId: task.id,
461
+ taskTitle: task.title || task.id,
462
+ assignee,
463
+ policy: {
464
+ ...retryPolicy,
465
+ classification: 'claimed_task_owner_unavailable',
466
+ summary: `Current task owner is unavailable (${retryPolicy.owner_state.replace(/_/g, ' ')}). Release the claim back to pending without selecting a replacement.`,
467
+ },
468
+ });
469
+ }
470
+
471
+ if (task.status === 'blocked' && !task.escalated_at) {
472
+ const blockedAgeMs = Math.max(0, nowMs - (Date.parse(task.updated_at || '') || 0));
473
+ const blockedPolicy = classifyBlockedTaskPolicy({
474
+ thresholds,
475
+ target,
476
+ context,
477
+ attemptCount: Array.isArray(task.attempt_agents) ? task.attempt_agents.length : 0,
478
+ ownerAlive,
479
+ idleMs,
480
+ blockedAgeMs,
481
+ });
482
+ if (blockedPolicy.signal === 'escalate') {
483
+ actions.push({
484
+ kind: 'escalate_blocked_task',
485
+ branchId,
486
+ taskId: task.id,
487
+ taskTitle: task.title || task.id,
488
+ assignee,
489
+ blockedAgeMs,
490
+ policy: blockedPolicy,
491
+ });
492
+ }
493
+ }
494
+ }
495
+
496
+ for (const workflow of workflows) {
497
+ if (!workflow || workflow.status !== 'active') continue;
498
+ for (const step of Array.isArray(workflow.steps) ? workflow.steps : []) {
499
+ if (!step || step.status !== 'in_progress' || !step.started_at) continue;
500
+ const assignee = normalizeText(step.assignee);
501
+ const branchId = normalizeText(workflow.branch_id)
502
+ || normalizeText(step.branch_id)
503
+ || (assignee && normalizeText(agents[assignee] && agents[assignee].branch))
504
+ || normalizeText(params.branchId)
505
+ || 'main';
506
+ const ownerAlive = assignee ? isAgentAlive(assignee, agents[assignee] || null) : true;
507
+ const lastActivityMs = assignee && agents[assignee] ? (Date.parse(agents[assignee].last_activity || '') || 0) : 0;
508
+ const idleMs = assignee ? Math.max(0, nowMs - lastActivityMs) : 0;
509
+ const stepAgeMs = Math.max(0, nowMs - (Date.parse(step.started_at || '') || 0));
510
+ const context = assignee ? resolveContext(assignee, branchId) : {};
511
+ const resumeContext = resolveStepResumeContext(workflow, step, branchId, assignee);
512
+ const stepPolicy = classifyStalledWorkflowStepPolicy({
513
+ thresholds,
514
+ target: buildWorkTarget({
515
+ work_type: 'workflow_step',
516
+ title: step.description,
517
+ description: workflow.name,
518
+ assigned: !!assignee,
519
+ required_capabilities: step.required_capabilities,
520
+ preferred_capabilities: step.preferred_capabilities,
521
+ }),
522
+ context,
523
+ ownerAlive,
524
+ idleMs,
525
+ stepAgeMs,
526
+ resumeContext,
527
+ });
528
+
529
+ if (stepPolicy.signal === 'checkin' && !step.watchdog_pinged_at && !step.watchdog_pinged) {
530
+ actions.push({
531
+ kind: 'signal_stalled_step',
532
+ signal: 'checkin',
533
+ branchId,
534
+ workflowId: workflow.id,
535
+ workflowName: workflow.name,
536
+ stepId: step.id,
537
+ stepDescription: step.description || step.id,
538
+ assignee,
539
+ stepAgeMs,
540
+ policy: stepPolicy,
541
+ });
542
+ }
543
+
544
+ if (stepPolicy.signal === 'escalate' && !step.watchdog_escalated_at && !step.watchdog_escalated) {
545
+ actions.push({
546
+ kind: 'signal_stalled_step',
547
+ signal: 'escalate',
548
+ branchId,
549
+ workflowId: workflow.id,
550
+ workflowName: workflow.name,
551
+ stepId: step.id,
552
+ stepDescription: step.description || step.id,
553
+ assignee,
554
+ stepAgeMs,
555
+ policy: stepPolicy,
556
+ });
557
+ }
558
+ }
559
+ }
560
+
561
+ return actions;
562
+ }
563
+
564
+ module.exports = {
565
+ WATCHDOG_POLICY_THRESHOLDS,
566
+ classifyBlockedTaskPolicy,
567
+ classifyRetryPolicy,
568
+ classifyStalledWorkflowStepPolicy,
569
+ planStalledStepOwnershipChange,
570
+ planWatchdogActions,
571
+ resolveOwnerState,
572
+ };