@inetafrica/open-claudia 3.0.26 → 3.0.28
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/CHANGELOG.md +15 -0
- package/bin/agency-ledger.js +91 -0
- package/bin/cli.js +15 -0
- package/bin/prompt-budget.js +49 -0
- package/bin/recall.js +6 -0
- package/channels/telegram/adapter.js +26 -3
- package/core/actions.js +25 -0
- package/core/agency/deliberate.js +332 -0
- package/core/agency/ledger.js +294 -0
- package/core/connected-apps.js +15 -0
- package/core/guardrail.js +251 -0
- package/core/handlers.js +50 -2
- package/core/prompt-budget.js +165 -0
- package/core/runner.js +19 -1
- package/core/state.js +16 -0
- package/core/system-prompt.js +29 -0
- package/package.json +6 -2
- package/test-agency-deliberate.js +119 -0
- package/test-agency-ledger.js +114 -0
- package/test-guardrail.js +126 -0
- package/test-prompt-budget.js +78 -0
- package/test-provider-language.js +11 -10
- package/test-telegram-poll-recovery.js +81 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Always-on prompt budget instrumentation (Track X0). One JSONL line per real
|
|
2
|
+
// user turn plus a rolling summary, so we can baseline the fixed prompt floor
|
|
3
|
+
// (soul + lessons + runtime state + slash-command list + tool headlines +
|
|
4
|
+
// relationship/mandate) and watch it grow as new systems are bolted on. This
|
|
5
|
+
// is read-only telemetry: it never changes what is sent to the model, it only
|
|
6
|
+
// measures it. Mirrors the storage shape of core/recall/metrics.js.
|
|
7
|
+
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
const CONFIG_DIR = require("../config-dir");
|
|
11
|
+
|
|
12
|
+
const LOG_FILE = path.join(CONFIG_DIR, "prompt-budget.jsonl");
|
|
13
|
+
const SUMMARY_FILE = path.join(CONFIG_DIR, "prompt-budget-summary.json");
|
|
14
|
+
let VERSION = "";
|
|
15
|
+
try { VERSION = require("../package.json").version || ""; } catch (e) {}
|
|
16
|
+
|
|
17
|
+
const MAX_LOG_BYTES = 2 * 1024 * 1024; // truncate the jsonl past ~2MB so telemetry never eats disk
|
|
18
|
+
|
|
19
|
+
function enabled() {
|
|
20
|
+
return String(process.env.PROMPT_BUDGET || "on").toLowerCase() !== "off";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Local, network-free token estimate. Markdown/English averages ~4 chars per
|
|
24
|
+
// token; this is deliberately an ESTIMATE — consistency across turns matters
|
|
25
|
+
// more than matching a specific tokenizer, since X0 tracks growth over time.
|
|
26
|
+
function estimateTokens(str) {
|
|
27
|
+
const chars = typeof str === "string" ? str.length : 0;
|
|
28
|
+
return Math.ceil(chars / 4);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function part(str) {
|
|
32
|
+
const chars = typeof str === "string" ? str.length : 0;
|
|
33
|
+
return { chars, tokens: estimateTokens(str) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function addSize(a, b) {
|
|
37
|
+
return { chars: a.chars + b.chars, tokens: a.tokens + b.tokens };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Which named prompt components form the fixed "always-on floor" vs the
|
|
41
|
+
// variable, on-demand recall payload and the turn's own user input.
|
|
42
|
+
const ALWAYS_ON_KEYS = [
|
|
43
|
+
"core", // soul + persona + lessons + skill index + tool index + fixed boilerplate
|
|
44
|
+
"runtime", // runtime state block
|
|
45
|
+
"speaker", // speaker + team roster + slash-command list
|
|
46
|
+
"speakerPersona",
|
|
47
|
+
"provider", // provider-native tools note
|
|
48
|
+
"voice",
|
|
49
|
+
"relationship", // external-mode / mandate
|
|
50
|
+
"spacesMode",
|
|
51
|
+
];
|
|
52
|
+
const VARIABLE_KEYS = ["transcript", "recall"];
|
|
53
|
+
|
|
54
|
+
// Break an assembled turn's named parts into sizes. `parts` keys mirror the
|
|
55
|
+
// components of buildTurnPrompt(); any subset is tolerated.
|
|
56
|
+
function measure(parts = {}) {
|
|
57
|
+
const components = {};
|
|
58
|
+
for (const [k, v] of Object.entries(parts)) components[k] = part(v);
|
|
59
|
+
|
|
60
|
+
let alwaysOn = { chars: 0, tokens: 0 };
|
|
61
|
+
for (const k of ALWAYS_ON_KEYS) if (components[k]) alwaysOn = addSize(alwaysOn, components[k]);
|
|
62
|
+
|
|
63
|
+
let variable = { chars: 0, tokens: 0 };
|
|
64
|
+
for (const k of VARIABLE_KEYS) if (components[k]) variable = addSize(variable, components[k]);
|
|
65
|
+
|
|
66
|
+
const recall = components.recall || { chars: 0, tokens: 0 };
|
|
67
|
+
const userPrompt = components.userPrompt || { chars: 0, tokens: 0 };
|
|
68
|
+
const total = addSize(addSize(alwaysOn, variable), userPrompt);
|
|
69
|
+
|
|
70
|
+
return { components, alwaysOn, recall, variable, userPrompt, total };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readSummary() {
|
|
74
|
+
try { return JSON.parse(fs.readFileSync(SUMMARY_FILE, "utf8")) || {}; } catch (e) { return {}; }
|
|
75
|
+
}
|
|
76
|
+
function writeSummary(s) {
|
|
77
|
+
try { fs.writeFileSync(SUMMARY_FILE, JSON.stringify(s, null, 2)); } catch (e) {}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function bumpSummary(m, meta) {
|
|
81
|
+
const s = readSummary();
|
|
82
|
+
s.turns = (s.turns || 0) + 1;
|
|
83
|
+
s.alwaysOnTokensTotal = (s.alwaysOnTokensTotal || 0) + m.alwaysOn.tokens;
|
|
84
|
+
s.recallTokensTotal = (s.recallTokensTotal || 0) + m.recall.tokens;
|
|
85
|
+
s.totalTokensTotal = (s.totalTokensTotal || 0) + m.total.tokens;
|
|
86
|
+
s.alwaysOnTokensMax = Math.max(s.alwaysOnTokensMax || 0, m.alwaysOn.tokens);
|
|
87
|
+
s.componentTokensTotal = s.componentTokensTotal || {};
|
|
88
|
+
for (const [k, v] of Object.entries(m.components)) {
|
|
89
|
+
s.componentTokensTotal[k] = (s.componentTokensTotal[k] || 0) + v.tokens;
|
|
90
|
+
}
|
|
91
|
+
// Latest snapshot — the current per-component breakdown ("what is the floor
|
|
92
|
+
// right now"), distinct from the rolling averages above.
|
|
93
|
+
s.latest = {
|
|
94
|
+
ts: new Date().toISOString(),
|
|
95
|
+
v: VERSION,
|
|
96
|
+
provider: meta.provider || "",
|
|
97
|
+
alwaysOnTokens: m.alwaysOn.tokens,
|
|
98
|
+
recallTokens: m.recall.tokens,
|
|
99
|
+
totalTokens: m.total.tokens,
|
|
100
|
+
components: Object.fromEntries(Object.entries(m.components).map(([k, v]) => [k, v.tokens])),
|
|
101
|
+
};
|
|
102
|
+
s.updatedAt = new Date().toISOString();
|
|
103
|
+
writeSummary(s);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function appendLog(rec) {
|
|
107
|
+
try {
|
|
108
|
+
try {
|
|
109
|
+
const st = fs.statSync(LOG_FILE);
|
|
110
|
+
if (st.size > MAX_LOG_BYTES) fs.rmSync(LOG_FILE, { force: true });
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + "\n");
|
|
113
|
+
} catch (e) {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Record one real turn's prompt budget. Best-effort; never throws.
|
|
117
|
+
function record(measurement, meta = {}) {
|
|
118
|
+
if (!enabled()) return;
|
|
119
|
+
try {
|
|
120
|
+
const m = measurement && measurement.total ? measurement : null;
|
|
121
|
+
if (!m) return;
|
|
122
|
+
const rec = {
|
|
123
|
+
ts: new Date().toISOString(),
|
|
124
|
+
v: VERSION,
|
|
125
|
+
provider: meta.provider || "",
|
|
126
|
+
runId: meta.runId || null,
|
|
127
|
+
alwaysOnTokens: m.alwaysOn.tokens,
|
|
128
|
+
recallTokens: m.recall.tokens,
|
|
129
|
+
totalTokens: m.total.tokens,
|
|
130
|
+
components: Object.fromEntries(Object.entries(m.components).map(([k, v]) => [k, v.tokens])),
|
|
131
|
+
};
|
|
132
|
+
appendLog(rec);
|
|
133
|
+
bumpSummary(m, meta);
|
|
134
|
+
} catch (e) {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Human-readable rolling view (rendered by `open-claudia prompt-budget`).
|
|
138
|
+
function summary() {
|
|
139
|
+
const s = readSummary();
|
|
140
|
+
const turns = s.turns || 0;
|
|
141
|
+
const avg = (total) => (turns ? Math.round((total || 0) / turns) : 0);
|
|
142
|
+
const componentAvg = {};
|
|
143
|
+
for (const [k, v] of Object.entries(s.componentTokensTotal || {})) componentAvg[k] = avg(v);
|
|
144
|
+
return {
|
|
145
|
+
turns,
|
|
146
|
+
avgAlwaysOnTokens: avg(s.alwaysOnTokensTotal),
|
|
147
|
+
maxAlwaysOnTokens: s.alwaysOnTokensMax || 0,
|
|
148
|
+
avgRecallTokens: avg(s.recallTokensTotal),
|
|
149
|
+
avgTotalTokens: avg(s.totalTokensTotal),
|
|
150
|
+
componentAvgTokens: componentAvg,
|
|
151
|
+
latest: s.latest || null,
|
|
152
|
+
updatedAt: s.updatedAt || "",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _resetForTest() {
|
|
157
|
+
try { fs.rmSync(LOG_FILE, { force: true }); } catch (e) {}
|
|
158
|
+
try { fs.rmSync(SUMMARY_FILE, { force: true }); } catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
LOG_FILE, SUMMARY_FILE, VERSION, enabled,
|
|
163
|
+
ALWAYS_ON_KEYS, VARIABLE_KEYS,
|
|
164
|
+
estimateTokens, measure, record, summary, _resetForTest,
|
|
165
|
+
};
|
package/core/runner.js
CHANGED
|
@@ -761,7 +761,7 @@ function createRunnerService(dependencies = {}) {
|
|
|
761
761
|
let bundle;
|
|
762
762
|
let invocation;
|
|
763
763
|
try {
|
|
764
|
-
bundle = await promptBuilder(prompt, { runContext, provider });
|
|
764
|
+
bundle = await promptBuilder(prompt, { runContext, provider, recordBudget: !capture });
|
|
765
765
|
invocation = invocationBuilder(provider, invocationContextForRun(runContext, bundle, provider));
|
|
766
766
|
if (dependencies.decorateInvocation) {
|
|
767
767
|
invocation = dependencies.decorateInvocation(invocation, runContext, bundle, provider);
|
|
@@ -1860,6 +1860,23 @@ async function handoffMemoryReview(result, runContext, store, state) {
|
|
|
1860
1860
|
});
|
|
1861
1861
|
}
|
|
1862
1862
|
|
|
1863
|
+
// Track C — owner-guardrail capture. After a foreground turn, a deterministic
|
|
1864
|
+
// detector spots durable-rule statements ("from now on / never / stop") in the
|
|
1865
|
+
// owner's message and captures them into lessons.md (or asks first). Owner-only
|
|
1866
|
+
// and best-effort: it never blocks or breaks a turn.
|
|
1867
|
+
async function considerGuardrail(result, runContext, store) {
|
|
1868
|
+
try {
|
|
1869
|
+
if (!result.ok || runContext.purpose !== "foreground") return;
|
|
1870
|
+
const guardrail = require("./guardrail");
|
|
1871
|
+
if (!guardrail.enabled()) return;
|
|
1872
|
+
const relationship = require("./relationship");
|
|
1873
|
+
const speaker = relationship.speakerFor(store?.adapter?.type, store?.channelId, store?.userId);
|
|
1874
|
+
if (relationship.guardActive(speaker)) return; // external speakers never capture
|
|
1875
|
+
const announce = (text, opts) => chatContext.run(store, () => send(text, opts));
|
|
1876
|
+
await guardrail.consider({ text: runContext.userPrompt, speaker, announce });
|
|
1877
|
+
} catch (e) { /* best-effort — a guardrail miss must never break a turn */ }
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1863
1880
|
function deferredRunResult() {
|
|
1864
1881
|
let resolve;
|
|
1865
1882
|
let reject;
|
|
@@ -1920,6 +1937,7 @@ async function executeAdmittedRun(runContext, state, store, capture, ownsAdmissi
|
|
|
1920
1937
|
try { service.turnObserver.onTurnSettled(!!result.ok); } catch (e) { /* best-effort */ }
|
|
1921
1938
|
}
|
|
1922
1939
|
if (!internalCapture) await handoffMemoryReview(result, runContext, store, state);
|
|
1940
|
+
if (!internalCapture) await considerGuardrail(result, runContext, store);
|
|
1923
1941
|
if (!internalCapture && state.settings?.backend === runContext.provider && state.settings.budget) {
|
|
1924
1942
|
state.settings.budget = null;
|
|
1925
1943
|
try { saveState({ strict: true }); }
|
package/core/state.js
CHANGED
|
@@ -652,6 +652,21 @@ function acquireRunLock(state = currentState()) {
|
|
|
652
652
|
return true;
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
// True when ANY user has a turn running, admitted-and-preparing, compacting, or
|
|
656
|
+
// waiting in its queue. Same busy predicate as acquireRunLock, but across every
|
|
657
|
+
// user state rather than one — the Telegram poll-wedge backstop consults this
|
|
658
|
+
// before a self-exit so a reboot never interrupts in-flight work. It fires on a
|
|
659
|
+
// timer outside any channel context (no "current" user) and a reboot kills the
|
|
660
|
+
// whole process, so it must scan all states, not just currentState().
|
|
661
|
+
function anyTurnActive() {
|
|
662
|
+
for (const s of userStates.values()) {
|
|
663
|
+
if (!s) continue;
|
|
664
|
+
if (s.runningProcess || s.preparingRun || s.isCompacting) return true;
|
|
665
|
+
if (Array.isArray(s.messageQueue) && s.messageQueue.length) return true;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
|
|
655
670
|
function migrationSources() {
|
|
656
671
|
return defaultMigrationSources({
|
|
657
672
|
configDir: CONFIG_DIR,
|
|
@@ -1363,6 +1378,7 @@ module.exports = {
|
|
|
1363
1378
|
resetSessionUsage,
|
|
1364
1379
|
resetSettings,
|
|
1365
1380
|
acquireRunLock,
|
|
1381
|
+
anyTurnActive,
|
|
1366
1382
|
saveState,
|
|
1367
1383
|
loadSessions,
|
|
1368
1384
|
loadSessionsDocument,
|
package/core/system-prompt.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
const tasksStore = require("./tasks");
|
|
16
16
|
const people = require("./people");
|
|
17
17
|
const commandsRegistry = require("./commands");
|
|
18
|
+
const promptBudget = require("./prompt-budget");
|
|
18
19
|
|
|
19
20
|
function loadSoul() {
|
|
20
21
|
const soul = fs.readFileSync(SOUL_FILE, "utf-8");
|
|
@@ -1068,12 +1069,40 @@ function createPromptBuilder(dependencies = {}) {
|
|
|
1068
1069
|
spacesTaskMode,
|
|
1069
1070
|
].filter((part) => typeof part === "string" && part.trim()).join("\n\n");
|
|
1070
1071
|
|
|
1072
|
+
// Track X0: measure the fixed always-on floor vs the variable recall
|
|
1073
|
+
// payload for this turn. Pure measurement — attached to the bundle for
|
|
1074
|
+
// any caller; only recorded to telemetry when the caller opts in
|
|
1075
|
+
// (real user turns, never sub-agent/capture builds).
|
|
1076
|
+
let budget = null;
|
|
1077
|
+
try {
|
|
1078
|
+
budget = promptBudget.measure({
|
|
1079
|
+
core: coreInstructions,
|
|
1080
|
+
transcript: transcriptContext,
|
|
1081
|
+
runtime: runtimeContext,
|
|
1082
|
+
speaker: speakerContext,
|
|
1083
|
+
speakerPersona,
|
|
1084
|
+
provider: providerContext,
|
|
1085
|
+
recall: recallResult.dynamicContext,
|
|
1086
|
+
voice: voiceContext,
|
|
1087
|
+
relationship: relationshipContext,
|
|
1088
|
+
spacesMode: spacesTaskMode,
|
|
1089
|
+
userPrompt,
|
|
1090
|
+
});
|
|
1091
|
+
if (opts.recordBudget) {
|
|
1092
|
+
promptBudget.record(budget, {
|
|
1093
|
+
provider: opts.provider?.id,
|
|
1094
|
+
runId: opts.runContext?.runId,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
} catch (_) { budget = null; }
|
|
1098
|
+
|
|
1071
1099
|
return {
|
|
1072
1100
|
coreInstructions,
|
|
1073
1101
|
dynamicContext,
|
|
1074
1102
|
userPrompt,
|
|
1075
1103
|
transcriptPaths,
|
|
1076
1104
|
recallMetadata: recallResult.metadata,
|
|
1105
|
+
promptBudget: budget,
|
|
1077
1106
|
};
|
|
1078
1107
|
},
|
|
1079
1108
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.28",
|
|
4
4
|
"description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js",
|
|
12
|
+
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js",
|
|
13
13
|
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"test-identity-prune.js",
|
|
67
67
|
"test-kazee-message-id.js",
|
|
68
68
|
"test-kazee-channel-health.js",
|
|
69
|
+
"test-prompt-budget.js",
|
|
70
|
+
"test-agency-ledger.js",
|
|
71
|
+
"test-agency-deliberate.js",
|
|
72
|
+
"test-guardrail.js",
|
|
69
73
|
"test-delivery-contract.js",
|
|
70
74
|
"test-run-lock.js",
|
|
71
75
|
"test-web-sessions.js",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Track B1 — advisory deliberation. Deterministic triage/rank, confidence floor
|
|
2
|
+
// and clear-margin behaviour, the injectable judge (including failure fallback),
|
|
3
|
+
// and best-effort shadow logging. No network, redirected config dir.
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "oc-delib-"));
|
|
11
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = TMP;
|
|
12
|
+
|
|
13
|
+
const D = require("./core/agency/deliberate");
|
|
14
|
+
|
|
15
|
+
const NOW = Date.parse("2026-07-15T12:00:00.000Z");
|
|
16
|
+
const DAY = 86400000;
|
|
17
|
+
|
|
18
|
+
function entry(o) {
|
|
19
|
+
return Object.assign(
|
|
20
|
+
{ source: "task", kind: "task", id: "x", title: "t", status: "open",
|
|
21
|
+
priority: null, due: null, lastTouched: NOW, staleDays: 0, blockedOn: null,
|
|
22
|
+
overdue: false, meta: {} },
|
|
23
|
+
o,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function ledgerOf(entries) {
|
|
27
|
+
return { generatedAt: NOW, counts: { total: entries.length }, entries };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- triage: notifications never candidates; bare open local task isn't either
|
|
31
|
+
assert.strictEqual(D.isCandidate(entry({ source: "notification", status: "info" }), NOW), false);
|
|
32
|
+
assert.strictEqual(D.isCandidate(entry({ source: "task", status: "open", due: null }), NOW), false);
|
|
33
|
+
assert.strictEqual(D.isCandidate(entry({ source: "task", status: "in_progress" }), NOW), true);
|
|
34
|
+
assert.strictEqual(D.isCandidate(entry({ source: "spaces", staleDays: 3 }), NOW), true);
|
|
35
|
+
|
|
36
|
+
// --- scoreOf: overdue dominates, signals surface
|
|
37
|
+
const sc = D.scoreOf(entry({ overdue: true, due: NOW - 2 * DAY, priority: "high" }), NOW);
|
|
38
|
+
assert.ok(sc.score > 100, "overdue+high scores high");
|
|
39
|
+
assert.ok(sc.signals.some((s) => s.k === "overdue"));
|
|
40
|
+
assert.ok(sc.signals.some((s) => s.k === "priority"));
|
|
41
|
+
|
|
42
|
+
(async () => {
|
|
43
|
+
// --- clear winner: overdue high-priority Spaces task beats an in-progress task
|
|
44
|
+
const p1 = await D.deliberate(ledgerOf([
|
|
45
|
+
entry({ source: "spaces", id: "s1", title: "Overdue!", overdue: true, due: NOW - 2 * DAY, priority: "high", staleDays: 2 }),
|
|
46
|
+
entry({ source: "task", id: "t1", title: "WIP", status: "in_progress", staleDays: 1 }),
|
|
47
|
+
entry({ source: "notification", id: "n1", status: "info" }),
|
|
48
|
+
]), { now: NOW });
|
|
49
|
+
assert.strictEqual(p1.proposed, true);
|
|
50
|
+
assert.strictEqual(p1.pick.id, "s1", "overdue item picked");
|
|
51
|
+
assert.strictEqual(p1.confidence, "high", "clear margin → high confidence");
|
|
52
|
+
assert.strictEqual(p1.method, "deterministic");
|
|
53
|
+
assert.strictEqual(p1.actionType, "ask", "overdue Spaces assignment → nudge");
|
|
54
|
+
assert.ok(p1.why.join(" ").includes("overdue"));
|
|
55
|
+
assert.ok(D.render(p1).includes("Overdue!"));
|
|
56
|
+
|
|
57
|
+
// --- default-silence: lone far-future item scores below the floor
|
|
58
|
+
const p2 = await D.deliberate(ledgerOf([
|
|
59
|
+
entry({ source: "spaces", id: "s2", title: "Someday", due: NOW + 20 * DAY, staleDays: 1 }),
|
|
60
|
+
]), { now: NOW });
|
|
61
|
+
assert.strictEqual(p2.proposed, false, "below confidence floor → propose nothing");
|
|
62
|
+
assert.ok(D.render(p2).includes("Nothing pressing"));
|
|
63
|
+
|
|
64
|
+
// --- ambiguous + judge OFF → deterministic, medium confidence
|
|
65
|
+
const twoWip = ledgerOf([
|
|
66
|
+
entry({ source: "task", id: "t1", title: "WIP one", status: "in_progress", staleDays: 1 }),
|
|
67
|
+
entry({ source: "task", id: "t2", title: "WIP two", status: "in_progress", staleDays: 1 }),
|
|
68
|
+
]);
|
|
69
|
+
const p3 = await D.deliberate(twoWip, { now: NOW });
|
|
70
|
+
assert.strictEqual(p3.proposed, true);
|
|
71
|
+
assert.strictEqual(p3.method, "deterministic");
|
|
72
|
+
assert.strictEqual(p3.confidence, "medium", "close margin → medium");
|
|
73
|
+
|
|
74
|
+
// --- ambiguous + injected judge overrides the pick + action
|
|
75
|
+
let judgeSaw = null;
|
|
76
|
+
const p4 = await D.deliberate(twoWip, {
|
|
77
|
+
now: NOW, useJudge: true,
|
|
78
|
+
judge: async (top) => { judgeSaw = top.map((c) => c.id); return { pickId: "t2", actionType: "schedule", why: "second is riper", confidence: "high" }; },
|
|
79
|
+
});
|
|
80
|
+
assert.deepStrictEqual(judgeSaw, ["t1", "t2"], "judge sees the top-N");
|
|
81
|
+
assert.strictEqual(p4.method, "judge");
|
|
82
|
+
assert.strictEqual(p4.pick.id, "t2", "judge override applied");
|
|
83
|
+
assert.strictEqual(p4.actionType, "schedule");
|
|
84
|
+
assert.strictEqual(p4.judgeWhy, "second is riper");
|
|
85
|
+
|
|
86
|
+
// --- judge failure falls back to deterministic, never throws
|
|
87
|
+
const p5 = await D.deliberate(twoWip, { now: NOW, useJudge: true, judge: async () => { throw new Error("boom"); } });
|
|
88
|
+
assert.strictEqual(p5.method, "deterministic");
|
|
89
|
+
assert.strictEqual(p5.pick.id, "t1");
|
|
90
|
+
|
|
91
|
+
// --- judge NOT called when the winner is clear (margin ≥ CLEAR_MARGIN)
|
|
92
|
+
let called = false;
|
|
93
|
+
const p6 = await D.deliberate(ledgerOf([
|
|
94
|
+
entry({ source: "spaces", id: "s1", title: "Overdue!", overdue: true, due: NOW - 5 * DAY, priority: "high" }),
|
|
95
|
+
entry({ source: "task", id: "t1", title: "WIP", status: "in_progress" }),
|
|
96
|
+
]), { now: NOW, useJudge: true, judge: async () => { called = true; return null; } });
|
|
97
|
+
assert.strictEqual(called, false, "clear margin skips the judge");
|
|
98
|
+
assert.strictEqual(p6.pick.id, "s1");
|
|
99
|
+
|
|
100
|
+
// --- shadow record: writes a jsonl row; AGENCY_SHADOW=off suppresses it
|
|
101
|
+
process.env.AGENCY_SHADOW = "on";
|
|
102
|
+
D.record(p1, { channel: "telegram:123" });
|
|
103
|
+
assert.ok(fs.existsSync(D.LOG_FILE), "shadow log written");
|
|
104
|
+
let lines = fs.readFileSync(D.LOG_FILE, "utf8").trim().split("\n");
|
|
105
|
+
assert.strictEqual(lines.length, 1);
|
|
106
|
+
const row = JSON.parse(lines[0]);
|
|
107
|
+
assert.strictEqual(row.pickId, "s1");
|
|
108
|
+
assert.strictEqual(row.channel, "telegram:123");
|
|
109
|
+
assert.strictEqual(row.method, "deterministic");
|
|
110
|
+
|
|
111
|
+
process.env.AGENCY_SHADOW = "off";
|
|
112
|
+
D.record(p1, {});
|
|
113
|
+
lines = fs.readFileSync(D.LOG_FILE, "utf8").trim().split("\n");
|
|
114
|
+
assert.strictEqual(lines.length, 1, "AGENCY_SHADOW=off suppresses the write");
|
|
115
|
+
process.env.AGENCY_SHADOW = "on";
|
|
116
|
+
|
|
117
|
+
try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (e) {}
|
|
118
|
+
console.log("test-agency-deliberate: OK");
|
|
119
|
+
})();
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Track B0 — agency ledger aggregator. Exercises the pure build()/normalizers/
|
|
2
|
+
// render, plus collect() with an injected Spaces fetch (no subprocess) and a
|
|
3
|
+
// redirected config dir so no real task/connector files are touched.
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "oc-ledger-"));
|
|
11
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = TMP;
|
|
12
|
+
|
|
13
|
+
const ledger = require("./core/agency/ledger");
|
|
14
|
+
|
|
15
|
+
const NOW = Date.parse("2026-07-15T12:00:00.000Z");
|
|
16
|
+
const DAY = 86400000;
|
|
17
|
+
|
|
18
|
+
// --- normalizeTask: local task, never overdue, no due date ---
|
|
19
|
+
const t1 = ledger.normalizeTask(
|
|
20
|
+
{ id: "a", content: "Ship X", status: "in_progress", updatedAt: NOW - 3600000 },
|
|
21
|
+
NOW,
|
|
22
|
+
);
|
|
23
|
+
assert.strictEqual(t1.source, "task");
|
|
24
|
+
assert.strictEqual(t1.status, "in_progress");
|
|
25
|
+
assert.strictEqual(t1.overdue, false);
|
|
26
|
+
assert.strictEqual(t1.due, null);
|
|
27
|
+
|
|
28
|
+
// --- normalizeSpacesTask: overdue + status + space name + priority ---
|
|
29
|
+
const s1 = ledger.normalizeSpacesTask(
|
|
30
|
+
{
|
|
31
|
+
id: "s1", title: "Leave page", status: "open", priority: "high",
|
|
32
|
+
dueDate: "2026-07-14T00:00:00.000Z", updatedAt: NOW - 2 * DAY,
|
|
33
|
+
spaceId: { name: "HR + Product" }, subtaskCount: 2, subtaskCompletedCount: 1,
|
|
34
|
+
},
|
|
35
|
+
NOW,
|
|
36
|
+
);
|
|
37
|
+
assert.strictEqual(s1.source, "spaces");
|
|
38
|
+
assert.strictEqual(s1.overdue, true, "past dueDate + not done → overdue");
|
|
39
|
+
assert.strictEqual(s1.meta.space, "HR + Product");
|
|
40
|
+
assert.strictEqual(s1.priority, "high");
|
|
41
|
+
|
|
42
|
+
// a completed Spaces task is done and never overdue, even past its due date
|
|
43
|
+
const s2 = ledger.normalizeSpacesTask(
|
|
44
|
+
{ id: "s2", title: "done", status: "completed", dueDate: "2026-07-01T00:00:00.000Z" },
|
|
45
|
+
NOW,
|
|
46
|
+
);
|
|
47
|
+
assert.strictEqual(s2.status, "done");
|
|
48
|
+
assert.strictEqual(s2.overdue, false);
|
|
49
|
+
|
|
50
|
+
// --- normalizeInbox: actor + task title fold into a readable line ---
|
|
51
|
+
const n1 = ledger.normalizeInbox(
|
|
52
|
+
{ id: "n1", type: "comment", actorName: "Jane", taskTitle: "Fix bug", createdAt: NOW - 1800000 },
|
|
53
|
+
NOW,
|
|
54
|
+
);
|
|
55
|
+
assert.strictEqual(n1.source, "notification");
|
|
56
|
+
assert.ok(n1.title.includes("Jane") && n1.title.includes("Fix bug"));
|
|
57
|
+
assert.strictEqual(n1.status, "info");
|
|
58
|
+
|
|
59
|
+
// --- build: drops completed, ranks overdue first, counts by source/stale ---
|
|
60
|
+
const l = ledger.build({
|
|
61
|
+
tasks: [
|
|
62
|
+
{ id: "a", content: "In progress task", status: "in_progress", updatedAt: NOW - 3600000 },
|
|
63
|
+
{ id: "b", content: "Done task", status: "completed", updatedAt: NOW - DAY },
|
|
64
|
+
{ id: "c", content: "Stale open", status: "pending", updatedAt: NOW - 30 * DAY },
|
|
65
|
+
],
|
|
66
|
+
spacesMine: [
|
|
67
|
+
{ id: "s1", title: "Overdue!", status: "open", dueDate: "2026-07-13T00:00:00.000Z", updatedAt: NOW - 5 * DAY },
|
|
68
|
+
],
|
|
69
|
+
inboxes: [
|
|
70
|
+
{ id: "n1", type: "comment", actorName: "Jane", taskTitle: "T", createdAt: NOW - 1800000 },
|
|
71
|
+
],
|
|
72
|
+
now: NOW,
|
|
73
|
+
});
|
|
74
|
+
assert.strictEqual(l.counts.total, 4, "1 completed dropped from 5 inputs");
|
|
75
|
+
assert.strictEqual(l.counts.overdue, 1);
|
|
76
|
+
assert.strictEqual(l.entries[0].id, "s1", "overdue entry ranked first");
|
|
77
|
+
assert.strictEqual(l.counts.stale, 1, "30d-untouched task is stale");
|
|
78
|
+
assert.strictEqual(l.counts.bySource.task, 2);
|
|
79
|
+
assert.strictEqual(l.counts.bySource.spaces, 1);
|
|
80
|
+
assert.strictEqual(l.counts.bySource.notification, 1);
|
|
81
|
+
|
|
82
|
+
// --- render: summary line + per-source sections ---
|
|
83
|
+
const text = ledger.render(l, { now: NOW });
|
|
84
|
+
assert.ok(text.includes("Agenda"));
|
|
85
|
+
assert.ok(text.includes("4 open"));
|
|
86
|
+
assert.ok(text.includes("1 overdue"));
|
|
87
|
+
assert.ok(text.includes("Spaces (assigned to me)"));
|
|
88
|
+
assert.ok(text.includes("Overdue!"));
|
|
89
|
+
|
|
90
|
+
// empty ledger renders the all-clear
|
|
91
|
+
const empty = ledger.render(ledger.build({ now: NOW }), { now: NOW });
|
|
92
|
+
assert.ok(empty.includes("Nothing on your plate"));
|
|
93
|
+
|
|
94
|
+
// --- collect: injected Spaces fetch, no adapter (no task files), no subprocess ---
|
|
95
|
+
(async () => {
|
|
96
|
+
const r = await ledger.collect({
|
|
97
|
+
now: NOW,
|
|
98
|
+
fetchSpacesMine: async () => [{ id: "s9", title: "Injected", status: "open" }],
|
|
99
|
+
});
|
|
100
|
+
assert.ok(r.entries.some((e) => e.id === "s9"), "injected spaces task present");
|
|
101
|
+
assert.strictEqual(typeof r.counts.total, "number");
|
|
102
|
+
|
|
103
|
+
// includeSpaces:false skips the fetch entirely
|
|
104
|
+
let called = false;
|
|
105
|
+
const r2 = await ledger.collect({
|
|
106
|
+
now: NOW, includeSpaces: false,
|
|
107
|
+
fetchSpacesMine: async () => { called = true; return [{ id: "nope" }]; },
|
|
108
|
+
});
|
|
109
|
+
assert.strictEqual(called, false, "includeSpaces:false skips the Spaces fetch");
|
|
110
|
+
assert.ok(!r2.entries.some((e) => e.id === "nope"));
|
|
111
|
+
|
|
112
|
+
try { fs.rmSync(TMP, { recursive: true, force: true }); } catch (e) {}
|
|
113
|
+
console.log("test-agency-ledger: OK");
|
|
114
|
+
})();
|