@cohortapp/agent-sdk 2.3.2 → 2.4.1
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/framework-features.json +30 -0
- package/lib/backlog.mjs +136 -0
- package/lib/cadences.mjs +63 -2
- package/lib/cadences.test.mjs +105 -0
- package/lib/capability/inventory.mjs +542 -0
- package/lib/capability/inventory.test.mjs +232 -0
- package/lib/capability/probe.mjs +255 -0
- package/lib/channels/contract.mjs +37 -1
- package/lib/channels/contract.test.mjs +25 -1
- package/lib/claude-bin.mjs +37 -3
- package/lib/claude-bin.test.mjs +42 -8
- package/lib/execution/disposition.mjs +501 -0
- package/lib/execution/disposition.test.mjs +482 -0
- package/lib/execution/drive.mjs +352 -0
- package/lib/execution/drive.test.mjs +270 -0
- package/lib/execution/effects.mjs +340 -0
- package/lib/execution/effects.test.mjs +193 -0
- package/lib/execution/index.mjs +152 -0
- package/lib/execution/intake.mjs +581 -0
- package/lib/execution/intake.test.mjs +343 -0
- package/lib/execution/journal.mjs +374 -0
- package/lib/execution/journal.test.mjs +261 -0
- package/lib/execution/match.mjs +331 -0
- package/lib/execution/match.test.mjs +235 -0
- package/lib/execution/pipeline.mjs +341 -0
- package/lib/execution/pipeline.test.mjs +389 -0
- package/lib/execution/route.mjs +332 -0
- package/lib/execution/route.test.mjs +186 -0
- package/lib/execution/surface-policy.mjs +446 -0
- package/lib/execution/surface-policy.test.mjs +162 -0
- package/lib/goals/admission.mjs +209 -0
- package/lib/goals/admission.test.mjs +139 -0
- package/lib/goals/classify.mjs +206 -0
- package/lib/goals/classify.test.mjs +109 -0
- package/lib/goals/collaborate.mjs +415 -0
- package/lib/goals/collaborate.test.mjs +324 -0
- package/lib/goals/gaps.mjs +111 -0
- package/lib/goals/gaps.test.mjs +284 -0
- package/lib/goals/loop.mjs +537 -0
- package/lib/goals/loop.test.mjs +719 -0
- package/lib/identity/persona.mjs +247 -0
- package/lib/identity/persona.test.mjs +117 -0
- package/lib/kpi.mjs +469 -0
- package/lib/kpi.test.mjs +244 -0
- package/lib/mandate/audit.mjs +168 -0
- package/lib/mandate/audit.test.mjs +195 -0
- package/lib/mandate/cache.mjs +162 -0
- package/lib/mandate/derive.mjs +317 -0
- package/lib/mandate/derive.test.mjs +224 -0
- package/lib/mandate/model.mjs +352 -0
- package/lib/mandate/model.test.mjs +145 -0
- package/lib/mandate/refresh.mjs +187 -0
- package/lib/mandate/refresh.test.mjs +293 -0
- package/lib/mcp/server.test.mjs +4 -4
- package/lib/org/approvals.mjs +14 -2
- package/lib/org/client.mjs +58 -22
- package/lib/org/client.test.mjs +3 -1
- package/lib/org/inbound/directedness.mjs +720 -0
- package/lib/org/inbound/directedness.test.mjs +543 -0
- package/lib/org/inbound/facts.mjs +501 -0
- package/lib/org/inbound/facts.test.mjs +375 -0
- package/lib/org/inbound/hydrate.mjs +535 -0
- package/lib/org/inbound/hydrate.test.mjs +326 -0
- package/lib/org/inbound/index.mjs +233 -0
- package/lib/org/inbound/index.test.mjs +324 -0
- package/lib/org/inbound/io.mjs +141 -0
- package/lib/org/inbound/project.mjs +201 -0
- package/lib/org/inbound/project.test.mjs +287 -0
- package/lib/org/inbound/surfaces.mjs +257 -0
- package/lib/org/knowledge.mjs +10 -1
- package/lib/org/knowledge.test.mjs +8 -1
- package/lib/org/leases.mjs +5 -0
- package/lib/org/mesh.mjs +17 -2
- package/lib/org/messaging.mjs +40 -4
- package/lib/org/messaging.test.mjs +40 -0
- package/lib/org/param-contract.mjs +694 -0
- package/lib/org/param-contract.test.mjs +451 -0
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +8 -0
- package/lib/org/protocol.test.mjs +5 -1
- package/lib/org/push.mjs +1025 -0
- package/lib/org/push.test.mjs +690 -0
- package/lib/org/tool-surface.mjs +138 -38
- package/lib/org/tool-surface.test.mjs +13 -8
- package/lib/org/typing.mjs +341 -0
- package/lib/org/typing.test.mjs +291 -0
- package/lib/plan/compile.mjs +510 -0
- package/lib/plan/compile.test.mjs +286 -0
- package/lib/plan/emit.mjs +256 -0
- package/lib/plan/emit.test.mjs +246 -0
- package/lib/plan/explain.mjs +226 -0
- package/lib/plan/explain.test.mjs +188 -0
- package/lib/plan/schema.mjs +140 -0
- package/lib/resource-governor.mjs +47 -1
- package/lib/resource-governor.test.mjs +21 -1
- package/lib/setup/enroll-from-cohort.mjs +105 -17
- package/lib/setup/enroll-from-cohort.test.mjs +68 -1
- package/lib/setup/sections/identity.mjs +15 -4
- package/lib/setup/sections/identity.test.mjs +94 -0
- package/lib/setup/sections/inventory.mjs +178 -0
- package/lib/setup/sections/inventory.test.mjs +198 -0
- package/lib/setup/sections/mandate.mjs +392 -0
- package/lib/setup/sections/mandate.test.mjs +373 -0
- package/lib/setup/sections/subagents.mjs +427 -0
- package/lib/setup/sections/subagents.test.mjs +429 -0
- package/lib/setup/sections/verify.mjs +121 -0
- package/lib/setup/sections/verify.test.mjs +175 -0
- package/lib/setup/sot.mjs +2 -0
- package/lib/subagents/cli.mjs +463 -0
- package/lib/subagents/cli.test.mjs +389 -0
- package/lib/subagents/client.mjs +373 -0
- package/lib/subagents/client.test.mjs +309 -0
- package/lib/subagents/gap.mjs +268 -0
- package/lib/subagents/gap.test.mjs +234 -0
- package/lib/subagents/lock.mjs +296 -0
- package/lib/subagents/lock.test.mjs +248 -0
- package/lib/subagents/manifest.mjs +224 -0
- package/lib/subagents/manifest.test.mjs +175 -0
- package/lib/subagents/refs.mjs +274 -0
- package/lib/subagents/refs.test.mjs +204 -0
- package/lib/subagents/resolve.mjs +455 -0
- package/lib/subagents/resolve.test.mjs +422 -0
- package/lib/subagents/schema.mjs +467 -0
- package/lib/subagents/schema.test.mjs +306 -0
- package/package.json +8 -3
- package/plugins/maestro-skills/.claude-plugin/marketplace.json +16 -0
- package/policies/ai-disclosure.yaml +42 -2
- package/scaffold/CLAUDE.md +16 -2
- package/schedules/triggers/goal-steward.md +79 -0
- package/scripts/ci/conformance-org-api.mjs +792 -0
- package/scripts/ci/conformance-org-api.test.mjs +417 -0
- package/scripts/daemon/agent-daemon.mjs +36 -4
- package/scripts/daemon/cadence-handlers.mjs +145 -1
- package/scripts/daemon/goal-steward-cadence.test.mjs +243 -0
- package/scripts/daemon/inbox-deferral.mjs +45 -2
- package/scripts/daemon/inbox-deferral.test.mjs +56 -0
- package/scripts/daemon/inbox-wake.mjs +282 -0
- package/scripts/daemon/inbox-wake.test.mjs +199 -0
- package/scripts/daemon/prompt-builder.mjs +41 -1
- package/scripts/daemon/typing-registry.mjs +55 -2
- package/scripts/daemon/typing-registry.test.mjs +25 -0
- package/scripts/local-triggers/generate-plists.test.mjs +5 -5
- package/scripts/setup/gen-subagent-manifest.mjs +95 -0
- package/scripts/setup/gen-subagent-manifest.test.mjs +124 -0
- package/scripts/setup/generate-plan.mjs +108 -0
- package/scripts/setup/init-capability-manifest.mjs +70 -0
- package/scripts/setup/init-skill-marketplace.mjs +155 -0
- package/scripts/setup/init-skill-marketplace.test.mjs +193 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/goals/loop.mjs — the reconciliation loop: a KPI gap becomes real backlog.
|
|
3
|
+
*
|
|
4
|
+
* This is the third runtime loop (Loop C in the spec): not reactive, not
|
|
5
|
+
* scheduled-because-a-plist-fired, but **self-directed** — the agent measures
|
|
6
|
+
* what it is accountable for, notices where it is short, and produces concrete,
|
|
7
|
+
* provenance-linked work. It is what separates "an agent with a charter" from
|
|
8
|
+
* "an agent that pursues its charter".
|
|
9
|
+
*
|
|
10
|
+
* The nine steps, each auditable:
|
|
11
|
+
*
|
|
12
|
+
* 1. MEASURE every adopted objective whose measurement window is open,
|
|
13
|
+
* through its manifest-declared sensor (lib/kpi.measureKpi).
|
|
14
|
+
* 2. GAP value vs target vs tolerance, plus a real TREND over the
|
|
15
|
+
* series — the thing neither repo could compute before.
|
|
16
|
+
* 3. DECIDE the decision table: nothing / create work / ask a question /
|
|
17
|
+
* log and wait / fix the sensor (goals/admission.decideForGap).
|
|
18
|
+
* 4. PLAN candidates. Deterministic by default; `deps.proposeCandidates`
|
|
19
|
+
* is the seam where a planning sub-session plugs in, and it is
|
|
20
|
+
* fed the last three interventions WITH their realized deltas so
|
|
21
|
+
* the loop learns instead of repeating itself.
|
|
22
|
+
* 5. ADMIT the hard gates (goals/admission.admit). Every rejection is
|
|
23
|
+
* audited with its gate — never silently dropped.
|
|
24
|
+
* 6. CLASSIFY SELF / COLLABORATE / HANDOFF / NEEDS_REVIEW (goals/classify).
|
|
25
|
+
* 7. ROUTE the execution rung (lib/execution/route.routeRung) and the
|
|
26
|
+
* staffing primitive (goals/collaborate.routeWorkItem).
|
|
27
|
+
* 8. MIRROR a local queue item the existing backlog-executor already
|
|
28
|
+
* consumes, carrying `advances` + `expected_delta` + `why`.
|
|
29
|
+
* 9. AUDIT one line per decision, with the provenance chain.
|
|
30
|
+
*
|
|
31
|
+
* OBSERVE-ONLY is the default posture. For the first period after this ships,
|
|
32
|
+
* `enforcement:"observe"` records everything and creates ZERO tasks — a human
|
|
33
|
+
* flips it to `"active"`. An autonomous loop that starts by writing is how you
|
|
34
|
+
* get a fleet of agents spamming a board on day one.
|
|
35
|
+
*
|
|
36
|
+
* Everything is injectable: clock, sensors, org primitives, budget, and the
|
|
37
|
+
* planner. `runGoalSteward` performs no network I/O of its own.
|
|
38
|
+
*
|
|
39
|
+
* @module lib/goals/loop
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
"use strict";
|
|
43
|
+
|
|
44
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
45
|
+
import { join, dirname } from "node:path";
|
|
46
|
+
|
|
47
|
+
import { resolveAgentRoot } from "../agent-root.mjs";
|
|
48
|
+
import { appendJsonl, writeFileAtomic } from "../fs-atomic.mjs";
|
|
49
|
+
import { readCache, stalenessTier, permissionsForTier } from "../mandate/cache.mjs";
|
|
50
|
+
import { adoptedObjectives, validateMandateBody, memberIdOf, provenanceOf, objectivesOf } from "../mandate/model.mjs";
|
|
51
|
+
import { recordDecision } from "../mandate/audit.mjs";
|
|
52
|
+
import {
|
|
53
|
+
readSeries,
|
|
54
|
+
recordMeasurement,
|
|
55
|
+
readInterventions,
|
|
56
|
+
measureKpi,
|
|
57
|
+
gapFor,
|
|
58
|
+
isDue,
|
|
59
|
+
windowFor,
|
|
60
|
+
} from "../kpi.mjs";
|
|
61
|
+
import { admit, decideForGap } from "./admission.mjs";
|
|
62
|
+
import { classifyWorkItem } from "./classify.mjs";
|
|
63
|
+
import { routeWorkItem, raiseEscalation } from "./collaborate.mjs";
|
|
64
|
+
import { routeRung } from "../execution/route.mjs";
|
|
65
|
+
|
|
66
|
+
/** The queue the loop writes into. The existing backlog-executor sweeps it. */
|
|
67
|
+
export const GOALS_QUEUE_REL = join("state", "queues", "goals.yaml");
|
|
68
|
+
/** Observe-only output: candidates that WOULD have been created. */
|
|
69
|
+
export const PROPOSED_REL = join("state", "goals", "proposed.jsonl");
|
|
70
|
+
|
|
71
|
+
/** Default per-candidate cost estimate when the planner supplies none. */
|
|
72
|
+
export const DEFAULT_EST_CENTS = 50;
|
|
73
|
+
|
|
74
|
+
function logOf(deps) {
|
|
75
|
+
return deps && typeof deps.log === "function" ? deps.log : () => {};
|
|
76
|
+
}
|
|
77
|
+
function nowMs(deps) {
|
|
78
|
+
return deps && typeof deps.now === "function" ? deps.now() : Date.now();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Local queue mirror
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render queue items in exactly the on-disk shape `lib/backlog.parseQueueItems`
|
|
87
|
+
* reads. Deterministic, dependency-free (no YAML lib on the hot path — the same
|
|
88
|
+
* reason the daemon hand-rolls its parser).
|
|
89
|
+
* @param {object[]} items @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function renderQueueItems(items) {
|
|
92
|
+
const L = [];
|
|
93
|
+
for (const it of items || []) {
|
|
94
|
+
L.push(` - id: ${it.id}`);
|
|
95
|
+
L.push(` title: "${String(it.title).replace(/"/g, "'")}"`);
|
|
96
|
+
L.push(` status: ${it.status || "open"}`);
|
|
97
|
+
L.push(` priority: ${it.priority || "normal"}`);
|
|
98
|
+
L.push(` next_action: "${String(it.next_action || it.title).replace(/"/g, "'")}"`);
|
|
99
|
+
L.push(` source: goal-steward`);
|
|
100
|
+
L.push(` advances: [${(it.advances || []).join(", ")}]`);
|
|
101
|
+
L.push(` expected_delta: ${it.expected_delta == null ? "null" : it.expected_delta}`);
|
|
102
|
+
L.push(` obligation_key: ${it.obligation_key || "null"}`);
|
|
103
|
+
L.push(` disposition: ${it.disposition || "SELF"}`);
|
|
104
|
+
L.push(` rung: ${it.rung == null ? "null" : it.rung}`);
|
|
105
|
+
L.push(` why: "${String((it.why && it.why.reason) || "kpi-gap").replace(/"/g, "'")}"`);
|
|
106
|
+
L.push(` created: ${it.created}`);
|
|
107
|
+
}
|
|
108
|
+
return L.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Append items to `state/queues/goals.yaml`, creating the file header on first
|
|
113
|
+
* write. Never throws.
|
|
114
|
+
* @param {string} agentRoot @param {object[]} items @param {object} [deps]
|
|
115
|
+
* @returns {{written:number, path:string, ok:boolean}}
|
|
116
|
+
*/
|
|
117
|
+
export function appendBacklogItems(agentRoot, items, deps = {}) {
|
|
118
|
+
const path = join(resolveAgentRoot(agentRoot), GOALS_QUEUE_REL);
|
|
119
|
+
if (!items || items.length === 0) return { written: 0, path, ok: true };
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
122
|
+
const header =
|
|
123
|
+
"queue: goals\n" +
|
|
124
|
+
"description: Self-directed work created by the goal-steward loop from measured KPI gaps. Every item carries the objective it advances and the expected delta.\n" +
|
|
125
|
+
"items:\n";
|
|
126
|
+
const prev = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
127
|
+
const body = prev && prev.includes("items:") ? prev.replace(/\s*$/, "\n") : header;
|
|
128
|
+
writeFileAtomic(path, `${body}${renderQueueItems(items)}\n`);
|
|
129
|
+
return { written: items.length, path, ok: true };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logOf(deps)("error", `[goals] could not write the goals queue: ${err && err.message ? err.message : err}`);
|
|
132
|
+
return { written: 0, path, ok: false };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Open items already on an objective, from the goals queue. Never throws. */
|
|
137
|
+
export function openItemsFor(agentRoot, objectiveKey) {
|
|
138
|
+
const path = join(resolveAgentRoot(agentRoot), GOALS_QUEUE_REL);
|
|
139
|
+
if (!existsSync(path)) return [];
|
|
140
|
+
let body;
|
|
141
|
+
try { body = readFileSync(path, "utf-8"); } catch { return []; }
|
|
142
|
+
const out = [];
|
|
143
|
+
for (const block of body.split(/^\s*-\s+/m).slice(1)) {
|
|
144
|
+
const status = block.match(/^\s*status:\s*(\S+)/m);
|
|
145
|
+
if (!status || !["open", "in_progress"].includes(status[1])) continue;
|
|
146
|
+
const advances = block.match(/^\s*advances:\s*\[([^\]]*)\]/m);
|
|
147
|
+
const list = advances ? advances[1].split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
148
|
+
if (objectiveKey && !list.includes(objectiveKey)) continue;
|
|
149
|
+
const title = block.match(/^\s*title:\s*"?(.+?)"?\s*$/m);
|
|
150
|
+
const id = block.match(/^\s*id:\s*(\S+)/m);
|
|
151
|
+
out.push({ id: id ? id[1] : null, title: title ? title[1] : "", advances: list, status: status[1] });
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// The default (deterministic, LLM-free) planner
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build one candidate for a lagging objective, with no model in the loop. This
|
|
162
|
+
* is deliberately conservative: it names the gap, cites the objective, and asks
|
|
163
|
+
* for the smallest concrete move. `deps.proposeCandidates` replaces it when a
|
|
164
|
+
* planning sub-session is available.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} o - { objective, gap, interventions }
|
|
167
|
+
* @returns {object[]} candidates
|
|
168
|
+
*/
|
|
169
|
+
export function defaultProposeCandidates(o = {}) {
|
|
170
|
+
const obj = o.objective || {};
|
|
171
|
+
const gap = o.gap || {};
|
|
172
|
+
const metric = obj.metric || obj.key;
|
|
173
|
+
const dir = obj.direction === "down" ? "reduce" : "raise";
|
|
174
|
+
// Aim to close half the gap in one period: honest about being an increment,
|
|
175
|
+
// and it makes `expectedDelta` a number the intervention ledger can score.
|
|
176
|
+
const expectedDelta = Number.isFinite(gap.gap) ? +(gap.gap / 2).toFixed(6) : null;
|
|
177
|
+
const priorFailures = (o.interventions || []).filter(
|
|
178
|
+
(i) => Number.isFinite(i.realizedDelta) && i.realizedDelta <= 0
|
|
179
|
+
).length;
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
title: `Close the ${metric} gap: ${dir} from ${gap.value} toward ${gap.target}`,
|
|
183
|
+
objectiveKey: obj.key,
|
|
184
|
+
advances: [obj.key],
|
|
185
|
+
expectedDelta,
|
|
186
|
+
estCents: DEFAULT_EST_CENTS,
|
|
187
|
+
collaborators: obj.collaborators || [],
|
|
188
|
+
actionClasses: obj.actionClasses || [],
|
|
189
|
+
requiredCapabilities: obj.sensor && obj.sensor.capability ? [obj.sensor.capability] : [],
|
|
190
|
+
successCriteria: [
|
|
191
|
+
`${metric} moves ${obj.direction === "down" ? "below" : "above"} ${obj.target} within one ${obj.cadence || "period"}`,
|
|
192
|
+
],
|
|
193
|
+
need: {
|
|
194
|
+
methods: obj.sensor && obj.sensor.capability ? [obj.sensor.capability] : [],
|
|
195
|
+
steps: 4,
|
|
196
|
+
paramsKnown: false,
|
|
197
|
+
openEnded: priorFailures >= 1,
|
|
198
|
+
},
|
|
199
|
+
why: {
|
|
200
|
+
reason: "kpi-gap",
|
|
201
|
+
objectiveKey: obj.key,
|
|
202
|
+
metric,
|
|
203
|
+
gap: gap.gap,
|
|
204
|
+
trend: gap.trend,
|
|
205
|
+
expectedDelta,
|
|
206
|
+
priorFailedInterventions: priorFailures,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// The loop
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run one goal-steward pass.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} deps - {
|
|
220
|
+
* agentRoot, memberId?, now?, log?,
|
|
221
|
+
* cacheRecord?, // injected mandate cache record (else read from disk)
|
|
222
|
+
* sensors?, reachable?, // capability id → async fn; reachable id set
|
|
223
|
+
* peers?, // [{memberId, capabilities:[]}] for handoff cover
|
|
224
|
+
* proposeCandidates?, // ({objective,gap,series,interventions}) => candidates[]
|
|
225
|
+
* enforcement?, // "observe" (default) | "active"
|
|
226
|
+
* remainingCents?, // obligation/seat envelope
|
|
227
|
+
* budgetStatus?, // lib/budget-guard.dailyStatus() shape
|
|
228
|
+
* maxOpenPerObjective?, maxNewPerRun?,
|
|
229
|
+
* …plus every goals/collaborate.mjs primitive seam
|
|
230
|
+
* }
|
|
231
|
+
* @returns {Promise<object>} the run report
|
|
232
|
+
*/
|
|
233
|
+
export async function runGoalSteward(deps = {}) {
|
|
234
|
+
const agentRoot = resolveAgentRoot(deps.agentRoot);
|
|
235
|
+
const log = logOf(deps);
|
|
236
|
+
const enforcement = deps.enforcement === "active" ? "active" : "observe";
|
|
237
|
+
const report = {
|
|
238
|
+
ok: true,
|
|
239
|
+
enforcement,
|
|
240
|
+
at: new Date(nowMs(deps)).toISOString(),
|
|
241
|
+
staleness: null,
|
|
242
|
+
validation: null,
|
|
243
|
+
measured: [],
|
|
244
|
+
gaps: [],
|
|
245
|
+
decisions: [],
|
|
246
|
+
candidates: 0,
|
|
247
|
+
admitted: [],
|
|
248
|
+
rejected: [],
|
|
249
|
+
routed: [],
|
|
250
|
+
escalations: [],
|
|
251
|
+
drift: [],
|
|
252
|
+
created: 0,
|
|
253
|
+
skipped: null,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// --- 0. mandate + staleness ----------------------------------------------
|
|
257
|
+
const cacheRecord = deps.cacheRecord !== undefined ? deps.cacheRecord : readCache(agentRoot);
|
|
258
|
+
|
|
259
|
+
const audit = (rec) =>
|
|
260
|
+
recordDecision(agentRoot, rec, { now: deps.now, mandate: cacheRecord, log, mirror: deps.auditMirror });
|
|
261
|
+
|
|
262
|
+
if (!cacheRecord || cacheRecord.corrupt) {
|
|
263
|
+
const reason = !cacheRecord
|
|
264
|
+
? "no mandate cache — this seat has never fetched an adopted mandate"
|
|
265
|
+
: `mandate cache is corrupt: ${cacheRecord.reason}`;
|
|
266
|
+
log("warn", `[goal-steward] ${reason}; nothing to steward`);
|
|
267
|
+
report.ok = false;
|
|
268
|
+
report.skipped = reason;
|
|
269
|
+
audit({ decision: "drift", detail: { kind: "mandate_stale", reason } });
|
|
270
|
+
report.drift.push({ kind: "mandate_stale", reason });
|
|
271
|
+
return report;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const tier = stalenessTier(cacheRecord, nowMs(deps));
|
|
275
|
+
const permissions = permissionsForTier(tier);
|
|
276
|
+
report.staleness = { tier, ...permissions };
|
|
277
|
+
if (permissions.outcome !== "full") {
|
|
278
|
+
const reason = `mandate cache is ${tier} — OUTCOME work is ${permissions.outcome}`;
|
|
279
|
+
log("warn", `[goal-steward] ${reason}; measuring is still allowed, creating work is not`);
|
|
280
|
+
report.skipped = reason;
|
|
281
|
+
audit({ decision: "drift", detail: { kind: "mandate_stale", tier, reason } });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const memberId = deps.memberId || memberIdOf(cacheRecord) || null;
|
|
285
|
+
const validation = validateMandateBody(cacheRecord, { ownerMemberId: memberId });
|
|
286
|
+
report.validation = validation;
|
|
287
|
+
for (const e of validation.errors) log("warn", `[goal-steward] mandate invalid: ${e}`);
|
|
288
|
+
for (const w of validation.warnings) log("info", `[goal-steward] mandate warning: ${w}`);
|
|
289
|
+
|
|
290
|
+
const objectives = adoptedObjectives(cacheRecord, { ownerMemberId: memberId, badKeys: validation.badKeys });
|
|
291
|
+
if (objectives.length === 0) {
|
|
292
|
+
const total = objectivesOf(cacheRecord).length;
|
|
293
|
+
log(
|
|
294
|
+
"info",
|
|
295
|
+
`[goal-steward] no adopted objectives (${total} in the mandate) — proposed objectives drive no work; the agent runs its standard + archetype cadences`
|
|
296
|
+
);
|
|
297
|
+
report.skipped = report.skipped || "no adopted objectives";
|
|
298
|
+
return report;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let admittedThisRun = 0;
|
|
302
|
+
const backlogItems = [];
|
|
303
|
+
const proposedRows = [];
|
|
304
|
+
|
|
305
|
+
for (const objective of objectives) {
|
|
306
|
+
// --- 1. MEASURE --------------------------------------------------------
|
|
307
|
+
let series = readSeries(agentRoot, objective.key);
|
|
308
|
+
const due = isDue(objective, series, deps);
|
|
309
|
+
if (due.due) {
|
|
310
|
+
const m = await measureKpi(objective, { sensors: deps.sensors, reachable: deps.reachable, log, now: deps.now });
|
|
311
|
+
if (m.ok) {
|
|
312
|
+
const rec = recordMeasurement(
|
|
313
|
+
agentRoot,
|
|
314
|
+
{ objectiveKey: objective.key, value: m.value, window: due.window, source: m.source, evidence: m.evidence },
|
|
315
|
+
{ now: deps.now, log }
|
|
316
|
+
);
|
|
317
|
+
report.measured.push({ objectiveKey: objective.key, value: m.value, window: due.window, source: m.source, recorded: rec.recorded });
|
|
318
|
+
audit({ decision: "measured", objectiveKey: objective.key, detail: { value: m.value, window: due.window, source: m.source, evidence: m.evidence } });
|
|
319
|
+
if (typeof deps.mirrorSample === "function") {
|
|
320
|
+
try { await deps.mirrorSample({ objective, value: m.value, window: due.window, source: m.source, evidence: m.evidence }); }
|
|
321
|
+
catch (err) { log("warn", `[goal-steward] mandate.sample mirror failed for "${objective.key}": ${err && err.message ? err.message : err}`); }
|
|
322
|
+
}
|
|
323
|
+
series = readSeries(agentRoot, objective.key);
|
|
324
|
+
} else {
|
|
325
|
+
report.measured.push({ objectiveKey: objective.key, ok: false, reason: m.reason });
|
|
326
|
+
report.drift.push({ kind: m.reason === "unreachable-capability" ? "unreachable_capability" : "stale_sensor", key: objective.key, reason: m.reason });
|
|
327
|
+
audit({ decision: "measure_failed", objectiveKey: objective.key, detail: { reason: m.reason, capability: objective.sensor && objective.sensor.capability } });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- 2. GAP ------------------------------------------------------------
|
|
332
|
+
const gap = gapFor(objective, series, deps);
|
|
333
|
+
report.gaps.push(gap);
|
|
334
|
+
audit({ decision: "gap", objectiveKey: objective.key, detail: gap });
|
|
335
|
+
|
|
336
|
+
// --- 3. DECIDE ---------------------------------------------------------
|
|
337
|
+
const openItems = openItemsFor(agentRoot, objective.key);
|
|
338
|
+
const decision = decideForGap(gap, { coverage: openItems.length });
|
|
339
|
+
report.decisions.push({ objectiveKey: objective.key, ...decision, coverage: openItems.length });
|
|
340
|
+
|
|
341
|
+
if (decision.action === "drift") {
|
|
342
|
+
report.drift.push({ kind: "stale_sensor", key: objective.key, reason: decision.reason });
|
|
343
|
+
audit({ decision: "drift", objectiveKey: objective.key, detail: { kind: "stale_sensor", reason: decision.reason } });
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (decision.action === "none" || decision.action === "log") {
|
|
347
|
+
audit({ decision: "candidate_rejected", objectiveKey: objective.key, detail: { gate: "trend", reason: decision.reason } });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (decision.action === "escalate") {
|
|
351
|
+
const question = `${objective.metric || objective.key} is ${gap.trend} at ${gap.value} against a target of ${gap.target}, with ${openItems.length} item(s) already in flight. More tasks will not move it. What should change?`;
|
|
352
|
+
const options = [
|
|
353
|
+
{ label: "Re-target the objective (the target is not achievable this period)" },
|
|
354
|
+
{ label: "Escalate resourcing — this needs another seat or a budget increase" },
|
|
355
|
+
{ label: "Accept the miss for this period and re-plan next cycle" },
|
|
356
|
+
];
|
|
357
|
+
audit({ decision: "candidate_rejected", objectiveKey: objective.key, detail: { gate: "trend", reason: decision.reason, escalating: true } });
|
|
358
|
+
if (enforcement === "active") {
|
|
359
|
+
const esc = await raiseEscalation({ objectiveKey: objective.key, question, options, gap }, deps);
|
|
360
|
+
report.escalations.push({ objectiveKey: objective.key, ...esc });
|
|
361
|
+
audit({ decision: "routed", objectiveKey: objective.key, detail: { primitive: esc.method || "escalation.create", ok: esc.ok, reason: esc.reason } });
|
|
362
|
+
} else {
|
|
363
|
+
report.escalations.push({ objectiveKey: objective.key, ok: false, reason: "observe-only", question, options });
|
|
364
|
+
audit({ decision: "observe_only", objectiveKey: objective.key, detail: { wouldEscalate: question } });
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// --- 4. PLAN -----------------------------------------------------------
|
|
370
|
+
const interventions = readInterventions(agentRoot, objective.key, 3);
|
|
371
|
+
const planner = typeof deps.proposeCandidates === "function" ? deps.proposeCandidates : defaultProposeCandidates;
|
|
372
|
+
let candidates = [];
|
|
373
|
+
try {
|
|
374
|
+
candidates = (await planner({ objective, gap, series, interventions, openItems, agentRoot })) || [];
|
|
375
|
+
} catch (err) {
|
|
376
|
+
log("error", `[goal-steward] planner threw for "${objective.key}": ${err && err.message ? err.message : err}`);
|
|
377
|
+
audit({ decision: "candidate_rejected", objectiveKey: objective.key, detail: { gate: "planner", reason: "planner-threw" } });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
report.candidates += candidates.length;
|
|
381
|
+
const latestSample = series[series.length - 1] || null;
|
|
382
|
+
|
|
383
|
+
for (const candidate of candidates) {
|
|
384
|
+
// --- 5. ADMIT --------------------------------------------------------
|
|
385
|
+
const verdict = admit(candidate, {
|
|
386
|
+
objective,
|
|
387
|
+
gap,
|
|
388
|
+
latestSample,
|
|
389
|
+
openItems,
|
|
390
|
+
admittedThisRun,
|
|
391
|
+
remainingCents: deps.remainingCents,
|
|
392
|
+
staleness: permissions,
|
|
393
|
+
maxOpenPerObjective: deps.maxOpenPerObjective,
|
|
394
|
+
maxNewPerRun: deps.maxNewPerRun,
|
|
395
|
+
});
|
|
396
|
+
if (!verdict.admitted) {
|
|
397
|
+
report.rejected.push({ title: candidate.title, objectiveKey: objective.key, gate: verdict.gate, reason: verdict.reason });
|
|
398
|
+
audit({ decision: "candidate_rejected", objectiveKey: objective.key, detail: { title: candidate.title, gate: verdict.gate, reason: verdict.reason, checks: verdict.checks } });
|
|
399
|
+
log("info", `[goal-steward] rejected "${candidate.title}" at gate ${verdict.gate}: ${verdict.reason}`);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// --- 6. CLASSIFY -----------------------------------------------------
|
|
404
|
+
const classification = classifyWorkItem(candidate, {
|
|
405
|
+
memberId,
|
|
406
|
+
objective,
|
|
407
|
+
reachable: deps.reachable,
|
|
408
|
+
peers: deps.peers,
|
|
409
|
+
});
|
|
410
|
+
audit({ decision: "disposition", objectiveKey: objective.key, detail: { title: candidate.title, disposition: classification.disposition, reason: classification.reason, target: classification.target, why: classification.why } });
|
|
411
|
+
|
|
412
|
+
// --- 7. ROUTE (rung) -------------------------------------------------
|
|
413
|
+
// `routeRung` reads the blast radius off `need.actionClasses`, but a
|
|
414
|
+
// candidate declares its action classes at the TOP level (that is where
|
|
415
|
+
// `classifyWorkItem` and `defaultProposeCandidates` both put them). Merge
|
|
416
|
+
// them in, or the router's "external | irreversible | financial forces an
|
|
417
|
+
// approval BEFORE any rung executes" gate never fires from this loop —
|
|
418
|
+
// `route.approval` would be permanently null and the gate dead code.
|
|
419
|
+
const need = {
|
|
420
|
+
...(candidate.need || {}),
|
|
421
|
+
actionClasses: [
|
|
422
|
+
...[].concat(candidate.actionClasses || []),
|
|
423
|
+
...[].concat((candidate.need && candidate.need.actionClasses) || []),
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
const route = routeRung(need, {
|
|
427
|
+
failures: candidate.failures || 0,
|
|
428
|
+
hasAdoptedObjective: true,
|
|
429
|
+
estCents: Number.isFinite(candidate.estCents) ? candidate.estCents : DEFAULT_EST_CENTS,
|
|
430
|
+
remainingCents: deps.remainingCents,
|
|
431
|
+
maxRung: deps.maxRung,
|
|
432
|
+
});
|
|
433
|
+
audit({ decision: "execution_rung", objectiveKey: objective.key, detail: { title: candidate.title, rung: route.rung, mechanism: route.mechanism, why: route.why, gate: route.gate, approval: route.approval } });
|
|
434
|
+
|
|
435
|
+
if (route.blocked && route.gate && route.gate.kind === "budget") {
|
|
436
|
+
report.rejected.push({ title: candidate.title, objectiveKey: objective.key, gate: "budget", reason: `estimate ${route.gate.detail.estCents}c exceeds envelope ${route.gate.detail.remainingCents}c` });
|
|
437
|
+
audit({ decision: "budget_refused", objectiveKey: objective.key, detail: { title: candidate.title, ...route.gate.detail } });
|
|
438
|
+
log("warn", `[goal-steward] "${candidate.title}" enqueued rather than run: over the remaining budget envelope`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (route.approval) {
|
|
442
|
+
audit({ decision: "approval_gate", objectiveKey: objective.key, detail: { title: candidate.title, classes: route.approval.classes } });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const provenance = provenanceOf(cacheRecord, objective.key);
|
|
446
|
+
const obligationKey = `outcome.${objective.key}`;
|
|
447
|
+
const item = {
|
|
448
|
+
id: `goal-${objective.key}-${windowFor(objective.cadence, nowMs(deps))}-${backlogItems.length + 1}`,
|
|
449
|
+
title: candidate.title,
|
|
450
|
+
objectiveKey: objective.key,
|
|
451
|
+
obligationKey,
|
|
452
|
+
priority: gap.normalizedGap > 0.25 ? "high" : "normal",
|
|
453
|
+
expectedDelta: candidate.expectedDelta ?? null,
|
|
454
|
+
successCriteria: candidate.successCriteria || [],
|
|
455
|
+
why: { ...(candidate.why || {}), clause: provenance.clause, obligationKey, adoptedBy: provenance.adoptedBy },
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// --- 8. STAFF + MIRROR -----------------------------------------------
|
|
459
|
+
if (enforcement !== "active") {
|
|
460
|
+
proposedRows.push({
|
|
461
|
+
at: new Date(nowMs(deps)).toISOString(),
|
|
462
|
+
item,
|
|
463
|
+
disposition: classification.disposition,
|
|
464
|
+
rung: route.rung,
|
|
465
|
+
provenance,
|
|
466
|
+
});
|
|
467
|
+
report.admitted.push({ title: candidate.title, objectiveKey: objective.key, disposition: classification.disposition, rung: route.rung, observed: true });
|
|
468
|
+
audit({ decision: "observe_only", objectiveKey: objective.key, obligationKey, detail: { title: candidate.title, disposition: classification.disposition, rung: route.rung } });
|
|
469
|
+
admittedThisRun += 1;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let routed;
|
|
474
|
+
try {
|
|
475
|
+
routed = await routeWorkItem(item, classification, { ...deps, agentRoot, memberId });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
log("error", `[goal-steward] routing "${candidate.title}" threw: ${err && err.message ? err.message : err}`);
|
|
478
|
+
routed = { routed: classification.disposition, ok: false, reason: "route-threw" };
|
|
479
|
+
}
|
|
480
|
+
report.routed.push({ title: candidate.title, objectiveKey: objective.key, ...routed });
|
|
481
|
+
audit({ decision: routed.ok ? "routed" : "route_failed", objectiveKey: objective.key, obligationKey, detail: { title: candidate.title, ...routed } });
|
|
482
|
+
|
|
483
|
+
// A lost work-claim lease means a peer owns it — do NOT also queue it.
|
|
484
|
+
if (routed.routed === "COLLABORATE" && !routed.ok && String(routed.reason || "").startsWith("lease-lost")) {
|
|
485
|
+
report.rejected.push({ title: candidate.title, objectiveKey: objective.key, gate: "lease", reason: routed.reason });
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
backlogItems.push({
|
|
490
|
+
id: item.id,
|
|
491
|
+
title: item.title,
|
|
492
|
+
status: "open",
|
|
493
|
+
priority: item.priority,
|
|
494
|
+
next_action: item.title,
|
|
495
|
+
advances: [objective.key],
|
|
496
|
+
expected_delta: item.expectedDelta,
|
|
497
|
+
obligation_key: obligationKey,
|
|
498
|
+
disposition: classification.disposition,
|
|
499
|
+
rung: route.rung,
|
|
500
|
+
why: item.why,
|
|
501
|
+
created: new Date(nowMs(deps)).toISOString().slice(0, 10),
|
|
502
|
+
});
|
|
503
|
+
report.admitted.push({ title: candidate.title, objectiveKey: objective.key, disposition: classification.disposition, rung: route.rung, itemId: routed.itemId || item.id });
|
|
504
|
+
audit({ decision: "candidate_admitted", objectiveKey: objective.key, obligationKey, detail: { title: candidate.title, expectedDelta: item.expectedDelta, rung: route.rung, disposition: classification.disposition } });
|
|
505
|
+
admittedThisRun += 1;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// --- 8b. write ------------------------------------------------------------
|
|
510
|
+
if (enforcement === "active") {
|
|
511
|
+
const w = appendBacklogItems(agentRoot, backlogItems, deps);
|
|
512
|
+
report.created = w.written;
|
|
513
|
+
} else if (proposedRows.length > 0) {
|
|
514
|
+
try {
|
|
515
|
+
const p = join(agentRoot, PROPOSED_REL);
|
|
516
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
517
|
+
for (const row of proposedRows) appendJsonl(p, row);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
log("error", `[goal-steward] could not write the observe-only proposals: ${err && err.message ? err.message : err}`);
|
|
520
|
+
}
|
|
521
|
+
report.created = 0;
|
|
522
|
+
log("info", `[goal-steward] observe-only: ${proposedRows.length} candidate(s) recorded, 0 tasks created. Set enforcement:"active" to turn the loop on.`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return report;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export default {
|
|
529
|
+
GOALS_QUEUE_REL,
|
|
530
|
+
PROPOSED_REL,
|
|
531
|
+
DEFAULT_EST_CENTS,
|
|
532
|
+
renderQueueItems,
|
|
533
|
+
appendBacklogItems,
|
|
534
|
+
openItemsFor,
|
|
535
|
+
defaultProposeCandidates,
|
|
536
|
+
runGoalSteward,
|
|
537
|
+
};
|