@cohortapp/agent-sdk 2.12.0 → 2.13.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 +6 -2
- package/docs/guides/front-door-session.md +49 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/global-setup-extras.mjs +44 -0
- package/lib/cli/global-setup-extras.test.mjs +95 -0
- package/lib/cli/session.mjs +11 -1
- package/lib/cli/session.test.mjs +17 -6
- package/lib/collective/global-config.mjs +5 -0
- package/lib/collective/global-config.test.mjs +5 -0
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/package.json +1 -1
- package/plugins/maestro-skills/plugin.json +4 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +3 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon.mjs +108 -0
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +61 -2
- package/scripts/daemon/cadence-consumer.mjs +46 -22
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/local-triggers/autoupdate.test.mjs +33 -3
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-daemon-design.test.mjs — the daemon re-syncs the brand foundation into
|
|
3
|
+
* DESIGN.md + PRODUCT.md, polled every 15 minutes and rewritten when the
|
|
4
|
+
* foundation's version moves (WP-M7 mechanic 4).
|
|
5
|
+
*
|
|
6
|
+
* Drives the REAL `refreshDesignFoundation` with an injected
|
|
7
|
+
* `branding.getFoundation` and a fixed clock under a tmp AGENT_DIR — the same
|
|
8
|
+
* shape as `agent-daemon-board-mine.test.mjs`, including its `imapflow` loader
|
|
9
|
+
* stub, because importing the daemon module pulls the whole seat in.
|
|
10
|
+
*
|
|
11
|
+
* The four claims: the 15-minute gate bounds the reads; a moved version is
|
|
12
|
+
* detected and named on the poll that sees it; an un-enrolled seat writes
|
|
13
|
+
* nothing; and no failure of hq's can blank a DESIGN.md already on disk.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { test, before } from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { register } from "node:module";
|
|
19
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, statSync, utimesSync, rmSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
|
|
23
|
+
const loaderSrc = `
|
|
24
|
+
export async function resolve(specifier, context, next) {
|
|
25
|
+
if (specifier === "imapflow") return { url: "imapflow-stub:main", shortCircuit: true };
|
|
26
|
+
return next(specifier, context);
|
|
27
|
+
}
|
|
28
|
+
export async function load(url, context, next) {
|
|
29
|
+
if (url === "imapflow-stub:main") return { format: "module", shortCircuit: true, source: "export class ImapFlow {}" };
|
|
30
|
+
return next(url, context);
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
register(`data:text/javascript,${encodeURIComponent(loaderSrc)}`);
|
|
34
|
+
|
|
35
|
+
const AGENT_DIR = mkdtempSync(join(tmpdir(), "maestro-daemon-design-"));
|
|
36
|
+
process.env.AGENT_DIR = AGENT_DIR;
|
|
37
|
+
process.env.MAESTRO_DAEMON_NO_AUTOSTART = "1";
|
|
38
|
+
|
|
39
|
+
const NOW = Date.parse("2026-09-08T12:00:00Z");
|
|
40
|
+
const FIFTEEN_MIN = 15 * 60_000;
|
|
41
|
+
const DESIGN = join(AGENT_DIR, "DESIGN.md");
|
|
42
|
+
const PRODUCT = join(AGENT_DIR, "PRODUCT.md");
|
|
43
|
+
const STATE = join(AGENT_DIR, "state", "design", "foundation.json");
|
|
44
|
+
|
|
45
|
+
const FIXTURE = JSON.parse(readFileSync(new URL("../../lib/design/fixtures/foundation.json", import.meta.url), "utf8"));
|
|
46
|
+
|
|
47
|
+
const frame = (over = {}) => ({
|
|
48
|
+
ok: true,
|
|
49
|
+
result: {
|
|
50
|
+
foundation: FIXTURE,
|
|
51
|
+
version: { id: "bv_01HQZ", seq: 7, label: "V2.7" },
|
|
52
|
+
headVersionId: "bv_01HQZ",
|
|
53
|
+
isHead: true,
|
|
54
|
+
unversioned: false,
|
|
55
|
+
history: [],
|
|
56
|
+
...over,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let daemon;
|
|
61
|
+
before(async () => {
|
|
62
|
+
mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
|
|
63
|
+
mkdirSync(join(AGENT_DIR, "state", "sessions", "locks"), { recursive: true });
|
|
64
|
+
writeFileSync(
|
|
65
|
+
join(AGENT_DIR, "config", "org.yaml"),
|
|
66
|
+
"org:\n cohort:\n enabled: true\n base: https://os.example.test\n token: nlk_test_token\n orgId: org_test\n orgName: Northwind\n",
|
|
67
|
+
);
|
|
68
|
+
writeFileSync(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({ firstName: "Nova", lastName: "Reyes" }));
|
|
69
|
+
const url = new URL("./agent-daemon.mjs", import.meta.url);
|
|
70
|
+
daemon = await import(`${url.href}?design=${Math.random()}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the first tick writes DESIGN.md + PRODUCT.md from the seat's own connection", async () => {
|
|
74
|
+
const calls = [];
|
|
75
|
+
const getFoundationImpl = async (params, o) => { calls.push({ params, o }); return frame(); };
|
|
76
|
+
|
|
77
|
+
const first = await daemon.refreshDesignFoundation({ now: NOW, getFoundationImpl });
|
|
78
|
+
assert.equal(first.refreshed, true);
|
|
79
|
+
assert.equal(first.reason, "never", "the gate's own words for a seat that had never synced");
|
|
80
|
+
assert.deepEqual(first.written, ["DESIGN.md", "PRODUCT.md"]);
|
|
81
|
+
assert.equal(first.version, "bv_01HQZ");
|
|
82
|
+
|
|
83
|
+
assert.equal(calls.length, 1);
|
|
84
|
+
assert.deepEqual(calls[0].params, { historyLimit: 0 });
|
|
85
|
+
assert.equal(calls[0].o.base, "https://os.example.test", "the daemon's org.yaml connection is what the read gets");
|
|
86
|
+
assert.equal(calls[0].o.orgId, "org_test");
|
|
87
|
+
|
|
88
|
+
assert.match(readFileSync(DESIGN, "utf8"), /^---\nname: "Northwind"\n/);
|
|
89
|
+
assert.match(readFileSync(PRODUCT, "utf8"), /# Northwind — product voice/);
|
|
90
|
+
assert.equal(statSync(DESIGN).mode & 0o777, 0o644);
|
|
91
|
+
assert.equal(JSON.parse(readFileSync(STATE, "utf8")).version, "bv_01HQZ");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("a tick inside 15 minutes spends no hq call at all", async () => {
|
|
95
|
+
let calls = 0;
|
|
96
|
+
const getFoundationImpl = async () => { calls += 1; return frame(); };
|
|
97
|
+
const soon = await daemon.refreshDesignFoundation({ now: NOW + 14 * 60_000, getFoundationImpl });
|
|
98
|
+
assert.deepEqual(soon, { refreshed: false, reason: "fresh" });
|
|
99
|
+
assert.equal(calls, 0, "the gate is asked BEFORE the read, which is the point of it");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a tick at 15 minutes reads, finds nothing changed, and touches no file", async () => {
|
|
103
|
+
const old = new Date(NOW - 86_400_000);
|
|
104
|
+
utimesSync(DESIGN, old, old);
|
|
105
|
+
utimesSync(PRODUCT, old, old);
|
|
106
|
+
const before = [statSync(DESIGN).mtimeMs, statSync(PRODUCT).mtimeMs];
|
|
107
|
+
|
|
108
|
+
let calls = 0;
|
|
109
|
+
const at = await daemon.refreshDesignFoundation({ now: NOW + FIFTEEN_MIN, getFoundationImpl: async () => { calls += 1; return frame(); } });
|
|
110
|
+
assert.equal(calls, 1, "the interval is what bounds the reads — hq has no cheap etag for this");
|
|
111
|
+
assert.equal(at.refreshed, false, "an unchanged foundation is not a refresh");
|
|
112
|
+
assert.equal(at.reason, "interval");
|
|
113
|
+
assert.deepEqual([statSync(DESIGN).mtimeMs, statSync(PRODUCT).mtimeMs], before, "not one mtime moved");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("a moved foundation is picked up on the next poll, and named 'version-changed' not 'interval'", async () => {
|
|
117
|
+
// The honest bound: hq exposes no cheap "has the foundation changed?" etag
|
|
118
|
+
// the way `integration.toolsetVersion` does, so a version change can only be
|
|
119
|
+
// SEEN by a read, and the 15-minute gate is what bounds the reads. What the
|
|
120
|
+
// gate's second call buys is a truthful reason — the daemon logs why this
|
|
121
|
+
// tick rewrote the file, and "the palette moved" is not "the timer fired".
|
|
122
|
+
const moved = JSON.parse(JSON.stringify(FIXTURE));
|
|
123
|
+
moved.colors[2].value = "#0f172a";
|
|
124
|
+
const r = await daemon.refreshDesignFoundation({
|
|
125
|
+
now: NOW + 2 * FIFTEEN_MIN,
|
|
126
|
+
getFoundationImpl: async () => frame({ foundation: moved, headVersionId: "bv_02", version: { id: "bv_02", seq: 8, label: "V2.8" } }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.equal(r.refreshed, true);
|
|
130
|
+
assert.equal(r.reason, "version-changed", "not 'interval' — the gate's second call names the real cause");
|
|
131
|
+
assert.ok(r.written.includes("DESIGN.md"));
|
|
132
|
+
assert.match(readFileSync(DESIGN, "utf8"), /primary-moss: "#0f172a"/);
|
|
133
|
+
assert.match(readFileSync(DESIGN, "utf8"), /\*\*Version:\*\* V2\.8/);
|
|
134
|
+
assert.equal(JSON.parse(readFileSync(STATE, "utf8")).version, "bv_02");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("a forced tick inside the interval still sees a version change for what it is", async () => {
|
|
138
|
+
const moved = JSON.parse(JSON.stringify(FIXTURE));
|
|
139
|
+
moved.colors[2].value = "#334155";
|
|
140
|
+
const r = await daemon.refreshDesignFoundation({
|
|
141
|
+
now: NOW + 2 * FIFTEEN_MIN + 60_000, // one minute later: the gate would say "fresh"
|
|
142
|
+
force: true,
|
|
143
|
+
getFoundationImpl: async () => frame({ foundation: moved, headVersionId: "bv_03", version: { id: "bv_03", seq: 9, label: "V2.9" } }),
|
|
144
|
+
});
|
|
145
|
+
assert.equal(r.reason, "version-changed");
|
|
146
|
+
assert.ok(r.written.includes("DESIGN.md"));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a fetch failure keeps the DESIGN.md already on disk — fail-open, never blanked", async () => {
|
|
150
|
+
const good = readFileSync(DESIGN, "utf8");
|
|
151
|
+
const old = new Date(NOW - 86_400_000);
|
|
152
|
+
utimesSync(DESIGN, old, old);
|
|
153
|
+
const mtime = statSync(DESIGN).mtimeMs;
|
|
154
|
+
const now = NOW + 4 * FIFTEEN_MIN;
|
|
155
|
+
|
|
156
|
+
const thrown = await daemon.refreshDesignFoundation({ now, getFoundationImpl: async () => { throw new Error("ECONNREFUSED"); } });
|
|
157
|
+
assert.deepEqual(thrown, { refreshed: false, reason: "fetch-failed" });
|
|
158
|
+
|
|
159
|
+
const errored = await daemon.refreshDesignFoundation({ now, getFoundationImpl: async () => ({ ok: false, error: { code: "UNAUTHORIZED", message: "bad token" } }) });
|
|
160
|
+
assert.deepEqual(errored, { refreshed: false, reason: "error:UNAUTHORIZED" });
|
|
161
|
+
|
|
162
|
+
const empty = await daemon.refreshDesignFoundation({ now, getFoundationImpl: async () => ({ ok: true, result: { foundation: null, headVersionId: null } }) });
|
|
163
|
+
assert.deepEqual(empty, { refreshed: false, reason: "no-foundation" });
|
|
164
|
+
|
|
165
|
+
assert.equal(readFileSync(DESIGN, "utf8"), good, "three failures, and the last-good file is byte-identical");
|
|
166
|
+
assert.equal(statSync(DESIGN).mtimeMs, mtime);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("an org-disabled seat writes nothing and spends no call", async () => {
|
|
170
|
+
const dir = mkdtempSync(join(tmpdir(), "maestro-daemon-design-off-"));
|
|
171
|
+
try {
|
|
172
|
+
mkdirSync(join(dir, "config"), { recursive: true });
|
|
173
|
+
mkdirSync(join(dir, "state", "sessions", "locks"), { recursive: true });
|
|
174
|
+
writeFileSync(join(dir, "config", "org.yaml"), "org:\n cohort:\n enabled: false\n");
|
|
175
|
+
writeFileSync(join(dir, "config", "agent.json"), JSON.stringify({ firstName: "Nova" }));
|
|
176
|
+
|
|
177
|
+
const prev = process.env.AGENT_DIR;
|
|
178
|
+
process.env.AGENT_DIR = dir;
|
|
179
|
+
let off;
|
|
180
|
+
try {
|
|
181
|
+
const url = new URL("./agent-daemon.mjs", import.meta.url);
|
|
182
|
+
off = await import(`${url.href}?designoff=${Math.random()}`);
|
|
183
|
+
} finally {
|
|
184
|
+
process.env.AGENT_DIR = prev;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let calls = 0;
|
|
188
|
+
const r = await off.refreshDesignFoundation({ now: NOW, getFoundationImpl: async () => { calls += 1; return frame(); } });
|
|
189
|
+
assert.deepEqual(r, { refreshed: false, reason: "org-disabled" });
|
|
190
|
+
assert.equal(calls, 0);
|
|
191
|
+
assert.ok(!existsSync(join(dir, "DESIGN.md")));
|
|
192
|
+
assert.ok(!existsSync(join(dir, "state", "design", "foundation.json")));
|
|
193
|
+
} finally {
|
|
194
|
+
rmSync(dir, { recursive: true, force: true });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("force skips the gate but nothing else — an unchanged foundation still writes nothing", async () => {
|
|
199
|
+
// Re-establish the on-disk files from the CURRENT head, then force a second
|
|
200
|
+
// read one second later: the gate would have said "fresh", the read happens
|
|
201
|
+
// anyway, and the byte comparison still refuses to touch the files.
|
|
202
|
+
const now = NOW + 5 * FIFTEEN_MIN;
|
|
203
|
+
await daemon.refreshDesignFoundation({ now, force: true, getFoundationImpl: async () => frame() });
|
|
204
|
+
let calls = 0;
|
|
205
|
+
const r = await daemon.refreshDesignFoundation({ now: now + 1_000, force: true, getFoundationImpl: async () => { calls += 1; return frame(); } });
|
|
206
|
+
assert.equal(calls, 1, "a forced tick reads even though the gate would have said 'fresh'");
|
|
207
|
+
assert.deepEqual(r.written, [], "and still writes nothing when the bytes already match");
|
|
208
|
+
assert.equal(r.refreshed, false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a workspace that never saved a foundation gets nothing written — not hq's stock defaults", async () => {
|
|
212
|
+
// hq NEVER returns a null foundation: an org with no saved one reads back
|
|
213
|
+
// `dsDefaults()` (a complete, plausible palette that is nobody's brand) with
|
|
214
|
+
// `unversioned: true`. Writing it would ground the seat's design skills in
|
|
215
|
+
// another product's palette, with a generated file's authority behind it.
|
|
216
|
+
const dir = mkdtempSync(join(tmpdir(), "maestro-daemon-design-unver-"));
|
|
217
|
+
const prevAgent = process.env.AGENT_DIR;
|
|
218
|
+
try {
|
|
219
|
+
mkdirSync(join(dir, "config"), { recursive: true });
|
|
220
|
+
writeFileSync(
|
|
221
|
+
join(dir, "config", "org.yaml"),
|
|
222
|
+
"org:\n cohort:\n enabled: true\n base: https://os.example.test\n token: nlk_test_token\n orgId: org_test\n orgName: Northwind\n",
|
|
223
|
+
);
|
|
224
|
+
process.env.AGENT_DIR = dir;
|
|
225
|
+
const mod = await import(`${new URL("./agent-daemon.mjs", import.meta.url).href}?unver=${Math.random()}`);
|
|
226
|
+
const r = await mod.refreshDesignFoundation({
|
|
227
|
+
now: NOW,
|
|
228
|
+
getFoundationImpl: async () => frame({ headVersionId: null, version: null, unversioned: true }),
|
|
229
|
+
});
|
|
230
|
+
assert.deepEqual(r, { refreshed: false, reason: "unversioned" });
|
|
231
|
+
assert.ok(!existsSync(join(dir, "DESIGN.md")), "no DESIGN.md invented from stock defaults");
|
|
232
|
+
assert.ok(!existsSync(join(dir, "PRODUCT.md")));
|
|
233
|
+
assert.ok(!existsSync(join(dir, "state", "design", "foundation.json")));
|
|
234
|
+
} finally {
|
|
235
|
+
process.env.AGENT_DIR = prevAgent;
|
|
236
|
+
rmSync(dir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
@@ -98,6 +98,9 @@ import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder
|
|
|
98
98
|
// read it. The when/what is pure (lib/org/board-mine-cache.mjs); the I/O is here.
|
|
99
99
|
import { shouldRefresh as boardMineShouldRefresh, renderRefresh as renderBoardMineRefresh, cacheTs as boardMineCacheTs, BOARD_MINE_REL } from "../../lib/org/board-mine-cache.mjs";
|
|
100
100
|
import { writeJsonAtomic as writeJsonAtomicFile } from "../../lib/fs-atomic.mjs";
|
|
101
|
+
import { brandingGetFoundation } from "../../lib/org/client.mjs";
|
|
102
|
+
import { shouldRefreshDesign, foundationVersion, isUnversionedFoundation, DESIGN_REFRESH_INTERVAL_MS } from "../../lib/design/refresh-gate.mjs";
|
|
103
|
+
import { syncDesign, readFoundationState, workspaceName as designWorkspaceName } from "../../lib/design/write.mjs";
|
|
101
104
|
import { remember as orgRemember } from "../../lib/org/knowledge.mjs";
|
|
102
105
|
import { sweepSessionOutcomes, resultTextFromStdout } from "./session-outcomes.mjs";
|
|
103
106
|
import { recordWorkStep as orgRecordWorkStep, cohortChannelId, cohortMessageId } from "../../lib/org/work-ledger.mjs";
|
|
@@ -1794,6 +1797,101 @@ export async function refreshBoardMineCache(deps = {}) {
|
|
|
1794
1797
|
return { refreshed: true, reason: doc.stale ? "kept-previous" : "written", count: doc.items.length };
|
|
1795
1798
|
}
|
|
1796
1799
|
|
|
1800
|
+
/**
|
|
1801
|
+
* Re-sync the workspace brand foundation into DESIGN.md + PRODUCT.md (WP-M7
|
|
1802
|
+
* mechanic 4), in the same idiom as `refreshIntegrationTools` in
|
|
1803
|
+
* `lib/org/mesh.mjs`: a cheap gated poll on the beat, fail-open end to end,
|
|
1804
|
+
* keeping the last-good files when hq is unreachable.
|
|
1805
|
+
*
|
|
1806
|
+
* WHY THIS EXISTS AT ALL. Every design skill on the seat is told to read
|
|
1807
|
+
* `$AGENT_ROOT/DESIGN.md` before it picks a colour or a typeface. A designer
|
|
1808
|
+
* working from a palette a human replaced last week is worse than one with no
|
|
1809
|
+
* palette, because the stale file looks authoritative — so the seat follows the
|
|
1810
|
+
* foundation rather than waiting for someone to re-run the CLI.
|
|
1811
|
+
*
|
|
1812
|
+
* THE GATE IS ASKED TWICE (see `lib/design/refresh-gate.mjs`): once before the
|
|
1813
|
+
* read, with the version unknown, to decide whether to spend a call at all
|
|
1814
|
+
* (15-minute floor); once after, with the version hq returned, to say honestly
|
|
1815
|
+
* WHY this tick refreshed. hq exposes no cheap "has the foundation changed"
|
|
1816
|
+
* etag the way `integration.toolsetVersion` does, so the version comparison can
|
|
1817
|
+
* only happen after a read and the interval is what bounds the reads.
|
|
1818
|
+
*
|
|
1819
|
+
* FAIL-OPEN, AND SILENT ABOUT NOTHING. An un-enrolled seat writes nothing and
|
|
1820
|
+
* says so; a fetch failure keeps the files that are already on disk and logs
|
|
1821
|
+
* the reason. The write itself is byte-idempotent (`syncDesign`), so a steady
|
|
1822
|
+
* foundation never touches an mtime — which is what keeps a git tree clean and
|
|
1823
|
+
* an editor's file watcher quiet on a seat that polls every 15 minutes.
|
|
1824
|
+
*
|
|
1825
|
+
* @param {{now?:number, intervalMs?:number, force?:boolean, getFoundationImpl?:Function, fetchImpl?:Function}} [deps]
|
|
1826
|
+
* @returns {Promise<{refreshed:boolean, reason:string, version?:string|null, written?:string[]}>}
|
|
1827
|
+
*/
|
|
1828
|
+
export async function refreshDesignFoundation(deps = {}) {
|
|
1829
|
+
const now = Number.isFinite(deps.now) ? deps.now : Date.now();
|
|
1830
|
+
const intervalMs = Number.isFinite(deps.intervalMs) ? deps.intervalMs : DESIGN_REFRESH_INTERVAL_MS;
|
|
1831
|
+
const prior = readFoundationState(AGENT_REPO_DIR);
|
|
1832
|
+
const before = shouldRefreshDesign({
|
|
1833
|
+
lastVersion: prior ? prior.version : null,
|
|
1834
|
+
currentVersion: null,
|
|
1835
|
+
lastAt: prior ? prior.fetchedAt : null,
|
|
1836
|
+
now,
|
|
1837
|
+
intervalMs,
|
|
1838
|
+
});
|
|
1839
|
+
if (!deps.force && !before.refresh) return { refreshed: false, reason: "fresh" };
|
|
1840
|
+
|
|
1841
|
+
const cfg = daemonOrgCfg();
|
|
1842
|
+
if (!orgEnabled(cfg)) return { refreshed: false, reason: "org-disabled" };
|
|
1843
|
+
const conn = configFromAgent(cfg) || {};
|
|
1844
|
+
|
|
1845
|
+
let frame;
|
|
1846
|
+
try {
|
|
1847
|
+
frame = await (deps.getFoundationImpl || brandingGetFoundation)(
|
|
1848
|
+
{ historyLimit: 0 },
|
|
1849
|
+
{ base: conn.base, token: conn.token, orgId: conn.orgId, fetchImpl: deps.fetchImpl },
|
|
1850
|
+
);
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
console.warn(`[daemon] design sync: foundation read failed (${err && err.message ? err.message : err}) — keeping the DESIGN.md already on disk`);
|
|
1853
|
+
return { refreshed: false, reason: "fetch-failed" };
|
|
1854
|
+
}
|
|
1855
|
+
if (!frame || !frame.ok || !frame.result || typeof frame.result !== "object" || !frame.result.foundation) {
|
|
1856
|
+
const why = frame && frame.error && frame.error.code ? frame.error.code : "no-foundation";
|
|
1857
|
+
return { refreshed: false, reason: why === "no-foundation" ? "no-foundation" : `error:${why}` };
|
|
1858
|
+
}
|
|
1859
|
+
// A workspace that has never SAVED a foundation reads back hq's own product
|
|
1860
|
+
// defaults with `unversioned: true` — a complete palette that is nobody's
|
|
1861
|
+
// brand. Writing it would ground every design skill on the seat in the wrong
|
|
1862
|
+
// palette, with a generated file's authority behind it. Nothing is written
|
|
1863
|
+
// and nothing already on disk is disturbed; see
|
|
1864
|
+
// refresh-gate.mjs#isUnversionedFoundation.
|
|
1865
|
+
if (isUnversionedFoundation(frame.result)) return { refreshed: false, reason: "unversioned" };
|
|
1866
|
+
|
|
1867
|
+
// The second gate call: now that the version is known, say why this tick ran.
|
|
1868
|
+
const after = shouldRefreshDesign({
|
|
1869
|
+
lastVersion: prior ? prior.version : null,
|
|
1870
|
+
currentVersion: foundationVersion(frame.result),
|
|
1871
|
+
lastAt: prior ? prior.fetchedAt : null,
|
|
1872
|
+
now,
|
|
1873
|
+
intervalMs,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
let out;
|
|
1877
|
+
try {
|
|
1878
|
+
out = syncDesign({
|
|
1879
|
+
agentRoot: AGENT_REPO_DIR,
|
|
1880
|
+
result: frame.result,
|
|
1881
|
+
now,
|
|
1882
|
+
name: designWorkspaceName(cfg),
|
|
1883
|
+
source: "cohort · branding.getFoundation",
|
|
1884
|
+
});
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
console.warn(`[daemon] design sync: write failed (${err && err.message ? err.message : err})`);
|
|
1887
|
+
return { refreshed: false, reason: "write-failed" };
|
|
1888
|
+
}
|
|
1889
|
+
if (out.written.length) {
|
|
1890
|
+
console.log(`[daemon] design sync (${after.reason}) → wrote ${out.written.join(", ")}${out.version ? ` — foundation ${out.version}` : ""}`);
|
|
1891
|
+
}
|
|
1892
|
+
return { refreshed: out.written.length > 0, reason: after.reason, version: out.version, written: out.written };
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1797
1895
|
/** Longest completion note we will put on a board comment. */
|
|
1798
1896
|
const COMPLETION_NOTE_CHARS = 1200;
|
|
1799
1897
|
|
|
@@ -2376,6 +2474,16 @@ async function main() {
|
|
|
2376
2474
|
}
|
|
2377
2475
|
}, POLL_INTERVAL);
|
|
2378
2476
|
|
|
2477
|
+
// Brand foundation → DESIGN.md + PRODUCT.md, every 15 min (WP-M7). Fired once
|
|
2478
|
+
// at boot so a seat that has never synced has its design files before the
|
|
2479
|
+
// first session opens, then on its own interval. Fail-open and never awaited:
|
|
2480
|
+
// the design docs are guidance, and no failure to fetch them may hold up the
|
|
2481
|
+
// poll loop.
|
|
2482
|
+
refreshDesignFoundation().catch((err) => console.error("[daemon] design sync error:", err.message));
|
|
2483
|
+
setInterval(() => {
|
|
2484
|
+
refreshDesignFoundation().catch((err) => console.error("[daemon] design sync error:", err.message));
|
|
2485
|
+
}, DESIGN_REFRESH_INTERVAL_MS);
|
|
2486
|
+
|
|
2379
2487
|
// Fast reactive cohort scan — drains org-push-fetched DMs in ~2s instead of
|
|
2380
2488
|
// waiting up to 60s for the full poll. Local YAML only; no API/rate-limit cost.
|
|
2381
2489
|
setInterval(() => {
|
|
@@ -26,7 +26,8 @@ import { join, dirname } from "node:path";
|
|
|
26
26
|
|
|
27
27
|
import { enqueueTick, listInbox, busDepth, getBusPaths } from "../../lib/cadence-bus.mjs";
|
|
28
28
|
import { listHandoffs, handoffPaths, writeHandoff, ackHandoff, DEFAULT_HANDOFF_DEADLINE_MS, DEFAULT_HANDOFF_RETENTION_MS } from "../../lib/session/handoffs.mjs";
|
|
29
|
-
import { startConsumer as startConsumerRaw } from "./cadence-consumer.mjs";
|
|
29
|
+
import { startConsumer as startConsumerRaw, renderCadencePromptBody } from "./cadence-consumer.mjs";
|
|
30
|
+
import { withParallelism, countParallelism, PARALLELISM_DIRECTIVE } from "../../lib/prompts/parallelism.mjs";
|
|
30
31
|
|
|
31
32
|
const ADMIT_ALL = { admit: () => ({ decision: "ADMIT", reason: "test", snapshot: {} }), defaultDeps: () => ({}) };
|
|
32
33
|
const RATE_OK = { checkRateLimit: () => ({ allowed: true, retryAt: null }), classifyStderr: () => false, recordRateLimit: () => {}, recordSuccess: () => {} };
|
|
@@ -85,7 +86,11 @@ test("live session: an escalate tick is handed to the session — handoff + rend
|
|
|
85
86
|
assert.equal(open[0].mode, "escalate");
|
|
86
87
|
assert.ok(open[0].deadlineAt, "carries a deadline");
|
|
87
88
|
assert.ok(existsSync(open[0].promptPath), "the rendered prompt is on disk");
|
|
88
|
-
assert.equal(
|
|
89
|
+
assert.equal(
|
|
90
|
+
readFileSync(open[0].promptPath, "utf-8"),
|
|
91
|
+
withParallelism("# daily-morning-brief for Nova"),
|
|
92
|
+
"identity tokens rendered, and the handed prompt is byte-identical to what a sub-session would have been given",
|
|
93
|
+
);
|
|
89
94
|
assert.equal(busDepth(root).inbox, 0);
|
|
90
95
|
assert.equal(busDepth(root).claimed, 0);
|
|
91
96
|
const done = processedEvents(root).find((e) => e.id === t.id);
|
|
@@ -332,3 +337,57 @@ test("handoff ledger hygiene: the sweep prunes done/ records and prompts past re
|
|
|
332
337
|
assert.equal(existsSync(join(paths.prompts, "evt-done.md")), false);
|
|
333
338
|
} finally { await consumer.stop(); await rmRoot(root); }
|
|
334
339
|
});
|
|
340
|
+
|
|
341
|
+
// ── the parallelism directive on the escalate/guarded lane (WP-M7 mechanic 5) ──
|
|
342
|
+
//
|
|
343
|
+
// The spec puts the directive on "every escalate/guarded sub-session prompt".
|
|
344
|
+
// Cadence trigger prompts never pass through `prompt-builder.mjs#buildPrompt`
|
|
345
|
+
// — they are read straight off schedules/triggers/<name>.md — so this lane has
|
|
346
|
+
// its own seam (`renderCadencePromptBody`) and needs its own pins. Without it
|
|
347
|
+
// an `escalate` tick whose trigger asks for six independent audits spawns a
|
|
348
|
+
// sub-session with no standing permission to fan out.
|
|
349
|
+
|
|
350
|
+
test("escalate: the spawned sub-session prompt leads with the parallelism directive, exactly once", async () => {
|
|
351
|
+
const root = await makeAgentRoot();
|
|
352
|
+
plantPrompt(root, "daily-morning-brief", "# daily-morning-brief for {{agent.firstName}}\n\nAudit six things.");
|
|
353
|
+
const spawns = [];
|
|
354
|
+
const consumer = startConsumer({
|
|
355
|
+
agentRoot: root,
|
|
356
|
+
frontDoorState: NOT_LIVE,
|
|
357
|
+
spawnSession: async (o) => { spawns.push(o); return { ok: true, exit_code: 0, duration_ms: 1 }; },
|
|
358
|
+
});
|
|
359
|
+
try {
|
|
360
|
+
enqueueTick({ cadence: "daily-morning-brief", source: "launchd", agentRoot: root });
|
|
361
|
+
await consumer.tickOnce();
|
|
362
|
+
assert.equal(spawns.length, 1);
|
|
363
|
+
// The injected spawner is handed the promptPath, not the body — so assert
|
|
364
|
+
// on the seam the real spawner uses to turn that path into argv.
|
|
365
|
+
const body = renderCadencePromptBody(root, readFileSync(join(root, spawns[0].promptPath), "utf-8"));
|
|
366
|
+
assert.ok(body.startsWith(PARALLELISM_DIRECTIVE), "the directive leads");
|
|
367
|
+
assert.equal(countParallelism(body), 1);
|
|
368
|
+
assert.ok(body.endsWith("# daily-morning-brief for Nova\n\nAudit six things."), "and the trigger body follows, tokens rendered");
|
|
369
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a trigger that already carries the directive is not given a second copy", () => {
|
|
373
|
+
const body = renderCadencePromptBody("/nonexistent-agent-root", withParallelism("# already-primed"));
|
|
374
|
+
assert.equal(countParallelism(body), 1, "idempotent — a repeated directive reads as emphasis and erodes the file-scope rule");
|
|
375
|
+
assert.equal(body, withParallelism("# already-primed"));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("the handed-off prompt and the spawned prompt are the same bytes", async () => {
|
|
379
|
+
const root = await makeAgentRoot();
|
|
380
|
+
const raw = "# weekly-strategic-memo for {{agent.firstName}}";
|
|
381
|
+
plantPrompt(root, "weekly-strategic-memo", raw);
|
|
382
|
+
const consumer = startConsumer({ agentRoot: root, frontDoorState: LIVE, spawnSession: async () => ({ ok: true, exit_code: 0 }) });
|
|
383
|
+
try {
|
|
384
|
+
enqueueTick({ cadence: "weekly-strategic-memo", source: "launchd", agentRoot: root });
|
|
385
|
+
await consumer.tickOnce();
|
|
386
|
+
const open = listHandoffs(root);
|
|
387
|
+
assert.equal(open.length, 1);
|
|
388
|
+
const handed = readFileSync(open[0].promptPath, "utf-8");
|
|
389
|
+
assert.equal(handed, renderCadencePromptBody(root, raw), "one seam, two lanes");
|
|
390
|
+
assert.equal(countParallelism(handed), 1);
|
|
391
|
+
assert.ok(handed.startsWith(PARALLELISM_DIRECTIVE));
|
|
392
|
+
} finally { await consumer.stop(); await rmRoot(root); }
|
|
393
|
+
});
|
|
@@ -100,6 +100,7 @@ import { obligationAllowedUnderPosture } from "../../lib/plan/compile.mjs";
|
|
|
100
100
|
import { isHumanLaneCadence } from "../../lib/cadences.mjs";
|
|
101
101
|
import { sessionPermissionArgs } from "../../lib/session-permissions.mjs";
|
|
102
102
|
import { renderTemplate, buildContext } from "../../lib/render.mjs";
|
|
103
|
+
import { withParallelism } from "../../lib/prompts/parallelism.mjs";
|
|
103
104
|
// Model router (opt-in). A `schema_version: 2` config routes each cadence
|
|
104
105
|
// sub-session's model through resolveChain (SPEC §6.2): the RouteDecision
|
|
105
106
|
// supplies the --model flag, the session-retarget env (third-party backends),
|
|
@@ -315,6 +316,43 @@ export function routeCadenceSpawn(agentRoot, cadence) {
|
|
|
315
316
|
}
|
|
316
317
|
}
|
|
317
318
|
|
|
319
|
+
/**
|
|
320
|
+
* A cadence trigger prompt, as the sub-session actually receives it.
|
|
321
|
+
*
|
|
322
|
+
* TWO transforms, and BOTH lanes must apply them or the two lanes disagree
|
|
323
|
+
* about what the same tick asked for:
|
|
324
|
+
*
|
|
325
|
+
* 1. `{{agent.*}}` / `{{company.*}}` identity tokens are rendered from
|
|
326
|
+
* config/agent.json + config/company.json (lib/render.mjs), so a trigger
|
|
327
|
+
* shipped generically in the package resolves to THIS seat. No-op on
|
|
328
|
+
* token-free text.
|
|
329
|
+
* 2. The fleet parallelism directive LEADS the body (WP-M7 mechanic 5). An
|
|
330
|
+
* escalate/guarded tick is exactly the sub-session the directive exists
|
|
331
|
+
* for — a trigger that asks for six independent audits has, without it,
|
|
332
|
+
* no standing permission to fan out, and runs them one at a time. The
|
|
333
|
+
* inbox/backlog lane gets this from `prompt-builder.mjs#buildPrompt`;
|
|
334
|
+
* cadence prompts never pass through that builder, which is how this lane
|
|
335
|
+
* was missed. `withParallelism` is idempotent, so a trigger file that
|
|
336
|
+
* already carries the text keeps exactly one copy.
|
|
337
|
+
*
|
|
338
|
+
* Pure apart from the two config reads; a missing/invalid config/agent.json
|
|
339
|
+
* leaves the body's tokens verbatim rather than failing the tick.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} agentRoot
|
|
342
|
+
* @param {string} body the raw trigger prompt as read from disk
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
export function renderCadencePromptBody(agentRoot, body) {
|
|
346
|
+
let out = typeof body === "string" ? body : "";
|
|
347
|
+
try {
|
|
348
|
+
const agentCfg = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
349
|
+
let companyCfg = {};
|
|
350
|
+
try { companyCfg = JSON.parse(readFileSync(join(agentRoot, "config/company.json"), "utf-8")); } catch { /* optional */ }
|
|
351
|
+
out = renderTemplate(out, buildContext(agentCfg, null, companyCfg));
|
|
352
|
+
} catch { /* no/invalid config/agent.json — leave body verbatim */ }
|
|
353
|
+
return withParallelism(out);
|
|
354
|
+
}
|
|
355
|
+
|
|
318
356
|
/**
|
|
319
357
|
* Spawn a sub-session running the cadence's trigger prompt and resolve
|
|
320
358
|
* with { exit_code, durationMs, stderr_tail }. Reads the prompt at call
|
|
@@ -339,17 +377,9 @@ function realSpawnSession({ agentRoot, cadence, promptPath, timeoutMs, log }) {
|
|
|
339
377
|
return;
|
|
340
378
|
}
|
|
341
379
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
// No-op on token-free text, so this is behaviour-preserving for any not-yet-
|
|
346
|
-
// tokenised trigger.
|
|
347
|
-
try {
|
|
348
|
-
const agentCfg = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
349
|
-
let companyCfg = {};
|
|
350
|
-
try { companyCfg = JSON.parse(readFileSync(join(agentRoot, "config/company.json"), "utf-8")); } catch { /* optional */ }
|
|
351
|
-
body = renderTemplate(body, buildContext(agentCfg, null, companyCfg));
|
|
352
|
-
} catch { /* no/invalid config/agent.json — leave body verbatim */ }
|
|
380
|
+
// Identity tokens + the parallelism directive, at spawn time — one seam,
|
|
381
|
+
// shared with the front-door handoff lane. See renderCadencePromptBody.
|
|
382
|
+
body = renderCadencePromptBody(agentRoot, body);
|
|
353
383
|
|
|
354
384
|
const bin = resolveClaudeBin();
|
|
355
385
|
// Permission scoping (audit H1). sessionPermissionArgs() defaults to the
|
|
@@ -592,20 +622,14 @@ export function startConsumer(opts = {}) {
|
|
|
592
622
|
}
|
|
593
623
|
|
|
594
624
|
/**
|
|
595
|
-
* Render a cadence prompt the way realSpawnSession does
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
625
|
+
* Render a cadence prompt the way realSpawnSession does — the SAME
|
|
626
|
+
* `renderCadencePromptBody`, so the handed-off prompt is byte-identical to
|
|
627
|
+
* what a sub-session would have been given, parallelism directive included —
|
|
628
|
+
* and land it under state/session/handoffs/prompts/<tickId>.md.
|
|
599
629
|
*/
|
|
600
630
|
function renderHandoffPrompt(tickId, promptPath) {
|
|
601
631
|
const fullPrompt = join(agentRoot, promptPath);
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const agentCfg = JSON.parse(readFileSync(join(agentRoot, "config/agent.json"), "utf-8"));
|
|
605
|
-
let companyCfg = {};
|
|
606
|
-
try { companyCfg = JSON.parse(readFileSync(join(agentRoot, "config/company.json"), "utf-8")); } catch { /* optional */ }
|
|
607
|
-
body = renderTemplate(body, buildContext(agentCfg, null, companyCfg));
|
|
608
|
-
} catch { /* no/invalid config/agent.json — leave body verbatim */ }
|
|
632
|
+
const body = renderCadencePromptBody(agentRoot, readFileSync(fullPrompt, "utf-8"));
|
|
609
633
|
const out = join(handoffPaths(agentRoot).prompts, `${tickId}.md`);
|
|
610
634
|
writeFileAtomic(out, body);
|
|
611
635
|
return out;
|
|
@@ -12,6 +12,7 @@ import { wrapExternalContent } from "../../lib/security/external-content.mjs";
|
|
|
12
12
|
import { isEnabled as orgEnabled } from "../../lib/org/client.mjs";
|
|
13
13
|
import { recall as orgRecall } from "../../lib/org/knowledge.mjs";
|
|
14
14
|
import { outcomeSourceShareable } from "./session-outcomes.mjs";
|
|
15
|
+
import { withParallelism } from "../../lib/prompts/parallelism.mjs";
|
|
15
16
|
|
|
16
17
|
const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
|
|
17
18
|
|
|
@@ -852,16 +853,23 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
852
853
|
const parts = [];
|
|
853
854
|
|
|
854
855
|
// 1. Persona block (cached) — rendered from config/agent.json, which the
|
|
855
|
-
// Cohort enrollment pull keeps current. Leads the
|
|
856
|
+
// Cohort enrollment pull keeps current. Leads the BODY and outranks the
|
|
856
857
|
// CLAUDE.md preamble below, because a scaffolded CLAUDE.md carries
|
|
857
858
|
// "identity not yet configured" placeholder prose that would otherwise
|
|
858
859
|
// contradict a perfectly good org record.
|
|
860
|
+
//
|
|
861
|
+
// THE PRECEDENCE SENTENCE NAMES ITSELF, NOT A POSITION. It used to say "the
|
|
862
|
+
// section above … anything below", which was true only while the persona was
|
|
863
|
+
// the first thing in the prompt. The parallelism directive is prepended at
|
|
864
|
+
// the end of this function (§10) and now leads, so "above" had two candidate
|
|
865
|
+
// referents and the directive sat outside the rule entirely — above it, not
|
|
866
|
+
// below. A self-naming sentence stays true wherever the block ends up.
|
|
859
867
|
const persona = loadPersona();
|
|
860
868
|
if (persona) {
|
|
861
869
|
parts.push(persona);
|
|
862
870
|
parts.push("");
|
|
863
871
|
parts.push(
|
|
864
|
-
"
|
|
872
|
+
"This section is your identity, resolved from your organisation's record of you. Where anything else in this prompt conflicts with it, this section wins.",
|
|
865
873
|
);
|
|
866
874
|
parts.push("");
|
|
867
875
|
}
|
|
@@ -1025,5 +1033,13 @@ export async function buildPrompt(item, classResult, options = {}) {
|
|
|
1025
1033
|
// 9. Audit instruction
|
|
1026
1034
|
parts.push(`Log all actions taken to logs/audit/${date}-actions.jsonl as a JSON line with: timestamp, action_type, target, result, session context.`);
|
|
1027
1035
|
|
|
1028
|
-
|
|
1036
|
+
// 10. The parallelism directive (WP-M7 mechanic 5). Every prompt this builder
|
|
1037
|
+
// produces is handed to a sub-session that may plan and dispatch work of its
|
|
1038
|
+
// own, and nothing else in the prompt ever says that fanning out is allowed —
|
|
1039
|
+
// which is why sessions handed six independent tasks ran them one at a time.
|
|
1040
|
+
// It LEADS the prompt because it governs how the session plans, before it has
|
|
1041
|
+
// read a word of the item. `withParallelism` is idempotent, so a prompt that
|
|
1042
|
+
// already carries it (composed by a caller, or handed on to `session spawn`,
|
|
1043
|
+
// which applies the same seam) is returned with exactly one copy.
|
|
1044
|
+
return withParallelism(parts.join("\n"));
|
|
1029
1045
|
}
|