@chllming/wave-orchestration 0.7.1 → 0.7.3
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 +30 -0
- package/README.md +8 -8
- package/docs/plans/component-cutover-matrix.json +50 -3
- package/docs/plans/current-state.md +1 -1
- package/docs/plans/end-state-architecture.md +927 -0
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +2 -2
- package/docs/plans/waves/wave-1.md +376 -0
- package/docs/plans/waves/wave-2.md +292 -0
- package/docs/plans/waves/wave-3.md +342 -0
- package/docs/plans/waves/wave-4.md +391 -0
- package/docs/plans/waves/wave-5.md +382 -0
- package/docs/plans/waves/wave-6.md +321 -0
- package/docs/reference/npmjs-trusted-publishing.md +2 -2
- package/docs/reference/sample-waves.md +4 -4
- package/package.json +1 -1
- package/releases/manifest.json +36 -0
- package/scripts/wave-orchestrator/agent-state.mjs +462 -35
- package/scripts/wave-orchestrator/artifact-schemas.mjs +81 -0
- package/scripts/wave-orchestrator/control-cli.mjs +7 -1
- package/scripts/wave-orchestrator/coordination.mjs +11 -10
- package/scripts/wave-orchestrator/human-input-workflow.mjs +289 -0
- package/scripts/wave-orchestrator/install.mjs +22 -0
- package/scripts/wave-orchestrator/launcher-derived-state.mjs +915 -0
- package/scripts/wave-orchestrator/launcher-gates.mjs +1061 -0
- package/scripts/wave-orchestrator/launcher-retry.mjs +873 -0
- package/scripts/wave-orchestrator/launcher-supervisor.mjs +704 -0
- package/scripts/wave-orchestrator/launcher.mjs +153 -2922
- package/scripts/wave-orchestrator/task-entity.mjs +557 -0
- package/scripts/wave-orchestrator/wave-files.mjs +11 -2
- package/scripts/wave-orchestrator/wave-state-reducer.mjs +566 -0
- package/wave.config.json +1 -1
|
@@ -403,3 +403,84 @@ export function writeWaveControlDeliveryState(filePath, payload, defaults = {})
|
|
|
403
403
|
export function cloneArtifactPayload(value) {
|
|
404
404
|
return cloneJson(value);
|
|
405
405
|
}
|
|
406
|
+
|
|
407
|
+
// ── Wave 4: Surface class metadata and additional schema normalizers ──
|
|
408
|
+
|
|
409
|
+
export const WAVE_STATE_SCHEMA_VERSION = 1;
|
|
410
|
+
export const TASK_ENTITY_SCHEMA_VERSION = 1;
|
|
411
|
+
export const AGENT_RESULT_ENVELOPE_SCHEMA_VERSION = 1;
|
|
412
|
+
export const RESUME_PLAN_SCHEMA_VERSION = 1;
|
|
413
|
+
export const HUMAN_INPUT_WORKFLOW_SCHEMA_VERSION = 1;
|
|
414
|
+
|
|
415
|
+
export const SURFACE_CLASS_CANONICAL_EVENT = "canonical-event";
|
|
416
|
+
export const SURFACE_CLASS_CANONICAL_SNAPSHOT = "canonical-snapshot";
|
|
417
|
+
export const SURFACE_CLASS_CACHED_DERIVED = "cached-derived";
|
|
418
|
+
export const SURFACE_CLASS_HUMAN_PROJECTION = "human-projection";
|
|
419
|
+
export const SURFACE_CLASSES = new Set([
|
|
420
|
+
SURFACE_CLASS_CANONICAL_EVENT,
|
|
421
|
+
SURFACE_CLASS_CANONICAL_SNAPSHOT,
|
|
422
|
+
SURFACE_CLASS_CACHED_DERIVED,
|
|
423
|
+
SURFACE_CLASS_HUMAN_PROJECTION,
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
export const WAVE_STATE_KIND = "wave-state-snapshot";
|
|
427
|
+
export const TASK_ENTITY_KIND = "wave-task-entity";
|
|
428
|
+
export const AGENT_RESULT_ENVELOPE_KIND = "agent-result-envelope";
|
|
429
|
+
export const RESUME_PLAN_KIND = "wave-resume-plan";
|
|
430
|
+
export const HUMAN_INPUT_WORKFLOW_KIND = "human-input-workflow-state";
|
|
431
|
+
|
|
432
|
+
export function normalizeWaveStateSnapshot(payload, defaults = {}) {
|
|
433
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
434
|
+
return {
|
|
435
|
+
schemaVersion: WAVE_STATE_SCHEMA_VERSION,
|
|
436
|
+
kind: WAVE_STATE_KIND,
|
|
437
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
438
|
+
lane: normalizeText(source.lane, normalizeText(defaults.lane, null)),
|
|
439
|
+
wave: normalizeInteger(source.wave, normalizeInteger(defaults.wave, null)),
|
|
440
|
+
...source,
|
|
441
|
+
schemaVersion: WAVE_STATE_SCHEMA_VERSION,
|
|
442
|
+
kind: WAVE_STATE_KIND,
|
|
443
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
444
|
+
generatedAt: normalizeText(source.generatedAt, toIsoTimestamp()),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function readWaveStateSnapshot(filePath, defaults = {}) {
|
|
449
|
+
const payload = readJsonOrNull(filePath);
|
|
450
|
+
if (!payload) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return normalizeWaveStateSnapshot(payload, defaults);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function writeWaveStateSnapshot(filePath, payload, defaults = {}) {
|
|
457
|
+
const normalized = normalizeWaveStateSnapshot(payload, defaults);
|
|
458
|
+
writeJsonAtomic(filePath, normalized);
|
|
459
|
+
return normalized;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function normalizeAgentResultEnvelope(payload) {
|
|
463
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
464
|
+
return {
|
|
465
|
+
schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
|
|
466
|
+
kind: AGENT_RESULT_ENVELOPE_KIND,
|
|
467
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
468
|
+
...source,
|
|
469
|
+
schemaVersion: AGENT_RESULT_ENVELOPE_SCHEMA_VERSION,
|
|
470
|
+
kind: AGENT_RESULT_ENVELOPE_KIND,
|
|
471
|
+
_meta: { surfaceClass: SURFACE_CLASS_CANONICAL_SNAPSHOT },
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function normalizeResumePlan(payload) {
|
|
476
|
+
const source = isPlainObject(payload) ? payload : {};
|
|
477
|
+
return {
|
|
478
|
+
schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
|
|
479
|
+
kind: RESUME_PLAN_KIND,
|
|
480
|
+
_meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
|
|
481
|
+
...source,
|
|
482
|
+
schemaVersion: RESUME_PLAN_SCHEMA_VERSION,
|
|
483
|
+
kind: RESUME_PLAN_KIND,
|
|
484
|
+
_meta: { surfaceClass: SURFACE_CLASS_CACHED_DERIVED },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
@@ -315,9 +315,15 @@ function buildLogicalAgents({ lanePaths, wave, tasks, dependencySnapshot, capabi
|
|
|
315
315
|
return wave.agents.map((agent) => {
|
|
316
316
|
const statusPath = statusPathForAgent(lanePaths, wave, agent);
|
|
317
317
|
const statusRecord = readStatusRecordIfPresent(statusPath);
|
|
318
|
+
const logPath = path.join(lanePaths.logsDir, `wave-${wave.wave}-${agent.slug}.log`);
|
|
318
319
|
const summary = augmentSummaryWithProofRegistry(
|
|
319
320
|
agent,
|
|
320
|
-
readAgentExecutionSummary(statusPath
|
|
321
|
+
readAgentExecutionSummary(statusPath, {
|
|
322
|
+
agent,
|
|
323
|
+
statusPath,
|
|
324
|
+
statusRecord,
|
|
325
|
+
logPath: fs.existsSync(logPath) ? logPath : null,
|
|
326
|
+
}),
|
|
321
327
|
proofRegistry || { entries: [] },
|
|
322
328
|
);
|
|
323
329
|
const proofValidation =
|
|
@@ -269,6 +269,15 @@ export function buildExecutionPrompt({
|
|
|
269
269
|
"- Use `clear` only when no unresolved findings or approvals remain. Use `blocked` only when the wave must stop before integration.",
|
|
270
270
|
]
|
|
271
271
|
: [];
|
|
272
|
+
const coordinationCommand = [
|
|
273
|
+
"pnpm exec wave coord post",
|
|
274
|
+
`--lane ${lane}`,
|
|
275
|
+
`--wave ${wave}`,
|
|
276
|
+
`--agent ${agent.agentId}`,
|
|
277
|
+
'--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
|
|
278
|
+
'--summary "<one-line summary>"',
|
|
279
|
+
'--detail "<short detail>"',
|
|
280
|
+
].join(" ");
|
|
272
281
|
const implementationRequirements =
|
|
273
282
|
![contQaAgentId, documentationAgentId].includes(agent.agentId) &&
|
|
274
283
|
!isSecurityReviewAgent(agent) &&
|
|
@@ -281,7 +290,8 @@ export function buildExecutionPrompt({
|
|
|
281
290
|
"- Emit one final structured component marker per owned component: `[wave-component] component=<id> level=<level> state=<met|gap> detail=<short-note>`.",
|
|
282
291
|
]
|
|
283
292
|
: []),
|
|
284
|
-
"- If
|
|
293
|
+
"- If the work is incomplete, keep the required proof/doc/component markers and set `state=gap` on the relevant final marker instead of narrating completion.",
|
|
294
|
+
`- Route unresolved architecture, integration, durability, ops, or docs issues through \`${coordinationCommand}\`. Do not append \`[wave-gap]\` lines after the final implementation markers.`,
|
|
285
295
|
]
|
|
286
296
|
: [];
|
|
287
297
|
const exitContractLines = agent.exitContract
|
|
@@ -305,15 +315,6 @@ export function buildExecutionPrompt({
|
|
|
305
315
|
'--context "<what you tried, options, and impact>"',
|
|
306
316
|
"--timeout-seconds 30",
|
|
307
317
|
].join(" ");
|
|
308
|
-
const coordinationCommand = [
|
|
309
|
-
"pnpm exec wave coord post",
|
|
310
|
-
`--lane ${lane}`,
|
|
311
|
-
`--wave ${wave}`,
|
|
312
|
-
`--agent ${agent.agentId}`,
|
|
313
|
-
'--kind "<request|ack|claim|evidence|decision|blocker|handoff|clarification-request|orchestrator-guidance|resolved-by-policy|human-escalation|human-feedback|integration-summary>"',
|
|
314
|
-
'--summary "<one-line summary>"',
|
|
315
|
-
'--detail "<short detail>"',
|
|
316
|
-
].join(" ");
|
|
317
318
|
const context7Selection = context7?.selection || agent?.context7Resolved || null;
|
|
318
319
|
const executorId = agent?.executorResolved?.id || "default";
|
|
319
320
|
const context7LibrarySummary =
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { toIsoTimestamp } from "./shared.mjs";
|
|
2
|
+
|
|
3
|
+
// ── Human Input Workflow State Machine ──
|
|
4
|
+
//
|
|
5
|
+
// States: open -> pending -> answered -> resolved
|
|
6
|
+
// -> escalated -> resolved
|
|
7
|
+
|
|
8
|
+
export const HUMAN_INPUT_STATES = new Set([
|
|
9
|
+
"open",
|
|
10
|
+
"pending",
|
|
11
|
+
"answered",
|
|
12
|
+
"escalated",
|
|
13
|
+
"resolved",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const HUMAN_INPUT_VALID_TRANSITIONS = {
|
|
17
|
+
open: ["pending", "escalated", "resolved"],
|
|
18
|
+
pending: ["answered", "escalated", "resolved"],
|
|
19
|
+
answered: ["resolved"],
|
|
20
|
+
escalated: ["answered", "resolved"],
|
|
21
|
+
resolved: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const BLOCKING_STATES = new Set(["open", "pending", "escalated"]);
|
|
25
|
+
|
|
26
|
+
const DEFAULT_TIMEOUT_POLICY = {
|
|
27
|
+
maxWaitMs: 300000,
|
|
28
|
+
escalateAfterMs: 120000,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const DEFAULT_REROUTE_POLICY = {
|
|
32
|
+
rerouteOnTimeout: true,
|
|
33
|
+
rerouteTo: "operator",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeText(value, fallback = null) {
|
|
41
|
+
const normalized = String(value ?? "").trim();
|
|
42
|
+
return normalized || fallback;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeHumanInputRequest(request, defaults = {}) {
|
|
46
|
+
const source = isPlainObject(request) ? request : {};
|
|
47
|
+
const defaultSource = isPlainObject(defaults) ? defaults : {};
|
|
48
|
+
const now = toIsoTimestamp();
|
|
49
|
+
|
|
50
|
+
const timeoutPolicy = isPlainObject(source.timeoutPolicy)
|
|
51
|
+
? {
|
|
52
|
+
maxWaitMs: Number.isFinite(source.timeoutPolicy.maxWaitMs)
|
|
53
|
+
? source.timeoutPolicy.maxWaitMs
|
|
54
|
+
: DEFAULT_TIMEOUT_POLICY.maxWaitMs,
|
|
55
|
+
escalateAfterMs: Number.isFinite(source.timeoutPolicy.escalateAfterMs)
|
|
56
|
+
? source.timeoutPolicy.escalateAfterMs
|
|
57
|
+
: DEFAULT_TIMEOUT_POLICY.escalateAfterMs,
|
|
58
|
+
}
|
|
59
|
+
: { ...DEFAULT_TIMEOUT_POLICY };
|
|
60
|
+
|
|
61
|
+
const reroutePolicy = isPlainObject(source.reroutePolicy)
|
|
62
|
+
? {
|
|
63
|
+
rerouteOnTimeout: source.reroutePolicy.rerouteOnTimeout !== false,
|
|
64
|
+
rerouteTo: normalizeText(source.reroutePolicy.rerouteTo, DEFAULT_REROUTE_POLICY.rerouteTo),
|
|
65
|
+
}
|
|
66
|
+
: { ...DEFAULT_REROUTE_POLICY };
|
|
67
|
+
|
|
68
|
+
const rawState = normalizeText(source.state, normalizeText(defaultSource.state, "open"));
|
|
69
|
+
const state = HUMAN_INPUT_STATES.has(rawState) ? rawState : "open";
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
requestId: normalizeText(source.requestId, normalizeText(defaultSource.requestId, null)),
|
|
73
|
+
kind: normalizeText(source.kind, normalizeText(defaultSource.kind, "human-input")),
|
|
74
|
+
state,
|
|
75
|
+
title: normalizeText(source.title, normalizeText(defaultSource.title, null)),
|
|
76
|
+
detail: normalizeText(source.detail, normalizeText(defaultSource.detail, null)),
|
|
77
|
+
requestedBy: normalizeText(source.requestedBy, normalizeText(defaultSource.requestedBy, null)),
|
|
78
|
+
assignedTo: normalizeText(source.assignedTo, normalizeText(defaultSource.assignedTo, null)),
|
|
79
|
+
timeoutPolicy,
|
|
80
|
+
reroutePolicy,
|
|
81
|
+
createdAt: normalizeText(source.createdAt, normalizeText(defaultSource.createdAt, now)),
|
|
82
|
+
updatedAt: normalizeText(source.updatedAt, normalizeText(defaultSource.updatedAt, now)),
|
|
83
|
+
answeredAt: normalizeText(source.answeredAt, null),
|
|
84
|
+
resolvedAt: normalizeText(source.resolvedAt, null),
|
|
85
|
+
escalatedAt: normalizeText(source.escalatedAt, null),
|
|
86
|
+
answer: normalizeText(source.answer, null),
|
|
87
|
+
resolution: normalizeText(source.resolution, null),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function transitionHumanInputState(currentState, targetState) {
|
|
92
|
+
if (!HUMAN_INPUT_STATES.has(currentState)) {
|
|
93
|
+
throw new Error(`Invalid current state: ${currentState}`);
|
|
94
|
+
}
|
|
95
|
+
if (!HUMAN_INPUT_STATES.has(targetState)) {
|
|
96
|
+
throw new Error(`Invalid target state: ${targetState}`);
|
|
97
|
+
}
|
|
98
|
+
const allowed = HUMAN_INPUT_VALID_TRANSITIONS[currentState];
|
|
99
|
+
if (!allowed || !allowed.includes(targetState)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid transition from "${currentState}" to "${targetState}". Allowed: [${(allowed || []).join(", ")}]`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return targetState;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isHumanInputBlocking(request) {
|
|
108
|
+
const source = isPlainObject(request) ? request : {};
|
|
109
|
+
const state = normalizeText(source.state, "open");
|
|
110
|
+
return BLOCKING_STATES.has(state);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildHumanInputRequests(coordinationState, feedbackRequests, options = {}) {
|
|
114
|
+
const results = [];
|
|
115
|
+
const coordState = isPlainObject(coordinationState) ? coordinationState : {};
|
|
116
|
+
const feedbackList = Array.isArray(feedbackRequests) ? feedbackRequests : [];
|
|
117
|
+
const now = toIsoTimestamp();
|
|
118
|
+
|
|
119
|
+
// Process clarification-request records from coordination state
|
|
120
|
+
const clarifications = Array.isArray(coordState.clarifications)
|
|
121
|
+
? coordState.clarifications
|
|
122
|
+
: [];
|
|
123
|
+
for (const record of clarifications) {
|
|
124
|
+
if (!isPlainObject(record)) continue;
|
|
125
|
+
const kind = normalizeText(record.kind, null);
|
|
126
|
+
if (
|
|
127
|
+
kind !== "clarification-request" &&
|
|
128
|
+
kind !== "human-escalation" &&
|
|
129
|
+
kind !== "human-feedback"
|
|
130
|
+
) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const mappedKind =
|
|
134
|
+
kind === "clarification-request"
|
|
135
|
+
? "clarification"
|
|
136
|
+
: kind === "human-escalation"
|
|
137
|
+
? "escalation"
|
|
138
|
+
: "feedback";
|
|
139
|
+
const rawStatus = normalizeText(record.status, "open");
|
|
140
|
+
let mappedState = "open";
|
|
141
|
+
if (rawStatus === "in_progress" || rawStatus === "pending") {
|
|
142
|
+
mappedState = "pending";
|
|
143
|
+
} else if (rawStatus === "resolved" || rawStatus === "closed") {
|
|
144
|
+
mappedState = "resolved";
|
|
145
|
+
} else if (rawStatus === "answered") {
|
|
146
|
+
mappedState = "answered";
|
|
147
|
+
}
|
|
148
|
+
results.push(
|
|
149
|
+
normalizeHumanInputRequest({
|
|
150
|
+
requestId: normalizeText(record.id, null),
|
|
151
|
+
kind: mappedKind,
|
|
152
|
+
state: mappedState,
|
|
153
|
+
title: normalizeText(record.summary, null),
|
|
154
|
+
detail: normalizeText(record.detail, null),
|
|
155
|
+
requestedBy: normalizeText(record.agentId, null),
|
|
156
|
+
assignedTo: null,
|
|
157
|
+
createdAt: normalizeText(record.createdAt, now),
|
|
158
|
+
updatedAt: normalizeText(record.updatedAt, now),
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Process human escalations from coordination state
|
|
164
|
+
const humanEscalations = Array.isArray(coordState.humanEscalations)
|
|
165
|
+
? coordState.humanEscalations
|
|
166
|
+
: [];
|
|
167
|
+
for (const record of humanEscalations) {
|
|
168
|
+
if (!isPlainObject(record)) continue;
|
|
169
|
+
const rawStatus = normalizeText(record.status, "open");
|
|
170
|
+
let mappedState = "escalated";
|
|
171
|
+
if (rawStatus === "resolved" || rawStatus === "closed") {
|
|
172
|
+
mappedState = "resolved";
|
|
173
|
+
} else if (rawStatus === "answered") {
|
|
174
|
+
mappedState = "answered";
|
|
175
|
+
}
|
|
176
|
+
results.push(
|
|
177
|
+
normalizeHumanInputRequest({
|
|
178
|
+
requestId: normalizeText(record.id, null),
|
|
179
|
+
kind: "escalation",
|
|
180
|
+
state: mappedState,
|
|
181
|
+
title: normalizeText(record.summary, null),
|
|
182
|
+
detail: normalizeText(record.detail, null),
|
|
183
|
+
requestedBy: normalizeText(record.agentId, null),
|
|
184
|
+
assignedTo: "operator",
|
|
185
|
+
createdAt: normalizeText(record.createdAt, now),
|
|
186
|
+
updatedAt: normalizeText(record.updatedAt, now),
|
|
187
|
+
escalatedAt: normalizeText(record.createdAt, now),
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Process feedback requests
|
|
193
|
+
for (const record of feedbackList) {
|
|
194
|
+
if (!isPlainObject(record)) continue;
|
|
195
|
+
const rawStatus = normalizeText(record.status, "pending");
|
|
196
|
+
let mappedState = "pending";
|
|
197
|
+
if (rawStatus === "answered") {
|
|
198
|
+
mappedState = "answered";
|
|
199
|
+
} else if (rawStatus === "resolved" || rawStatus === "closed") {
|
|
200
|
+
mappedState = "resolved";
|
|
201
|
+
}
|
|
202
|
+
results.push(
|
|
203
|
+
normalizeHumanInputRequest({
|
|
204
|
+
requestId: normalizeText(record.id, null),
|
|
205
|
+
kind: "feedback",
|
|
206
|
+
state: mappedState,
|
|
207
|
+
title: normalizeText(record.question, null),
|
|
208
|
+
detail: normalizeText(record.context, null),
|
|
209
|
+
requestedBy: normalizeText(record.agentId, null),
|
|
210
|
+
assignedTo: "operator",
|
|
211
|
+
createdAt: normalizeText(record.createdAt, now),
|
|
212
|
+
updatedAt: normalizeText(record.updatedAt, now),
|
|
213
|
+
answeredAt: normalizeText(record.response?.answeredAt, null),
|
|
214
|
+
answer: normalizeText(record.response?.text, null),
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return results;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function evaluateHumanInputTimeout(request, now = Date.now()) {
|
|
223
|
+
const source = isPlainObject(request) ? request : {};
|
|
224
|
+
const createdAtMs = Date.parse(source.createdAt || "");
|
|
225
|
+
if (!Number.isFinite(createdAtMs)) {
|
|
226
|
+
return { expired: false, shouldEscalate: false, elapsedMs: 0 };
|
|
227
|
+
}
|
|
228
|
+
const elapsedMs = Math.max(0, now - createdAtMs);
|
|
229
|
+
const policy = isPlainObject(source.timeoutPolicy)
|
|
230
|
+
? source.timeoutPolicy
|
|
231
|
+
: DEFAULT_TIMEOUT_POLICY;
|
|
232
|
+
const maxWaitMs = Number.isFinite(policy.maxWaitMs)
|
|
233
|
+
? policy.maxWaitMs
|
|
234
|
+
: DEFAULT_TIMEOUT_POLICY.maxWaitMs;
|
|
235
|
+
const escalateAfterMs = Number.isFinite(policy.escalateAfterMs)
|
|
236
|
+
? policy.escalateAfterMs
|
|
237
|
+
: DEFAULT_TIMEOUT_POLICY.escalateAfterMs;
|
|
238
|
+
const expired = elapsedMs >= maxWaitMs;
|
|
239
|
+
const shouldEscalate = elapsedMs >= escalateAfterMs;
|
|
240
|
+
return { expired, shouldEscalate, elapsedMs };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function computeHumanInputMetrics(requests) {
|
|
244
|
+
const list = Array.isArray(requests) ? requests : [];
|
|
245
|
+
const counts = { open: 0, pending: 0, answered: 0, escalated: 0, resolved: 0 };
|
|
246
|
+
let blocking = 0;
|
|
247
|
+
let overdueCount = 0;
|
|
248
|
+
let totalResolutionMs = 0;
|
|
249
|
+
let resolvedWithTimesCount = 0;
|
|
250
|
+
|
|
251
|
+
for (const request of list) {
|
|
252
|
+
const source = isPlainObject(request) ? request : {};
|
|
253
|
+
const state = normalizeText(source.state, "open");
|
|
254
|
+
if (state in counts) {
|
|
255
|
+
counts[state] += 1;
|
|
256
|
+
}
|
|
257
|
+
if (BLOCKING_STATES.has(state)) {
|
|
258
|
+
blocking += 1;
|
|
259
|
+
}
|
|
260
|
+
// Check overdue based on timeout policy
|
|
261
|
+
const timeout = evaluateHumanInputTimeout(source);
|
|
262
|
+
if (timeout.expired && BLOCKING_STATES.has(state)) {
|
|
263
|
+
overdueCount += 1;
|
|
264
|
+
}
|
|
265
|
+
// Compute resolution time for resolved requests
|
|
266
|
+
if (state === "resolved" && source.createdAt && source.resolvedAt) {
|
|
267
|
+
const createdMs = Date.parse(source.createdAt);
|
|
268
|
+
const resolvedMs = Date.parse(source.resolvedAt);
|
|
269
|
+
if (Number.isFinite(createdMs) && Number.isFinite(resolvedMs) && resolvedMs >= createdMs) {
|
|
270
|
+
totalResolutionMs += resolvedMs - createdMs;
|
|
271
|
+
resolvedWithTimesCount += 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
total: list.length,
|
|
278
|
+
open: counts.open,
|
|
279
|
+
pending: counts.pending,
|
|
280
|
+
answered: counts.answered,
|
|
281
|
+
escalated: counts.escalated,
|
|
282
|
+
resolved: counts.resolved,
|
|
283
|
+
blocking,
|
|
284
|
+
overdueCount,
|
|
285
|
+
avgResolutionMs: resolvedWithTimesCount > 0
|
|
286
|
+
? Math.round(totalResolutionMs / resolvedWithTimesCount)
|
|
287
|
+
: null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
@@ -198,6 +198,28 @@ function copyTemplateFile(relPath) {
|
|
|
198
198
|
throw new Error(`Missing packaged template: ${relPath}`);
|
|
199
199
|
}
|
|
200
200
|
ensureDirectory(path.dirname(targetPath));
|
|
201
|
+
if (relPath === "docs/plans/component-cutover-matrix.json") {
|
|
202
|
+
const payload = readJsonOrNull(sourcePath);
|
|
203
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
204
|
+
throw new Error(`Invalid packaged template JSON: ${relPath}`);
|
|
205
|
+
}
|
|
206
|
+
const components = Object.fromEntries(
|
|
207
|
+
Object.entries(payload.components || {}).map(([componentId, component]) => [
|
|
208
|
+
componentId,
|
|
209
|
+
{
|
|
210
|
+
...component,
|
|
211
|
+
promotions: Array.isArray(component?.promotions)
|
|
212
|
+
? component.promotions.filter((entry) => Number(entry?.wave) === 0)
|
|
213
|
+
: [],
|
|
214
|
+
},
|
|
215
|
+
]),
|
|
216
|
+
);
|
|
217
|
+
writeJsonAtomic(targetPath, {
|
|
218
|
+
...payload,
|
|
219
|
+
components,
|
|
220
|
+
});
|
|
221
|
+
return targetPath;
|
|
222
|
+
}
|
|
201
223
|
fs.copyFileSync(sourcePath, targetPath);
|
|
202
224
|
return targetPath;
|
|
203
225
|
}
|