arkaos 5.12.0 → 5.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 (65) hide show
  1. package/THE-ARKAOS-GUIDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/bin/arka-py.ps1 +16 -1
  4. package/config/evals/marketing.yaml +4 -4
  5. package/config/hooks/_lib/arka_python.ps1 +40 -7
  6. package/config/hooks/_lib/arka_python.sh +37 -12
  7. package/config/hooks/_lib/workflow-classifier.sh +25 -6
  8. package/config/hooks/session-start.ps1 +20 -4
  9. package/config/hooks/session-start.sh +7 -3
  10. package/config/hooks/stop.ps1 +36 -118
  11. package/config/hooks/user-prompt-submit.ps1 +4 -2
  12. package/core/governance/evidence_checks.py +526 -21
  13. package/core/hooks/stop.py +38 -1
  14. package/core/hooks/stop_governance.py +170 -0
  15. package/core/shared/temp_paths.py +16 -5
  16. package/core/workflow/state_reader.sh +7 -1
  17. package/departments/dev/skills/security-compliance/SKILL.md +0 -5
  18. package/departments/dev/skills/threejs/SKILL.md +2 -2
  19. package/departments/landing/skills/offer-create/references/offer-formats.md +2 -2
  20. package/departments/marketing/skills/ad-creative/SKILL.md +1 -1
  21. package/departments/marketing/skills/ad-creative/references/creative-review-page.md +5 -5
  22. package/departments/ops/skills/gdpr-compliance/SKILL.md +0 -5
  23. package/departments/ops/skills/iso27001/SKILL.md +0 -5
  24. package/departments/ops/skills/quality-management/SKILL.md +0 -5
  25. package/departments/ops/skills/risk-management/SKILL.md +0 -5
  26. package/departments/ops/skills/soc2-compliance/SKILL.md +0 -5
  27. package/departments/sales/skills/prospecting/references/demand-signals.md +1 -1
  28. package/harness/codex/AGENTS.md +1 -1
  29. package/harness/copilot/copilot-instructions.md +1 -1
  30. package/harness/cursor/rules/arkaos.mdc +2 -2
  31. package/harness/gemini/GEMINI.md +1 -1
  32. package/harness/opencode/AGENTS.md +1 -1
  33. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  34. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  35. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  36. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  37. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  38. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  39. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  40. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  41. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  42. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  43. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  44. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  45. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  46. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  47. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  48. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  49. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  50. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  51. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  52. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  53. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  54. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  55. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  56. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  57. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  58. package/harness/zed/.rules +1 -1
  59. package/knowledge/skills-manifest.json +1 -1
  60. package/package.json +3 -1
  61. package/pyproject.toml +36 -2
  62. package/scripts/fenced-code-allowlist.json +308 -0
  63. package/scripts/lint_fenced_code.mjs +591 -0
  64. package/scripts/skills_catalog_gen.py +21 -8
  65. package/scripts/tools/docs_stats.py +42 -2
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v5.12.0 — 89 agents, 17 departments, 340 skills, 306 commands, 20 ADRs.
3
+ > v5.14.0 — 89 agents, 17 departments, 340 skills, 306 commands, 20 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
- 5.12.0
1
+ 5.14.0
package/bin/arka-py.ps1 CHANGED
@@ -25,7 +25,22 @@ $libs = @(
25
25
  foreach ($lib in $libs) {
26
26
  if (Test-Path -LiteralPath $lib) { . $lib; break }
27
27
  }
28
- if (-not $env:ARKA_PY) { $env:ARKA_PY = "python" }
28
+ # Empty/unset means one of two things: the shared resolver was not found at
29
+ # all, or it WAS found and deliberately handed back nothing because every
30
+ # candidate is a Microsoft Store App Execution Alias. A blind `= "python"`
31
+ # here cannot tell those apart, and on a stock Windows box it picks the
32
+ # alias — running the Python install manager, which downloads a full CPython
33
+ # into the caller's cwd. Probe once more, alias-aware, then fail loudly.
34
+ if (-not $env:ARKA_PY) {
35
+ $fallback = Get-Command python -ErrorAction SilentlyContinue
36
+ if ($fallback -and $fallback.Source -notmatch '\\Microsoft\\WindowsApps\\') {
37
+ $env:ARKA_PY = $fallback.Source
38
+ } else {
39
+ [Console]::Error.WriteLine(
40
+ "arka-py: no usable Python interpreter on PATH (none found, or only Store aliases) - run npx arkaos doctor")
41
+ exit 1
42
+ }
43
+ }
29
44
 
30
45
  # ─── Make `-m core.*` resolvable regardless of cwd ────────────────────────
31
46
  $root = $env:ARKAOS_ROOT
@@ -389,7 +389,7 @@
389
389
  the carousel frames, and the different headline options they can compare. Can you put
390
390
  that together?
391
391
  expected_properties:
392
- - Produces a creative review page from the HTML template, not plain markdown
392
+ - Produces a self-contained HTML creative review page, not plain markdown
393
393
  - Populates the DATA object (concept tabs, in-feed preview, frame storyboard, headline variations,
394
394
  copy, destination)
395
395
  - Labels storyboard frames by narrative job rather than by pictured content
@@ -398,9 +398,9 @@
398
398
  - Does not present invented stats or stock imagery as the brand's real assets
399
399
  - Uses placeholders for unrendered frames and keeps image paths relative
400
400
  - Explains how to deliver the page (open locally / host / hand off the file)
401
- rubric: Should recognize this as a creative review page request and apply references/creative-review-page.md
402
- + the assets/creative-review-template.html template rather than producing plain markdown.
403
- Should copy the template into the output folder and populate its DATA object with the
401
+ rubric: Should recognize this as a creative review page request and apply the build spec in
402
+ references/creative-review-page.md rather than producing plain markdown. Should build a
403
+ single self-contained review.html in the output folder and populate its DATA block with the
404
404
  four concepts as tabs, each with an in-feed Instagram preview, a labeled frame-by-frame
405
405
  storyboard (frames labeled by narrative job — Hook / Problem / Proof / Ask — not by pictured
406
406
  content), selectable headline variations, primary text, and destination/CTA. Should curate
@@ -8,9 +8,12 @@
8
8
  # Resolution order:
9
9
  # 1. $env:ARKAOS_PYTHON (explicit override), if it exists.
10
10
  # 2. The ArkaOS venv — Scripts\python.exe (Windows) then bin/python.
11
- # 3. Any python3/python/py on PATH that can `import yaml`, skipping the
12
- # Microsoft Store App Execution Aliases (see Test-StoreAlias).
13
- # 4. Bare `python` as a last resort.
11
+ # 3. Any python/py/python3 on PATH that can `import yaml`, in THAT order
12
+ # (see the comment on the probe loop), skipping the Microsoft Store
13
+ # App Execution Aliases (see Test-StoreAlias).
14
+ # 4. `python` as a last resort — but '' when that name is a Store alias
15
+ # too, because a caller cannot tell an alias from an interpreter and
16
+ # would run it (see Get-ArkaPythonLastResort).
14
17
 
15
18
  function Test-StoreAlias {
16
19
  <#
@@ -30,15 +33,45 @@ function Test-StoreAlias {
30
33
  tree and a python_install_<timestamp>.log into their repo. Observed
31
34
  on Windows 10, Python install manager 26.3.
32
35
 
33
- Probing an alias can never help anyway: the stub carries no site
34
- packages, so the `import yaml` check it exists to answer would fail
35
- after paying the install.
36
+ Probing an alias can never help anyway. Either the manager installs
37
+ a CPython and the probe measures THAT interpreter a fresh install
38
+ with no pyyaml, so `import yaml` fails and the candidate is rejected
39
+ after paying the full cost — or the install is declined and the stub
40
+ exits without ever running the check. There is no path where probing
41
+ the alias answers the question it was asked.
36
42
  #>
37
43
  param([string]$Path)
38
44
  if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
39
45
  return $Path -match '\\Microsoft\\WindowsApps\\'
40
46
  }
41
47
 
48
+ function Get-ArkaPythonLastResort {
49
+ <#
50
+ .SYNOPSIS
51
+ The interpreter to hand back when no probed candidate answered, or
52
+ '' when the only name left is a Store alias.
53
+
54
+ .DESCRIPTION
55
+ Extracted from Resolve-ArkaPython so the branch is reachable from a
56
+ test: in place it sits behind a probe that matches on any machine
57
+ with a working interpreter.
58
+
59
+ Returning the bare string "python" from here was the hole in this
60
+ file's own guard. Every caller gates on `-and $env:ARKA_PY`, and any
61
+ non-empty string is truthy in PowerShell, so an aliased "python"
62
+ passed the gate and was then run — the exact install-manager side
63
+ effect Test-StoreAlias exists to prevent, arriving through the
64
+ fallback instead of the probe. '' is the only value that makes the
65
+ callers degrade, so '' is what a box with nothing but aliases gets.
66
+
67
+ Mirror of arka_python_last_resort() in arka_python.sh.
68
+ #>
69
+ param([string]$Name = "python")
70
+ $cmd = Get-Command $Name -ErrorAction SilentlyContinue
71
+ if ($cmd -and -not (Test-StoreAlias $cmd.Source)) { return $cmd.Source }
72
+ return ""
73
+ }
74
+
42
75
  function Resolve-ArkaPython {
43
76
  if ($env:ARKAOS_PYTHON -and (Test-Path -LiteralPath $env:ARKAOS_PYTHON)) {
44
77
  return $env:ARKAOS_PYTHON
@@ -67,7 +100,7 @@ function Resolve-ArkaPython {
67
100
  }
68
101
  }
69
102
 
70
- return "python"
103
+ return (Get-ArkaPythonLastResort)
71
104
  }
72
105
 
73
106
  # Windows mirror of arka_resolve_root() in arka_python.sh: env override wins
@@ -10,7 +10,9 @@
10
10
  # Contract of arka_resolve_python():
11
11
  # Echoes the interpreter path and returns 0 when a provisioned ArkaOS
12
12
  # interpreter is found; echoes a best-effort fallback and returns 1 when
13
- # only a bare `python3` remains (caller degrades gracefully).
13
+ # only a bare `python3` remains (caller degrades gracefully). Echoes
14
+ # NOTHING and returns 1 when every remaining candidate is a Microsoft
15
+ # Store App Execution Alias — see arka_python_last_resort().
14
16
  #
15
17
  # Resolution order:
16
18
  # 1. $ARKAOS_PYTHON (explicit operator override), if executable.
@@ -18,7 +20,39 @@
18
20
  # python3. `[ -x ]` follows symlinks, so a Homebrew-rotated broken
19
21
  # symlink is skipped automatically.
20
22
  # 3. Any python3 on PATH / known locations that can `import yaml`.
21
- # 4. Bare `python3` as a last resort (return 1).
23
+ # 4. Bare `python3` as a last resort (return 1), or nothing at all when
24
+ # that name — and `python` behind it — is only a Store alias.
25
+
26
+ # Last-resort name selection, extracted from arka_resolve_python() so the
27
+ # branch is reachable from a test: in place it sits behind a PATH probe
28
+ # that matches on every POSIX box with a yaml-capable python3, so it can
29
+ # only be exercised on the machine it was written for.
30
+ #
31
+ # Contract: echoes the name to hand back, or NOTHING when every remaining
32
+ # candidate is a Store alias. Empty is the point, not an oversight. The
33
+ # callers gate on `command -v "$ARKA_PY"` and an alias stub PASSES that
34
+ # gate — the file is real, it is just the Python install manager rather
35
+ # than an interpreter — so the only value that degrades a hook instead of
36
+ # downloading a CPython into the user's project is no value at all.
37
+ arka_python_last_resort() {
38
+ local py3 py
39
+ py3="$(command -v python3 2>/dev/null)"
40
+ case "$py3" in
41
+ *[Ww]indows[Aa]pps*) ;;
42
+ # Absent or real: the documented `python3` contract, unchanged. POSIX
43
+ # never reaches the alias branch below.
44
+ *) printf '%s\n' "python3"; return 0 ;;
45
+ esac
46
+
47
+ # `python3` is the alias. On a Windows box that HAS a real interpreter,
48
+ # `python` is the name it answers to; on a stock box with none, that is
49
+ # an alias too and we must hand back nothing.
50
+ py="$(command -v python 2>/dev/null)"
51
+ case "$py" in
52
+ "" | *[Ww]indows[Aa]pps*) return 0 ;;
53
+ esac
54
+ printf '%s\n' "python"
55
+ }
22
56
 
23
57
  arka_resolve_python() {
24
58
  local cand
@@ -67,16 +101,7 @@ arka_resolve_python() {
67
101
  fi
68
102
  done
69
103
 
70
- # Last resort. The documented contract is `python3` + rc 1, kept as-is,
71
- # except where that name is the Store alias: handing an alias back to a
72
- # caller that will `exec` it is the very trap this function now avoids.
73
- case "$(command -v python3 2>/dev/null)" in
74
- *[Ww]indows[Aa]pps*)
75
- printf '%s\n' "python"
76
- return 1
77
- ;;
78
- esac
79
- printf '%s\n' "python3"
104
+ arka_python_last_resort
80
105
  return 1
81
106
  }
82
107
 
@@ -3,7 +3,13 @@
3
3
  # ArkaOS v2 — Shared Workflow Classifier
4
4
  #
5
5
  # Decides whether a user prompt triggers the 4-gate evidence flow.
6
- # Used by: user-prompt-submit.sh, pre-tool-use.sh, stop.sh.
6
+ #
7
+ # CLI AND TEST SURFACE ONLY — no hook sources this file. The runtime path
8
+ # is core/hooks/user_prompt_submit.py, which user-prompt-submit.sh execs;
9
+ # these functions survive as the documented CLI (see the dispatcher at the
10
+ # bottom) and as the bash half of the parity contract. That is precisely
11
+ # why the two implementations must be diffed by hand: nothing at runtime
12
+ # will ever notice them disagreeing.
7
13
  #
8
14
  # Contract:
9
15
  # arka_wf_classify "<prompt text>" → echoes "true" or "false", exits 0.
@@ -11,9 +17,11 @@
11
17
  # arka_wf_is_required "<session_id>" → exits 0 if required, 1 otherwise.
12
18
  # arka_wf_clear_required "<session_id>" → removes marker file.
13
19
  #
14
- # Markers live under /tmp/arkaos-wf-required/<session_id>.
15
- # Python path for mark/clear: delegates to flow_enforcer.py when available,
16
- # otherwise falls back to touching the marker file directly.
20
+ # Markers live under $ARKA_WF_REQUIRED_DIR/<session_id>, defaulting to
21
+ # /tmp/arkaos-wf-required (see below). The override is the seam that keeps
22
+ # writer and reader on the same directory — core/shared/temp_paths.py
23
+ # ::wf_required_dir is the Python side of it. mark/clear here write the
24
+ # marker file directly; they do NOT call flow_enforcer.py.
17
25
  # ============================================================================
18
26
 
19
27
  ARKA_WF_REQUIRED_DIR="${ARKA_WF_REQUIRED_DIR:-/tmp/arkaos-wf-required}"
@@ -65,11 +73,22 @@ arka_wf_classify() {
65
73
 
66
74
  # Question guard: interrogative lead + trailing "?" = information
67
75
  # question, never creation intent (X5 false positive).
68
- local trimmed
76
+ #
77
+ # The lead is taken from the FIRST NON-BLANK LINE, never from the whole
78
+ # text. The Python twin uses re.match on text.strip(), which can only
79
+ # match at offset 0; grep anchors ^ PER LINE, so feeding it everything
80
+ # made any later line able to trigger the guard. The two then disagreed
81
+ # on every multi-line prompt ending in a question — "implementa X\no que
82
+ # e que isto faz?" is a directive followed by a question, and a directive
83
+ # still requires the flow. awk reproduces .strip() closely enough for an
84
+ # anchor that already tolerates leading whitespace: skip blank lines,
85
+ # print the first line with content, stop.
86
+ local trimmed lead
69
87
  trimmed=$(printf '%s' "$text" | sed 's/[[:space:]]*$//')
70
88
  case "$trimmed" in
71
89
  *\?)
72
- if echo "$text" | grep -qiE "$ARKA_WF_QUESTION_LEAD"; then
90
+ lead=$(printf '%s' "$text" | awk 'NF { print; exit }')
91
+ if printf '%s' "$lead" | grep -qiE "$ARKA_WF_QUESTION_LEAD"; then
73
92
  echo "false"
74
93
  return 0
75
94
  fi
@@ -5,8 +5,10 @@
5
5
  # (core.hooks.session_start) builds the whole payload: the visible banner in
6
6
  # "systemMessage" AND the operating contracts in
7
7
  # hookSpecificOutput.additionalContext. This file only resolves the
8
- # interpreter and delegates; with no usable interpreter it emits a static
9
- # banner and exits 0 (fail-open, dependency-free).
8
+ # interpreter and delegates; whenever it cannot reach that producer no
9
+ # repo root, no module file, no interpreter, or the module itself exiting
10
+ # non-zero — it emits a static banner and exits 0 (fail-open,
11
+ # dependency-free).
10
12
  #
11
13
  # WHY A WRAPPER AND NOT A PORT
12
14
  # The previous version reimplemented the banner natively in PowerShell. It
@@ -16,7 +18,10 @@
16
18
  # (EVIDENCE-FLOW, META-TAG, AUTHORITY, MODEL-FABRIC) were computed by nobody
17
19
  # and never reached the model, while the Linux side got them every session.
18
20
  # Reimplementing the blocks here would duplicate the producer and recreate
19
- # the same drift one release later. One producer, two thin wrappers.
21
+ # the same drift one release later, so this file delegates instead. The
22
+ # twin-parity test in tests/python/test_hook_output_contract.py is what
23
+ # holds that open: it asserts both wrappers run `-m core.hooks
24
+ # .session_start`, and fails if either stops.
20
25
  #
21
26
  # Minimum PowerShell: 5.1 (shipped with every Windows 10+). No pwsh 7 prereq.
22
27
  # This file stays pure ASCII; every non-ASCII byte in the output comes from
@@ -73,8 +78,19 @@ if ($repo -and $moduleFile -and (Test-Path -LiteralPath $moduleFile) -and $env:A
73
78
  # --- Degraded fallback: static banner, valid JSON, exit 0 ------------------
74
79
  # Same contract as the .sh fallback. Built from [char] codes so this source
75
80
  # file needs no particular encoding to produce correct output.
81
+ #
82
+ # The message names the SYMPTOM, not a cause. Four conditions reach this
83
+ # line (see the guard above) and only one of them is a missing interpreter;
84
+ # the fourth — the module running and exiting non-zero — asserts the
85
+ # opposite. The .sh twin execs, so its fallback is unreachable once an
86
+ # interpreter is launched and the narrower wording survived there; this
87
+ # port falls through on $LASTEXITCODE, which is what made it false. Sending
88
+ # a Windows operator with a healthy Python to debug their Python, when the
89
+ # real fault is a stale ~/.arkaos/.repo-path, costs more than the wording
90
+ # saves. `npx arkaos doctor` diagnoses all four.
76
91
  $tri = [char]0x25B2
77
92
  $oAcute = [char]0x00F3
78
- $banner = "`n $tri A R K A O S`n The Operating System for AI Agent Teams`n`n Ol$oAcute, founder`n degraded: no usable interpreter - run npx arkaos doctor"
93
+ $emDash = [char]0x2014
94
+ $banner = "`n $tri A R K A O S`n The Operating System for AI Agent Teams`n`n Ol$oAcute, founder`n degraded: ArkaOS core not reachable $emDash run npx arkaos doctor"
79
95
  [pscustomobject]@{ systemMessage = $banner } | ConvertTo-Json -Compress
80
96
  exit 0
@@ -5,8 +5,9 @@
5
5
  # ONE python process (core.hooks.session_start) builds the entire
6
6
  # systemMessage — the 13 inline python spawns of the previous version
7
7
  # live there now (baseline 251ms p50 before consolidation). This file
8
- # only resolves the interpreter and execs; with no usable interpreter
9
- # it emits a static banner and exits 0 (fail-open, stdlib-free).
8
+ # only resolves the interpreter and execs; whenever it cannot reach that
9
+ # producer no repo root, no module file, no interpreter — it emits a
10
+ # static banner and exits 0 (fail-open, stdlib-free).
10
11
  # ============================================================================
11
12
 
12
13
  # _HOOK_CWD captured BEFORE any cd — inside a compound command $PWD
@@ -55,7 +56,10 @@ if [ -n "$REPO" ] && [ -f "$REPO/core/hooks/session_start.py" ] \
55
56
  fi
56
57
 
57
58
  # ─── Degraded fallback: static banner, valid JSON, exit 0 ──────────────
59
+ # Names the symptom, not a cause: three conditions reach here and only one
60
+ # of them is a missing interpreter. Kept byte-identical in wording to the
61
+ # .ps1 twin so an operator on either platform reads the same diagnosis.
58
62
  cat <<'EOF'
59
- {"systemMessage": "\n ▲ A R K A O S\n The Operating System for AI Agent Teams\n\n Olá, founder\n degraded: no usable interpreter — run npx arkaos doctor"}
63
+ {"systemMessage": "\n ▲ A R K A O S\n The Operating System for AI Agent Teams\n\n Olá, founder\n degraded: ArkaOS core not reachable — run npx arkaos doctor"}
60
64
  EOF
61
65
  exit 0
@@ -24,9 +24,15 @@ $cwd = [string]$inp.cwd
24
24
 
25
25
  if ($stopHookActive -eq "true") { exit 0 }
26
26
 
27
- # Marker dir must match the Python writer (_wf_mark_required /
28
- # core.shared.temp_paths.arkaos_temp_dir): %TEMP%\arkaos-wf-required on
29
- # Windows, with the same ARKA_WF_REQUIRED_DIR override the writer honors.
27
+ # Marker dir must match the Python writer: core.hooks.user_prompt_submit
28
+ # ::_wf_mark_required resolves it through core.shared.temp_paths
29
+ # ::wf_required_dir which is arkaos_temp_dir('arkaos-wf-required'),
30
+ # i.e. %TEMP%\arkaos-wf-required on Windows, EXCEPT that it honors
31
+ # ARKA_WF_REQUIRED_DIR first. Read arkaos_temp_dir directly here and the
32
+ # override splits writer from reader: the writer moves, this gate finds no
33
+ # marker and allows everything, silently. That is the fail-open
34
+ # wf_required_dir exists to close, so mirror wf_required_dir, not
35
+ # arkaos_temp_dir.
30
36
  # The previous hardcoded "/tmp/arkaos-wf-required" resolves to a path that
31
37
  # exists on no stock Windows machine, so this gate exited 0 on every real
32
38
  # session and the whole compliance chain below never ran.
@@ -204,131 +210,43 @@ try {
204
210
  }
205
211
 
206
212
  # ─── Governance parity (skill capture + warn-only detectors) ───────────
207
- # config/hooks/stop.sh delegates the whole event to `python -m
208
- # core.hooks.stop`, which runs seventeen core modules. This port
209
- # reimplements the event inline and reached only two of them, so four
213
+ # config/hooks/stop.sh delegates the whole event to `python -m` the Stop
214
+ # entrypoint, which runs the full detector chain (the authoritative list
215
+ # is that module's own imports no count is stated here because several
216
+ # are conditional, so no fixed number is true of a given event). This
217
+ # port reimplements the event and reached only two of them, so four
210
218
  # detectors that exist, are tested and are wired on POSIX never ran on
211
219
  # Windows at all — most visibly skill_proposer, whose constitution rule
212
220
  # (mandatory-skill-evaluation) mandates a capability sweep after every
213
221
  # completed task.
214
222
  #
223
+ # The sweep is a real module, not an inline script: core/hooks/
224
+ # stop_governance.py. Delegating rather than mirroring is what keeps the
225
+ # owner-only umask on the state files it writes — the previous inline
226
+ # copy of _write_tmp_state had already lost it — and it is what lets the
227
+ # parity test IMPORT AND RUN the sweep instead of grepping module names
228
+ # out of this file, which a comment could satisfy.
229
+ #
215
230
  # Scope is deliberately the NON-ENFORCEMENT subset: these four write
216
231
  # proposals and diagnostic state only. The gating checks
217
232
  # (closing_marker_check, meta_tag_check, kb_cite_check, dna_fidelity)
218
233
  # are left out of this change — they feed enforcement surfaces and
219
234
  # deserve their own baseline on Windows before being switched on.
220
- $governanceScript = @'
221
- import os
222
- import sys
223
- from pathlib import Path
224
-
225
- sys.path.insert(0, os.environ.get("ARKAOS_ROOT", ""))
226
- try:
227
- from core.workflow.flow_enforcer import _load_last_assistant_messages
228
- except Exception:
229
- sys.exit(0)
230
-
231
- transcript_path = os.environ.get("TRANSCRIPT_PATH_VAL", "")
232
- session_id = os.environ.get("SESSION_ID_VAL", "")
233
- if not transcript_path:
234
- sys.exit(0)
235
-
236
- try:
237
- msgs = _load_last_assistant_messages(transcript_path, n=1)
238
- last = msgs[-1] if msgs else ""
239
- except Exception:
240
- last = ""
241
-
242
- raw = None
243
- try:
244
- raw = Path(transcript_path).read_text(encoding="utf-8", errors="replace")
245
- except Exception:
246
- raw = None
247
-
248
-
249
- def _write_tmp_state(prefix, sid, payload):
250
- """Mirror of core.hooks.stop._write_tmp_state (best-effort)."""
251
- if not sid:
252
- return
253
- try:
254
- import json
255
-
256
- from core.shared.temp_paths import arkaos_temp_dir
257
-
258
- target = arkaos_temp_dir(prefix)
259
- target.mkdir(parents=True, exist_ok=True)
260
- (target / f"{sid}.json").write_text(
261
- json.dumps(payload), encoding="utf-8"
262
- )
263
- except Exception:
264
- pass
265
-
266
-
267
- safe_sid = None
268
- try:
269
- from core.shared.safe_session_id import safe_session_id
270
-
271
- safe_sid = safe_session_id(session_id)
272
- except Exception:
273
- safe_sid = None
274
-
275
- # 1. Capability capture — the constitution rule this port was missing.
276
- try:
277
- from core.governance.skill_proposer import evaluate as _eval_skill
278
-
279
- _eval_skill(last)
280
- except Exception:
281
- pass
282
-
283
- # 2. Sycophancy (warn-only telemetry).
284
- try:
285
- from core.governance.sycophancy_detector import detect_sycophancy
286
-
287
- sv = detect_sycophancy(last)
288
- if sv.is_sycophantic and safe_sid:
289
- _write_tmp_state("arkaos-sycophancy", safe_sid, {
290
- "is_sycophantic": sv.is_sycophantic,
291
- "signals": sv.signals,
292
- "confidence": sv.confidence,
293
- })
294
- except Exception:
295
- pass
296
-
297
- # 3. Phantom actions — narrated effects with no tool call in the turn.
298
- try:
299
- from core.governance.phantom_action_check import check_phantom_actions
300
-
301
- pr = check_phantom_actions(last, raw)
302
- if safe_sid:
303
- _write_tmp_state("arkaos-phantom", safe_sid, {
304
- "passed": pr.passed,
305
- "reason": pr.reason,
306
- "claims": pr.claims,
307
- "suggestion": pr.suggestion,
308
- })
309
- except Exception:
310
- pass
311
-
312
- # 4. Tool loops.
313
- try:
314
- from core.governance.tool_loop_check import check_tool_loops
315
-
316
- lv = check_tool_loops(raw)
317
- if lv.detected and safe_sid:
318
- _write_tmp_state("arkaos-tool-loop", safe_sid, {
319
- "tool": lv.tool,
320
- "repeats": lv.repeats,
321
- "pattern": lv.pattern,
322
- "total_tool_uses": lv.total_tool_uses,
323
- })
324
- except Exception:
325
- pass
326
- '@
327
-
328
- try {
329
- $governanceScript | & $pythonExe - | Out-Null
330
- } catch {
331
- # Swallow — governance observation must never affect the Stop hook.
235
+ $governanceModule = Join-Path $env:ARKAOS_ROOT "core/hooks/stop_governance.py"
236
+ if (Test-Path -LiteralPath $governanceModule) {
237
+ # PREPEND, never clobber: an operator's own PYTHONPATH must survive
238
+ # the hook (the .sh twin prepends for the same reason).
239
+ $sep = [IO.Path]::PathSeparator
240
+ $env:PYTHONPATH = if ($env:PYTHONPATH) {
241
+ "$($env:ARKAOS_ROOT)$sep$($env:PYTHONPATH)"
242
+ } else {
243
+ $env:ARKAOS_ROOT
244
+ }
245
+ try {
246
+ & $pythonExe -m core.hooks.stop_governance | Out-Null
247
+ } catch {
248
+ # Swallow — governance observation must never affect the Stop hook.
249
+ }
332
250
  }
333
251
 
334
252
  # Belt-and-braces marker cleanup (safe even if the Python block crashed).
@@ -18,8 +18,10 @@
18
18
  # called the workflow classifier, so the wf-required marker was never
19
19
  # created on Windows: the per-prompt half of the evidence flow (and the
20
20
  # whole compliance chain behind the Stop hook) was dead on this platform.
21
- # Same failure mode as session-start.ps1 (twin drift). One producer, two
22
- # thin wrappers.
21
+ # Same failure mode as session-start.ps1 (twin drift), and the same fix:
22
+ # delegate, and let the twin-parity test in tests/python/
23
+ # test_hook_output_contract.py fail the build if this file ever stops
24
+ # running `-m core.hooks.user_prompt_submit`.
23
25
  #
24
26
  # Minimum PowerShell: 5.1. This file stays pure ASCII; every non-ASCII byte
25
27
  # in the output comes from Python's UTF-8 stdout.