arkaos 4.38.0 → 4.40.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 (70) hide show
  1. package/THE-ARKAOS-GUIDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/bin/arka-doctor +3 -2
  4. package/config/hooks/_lib/fastpath/engine.cjs +44 -7
  5. package/config/hooks/cwd-changed.ps1 +11 -5
  6. package/config/hooks/cwd-changed.sh +11 -4
  7. package/config/hooks/gate-manifest.json +2 -6
  8. package/config/hooks/post-tool-use.ps1 +42 -7
  9. package/config/hooks/session-start.sh +25 -1
  10. package/config/hooks/user-prompt-submit.ps1 +9 -7
  11. package/config/hooks/user-prompt-submit.sh +3 -2
  12. package/config/settings-template.json +12 -1
  13. package/core/governance/evidence_checks.py +33 -3
  14. package/core/governance/reviewer_ledger.py +708 -0
  15. package/core/hooks/_shared.py +20 -0
  16. package/core/hooks/gate_manifest.py +14 -6
  17. package/core/hooks/post_tool_use.py +164 -50
  18. package/core/hooks/session_end.py +16 -0
  19. package/core/hooks/session_start.py +62 -13
  20. package/core/hooks/stop.py +38 -0
  21. package/core/hooks/subagent_stop.py +158 -17
  22. package/core/hooks/user_prompt_submit.py +230 -81
  23. package/core/runtime/opencode.py +2 -2
  24. package/core/synapse/engine.py +2 -2
  25. package/dashboard/nuxt.config.ts +6 -0
  26. package/departments/ops/skills/hookify/SKILL.md +7 -4
  27. package/harness/codex/AGENTS.md +1 -1
  28. package/harness/copilot/copilot-instructions.md +1 -1
  29. package/harness/cursor/rules/arkaos.mdc +2 -2
  30. package/harness/gemini/GEMINI.md +1 -1
  31. package/harness/opencode/AGENTS.md +1 -1
  32. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  33. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  34. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  35. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  36. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  37. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  38. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  39. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  40. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  41. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  42. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  43. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  44. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  45. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  46. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  47. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  48. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  49. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  50. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  51. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  52. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  53. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  54. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  55. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  56. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  57. package/harness/zed/.rules +1 -1
  58. package/installer/adapters/claude-code.js +14 -2
  59. package/installer/doctor.js +21 -0
  60. package/knowledge/commands-registry.json.bak +705 -98
  61. package/knowledge/skills-manifest.json +1 -1
  62. package/mcps/arka-tools/pyproject.toml +1 -1
  63. package/package.json +1 -1
  64. package/pyproject.toml +5 -2
  65. package/scripts/start-dashboard.ps1 +42 -2
  66. package/scripts/synapse-bridge.py +1 -1
  67. package/scripts/tools/prompt_surface_benchmark.py +8 -6
  68. package/config/hooks/post-tool-use-v2.sh +0 -120
  69. package/config/hooks/pre-compact-v2.sh +0 -43
  70. package/config/hooks/user-prompt-submit-v2.sh +0 -159
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v4.38.0 — 89 agents, 17 departments, 332 skills, 297 commands, 17 ADRs.
3
+ > v4.40.0 — 89 agents, 17 departments, 332 skills, 297 commands, 17 ADRs.
4
4
  > One file, everything you need to start. Generated by `scripts/guide_gen.py` — never hand-edited.
5
5
 
6
6
  ## What it is
package/VERSION CHANGED
@@ -1 +1 @@
1
- 4.38.0
1
+ 4.40.0
package/bin/arka-doctor CHANGED
@@ -162,9 +162,10 @@ if [ "$FIX_MODE" = true ] && [ "$HOOKS_OK" = false ]; then
162
162
  --arg pc "$HOOKS_DIR/pre-compact.sh" \
163
163
  --arg ptu "$HOOKS_DIR/post-tool-use.sh" \
164
164
  '{
165
- "UserPromptSubmit": [{"hooks":[{"type":"command","command":$ups,"timeout":10}]}],
165
+ "UserPromptSubmit": [{"hooks":[{"type":"command","command":$ups,"timeout":20}]}],
166
166
  "PreCompact": [{"hooks":[{"type":"command","command":$pc,"timeout":30}]}],
167
- "PostToolUse": [{"hooks":[{"type":"command","command":$ptu,"timeout":5}]}]
167
+ "PostToolUse": [{"hooks":[{"type":"command","command":$ptu,"timeout":5}]}],
168
+ "PostToolUseFailure": [{"hooks":[{"type":"command","command":$ptu,"timeout":5}]}]
168
169
  }')
169
170
  jq --argjson hooks "$HOOKS_JSON" '.hooks = $hooks' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && \
170
171
  mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
@@ -17,7 +17,10 @@
17
17
  * this file — that is exactly the drift the manifest exists to kill.
18
18
  */
19
19
 
20
- const SCHEMA_VERSION = 1;
20
+ // v2: post.success_exit_codes removed with the PostToolUseFailure
21
+ // wiring — an old engine + new manifest mismatch now delegates via
22
+ // validateManifest instead of crashing into the shim's fail-open {}.
23
+ const SCHEMA_VERSION = 2;
21
24
 
22
25
  /** Python truthiness — NOT JS Boolean(): [] and {} are falsy in Python,
23
26
  * "false" (non-empty string) is truthy in both. flow_enforcer coerces
@@ -255,6 +258,34 @@ function flowAuthFresh(sid, manifest, ctx) {
255
258
  return nowSeconds - ts <= manifest.numbers.auth_ttl_seconds;
256
259
  }
257
260
 
261
+ /** Textual output of a PostToolUse tool_response — parity with the
262
+ * PostToolUse paths of _tool_text in post_tool_use.py (string,
263
+ * stdout/stderr and Task/content shapes, isAsync guard and Python's
264
+ * `or ""` falsy coercion included). Failure-event payloads never
265
+ * reach this — decidePost delegates PostToolUseFailure outright. */
266
+ function toolText(payload) {
267
+ const response = payload.tool_response;
268
+ if (typeof response === "string") return response;
269
+ if (response && typeof response === "object") {
270
+ if ("stdout" in response || "stderr" in response) {
271
+ return [response.stdout, response.stderr]
272
+ .map((part) => String(part || ""))
273
+ .filter(Boolean)
274
+ .join("\n");
275
+ }
276
+ if (response.isAsync) return "";
277
+ const content = response.content;
278
+ if (typeof content === "string") return content;
279
+ if (Array.isArray(content)) {
280
+ return content
281
+ .filter((block) => block && block.type === "text")
282
+ .map((block) => String(block.text || ""))
283
+ .join("");
284
+ }
285
+ }
286
+ return "";
287
+ }
288
+
258
289
  /**
259
290
  * PostToolUse decision. Returns
260
291
  * {action:"fast-exit", stdout:"{}", writes:[...]} — mcp telemetry only
@@ -267,18 +298,23 @@ function decidePost(payload, manifest, ctx) {
267
298
  }
268
299
  const toolName = String(payload.tool_name || "");
269
300
 
301
+ // A failing tool throws inside the runtime and fires
302
+ // PostToolUseFailure (2.1.220 contract) — failed-command turns are
303
+ // the gotchas pipeline's primary material, so they always go to
304
+ // Python. Rare by construction: no fast path is worth having here.
305
+ if (payload.hook_event_name === "PostToolUseFailure") {
306
+ return { action: "delegate", reason: "failure-event" };
307
+ }
308
+
270
309
  // Q3/Q4 — plan approval (G2) + subagent tracking live in Python.
271
310
  if (manifest.tools.post_delegate_always.includes(toolName)) {
272
311
  return { action: "delegate", reason: "post-stateful-tool" };
273
312
  }
274
313
 
275
314
  // 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)) {
315
+ // PostToolUse never carries an exit code (failures fire the event
316
+ // above), so the only error signal on this path is trigger text.
317
+ if (compileRegex(manifest.post.error_trigger).test(toolText(payload))) {
282
318
  return { action: "delegate", reason: "error-trigger" };
283
319
  }
284
320
 
@@ -333,5 +369,6 @@ module.exports = {
333
369
  pythonTruthy,
334
370
  readJsonFile,
335
371
  safeSessionId,
372
+ toolText,
336
373
  validateManifest,
337
374
  };
@@ -2,13 +2,17 @@
2
2
  # ArkaOS — CwdChanged Hook (Windows / PowerShell 5.1+)
3
3
  #
4
4
  # Port of config/hooks/cwd-changed.sh. Fires when the working directory
5
- # changes. Detects ecosystem and stack so Claude knows which squad and
6
- # tooling apply to the project.
5
+ # changes. Detects the ecosystem/stack and surfaces them to the operator
6
+ # as a system message.
7
7
  #
8
8
  # Contract:
9
9
  # - Reads a JSON object on stdin with a `cwd` field.
10
- # - Emits `{"additionalContext": "..."}` on stdout when context is found,
11
- # or nothing (and exit 0) when there is nothing to say.
10
+ # - Emits `{"systemMessage": "..."}` on stdout when context is found, or
11
+ # nothing (and exit 0) when there is nothing to say. CwdChanged's
12
+ # hookSpecificOutput accepts watchPaths only (Claude Code 2.1.220) —
13
+ # additionalContext is unreachable on this event. On later prompts the
14
+ # cwd-reading Synapse layers refresh (git branch, graph context,
15
+ # knowledge retrieval, cwd-scoped session memory).
12
16
  # ============================================================================
13
17
 
14
18
  $ErrorActionPreference = 'Stop'
@@ -148,5 +152,7 @@ if ($descriptor) {
148
152
  }
149
153
 
150
154
  if ($context) {
151
- [pscustomobject]@{ additionalContext = $context } | ConvertTo-Json -Compress
155
+ # CwdChanged accepts no additionalContext (watchPaths only) — the
156
+ # top-level systemMessage is the one deliverable surface on this event.
157
+ [pscustomobject]@{ systemMessage = $context } | ConvertTo-Json -Compress
152
158
  }
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # ============================================================================
3
3
  # ArkaOS — CwdChanged Hook
4
- # Fires when the working directory changes. Detects ecosystem and injects
5
- # project context so Claude knows which squad and stack to use.
4
+ # Fires when the working directory changes. Detects the ecosystem/stack
5
+ # and surfaces a systemMessage naming them (the one deliverable surface
6
+ # on this event — CwdChanged's hookSpecificOutput accepts watchPaths
7
+ # only).
6
8
  # ============================================================================
7
9
 
8
10
  input=$(cat)
@@ -116,7 +118,12 @@ if [ -n "$DESCRIPTOR" ]; then
116
118
  fi
117
119
 
118
120
  if [ -n "$CONTEXT" ]; then
119
- # Build the JSON with jq so any quote/backslash in the ecosystem name or
121
+ # CwdChanged's hookSpecificOutput accepts watchPaths ONLY (Claude Code
122
+ # 2.1.220 schema) — additionalContext is unreachable on this event. The
123
+ # top-level systemMessage key IS consumed and surfaced to the operator.
124
+ # On later prompts the cwd-reading Synapse layers refresh (git branch,
125
+ # graph context, knowledge retrieval, cwd-scoped session memory).
126
+ # Built with jq so any quote/backslash in the ecosystem name or
120
127
  # descriptor path is escaped, never breaking the envelope (OWASP A03).
121
- jq -nc --arg ctx "$CONTEXT" '{additionalContext: $ctx}'
128
+ jq -nc --arg msg "$CONTEXT" '{systemMessage: $msg}'
122
129
  fi
@@ -635,13 +635,9 @@
635
635
  "flags": "i",
636
636
  "js": "(error:|fatal:|exception:|failed|ENOENT|EACCES|EPERM|panic:)",
637
637
  "py": "(error:|fatal:|exception:|failed|ENOENT|EACCES|EPERM|panic:)"
638
- },
639
- "success_exit_codes": [
640
- "0",
641
- ""
642
- ]
638
+ }
643
639
  },
644
- "schema_version": 1,
640
+ "schema_version": 2,
645
641
  "session_id": {
646
642
  "flags": "",
647
643
  "js": "^[A-Za-z0-9._-]{1,128}$",
@@ -6,8 +6,16 @@
6
6
  # which mistakes repeat across projects.
7
7
  #
8
8
  # Contract:
9
- # - Reads a hook payload JSON from stdin (tool_name, tool_output, exit_code,
10
- # cwd).
9
+ # - Reads a hook payload JSON from stdin. Registered for two events
10
+ # (2.1.220 contract): PostToolUse delivers a structured tool_response
11
+ # (Bash dicts carry stdout/stderr; this port handles the string and
12
+ # stdout/stderr shapes only — no Task/content path); a failing tool
13
+ # throws inside the runtime and fires PostToolUseFailure instead,
14
+ # which carries the output in `error`, prefixed "Exit code N" for
15
+ # Bash. The payload has no top-level output or exit-code fields.
16
+ # Also read: tool_name, cwd, session_id, and a legacy
17
+ # assistant_message the payload never carries (dead read, see
18
+ # core/hooks/post_tool_use.py `_confirm_flow_authorization`).
11
19
  # - Always exits 0 and writes `{}` on stdout — PostToolUse does not inject
12
20
  # context.
13
21
  # - Side effect: updates %USERPROFILE%\.arkaos\gotchas.json and
@@ -100,7 +108,7 @@ $stdinText = [Console]::In.ReadToEnd()
100
108
  $shouldProcessGotchas = $true
101
109
  $payload = $null
102
110
  $toolName = ''
103
- $toolOutput = ''
111
+ $toolText = ''
104
112
  $exitCode = '0'
105
113
  $cwd = ''
106
114
 
@@ -116,10 +124,37 @@ if ([string]::IsNullOrWhiteSpace($stdinText)) {
116
124
 
117
125
  if ($shouldProcessGotchas -and $null -ne $payload) {
118
126
  $toolName = if ($null -ne $payload.tool_name) { [string]$payload.tool_name } else { '' }
119
- $toolOutput = if ($null -ne $payload.tool_output) { [string]$payload.tool_output } else { '' }
120
- $exitCode = if ($null -ne $payload.exit_code) { [string]$payload.exit_code } else { '0' }
121
127
  $cwd = if ($null -ne $payload.cwd) { [string]$payload.cwd } else { '' }
122
128
 
129
+ # Event → (text, exit code). Mirrors the string and stdout/stderr
130
+ # paths of _tool_text / _tool_exit_code in core/hooks/post_tool_use.py
131
+ # (no Task/content path in this port). On PostToolUseFailure the
132
+ # output arrives in `error`; the runtime's "Exit code N" envelope is
133
+ # its wrapper, not command output, so it is stripped from the text
134
+ # and surfaced as the exit code. A user interrupt is not a failure.
135
+ $eventName = if ($null -ne $payload.hook_event_name) { [string]$payload.hook_event_name } else { '' }
136
+ if ($eventName -eq 'PostToolUseFailure') {
137
+ $errText = if ($null -ne $payload.error) { [string]$payload.error } else { '' }
138
+ $envMatch = [regex]::Match($errText, '^Exit code (\d+)\n?')
139
+ if ($envMatch.Success) {
140
+ $exitCode = $envMatch.Groups[1].Value
141
+ $toolText = $errText.Substring($envMatch.Length)
142
+ } else {
143
+ $exitCode = '1'
144
+ $toolText = $errText
145
+ }
146
+ if ($payload.is_interrupt) { $exitCode = '0' }
147
+ } else {
148
+ $resp = $payload.tool_response
149
+ if ($resp -is [string]) {
150
+ $toolText = $resp
151
+ } elseif ($null -ne $resp) {
152
+ $stdoutPart = if ($null -ne $resp.stdout) { [string]$resp.stdout } else { '' }
153
+ $stderrPart = if ($null -ne $resp.stderr) { [string]$resp.stderr } else { '' }
154
+ $toolText = (@($stdoutPart, $stderrPart) | Where-Object { $_ }) -join "`n"
155
+ }
156
+ }
157
+
123
158
  # --- Flow marker cache write (v2 ALLOW accelerator) ------------------
124
159
  # Mirror of the bash hook: detect [arka:routing] or [arka:trivial] in
125
160
  # $payload.assistant_message and persist via core.workflow.marker_cache.
@@ -211,7 +246,7 @@ except Exception:
211
246
  # ─── Only process when there is actually an error ─────────────────
212
247
  $errorPattern = '(?i)(error:|fatal:|exception:|failed|ENOENT|EACCES|EPERM|panic:)'
213
248
  if ($exitCode -eq '0' -or [string]::IsNullOrEmpty($exitCode)) {
214
- if ($toolOutput -notmatch $errorPattern) {
249
+ if ($toolText -notmatch $errorPattern) {
215
250
  $shouldProcessGotchas = $false
216
251
  }
217
252
  }
@@ -230,7 +265,7 @@ $null = New-Item -ItemType Directory -Force -Path $arkaosRuntimeDir -ErrorAction
230
265
  if ($shouldProcessGotchas) { do {
231
266
 
232
267
  # ─── Extract first meaningful error line ─────────────────────────────
233
- $lines = $toolOutput -split "`r?`n"
268
+ $lines = $toolText -split "`r?`n"
234
269
  $errorLineRegex = '(?i)(error|fatal|exception|failed|ENOENT|EACCES|EPERM|panic|cannot|not found|permission denied)'
235
270
  $errorLine = $null
236
271
  foreach ($ln in $lines) {
@@ -18,8 +18,32 @@ export ARKA_HOOK_CWD="$_HOOK_CWD"
18
18
  _ARKA_LIB="$(dirname "${BASH_SOURCE[0]:-$0}")/_lib/arka_python.sh"
19
19
  if [ -f "$_ARKA_LIB" ]; then . "$_ARKA_LIB"; else ARKA_PY="python3"; fi
20
20
 
21
+ # ─── Resolve the root via the shared resolver (one root per session) ───
22
+ # Reading .repo-path raw diverged from every other hook: the file points
23
+ # at an npx cache that `npm cache clean` can purge, while
24
+ # arka_resolve_root falls through to the ~/.arkaos/lib snapshot. The
25
+ # chosen root AND source both travel to the entrypoint so the session
26
+ # can name the root it actually ran from ([arka:root]).
21
27
  REPO=""
22
- [ -f "$HOME/.arkaos/.repo-path" ] && REPO=$(cat "$HOME/.arkaos/.repo-path")
28
+ ARKA_ROOT_SOURCE="legacy"
29
+ if command -v arka_resolve_root >/dev/null 2>&1; then
30
+ [ -n "${ARKAOS_ROOT:-}" ] && ARKA_ROOT_SOURCE="env"
31
+ REPO="$(arka_resolve_root)"
32
+ if [ "$ARKA_ROOT_SOURCE" != "env" ]; then
33
+ if [ -f "$HOME/.arkaos/.repo-path" ] \
34
+ && [ "$REPO" = "$(cat "$HOME/.arkaos/.repo-path" 2>/dev/null)" ]; then
35
+ ARKA_ROOT_SOURCE="repo-path"
36
+ elif [ "$REPO" = "$HOME/.arkaos/lib" ]; then
37
+ ARKA_ROOT_SOURCE="lib-snapshot"
38
+ else
39
+ ARKA_ROOT_SOURCE="fallback"
40
+ fi
41
+ fi
42
+ elif [ -f "$HOME/.arkaos/.repo-path" ]; then
43
+ REPO=$(cat "$HOME/.arkaos/.repo-path")
44
+ fi
45
+ export ARKA_ROOT_SOURCE
46
+ export ARKA_ROOT="$REPO"
23
47
 
24
48
  # The MODULE file is the guard, not just core/: an older installed
25
49
  # snapshot without it would exec into "No module named ..." (exit 1,
@@ -1,17 +1,19 @@
1
1
  # ============================================================================
2
2
  # ArkaOS v2 - UserPromptSubmit Hook (Synapse Bridge) (Windows / PowerShell 5.1+)
3
3
  #
4
- # Port of config/hooks/user-prompt-submit.sh. Calls the Python Synapse bridge
5
- # for 8-layer context injection, with a pure-PowerShell fallback for
6
- # installations where Python is unavailable or the bridge is missing.
4
+ # Port of config/hooks/user-prompt-submit.sh. Calls the Python Synapse
5
+ # bridge for layered context injection, with a pure-PowerShell fallback
6
+ # for installations where Python is unavailable or the bridge is missing.
7
7
  #
8
8
  # Contract:
9
9
  # - Reads a JSON payload from stdin (userInput / message / raw).
10
10
  # - Emits a single-line JSON object on stdout:
11
- # {"additionalContext": "<sync-notice><synapse-or-fallback>"}
11
+ # {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit",
12
+ # "additionalContext": "<sync-notice><route-reminder> <synapse-or-fallback>"}}
13
+ # (a top-level additionalContext key is ignored by Claude Code)
12
14
  # - Side effect: writes one line to the cache metrics JSONL.
13
15
  #
14
- # Target latency: <100ms (hook timeout is 10s in settings).
16
+ # Target latency: <100ms (hook timeout is 20s in settings).
15
17
  #
16
18
  # File is pure ASCII. Any typographic characters that need to appear in the
17
19
  # output are built from [char] codes at runtime so PS 5.1's default ANSI
@@ -54,7 +56,7 @@ if (-not $v2Installed) {
54
56
  foreach ($v1 in $v1Paths) {
55
57
  if ((Test-Path -LiteralPath $v1 -PathType Container) -and -not (Test-Path -LiteralPath $migrationMarker)) {
56
58
  $msg = "[MIGRATION] ArkaOS v1 detected at $v1. Run: npx arkaos migrate - This will backup v1, preserve your data, and install v2. See: https://github.com/andreagroferreira/arka-os#install"
57
- [pscustomobject]@{ additionalContext = $msg } | ConvertTo-Json -Compress
59
+ [pscustomobject]@{ hookSpecificOutput = [pscustomobject]@{ hookEventName = 'UserPromptSubmit'; additionalContext = $msg } } | ConvertTo-Json -Compress -Depth 4
58
60
  exit 0
59
61
  }
60
62
  }
@@ -336,7 +338,7 @@ $routeReminder = '[arka:route] Every response MUST route through a department sq
336
338
 
337
339
  # --- Output ----------------------------------------------------------------
338
340
  $additionalContext = "$syncNotice$routeReminder $pythonResult"
339
- [pscustomobject]@{ additionalContext = $additionalContext } | ConvertTo-Json -Compress
341
+ [pscustomobject]@{ hookSpecificOutput = [pscustomobject]@{ hookEventName = 'UserPromptSubmit'; additionalContext = $additionalContext } } | ConvertTo-Json -Compress -Depth 4
340
342
 
341
343
  # --- Metrics (JSONL append) ------------------------------------------------
342
344
  $elapsed = [int]$sw.ElapsedMilliseconds
@@ -20,7 +20,8 @@
20
20
  # - One-shot nudges (KB-cite, meta-tag, closing-marker) gated by
21
21
  # effort level; cognitive context injection.
22
22
  #
23
- # Timeout: 10s | Output: JSON to stdout | Target: <100ms
23
+ # Timeout: 20 s (in-hook budget ARKA_UPS_BUDGET_MS, default 6000 ms)
24
+ # Output: JSON to stdout | Target: <100ms
24
25
  # ============================================================================
25
26
 
26
27
  # ─── Shared Python resolver (exports ARKA_PY) ──────────────────────────
@@ -44,7 +45,7 @@ elif [ -z "${ARKAOS_ROOT:-}" ]; then
44
45
  fi
45
46
  export ARKAOS_ROOT
46
47
 
47
- _ARKA_L0_FALLBACK='{"additionalContext": "[Constitution] NON-NEGOTIABLE: branch-isolation, security-gate, mandatory-qa, evidence-flow, arkaos-not-yes-man, excellence-mandate | QUALITY-GATE: marta-cqo, eduardo-copy, francisca-tech-ux | MUST (28) incl.: squad-routing, spec-driven, conventional-commits, test-coverage, subagent-discipline, persona-vs-artifact"}'
48
+ _ARKA_L0_FALLBACK='{"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "[Constitution] NON-NEGOTIABLE: branch-isolation, security-gate, mandatory-qa, evidence-flow, arkaos-not-yes-man, excellence-mandate | QUALITY-GATE: marta-cqo, eduardo-copy, francisca-tech-ux | MUST (28) incl.: squad-routing, spec-driven, conventional-commits, test-coverage, subagent-discipline, persona-vs-artifact"}}'
48
49
 
49
50
  # Test seam: bats asserts the fallback CONTENT deterministically —
50
51
  # the shared resolver overwrites ARKA_PY, so an env flag is the only
@@ -25,7 +25,7 @@
25
25
  {
26
26
  "type": "command",
27
27
  "command": "{{HOOKS_DIR}}/user-prompt-submit.sh",
28
- "timeout": 10
28
+ "timeout": 20
29
29
  }
30
30
  ]
31
31
  }
@@ -52,6 +52,17 @@
52
52
  ]
53
53
  }
54
54
  ],
55
+ "PostToolUseFailure": [
56
+ {
57
+ "hooks": [
58
+ {
59
+ "type": "command",
60
+ "command": "{{HOOKS_DIR}}/post-tool-use.sh",
61
+ "timeout": 5
62
+ }
63
+ ]
64
+ }
65
+ ],
55
66
  "CwdChanged": [
56
67
  {
57
68
  "hooks": [
@@ -390,24 +390,54 @@ def _foreign_pytest_can_collect(
390
390
  return probe.returncode in (0, 5)
391
391
 
392
392
 
393
+ def _degrade_pytest_no_tests(result: CheckResult) -> CheckResult:
394
+ """Map pytest exit 5 (no tests collected) to insufficient evidence.
395
+
396
+ A bare ``pytest`` in a Python project that ships no tests exits 5.
397
+ Absence of tests is not evidence of FAILURE, yet ``passed =
398
+ returncode == 0`` read exit 5 as FAIL and ``_derive_overall`` then
399
+ forced REJECTED at the QG (issue #354). ``_foreign_pytest_can_collect``
400
+ already treats exit 5 as a valid import path — this keeps that
401
+ precedent for the verdict: exit 5 becomes ``passed=None``
402
+ (non-conclusive), never ``False``.
403
+ """
404
+ if result.exit_code != 5:
405
+ return result
406
+ prefix = "no tests collected (pytest exit 5) — insufficient evidence; "
407
+ return replace(result, passed=None, summary=prefix + result.summary)
408
+
409
+
393
410
  def _check_tests(
394
411
  project_dir: Path, changed: list[str] | None,
395
412
  test_command: str | None, timeout: int,
396
413
  ) -> CheckResult:
397
414
  if test_command:
398
- return _run("tests", shlex.split(test_command), project_dir, timeout)
415
+ argv = shlex.split(test_command)
416
+ result = _run("tests", argv, project_dir, timeout)
417
+ # exit 5 is pytest's "no tests collected"; scan the first 3
418
+ # tokens so `python -m pytest` / `arka-py -m pytest` degrade too,
419
+ # while non-pytest runners (npm test) stay a real FAIL. Bounded to
420
+ # argv[:3] so a later test-path arg named *pytest* never matches
421
+ # (issue #354).
422
+ if any("pytest" in Path(tok).name for tok in argv[:3]):
423
+ return _degrade_pytest_no_tests(result)
424
+ return result
399
425
  if _has_python(project_dir, changed):
400
426
  local_pytest = _project_pytest(project_dir)
401
427
  if local_pytest:
402
428
  return _labelled(
403
- _run("tests", [*local_pytest, "-q"], project_dir, timeout),
429
+ _degrade_pytest_no_tests(
430
+ _run("tests", [*local_pytest, "-q"], project_dir, timeout),
431
+ ),
404
432
  "tests(project-venv)",
405
433
  )
406
434
  pytest_cmd = _tool_cmd("pytest")
407
435
  if pytest_cmd and _foreign_pytest_can_collect(
408
436
  pytest_cmd, project_dir, timeout
409
437
  ):
410
- return _run("tests", [*pytest_cmd, "-q"], project_dir, timeout)
438
+ return _degrade_pytest_no_tests(
439
+ _run("tests", [*pytest_cmd, "-q"], project_dir, timeout),
440
+ )
411
441
  if pytest_cmd:
412
442
  return _skip(
413
443
  "tests",