@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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for check-subagent-frontmatter.mjs — the sub-agent definition guard.
|
|
3
|
+
*
|
|
4
|
+
* Runs against throwaway trees under os.tmpdir() rather than the repo's own
|
|
5
|
+
* agents/, so the tests still describe the guard's behaviour when the repo is
|
|
6
|
+
* clean (a guard that can only be tested by breaking the repo is a guard nobody
|
|
7
|
+
* tests).
|
|
8
|
+
*
|
|
9
|
+
* Run: node --test scripts/ci/check-subagent-frontmatter.test.mjs
|
|
10
|
+
* @module scripts/ci/check-subagent-frontmatter.test
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { findInvalidAgents } from "./check-subagent-frontmatter.mjs";
|
|
20
|
+
|
|
21
|
+
/** Build a temp repo root containing `agents/<slug>/agent.md` for each entry. */
|
|
22
|
+
function makeTree(agents) {
|
|
23
|
+
const root = mkdtempSync(join(tmpdir(), "subagent-guard-"));
|
|
24
|
+
for (const [slug, text] of Object.entries(agents)) {
|
|
25
|
+
mkdirSync(join(root, "agents", slug), { recursive: true });
|
|
26
|
+
if (text !== null)
|
|
27
|
+
writeFileSync(join(root, "agents", slug, "agent.md"), text);
|
|
28
|
+
}
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const VALID = [
|
|
33
|
+
"---",
|
|
34
|
+
"name: alpha",
|
|
35
|
+
"description: Does the alpha work.",
|
|
36
|
+
"model: claude-sonnet-4-6",
|
|
37
|
+
'tools: ["Read", "Grep"]',
|
|
38
|
+
"---",
|
|
39
|
+
"",
|
|
40
|
+
"You are the alpha sub-agent.",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
describe("findInvalidAgents", () => {
|
|
45
|
+
it("passes a well-formed definition", () => {
|
|
46
|
+
const { offenders, checked } = findInvalidAgents(
|
|
47
|
+
makeTree({ alpha: VALID }),
|
|
48
|
+
);
|
|
49
|
+
assert.deepEqual(offenders, []);
|
|
50
|
+
assert.equal(checked, 1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("catches the exact defect that shipped: frontmatter with no tools key", () => {
|
|
54
|
+
// This is the whole reason the guard exists — 57 of 70 agents in this repo
|
|
55
|
+
// were generated in precisely this shape and every one failed at dispatch.
|
|
56
|
+
const noTools = VALID.split("\n")
|
|
57
|
+
.filter((l) => !l.startsWith("tools:"))
|
|
58
|
+
.join("\n");
|
|
59
|
+
const { offenders } = findInvalidAgents(makeTree({ alpha: noTools }));
|
|
60
|
+
assert.equal(offenders.length, 1);
|
|
61
|
+
assert.equal(offenders[0].slug, "alpha");
|
|
62
|
+
assert.ok(
|
|
63
|
+
offenders[0].errors.some((e) => e.includes("tools")),
|
|
64
|
+
`expected a tools error, got: ${offenders[0].errors.join("; ")}`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("fails an EMPTY agents/<slug>/ directory rather than passing it", () => {
|
|
69
|
+
// doctor used to accept this, which is how an agent could be referenced
|
|
70
|
+
// everywhere and defined nowhere.
|
|
71
|
+
const { offenders } = findInvalidAgents(makeTree({ ghost: null }));
|
|
72
|
+
assert.equal(offenders.length, 1);
|
|
73
|
+
assert.equal(offenders[0].slug, "ghost");
|
|
74
|
+
assert.match(offenders[0].errors[0], /no agent\.md/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("fails when frontmatter.name disagrees with the directory name", () => {
|
|
78
|
+
const renamed = VALID.replace("name: alpha", "name: beta");
|
|
79
|
+
const { offenders } = findInvalidAgents(makeTree({ alpha: renamed }));
|
|
80
|
+
assert.equal(offenders.length, 1);
|
|
81
|
+
assert.ok(offenders[0].errors.some((e) => e.includes("must equal")));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("fails a body that uses a token its tokens list does not declare", () => {
|
|
85
|
+
const undeclared = VALID.replace(
|
|
86
|
+
"You are the alpha sub-agent.",
|
|
87
|
+
"You are {{agent.fullName}}'s alpha sub-agent.",
|
|
88
|
+
).replace('tools: ["Read", "Grep"]', 'tools: ["Read"]\ntokens: []');
|
|
89
|
+
const { offenders } = findInvalidAgents(makeTree({ alpha: undeclared }));
|
|
90
|
+
assert.equal(offenders.length, 1);
|
|
91
|
+
assert.ok(offenders[0].errors.some((e) => e.includes("undeclared token")));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("reports an unknown declared token as a WARNING, not a build failure", () => {
|
|
95
|
+
// Advisory by design: hq does not vendor KNOWN_TOKENS, so a body authored
|
|
96
|
+
// against a newer vocabulary must not hard-fail an older agent's build.
|
|
97
|
+
const odd = VALID.replace(
|
|
98
|
+
'tools: ["Read", "Grep"]',
|
|
99
|
+
'tools: ["Read"]\ntokens: ["agent.notAThing"]',
|
|
100
|
+
);
|
|
101
|
+
const { offenders, warned } = findInvalidAgents(makeTree({ alpha: odd }));
|
|
102
|
+
assert.deepEqual(offenders, []);
|
|
103
|
+
assert.equal(warned.length, 1);
|
|
104
|
+
assert.ok(warned[0].warnings.some((w) => w.includes("agent.notAThing")));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("reports every offender, not just the first", () => {
|
|
108
|
+
const noTools = VALID.split("\n")
|
|
109
|
+
.filter((l) => !l.startsWith("tools:"))
|
|
110
|
+
.join("\n");
|
|
111
|
+
const { offenders, checked } = findInvalidAgents(
|
|
112
|
+
makeTree({ alpha: VALID, beta: noTools, gamma: noTools }),
|
|
113
|
+
);
|
|
114
|
+
assert.equal(checked, 3);
|
|
115
|
+
assert.deepEqual(offenders.map((o) => o.slug).sort(), ["beta", "gamma"]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("treats a missing agents/ directory as nothing to check, not an error", () => {
|
|
119
|
+
const root = mkdtempSync(join(tmpdir(), "subagent-guard-empty-"));
|
|
120
|
+
const { offenders, checked } = findInvalidAgents(root);
|
|
121
|
+
assert.deepEqual(offenders, []);
|
|
122
|
+
assert.equal(checked, 0);
|
|
123
|
+
});
|
|
124
|
+
});
|
package/scripts/ci/check.mjs
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* - check-no-residual-identity: de-Robin'd paths stay persona-agnostic
|
|
20
20
|
* - check-no-build-artifacts : no tracked .pyc/symlinks reach the tarball
|
|
21
21
|
* - check-docs-accuracy : README links resolve; counts/agent-refs WARN
|
|
22
|
+
* - check-subagent-frontmatter: every agents/*.md passes the registry validator
|
|
22
23
|
*
|
|
23
24
|
* Usage: `node scripts/ci/check.mjs`
|
|
24
25
|
* exit 0 → all checks passed; exit 1 → one or more failed.
|
|
@@ -36,6 +37,7 @@ import { run as runUnresolvedTokens } from "./check-unresolved-tokens.mjs";
|
|
|
36
37
|
import { run as runNoResidualIdentity } from "./check-no-residual-identity.mjs";
|
|
37
38
|
import { run as runNoBuildArtifacts } from "./check-no-build-artifacts.mjs";
|
|
38
39
|
import { run as runDocsAccuracy } from "./check-docs-accuracy.mjs";
|
|
40
|
+
import { run as runSubagentFrontmatter } from "./check-subagent-frontmatter.mjs";
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* The ordered list of guards this aggregator runs.
|
|
@@ -51,6 +53,7 @@ export const CHECKS = [
|
|
|
51
53
|
{ name: "check-no-residual-identity", run: runNoResidualIdentity },
|
|
52
54
|
{ name: "check-no-build-artifacts", run: runNoBuildArtifacts },
|
|
53
55
|
{ name: "check-docs-accuracy", run: runDocsAccuracy },
|
|
56
|
+
{ name: "check-subagent-frontmatter", run: runSubagentFrontmatter },
|
|
54
57
|
];
|
|
55
58
|
|
|
56
59
|
/**
|
|
@@ -441,6 +441,22 @@ export function buildProbes() {
|
|
|
441
441
|
guard: "ghost",
|
|
442
442
|
why: "the task target 404s before the Escalation row",
|
|
443
443
|
},
|
|
444
|
+
{
|
|
445
|
+
// The OpenQuestion sibling. Two DISTINCT options are mandatory server-side
|
|
446
|
+
// (one option is a notification, none is prose), and `context` is a STRING
|
|
447
|
+
// — the exact shape the contract row asserts, so the probe tests it rather
|
|
448
|
+
// than restating it. Ghost-guarded on the task target like its sibling.
|
|
449
|
+
method: "escalation.ask",
|
|
450
|
+
params: {
|
|
451
|
+
question: "conformance probe — which option?",
|
|
452
|
+
options: ["probe-option-a", "probe-option-b"],
|
|
453
|
+
context: "raised by the org-api conformance probe",
|
|
454
|
+
taskId: ghostTask,
|
|
455
|
+
},
|
|
456
|
+
writes: false,
|
|
457
|
+
guard: "ghost",
|
|
458
|
+
why: "the task target 404s before any OpenQuestion row is created",
|
|
459
|
+
},
|
|
444
460
|
{
|
|
445
461
|
method: "knowledge.invalidate",
|
|
446
462
|
params: { id: ghostId("epi"), reason: "conformance probe" },
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/ci/journey-approval-escalation.mjs — ONE JOURNEY, END TO END, LIVE.
|
|
4
|
+
*
|
|
5
|
+
* ── WHY THIS EXISTS ──
|
|
6
|
+
* Every layer of the inbound → execution stack has unit tests and every one of
|
|
7
|
+
* them passes with fakes on both sides of the joint. That proves the layers, not
|
|
8
|
+
* the seams. This script drives ONE real journey against a REAL org and prints
|
|
9
|
+
* what happened at every hop, so "the units connect" is a thing you can read
|
|
10
|
+
* rather than a thing you assert:
|
|
11
|
+
*
|
|
12
|
+
* hop 1 hq approval.request → an Approval row
|
|
13
|
+
* hop 2 hq ctx.appendEvent → an `approval.requested` frame
|
|
14
|
+
* hop 3 SDK pullWideInbound → the frame off /v1/events
|
|
15
|
+
* hop 4 SDK directedness.resolveDirected → "mine, because <reason>"
|
|
16
|
+
* hop 5 SDK match.matchObligation → the plan obligation it binds to
|
|
17
|
+
* hop 6 SDK route.routeRung → rung + mechanism + gates
|
|
18
|
+
* hop 7 SDK drive → effects.escalate → escalation.ask (an OpenQuestion)
|
|
19
|
+
* hop 8 SDK journal.recordOutcome → state/execution/journal.jsonl
|
|
20
|
+
* hop 9 SDK kpi-sensors → is this lane measurable at all?
|
|
21
|
+
*
|
|
22
|
+
* ── WHAT IS REAL AND WHAT IS NOT ──
|
|
23
|
+
* REAL: the org, the credentials, the `/v1/events` ledger page, the board read,
|
|
24
|
+
* the channel roster, the directedness resolver, the whole execution ladder, the
|
|
25
|
+
* journal write, and the exact JSON body the escalation write would put on the
|
|
26
|
+
* wire.
|
|
27
|
+
*
|
|
28
|
+
* NOT REAL, and deliberately so: the Approval row itself. Creating one means
|
|
29
|
+
* writing to a production org, so the row and its ledger frame are synthesised —
|
|
30
|
+
* the frame BYTE-FOR-BYTE as `hq src/server/methods/approval/request.ts`
|
|
31
|
+
* appends it, the row byte-for-byte as `hq methods/approval/_shared.ts
|
|
32
|
+
* #projectApproval` returns it. Everything downstream of the frame is the
|
|
33
|
+
* shipping code path with nothing stubbed.
|
|
34
|
+
*
|
|
35
|
+
* EVERY SIDE-EFFECTING POST IS BLOCKED. The guard consults the vendored protocol
|
|
36
|
+
* table and refuses anything `sideEffecting:true`, printing the body instead. So
|
|
37
|
+
* this script can be run against production without writing to it — which is the
|
|
38
|
+
* only way it is useful, because a journey you can only run against an empty
|
|
39
|
+
* fixture org is a journey you have not tested.
|
|
40
|
+
*
|
|
41
|
+
* ── USAGE ──
|
|
42
|
+
* AGENT_DIR=/path/to/agent \
|
|
43
|
+
* COHORT_BASE=… COHORT_ORG_ID=… COHORT_AGENT_ID=… COHORT_API_KEY=… \
|
|
44
|
+
* node scripts/ci/journey-approval-escalation.mjs
|
|
45
|
+
*
|
|
46
|
+
* --allow-writes actually send the escalation write (NOT the default)
|
|
47
|
+
* --json machine-readable hop report on stdout
|
|
48
|
+
*
|
|
49
|
+
* Exit 0 when every hop moved; exit 1 naming the first hop that did not.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
"use strict";
|
|
53
|
+
|
|
54
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
55
|
+
import { join } from "node:path";
|
|
56
|
+
|
|
57
|
+
import { METHODS } from "../../lib/org/protocol.mjs";
|
|
58
|
+
import { loadOrgConfig } from "../../lib/org/client.mjs";
|
|
59
|
+
|
|
60
|
+
const ARGV = new Set(process.argv.slice(2));
|
|
61
|
+
const ALLOW_WRITES = ARGV.has("--allow-writes");
|
|
62
|
+
const AS_JSON = ARGV.has("--json");
|
|
63
|
+
|
|
64
|
+
const AGENT_DIR = process.env.AGENT_DIR || process.env.AGENT_ROOT || process.cwd();
|
|
65
|
+
const BASE = process.env.COHORT_BASE || "";
|
|
66
|
+
const KEY = process.env.COHORT_API_KEY || process.env.COHORT_API_TOKEN || "";
|
|
67
|
+
const ME = process.env.COHORT_AGENT_ID || "";
|
|
68
|
+
|
|
69
|
+
const hops = [];
|
|
70
|
+
const writes = [];
|
|
71
|
+
|
|
72
|
+
function hop(n, name, ok, detail) {
|
|
73
|
+
hops.push({ n, name, ok, detail });
|
|
74
|
+
const mark = ok ? "✓" : "✗";
|
|
75
|
+
if (!AS_JSON) console.log(`\n${mark} HOP ${n} ${name}`);
|
|
76
|
+
if (!AS_JSON && detail) console.log(String(detail).replace(/^/gm, " "));
|
|
77
|
+
return ok;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function die(n, name, why) {
|
|
81
|
+
hop(n, name, false, why);
|
|
82
|
+
report();
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function report() {
|
|
87
|
+
if (AS_JSON) {
|
|
88
|
+
console.log(JSON.stringify({ hops, writes }, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log("\n──────────────────────────────────────────────────────────────");
|
|
92
|
+
for (const h of hops) console.log(` ${h.ok ? "✓" : "✗"} hop ${h.n} ${h.name}`);
|
|
93
|
+
console.log("──────────────────────────────────────────────────────────────");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// the fetch guard: live reads, blocked writes, one synthesised row
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wrap `globalThis.fetch` so that:
|
|
102
|
+
* - `GET /v1/events` is served live and the synthetic frame is appended;
|
|
103
|
+
* - `approval.get` for the synthetic id returns the synthetic row;
|
|
104
|
+
* - any POST to a `sideEffecting` protocol method is RECORDED, not sent.
|
|
105
|
+
*
|
|
106
|
+
* @param {{seq:number, approvalId:string, frame:object, row:object}} synth
|
|
107
|
+
*/
|
|
108
|
+
function installGuard(synth) {
|
|
109
|
+
const real = globalThis.fetch;
|
|
110
|
+
const json = (o) => new Response(JSON.stringify(o), { status: 200, headers: { "content-type": "application/json" } });
|
|
111
|
+
|
|
112
|
+
globalThis.fetch = async (input, init = {}) => {
|
|
113
|
+
const url = String(typeof input === "string" ? input : (input && input.url) || input);
|
|
114
|
+
const method = String(init.method || "GET").toUpperCase();
|
|
115
|
+
const m = url.match(/\/api\/v1\/([^?]+)/);
|
|
116
|
+
const rpc = m ? m[1] : "";
|
|
117
|
+
|
|
118
|
+
if (rpc === "approval.get") {
|
|
119
|
+
const body = init.body ? JSON.parse(init.body) : {};
|
|
120
|
+
if (String(body.id) === synth.approvalId) return json({ ok: true, result: synth.row });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (rpc === "events" && method === "GET") {
|
|
124
|
+
const r = await real(input, init);
|
|
125
|
+
const j = await r.json();
|
|
126
|
+
const events = Array.isArray(j.events) ? j.events : [];
|
|
127
|
+
events.push(synth.frame);
|
|
128
|
+
return json({ events });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const d = METHODS[rpc];
|
|
132
|
+
if (method === "POST" && d && d.sideEffecting && !ALLOW_WRITES) {
|
|
133
|
+
const params = init.body ? JSON.parse(init.body) : {};
|
|
134
|
+
writes.push({ method: rpc, params });
|
|
135
|
+
return json({ ok: true, result: { id: `blocked_${writes.length}`, __blocked: true } });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (method === "POST" && d && d.sideEffecting) writes.push({ method: rpc, params: init.body ? JSON.parse(init.body) : {} });
|
|
139
|
+
return real(input, init);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
async function main() {
|
|
146
|
+
if (!BASE || !KEY || !ME) {
|
|
147
|
+
console.error("set COHORT_BASE, COHORT_API_KEY and COHORT_AGENT_ID");
|
|
148
|
+
process.exit(2);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// agent cfg exactly as `cadence-handlers.loadAgentCfg` assembles it
|
|
152
|
+
let cfg = {};
|
|
153
|
+
try {
|
|
154
|
+
const p = join(AGENT_DIR, "config", "agent.json");
|
|
155
|
+
if (existsSync(p)) cfg = JSON.parse(readFileSync(p, "utf8"));
|
|
156
|
+
} catch { /* fail-open */ }
|
|
157
|
+
const orgCfg = loadOrgConfig(AGENT_DIR);
|
|
158
|
+
if (orgCfg && orgCfg.org) cfg.org = orgCfg.org;
|
|
159
|
+
if (!cfg.org || !cfg.org.cohort) die(0, "agent config", "no `org.cohort` in config/org.yaml — nothing to talk to");
|
|
160
|
+
|
|
161
|
+
const auth = { authorization: `Bearer ${KEY}` };
|
|
162
|
+
|
|
163
|
+
// ── the ledger head, so the synthetic frame is the ONLY new row ──────────
|
|
164
|
+
let head = 0;
|
|
165
|
+
{
|
|
166
|
+
let c = 0;
|
|
167
|
+
for (let page = 0; page < 200; page += 1) {
|
|
168
|
+
const r = await fetch(`${BASE}/api/v1/events?cursor=${c}&limit=500`, { headers: auth });
|
|
169
|
+
const j = await r.json();
|
|
170
|
+
const e = Array.isArray(j.events) ? j.events : [];
|
|
171
|
+
if (!e.length) break;
|
|
172
|
+
c = e[e.length - 1].seq;
|
|
173
|
+
if (e.length < 500) break;
|
|
174
|
+
}
|
|
175
|
+
head = c;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── a REAL board item this seat owns or reviews: the thing the approval blocks
|
|
179
|
+
const { collectTasks } = await import("../../lib/org/inbound/facts.mjs");
|
|
180
|
+
const board = await (await fetch(`${BASE}/api/v1/board.context`, { headers: auth })).json();
|
|
181
|
+
const tasks = collectTasks(board);
|
|
182
|
+
const mine = tasks.filter(
|
|
183
|
+
(t) => String(t.assigneeId || t.assignee || "") === ME || String(t.reviewerId || t.reviewer || "") === ME,
|
|
184
|
+
);
|
|
185
|
+
if (!mine.length) die(0, "fixture", `no live board item names ${ME} as assignee or reviewer — nothing to hang an approval on`);
|
|
186
|
+
const task = mine.find((t) => String(t.reviewerId || "") === ME) || mine[0];
|
|
187
|
+
const requester = String(task.assigneeId || task.assignee || "") || "unknown";
|
|
188
|
+
|
|
189
|
+
// ── hop 1 + 2: the hq write and its chain append, synthesised to hq's shape
|
|
190
|
+
const approvalId = `aprv_journey_${Date.now().toString(36)}`;
|
|
191
|
+
const payloadHash = "0".repeat(64);
|
|
192
|
+
const row = {
|
|
193
|
+
id: approvalId,
|
|
194
|
+
requester,
|
|
195
|
+
approver: null, // hq: NULL until resolved (G5)
|
|
196
|
+
actionClass: "exec",
|
|
197
|
+
payloadHash,
|
|
198
|
+
status: "pending",
|
|
199
|
+
itemId: task.id,
|
|
200
|
+
reason: null,
|
|
201
|
+
subject: { note: "journey harness — synthetic, never written to the org" },
|
|
202
|
+
createdAt: new Date().toISOString(),
|
|
203
|
+
expiresAt: null,
|
|
204
|
+
decidedAt: null,
|
|
205
|
+
};
|
|
206
|
+
const frame = {
|
|
207
|
+
seq: head + 1,
|
|
208
|
+
family: "approval",
|
|
209
|
+
kind: "approval.requested",
|
|
210
|
+
entity_id: approvalId,
|
|
211
|
+
actor: requester,
|
|
212
|
+
at: new Date().toISOString(),
|
|
213
|
+
payload: { requester, actionClass: "exec", payloadHash, itemId: task.id },
|
|
214
|
+
prev_hash: "",
|
|
215
|
+
row_hash: "",
|
|
216
|
+
};
|
|
217
|
+
hop(1, "hq approval.request → Approval row", true,
|
|
218
|
+
`hq src/server/methods/approval/request.ts\n` +
|
|
219
|
+
`approval ${approvalId} blocks task ${task.id} (${task.title})\nrequester ${requester}, approver NULL (G5), status pending`);
|
|
220
|
+
hop(2, "hq ctx.appendEvent → approval.requested", true,
|
|
221
|
+
`family=approval kind=approval.requested seq=${frame.seq}\n${JSON.stringify(frame.payload)}`);
|
|
222
|
+
|
|
223
|
+
installGuard({ seq: frame.seq, approvalId, frame, row });
|
|
224
|
+
|
|
225
|
+
// ── hop 3: delivery ──────────────────────────────────────────────────────
|
|
226
|
+
const { pullWideInbound } = await import("../../lib/org/inbound/index.mjs");
|
|
227
|
+
const seen = [];
|
|
228
|
+
const log = (lvl, msg) => { seen.push(`[${lvl}] ${msg}`); };
|
|
229
|
+
const pulled = await pullWideInbound({ cfg, agentId: ME, cursor: head, log });
|
|
230
|
+
const ok3 = pulled.events.length > 0;
|
|
231
|
+
hop(3, "SDK pullWideInbound → the frame off /v1/events", ok3,
|
|
232
|
+
`scanned=${pulled.stats.scanned} classified=${pulled.stats.classified} directed=${pulled.stats.directed} delivered=${pulled.stats.delivered}\n` +
|
|
233
|
+
`dropped=${JSON.stringify(pulled.stats.dropped)}\ndegraded=${JSON.stringify(pulled.stats.degraded)}\n` +
|
|
234
|
+
seen.join("\n"));
|
|
235
|
+
if (!ok3) die(3, "SDK pullWideInbound", `nothing was delivered — dropped: ${JSON.stringify(pulled.stats.dropped)}`);
|
|
236
|
+
|
|
237
|
+
let roundTrip = "not attempted";
|
|
238
|
+
const ev = pulled.events.find((e) => e.cohort && e.cohort.surface === "approval") || pulled.events[0];
|
|
239
|
+
|
|
240
|
+
// ── hop 4: directedness ─────────────────────────────────────────────────
|
|
241
|
+
hop(4, "directedness → mine, and why", !!(ev.cohort && ev.cohort.reason),
|
|
242
|
+
`surface=${ev.cohort.surface} reason=${ev.cohort.reason} family=${ev.cohort.family} kind=${ev.cohort.event_kind}\n` +
|
|
243
|
+
`subject: ${ev.subject}\ntext: ${ev.text}`);
|
|
244
|
+
|
|
245
|
+
// ── hops 5-8: the execution ladder ──────────────────────────────────────
|
|
246
|
+
process.env.MAESTRO_DAEMON_NO_AUTOSTART = "1";
|
|
247
|
+
const { eventToInboxItem } = await import("../../lib/channels/inbox-item.mjs");
|
|
248
|
+
const daemon = await import("../daemon/agent-daemon.mjs");
|
|
249
|
+
|
|
250
|
+
// THE REAL POLL LANE, not a shortcut past it. `guardMessagingInbound` writes
|
|
251
|
+
// the flattened item to `state/inbox/cohort/*.yaml` and the daemon's scan
|
|
252
|
+
// poller reads it back; anything the writer does not enumerate dies in
|
|
253
|
+
// between. Going straight from MessageEvent to the ladder would hide exactly
|
|
254
|
+
// that class of bug — which is how the chain kind, the directedness reason and
|
|
255
|
+
// the subject id all came to be dropped at this boundary.
|
|
256
|
+
const flat = eventToInboxItem(ev);
|
|
257
|
+
const { writeInboxItem } = await import("../poller/utils.mjs");
|
|
258
|
+
const { parseInboxItemYaml } = await import("../poller/inbox-scan-poller.mjs");
|
|
259
|
+
let item = flat;
|
|
260
|
+
try {
|
|
261
|
+
writeInboxItem("cohort", flat, AGENT_DIR);
|
|
262
|
+
const dir = join(AGENT_DIR, "state", "inbox", "cohort");
|
|
263
|
+
const { readdirSync } = await import("node:fs");
|
|
264
|
+
const file = readdirSync(dir).filter((f) => f.includes(String(flat.id))).pop();
|
|
265
|
+
if (file && typeof parseInboxItemYaml === "function") {
|
|
266
|
+
item = parseInboxItemYaml(readFileSync(join(dir, file), "utf8"));
|
|
267
|
+
roundTrip = `wrote + re-read ${file}`;
|
|
268
|
+
} else {
|
|
269
|
+
roundTrip = `WROTE but could not re-read (parseInboxItemYaml exported? ${typeof parseInboxItemYaml}) — using the in-memory item`;
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
roundTrip = `YAML round-trip skipped: ${err.message}`;
|
|
273
|
+
}
|
|
274
|
+
daemon.enrichItem(item, "cohort");
|
|
275
|
+
const gate = await daemon.runExecutionLadder(item, "cohort", item.raw_ref || item.id, "journey", {
|
|
276
|
+
answerItem: async () => ({ ok: true, path: "journey-stub" }),
|
|
277
|
+
});
|
|
278
|
+
hop(3.5, "poll-lane YAML round-trip", !/skipped|could not/.test(roundTrip), roundTrip);
|
|
279
|
+
|
|
280
|
+
const d = gate.decision;
|
|
281
|
+
if (!d) die(5, "execution ladder", "the ladder produced no decision at all");
|
|
282
|
+
|
|
283
|
+
hop(5, "obligation match", true,
|
|
284
|
+
d.obligationKey ? `bound to ${d.obligationKey}` : "UNCOVERED — no compiled REACT obligation matched (config/plan.yaml)");
|
|
285
|
+
hop(6, "rung decision", Number.isFinite(d.rung),
|
|
286
|
+
`disposition=${d.disposition} reason=${d.reason} rung=${d.rung} mechanism=${d.mechanism}\n` +
|
|
287
|
+
`gates=${JSON.stringify(d.gates)}\nwhy:\n` + (d.why || []).map((w) => ` - ${w}`).join("\n"));
|
|
288
|
+
|
|
289
|
+
const escalation = writes.find((w) => w.method === "escalation.ask" || w.method === "escalation.create");
|
|
290
|
+
hop(7, "the action taken", !!escalation,
|
|
291
|
+
escalation
|
|
292
|
+
? `${escalation.method}\n${JSON.stringify(escalation.params, null, 2)}`
|
|
293
|
+
: `NO escalation write was attempted. writes=${JSON.stringify(writes.map((w) => w.method))}`);
|
|
294
|
+
|
|
295
|
+
// ── hop 8: the journal ──────────────────────────────────────────────────
|
|
296
|
+
const { journalPath } = await import("../../lib/execution/journal.mjs");
|
|
297
|
+
const jp = journalPath(AGENT_DIR);
|
|
298
|
+
let tail = [];
|
|
299
|
+
try {
|
|
300
|
+
tail = readFileSync(jp, "utf8").trim().split("\n").map((l) => JSON.parse(l))
|
|
301
|
+
.filter((r) => String(r.key || "").includes(String(frame.seq)));
|
|
302
|
+
} catch (err) {
|
|
303
|
+
hop(8, "outcome journalled", false, `${jp} unreadable: ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
hop(8, "outcome journalled", tail.length > 0,
|
|
306
|
+
tail.length ? `${jp}\n` + tail.map((r) => JSON.stringify(r)).join("\n")
|
|
307
|
+
: `no row in ${jp} carries this event's key`);
|
|
308
|
+
|
|
309
|
+
// ── hop 9: the KPI ──────────────────────────────────────────────────────
|
|
310
|
+
// The goal loop measures against the CAPABILITY SENSORS, so the honest check
|
|
311
|
+
// is whether the surface this journey exercised is reachable by one at all.
|
|
312
|
+
let kpiDetail = "";
|
|
313
|
+
let kpiOk = false;
|
|
314
|
+
try {
|
|
315
|
+
const { buildSensorRegistry, reachableCapabilities } = await import("../../lib/kpi-sensors.mjs");
|
|
316
|
+
const reg = buildSensorRegistry({ agentRoot: AGENT_DIR, memberId: ME });
|
|
317
|
+
const reach = reachableCapabilities(AGENT_DIR);
|
|
318
|
+
const names = Object.keys(reg || {});
|
|
319
|
+
const escalationSensors = names.filter((n) => /escal|approv|decision/i.test(n));
|
|
320
|
+
kpiOk = escalationSensors.length > 0;
|
|
321
|
+
kpiDetail =
|
|
322
|
+
`${names.length} sensor(s) registered, ${reach ? reach.size : "n/a"} reachable capabilities\n` +
|
|
323
|
+
(escalationSensors.length
|
|
324
|
+
? `governance sensors: ${escalationSensors.join(", ")}`
|
|
325
|
+
: "NO sensor measures the escalation/approval/decision lane — this journey moves no KPI");
|
|
326
|
+
} catch (err) {
|
|
327
|
+
kpiDetail = `kpi-sensors unavailable: ${err.message}`;
|
|
328
|
+
}
|
|
329
|
+
hop(9, "KPI — is this lane measurable at all?", kpiOk, kpiDetail +
|
|
330
|
+
"\n(NOTE: this journey moved no KPI number. `escalate` is journalled, not scored — " +
|
|
331
|
+
"the goal loop measures obligations, and this event bound to none.)");
|
|
332
|
+
|
|
333
|
+
report();
|
|
334
|
+
const failed = hops.find((h) => !h.ok);
|
|
335
|
+
process.exit(failed ? 1 : 0);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
main().catch((err) => {
|
|
339
|
+
console.error("[journey] fatal:", err && err.stack ? err.stack : err);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
});
|