arkaos 4.13.2 → 4.14.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.
Files changed (130) hide show
  1. package/VERSION +1 -1
  2. package/arka/SKILL.md +16 -0
  3. package/bin/arka-doctor +20 -1
  4. package/config/cognition/schedules.yaml +16 -0
  5. package/config/hooks/_lib/fastpath/engine.cjs +337 -0
  6. package/config/hooks/gate-manifest.json +741 -0
  7. package/config/hooks/post-tool-use.cjs +113 -0
  8. package/config/hooks/pre-tool-use.cjs +109 -0
  9. package/config/hooks/session-end.ps1 +40 -0
  10. package/config/hooks/session-end.sh +55 -0
  11. package/config/hooks/session-start.sh +25 -189
  12. package/config/hooks/subagent-stop.ps1 +40 -0
  13. package/config/hooks/subagent-stop.sh +56 -0
  14. package/config/mcp-policy.yaml +8 -0
  15. package/config/skills-curated.yaml +39 -0
  16. package/config/statusline.sh +57 -1
  17. package/core/cognition/capture/__pycache__/store.cpython-313.pyc +0 -0
  18. package/core/cognition/capture/store.py +7 -2
  19. package/core/evals/__pycache__/__init__.cpython-314.pyc +0 -0
  20. package/core/evals/__pycache__/sanitizer.cpython-314.pyc +0 -0
  21. package/core/evals/__pycache__/schema.cpython-314.pyc +0 -0
  22. package/core/evals/__pycache__/verdict_labels.cpython-314.pyc +0 -0
  23. package/core/forge/__pycache__/complexity.cpython-314.pyc +0 -0
  24. package/core/governance/__pycache__/evidence_checks.cpython-313.pyc +0 -0
  25. package/core/governance/__pycache__/judge.cpython-314.pyc +0 -0
  26. package/core/governance/__pycache__/leak_scanner.cpython-314.pyc +0 -0
  27. package/core/governance/__pycache__/qg_verdict.cpython-314.pyc +0 -0
  28. package/core/governance/__pycache__/routing_feedback.cpython-313.pyc +0 -0
  29. package/core/governance/__pycache__/routing_feedback.cpython-314.pyc +0 -0
  30. package/core/governance/__pycache__/routing_feedback_cli.cpython-313.pyc +0 -0
  31. package/core/governance/evidence_checks.py +42 -7
  32. package/core/governance/routing_feedback.py +229 -0
  33. package/core/governance/routing_feedback_cli.py +49 -0
  34. package/core/hooks/__pycache__/_shared.cpython-312.pyc +0 -0
  35. package/core/hooks/__pycache__/gate_manifest.cpython-313.pyc +0 -0
  36. package/core/hooks/__pycache__/post_tool_use.cpython-312.pyc +0 -0
  37. package/core/hooks/__pycache__/post_tool_use.cpython-313.pyc +0 -0
  38. package/core/hooks/__pycache__/pre_tool_use.cpython-312.pyc +0 -0
  39. package/core/hooks/__pycache__/session_end.cpython-313.pyc +0 -0
  40. package/core/hooks/__pycache__/session_start.cpython-312.pyc +0 -0
  41. package/core/hooks/__pycache__/session_start.cpython-313.pyc +0 -0
  42. package/core/hooks/__pycache__/stop.cpython-313.pyc +0 -0
  43. package/core/hooks/__pycache__/stop.cpython-314.pyc +0 -0
  44. package/core/hooks/__pycache__/subagent_stop.cpython-313.pyc +0 -0
  45. package/core/hooks/__pycache__/subagent_stop.cpython-314.pyc +0 -0
  46. package/core/hooks/__pycache__/user_prompt_submit.cpython-313.pyc +0 -0
  47. package/core/hooks/__pycache__/user_prompt_submit.cpython-314.pyc +0 -0
  48. package/core/hooks/gate_manifest.py +366 -0
  49. package/core/hooks/post_tool_use.py +5 -2
  50. package/core/hooks/session_end.py +127 -0
  51. package/core/hooks/session_start.py +322 -0
  52. package/core/hooks/stop.py +69 -0
  53. package/core/hooks/subagent_stop.py +186 -0
  54. package/core/hooks/user_prompt_submit.py +10 -6
  55. package/core/knowledge/__pycache__/embedding_backends.cpython-313.pyc +0 -0
  56. package/core/knowledge/__pycache__/embedding_backends.cpython-314.pyc +0 -0
  57. package/core/knowledge/__pycache__/pattern_cards.cpython-313.pyc +0 -0
  58. package/core/knowledge/__pycache__/pattern_cards.cpython-314.pyc +0 -0
  59. package/core/knowledge/__pycache__/recipes.cpython-314.pyc +0 -0
  60. package/core/knowledge/__pycache__/vector_store.cpython-313.pyc +0 -0
  61. package/core/knowledge/__pycache__/vector_store.cpython-314.pyc +0 -0
  62. package/core/knowledge/embedding_backends.py +278 -0
  63. package/core/knowledge/pattern_cards.py +49 -1
  64. package/core/knowledge/vector_store.py +14 -2
  65. package/core/memory/__pycache__/semantic_store.cpython-312.pyc +0 -0
  66. package/core/memory/__pycache__/semantic_store.cpython-313.pyc +0 -0
  67. package/core/memory/__pycache__/semantic_store.cpython-314.pyc +0 -0
  68. package/core/memory/__pycache__/turn_capture.cpython-313.pyc +0 -0
  69. package/core/memory/__pycache__/turn_capture.cpython-314.pyc +0 -0
  70. package/core/memory/semantic_store.py +301 -0
  71. package/core/memory/turn_capture.py +277 -0
  72. package/core/registry/__pycache__/generator.cpython-312.pyc +0 -0
  73. package/core/runtime/__pycache__/claude_code.cpython-312.pyc +0 -0
  74. package/core/runtime/__pycache__/codex_cli.cpython-312.pyc +0 -0
  75. package/core/runtime/__pycache__/cost_governor.cpython-314.pyc +0 -0
  76. package/core/runtime/__pycache__/gemini_cli.cpython-312.pyc +0 -0
  77. package/core/runtime/__pycache__/llm_cost_telemetry.cpython-312.pyc +0 -0
  78. package/core/runtime/__pycache__/mcp_telemetry.cpython-312.pyc +0 -0
  79. package/core/shared/__pycache__/decay.cpython-313.pyc +0 -0
  80. package/core/shared/__pycache__/decay.cpython-314.pyc +0 -0
  81. package/core/shared/__pycache__/sqlite_recovery.cpython-312.pyc +0 -0
  82. package/core/shared/__pycache__/sqlite_recovery.cpython-313.pyc +0 -0
  83. package/core/shared/__pycache__/sqlite_recovery.cpython-314.pyc +0 -0
  84. package/core/shared/decay.py +94 -0
  85. package/core/shared/sqlite_recovery.py +266 -0
  86. package/core/synapse/__pycache__/agent_experiences_layer.cpython-313.pyc +0 -0
  87. package/core/synapse/__pycache__/agent_experiences_layer.cpython-314.pyc +0 -0
  88. package/core/synapse/__pycache__/engine.cpython-312.pyc +0 -0
  89. package/core/synapse/__pycache__/engine.cpython-313.pyc +0 -0
  90. package/core/synapse/__pycache__/engine.cpython-314.pyc +0 -0
  91. package/core/synapse/__pycache__/layers.cpython-312.pyc +0 -0
  92. package/core/synapse/__pycache__/pattern_library_layer.cpython-314.pyc +0 -0
  93. package/core/synapse/__pycache__/recipe_layer.cpython-313.pyc +0 -0
  94. package/core/synapse/__pycache__/recipe_layer.cpython-314.pyc +0 -0
  95. package/core/synapse/__pycache__/routing_feedback_layer.cpython-313.pyc +0 -0
  96. package/core/synapse/__pycache__/routing_feedback_layer.cpython-314.pyc +0 -0
  97. package/core/synapse/__pycache__/session_memory_layer.cpython-312.pyc +0 -0
  98. package/core/synapse/__pycache__/session_memory_layer.cpython-313.pyc +0 -0
  99. package/core/synapse/__pycache__/session_memory_layer.cpython-314.pyc +0 -0
  100. package/core/synapse/agent_experiences_layer.py +42 -3
  101. package/core/synapse/engine.py +13 -0
  102. package/core/synapse/recipe_layer.py +27 -4
  103. package/core/synapse/routing_feedback_layer.py +132 -0
  104. package/core/synapse/session_memory_layer.py +152 -0
  105. package/core/workflow/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/core/workflow/__pycache__/design_authorization.cpython-314.pyc +0 -0
  107. package/core/workflow/__pycache__/frontend_gate.cpython-314.pyc +0 -0
  108. package/core/workflow/__pycache__/specialist_enforcer.cpython-314.pyc +0 -0
  109. package/installer/adapters/claude-code.js +37 -0
  110. package/installer/claude-plugins.js +6 -0
  111. package/installer/cli.js +23 -2
  112. package/installer/config-seed.js +25 -16
  113. package/installer/doctor.js +116 -1
  114. package/installer/hook-lib.js +28 -0
  115. package/installer/index.js +48 -153
  116. package/installer/mcp-runner.js +108 -0
  117. package/installer/skill-deploy.js +189 -0
  118. package/installer/skills-mode.js +64 -0
  119. package/installer/update.js +55 -79
  120. package/knowledge/commands-registry.json.bak +171 -4
  121. package/knowledge/skills-manifest.json +2337 -0
  122. package/package.json +1 -1
  123. package/pyproject.toml +9 -1
  124. package/scripts/__pycache__/marketplace_gen.cpython-313.pyc +0 -0
  125. package/scripts/__pycache__/synapse-bridge.cpython-314.pyc +0 -0
  126. package/scripts/marketplace_gen.py +310 -0
  127. package/scripts/tools/__pycache__/docs_stats.cpython-313.pyc +0 -0
  128. package/scripts/tools/__pycache__/skill_budget.cpython-313.pyc +0 -0
  129. package/scripts/tools/docs_stats.py +6 -4
  130. package/scripts/tools/skill_budget.py +209 -0
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.13.2
1
+ 4.14.0
package/arka/SKILL.md CHANGED
@@ -220,6 +220,22 @@ Users don't need to memorize commands. Just describe what you need:
220
220
  English prompt, then dispatches back through `/do`. A request that is
221
221
  already specific skips refinement.
222
222
 
223
+ ### Skill Not Installed (curated default, F2-7c)
224
+
225
+ When /do resolves to a skill that is not installed (curated default;
226
+ the long tail lives in the department plugins), the Skill tool fails.
227
+ NEVER answer as a generic assistant or improvise the skill: look the
228
+ slug up in `knowledge/skills-manifest.json` and reply — in the user's
229
+ language — with the actionable install hint:
230
+
231
+ ```
232
+ The <slug> skill is not in the installed curated set.
233
+ Install the department pack: /plugin install arkaos-<dept>@arkaos
234
+ Or restore the full set: npx arkaos update --skills full
235
+ ```
236
+
237
+ Fail loud and actionable, never silently.
238
+
223
239
  ### Squad Routing (MUST)
224
240
 
225
241
  EVERY request routes through the appropriate department squad. ArkaOS never responds
package/bin/arka-doctor CHANGED
@@ -2,7 +2,7 @@
2
2
  # ============================================================================
3
3
  # ARKA OS — Doctor (Health Check System)
4
4
  # Usage: arka doctor [--fix] [--json]
5
- # 17 modular checks: fail = critical, warn = advisory
5
+ # 18 modular checks: fail = critical, warn = advisory
6
6
  # ============================================================================
7
7
  set -euo pipefail
8
8
 
@@ -376,6 +376,25 @@ for hf_name in hyperframes hyperframes-core; do
376
376
  done
377
377
  run_check "hyperframes-skills" "warn" "$HF_DESC" "$HF_OK" "Run /content video-setup in Claude Code"
378
378
 
379
+ # ─── Check 18: SQLite corrupt-store backups (self-heal F1-D1) ──────────────
380
+ # Parity with installer/doctor.js `sqlite-corrupt-backups`: same name,
381
+ # same pattern, warn-only. A <db>.corrupt-<ts>.bak means a store healed
382
+ # itself — the operator should inspect, then delete the backup.
383
+ BAK_OK=true
384
+ BAK_DESC="No self-healed SQLite stores awaiting inspection"
385
+ BAK_LIST=""
386
+ if [ -d "$HOME/.arkaos" ]; then
387
+ # `|| true`: set -euo pipefail must not kill the doctor when find
388
+ # errors or head closes the pipe early — absence of backups is fine.
389
+ BAK_LIST=$(find "$HOME/.arkaos" -name "*.corrupt-*.bak" -type f 2>/dev/null | head -3 || true)
390
+ fi
391
+ if [ -n "$BAK_LIST" ]; then
392
+ BAK_OK=false
393
+ BAK_FIRST=$(echo "$BAK_LIST" | head -1)
394
+ BAK_DESC="Self-healed store backup(s) found (e.g. ${BAK_FIRST##*/})"
395
+ fi
396
+ run_check "sqlite-corrupt-backups" "warn" "$BAK_DESC" "$BAK_OK" "Inspect then delete ~/.arkaos/**/*.corrupt-*.bak"
397
+
379
398
  # ─── Output ────────────────────────────────────────────────────────────────
380
399
  TOTAL=$((PASS_COUNT + WARN_COUNT + FAIL_COUNT))
381
400
 
@@ -20,6 +20,22 @@ schedules:
20
20
  max_retries: 2
21
21
  timeout_minutes: 60
22
22
 
23
+ session-memory-maintenance:
24
+ command: session-memory-maintenance
25
+ # F1-A4 (memory/learning reform): full embedding backfill, retention
26
+ # prune and VACUUM for ~/.arkaos/session-memory.db. The per-turn
27
+ # Stop-hook worker only amortises a small slice of this; the nightly
28
+ # job finishes the backlog. Cheap and idempotent; respects
29
+ # memory.sessionMemory + ARKA_SESSION_MEMORY=0.
30
+ python_module: "core.memory.turn_capture"
31
+ module_args: ["maintenance"]
32
+ time: "03:30"
33
+ timezone: auto
34
+ enabled: true
35
+ retry_on_fail: false
36
+ max_retries: 0
37
+ timeout_minutes: 20
38
+
23
39
  research:
24
40
  command: research
25
41
  prompt_file: "~/.arkaos/cognition/prompts/research.md"
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ /**
3
+ * ArkaOS hook fast-path engine (F2-6) — pure decision logic, no I/O
4
+ * beyond what the caller injects. Consumed by pre-tool-use.cjs and
5
+ * post-tool-use.cjs; exercised directly by
6
+ * tests/installer/gate-manifest.test.js against the SAME corpora the
7
+ * pytest side runs against the real Python functions.
8
+ *
9
+ * Security invariant #0: this engine has NO deny path. Every decision
10
+ * is either {action:"fast-allow"} on a closed, corpus-proven set, or
11
+ * {action:"delegate"} to the bash->Python chain. A bug here degrades
12
+ * to latency, never to a skipped deny.
13
+ *
14
+ * Every constant comes from the injected manifest
15
+ * (config/gate-manifest.json, generated by core/hooks/gate_manifest.py
16
+ * from the real Python gate constants). No constant may be declared in
17
+ * this file — that is exactly the drift the manifest exists to kill.
18
+ */
19
+
20
+ const SCHEMA_VERSION = 1;
21
+
22
+ /** Python truthiness — NOT JS Boolean(): [] and {} are falsy in Python,
23
+ * "false" (non-empty string) is truthy in both. flow_enforcer coerces
24
+ * hooks.hardEnforcement with bool(), so the shim must match. */
25
+ function pythonTruthy(value) {
26
+ if (Array.isArray(value)) return value.length > 0;
27
+ if (value !== null && typeof value === "object") {
28
+ return Object.keys(value).length > 0;
29
+ }
30
+ return Boolean(value);
31
+ }
32
+
33
+ /** datetime.now(timezone.utc).isoformat() shape: microseconds + +00:00.
34
+ * JS has ms precision; pad to 6 digits — still fromisoformat-parseable. */
35
+ function pyIsoNow(now) {
36
+ const d = now || new Date();
37
+ const pad = (n, w) => String(n).padStart(w, "0");
38
+ const micros = pad(d.getUTCMilliseconds(), 3) + "000";
39
+ return (
40
+ `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1, 2)}-` +
41
+ `${pad(d.getUTCDate(), 2)}T${pad(d.getUTCHours(), 2)}:` +
42
+ `${pad(d.getUTCMinutes(), 2)}:${pad(d.getUTCSeconds(), 2)}.` +
43
+ `${micros}+00:00`
44
+ );
45
+ }
46
+
47
+ function validateManifest(manifest) {
48
+ return Boolean(
49
+ manifest &&
50
+ manifest.schema_version === SCHEMA_VERSION &&
51
+ manifest.tools && manifest.bash && manifest.post &&
52
+ manifest.telemetry && manifest.io_contract
53
+ );
54
+ }
55
+
56
+ function compileRegex(entry) {
57
+ return new RegExp(entry.js, entry.flags || "");
58
+ }
59
+
60
+ /** Mirror of flow_enforcer.bash_is_effect: blacklist anywhere first,
61
+ * strip VAR=val prefixes, whitelist first token, unknown → effect. */
62
+ function bashIsEffect(command, manifest) {
63
+ if (!command || !command.trim()) return false;
64
+ const stripped = command.trim();
65
+ // QG B1 (redo 1): JS \s / trim / split treat U+FEFF (and other
66
+ // non-ASCII whitespace) as separators; Python's re \s and str.split
67
+ // do NOT for U+FEFF — a token like "FOO=1<U+FEFF>grep" is ONE unknown
68
+ // token (EFFECT) to Python but splits to a whitelisted "grep" here,
69
+ // a skipped deny under hard enforcement. Any whitespace-class char
70
+ // outside the ASCII set forces EFFECT → the shim delegates and
71
+ // Python classifies with its own semantics (safe: no deny path).
72
+ if (/[^\S \t\n\r\f\v]/.test(stripped)) return true;
73
+ for (const entry of manifest.bash.effect_patterns) {
74
+ if (compileRegex(entry).test(stripped)) return true;
75
+ }
76
+ let rest = stripped;
77
+ let first = rest.split(/\s+/, 1)[0] || "";
78
+ while (first.includes("=")) {
79
+ const idx = rest.search(/\s/);
80
+ if (idx < 0) return false;
81
+ rest = rest.slice(idx).trim();
82
+ first = rest.split(/\s+/, 1)[0] || "";
83
+ if (!rest) return false;
84
+ }
85
+ if (!first) return false;
86
+ if (manifest.bash.discovery_first_tokens.includes(first)) return false;
87
+ return true;
88
+ }
89
+
90
+ /** Strict session-id allowlist (core/shared/safe_session_id.py):
91
+ * charset pattern + dot-only rejection. null means "never build a
92
+ * filesystem path from this id". */
93
+ function safeSessionId(sid, manifest) {
94
+ if (!sid || typeof sid !== "string") return null;
95
+ if (!compileRegex(manifest.session_id).test(sid)) return null;
96
+ if (manifest.session_id.reject_dot_only && /^\.+$/.test(sid)) return null;
97
+ return sid;
98
+ }
99
+
100
+ function readJsonFile(fs, filePath) {
101
+ // {state: "missing" | "corrupt" | "ok", data}
102
+ let raw;
103
+ try {
104
+ raw = fs.readFileSync(filePath, "utf8");
105
+ } catch {
106
+ return { state: "missing", data: null };
107
+ }
108
+ try {
109
+ const data = JSON.parse(raw);
110
+ return { state: "ok", data };
111
+ } catch {
112
+ return { state: "corrupt", data: null };
113
+ }
114
+ }
115
+
116
+ /** cost_governor semantics: budget active iff the section is a dict and
117
+ * any cap key parses as float > 0. Missing/corrupt config → {} → no cap
118
+ * (cost_governor.py:68-86). */
119
+ function budgetActive(configResult, manifest) {
120
+ if (configResult.state !== "ok") return false;
121
+ const data = configResult.data;
122
+ const section =
123
+ data && typeof data === "object" && !Array.isArray(data)
124
+ ? data[manifest.flags.budget.section]
125
+ : null;
126
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
127
+ return false;
128
+ }
129
+ for (const key of manifest.flags.budget.cap_keys) {
130
+ const value = Number(section[key]);
131
+ if (Number.isFinite(value) && value > 0) return true;
132
+ }
133
+ return false;
134
+ }
135
+
136
+ /** flow_enforcer._feature_flag_on: missing file → false, corrupt →
137
+ * false, else python-truthy of hooks.hardEnforcement. */
138
+ function hardEnforcementOn(configResult, manifest) {
139
+ if (configResult.state !== "ok") return false;
140
+ const [section, key] = manifest.flags.hardEnforcement.path;
141
+ const data = configResult.data;
142
+ const hooks =
143
+ data && typeof data === "object" && !Array.isArray(data)
144
+ ? data[section]
145
+ : null;
146
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
147
+ return false;
148
+ }
149
+ return pythonTruthy(hooks[key]);
150
+ }
151
+
152
+ function telemetryLine(prefix, template, now) {
153
+ return JSON.stringify({ ...prefix, ...template }) + "\n";
154
+ }
155
+
156
+ /**
157
+ * PreToolUse decision. Returns
158
+ * {action:"fast-allow", writes:[{path, line}]} — exit 0, empty stdout
159
+ * {action:"delegate", reason} — spawn the sibling .sh
160
+ * ctx: {homeDir, config: readJsonFile result for ~/.arkaos/config.json,
161
+ * now?: Date}
162
+ */
163
+ function decidePre(payload, manifest, ctx) {
164
+ if (!validateManifest(manifest)) {
165
+ return { action: "delegate", reason: "manifest-invalid" };
166
+ }
167
+ const toolName = String(payload.tool_name || "");
168
+ const tools = manifest.tools;
169
+
170
+ // P3 — research tools can deny (2nd violation) + vault rglob nudge.
171
+ if (tools.research_external.includes(toolName)) {
172
+ return { action: "delegate", reason: "research-gated" };
173
+ }
174
+
175
+ // P4 — not flow-gated: no Python gate ever denies these; the only
176
+ // parity duty is the kb_first telemetry append (research_gate
177
+ // record_telemetry runs for every tool).
178
+ if (!tools.flow_gated.includes(toolName)) {
179
+ return {
180
+ action: "fast-allow",
181
+ writes: [kbFirstWrite(payload, manifest, ctx, toolName)],
182
+ };
183
+ }
184
+
185
+ // P5/P6/P7 — Bash: discovery commands are allow-with-telemetry before
186
+ // any flag/bypass check (flow_enforcer.py:456-463); budget must be
187
+ // provably inactive because cost_governor runs first for flow-gated
188
+ // tools and can warn/deny.
189
+ if (toolName === "Bash") {
190
+ const command = String(
191
+ (payload.tool_input && payload.tool_input.command) || ""
192
+ );
193
+ if (!bashIsEffect(command, manifest) && !budgetActive(ctx.config, manifest)) {
194
+ return {
195
+ action: "fast-allow",
196
+ writes: [
197
+ kbFirstWrite(payload, manifest, ctx, toolName),
198
+ enforcementWrite(payload, manifest, ctx, toolName),
199
+ ],
200
+ };
201
+ }
202
+ return { action: "delegate", reason: "bash-effect-or-budget" };
203
+ }
204
+
205
+ // P7/P8 — Write/Edit/MultiEdit/NotebookEdit/Task/Skill: specialist +
206
+ // frontend + flow chains with decision-bearing state writes.
207
+ return { action: "delegate", reason: "flow-gated" };
208
+ }
209
+
210
+ function kbFirstWrite(payload, manifest, ctx, toolName) {
211
+ const t = manifest.telemetry;
212
+ return {
213
+ path: `${ctx.homeDir}/${manifest.home_paths.telemetry_kb_first}`,
214
+ line: telemetryLine(
215
+ {
216
+ ts: pyIsoNow(ctx.now),
217
+ session_id: String(payload.session_id || ""),
218
+ tool: toolName,
219
+ },
220
+ t.kb_first_template
221
+ ),
222
+ };
223
+ }
224
+
225
+ function enforcementWrite(payload, manifest, ctx, toolName) {
226
+ const t = manifest.telemetry;
227
+ return {
228
+ path: `${ctx.homeDir}/${manifest.home_paths.telemetry_enforcement}`,
229
+ line: telemetryLine(
230
+ {
231
+ ts: pyIsoNow(ctx.now),
232
+ session_id: String(payload.session_id || ""),
233
+ tool: toolName,
234
+ cwd: String(payload.cwd || ""),
235
+ },
236
+ t.enforcement_template
237
+ ),
238
+ };
239
+ }
240
+
241
+ /** Flow-auth freshness (Q6): auth file exists, valid JSON, numeric
242
+ * confirmed_ts within TTL. Anything else → not fresh → delegate. */
243
+ function flowAuthFresh(sid, manifest, ctx) {
244
+ const safe = safeSessionId(sid, manifest);
245
+ if (!safe) return false;
246
+ const dirs = manifest.state_dirs;
247
+ const override = (ctx.env[dirs.flow_auth_env_override] || "").trim();
248
+ const base = override ||
249
+ `${dirs.tmp_base_posix}/${dirs.flow_auth_dirname}`;
250
+ const result = readJsonFile(ctx.fs, `${base}/${safe}.json`);
251
+ if (result.state !== "ok" || !result.data) return false;
252
+ const ts = result.data.confirmed_ts;
253
+ if (typeof ts !== "number" || !Number.isFinite(ts)) return false;
254
+ const nowSeconds = (ctx.now ? ctx.now.getTime() : Date.now()) / 1000;
255
+ return nowSeconds - ts <= manifest.numbers.auth_ttl_seconds;
256
+ }
257
+
258
+ /**
259
+ * PostToolUse decision. Returns
260
+ * {action:"fast-exit", stdout:"{}", writes:[...]} — mcp telemetry only
261
+ * {action:"delegate", reason}
262
+ * ctx: {homeDir, config, env, fs, now?: Date}
263
+ */
264
+ function decidePost(payload, manifest, ctx) {
265
+ if (!validateManifest(manifest)) {
266
+ return { action: "delegate", reason: "manifest-invalid" };
267
+ }
268
+ const toolName = String(payload.tool_name || "");
269
+
270
+ // Q3/Q4 — plan approval (G2) + subagent tracking live in Python.
271
+ if (manifest.tools.post_delegate_always.includes(toolName)) {
272
+ return { action: "delegate", reason: "post-stateful-tool" };
273
+ }
274
+
275
+ // Q5 — error path: gotchas, violations, cognition capture, metrics.
276
+ const exitCode = String(payload.exit_code ?? "") || "0";
277
+ if (!manifest.post.success_exit_codes.includes(exitCode)) {
278
+ return { action: "delegate", reason: "error-exit-code" };
279
+ }
280
+ const toolOutput = String(payload.tool_output ?? "");
281
+ if (compileRegex(manifest.post.error_trigger).test(toolOutput)) {
282
+ return { action: "delegate", reason: "error-trigger" };
283
+ }
284
+
285
+ // Q6/Q7 — the only remaining Python work on this path is the flow-auth
286
+ // confirm rescan. Redundant when enforcement is off (the pre-side flag
287
+ // check short-circuits before ever reading auth) or when a confirmed
288
+ // auth is fresh; load-bearing otherwise (GRACE_CAP exhaustion) →
289
+ // delegate.
290
+ const sid = String(payload.session_id || "");
291
+ const enforcementOff =
292
+ ctx.config.state !== "missing"
293
+ ? !hardEnforcementOn(ctx.config, manifest)
294
+ : true; // missing config file → flag definitively false
295
+ if (!enforcementOff && !flowAuthFresh(sid, manifest, ctx)) {
296
+ return { action: "delegate", reason: "flow-auth-not-fresh" };
297
+ }
298
+
299
+ const writes = [];
300
+ const prefix = manifest.telemetry.mcp_prefix;
301
+ if (toolName.startsWith(prefix)) {
302
+ const rest = toolName.slice(prefix.length);
303
+ const sep = rest.indexOf("__");
304
+ const server = sep > 0 ? rest.slice(0, sep) : "";
305
+ const tool = sep > 0 ? rest.slice(sep + 2) : "";
306
+ if (server && tool) {
307
+ writes.push({
308
+ path: `${ctx.homeDir}/${manifest.home_paths.telemetry_mcp}`,
309
+ line: JSON.stringify({
310
+ ts: pyIsoNow(ctx.now),
311
+ server,
312
+ tool,
313
+ session: sid,
314
+ }) + "\n",
315
+ });
316
+ }
317
+ }
318
+ return {
319
+ action: "fast-exit",
320
+ stdout: manifest.io_contract.post.success_stdout,
321
+ writes,
322
+ };
323
+ }
324
+
325
+ module.exports = {
326
+ bashIsEffect,
327
+ budgetActive,
328
+ decidePost,
329
+ decidePre,
330
+ flowAuthFresh,
331
+ hardEnforcementOn,
332
+ pyIsoNow,
333
+ pythonTruthy,
334
+ readJsonFile,
335
+ safeSessionId,
336
+ validateManifest,
337
+ };