@cohortapp/agent-sdk 2.4.0 → 2.5.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/bin/maestro.mjs +9 -0
- package/lib/backlog.mjs +35 -0
- package/lib/backlog.test.mjs +36 -0
- package/lib/channels/contract.mjs +1 -0
- package/lib/channels/contract.test.mjs +2 -1
- package/lib/channels/inbox-item.mjs +54 -0
- package/lib/comms/send-gate.mjs +56 -1
- package/lib/comms/send-gate.test.mjs +56 -0
- package/lib/execution/disposition.mjs +62 -2
- package/lib/execution/disposition.test.mjs +54 -0
- package/lib/execution/drive.mjs +1 -1
- package/lib/execution/effects.mjs +282 -24
- package/lib/execution/effects.test.mjs +112 -0
- package/lib/execution/index.mjs +1 -0
- package/lib/execution/intake.mjs +43 -9
- package/lib/execution/intake.test.mjs +46 -0
- package/lib/execution/pipeline.mjs +5 -0
- package/lib/execution/surface-policy.mjs +80 -30
- package/lib/goals/classify.mjs +49 -5
- package/lib/goals/classify.test.mjs +58 -0
- package/lib/goals/collaborate.mjs +131 -17
- package/lib/goals/collaborate.test.mjs +16 -4
- package/lib/goals/loop.mjs +160 -9
- package/lib/goals/loop.test.mjs +129 -3
- package/lib/kpi-sensors.mjs +666 -0
- package/lib/kpi-sensors.test.mjs +275 -0
- package/lib/kpi.mjs +23 -0
- package/lib/mandate/audit.mjs +3 -0
- package/lib/mandate/contract.mjs +277 -0
- package/lib/mandate/contract.test.mjs +185 -0
- package/lib/mandate/derive.mjs +49 -5
- package/lib/mandate/derive.test.mjs +7 -1
- package/lib/mandate/model.mjs +10 -1
- package/lib/mandate/model.test.mjs +22 -3
- package/lib/mandate/refresh.mjs +53 -5
- package/lib/mandate/refresh.test.mjs +83 -1
- package/lib/org/doctor.mjs +66 -0
- package/lib/org/doctor.test.mjs +73 -1
- package/lib/org/inbound/directedness.mjs +119 -1
- package/lib/org/inbound/directedness.test.mjs +67 -0
- package/lib/org/inbound/facts.mjs +132 -9
- package/lib/org/inbound/facts.test.mjs +96 -0
- package/lib/org/inbound/hydrate.mjs +40 -0
- package/lib/org/inbound/index.test.mjs +83 -0
- package/lib/org/inbound/project.mjs +8 -0
- package/lib/org/inbound/surfaces.mjs +20 -0
- package/lib/org/param-contract.mjs +16 -2
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +214 -2
- package/lib/org/protocol.test.mjs +11 -2
- package/lib/org/push.mjs +213 -49
- package/lib/org/push.test.mjs +112 -10
- package/lib/plan/compile.mjs +85 -8
- package/lib/plan/compile.test.mjs +82 -0
- package/lib/plan/emit.test.mjs +6 -1
- package/lib/setup/enroll-from-cohort.mjs +22 -2
- package/lib/setup/enroll-from-cohort.test.mjs +25 -0
- package/lib/setup/sections/mandate.mjs +43 -1
- package/lib/subagents/schema.mjs +14 -2
- package/lib/subagents/schema.test.mjs +22 -0
- package/package.json +1 -1
- package/scripts/ci/check-subagent-frontmatter.mjs +139 -0
- package/scripts/ci/check-subagent-frontmatter.test.mjs +124 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/ci/conformance-org-api.mjs +16 -0
- package/scripts/ci/journey-approval-escalation.mjs +341 -0
- package/scripts/daemon/agent-daemon.mjs +582 -28
- package/scripts/daemon/cadence-handlers.mjs +273 -17
- package/scripts/daemon/cadence-handlers.test.mjs +101 -0
- package/scripts/daemon/execution-ladder.test.mjs +430 -0
- package/scripts/daemon/goal-steward-cadence.test.mjs +69 -0
- package/scripts/daemon/maestro-daemon.mjs +53 -0
- package/scripts/daemon/prompt-builder.mjs +47 -0
- package/scripts/daemon/responder.mjs +70 -3
- package/scripts/poller/imap-client.mjs +20 -1
- package/scripts/poller/inbox-scan-poller.mjs +15 -0
- package/scripts/poller/utils.mjs +51 -0
- package/scripts/setup/generate-capability.mjs +120 -11
- package/scripts/setup/generate-capability.test.mjs +134 -0
- package/scripts/setup/generate-plan.mjs +6 -1
- package/scripts/setup/repair-subagent-frontmatter.mjs +231 -0
package/lib/goals/loop.mjs
CHANGED
|
@@ -52,6 +52,7 @@ import { recordDecision } from "../mandate/audit.mjs";
|
|
|
52
52
|
import {
|
|
53
53
|
readSeries,
|
|
54
54
|
recordMeasurement,
|
|
55
|
+
recordIntervention,
|
|
55
56
|
readInterventions,
|
|
56
57
|
measureKpi,
|
|
57
58
|
gapFor,
|
|
@@ -133,13 +134,56 @@ export function appendBacklogItems(agentRoot, items, deps = {}) {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* The queue the EXECUTION LADDER writes into when it decides a directed event is
|
|
139
|
+
* work rather than conversation (`lib/execution/effects.scheduleToQueue`).
|
|
140
|
+
*
|
|
141
|
+
* It is read here, and not only in the backlog sweep, because reactive work and
|
|
142
|
+
* self-directed work advance the SAME objectives and must be scored the same
|
|
143
|
+
* way. Reading only `goals.yaml` meant a board item the ladder queued could
|
|
144
|
+
* never be counted as an intervention: `journalRealizedDeltas` iterates
|
|
145
|
+
* `openItemsFor`, so an item outside that view never reached
|
|
146
|
+
* `lib/kpi.recordIntervention` and the agent could not learn from anything it
|
|
147
|
+
* did REACTIVELY — only from what it planned for itself.
|
|
148
|
+
*/
|
|
149
|
+
export const INBOUND_QUEUE_REL = join("state", "queues", "inbound.yaml");
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Open items already on an objective, from BOTH work queues (goal-steward and
|
|
153
|
+
* execution-ladder). Never throws.
|
|
154
|
+
*
|
|
155
|
+
* Rows are selected by their `advances: [...]` list, so a queue row that names
|
|
156
|
+
* no objective is correctly invisible here whichever file it came from.
|
|
157
|
+
*/
|
|
137
158
|
export function openItemsFor(agentRoot, objectiveKey) {
|
|
138
|
-
const
|
|
139
|
-
if (!existsSync(path)) return [];
|
|
140
|
-
let body;
|
|
141
|
-
try { body = readFileSync(path, "utf-8"); } catch { return []; }
|
|
159
|
+
const root = resolveAgentRoot(agentRoot);
|
|
142
160
|
const out = [];
|
|
161
|
+
for (const rel of [GOALS_QUEUE_REL, INBOUND_QUEUE_REL]) {
|
|
162
|
+
collectOpenItems(join(root, rel), objectiveKey, out);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Strip a YAML scalar's surrounding quotes.
|
|
169
|
+
*
|
|
170
|
+
* The two writers that now feed this reader disagree: `renderQueueItems` above
|
|
171
|
+
* emits bare scalars (`id: inb-x`), `lib/execution/effects.scheduleToQueue`
|
|
172
|
+
* emits quoted ones (`id: "inb-x"`) because its ids contain `#` and `.`. The
|
|
173
|
+
* `(\S+)` captures include the quotes, and the id is used as the intervention's
|
|
174
|
+
* `taskId` AND as half of the `${taskId}::${window}` dedupe key — so an unstripped
|
|
175
|
+
* quote would both corrupt the ledger row and defeat the dedupe.
|
|
176
|
+
*/
|
|
177
|
+
function unquote(v) {
|
|
178
|
+
const s = String(v == null ? "" : v).trim();
|
|
179
|
+
return s.replace(/^["']/, "").replace(/["']$/, "");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Parse one queue file's open items into `out`. Never throws. */
|
|
183
|
+
function collectOpenItems(path, objectiveKey, out) {
|
|
184
|
+
if (!existsSync(path)) return;
|
|
185
|
+
let body;
|
|
186
|
+
try { body = readFileSync(path, "utf-8"); } catch { return; }
|
|
143
187
|
for (const block of body.split(/^\s*-\s+/m).slice(1)) {
|
|
144
188
|
const status = block.match(/^\s*status:\s*(\S+)/m);
|
|
145
189
|
if (!status || !["open", "in_progress"].includes(status[1])) continue;
|
|
@@ -148,9 +192,98 @@ export function openItemsFor(agentRoot, objectiveKey) {
|
|
|
148
192
|
if (objectiveKey && !list.includes(objectiveKey)) continue;
|
|
149
193
|
const title = block.match(/^\s*title:\s*"?(.+?)"?\s*$/m);
|
|
150
194
|
const id = block.match(/^\s*id:\s*(\S+)/m);
|
|
151
|
-
|
|
195
|
+
// `expected_delta` and `obligation_key` are not decoration: they are what
|
|
196
|
+
// `journalRealizedDeltas` scores the intervention AGAINST. Dropping them here
|
|
197
|
+
// is why the intervention ledger stayed empty — the loop could see that an
|
|
198
|
+
// item was open but not what it had promised to move.
|
|
199
|
+
const expected = block.match(/^\s*expected_delta:\s*(\S+)/m);
|
|
200
|
+
const obligation = block.match(/^\s*obligation_key:\s*(\S+)/m);
|
|
201
|
+
out.push({
|
|
202
|
+
id: id ? unquote(id[1]) : null,
|
|
203
|
+
title: title ? title[1] : "",
|
|
204
|
+
advances: list.map(unquote),
|
|
205
|
+
status: status[1],
|
|
206
|
+
expectedDelta: expected && expected[1] !== "null" ? Number(expected[1]) : null,
|
|
207
|
+
obligationKey: obligation && obligation[1] !== "null" ? unquote(obligation[1]) : null,
|
|
208
|
+
});
|
|
152
209
|
}
|
|
153
|
-
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// The learning signal
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Score every in-flight intervention against the sample that just landed.
|
|
218
|
+
*
|
|
219
|
+
* `lib/kpi.recordIntervention` had ZERO callers, so `readInterventions` (which
|
|
220
|
+
* step 4 already consults, and which `defaultProposeCandidates` counts prior
|
|
221
|
+
* failures from) always read an empty file. The loop could plan but never learn:
|
|
222
|
+
* an intervention that made a number WORSE was proposed again, identically,
|
|
223
|
+
* forever.
|
|
224
|
+
*
|
|
225
|
+
* The delta is measured on the GAP, not the raw value, so `direction` is
|
|
226
|
+
* honoured: positive `realizedDelta` = the gap closed, whatever way the metric
|
|
227
|
+
* points. Idempotent on `(taskId, window)` — a second steward pass inside the
|
|
228
|
+
* same measurement window re-scores nothing.
|
|
229
|
+
*
|
|
230
|
+
* @param {object} o - { agentRoot, objective, seriesBefore, seriesAfter, window, openItems }
|
|
231
|
+
* @param {object} [deps] - { now, log }
|
|
232
|
+
* @returns {{recorded:number, skipped:number, reason:string, rows:object[]}}
|
|
233
|
+
*/
|
|
234
|
+
export function journalRealizedDeltas(o = {}, deps = {}) {
|
|
235
|
+
const log = logOf(deps);
|
|
236
|
+
const { agentRoot, objective, seriesBefore, seriesAfter, window } = o;
|
|
237
|
+
const openItems = Array.isArray(o.openItems) ? o.openItems : [];
|
|
238
|
+
if (openItems.length === 0) {
|
|
239
|
+
return { recorded: 0, skipped: 0, reason: "no in-flight interventions to score", rows: [] };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const before = gapFor(objective, seriesBefore || [], deps);
|
|
243
|
+
const after = gapFor(objective, seriesAfter || [], deps);
|
|
244
|
+
if (before.gap == null || after.gap == null) {
|
|
245
|
+
log(
|
|
246
|
+
"info",
|
|
247
|
+
`[goals] "${objective.key}": a sample landed for ${window} but there is no PRIOR measured gap — the in-flight work cannot be scored yet (this is the first data point, not a failure)`
|
|
248
|
+
);
|
|
249
|
+
return { recorded: 0, skipped: openItems.length, reason: "no prior gap", rows: [] };
|
|
250
|
+
}
|
|
251
|
+
if (before.window === after.window) {
|
|
252
|
+
return { recorded: 0, skipped: openItems.length, reason: "same window — nothing new to score", rows: [] };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const realizedDelta = +(before.gap - after.gap).toFixed(6);
|
|
256
|
+
const priors = readInterventions(agentRoot, objective.key, 0);
|
|
257
|
+
const already = new Set(priors.map((p) => `${p.taskId}::${p.window}`));
|
|
258
|
+
|
|
259
|
+
const rows = [];
|
|
260
|
+
let skipped = 0;
|
|
261
|
+
for (const item of openItems) {
|
|
262
|
+
if (already.has(`${item.id}::${window}`)) { skipped += 1; continue; }
|
|
263
|
+
const res = recordIntervention(
|
|
264
|
+
agentRoot,
|
|
265
|
+
{
|
|
266
|
+
objectiveKey: objective.key,
|
|
267
|
+
taskId: item.id,
|
|
268
|
+
obligationKey: item.obligationKey || `outcome.${objective.key}`,
|
|
269
|
+
expectedDelta: item.expectedDelta,
|
|
270
|
+
realizedDelta,
|
|
271
|
+
measuredAt: after.window,
|
|
272
|
+
window,
|
|
273
|
+
basis: `gap ${before.gap} (${before.window}) → ${after.gap} (${after.window})`,
|
|
274
|
+
},
|
|
275
|
+
deps
|
|
276
|
+
);
|
|
277
|
+
if (res.recorded) rows.push(res.record);
|
|
278
|
+
}
|
|
279
|
+
if (rows.length > 0) {
|
|
280
|
+
const verdict = realizedDelta > 0 ? "closed" : realizedDelta < 0 ? "WIDENED" : "did not move";
|
|
281
|
+
log(
|
|
282
|
+
"info",
|
|
283
|
+
`[goals] "${objective.key}": ${rows.length} in-flight intervention(s) scored — the gap ${verdict} by ${Math.abs(realizedDelta)} between ${before.window} and ${after.window}`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return { recorded: rows.length, skipped, reason: "scored", rows };
|
|
154
287
|
}
|
|
155
288
|
|
|
156
289
|
// ---------------------------------------------------------------------------
|
|
@@ -241,6 +374,7 @@ export async function runGoalSteward(deps = {}) {
|
|
|
241
374
|
staleness: null,
|
|
242
375
|
validation: null,
|
|
243
376
|
measured: [],
|
|
377
|
+
interventions: [],
|
|
244
378
|
gaps: [],
|
|
245
379
|
decisions: [],
|
|
246
380
|
candidates: 0,
|
|
@@ -303,6 +437,9 @@ export async function runGoalSteward(deps = {}) {
|
|
|
303
437
|
const proposedRows = [];
|
|
304
438
|
|
|
305
439
|
for (const objective of objectives) {
|
|
440
|
+
// Hoisted above MEASURE: the in-flight items are what a new sample SCORES.
|
|
441
|
+
const openItems = openItemsFor(agentRoot, objective.key);
|
|
442
|
+
|
|
306
443
|
// --- 1. MEASURE --------------------------------------------------------
|
|
307
444
|
let series = readSeries(agentRoot, objective.key);
|
|
308
445
|
const due = isDue(objective, series, deps);
|
|
@@ -320,7 +457,19 @@ export async function runGoalSteward(deps = {}) {
|
|
|
320
457
|
try { await deps.mirrorSample({ objective, value: m.value, window: due.window, source: m.source, evidence: m.evidence }); }
|
|
321
458
|
catch (err) { log("warn", `[goal-steward] mandate.sample mirror failed for "${objective.key}": ${err && err.message ? err.message : err}`); }
|
|
322
459
|
}
|
|
460
|
+
const seriesBefore = series;
|
|
323
461
|
series = readSeries(agentRoot, objective.key);
|
|
462
|
+
// --- 1b. LEARN: score the in-flight work against the new number ----
|
|
463
|
+
const scored = journalRealizedDeltas(
|
|
464
|
+
{ agentRoot, objective, seriesBefore, seriesAfter: series, window: due.window, openItems },
|
|
465
|
+
{ now: deps.now, log }
|
|
466
|
+
);
|
|
467
|
+
if (scored.recorded > 0) {
|
|
468
|
+
report.interventions.push({ objectiveKey: objective.key, window: due.window, ...scored, rows: undefined });
|
|
469
|
+
for (const row of scored.rows) {
|
|
470
|
+
audit({ decision: "intervention_scored", objectiveKey: objective.key, obligationKey: row.obligationKey, detail: row });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
324
473
|
} else {
|
|
325
474
|
report.measured.push({ objectiveKey: objective.key, ok: false, reason: m.reason });
|
|
326
475
|
report.drift.push({ kind: m.reason === "unreachable-capability" ? "unreachable_capability" : "stale_sensor", key: objective.key, reason: m.reason });
|
|
@@ -334,7 +483,6 @@ export async function runGoalSteward(deps = {}) {
|
|
|
334
483
|
audit({ decision: "gap", objectiveKey: objective.key, detail: gap });
|
|
335
484
|
|
|
336
485
|
// --- 3. DECIDE ---------------------------------------------------------
|
|
337
|
-
const openItems = openItemsFor(agentRoot, objective.key);
|
|
338
486
|
const decision = decideForGap(gap, { coverage: openItems.length });
|
|
339
487
|
report.decisions.push({ objectiveKey: objective.key, ...decision, coverage: openItems.length });
|
|
340
488
|
|
|
@@ -356,7 +504,9 @@ export async function runGoalSteward(deps = {}) {
|
|
|
356
504
|
];
|
|
357
505
|
audit({ decision: "candidate_rejected", objectiveKey: objective.key, detail: { gate: "trend", reason: decision.reason, escalating: true } });
|
|
358
506
|
if (enforcement === "active") {
|
|
359
|
-
|
|
507
|
+
// `agentRoot` is load-bearing, not decoration: it is how the escalation
|
|
508
|
+
// primitive resolves this seat's org credential (collaborate.orgConn).
|
|
509
|
+
const esc = await raiseEscalation({ objectiveKey: objective.key, question, options, gap }, { ...deps, agentRoot, memberId, log });
|
|
360
510
|
report.escalations.push({ objectiveKey: objective.key, ...esc });
|
|
361
511
|
audit({ decision: "routed", objectiveKey: objective.key, detail: { primitive: esc.method || "escalation.create", ok: esc.ok, reason: esc.reason } });
|
|
362
512
|
} else {
|
|
@@ -532,6 +682,7 @@ export default {
|
|
|
532
682
|
renderQueueItems,
|
|
533
683
|
appendBacklogItems,
|
|
534
684
|
openItemsFor,
|
|
685
|
+
journalRealizedDeltas,
|
|
535
686
|
defaultProposeCandidates,
|
|
536
687
|
runGoalSteward,
|
|
537
688
|
};
|
package/lib/goals/loop.test.mjs
CHANGED
|
@@ -29,11 +29,13 @@ import {
|
|
|
29
29
|
renderQueueItems,
|
|
30
30
|
appendBacklogItems,
|
|
31
31
|
openItemsFor,
|
|
32
|
+
journalRealizedDeltas,
|
|
32
33
|
defaultProposeCandidates,
|
|
33
34
|
GOALS_QUEUE_REL,
|
|
34
35
|
PROPOSED_REL,
|
|
35
36
|
} from "./loop.mjs";
|
|
36
37
|
import { parseQueueItems } from "../backlog.mjs";
|
|
38
|
+
import { readInterventions } from "../kpi.mjs";
|
|
37
39
|
|
|
38
40
|
// ---------------------------------------------------------------------------
|
|
39
41
|
// Fixtures
|
|
@@ -342,10 +344,14 @@ test("enforcement:active turns a measured gap into a routed, provenance-linked b
|
|
|
342
344
|
assert.equal(calls.board.length, 1);
|
|
343
345
|
const why = calls.board[0].why;
|
|
344
346
|
assert.equal(why.reason, "kpi-gap");
|
|
345
|
-
assert.equal(why.objectiveKey, "pipeline-coverage");
|
|
346
347
|
assert.equal(why.clause, "cs_12");
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
// hq's `why` block is CLOSED ({reason, options, clause, wouldChange}); the
|
|
349
|
+
// keys it has no column for ride in `detail` rather than being sent and
|
|
350
|
+
// silently dropped — or, as before this fix, rejected BAD_REQUEST wholesale.
|
|
351
|
+
const detail = calls.board[0].detail;
|
|
352
|
+
assert.match(detail, /Advances objective: pipeline-coverage/);
|
|
353
|
+
assert.match(detail, /Obligation: outcome\.pipeline-coverage/);
|
|
354
|
+
assert.equal(calls.board[0].assigneeId, SELF);
|
|
349
355
|
|
|
350
356
|
// And the local queue item the backlog-executor will sweep.
|
|
351
357
|
const parsed = parseQueueItems(readFileSync(join(root, GOALS_QUEUE_REL), "utf-8"), "goals.yaml");
|
|
@@ -717,3 +723,123 @@ test("WIP cap: a second run does not pile more work onto the same objective", as
|
|
|
717
723
|
assert.notEqual(second.decisions[0].action, "create");
|
|
718
724
|
} finally { cleanup(root); }
|
|
719
725
|
});
|
|
726
|
+
|
|
727
|
+
// ---------------------------------------------------------------------------
|
|
728
|
+
// THE LEARNING SIGNAL — recordIntervention had zero callers
|
|
729
|
+
// ---------------------------------------------------------------------------
|
|
730
|
+
|
|
731
|
+
test("openItemsFor carries the expected delta and obligation an intervention is scored against", () => {
|
|
732
|
+
const root = makeRoot();
|
|
733
|
+
try {
|
|
734
|
+
appendBacklogItems(root, [{
|
|
735
|
+
id: "goal-1", title: "Close the gap", status: "open", priority: "high",
|
|
736
|
+
next_action: "Close the gap", advances: ["pipeline-coverage"],
|
|
737
|
+
expected_delta: 0.75, obligation_key: "outcome.pipeline-coverage",
|
|
738
|
+
disposition: "SELF", rung: 3, why: { reason: "kpi-gap" }, created: "2026-08-11",
|
|
739
|
+
}]);
|
|
740
|
+
const [item] = openItemsFor(root, "pipeline-coverage");
|
|
741
|
+
assert.equal(item.id, "goal-1");
|
|
742
|
+
assert.equal(item.expectedDelta, 0.75);
|
|
743
|
+
assert.equal(item.obligationKey, "outcome.pipeline-coverage");
|
|
744
|
+
} finally { cleanup(root); }
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("a new sample SCORES the work in flight — the realized delta lands in the ledger", () => {
|
|
748
|
+
const root = makeRoot();
|
|
749
|
+
try {
|
|
750
|
+
const objective = { key: "pipeline-coverage", target: 3, direction: "up", tolerance: 0.2, cadence: "weekly" };
|
|
751
|
+
const before = [{ window: "2026-W32", value: 1.5, source: "method", at: "2026-08-04T09:00:00.000Z" }];
|
|
752
|
+
const after = [...before, { window: "2026-W33", value: 2.5, source: "method", at: "2026-08-11T09:00:00.000Z" }];
|
|
753
|
+
const openItems = [{ id: "goal-1", expectedDelta: 0.75, obligationKey: "outcome.pipeline-coverage" }];
|
|
754
|
+
|
|
755
|
+
const r = journalRealizedDeltas(
|
|
756
|
+
{ agentRoot: root, objective, seriesBefore: before, seriesAfter: after, window: "2026-W33", openItems },
|
|
757
|
+
{ now: () => NOW },
|
|
758
|
+
);
|
|
759
|
+
assert.equal(r.recorded, 1);
|
|
760
|
+
// gap 1.5 → 0.5: the gap CLOSED by 1.0, whatever way the metric points.
|
|
761
|
+
assert.equal(r.rows[0].realizedDelta, 1);
|
|
762
|
+
assert.equal(r.rows[0].expectedDelta, 0.75);
|
|
763
|
+
assert.equal(r.rows[0].taskId, "goal-1");
|
|
764
|
+
assert.equal(r.rows[0].window, "2026-W33");
|
|
765
|
+
|
|
766
|
+
// Idempotent on (taskId, window): a second pass in the same window re-scores nothing.
|
|
767
|
+
const again = journalRealizedDeltas(
|
|
768
|
+
{ agentRoot: root, objective, seriesBefore: before, seriesAfter: after, window: "2026-W33", openItems },
|
|
769
|
+
{ now: () => NOW },
|
|
770
|
+
);
|
|
771
|
+
assert.equal(again.recorded, 0);
|
|
772
|
+
assert.equal(readInterventions(root, "pipeline-coverage", 0).length, 1);
|
|
773
|
+
} finally { cleanup(root); }
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("an intervention that made the number WORSE is recorded as a negative delta", () => {
|
|
777
|
+
const root = makeRoot();
|
|
778
|
+
try {
|
|
779
|
+
const objective = { key: "pipeline-coverage", target: 3, direction: "up", tolerance: 0.2, cadence: "weekly" };
|
|
780
|
+
const r = journalRealizedDeltas(
|
|
781
|
+
{
|
|
782
|
+
agentRoot: root, objective,
|
|
783
|
+
seriesBefore: [{ window: "2026-W32", value: 2.5, source: "method", at: "2026-08-04T09:00:00.000Z" }],
|
|
784
|
+
seriesAfter: [{ window: "2026-W32", value: 2.5, source: "method", at: "2026-08-04T09:00:00.000Z" },
|
|
785
|
+
{ window: "2026-W33", value: 1.5, source: "method", at: "2026-08-11T09:00:00.000Z" }],
|
|
786
|
+
window: "2026-W33",
|
|
787
|
+
openItems: [{ id: "goal-1", expectedDelta: 0.75 }],
|
|
788
|
+
},
|
|
789
|
+
{ now: () => NOW },
|
|
790
|
+
);
|
|
791
|
+
assert.equal(r.rows[0].realizedDelta, -1);
|
|
792
|
+
// …and the planner sees it, which is the whole point of the ledger.
|
|
793
|
+
const priors = readInterventions(root, "pipeline-coverage", 3);
|
|
794
|
+
const candidates = defaultProposeCandidates({ objective, gap: { gap: 1.5, trend: "worsening", value: 1.5, target: 3 }, interventions: priors });
|
|
795
|
+
assert.equal(candidates[0].why.priorFailedInterventions, 1);
|
|
796
|
+
assert.equal(candidates[0].need.openEnded, true, "a repeat failure must stop being planned as a one-shot");
|
|
797
|
+
} finally { cleanup(root); }
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
test("the FIRST data point scores nothing — there is no prior gap, and that is not a failure", () => {
|
|
801
|
+
const root = makeRoot();
|
|
802
|
+
const logs = [];
|
|
803
|
+
try {
|
|
804
|
+
const r = journalRealizedDeltas(
|
|
805
|
+
{
|
|
806
|
+
agentRoot: root,
|
|
807
|
+
objective: { key: "pipeline-coverage", target: 3, direction: "up", cadence: "weekly" },
|
|
808
|
+
seriesBefore: [],
|
|
809
|
+
seriesAfter: [{ window: "2026-W33", value: 1.5, source: "method", at: "2026-08-11T09:00:00.000Z" }],
|
|
810
|
+
window: "2026-W33",
|
|
811
|
+
openItems: [{ id: "goal-1", expectedDelta: 0.75 }],
|
|
812
|
+
},
|
|
813
|
+
{ now: () => NOW, log: (l, m) => logs.push(`${l}:${m}`) },
|
|
814
|
+
);
|
|
815
|
+
assert.equal(r.recorded, 0);
|
|
816
|
+
assert.equal(r.reason, "no prior gap");
|
|
817
|
+
assert.ok(logs.some((l) => /first data point, not a failure/.test(l)), "the skip must be logged, not silent");
|
|
818
|
+
assert.equal(readInterventions(root, "pipeline-coverage", 0).length, 0);
|
|
819
|
+
} finally { cleanup(root); }
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("the loop journals interventions on its own — end to end through runGoalSteward", async () => {
|
|
823
|
+
const root = makeRoot();
|
|
824
|
+
try {
|
|
825
|
+
seedSeries(root, "pipeline-coverage", [
|
|
826
|
+
{ window: "2026-W32", value: 1.5, source: "method", at: "2026-08-04T09:00:00.000Z" },
|
|
827
|
+
]);
|
|
828
|
+
appendBacklogItems(root, [{
|
|
829
|
+
id: "goal-prior", title: "A prior intervention", status: "open", priority: "normal",
|
|
830
|
+
next_action: "…", advances: ["pipeline-coverage"], expected_delta: 0.75,
|
|
831
|
+
obligation_key: "outcome.pipeline-coverage", disposition: "SELF", rung: 3,
|
|
832
|
+
why: { reason: "kpi-gap" }, created: "2026-08-04",
|
|
833
|
+
}]);
|
|
834
|
+
|
|
835
|
+
const { deps } = spyDeps({ sensors: sensors(2.5) });
|
|
836
|
+
const r = await runGoalSteward({ ...deps, agentRoot: root, cacheRecord: mandate(), enforcement: "active" });
|
|
837
|
+
|
|
838
|
+
assert.equal(r.interventions.length, 1);
|
|
839
|
+
assert.equal(r.interventions[0].recorded, 1);
|
|
840
|
+
const rows = readInterventions(root, "pipeline-coverage", 0);
|
|
841
|
+
assert.equal(rows.length, 1);
|
|
842
|
+
assert.equal(rows[0].taskId, "goal-prior");
|
|
843
|
+
assert.equal(rows[0].realizedDelta, 1);
|
|
844
|
+
} finally { cleanup(root); }
|
|
845
|
+
});
|