@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
package/framework-features.json
CHANGED
|
@@ -262,6 +262,36 @@
|
|
|
262
262
|
"command": "true",
|
|
263
263
|
"description": "lib/org/knowledge.mjs — recall()/remember() over the org's shared knowledge plane (backed by the Cohort server). Once the agent is enrolled (org-cohort feature: config/org.yaml enabled with base + token), the daemon's prompt-builder can consult recall() so replies are informed by what the fleet already knows, and finished work can remember() durable facts. Server-side ACL governs visibility. Inert + fail-open until enrollment; no standalone init."
|
|
264
264
|
}
|
|
265
|
+
},
|
|
266
|
+
"skill-marketplace": {
|
|
267
|
+
"version": "1",
|
|
268
|
+
"since": "2.4.0",
|
|
269
|
+
"title": "Skill marketplace registration (rung 1 of the execution ladder)",
|
|
270
|
+
"init": {
|
|
271
|
+
"auto": true,
|
|
272
|
+
"command": "node scripts/setup/init-skill-marketplace.mjs",
|
|
273
|
+
"description": "scripts/setup/init-skill-marketplace.mjs — Claude Code only loads a skill when its plugin is declared by a .claude-plugin/marketplace.json AND listed in ~/.claude/settings.json enabledPlugins. Neither was ever written, so every skill markdown file under plugins/*/skills/ was an orphan: present on disk, dead at runtime, silently. This registers + enables each skill plugin through the SAME lib/capability/probe.probeSkillPlugin({repair}) the inventory section uses, so rung 1 of the execution ladder stops being a lie. Idempotent; an already-registered plugin is untouched. A plugin that cannot be repaired is reported loudly and its skills stay reachable:false, which the plan compiler then refuses to cite."
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
"capability-manifest": {
|
|
277
|
+
"version": "1",
|
|
278
|
+
"since": "2.4.0",
|
|
279
|
+
"title": "Capability inventory (probed, durable capability map across four planes)",
|
|
280
|
+
"init": {
|
|
281
|
+
"auto": true,
|
|
282
|
+
"command": "node scripts/setup/init-capability-manifest.mjs",
|
|
283
|
+
"description": "lib/capability/{inventory,probe}.mjs — exhaustively discover Claude Code built-ins, plugin skills (registering the marketplaces so the skill files are actually invocable), MCP servers (.mcp.json cross-read with config/mcp-servers.yaml), and the Cohort org tools + granted integration tools, PROBE each for reachability, and write config/capability-manifest.json (tracked). The law it establishes: an entry with reachable:false may not be cited by any obligation — the plan compiler enforces it. Idempotent; a no-op re-run yields the same checksum. Degradations are recorded in the manifest AND logged, never swallowed."
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
"mandate-spine": {
|
|
287
|
+
"version": "1",
|
|
288
|
+
"since": "2.4.0",
|
|
289
|
+
"title": "Mandate spine + plan compiler (derived, justified per-agent schedule)",
|
|
290
|
+
"init": {
|
|
291
|
+
"auto": false,
|
|
292
|
+
"command": "node scripts/setup/generate-plan.mjs --derive",
|
|
293
|
+
"description": "lib/mandate/{derive,cache}.mjs + lib/plan/{schema,compile,emit}.mjs — derive a PROPOSED objective tree from the agent's charter pillars, the org's strategy streams, its archetype KPI categories and config/priorities.yaml; bind each metric to a REACHABLE sensor capability; then deterministically compile obligations (SCHEDULE/OUTCOME/REACT) and emit config/plan.yaml + cadences.yaml + .cadence-registry.json (extended shape: allowedTools/guardModule/budgetCents/obligationKey) + .cadence-plists.tsv, which generate-plists.sh turns into real launchd jobs. MANUAL because it proposes objectives to hq: nothing is adopted by the agent, and PROPOSED objectives compile no obligations — until a human adopts, the schedule is exactly today's standard + archetype cadence set."
|
|
294
|
+
}
|
|
265
295
|
}
|
|
266
296
|
}
|
|
267
297
|
}
|
package/lib/backlog.mjs
CHANGED
|
@@ -341,11 +341,147 @@ export function parseQueueItems(content, sourceFile = "") {
|
|
|
341
341
|
// the claim subsystem keys on a defined id and tolerates undefined.
|
|
342
342
|
const id = idMatch?.[1]?.trim();
|
|
343
343
|
if (id) item.id = id;
|
|
344
|
+
|
|
345
|
+
// --- goal-steward provenance (D3) ------------------------------------
|
|
346
|
+
// `lib/goals/loop.renderQueueItems` writes `advances` / `expected_delta` /
|
|
347
|
+
// `obligation_key` / `disposition` onto every self-directed item. Without
|
|
348
|
+
// extracting them here the daemon's sweep could not tell goal-linked work
|
|
349
|
+
// from seeded work, and the gap-weighted ranking below would be dead code.
|
|
350
|
+
// All four are OPTIONAL: a legacy or hand-written item simply lacks them
|
|
351
|
+
// and scores exactly as it does today.
|
|
352
|
+
const advancesMatch = cleanBlock.match(/^\s*-?\s*"?advances"?:\s*\[([^\]]*)\]/m);
|
|
353
|
+
if (advancesMatch) {
|
|
354
|
+
const list = advancesMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
355
|
+
if (list.length) item.advances = list;
|
|
356
|
+
}
|
|
357
|
+
const deltaMatch = cleanBlock.match(/^\s*-?\s*"?expected_delta"?:\s*["']?(-?[0-9]*\.?[0-9]+)["']?\s*$/m);
|
|
358
|
+
if (deltaMatch) {
|
|
359
|
+
const d = Number(deltaMatch[1]);
|
|
360
|
+
if (Number.isFinite(d)) item.expected_delta = d;
|
|
361
|
+
}
|
|
362
|
+
const obligationMatch = cleanBlock.match(/^\s*-?\s*"?obligation_key"?:\s*["']?([^"'\n]+?)["']?\s*$/m);
|
|
363
|
+
const ok = obligationMatch?.[1]?.trim();
|
|
364
|
+
if (ok && ok !== "null") item.obligation_key = ok;
|
|
365
|
+
const dispMatch = cleanBlock.match(/^\s*-?\s*"?disposition"?:\s*["']?(SELF|COLLABORATE|HANDOFF|NEEDS_REVIEW)["']?/m);
|
|
366
|
+
if (dispMatch) item.disposition = dispMatch[1];
|
|
367
|
+
|
|
344
368
|
out.push(item);
|
|
345
369
|
}
|
|
346
370
|
return out;
|
|
347
371
|
}
|
|
348
372
|
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
374
|
+
// Gap-weighted backlog ranking (SPEC §6.3 step 6)
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
|
|
377
|
+
/** Flat priority ordering — the behaviour the daemon's sweep has always had. */
|
|
378
|
+
export const PRIORITY_ORDER = Object.freeze({ critical: 0, high: 1, normal: 2, low: 3 });
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Default ranking weights. `Charter.rewardWeights` overrides these when the
|
|
382
|
+
* charter carries them — which is the first thing in either repo that actually
|
|
383
|
+
* CONSUMES that field.
|
|
384
|
+
*
|
|
385
|
+
* priority how much the declared priority still counts
|
|
386
|
+
* gap how much "this objective is badly missed AND this item claims a
|
|
387
|
+
* big delta" counts
|
|
388
|
+
* age how much an item is penalised for sitting (prevents starvation
|
|
389
|
+
* of un-linked work by a permanently-lagging KPI)
|
|
390
|
+
*/
|
|
391
|
+
export const DEFAULT_BACKLOG_WEIGHTS = Object.freeze({ priority: 1, gap: 1, age: 0.25 });
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Resolve weights from a charter, falling back per-key. Non-finite / negative
|
|
395
|
+
* values are ignored rather than silently poisoning the ranking.
|
|
396
|
+
* @param {object} [charter] - { rewardWeights?: {priority?, gap?, age?} }
|
|
397
|
+
* @returns {{priority:number, gap:number, age:number}}
|
|
398
|
+
*/
|
|
399
|
+
export function resolveBacklogWeights(charter) {
|
|
400
|
+
const rw = (charter && (charter.rewardWeights || charter.reward_weights)) || {};
|
|
401
|
+
const pick = (k) => {
|
|
402
|
+
const v = Number(rw[k]);
|
|
403
|
+
return Number.isFinite(v) && v >= 0 ? v : DEFAULT_BACKLOG_WEIGHTS[k];
|
|
404
|
+
};
|
|
405
|
+
return { priority: pick("priority"), gap: pick("gap"), age: pick("age") };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Score one queue item. HIGHER is more urgent (the sweep sorts descending).
|
|
410
|
+
*
|
|
411
|
+
* The flat 4-level priority ordering is preserved exactly as the base term, so
|
|
412
|
+
* an item with no `advances[]` ranks identically to how it ranks today — the
|
|
413
|
+
* gap term can only ever *promote* goal-linked work, never demote seeded work
|
|
414
|
+
* below its priority peers by more than the age penalty already would.
|
|
415
|
+
*
|
|
416
|
+
* @param {object} item - parseQueueItems() shape
|
|
417
|
+
* @param {object} [ctx]
|
|
418
|
+
* @param {Object<string, {normalizedGap?:number}>} [ctx.gapsByObjective] keyed by objective key
|
|
419
|
+
* @param {object} [ctx.weights] resolveBacklogWeights() output
|
|
420
|
+
* @param {number} [ctx.now] epoch ms (injected — never Date.now here)
|
|
421
|
+
* @returns {{score:number, base:number, gapTerm:number, agePenalty:number, objectiveKey:string|null}}
|
|
422
|
+
*/
|
|
423
|
+
export function scoreQueueItem(item = {}, ctx = {}) {
|
|
424
|
+
const w = ctx.weights || DEFAULT_BACKLOG_WEIGHTS;
|
|
425
|
+
const gaps = ctx.gapsByObjective || {};
|
|
426
|
+
|
|
427
|
+
// Base: invert the priority ladder so higher == more urgent (critical → 3).
|
|
428
|
+
const rank = PRIORITY_ORDER[item.priority] ?? PRIORITY_ORDER.normal;
|
|
429
|
+
const base = (3 - rank) * w.priority;
|
|
430
|
+
|
|
431
|
+
// Gap term: normalizedGap × expectedDelta, over every objective the item
|
|
432
|
+
// claims to advance. An item that advances a badly-missed objective with a
|
|
433
|
+
// big claimed delta floats. Missing either factor contributes nothing —
|
|
434
|
+
// an unmeasured objective must not manufacture urgency.
|
|
435
|
+
let gapTerm = 0;
|
|
436
|
+
let objectiveKey = null;
|
|
437
|
+
for (const key of Array.isArray(item.advances) ? item.advances : []) {
|
|
438
|
+
const g = gaps[key];
|
|
439
|
+
// BOTH factors are clamped to [0,1] so the gap term is bounded by w_gap.
|
|
440
|
+
// Unclamped, a 100%-missed target with a big claimed delta would swamp the
|
|
441
|
+
// priority ladder entirely — a KPI gap must ESCALATE work, not let the
|
|
442
|
+
// agent redefine what "critical" means.
|
|
443
|
+
const ng = g && Number.isFinite(g.normalizedGap) ? Math.min(1, Math.max(0, g.normalizedGap)) : 0;
|
|
444
|
+
const delta = Number.isFinite(item.expected_delta) ? Math.abs(item.expected_delta) : 0;
|
|
445
|
+
const contribution = ng * (delta > 0 ? Math.min(1, delta) : 0);
|
|
446
|
+
if (contribution > gapTerm) { gapTerm = contribution; objectiveKey = key; }
|
|
447
|
+
if (objectiveKey === null) objectiveKey = key;
|
|
448
|
+
}
|
|
449
|
+
gapTerm *= w.gap;
|
|
450
|
+
|
|
451
|
+
// Age penalty: days since `created`, damped. Bounded so a stale item cannot
|
|
452
|
+
// dominate; it exists to stop a permanently-lagging KPI starving everything.
|
|
453
|
+
let agePenalty = 0;
|
|
454
|
+
if (item.created && Number.isFinite(ctx.now)) {
|
|
455
|
+
const t = Date.parse(item.created);
|
|
456
|
+
if (Number.isFinite(t)) {
|
|
457
|
+
const days = Math.max(0, (ctx.now - t) / 86400000);
|
|
458
|
+
agePenalty = Math.min(2, days / 14) * w.age;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { score: base + gapTerm - agePenalty, base, gapTerm, agePenalty, objectiveKey };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Rank a backlog. Stable: ties fall back to the flat priority ordering and then
|
|
467
|
+
* to the original array order, so a run with no measurements produces byte-for-
|
|
468
|
+
* byte today's dispatch order.
|
|
469
|
+
*
|
|
470
|
+
* @param {object[]} items @param {object} [ctx] see scoreQueueItem
|
|
471
|
+
* @returns {object[]} a new array, most urgent first, each item carrying `_score`
|
|
472
|
+
*/
|
|
473
|
+
export function rankBacklog(items, ctx = {}) {
|
|
474
|
+
const scored = (items || []).map((item, i) => ({ item, i, s: scoreQueueItem(item, ctx) }));
|
|
475
|
+
scored.sort((a, b) => {
|
|
476
|
+
if (b.s.score !== a.s.score) return b.s.score - a.s.score;
|
|
477
|
+
const pa = PRIORITY_ORDER[a.item.priority] ?? PRIORITY_ORDER.normal;
|
|
478
|
+
const pb = PRIORITY_ORDER[b.item.priority] ?? PRIORITY_ORDER.normal;
|
|
479
|
+
if (pa !== pb) return pa - pb;
|
|
480
|
+
return a.i - b.i;
|
|
481
|
+
});
|
|
482
|
+
return scored.map(({ item, s }) => ({ ...item, _score: s.score, _scoreParts: s }));
|
|
483
|
+
}
|
|
484
|
+
|
|
349
485
|
/** Count immediately-actionable (d30) tasks in a WBS. @param {object} wbs @returns {number} */
|
|
350
486
|
export function countReadyTasks(wbs) {
|
|
351
487
|
if (!wbs || !Array.isArray(wbs.streams)) return 0;
|
package/lib/cadences.mjs
CHANGED
|
@@ -76,6 +76,20 @@ export const STANDARD_CADENCES = [
|
|
|
76
76
|
// the guard tails the org event chain for branding version.saved and escalates
|
|
77
77
|
// only on a real save. 15min keeps the reaction prompt without polling hard.
|
|
78
78
|
{ id: "brand-steward", scope: "standard", mode: "guarded", interval: 900, prompt: "schedules/triggers/brand-steward.md" },
|
|
79
|
+
// goal-steward (D3, SPEC §6.3): the self-directed loop. Measures every adopted
|
|
80
|
+
// objective whose window is open, computes the gap, and turns a real gap into
|
|
81
|
+
// real backlog — classified SELF/COLLABORATE/HANDOFF/NEEDS_REVIEW and routed
|
|
82
|
+
// through the existing collaboration primitives. GUARDED: the guard runs the
|
|
83
|
+
// whole measure→decide→admit pass INLINE (it is pure local computation plus
|
|
84
|
+
// sensor reads) and escalates to a planning sub-session only when a gap
|
|
85
|
+
// actually needs candidates it cannot generate deterministically. An agent
|
|
86
|
+
// with no adopted mandate costs one cache read a day and never spawns.
|
|
87
|
+
//
|
|
88
|
+
// Defaults to OBSERVE-ONLY (config/plan.yaml: enforcement) — it records what
|
|
89
|
+
// it would create and creates nothing until a human flips enforcement to
|
|
90
|
+
// "active". A fleet of agents that starts by writing to a board on day one is
|
|
91
|
+
// how you lose the fleet.
|
|
92
|
+
{ id: "goal-steward", scope: "standard", mode: "guarded", calendar: { hour: 7, minute: 15 }, prompt: "schedules/triggers/goal-steward.md" },
|
|
79
93
|
];
|
|
80
94
|
|
|
81
95
|
/** Altitude-specific cadences (reuse existing triggers where one exists). */
|
|
@@ -168,8 +182,42 @@ function normalize(entry, scope) {
|
|
|
168
182
|
}
|
|
169
183
|
|
|
170
184
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
185
|
+
* Project a compiled SCHEDULE obligation onto a cadence entry. The obligation
|
|
186
|
+
* already carries a solved slot and a computed tool scope, so nothing here
|
|
187
|
+
* re-derives a schedule — `normalize()`'s `scheduleFor()` stagger must NOT be
|
|
188
|
+
* allowed to overwrite a slot the plan compiler solved against the principal's
|
|
189
|
+
* calendar.
|
|
190
|
+
* @param {object} ob @returns {object|null}
|
|
191
|
+
*/
|
|
192
|
+
function fromObligation(ob) {
|
|
193
|
+
if (!ob || ob.kind !== "SCHEDULE" || !ob.cadence_id) return null;
|
|
194
|
+
if (ob.status && ob.status !== "active") return null;
|
|
195
|
+
const entry = { id: ob.cadence_id, mode: ob.mode || "escalate", prompt: ob.prompt || `schedules/triggers/${ob.cadence_id}.md` };
|
|
196
|
+
if (ob.schedule && Number.isFinite(ob.schedule.interval)) entry.interval = ob.schedule.interval;
|
|
197
|
+
else if (ob.schedule && ob.schedule.calendar) entry.calendar = { ...ob.schedule.calendar };
|
|
198
|
+
if (ob.purpose) entry.purpose = ob.purpose;
|
|
199
|
+
if (ob.cadence) entry.cadence = ob.cadence;
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve the full cadence set for a { function, altitude }, optionally grafted
|
|
205
|
+
* with the agent's own compiled PLAN.
|
|
206
|
+
*
|
|
207
|
+
* Three legs, applied in order, first writer of an id wins:
|
|
208
|
+
*
|
|
209
|
+
* 1. STANDARD_CADENCES — the floor. Every agent, always.
|
|
210
|
+
* 2. function + altitude — the archetype's recurring workflows.
|
|
211
|
+
* 3. `obligations` — the agent's compiled plan (SPEC §5.6).
|
|
212
|
+
*
|
|
213
|
+
* Legs 1 and 2 are retained DELIBERATELY as the fallback: proposed-but-unadopted
|
|
214
|
+
* objectives compile no obligations, so an agent whose human has not yet adopted
|
|
215
|
+
* anything keeps running exactly today's archetype cadence set rather than
|
|
216
|
+
* falling to an empty schedule. The obligations leg only ever ADDS mandate-derived
|
|
217
|
+
* cadences on top — it can neither delete nor re-slot a standard one, which is
|
|
218
|
+
* what keeps existing launchd plists from churning.
|
|
219
|
+
*
|
|
220
|
+
* @param {{function:string, altitude:string, obligations?:object[]}} sel
|
|
173
221
|
* @returns {Promise<object[]>} ordered, de-duplicated cadence entries
|
|
174
222
|
*/
|
|
175
223
|
export async function resolveCadences(sel = {}) {
|
|
@@ -196,6 +244,19 @@ export async function resolveCadences(sel = {}) {
|
|
|
196
244
|
|
|
197
245
|
for (const c of ALTITUDE_CADENCES[altitude] || []) add(c, "altitude");
|
|
198
246
|
|
|
247
|
+
// Plan leg: mandate-derived cadences (measure/review obligations). Carries the
|
|
248
|
+
// obligation key through so cadence-handlers can meter the tick's budget and
|
|
249
|
+
// lib/plan/explain.mjs can walk the tick back to the objective that justifies it.
|
|
250
|
+
for (const ob of Array.isArray(sel.obligations) ? sel.obligations : []) {
|
|
251
|
+
const entry = fromObligation(ob);
|
|
252
|
+
if (!entry || seen.has(entry.id)) continue;
|
|
253
|
+
seen.add(entry.id);
|
|
254
|
+
const norm = normalize(entry, ob.scope === "mandate" ? "mandate" : (ob.scope || "mandate"));
|
|
255
|
+
if (ob.key) norm.obligationKey = ob.key;
|
|
256
|
+
if (ob.objective_id) norm.objectiveId = ob.objective_id;
|
|
257
|
+
out.push(norm);
|
|
258
|
+
}
|
|
259
|
+
|
|
199
260
|
return out;
|
|
200
261
|
}
|
|
201
262
|
|
package/lib/cadences.test.mjs
CHANGED
|
@@ -123,3 +123,108 @@ test("every STANDARD_CADENCES entry that declares a prompt has the file on disk"
|
|
|
123
123
|
assert.ok(existsSync(join(root, c.prompt)), `cadence ${c.id} prompt missing: ${c.prompt}`);
|
|
124
124
|
}
|
|
125
125
|
});
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// The PLAN leg (SPEC §4/§5.6): mandate-derived obligations graft onto the
|
|
129
|
+
// standard + archetype floor. This is what makes the schedule per-agent and
|
|
130
|
+
// justified rather than a static default list.
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/** A compiled SCHEDULE obligation, shaped as lib/plan/compile.mjs emits it. */
|
|
134
|
+
function measureObligation(over = {}) {
|
|
135
|
+
return {
|
|
136
|
+
key: "schedule.measure.pipeline-coverage",
|
|
137
|
+
kind: "SCHEDULE",
|
|
138
|
+
cadence_id: "measure-pipeline-coverage",
|
|
139
|
+
scope: "mandate",
|
|
140
|
+
mode: "guarded",
|
|
141
|
+
prompt: "schedules/triggers/measure-pipeline-coverage.md",
|
|
142
|
+
schedule: { calendar: { weekday: 2, hour: 14, minute: 30 } },
|
|
143
|
+
cadence: "weekly",
|
|
144
|
+
objective_id: "obj_7Kx",
|
|
145
|
+
status: "active",
|
|
146
|
+
...over,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
test("plan leg: an adopted objective's measure obligation becomes a real cadence", async () => {
|
|
151
|
+
const cads = await resolveCadences({
|
|
152
|
+
function: "commercial-leader",
|
|
153
|
+
altitude: "vp",
|
|
154
|
+
obligations: [measureObligation()],
|
|
155
|
+
});
|
|
156
|
+
const c = cads.find((x) => x.id === "measure-pipeline-coverage");
|
|
157
|
+
assert.ok(c, "mandate-derived cadence resolves");
|
|
158
|
+
assert.equal(c.scope, "mandate");
|
|
159
|
+
assert.equal(c.mode, "guarded");
|
|
160
|
+
// The compiler solved this slot against the principal's calendar — the
|
|
161
|
+
// resolver's own scheduleFor() stagger must NOT overwrite it.
|
|
162
|
+
assert.deepEqual(c.calendar, { weekday: 2, hour: 14, minute: 30 });
|
|
163
|
+
// Provenance rides along so a tick can be walked back to its objective.
|
|
164
|
+
assert.equal(c.obligationKey, "schedule.measure.pipeline-coverage");
|
|
165
|
+
assert.equal(c.objectiveId, "obj_7Kx");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("plan leg: the standard + archetype floor is RETAINED, never replaced", async () => {
|
|
169
|
+
// An agent whose human has adopted nothing must keep running today's cadences.
|
|
170
|
+
const withPlan = await resolveCadences({ function: "commercial-leader", altitude: "vp", obligations: [measureObligation()] });
|
|
171
|
+
const withoutPlan = await resolveCadences({ function: "commercial-leader", altitude: "vp" });
|
|
172
|
+
const baseIds = withoutPlan.map((c) => c.id);
|
|
173
|
+
const planIds = withPlan.map((c) => c.id);
|
|
174
|
+
for (const id of baseIds) assert.ok(planIds.includes(id), `${id} survives the plan leg`);
|
|
175
|
+
assert.equal(planIds.length, baseIds.length + 1, "the plan leg only ADDS");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("plan leg: an empty/absent obligation list is exactly today's behaviour", async () => {
|
|
179
|
+
const none = await resolveCadences({ function: "technical-leader", altitude: "c-suite" });
|
|
180
|
+
const empty = await resolveCadences({ function: "technical-leader", altitude: "c-suite", obligations: [] });
|
|
181
|
+
assert.deepEqual(empty, none);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("plan leg: a plan obligation cannot re-slot or override a standard cadence", async () => {
|
|
185
|
+
// First writer wins. A mandate obligation colliding with `inbox-processor`
|
|
186
|
+
// must not move it — that is what keeps existing launchd plists from churning.
|
|
187
|
+
const base = await resolveCadences({ function: "technical-leader", altitude: "c-suite" });
|
|
188
|
+
const original = base.find((c) => c.id === "inbox-processor");
|
|
189
|
+
const cads = await resolveCadences({
|
|
190
|
+
function: "technical-leader",
|
|
191
|
+
altitude: "c-suite",
|
|
192
|
+
obligations: [measureObligation({ cadence_id: "inbox-processor", schedule: { interval: 99999 }, key: "schedule.hijack" })],
|
|
193
|
+
});
|
|
194
|
+
const after = cads.find((c) => c.id === "inbox-processor");
|
|
195
|
+
assert.deepEqual(after, original, "the standard cadence is untouched");
|
|
196
|
+
assert.equal(cads.filter((c) => c.id === "inbox-processor").length, 1, "no duplicate entry");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("plan leg: non-active / non-SCHEDULE obligations emit no cadence", async () => {
|
|
200
|
+
const cads = await resolveCadences({
|
|
201
|
+
function: "technical-leader",
|
|
202
|
+
altitude: "c-suite",
|
|
203
|
+
obligations: [
|
|
204
|
+
measureObligation({ cadence_id: "suspended-job", status: "suspended", key: "schedule.suspended" }),
|
|
205
|
+
{ key: "outcome.pipeline-coverage", kind: "OUTCOME", cadence_id: "outcome-job", status: "active" },
|
|
206
|
+
{ key: "react.email.received", kind: "REACT", status: "active", trigger: { topic: "email" } },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
const ids = cads.map((c) => c.id);
|
|
210
|
+
assert.ok(!ids.includes("suspended-job"), "a suspended obligation is not scheduled");
|
|
211
|
+
assert.ok(!ids.includes("outcome-job"), "an OUTCOME is measured, not launchd-scheduled");
|
|
212
|
+
assert.ok(!ids.includes("react.email.received"), "a REACT is event-driven, not scheduled");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("plan leg: an interval obligation carries its interval through", async () => {
|
|
216
|
+
const cads = await resolveCadences({
|
|
217
|
+
function: "technical-leader",
|
|
218
|
+
altitude: "c-suite",
|
|
219
|
+
obligations: [measureObligation({ cadence_id: "fast-sensor", schedule: { interval: 300 }, key: "schedule.fast" })],
|
|
220
|
+
});
|
|
221
|
+
const c = cads.find((x) => x.id === "fast-sensor");
|
|
222
|
+
assert.equal(c.interval, 300);
|
|
223
|
+
assert.equal(c.calendar, undefined);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("plan leg: founder altitude resolves its own cadences (was unreachable from the wizard)", async () => {
|
|
227
|
+
const ids = (await resolveCadences({ function: "finance-leader", altitude: "founder" })).map((c) => c.id);
|
|
228
|
+
assert.ok(ids.includes("weekly-runway-review"), "founder gets the runway review");
|
|
229
|
+
assert.ok(ids.includes("monthly-investor-update"), "founder gets the investor update");
|
|
230
|
+
});
|