Skip to content

Commit 4821037

Browse files
committed
feat: queue Agent Lightning approvals
1 parent 48624ff commit 4821037

11 files changed

Lines changed: 400 additions & 33 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,21 @@ flowchart LR
337337

338338
The Library counts `.md`, `.mdx`, and `.txt` files from configured collections. Memory entries live in separate memory services and SQLite tables, so a collection file count is not the same thing as total memories.
339339

340+
## Agent Lightning Approvals
341+
342+
Sous Vide approvals are intentionally two-step:
343+
344+
1. The UI/API approval moves the proposal into a durable `approved/` work queue.
345+
2. The worker CLI applies queued approvals and archives the proposal for audit history.
346+
347+
Qwen is the default executor assignment for this queue:
348+
349+
```bash
350+
npm --prefix apps/kitchen run apo:worker -- --executor qwen
351+
```
352+
353+
Use `APO_APPROVAL_CLI=qwen` to keep that default for scheduled runs, or pass `--executor codex` / `--executor claude` when you explicitly want a different CLI to own the implementation pass.
354+
340355
## Security Model
341356

342357
Kitchen is built for private-network production first.

apps/kitchen/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"build": "next build",
99
"start": "next start",
1010
"start:all": "bash ../../start.sh",
11+
"apo:worker": "node ../../scripts/apply-agent-lightning-approvals.mjs",
1112
"check:agents": "node ../../scripts/check-agent-systems.mjs",
1213
"curate:skills": "node ../../scripts/curate-agent-skills.mjs",
1314
"install:skill-curation": "node ../../scripts/install-skill-curation-launchd.mjs",

apps/kitchen/src/app/api/apo/__tests__/route.test.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,62 @@ describe("POST /api/apo approve", () => {
5959
rmSync(root, { recursive: true, force: true });
6060
});
6161

62-
it("appends the APO constraint to the target skill and archives the proposal", async () => {
62+
it("queues an approved proposal without mutating the target skill", async () => {
6363
const { POST } = await loadRoute();
6464

6565
const response = await POST(makeApproveRequest({ action: "approve", proposalId: proposalFilename }));
6666
const body = await response.json();
6767

6868
expect(response.status).toBe(200);
69-
expect(body).toMatchObject({ ok: true, proposalId: proposalFilename, skillId: "ceo", archived: true, applied: true });
70-
expect(readFileSync(path.join(skillsPath, "ceo", "SKILL.md"), "utf-8")).toContain(
69+
expect(body).toMatchObject({
70+
ok: true,
71+
proposalId: proposalFilename,
72+
skillId: "ceo",
73+
queued: true,
74+
targetKind: "skill",
75+
executorCli: "qwen",
76+
});
77+
expect(readFileSync(path.join(skillsPath, "ceo", "SKILL.md"), "utf-8")).not.toContain(
7178
"CRITICAL: Check provider auth before retrying blocked work."
7279
);
7380
expect(existsSync(path.join(proposalsPath, proposalFilename))).toBe(false);
81+
expect(existsSync(path.join(proposalsPath, "approved", proposalFilename))).toBe(true);
82+
expect(existsSync(path.join(proposalsPath, "approved", `${proposalFilename}.json`))).toBe(true);
83+
});
84+
85+
it("lists queued approvals separately from pending and archived proposals", async () => {
86+
const { GET, POST } = await loadRoute();
87+
88+
await POST(makeApproveRequest({ action: "approve", proposalId: proposalFilename }));
89+
const response = await GET();
90+
const body = await response.json();
91+
92+
expect(response.status).toBe(200);
93+
expect(body.stats).toMatchObject({ pendingProposals: 0, approvedProposals: 1, archivedProposals: 0 });
94+
expect(body.proposals[0]).toMatchObject({ id: proposalFilename, status: "approved" });
95+
});
96+
97+
it("applies queued approved proposals and archives them from the worker action", async () => {
98+
const { POST } = await loadRoute();
99+
100+
await POST(makeApproveRequest({ action: "approve", proposalId: proposalFilename }));
101+
const response = await POST(makeApproveRequest({ action: "process-approved" }));
102+
const body = await response.json();
103+
104+
expect(response.status).toBe(200);
105+
expect(body).toMatchObject({ ok: true, processed: 1, failed: 0 });
106+
expect(body.results[0]).toMatchObject({
107+
ok: true,
108+
proposalId: proposalFilename,
109+
skillId: "ceo",
110+
archived: true,
111+
applied: true,
112+
executorCli: "qwen",
113+
});
114+
expect(readFileSync(path.join(skillsPath, "ceo", "SKILL.md"), "utf-8")).toContain(
115+
"CRITICAL: Check provider auth before retrying blocked work."
116+
);
117+
expect(existsSync(path.join(proposalsPath, "approved", proposalFilename))).toBe(false);
74118
expect(existsSync(path.join(proposalsPath, "archived", proposalFilename))).toBe(true);
75119
});
76120

@@ -97,7 +141,13 @@ describe("POST /api/apo approve", () => {
97141
const body = await response.json();
98142

99143
expect(response.status).toBe(200);
100-
expect(body).toMatchObject({ ok: true, proposalId: staffProposal, skillId: "chief-of-staff", targetKind: "agent" });
144+
expect(body).toMatchObject({ ok: true, proposalId: staffProposal, skillId: "chief-of-staff", targetKind: "agent", queued: true });
145+
expect(readFileSync(path.join(agentsPath, "chief_of_staff", "AGENTS.md"), "utf-8")).not.toContain(
146+
"CRITICAL: Check provider auth before retrying blocked work."
147+
);
148+
149+
const applyResponse = await POST(makeApproveRequest({ action: "apply-approved", proposalId: staffProposal }));
150+
expect(applyResponse.status).toBe(200);
101151
expect(readFileSync(path.join(agentsPath, "chief_of_staff", "AGENTS.md"), "utf-8")).toContain(
102152
"CRITICAL: Check provider auth before retrying blocked work."
103153
);

0 commit comments

Comments
 (0)