arkaos 5.11.0 → 5.12.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/config/hooks/_lib/arka_python.ps1 +33 -3
  4. package/config/hooks/_lib/arka_python.sh +33 -3
  5. package/config/hooks/_lib/workflow-classifier.sh +20 -0
  6. package/config/hooks/post-tool-use.ps1 +9 -2
  7. package/config/hooks/session-start.ps1 +60 -101
  8. package/config/hooks/stop.ps1 +139 -1
  9. package/config/hooks/user-prompt-submit.ps1 +62 -340
  10. package/core/cognition/scheduler/daemon.py +8 -0
  11. package/core/cognition/scheduler/platform.py +26 -2
  12. package/core/hooks/post_tool_use.py +18 -0
  13. package/core/hooks/stop.py +2 -2
  14. package/core/hooks/user_prompt_submit.py +40 -8
  15. package/core/knowledge/chunker.py +108 -8
  16. package/core/knowledge/doctrine.py +435 -0
  17. package/core/knowledge/indexer.py +83 -5
  18. package/core/knowledge/lexical.py +225 -0
  19. package/core/knowledge/lexical_fusion.py +130 -0
  20. package/core/knowledge/recall_cli.py +162 -0
  21. package/core/knowledge/retrieval_eval.py +290 -0
  22. package/core/knowledge/retrieval_eval_cli.py +149 -0
  23. package/core/knowledge/vault.py +67 -0
  24. package/core/knowledge/vector_store.py +61 -23
  25. package/core/runtime/claude_code.py +8 -1
  26. package/core/shared/temp_paths.py +14 -0
  27. package/core/synapse/kb_cache.py +37 -6
  28. package/core/synapse/layers_kb.py +84 -6
  29. package/core/workflow/flow_enforcer.py +2 -2
  30. package/core/workflow/research_gate.py +120 -39
  31. package/harness/codex/AGENTS.md +1 -1
  32. package/harness/copilot/copilot-instructions.md +1 -1
  33. package/harness/cursor/rules/arkaos.mdc +2 -2
  34. package/harness/gemini/GEMINI.md +1 -1
  35. package/harness/opencode/AGENTS.md +1 -1
  36. package/harness/opencode/agents/arka-architect-gabriel.md +1 -1
  37. package/harness/opencode/agents/arka-brand-director-valentina.md +1 -1
  38. package/harness/opencode/agents/arka-cfo-helena.md +1 -1
  39. package/harness/opencode/agents/arka-chief-of-staff-afonso.md +1 -1
  40. package/harness/opencode/agents/arka-community-strategist-beatriz.md +1 -1
  41. package/harness/opencode/agents/arka-content-strategist-rafael.md +1 -1
  42. package/harness/opencode/agents/arka-conversion-strategist-ines.md +1 -1
  43. package/harness/opencode/agents/arka-coo-sofia.md +1 -1
  44. package/harness/opencode/agents/arka-copy-director-eduardo.md +1 -1
  45. package/harness/opencode/agents/arka-cqo-marta.md +1 -1
  46. package/harness/opencode/agents/arka-cto-marco.md +1 -1
  47. package/harness/opencode/agents/arka-design-ops-lead-iris.md +1 -1
  48. package/harness/opencode/agents/arka-ecom-director-ricardo.md +1 -1
  49. package/harness/opencode/agents/arka-knowledge-director-clara.md +1 -1
  50. package/harness/opencode/agents/arka-leadership-director-rodrigo.md +1 -1
  51. package/harness/opencode/agents/arka-marketing-director-luna.md +1 -1
  52. package/harness/opencode/agents/arka-ops-lead-daniel.md +1 -1
  53. package/harness/opencode/agents/arka-pm-director-carolina.md +1 -1
  54. package/harness/opencode/agents/arka-revops-lead-vicente.md +1 -1
  55. package/harness/opencode/agents/arka-saas-strategist-tiago.md +1 -1
  56. package/harness/opencode/agents/arka-sales-director-miguel.md +1 -1
  57. package/harness/opencode/agents/arka-strategy-director-tomas.md +1 -1
  58. package/harness/opencode/agents/arka-tech-director-francisca.md +1 -1
  59. package/harness/opencode/agents/arka-tech-lead-paulo.md +1 -1
  60. package/harness/opencode/agents/arka-video-producer-simao.md +1 -1
  61. package/harness/zed/.rules +1 -1
  62. package/knowledge/skills-manifest.json +1 -1
  63. package/package.json +1 -1
  64. package/pyproject.toml +1 -1
  65. package/scripts/knowledge-index.py +157 -62
@@ -1,6 +1,6 @@
1
1
  # The ArkaOS Guide
2
2
 
3
- > v5.10.0 — 89 agents, 17 departments, 340 skills, 306 commands, 20 ADRs.
3
+ > v5.12.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.11.0
1
+ 5.12.0
@@ -8,9 +8,37 @@
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`.
11
+ # 3. Any python3/python/py on PATH that can `import yaml`, skipping the
12
+ # Microsoft Store App Execution Aliases (see Test-StoreAlias).
12
13
  # 4. Bare `python` as a last resort.
13
14
 
15
+ function Test-StoreAlias {
16
+ <#
17
+ .SYNOPSIS
18
+ True when a resolved command is a Microsoft Store App Execution Alias.
19
+
20
+ .DESCRIPTION
21
+ %LOCALAPPDATA%\Microsoft\WindowsApps holds zero-length reparse points
22
+ that Get-Command resolves like any other executable. On a default
23
+ Windows install `python3.exe` there points at the Python install
24
+ manager, and *running* it — which the probe below does, with
25
+ `-c "import yaml"` — makes the manager download and install a full
26
+ CPython into the current working directory.
27
+
28
+ For a hook that is a catastrophic side effect: the cwd is the user's
29
+ project, so a background hook silently drops a ~3700-file Python/
30
+ tree and a python_install_<timestamp>.log into their repo. Observed
31
+ on Windows 10, Python install manager 26.3.
32
+
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
+ #>
37
+ param([string]$Path)
38
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
39
+ return $Path -match '\\Microsoft\\WindowsApps\\'
40
+ }
41
+
14
42
  function Resolve-ArkaPython {
15
43
  if ($env:ARKAOS_PYTHON -and (Test-Path -LiteralPath $env:ARKAOS_PYTHON)) {
16
44
  return $env:ARKAOS_PYTHON
@@ -29,9 +57,11 @@ function Resolve-ArkaPython {
29
57
  }
30
58
  }
31
59
 
32
- foreach ($name in @("python3", "python", "py")) {
60
+ # "python" before "python3": on Windows a real interpreter answers to
61
+ # the former, while the latter is usually only the Store alias.
62
+ foreach ($name in @("python", "py", "python3")) {
33
63
  $cmd = Get-Command $name -ErrorAction SilentlyContinue
34
- if ($cmd) {
64
+ if ($cmd -and -not (Test-StoreAlias $cmd.Source)) {
35
65
  & $cmd.Source -c "import yaml" 2>$null
36
66
  if ($LASTEXITCODE -eq 0) { return $cmd.Source }
37
67
  }
@@ -28,24 +28,54 @@ arka_resolve_python() {
28
28
  return 0
29
29
  fi
30
30
 
31
+ # Scripts/python.exe is the Windows venv layout. Without it, a Git-Bash
32
+ # hook on Windows never matches the venv and always falls through to the
33
+ # PATH probe below -- which is where the Store alias lives.
31
34
  for cand in \
32
35
  "$HOME/.arkaos/venv/bin/python" \
33
36
  "$HOME/.arkaos/venv/bin/python3" \
37
+ "$HOME/.arkaos/venv/Scripts/python.exe" \
34
38
  "$HOME/.arkaos/.venv/bin/python" \
35
- "$HOME/.arkaos/.venv/bin/python3"; do
39
+ "$HOME/.arkaos/.venv/bin/python3" \
40
+ "$HOME/.arkaos/.venv/Scripts/python.exe"; do
36
41
  if [ -x "$cand" ]; then
37
42
  printf '%s\n' "$cand"
38
43
  return 0
39
44
  fi
40
45
  done
41
46
 
42
- for cand in python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3; do
43
- if command -v "$cand" >/dev/null 2>&1 && "$cand" -c "import yaml" >/dev/null 2>&1; then
47
+ # Never probe a Microsoft Store App Execution Alias: under Git Bash on
48
+ # Windows `command -v python3` resolves to .../Microsoft/WindowsApps/
49
+ # python3, which is the Python install manager, not an interpreter. The
50
+ # `import yaml` check RUNS each candidate, and running that one installs
51
+ # a CPython into the current working directory -- the user's project, for
52
+ # a hook. It could not have succeeded anyway: an alias stub carries no
53
+ # site packages. On POSIX nothing ever matches that path: pure no-op.
54
+ #
55
+ # `python` is LAST on purpose. On Windows it is the only real interpreter
56
+ # left once the alias is filtered out; on POSIX one of the entries before
57
+ # it always matches first, so an old box where `python` is Python 2 keeps
58
+ # resolving exactly as it does today.
59
+ for cand in python3 /opt/homebrew/bin/python3 /usr/local/bin/python3 /usr/bin/python3 python; do
60
+ case "$(command -v "$cand" 2>/dev/null)" in
61
+ "") continue ;;
62
+ *[Ww]indows[Aa]pps*) continue ;;
63
+ esac
64
+ if "$cand" -c "import yaml" >/dev/null 2>&1; then
44
65
  printf '%s\n' "$cand"
45
66
  return 0
46
67
  fi
47
68
  done
48
69
 
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
49
79
  printf '%s\n' "python3"
50
80
  return 1
51
81
  }
@@ -40,6 +40,13 @@ arka_wf_safe_session_id() {
40
40
  # missed prompts were short continuations ("continua", "força") or
41
41
  # ship-tier verbs ("ship", "publish", "merge", "release", "deploy")
42
42
  # that prolong existing flow-required activity.
43
+ # Keep in sync with _WF_QUESTION_LEAD_PATTERN in core/hooks/
44
+ # user_prompt_submit.py. Interrogative-led prompts ending in "?" are
45
+ # information questions, not creation intent, even when they contain an
46
+ # action verb ("o que e que este projeto faz?"). Polite requests ("podes
47
+ # implementar X?") keep matching: their lead word is not interrogative.
48
+ ARKA_WF_QUESTION_LEAD='^[[:space:]]*(o[[:space:]]+que|porqu[eê]|por[[:space:]]+que|para[[:space:]]+que|qual|quais|quando|onde|quem|como|what|why|which|who|whose|when|where|how|does)\b'
49
+
43
50
  ARKA_WF_VERB_PATTERN='(criar?|crie[ms]?|cria[mr]?|adicionar?|adiciona[mr]?|implementar?|implementa[mr]?|desenvolver?|desenvolve[mr]?|construir?|constru[ií]a?[mr]?|fazer?|faz[ae]?[mr]?|refactor(izar?)?|corrigir?|corrige[mr]?|consertar?|conserta[mr]?|continuar?|continua[mr]?|forçar?|força[mr]?|colocar?|coloca[mr]?|p[oô]r|melhorar?|melhora[mr]?|terminar?|termina[mr]?|acabar?|acaba[mr]?|publicar?|publica[mr]?|lançar?|lança[mr]?|create[sd]?|creating|build(s|ing)?|add(s|ed|ing)?|implement(s|ed|ing)?|develop(s|ed|ing)?|fix(es|ed|ing)?|refactor(s|ed|ing)?|make[sd]?|making|continue[sd]?|continuing|ship(s|ped|ping)?|merge[sd]?|merging|publish(es|ed|ing)?|release[sd]?|releasing|deploy(s|ed|ing)?|finish(es|ed|ing)?|improve[sd]?|improving)'
44
51
 
45
52
  # Classify: returns "true" if the prompt looks like a creation/
@@ -56,6 +63,19 @@ arka_wf_classify() {
56
63
  return 0
57
64
  fi
58
65
 
66
+ # Question guard: interrogative lead + trailing "?" = information
67
+ # question, never creation intent (X5 false positive).
68
+ local trimmed
69
+ trimmed=$(printf '%s' "$text" | sed 's/[[:space:]]*$//')
70
+ case "$trimmed" in
71
+ *\?)
72
+ if echo "$text" | grep -qiE "$ARKA_WF_QUESTION_LEAD"; then
73
+ echo "false"
74
+ return 0
75
+ fi
76
+ ;;
77
+ esac
78
+
59
79
  if echo "$text" | grep -qiE "\b${ARKA_WF_VERB_PATTERN}\b"; then
60
80
  echo "true"
61
81
  else
@@ -203,9 +203,16 @@ if ($shouldProcessGotchas -and $null -ne $payload) {
203
203
  if (Test-Path -LiteralPath $venvPy) {
204
204
  $pythonForMarker = $venvPy
205
205
  } else {
206
- foreach ($cmd in 'python3','python','py') {
206
+ # Never accept %LOCALAPPDATA%\Microsoft\WindowsApps: the
207
+ # python3.exe there is the Store install-manager alias,
208
+ # and running it downloads a full CPython into the cwd —
209
+ # which for a hook is the user's own project directory.
210
+ # Same reason "python" is tried before "python3" here.
211
+ foreach ($cmd in 'python','py','python3') {
207
212
  $resolved = Get-Command $cmd -ErrorAction SilentlyContinue
208
- if ($resolved) { $pythonForMarker = $resolved.Source; break }
213
+ if ($resolved -and $resolved.Source -notmatch '\\Microsoft\\WindowsApps\\') {
214
+ $pythonForMarker = $resolved.Source; break
215
+ }
209
216
  }
210
217
  }
211
218
  if ($pythonForMarker) {
@@ -1,121 +1,80 @@
1
1
  # ============================================================================
2
- # ArkaOS SessionStart Hook (Windows / PowerShell 5.1+)
2
+ # ArkaOS - SessionStart Hook (Windows / PowerShell 5.1+)
3
3
  #
4
- # Port of config/hooks/session-start.sh. Must remain behaviourally identical:
5
- # - Same profile lookup, time-based greeting, version-drift detection.
6
- # - Same JSON output contract: {"systemMessage": "<...>"} on stdout.
4
+ # Thin wrapper - mirror of config/hooks/session-start.sh. ONE python process
5
+ # (core.hooks.session_start) builds the whole payload: the visible banner in
6
+ # "systemMessage" AND the operating contracts in
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).
10
+ #
11
+ # WHY A WRAPPER AND NOT A PORT
12
+ # The previous version reimplemented the banner natively in PowerShell. It
13
+ # was a faithful port of the *older* .sh, from the era when the .sh built the
14
+ # banner in bash. When the .sh became a thin wrapper delegating to Python,
15
+ # this file did not follow - so on Windows the four contract blocks
16
+ # (EVIDENCE-FLOW, META-TAG, AUTHORITY, MODEL-FABRIC) were computed by nobody
17
+ # and never reached the model, while the Linux side got them every session.
18
+ # Reimplementing the blocks here would duplicate the producer and recreate
19
+ # the same drift one release later. One producer, two thin wrappers.
7
20
  #
8
21
  # Minimum PowerShell: 5.1 (shipped with every Windows 10+). No pwsh 7 prereq.
9
- # Box-drawing characters are built from [char] codes so this file stays pure
10
- # ASCII and avoids source-encoding pitfalls with PS 5.1 default ANSI reads.
22
+ # This file stays pure ASCII; every non-ASCII byte in the output comes from
23
+ # Python's UTF-8 stdout, which is why the encoding setup below is required.
11
24
  # ============================================================================
12
25
 
13
- $ErrorActionPreference = 'Stop'
14
- # Force UTF-8 on stdout so ConvertTo-Json does not emit mojibake box chars.
15
- [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
26
+ $ErrorActionPreference = 'SilentlyContinue'
16
27
 
17
- $arkaosHome = Join-Path $env:USERPROFILE '.arkaos'
28
+ # Captured BEFORE any location change - inside a compound command the CWD
29
+ # would resolve after the push (QG blocker F1-A3: cross-project leak).
30
+ $env:ARKA_HOOK_CWD = (Get-Location).Path
18
31
 
19
- # ─── Profile ───────────────────────────────────────────────────────────
20
- $name = 'founder'
21
- $company = 'WizardingCode'
22
- $version = '2.x'
32
+ # Force UTF-8 on both sides of the pipe. Python emits UTF-8; without this
33
+ # PS 5.1 decodes it as the ANSI codepage (cp1252 on pt-PT/pt-BR machines)
34
+ # and the banner arrives as mojibake - the failure mode tracked in #395/#396.
35
+ # PYTHONIOENCODING covers the producer, OutputEncoding the consumer.
36
+ [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
37
+ $env:PYTHONIOENCODING = 'utf-8'
23
38
 
24
- $profilePath = Join-Path $arkaosHome 'profile.json'
25
- if (Test-Path -LiteralPath $profilePath) {
26
- try {
27
- # $profile is an automatic variable in PowerShell — use $arkaosProfile.
28
- $arkaosProfile = Get-Content -Raw -LiteralPath $profilePath -Encoding UTF8 | ConvertFrom-Json
29
- if ($arkaosProfile.name) { $name = [string]$arkaosProfile.name }
30
- elseif ($arkaosProfile.role) { $name = [string]$arkaosProfile.role }
31
- if ($arkaosProfile.company) { $company = [string]$arkaosProfile.company }
32
- } catch {
33
- # Corrupt profile — keep defaults and continue.
34
- }
35
- }
39
+ # --- Shared Python resolver (sets $env:ARKA_PY, provides Resolve-ArkaRoot) ---
40
+ $arkaLib = Join-Path $PSScriptRoot '_lib\arka_python.ps1'
41
+ if (Test-Path -LiteralPath $arkaLib) { . $arkaLib }
36
42
 
37
- $repoPathFile = Join-Path $arkaosHome '.repo-path'
38
- if (Test-Path -LiteralPath $repoPathFile) {
39
- try {
43
+ # --- Resolve the repo root ---
44
+ # .repo-path can point at a purged npx cache; Resolve-ArkaRoot falls through
45
+ # to the ~/.arkaos/lib snapshot instead of returning a dead root.
46
+ $repo = ''
47
+ if (Get-Command Resolve-ArkaRoot -ErrorAction SilentlyContinue) {
48
+ $repo = Resolve-ArkaRoot
49
+ } else {
50
+ $repoPathFile = Join-Path $HOME '.arkaos/.repo-path'
51
+ if (Test-Path -LiteralPath $repoPathFile) {
40
52
  $repo = (Get-Content -Raw -LiteralPath $repoPathFile -Encoding UTF8).Trim()
41
- if ($repo) {
42
- $versionFile = Join-Path $repo 'VERSION'
43
- if (Test-Path -LiteralPath $versionFile) {
44
- $version = (Get-Content -Raw -LiteralPath $versionFile -Encoding UTF8).Trim()
45
- }
46
- }
47
- } catch { }
48
- }
49
- # .repo-path may point at a purged npx cache — the installer's snapshot
50
- # keeps the drift banner alive ('2.x' is the untouched default above).
51
- if ($version -eq '2.x') {
52
- $libVersionFile = Join-Path $arkaosHome 'lib\VERSION'
53
- if (Test-Path -LiteralPath $libVersionFile) {
54
- try {
55
- $version = (Get-Content -Raw -LiteralPath $libVersionFile -Encoding UTF8).Trim()
56
- } catch { }
57
53
  }
58
54
  }
59
55
 
60
- # ─── Time greeting ─────────────────────────────────────────────────────
61
- $hour = [int](Get-Date -Format 'HH')
62
- if ($hour -ge 5 -and $hour -lt 12) { $greeting = 'Bom dia' }
63
- elseif ($hour -ge 12 -and $hour -lt 19) { $greeting = 'Boa tarde' }
64
- else { $greeting = 'Boa noite' }
56
+ # The MODULE file is the guard, not just core/: an older installed snapshot
57
+ # without it would delegate into "No module named ..." (exit 1, empty output
58
+ # - a broken SessionStart instead of a degraded banner).
59
+ $moduleFile = if ($repo) { Join-Path $repo 'core/hooks/session_start.py' } else { '' }
65
60
 
66
- # ─── Version drift ─────────────────────────────────────────────────────
67
- $drift = ''
68
- $syncStatePath = Join-Path $arkaosHome 'sync-state.json'
69
- if (Test-Path -LiteralPath $syncStatePath) {
61
+ if ($repo -and $moduleFile -and (Test-Path -LiteralPath $moduleFile) -and $env:ARKA_PY) {
62
+ $env:PYTHONPATH = $repo
63
+ Push-Location -LiteralPath $repo
70
64
  try {
71
- $syncState = Get-Content -Raw -LiteralPath $syncStatePath -Encoding UTF8 | ConvertFrom-Json
72
- $synced = if ($null -ne $syncState.version) { [string]$syncState.version } else { 'none' }
73
- if ($synced -ne $version) {
74
- $drift = "`n[arka:update-available] Core v$version != synced v$synced. Run /arka update."
75
- }
65
+ & $env:ARKA_PY -m core.hooks.session_start
66
+ if ($LASTEXITCODE -eq 0) { Pop-Location; exit 0 }
76
67
  } catch {
77
- $drift = "`n[arka:update-available] Sync state unreadable. Run /arka update."
68
+ # fall through to the degraded banner
78
69
  }
79
- } else {
80
- $drift = "`n[arka:update-available] Never synced. Run /arka update."
81
- }
82
-
83
- # ─── Build ASCII header from char codes ───────────────────────────────
84
- $tl = [char]0x2554 # ╔
85
- $tr = [char]0x2557 # ╗
86
- $bl = [char]0x255A # ╚
87
- $br = [char]0x255D # ╝
88
- $h = [char]0x2550 # ═
89
- $v = [char]0x2551 # ║
90
- $bar = [string]($h.ToString() * 46)
91
-
92
- $topLine = "$tl$bar$tr"
93
- $bottomLine = "$bl$bar$br"
94
- $empty = "$v" + (' ' * 46) + "$v"
95
- function Pad-Line([string]$text) {
96
- $innerWidth = 46
97
- $padLeft = [int](($innerWidth - $text.Length) / 2)
98
- $padRight = $innerWidth - $text.Length - $padLeft
99
- return "$v" + (' ' * $padLeft) + $text + (' ' * $padRight) + "$v"
70
+ Pop-Location
100
71
  }
101
72
 
102
- $lines = @(
103
- $topLine
104
- $empty
105
- (Pad-Line 'A R K A O S')
106
- $empty
107
- (Pad-Line 'The Operating System for AI Teams')
108
- (Pad-Line 'by WizardingCode')
109
- $empty
110
- $bottomLine
111
- ''
112
- "$greeting, $name ($company)"
113
- # Counts intentionally omitted (prompt-surface P0 2026-07-08): hand-typed
114
- # numbers drift from docs_stats.py truth and change no model behavior.
115
- "ArkaOS v$version$drift"
116
- )
117
-
118
- $msg = "`n" + ($lines -join "`n")
119
-
120
- # ─── Output as systemMessage JSON (single line, same contract as .sh) ──
121
- [pscustomobject]@{ systemMessage = $msg } | ConvertTo-Json -Compress
73
+ # --- Degraded fallback: static banner, valid JSON, exit 0 ------------------
74
+ # Same contract as the .sh fallback. Built from [char] codes so this source
75
+ # file needs no particular encoding to produce correct output.
76
+ $tri = [char]0x25B2
77
+ $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"
79
+ [pscustomobject]@{ systemMessage = $banner } | ConvertTo-Json -Compress
80
+ exit 0
@@ -24,7 +24,17 @@ $cwd = [string]$inp.cwd
24
24
 
25
25
  if ($stopHookActive -eq "true") { exit 0 }
26
26
 
27
- $wfMarker = Join-Path "/tmp/arkaos-wf-required" $sessionId
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.
30
+ # The previous hardcoded "/tmp/arkaos-wf-required" resolves to a path that
31
+ # exists on no stock Windows machine, so this gate exited 0 on every real
32
+ # session and the whole compliance chain below never ran.
33
+ $wfDir = $env:ARKA_WF_REQUIRED_DIR
34
+ if ([string]::IsNullOrWhiteSpace($wfDir)) {
35
+ $wfDir = Join-Path ([System.IO.Path]::GetTempPath()) 'arkaos-wf-required'
36
+ }
37
+ $wfMarker = Join-Path $wfDir $sessionId
28
38
  if ([string]::IsNullOrWhiteSpace($sessionId) -or -not (Test-Path $wfMarker)) {
29
39
  exit 0
30
40
  }
@@ -193,6 +203,134 @@ try {
193
203
  # Swallow — enqueue is fire-and-forget.
194
204
  }
195
205
 
206
+ # ─── 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
210
+ # detectors that exist, are tested and are wired on POSIX never ran on
211
+ # Windows at all — most visibly skill_proposer, whose constitution rule
212
+ # (mandatory-skill-evaluation) mandates a capability sweep after every
213
+ # completed task.
214
+ #
215
+ # Scope is deliberately the NON-ENFORCEMENT subset: these four write
216
+ # proposals and diagnostic state only. The gating checks
217
+ # (closing_marker_check, meta_tag_check, kb_cite_check, dna_fidelity)
218
+ # are left out of this change — they feed enforcement surfaces and
219
+ # 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.
332
+ }
333
+
196
334
  # Belt-and-braces marker cleanup (safe even if the Python block crashed).
197
335
  if ($sessionId -match '^[A-Za-z0-9._-]{1,128}$') {
198
336
  Remove-Item -LiteralPath $wfMarker -ErrorAction SilentlyContinue