pi-ca-leash 0.10.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/AGENTS.md +77 -0
- package/ARCHITECTURE.md +290 -0
- package/CHANGELOG.md +158 -0
- package/DEVELOPMENT.md +197 -0
- package/KNOWN_LIMITS.md +80 -0
- package/LICENSE +21 -0
- package/README.md +288 -0
- package/extensions/backend-tool-actions.test.ts +59 -0
- package/extensions/backend-tool-actions.ts +31 -0
- package/extensions/command-drivers.test.ts +37 -0
- package/extensions/command-drivers.ts +126 -0
- package/extensions/command-parity.test.ts +560 -0
- package/extensions/command-visibility.test.ts +21 -0
- package/extensions/command-visibility.ts +10 -0
- package/extensions/index.ts +3218 -0
- package/extensions/llm-tools.test.ts +537 -0
- package/extensions/model-catalog.test.ts +34 -0
- package/extensions/model-catalog.ts +173 -0
- package/extensions/peer-history.test.ts +141 -0
- package/extensions/peer-history.ts +90 -0
- package/extensions/peer-naming.test.ts +25 -0
- package/extensions/peer-naming.ts +129 -0
- package/extensions/peer-relay.test.ts +122 -0
- package/extensions/peer-relay.ts +83 -0
- package/extensions/peer-ux.test.ts +239 -0
- package/extensions/peer-ux.ts +327 -0
- package/extensions/persistence.test.ts +68 -0
- package/extensions/persistence.ts +67 -0
- package/extensions/prompts/extension-log-tool.md +5 -0
- package/extensions/prompts/peer-ask-tool.md +5 -0
- package/extensions/prompts/peer-bridge-system.md +4 -0
- package/extensions/prompts/peer-history-tool.md +3 -0
- package/extensions/prompts/peer-init-user-help.md +11 -0
- package/extensions/prompts/peer-init.md +17 -0
- package/extensions/prompts/peer-interrupt-tool.md +2 -0
- package/extensions/prompts/peer-list-tool.md +3 -0
- package/extensions/prompts/peer-no-babysitting.md +6 -0
- package/extensions/prompts/peer-send-tool.md +5 -0
- package/extensions/prompts/peer-start-tool.md +7 -0
- package/extensions/prompts/peer-stop-tool.md +3 -0
- package/extensions/prompts/runtime-models-tool.md +6 -0
- package/extensions/prompts/subagent-list-tool.md +3 -0
- package/extensions/prompts/subagent-run-tool.md +6 -0
- package/extensions/prompts/subagent-status-tool.md +2 -0
- package/extensions/prompts/team-list-tool.md +2 -0
- package/extensions/prompts/team-message-tool.md +2 -0
- package/extensions/prompts/team-spawn-tool.md +5 -0
- package/extensions/prompts/team-stop-tool.md +2 -0
- package/extensions/prompts/team-task-tool.md +3 -0
- package/extensions/prompts.ts +41 -0
- package/extensions/runtime-driver.test.ts +38 -0
- package/extensions/runtime-driver.ts +33 -0
- package/extensions/runtime-safety.test.ts +21 -0
- package/extensions/runtime-safety.ts +49 -0
- package/extensions/support.test.ts +144 -0
- package/extensions/support.ts +205 -0
- package/extensions/tool-inputs.test.ts +45 -0
- package/extensions/tool-inputs.ts +79 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.d.ts +48 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js +406 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/bridge.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.d.ts +2 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js +18 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/cli.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.d.ts +5 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js +5 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/index.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.d.ts +12 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js +31 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/persistence.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.d.ts +12 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js +347 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/pi-intercom-transport.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.d.ts +103 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js +2 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/dist/types.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/intercom-bridge/package.json +32 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/cli.d.ts +2 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js +26 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/cli.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.d.ts +4 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js +12 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/driver-config.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.d.ts +8 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js +320 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/claude-sdk.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.d.ts +24 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js +266 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/codex-cli.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.d.ts +72 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js +2 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/drivers/messages.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/index.d.ts +6 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/index.js +5 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/index.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.d.ts +16 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js +94 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/persistence.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.d.ts +31 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js +409 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/runtime.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/types.d.ts +185 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/types.js +2 -0
- package/node_modules/@pi-claude-code-agent/runtime/dist/types.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/runtime/package.json +32 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.d.ts +34 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js +327 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/backend.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.d.ts +2 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js +17 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/cli.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.d.ts +4 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js +4 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/index.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.d.ts +12 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js +81 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/persistence.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.d.ts +72 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js +2 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/dist/types.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/subagents-backend/package.json +32 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.d.ts +27 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js +194 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/backend.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.d.ts +2 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js +21 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/cli.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.d.ts +4 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js +4 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/index.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.d.ts +8 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js +66 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/persistence.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.d.ts +41 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js +2 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/dist/types.js.map +1 -0
- package/node_modules/@pi-claude-code-agent/teams-backend/package.json +33 -0
- package/package.json +98 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createAttentionLedger } from "./support.ts";
|
|
7
|
+
import { readAttentionLedger, serializeAttentionLedger, writeAttentionLedger } from "./persistence.ts";
|
|
8
|
+
|
|
9
|
+
test("attention ledger persistence round-trips ack and snooze state", async () => {
|
|
10
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-ca-leash-attention-"));
|
|
11
|
+
const file = join(dir, "attention-ledger.json");
|
|
12
|
+
const ledger = {
|
|
13
|
+
runs: {
|
|
14
|
+
run1: {
|
|
15
|
+
note: "Needs attention: idle for 6000ms",
|
|
16
|
+
lastNotifiedNote: "Needs attention: idle for 6000ms",
|
|
17
|
+
acknowledgedNote: "Needs attention: idle for 6000ms",
|
|
18
|
+
snoozedUntil: 12_345,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await writeAttentionLedger(file, ledger);
|
|
24
|
+
const restored = await readAttentionLedger(file);
|
|
25
|
+
assert.deepEqual(restored, ledger);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("missing or invalid attention ledger falls back to empty state", async () => {
|
|
29
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-ca-leash-attention-"));
|
|
30
|
+
const file = join(dir, "attention-ledger.json");
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(await readAttentionLedger(file), createAttentionLedger());
|
|
33
|
+
|
|
34
|
+
await writeFile(file, "{ definitely-not-json\n", "utf8");
|
|
35
|
+
assert.deepEqual(await readAttentionLedger(file), createAttentionLedger());
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("attention ledger persistence sanitizes malformed entries", async () => {
|
|
39
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-ca-leash-attention-"));
|
|
40
|
+
const file = join(dir, "attention-ledger.json");
|
|
41
|
+
|
|
42
|
+
await writeFile(file, JSON.stringify({
|
|
43
|
+
runs: {
|
|
44
|
+
keep: {
|
|
45
|
+
note: "Needs attention: idle for 9000ms",
|
|
46
|
+
lastNotifiedNote: "Needs attention: idle for 9000ms",
|
|
47
|
+
acknowledgedNote: 42,
|
|
48
|
+
snoozedUntil: "later",
|
|
49
|
+
},
|
|
50
|
+
drop1: null,
|
|
51
|
+
drop2: { acknowledgedNote: "missing note" },
|
|
52
|
+
},
|
|
53
|
+
}), "utf8");
|
|
54
|
+
|
|
55
|
+
const restored = await readAttentionLedger(file);
|
|
56
|
+
assert.deepEqual(restored, {
|
|
57
|
+
runs: {
|
|
58
|
+
keep: {
|
|
59
|
+
note: "Needs attention: idle for 9000ms",
|
|
60
|
+
lastNotifiedNote: "Needs attention: idle for 9000ms",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const serialized = serializeAttentionLedger(restored);
|
|
66
|
+
assert.equal(JSON.parse(serialized).runs.keep.note, "Needs attention: idle for 9000ms");
|
|
67
|
+
assert.match(await readFile(file, "utf8"), /drop1/);
|
|
68
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import type { AttentionLedger, AttentionLedgerEntry } from "./support.js";
|
|
5
|
+
import { createAttentionLedger } from "./support.js";
|
|
6
|
+
|
|
7
|
+
export async function readAttentionLedger(path: string): Promise<AttentionLedger> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(path, "utf8");
|
|
10
|
+
return normalizeAttentionLedger(JSON.parse(raw));
|
|
11
|
+
} catch {
|
|
12
|
+
return createAttentionLedger();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function writeAttentionLedger(path: string, ledger: AttentionLedger): Promise<void> {
|
|
17
|
+
await mkdir(dirname(path), { recursive: true });
|
|
18
|
+
const tempPath = `${path}.${randomUUID()}.tmp`;
|
|
19
|
+
await writeFile(tempPath, `${serializeAttentionLedger(ledger)}\n`, "utf8");
|
|
20
|
+
await rename(tempPath, path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function serializeAttentionLedger(ledger: AttentionLedger): string {
|
|
24
|
+
return JSON.stringify(normalizeAttentionLedger(ledger), null, 2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeAttentionLedger(value: unknown): AttentionLedger {
|
|
28
|
+
const result = createAttentionLedger();
|
|
29
|
+
if (!isRecord(value) || !isRecord(value.runs)) {
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const [runId, entry] of Object.entries(value.runs)) {
|
|
34
|
+
const normalized = normalizeAttentionLedgerEntry(entry);
|
|
35
|
+
if (normalized) {
|
|
36
|
+
result.runs[runId] = normalized;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeAttentionLedgerEntry(value: unknown): AttentionLedgerEntry | undefined {
|
|
44
|
+
if (!isRecord(value) || typeof value.note !== "string" || value.note.trim().length === 0) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const entry: AttentionLedgerEntry = {
|
|
49
|
+
note: value.note,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (typeof value.lastNotifiedNote === "string") {
|
|
53
|
+
entry.lastNotifiedNote = value.lastNotifiedNote;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value.acknowledgedNote === "string") {
|
|
56
|
+
entry.acknowledgedNote = value.acknowledgedNote;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value.snoozedUntil === "number" && Number.isFinite(value.snoozedUntil) && value.snoozedUntil > 0) {
|
|
59
|
+
entry.snoozedUntil = value.snoozedUntil;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return entry;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
66
|
+
return typeof value === "object" && value !== null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Record structured pi-ca-leash extension feedback in .pi-ca-leash/log.md.
|
|
2
|
+
- Use `extension_log` for product friction, confusing UX, missing guidance, brittle flows, poor defaults, repeated agent mistakes caused by the extension, or runtime behavior that should be smoothed.
|
|
3
|
+
- Do not use it for normal task notes, user project TODOs, or every expected runtime failure.
|
|
4
|
+
- Do not log secrets, credentials, full private transcripts, or unnecessary user-private content.
|
|
5
|
+
- Keep entries concise and actionable.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Ask an existing peer for a status update, follow-up, or delegated result. Optionally pass model to switch the peer model persistently.
|
|
2
|
+
- Use `peer_ask` only with an existing peer name.
|
|
3
|
+
- Prefer concise direct asks because the peer reply is returned into the current turn.
|
|
4
|
+
- Call `runtime_models` first when you need the supported model ids for a driver.
|
|
5
|
+
- Pass `model` when you want this peer to switch models for this and future asks.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
You are a long-lived coding agent worker reached through intercom-style messages.
|
|
2
|
+
Treat new messages as continuation of same session, not fresh bootstrap.
|
|
3
|
+
For asks and replies, answer concisely and directly.
|
|
4
|
+
When you finish handling one inbound message, end in a clean idle state.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Peer mode is active.
|
|
2
|
+
|
|
3
|
+
Common commands:
|
|
4
|
+
- `/peer start <prompt>` starts a background peer.
|
|
5
|
+
- `/peer ask <name> | <message>` waits for a reply.
|
|
6
|
+
- `/peer send <name> | <message>` sends fire-and-forget work.
|
|
7
|
+
- `/peer list` shows peers and current state.
|
|
8
|
+
- `/peer history <name>` scrolls a peer transcript.
|
|
9
|
+
- `/peer models` shows model ids and aliases.
|
|
10
|
+
- `/peer dashboard` opens the peer dashboard.
|
|
11
|
+
- `/peer help` shows full command help.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
How to work with pi-ca-leash:
|
|
2
|
+
- You are the orchestrator in the driver's seat. Keep the plan, state, decisions, and final judgment in the main turn.
|
|
3
|
+
- Treat peers as long-lived coding agents you coordinate through short handoffs, either in parallel or across many sequential sessions.
|
|
4
|
+
- You can run multiple specialized peers at the same time when their work is independent, for example one planning/review peer and one implementation peer.
|
|
5
|
+
- Most peers work in the same repository checkout. Prefer file-based handoffs through changed files, notes, tests, and diffs when that is cheaper than copying context through prompts and replies.
|
|
6
|
+
- Optimize for control-plane tokens: read the latest peer relay first, then inspect only the evidence needed to decide the next step.
|
|
7
|
+
- Delegate bounded work with a clear output: investigate one area, review one change, implement one scoped fix, or summarize one source.
|
|
8
|
+
- Do not delegate work that blocks your immediate next step unless the peer can run while you make progress elsewhere.
|
|
9
|
+
- It is okay to wait passively for a peer. Peer completion, blocked, or failure states are automatically relayed into the main chat with the peer's latest visible message.
|
|
10
|
+
- Do not poll peers with repeated `peer_list`, `peer_history`, or `peer_ask` status checks. Use `peer_list` only when you need names or states.
|
|
11
|
+
- Use `peer_history` like a human scrolling back in a chat: start with a small recent page, follow cursors only as needed, and do not reread full transcripts by default.
|
|
12
|
+
- Prompt peers to return compact, verifiable outputs: changed files, commands run, test results, key findings, blockers, and residual risk. Do not ask them to paste large logs unless necessary.
|
|
13
|
+
- Verification is risk-based. For low-risk read-only work, sanity-check the conclusion; for code changes, inspect the diff and relevant tests; for high-risk architecture, security, data loss, or broad edits, do deeper verification or assign an independent review peer.
|
|
14
|
+
- When a peer returns, integrate useful parts and discard weak or stale assumptions. The orchestrator owns the final answer.
|
|
15
|
+
- Choose models by risk and cost: strongest for architecture, ambiguous debugging, and risky edits; coding-focused for implementation; cheaper or faster for narrow checks, summaries, and parallel exploration.
|
|
16
|
+
- Treat model catalog data as advisory. The local runtime decides actual availability.
|
|
17
|
+
- While pi-ca-leash is pre-1.0, record concise extension friction, confusing guidance, bad defaults, brittle flows, or repeated interaction problems with `extension_log`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
How to work with this peer:
|
|
2
|
+
- Treat it as an async worker/subagent.
|
|
3
|
+
- Do not poll it with peer_list, peer_history, or repeated peer_ask status checks.
|
|
4
|
+
- Continue your own work or wait passively.
|
|
5
|
+
- The peer will send a follow-up into the main context when it is done, blocked, or failed.
|
|
6
|
+
- Only contact the peer if the user explicitly asks, or if the peer asks for input.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Send input to a peer without waiting for the reply. Use this when the peer will report back asynchronously.
|
|
2
|
+
- Use `peer_send` instead of `peer_ask` when you do not need an immediate reply.
|
|
3
|
+
- Do not use it to poll for status; wait for automated peer updates.
|
|
4
|
+
- If the peer is busy, wait for its automated update before sending more input.
|
|
5
|
+
- Call `runtime_models` first when you need the supported model ids for a driver.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Start a long-lived runtime-backed peer for delegated work. Returns peer name, state, session id, driver, model, cwd, and latest visible peer reply when available.
|
|
2
|
+
- Use `peer_start` when you want a reusable long-lived peer instead of solving the task in the current turn.
|
|
3
|
+
- Pass `name` only when you need a stable explicit peer name; otherwise let the tool auto-name from prompt.
|
|
4
|
+
- Pass `driver` when you need to force `claude-sdk` or `codex-cli` for this peer instead of using the extension default.
|
|
5
|
+
- Call `runtime_models` first when you need the supported model ids for a driver.
|
|
6
|
+
- Pass `model` and `cwd` when you need a specific model and working directory.
|
|
7
|
+
- Keep bootstrap prompts scoped. If the tool reports a prompt size warning, start a smaller peer and point it at files instead of pasting large context.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Inspect available model ids for claude-sdk and codex-cli before passing a model override to peer_start, peer_ask, subagent_run, or team_spawn.
|
|
2
|
+
- Use `runtime_models` before choosing a non-default model.
|
|
3
|
+
- Use `driver` to narrow results to `claude-sdk` or `codex-cli`.
|
|
4
|
+
- Default output is a short recommended list with advisory use cases; pass `verbose: true` only when you need every bundled model id.
|
|
5
|
+
- The report includes supported shorthand aliases. For example, Claude aliases such as `sonnet`, `opus`, and `haiku` are resolved to concrete model ids before launch.
|
|
6
|
+
- Catalog entries are advisory; provider and CLI entitlements can drift, so unknown model ids are passed through to the runtime.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Run a delegated subagent task through the local backend. Supports driver, model, cwd, and optional async execution.
|
|
2
|
+
- Use `subagent_run` when you need a bounded delegated run instead of a reusable peer.
|
|
3
|
+
- Pass `driver` to force `claude-sdk` or `codex-cli` for this run instead of using the extension default.
|
|
4
|
+
- Call `runtime_models` first when you need the supported model ids for a driver.
|
|
5
|
+
- Pass `async: true` when you want to launch a background run and inspect it later with `subagent_status` or `subagent_list`.
|
|
6
|
+
- Keep delegated tasks bounded. If the tool reports a prompt size warning, split the run into smaller slices.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Spawn a persistent teammate when you want a named worker you can task or message repeatedly.
|
|
2
|
+
- Use `team_spawn` for a persistent named worker, not for one-off bounded work.
|
|
3
|
+
- Pass `driver` to force `claude-sdk` or `codex-cli` for this teammate instead of using the extension default.
|
|
4
|
+
- Call `runtime_models` first when you need the supported model ids for a driver.
|
|
5
|
+
- Keep the teammate bootstrap prompt compact; give detailed work through later `team_task` or `team_message` calls.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
function readPrompt(name: string): string {
|
|
5
|
+
return readFileSync(fileURLToPath(new URL(`./prompts/${name}.md`, import.meta.url)), "utf8").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function readPromptBlock(name: string): { snippet: string; guidelines: string[] } {
|
|
9
|
+
const lines = readPrompt(name)
|
|
10
|
+
.split(/\r?\n/)
|
|
11
|
+
.map((line) => line.trim())
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
const [snippet = "", ...guidelines] = lines;
|
|
14
|
+
return {
|
|
15
|
+
snippet,
|
|
16
|
+
guidelines: guidelines.map((line) => line.replace(/^- /, "")),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const PEER_BRIDGE_APPEND_SYSTEM_PROMPT = readPrompt("peer-bridge-system").replace(/\s*\n\s*/g, " ");
|
|
21
|
+
export const PEER_NO_BABYSITTING_GUIDANCE = readPrompt("peer-no-babysitting");
|
|
22
|
+
export const PEER_INIT_GUIDE = readPrompt("peer-init");
|
|
23
|
+
export const PEER_INIT_USER_HELP = readPrompt("peer-init-user-help");
|
|
24
|
+
|
|
25
|
+
export const RUNTIME_MODELS_TOOL_PROMPT = readPromptBlock("runtime-models-tool");
|
|
26
|
+
export const EXTENSION_LOG_TOOL_PROMPT = readPromptBlock("extension-log-tool");
|
|
27
|
+
export const PEER_START_TOOL_PROMPT = readPromptBlock("peer-start-tool");
|
|
28
|
+
export const PEER_ASK_TOOL_PROMPT = readPromptBlock("peer-ask-tool");
|
|
29
|
+
export const PEER_HISTORY_TOOL_PROMPT = readPromptBlock("peer-history-tool");
|
|
30
|
+
export const PEER_INTERRUPT_TOOL_PROMPT = readPromptBlock("peer-interrupt-tool");
|
|
31
|
+
export const PEER_LIST_TOOL_PROMPT = readPromptBlock("peer-list-tool");
|
|
32
|
+
export const PEER_SEND_TOOL_PROMPT = readPromptBlock("peer-send-tool");
|
|
33
|
+
export const SUBAGENT_RUN_TOOL_PROMPT = readPromptBlock("subagent-run-tool");
|
|
34
|
+
export const PEER_STOP_TOOL_PROMPT = readPromptBlock("peer-stop-tool");
|
|
35
|
+
export const SUBAGENT_LIST_TOOL_PROMPT = readPromptBlock("subagent-list-tool");
|
|
36
|
+
export const SUBAGENT_STATUS_TOOL_PROMPT = readPromptBlock("subagent-status-tool");
|
|
37
|
+
export const TEAM_SPAWN_TOOL_PROMPT = readPromptBlock("team-spawn-tool");
|
|
38
|
+
export const TEAM_LIST_TOOL_PROMPT = readPromptBlock("team-list-tool");
|
|
39
|
+
export const TEAM_MESSAGE_TOOL_PROMPT = readPromptBlock("team-message-tool");
|
|
40
|
+
export const TEAM_STOP_TOOL_PROMPT = readPromptBlock("team-stop-tool");
|
|
41
|
+
export const TEAM_TASK_TOOL_PROMPT = readPromptBlock("team-task-tool");
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_RUNTIME_DRIVER_ENV,
|
|
5
|
+
parseRuntimeDriverName,
|
|
6
|
+
resolveExtensionRuntimeDriverConfig,
|
|
7
|
+
} from "./runtime-driver.ts";
|
|
8
|
+
|
|
9
|
+
test("parseRuntimeDriverName accepts supported names and rejects others", () => {
|
|
10
|
+
assert.equal(parseRuntimeDriverName("claude-sdk"), "claude-sdk");
|
|
11
|
+
assert.equal(parseRuntimeDriverName("codex-cli"), "codex-cli");
|
|
12
|
+
assert.equal(parseRuntimeDriverName(" codex-cli "), "codex-cli");
|
|
13
|
+
assert.equal(parseRuntimeDriverName("wat"), undefined);
|
|
14
|
+
assert.equal(parseRuntimeDriverName(undefined), undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("default runtime driver is claude-sdk when env unset", () => {
|
|
18
|
+
assert.deepEqual(resolveExtensionRuntimeDriverConfig({}), {
|
|
19
|
+
defaultDriver: "claude-sdk",
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("codex-cli env selects codex default driver", () => {
|
|
24
|
+
assert.deepEqual(resolveExtensionRuntimeDriverConfig({
|
|
25
|
+
[DEFAULT_RUNTIME_DRIVER_ENV]: "codex-cli",
|
|
26
|
+
}), {
|
|
27
|
+
defaultDriver: "codex-cli",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("invalid env falls back to claude-sdk with note", () => {
|
|
32
|
+
assert.deepEqual(resolveExtensionRuntimeDriverConfig({
|
|
33
|
+
[DEFAULT_RUNTIME_DRIVER_ENV]: "wat",
|
|
34
|
+
}), {
|
|
35
|
+
defaultDriver: "claude-sdk",
|
|
36
|
+
note: `invalid ${DEFAULT_RUNTIME_DRIVER_ENV}=wat; using claude-sdk`,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RuntimeDriverName } from "@pi-claude-code-agent/runtime";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_RUNTIME_DRIVER_ENV = "PI_CLAUDE_RUNTIME_DRIVER";
|
|
4
|
+
|
|
5
|
+
export function parseRuntimeDriverName(value: unknown): RuntimeDriverName | undefined {
|
|
6
|
+
if (typeof value !== "string") {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
return trimmed === "claude-sdk" || trimmed === "codex-cli" ? trimmed : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExtensionRuntimeDriverConfig {
|
|
14
|
+
defaultDriver: RuntimeDriverName;
|
|
15
|
+
note?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveExtensionRuntimeDriverConfig(
|
|
19
|
+
env: Record<string, string | undefined> = process.env,
|
|
20
|
+
): ExtensionRuntimeDriverConfig {
|
|
21
|
+
const raw = env[DEFAULT_RUNTIME_DRIVER_ENV]?.trim();
|
|
22
|
+
if (!raw) {
|
|
23
|
+
return { defaultDriver: "claude-sdk" };
|
|
24
|
+
}
|
|
25
|
+
const parsed = parseRuntimeDriverName(raw);
|
|
26
|
+
if (parsed) {
|
|
27
|
+
return { defaultDriver: parsed };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
defaultDriver: "claude-sdk",
|
|
31
|
+
note: `invalid ${DEFAULT_RUNTIME_DRIVER_ENV}=${raw}; using claude-sdk`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { describePromptSize, explainRuntimeFailure } from "./runtime-safety.ts";
|
|
4
|
+
|
|
5
|
+
test("describePromptSize warns for large prompts", () => {
|
|
6
|
+
assert.equal(describePromptSize("peer prompt", "small"), undefined);
|
|
7
|
+
|
|
8
|
+
const note = describePromptSize("peer prompt", "x".repeat(24_000));
|
|
9
|
+
assert.match(note ?? "", /prompt size note/);
|
|
10
|
+
assert.match(note ?? "", /peer prompt is 24000 chars/);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("explainRuntimeFailure adds actionable runtime hints", () => {
|
|
14
|
+
const binary = explainRuntimeFailure("Claude Code native binary not found", "claude-sdk");
|
|
15
|
+
assert.match(binary, /CLAUDE_CODE_EXECUTABLE/);
|
|
16
|
+
|
|
17
|
+
const bedrock = explainRuntimeFailure("Amazon Bedrock rejected request: missing API key", "claude-sdk", "opus");
|
|
18
|
+
assert.match(bedrock, /runtime_models/);
|
|
19
|
+
assert.match(bedrock, /credentials/);
|
|
20
|
+
assert.match(bedrock, /shorthand alias/);
|
|
21
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const PROMPT_WARNING_CHAR_THRESHOLD = 24_000;
|
|
2
|
+
const PROMPT_DANGER_CHAR_THRESHOLD = 100_000;
|
|
3
|
+
|
|
4
|
+
export function describePromptSize(label: string, text: string): string | undefined {
|
|
5
|
+
const chars = text.length;
|
|
6
|
+
if (chars < PROMPT_WARNING_CHAR_THRESHOLD) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const approxTokens = Math.ceil(chars / 4);
|
|
11
|
+
const severity = chars >= PROMPT_DANGER_CHAR_THRESHOLD ? "large prompt risk" : "prompt size note";
|
|
12
|
+
return `${severity}: ${label} is ${chars} chars (~${approxTokens} tokens). Prefer a smaller scoped prompt or point the peer at files instead of pasting large context.`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function explainRuntimeFailure(message: string, driver?: string, model?: string): string {
|
|
16
|
+
const lower = message.toLowerCase();
|
|
17
|
+
const hints: string[] = [];
|
|
18
|
+
|
|
19
|
+
if (lower.includes("native binary not found") || lower.includes("claude code") && lower.includes("not found")) {
|
|
20
|
+
hints.push("Claude Code executable was not found. Install Claude Code or set CLAUDE_CODE_EXECUTABLE to the native binary path.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (lower.includes("enoent") || lower.includes("spawn") && lower.includes("not found")) {
|
|
24
|
+
const binary = driver === "codex-cli" ? "codex" : driver === "claude-sdk" ? "Claude Code" : "runtime";
|
|
25
|
+
hints.push(`${binary} executable could not be spawned. Check PATH or configure the matching executable env var before starting peers.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (lower.includes("bedrock")) {
|
|
29
|
+
hints.push("The provider appears to route through Amazon Bedrock. Use a fully qualified catalog model id from runtime_models, or configure the required Bedrock credentials.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (lower.includes("api key") || lower.includes("apikey") || lower.includes("unauthorized")) {
|
|
33
|
+
hints.push("Provider credentials are missing or rejected for this runtime/model.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (lower.includes("prompt is too long") || lower.includes("prompt too long") || lower.includes("context length")) {
|
|
37
|
+
hints.push("The prompt exceeded the provider context budget. Split the task into smaller slices or ask the peer to inspect files instead of pasting large context.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (model && /^(opus|sonnet|haiku)$/i.test(model.trim())) {
|
|
41
|
+
hints.push(`"${model}" is a shorthand alias; pi-ca-leash now resolves it before future runtime calls. Use runtime_models to inspect the exact id.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (hints.length === 0) {
|
|
45
|
+
return message;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [message, ...[...new Set(hints)].map((hint) => `Hint: ${hint}`)].join("\n");
|
|
49
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import type { SubagentRunRecord } from "@pi-claude-code-agent/subagents-backend";
|
|
4
|
+
import {
|
|
5
|
+
acknowledgeAttention,
|
|
6
|
+
computeWidgetSignature,
|
|
7
|
+
createAttentionLedger,
|
|
8
|
+
createDashboardState,
|
|
9
|
+
describeAttentionState,
|
|
10
|
+
detectConnectivityTransition,
|
|
11
|
+
recordDashboardEvent,
|
|
12
|
+
recordDashboardRefresh,
|
|
13
|
+
reconcileAttentionLedger,
|
|
14
|
+
shouldRebindTransport,
|
|
15
|
+
shouldSkipBackgroundRefresh,
|
|
16
|
+
snoozeAttention,
|
|
17
|
+
type WidgetSignatureInput,
|
|
18
|
+
} from "./support.ts";
|
|
19
|
+
|
|
20
|
+
function makeRun(overrides: Partial<SubagentRunRecord> = {}): SubagentRunRecord {
|
|
21
|
+
return {
|
|
22
|
+
runId: overrides.runId ?? "run-12345678",
|
|
23
|
+
runner: "claude-code-agent",
|
|
24
|
+
agentName: overrides.agentName ?? "worker",
|
|
25
|
+
sessionId: overrides.sessionId,
|
|
26
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
27
|
+
model: overrides.model,
|
|
28
|
+
state: overrides.state ?? "running",
|
|
29
|
+
context: overrides.context ?? "fresh",
|
|
30
|
+
createdAt: overrides.createdAt ?? "2026-04-29T10:00:00.000Z",
|
|
31
|
+
updatedAt: overrides.updatedAt ?? "2026-04-29T10:00:00.000Z",
|
|
32
|
+
lastActivityAt: overrides.lastActivityAt ?? "2026-04-29T10:00:00.000Z",
|
|
33
|
+
task: overrides.task ?? "Investigate",
|
|
34
|
+
result: overrides.result,
|
|
35
|
+
note: overrides.note,
|
|
36
|
+
raw: overrides.raw,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("dashboard refresh does not overwrite last event timestamp", () => {
|
|
41
|
+
const state = createDashboardState("startup", 100);
|
|
42
|
+
recordDashboardRefresh(state, 250);
|
|
43
|
+
assert.deepEqual(state, {
|
|
44
|
+
lastEvent: "startup",
|
|
45
|
+
lastEventAt: 100,
|
|
46
|
+
lastRefreshedAt: 250,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
recordDashboardEvent(state, "intercom connected", 400);
|
|
50
|
+
assert.deepEqual(state, {
|
|
51
|
+
lastEvent: "intercom connected",
|
|
52
|
+
lastEventAt: 400,
|
|
53
|
+
lastRefreshedAt: 400,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("connectivity transitions only fire on real flips", () => {
|
|
58
|
+
assert.equal(detectConnectivityTransition(undefined, false), undefined);
|
|
59
|
+
assert.equal(detectConnectivityTransition(undefined, true), undefined);
|
|
60
|
+
assert.equal(detectConnectivityTransition(false, false), undefined);
|
|
61
|
+
assert.equal(detectConnectivityTransition(true, true), undefined);
|
|
62
|
+
assert.equal(detectConnectivityTransition(false, true), "connected");
|
|
63
|
+
assert.equal(detectConnectivityTransition(true, false), "disconnected");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("transport rebind only needed when peers are missing broker connections", () => {
|
|
67
|
+
assert.equal(shouldRebindTransport(undefined), false);
|
|
68
|
+
assert.equal(shouldRebindTransport({ kind: "pi-intercom", boundPeers: 0, connectedPeers: 0 }), false);
|
|
69
|
+
assert.equal(shouldRebindTransport({ kind: "pi-intercom", boundPeers: 2, connectedPeers: 2 }), false);
|
|
70
|
+
assert.equal(shouldRebindTransport({ kind: "pi-intercom", boundPeers: 2, connectedPeers: 1 }), true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("computeWidgetSignature is stable for identical content and changes on peer state, model, or update time", () => {
|
|
74
|
+
const base: WidgetSignatureInput = {
|
|
75
|
+
peerRows: [{ name: "reviewer", state: "idle", activity: "last reply: ok", lastUpdateAt: "2026-05-02T10:00:00.000Z" }],
|
|
76
|
+
transportDegraded: false,
|
|
77
|
+
lastEvent: "startup",
|
|
78
|
+
};
|
|
79
|
+
assert.equal(computeWidgetSignature(base), computeWidgetSignature(base));
|
|
80
|
+
assert.notEqual(
|
|
81
|
+
computeWidgetSignature(base),
|
|
82
|
+
computeWidgetSignature({ ...base, peerRows: [{ name: "reviewer", state: "busy", activity: "Bash: npm test", lastUpdateAt: "2026-05-02T10:00:00.000Z" }] }),
|
|
83
|
+
);
|
|
84
|
+
assert.notEqual(
|
|
85
|
+
computeWidgetSignature(base),
|
|
86
|
+
computeWidgetSignature({ ...base, peerRows: [{ name: "reviewer", state: "idle", activity: "last reply: ok", lastUpdateAt: "2026-05-02T10:00:00.000Z", model: "claude-sonnet-4-6" }] }),
|
|
87
|
+
);
|
|
88
|
+
assert.notEqual(
|
|
89
|
+
computeWidgetSignature(base),
|
|
90
|
+
computeWidgetSignature({ ...base, peerRows: [{ name: "reviewer", state: "idle", activity: "last reply: ok", lastUpdateAt: "2026-05-02T10:01:00.000Z" }] }),
|
|
91
|
+
);
|
|
92
|
+
assert.notEqual(
|
|
93
|
+
computeWidgetSignature(base),
|
|
94
|
+
computeWidgetSignature({ ...base, peerRows: [{ name: "reviewer", state: "idle", activity: "last reply: ok", lastUpdateAt: "2026-05-02T10:00:00.000Z", contextPercentage: 25 }] }),
|
|
95
|
+
);
|
|
96
|
+
assert.equal(
|
|
97
|
+
computeWidgetSignature(base),
|
|
98
|
+
computeWidgetSignature({ ...base }),
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("shouldSkipBackgroundRefresh suppresses rapid refreshes within min interval", () => {
|
|
103
|
+
const state = createDashboardState("startup", 1_000);
|
|
104
|
+
|
|
105
|
+
assert.equal(shouldSkipBackgroundRefresh(state, 1_500, 3_000), true);
|
|
106
|
+
assert.equal(shouldSkipBackgroundRefresh(state, 3_999, 3_000), true);
|
|
107
|
+
assert.equal(shouldSkipBackgroundRefresh(state, 4_000, 3_000), false);
|
|
108
|
+
assert.equal(shouldSkipBackgroundRefresh(state, 5_000, 3_000), false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("attention ledger notifies once, supports ack, and resets on note change", () => {
|
|
112
|
+
const run = makeRun({ note: "Needs attention: idle for 6000ms" });
|
|
113
|
+
const first = reconcileAttentionLedger(createAttentionLedger(), [run], 1_000);
|
|
114
|
+
assert.equal(first.notify.length, 1);
|
|
115
|
+
assert.equal(first.active.length, 1);
|
|
116
|
+
assert.equal(describeAttentionState(first.active[0]!, 1_000), "active");
|
|
117
|
+
|
|
118
|
+
const acknowledged = acknowledgeAttention(first.ledger, run.runId);
|
|
119
|
+
const second = reconcileAttentionLedger(acknowledged, [run], 2_000);
|
|
120
|
+
assert.equal(second.notify.length, 0);
|
|
121
|
+
assert.equal(describeAttentionState(second.active[0]!, 2_000), "acked");
|
|
122
|
+
|
|
123
|
+
const changed = reconcileAttentionLedger(acknowledged, [{ ...run, note: "Needs attention: idle for 12000ms" }], 3_000);
|
|
124
|
+
assert.equal(changed.notify.length, 1);
|
|
125
|
+
assert.equal(describeAttentionState(changed.active[0]!, 3_000), "active");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("attention snooze suppresses alerts until expiry and clears when attention ends", () => {
|
|
129
|
+
const run = makeRun({ note: "Needs attention: idle for 6000ms" });
|
|
130
|
+
const first = reconcileAttentionLedger(createAttentionLedger(), [run], 1_000);
|
|
131
|
+
const snoozed = snoozeAttention(first.ledger, run.runId, 10_000);
|
|
132
|
+
|
|
133
|
+
const hidden = reconcileAttentionLedger(snoozed, [run], 5_000);
|
|
134
|
+
assert.equal(hidden.notify.length, 0);
|
|
135
|
+
assert.equal(describeAttentionState(hidden.active[0]!, 5_000), "snoozed 1m");
|
|
136
|
+
|
|
137
|
+
const resumed = reconcileAttentionLedger(hidden.ledger, [run], 11_000);
|
|
138
|
+
assert.equal(resumed.notify.length, 1);
|
|
139
|
+
assert.equal(describeAttentionState(resumed.active[0]!, 11_000), "active");
|
|
140
|
+
|
|
141
|
+
const cleared = reconcileAttentionLedger(resumed.ledger, [{ ...run, note: undefined }], 12_000);
|
|
142
|
+
assert.equal(cleared.active.length, 0);
|
|
143
|
+
assert.deepEqual(cleared.ledger.runs, {});
|
|
144
|
+
});
|