pantheon-opencode 1.5.2 → 1.6.0-beta.10

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 (146) hide show
  1. package/.pantheon/code-mode/README.md +4 -2
  2. package/.pantheon/code-mode/checkpoint_session.py +138 -102
  3. package/.pantheon/code-mode/compress-inline.py +102 -57
  4. package/.pantheon/code-mode/manifest.json +2 -8
  5. package/AGENTS.global.md +182 -0
  6. package/AGENTS.md +10 -891
  7. package/CHANGELOG.md +343 -0
  8. package/README.md +577 -79
  9. package/README.pt-BR.md +345 -60
  10. package/bin/pantheon-init.mjs +26 -3
  11. package/commands/pantheon-audit.md +7 -1
  12. package/commands/pantheon-consolidate.md +1 -1
  13. package/commands/pantheon-deepwork.md +14 -3
  14. package/commands/pantheon-focus.md +17 -0
  15. package/commands/pantheon-optimize.md +2 -2
  16. package/commands/pantheon-vision.md +61 -12
  17. package/package.json +30 -11
  18. package/plugin.json +2 -2
  19. package/pyproject.toml +11 -16
  20. package/scripts/build-agents-md.mjs +376 -58
  21. package/scripts/changelog-merge.mjs +161 -0
  22. package/scripts/check-plugin-v2-coverage.mjs +213 -0
  23. package/scripts/ci-validate-yaml.py +7 -5
  24. package/scripts/{code_mode_server.py → code_mode.py} +119 -27
  25. package/scripts/cost.mjs +244 -41
  26. package/scripts/doctor.mjs +236 -29
  27. package/scripts/install/agents-md.mjs +4 -4
  28. package/scripts/install/cli-ui.mjs +1 -1
  29. package/scripts/install/config-migration.mjs +319 -6
  30. package/scripts/install/health-check.mjs +26 -37
  31. package/scripts/install/interactive.mjs +7 -7
  32. package/scripts/install/migrate.mjs +1 -1
  33. package/scripts/install/opencode-version.mjs +246 -4
  34. package/scripts/install/opencode.mjs +627 -179
  35. package/scripts/install/plugin.mjs +43 -9
  36. package/scripts/install/shared.mjs +120 -12
  37. package/scripts/install/vision-prompt.mjs +161 -0
  38. package/scripts/install-mcp.mjs +28 -9
  39. package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  40. package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +2 -2
  41. package/scripts/{memory_mcp_server.py → memory_mcp.py} +372 -198
  42. package/scripts/postinstall.mjs +0 -7
  43. package/scripts/probe-context-rehydrate.mjs +36 -13
  44. package/scripts/probe-pantheon-cost.mjs +71 -15
  45. package/scripts/prune.mjs +2 -2
  46. package/scripts/run-node-tests.mjs +189 -0
  47. package/scripts/sync-artifacts.mjs +26 -13
  48. package/scripts/sync-tui.mjs +24 -3
  49. package/scripts/test-opencode-v2-sandbox.sh +1913 -0
  50. package/scripts/validate-routing.mjs +43 -3
  51. package/scripts/validate-sandbox-install.mjs +138 -0
  52. package/scripts/versioning.mjs +13 -1
  53. package/scripts/vision-status.mjs +682 -0
  54. package/skills-lock.json +15 -16
  55. package/src/agents/aphrodite.md +20 -37
  56. package/src/agents/apollo.md +7 -3
  57. package/src/agents/athena.md +63 -35
  58. package/src/agents/demeter.md +8 -28
  59. package/src/agents/gaia.md +1 -1
  60. package/src/agents/hephaestus.md +10 -21
  61. package/src/agents/hermes.md +15 -38
  62. package/src/agents/iris.md +1 -1
  63. package/src/agents/mnemosyne.md +23 -60
  64. package/src/agents/nyx.md +1 -1
  65. package/src/agents/prometheus.md +16 -20
  66. package/src/agents/talos.md +10 -2
  67. package/src/agents/themis.md +5 -5
  68. package/src/agents/zeus.md +99 -130
  69. package/src/instructions/agent-return-format.instructions.md +18 -3
  70. package/src/instructions/backend-standards.instructions.md +2 -0
  71. package/src/instructions/frontend-standards.instructions.md +3 -1
  72. package/src/instructions/memory-protocol.instructions.md +11 -110
  73. package/src/instructions/zeus-anti-stall.instructions.md +14 -61
  74. package/src/instructions/zeus-communication-rules.instructions.md +0 -1
  75. package/src/instructions/zeus-council-synthesis.instructions.md +32 -260
  76. package/src/instructions/zeus-memory-operations.instructions.md +26 -0
  77. package/src/instructions/zeus-timeout-retry.instructions.md +40 -87
  78. package/src/mcp/{code_mode_server.py → code_mode.py} +119 -27
  79. package/src/mcp/eval_store.py +2 -2
  80. package/src/mcp/mcp_codemap_module.py +559 -129
  81. package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +392 -78
  82. package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +2 -2
  83. package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +373 -250
  84. package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +396 -40
  85. package/src/mcp/requirements-mcp.txt +10 -6
  86. package/src/mcp/requirements-vision.txt +4 -2
  87. package/src/pantheon/cost-command.ts +399 -101
  88. package/src/pantheon/delegation-enforce.ts +10 -1
  89. package/src/pantheon/model-command.ts +18 -7
  90. package/src/pantheon/presets.d.mts +0 -8
  91. package/src/pantheon/presets.mjs +5 -32
  92. package/src/pantheon/todo-enforcer.ts +1 -1
  93. package/src/pantheon/v2-bounded-read.ts +640 -0
  94. package/src/pantheon/v2-delegation-enforce.ts +214 -0
  95. package/src/pantheon/v2-execute-after.ts +186 -0
  96. package/src/pantheon/v2-tools.ts +429 -0
  97. package/src/pantheon/v2-unsupported.d.mts +11 -0
  98. package/src/pantheon/v2-unsupported.mjs +90 -0
  99. package/src/pantheon/v2-vision.ts +622 -0
  100. package/src/pantheon/vision-local.ts +531 -0
  101. package/src/pantheon/vision.ts +1161 -116
  102. package/src/plugin-v2.ts +1244 -288
  103. package/src/plugin.ts +12 -9
  104. package/src/plugins/tui/dist/server.d.ts +1 -2
  105. package/src/plugins/tui/dist/server.d.ts.map +1 -1
  106. package/src/plugins/tui/dist/tui-v2.d.ts +154 -0
  107. package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
  108. package/src/plugins/tui/dist/tui-v2.js +659 -0
  109. package/src/plugins/tui/dist/tui-v2.js.map +1 -0
  110. package/src/plugins/tui/dist/tui.d.ts +85 -82
  111. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  112. package/src/plugins/tui/dist/tui.js +72 -59
  113. package/src/plugins/tui/index-v2.ts +6 -0
  114. package/src/plugins/tui/package-lock.json +5504 -1341
  115. package/src/plugins/tui/package.json +25 -15
  116. package/src/plugins/tui/src/index-v2.tsx +734 -0
  117. package/src/plugins/tui/src/index.tsx +106 -85
  118. package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
  119. package/src/plugins/tui/tsconfig.build.json +5 -0
  120. package/src/plugins/tui/tsconfig.typecheck.json +5 -0
  121. package/src/plugins/tui/tsdown.config.ts +22 -2
  122. package/src/plugins/tui/tui-v2.ts +2 -0
  123. package/src/plugins/tui/tui.ts +3 -0
  124. package/src/routing.yml +11 -22
  125. package/src/skills/README.md +34 -39
  126. package/src/skills/agent-coordination/SKILL.md +7 -2
  127. package/src/skills/artifact-management/SKILL.md +12 -5
  128. package/src/skills/auto-continue/SKILL.md +54 -95
  129. package/src/skills/code-review-checklist/SKILL.md +5 -5
  130. package/src/skills/context-compression/SKILL.md +26 -879
  131. package/src/skills/session-goal/SKILL.md +14 -5
  132. package/src/skills/spec-driven-development/SKILL.md +3 -1
  133. package/src/skills/tdd-with-agents/SKILL.md +18 -10
  134. package/.pantheon/code-mode/eval-llm-judge.py +0 -255
  135. package/.pantheon/code-mode/eval-monte-carlo.py +0 -368
  136. package/.pantheon/code-mode/eval-run.py +0 -197
  137. package/.pantheon/code-mode/eval-static.py +0 -321
  138. package/.pantheon/code-mode/session-end-save.py +0 -186
  139. package/.pantheon/code-mode/session-end-save.sh +0 -18
  140. package/opencode.json +0 -58
  141. package/scripts/recover-zenodo-deposition.mjs +0 -146
  142. package/scripts/test-opencode-v1-v2-sandbox.sh +0 -862
  143. package/scripts/validate-zenodo-release.mjs +0 -154
  144. package/scripts/zenodo-artifact.mjs +0 -94
  145. package/scripts/zenodo-release-state.mjs +0 -53
  146. package/src/skills/orchestration-workflow/SKILL.md +0 -311
@@ -0,0 +1,26 @@
1
+ ---
2
+ description: "Zeus-only council memory procedures"
3
+ name: "Zeus Memory Operations"
4
+ agents: [zeus]
5
+ fallback: shared
6
+ ---
7
+
8
+ # Zeus Memory Operations
9
+
10
+ ## Routing
11
+
12
+ Reuse relevant memory already present in the request or task context. If prior project history matters and no result is present, Zeus may do one targeted search and pass useful hits with the delegation. Do not search on every turn or add KV/cache calls to ordinary dispatches.
13
+
14
+ ## Council Decisions
15
+
16
+ Before a council, search separately from the task-level lookup only when a past council decision could affect the recommendation:
17
+ `memory_search(query=question, top_k=2, namespace="council_decisions")`.
18
+ Use a recent, directly matching decision only after checking current context;
19
+ otherwise treat it as background, never as authority to skip a needed review.
20
+
21
+ Persist only reusable, material decisions. Use
22
+ `key="council:<yyyy-mm-dd>:<slug>"`; JSON-stringify both `value` and
23
+ `metadata` for `memory_store(namespace="council_decisions", ...)`. Keep the
24
+ record concise: question, recommendation, specialists, confidence, unresolved
25
+ trade-off, and timestamp. Do not store routine subtask summaries or status
26
+ updates. Use the memory bank for ADRs only when explicitly requested.
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: "Timeout enforcement, retry policies, subtask dispatch, and timeout tracking for Zeus"
3
3
  name: "Zeus Timeout & Retry"
4
- applyTo: "agents/zeus.agent.md"
4
+ agents: [zeus]
5
+ fallback: shared
5
6
  ---
6
7
 
7
8
  # ⏱️ TIMEOUT & RETRY ENFORCEMENT
@@ -10,123 +11,75 @@ When a delegated agent does not respond in time, enforce the timeout policy from
10
11
 
11
12
  ## Timeout Behavior by Agent Role
12
13
 
13
- | Agent Role | Timeout | Retry Policy | Fallback Chain | Partial Results OK? | Reasoning Effort |
14
- |------------|---------|-------------|----------|---------------------|------------------|
15
- | Explorer (@apollo) | 60s | 2 retries, exp backoff | @athena (plan) → @hermes (impl) | ✅ Yes | low |
16
- | Implementer (@hermes, @aphrodite, @demeter) | 180s | 3 retries, exp backoff | @talos (hotfix) → @athena (replan) | ❌ No | medium |
17
- | Reviewer (@themis) | 120s | 3 retries, exp backoff | @zeus (escalate) → user | ❌ No | high |
18
- | Infrastructure (@prometheus) | 300s | 3 retries, exp backoff | @hermes (config) → @zeus (escalate) | ❌ No | medium |
19
- | Hotfix (@talos) | 30s | 2 retries, no backoff | @hermes (full fix) | ✅ Yes | low |
20
- | Remote Sensing (@gaia) | 120s | 2 retries, exp backoff | @hermes (generic) | ✅ Yes | high |
14
+ | Agent Role | Timeout | Retry Policy | Partial Results OK? | Reasoning Effort |
15
+ |------------|---------|--------------|---------------------|------------------|
16
+ | Explorer (@apollo) | 60s | 1 retry | ✅ Yes | low |
17
+ | Implementer (@hermes, @aphrodite, @demeter) | 180s | 1 retry | ❌ No | medium |
18
+ | Reviewer (@themis) | 120s | 1 retry | ❌ No | high |
19
+ | Infrastructure (@prometheus) | 300s | 1 retry | ❌ No | medium |
20
+ | Hotfix (@talos) | 30s | 1 retry | ✅ Yes | low |
21
+ | Remote Sensing (@gaia) | 120s | 1 retry | ✅ Yes | high |
21
22
 
22
23
  ## Retry Flow
23
24
 
24
25
  ```
25
- Task dispatch → timeout elapsed → log timeout
26
- ├─ retry_count > 0 → retry with exponential backoff → decrement retry_count
27
- │ └─ still failing? → retry again (up to retry_count limit)
28
- │
29
- ├─ retries exhausted → FALLBACK CHAIN:
30
- │ ├─ fallback[0] exists? → dispatch to fallback agent
31
- │ │ └─ keep original task spec + context; add error context
32
- │ ├─ fallback[1] exists? → dispatch to second fallback
33
- │ └─ all fallbacks failed? → ESCALATE
34
- │
35
- ├─ no fallbacks defined → return TIMEOUT error to user
36
- │
37
- └─ ESCALATE: "Agent X failed after N retries. Fallbacks Y, Z also failed.
38
- Options: (a) try different agent, (b) simplify scope, (c) manual"
26
+ Initial attempt → timeout/failure → log it → make at most one retry
27
+ └─ retry fails → try fallback agents left-to-right with original task/context
28
+ └─ all fallbacks fail (or none exist) → report the chain and escalate
39
29
  ```
30
+ `background_delegation.retry_count: 1` means one retry after the initial attempt, not one total attempt. Never restart an exhausted chain automatically.
40
31
 
41
-
32
+ Apply the retry only to a transient dispatch failure (timeout, crash, or empty response). A failed test, validation, or migration is a result to diagnose, not a reason to repeat the same command or agent blindly. Track agents already tried: each agent may be retried at most once and each listed fallback may be tried at most once. For auth/security/data/schema work, a fallback must preserve specialist competence, Themis review, and human approval; otherwise stop and escalate. Never let full-auto bypass these gates.
33
+ An agent refusal or scope-boundary response is not a transient failure: do not retry it with a rephrased prompt. Correct the route once if the right specialist is clear; otherwise stop and ask.
42
34
 
43
35
  ## Fallback Chain Definitions
44
36
 
45
- Each fallback chain is evaluated left-to-right: if the first fallback fails, try the second, etc.
46
-
47
- | Agent | Fallback[0] | Fallback[1] | Escalate To |
48
- |-------|------------|------------|-------------|
49
- | @apollo | @athena (plan scoped task) | @hermes (implement search) | @zeus |
50
- | @hermes | @talos (minimal fix) | @athena (replan + simplify) | @zeus |
51
- | @aphrodite | @talos (CSS/UX fix) | @hermes (generic fallback) | @zeus |
52
- | @demeter | @hermes (generic backend) | @athena (replan schema) | @zeus |
53
- | @themis | @zeus (direct escalation) | — | user |
54
- | @prometheus | @hermes (config/deploy) | @zeus | user |
55
- | @talos | @hermes (full implementation) | — | @zeus |
56
- | @hephaestus | @nyx (observability debug) | @hermes (generic) | @zeus |
57
- | @nyx | @hermes (generic) | — | @zeus |
58
- | @iris | @zeus (manual override) | — | user |
59
- | @mnemosyne | @zeus (manual) | — | user |
37
+ `routing.yml` → `fallback_chains` is the canonical agent-by-agent mapping. Evaluate
38
+ the configured chain left-to-right; if an agent has no configured chain, stop
39
+ and escalate rather than guessing a fallback. Each listed fallback may be tried
40
+ at most once, and an exhausted chain is never restarted automatically.
60
41
 
61
- ### Escalation Protocol
62
- When ALL fallbacks fail:
63
- 1. Log the full failure chain: which agents were tried, what error each returned
64
- 2. Report to user: "Task [X] failed. Tried: [agent list]. Error: [summary]."
65
- 3. Offer options: (a) try different approach, (b) simplify scope, (c) manual fix
66
- 4. NEVER retry the same chain automatically — break the cycle
42
+ ### Refusal Is a Routing Error
67
43
 
68
- ### Session Reuse Check
69
- Before dispatching a task, check if a reusable session exists:
44
+ For a domain/scope refusal, do not retry with a rephrased prompt. Correct the
45
+ route once only when the right specialist is clear; otherwise stop and ask.
46
+ Record one `DelegationCacheDecision` telemetry item for the corrected route,
47
+ including `reroute_from`, `reroute_to`, and `delegation_id` when available.
70
48
 
71
- ```
72
- @hermes — continuing from previous session.
73
- Files already explored: backend/routers/auth.py, backend/services/auth_service.py.
74
- New task: add refresh token rotation.
75
- ```
49
+ ### Escalation Protocol
50
+ When all fallbacks fail, report the tried agents and errors, offer a different approach/simpler scope/manual fix, and stop. Never retry the same chain automatically.
76
51
 
77
- Use `session_max` from routing.yml to determine how many sessions to keep per agent.
52
+ ### Session Reuse Check
53
+ Check for a reusable session before dispatch and obey `session_max` in `routing.yml`.
78
54
 
79
55
  ---
80
56
 
81
57
  # 📦 SUBTASK DISPATCH (Lightweight Delegation)
82
58
 
83
- Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle. Use it for focused work that doesn't need Themis review.
59
+ Subtask is a bounded, low-risk delegation mode that **skips** the standard artifact lifecycle. Choose it by risk and need for review, not by a fixed file/line count.
84
60
 
85
61
  ## When to Use Subtask vs Full Task
86
62
 
87
- > **REGRA DE OURO:** Quando em dúvida, use full task. Subtask é para o que você tem 100% de certeza que é seguro pular revisão.
63
+ Use the lightest path that safely meets the request. A multi-file but bounded low-risk change does not automatically need a plan/artifact phase.
88
64
 
89
65
  ### Subtask Decision Tree (run BEFORE every delegation)
90
66
 
91
67
  ```
92
- □ Scope: ≤2 files AND ≤10 lines changed? [YES→continue | NO→full task]
93
- □ Risk: No schema change, no security impact? [YES→continue | NO→full task]
94
- □ Auth: No authentication/authorization logic? [YES→continue | NO→full task]
95
- □ Data: No data loss risk, no migration? [YES→continue | NO→full task]
96
- □ Review: Output does NOT feed into Themis review? [YES→continue | NO→full task]
97
-
98
- ALL YES → subtask (skip artifact + Themis)
99
- ANY NO → full task (IMPL artifact + Themis review mandatory)
100
- ```
101
-
102
- ### Comparison Table
68
+ □ Is the scope clear, bounded, and reversible? [YES→continue | NO→clarify/plan]
69
+ □ Does it avoid auth/security, data/schema, and destructive risk? [YES→continue | NO→full review/gates]
70
+ □ Is there no required Themis/audit handoff for this change? [YES→continue | NO→preserve that review]
103
71
 
104
- | Aspect | Subtask | Full Task |
105
- |--------|---------|-----------|
106
- | Scope | Single file, <10 lines, read-only | Feature, multi-file, schema change |
107
- | Risk | Low (no security/data implications) | Any risk level |
108
- | Artifact | ❌ No IMPL artifact | ✅ IMPL artifact required |
109
- | Themis review | ❌ None | ✅ Mandatory |
110
- | Use case | Apollo discovery, Talos hotfix, bounded fix | Feature implementation, migration, API change |
111
-
112
- ### Concrete Examples
113
-
114
- | Task | Scope | Risk | Subtask? | Why |
115
- |------|-------|------|----------|-----|
116
- | Fix typo in CSS class | 1 file, 1 line | None | ✅ | Bounded, no security impact |
117
- | Add error handling to existing endpoint | 1 file, 5 lines | Low | ✅ | No schema change |
118
- | Implement login endpoint | 2+ files, 50+ lines | High (auth) | ❌ | Security-critical, needs Themis |
119
- | Database migration | 1 file, 15 lines | High (data) | ❌ | Data loss risk, needs rollback |
120
- | Apollo codebase search | 0 files | None | ✅ | Read-only investigation |
121
- | Update README | 1 file, 3 lines | None | ✅ | Documentation only |
72
+ ALL YES → direct/lightweight execution; no routine plan or artifact
73
+ ANY NO → add only the planning, artifact, specialist review, and approval gates the risk requires
74
+ ```
122
75
 
123
76
  ### Safety Rules
124
- 1. **Bounded scope** — single file or read-only investigation
77
+ 1. **Bounded scope** — can be a small multi-file change or a read-only investigation
125
78
  2. **Low risk** — no security implications, no data loss, no breaking changes
126
- 3. **No Themis dependency** — output doesn't feed into a phase that requires review
79
+ 3. **No required Themis dependency** — sensitive/material output retains its review gate
127
80
 
128
81
  ## Subtask Return Format
129
- Return the standard `## subtask_summary` defined in `## Agent Return Format` (files_changed, summary, tests, coverage, tokens, status, blockers).
82
+ Return the required `## subtask_summary` fields defined in `## Agent Return Format`; include `memory_context` when memory was used.
130
83
 
131
84
  ## Timeout Parcial (Partial Results)
132
85
 
@@ -5,12 +5,12 @@ Provides a confined execution environment for orchestration scripts
5
5
  via MCP tools and resources.
6
6
 
7
7
  Usage:
8
- python scripts/code_mode_server.py
8
+ python scripts/code_mode.py
9
9
 
10
10
  Or via MCP client (stdio transport):
11
11
  pantheon-code-mode:
12
12
  command: python
13
- args: ["scripts/code_mode_server.py"]
13
+ args: ["scripts/code_mode.py"]
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -20,6 +20,11 @@ import hashlib
20
20
  import json
21
21
  import logging
22
22
  import os
23
+
24
+ try:
25
+ import resource
26
+ except (ImportError, OSError, ValueError): # pragma: no cover - platform-specific
27
+ resource = None # type: ignore[assignment]
23
28
  import shutil
24
29
  import signal
25
30
  import stat
@@ -31,7 +36,7 @@ from typing import Any
31
36
 
32
37
  import yaml
33
38
  from _pantheon_paths import pantheon_home, pantheon_project
34
- from mcp.server.fastmcp import FastMCP
39
+ from fastmcp import FastMCP
35
40
 
36
41
  # ── Constants ─────────────────────────────────────────────────────────────────
37
42
  ALLOWED_EXTENSIONS: frozenset[str] = frozenset({".sh", ".py"})
@@ -125,6 +130,88 @@ def _build_script_env(
125
130
  return env
126
131
 
127
132
 
133
+ # Additional tasks a sandboxed script may create beyond the UID's current
134
+ # count. RLIMIT_NPROC is a per-real-UID limit on the *total* number of
135
+ # processes AND threads, so the sandbox floor must clear everything the host
136
+ # already owns before a script can fork at all. This headroom is the actual
137
+ # anti-fork-bomb budget: the script still cannot exceed current + headroom.
138
+ NPROC_HEADROOM: int = 64
139
+
140
+
141
+ def _uid_task_count() -> int | None:
142
+ """Count the tasks (processes + threads) this real UID already owns.
143
+
144
+ The kernel compares the post-fork total against RLIMIT_NPROC, so this is
145
+ the number the sandbox limit has to exceed. Returns *None* when the count
146
+ cannot be established (no readable ``/proc``), which makes the caller
147
+ fail open on ``--nproc`` rather than pin a value that may be too low and
148
+ break every script.
149
+
150
+ The tally is one too high per process: ``/proc/<pid>/task`` already lists
151
+ the thread-group leader, so the extra ``1`` counts it twice. That bias
152
+ only widens the derived limit (the fail-open direction), so it is left in
153
+ place deliberately.
154
+ """
155
+ uid = os.getuid()
156
+ count = 0
157
+ try:
158
+ pids = os.listdir("/proc")
159
+ except OSError:
160
+ return None
161
+ for pid in pids:
162
+ if not pid.isdigit():
163
+ continue
164
+ proc_dir = f"/proc/{pid}"
165
+ try:
166
+ if os.stat(proc_dir).st_uid != uid:
167
+ continue
168
+ # ``/proc/<pid>/task`` already includes the thread-group leader,
169
+ # so the extra 1 counts it twice (over-count by one per process).
170
+ count += 1 + len(os.listdir(f"{proc_dir}/task"))
171
+ except OSError:
172
+ # Process exited between listdir and stat, or is not ours to read.
173
+ continue
174
+ return count
175
+
176
+
177
+ def _sandbox_nproc() -> str | None:
178
+ """Return the ``--nproc`` value for a sandboxed script, or *None*.
179
+
180
+ Derived from the host rather than hardcoded: the value is the UID's
181
+ current task count plus ``NPROC_HEADROOM``, clamped to the hard limit
182
+ (``prlimit`` fails with EPERM above it, and an unprivileged process cannot
183
+ raise its soft limit past the hard one).
184
+
185
+ Returns *None* when the count or the limit cannot be read, in which case
186
+ the caller omits ``--nproc``. ``--as`` and ``--cpu`` still apply, so the
187
+ sandbox keeps its memory and CPU boundaries.
188
+ """
189
+ current = _uid_task_count()
190
+ if current is None:
191
+ _log.warning(
192
+ "code-mode: cannot count this UID's tasks; omitting --nproc, so "
193
+ "the sandboxed script's fork budget is unbounded"
194
+ )
195
+ return None
196
+ if resource is None:
197
+ _log.warning(
198
+ "code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
199
+ "sandboxed script's fork budget is unbounded"
200
+ )
201
+ return None
202
+ try:
203
+ _soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
204
+ except (OSError, ValueError):
205
+ _log.warning(
206
+ "code-mode: cannot read RLIMIT_NPROC; omitting --nproc, so the "
207
+ "sandboxed script's fork budget is unbounded"
208
+ )
209
+ return None
210
+ if hard == getattr(resource, "RLIM_INFINITY", -1):
211
+ return str(current + NPROC_HEADROOM)
212
+ return str(max(1, min(current + NPROC_HEADROOM, hard)))
213
+
214
+
128
215
  def _prlimit_prefix(
129
216
  prlimit_path: str | None,
130
217
  timeout_s: int = SCRIPT_TIMEOUT,
@@ -132,21 +219,34 @@ def _prlimit_prefix(
132
219
  """Return a ``prlimit`` command prefix to wrap a subprocess, or *None*.
133
220
 
134
221
  On Linux with ``prlimit`` available the prefix adds:
135
- --nproc=512 --as=1073741824 --cpu=<timeout_s + 5>
136
-
137
- On non-Linux or when *prlimit_path* is falsy, returns *None* (fail-open).
222
+ --nproc=<uid task count + NPROC_HEADROOM> --as=1073741824
223
+ --cpu=<timeout_s + 5>
224
+
225
+ ``--nproc`` is derived per invocation by :func:`_sandbox_nproc` rather
226
+ than hardcoded. RLIMIT_NPROC is a per-real-UID total across processes AND
227
+ threads, so a fixed value below the host's current usage makes the very
228
+ first ``fork()`` inside the sandbox fail with EAGAIN — which is exactly
229
+ what a hardcoded ``--nproc=512`` did on a host already running more than
230
+ 512 tasks. The isolation property is unchanged: a script still cannot
231
+ create more than ``NPROC_HEADROOM`` additional tasks.
232
+
233
+ On non-Linux, when *prlimit_path* is falsy, or when the nproc limit cannot
234
+ be derived, the flag is omitted (fail-open); ``--as``/``--cpu`` remain.
138
235
  """
139
236
  if not prlimit_path:
140
237
  return None
141
238
 
142
239
  cpu_limit = timeout_s + 5
143
- return [
144
- prlimit_path,
145
- "--nproc=512",
240
+ prefix = [prlimit_path]
241
+ nproc = _sandbox_nproc()
242
+ if nproc is not None:
243
+ prefix.append(f"--nproc={nproc}")
244
+ prefix += [
146
245
  "--as=1073741824", # 1 GiB in bytes
147
246
  f"--cpu={cpu_limit}",
148
247
  "--",
149
248
  ]
249
+ return prefix
150
250
 
151
251
 
152
252
  def _detect_prlimit() -> str | None:
@@ -226,7 +326,9 @@ def _packaged_scripts_dir() -> Path | None:
226
326
  return None
227
327
 
228
328
 
229
- def _resolve_scripts_dir(candidates: list[Path], *, allow_packaged: bool = True) -> Path | None:
329
+ def _resolve_scripts_dir(
330
+ candidates: list[Path], *, allow_packaged: bool = True
331
+ ) -> Path | None:
230
332
  """Select the first usable candidate, optionally trying the package copy."""
231
333
  for candidate in candidates:
232
334
  if _has_usable_scripts(candidate):
@@ -377,9 +479,7 @@ def _load_manifest(scripts_dir: Path | None = None) -> dict[str, str]:
377
479
  ) from None
378
480
 
379
481
  if not isinstance(data, dict):
380
- raise ManifestError(
381
- "CORRUPT_DATA", "Code-mode manifest must be a JSON object."
382
- )
482
+ raise ManifestError("CORRUPT_DATA", "Code-mode manifest must be a JSON object.")
383
483
 
384
484
  if data.get("version") != 1:
385
485
  raise ManifestError(
@@ -527,7 +627,6 @@ def _contract_result(
527
627
  return f"[{status}] {message}"
528
628
 
529
629
 
530
-
531
630
  def _format_output(
532
631
  stdout: str,
533
632
  stderr: str,
@@ -818,22 +917,15 @@ async def execute_code_script(
818
917
  timeout_s=timeout_s,
819
918
  status="TIMEOUT",
820
919
  )
821
- except FileNotFoundError:
822
- return _build_result(
823
- "",
824
- f"Script '{script_name}' not found or interpreter missing.",
825
- -1,
826
- timed_out=False,
827
- duration_ms=0,
828
- metadata=metadata,
829
- json_output=json_output,
830
- timeout_s=timeout_s,
831
- status="UNAVAILABLE",
832
- )
833
920
  except OSError as e:
921
+ error = (
922
+ f"Script '{script_name}' not found or interpreter missing."
923
+ if isinstance(e, FileNotFoundError)
924
+ else f"Failed to execute script: {e}"
925
+ )
834
926
  return _build_result(
835
927
  "",
836
- f"Failed to execute script: {e}",
928
+ error,
837
929
  -1,
838
930
  timed_out=False,
839
931
  duration_ms=0,
@@ -2,7 +2,7 @@
2
2
  """Plugin eval certification storage — ``plugin_eval`` memory namespace.
3
3
 
4
4
  Thin stdlib-only wrapper around the memory SQLite DB (same schema as
5
- ``src/mcp/memory_mcp_server.py``) for plugin-eval certification results.
5
+ ``src/mcp/memory_mcp.py``) for plugin-eval certification results.
6
6
 
7
7
  Namespace: ``plugin_eval``
8
8
  Key format: ``eval:<name>:<date>`` (e.g. ``eval:tdd-with-agents:2026-08-21``)
@@ -30,7 +30,7 @@ from typing import Any
30
30
 
31
31
  NAMESPACE = "plugin_eval"
32
32
 
33
- # Minimal schema — matches memory_mcp_server.SCHEMA_SQL for the memories
33
+ # Minimal schema — matches memory_mcp.SCHEMA_SQL for the memories
34
34
  # table (vec/FTS tables are initialized by the memory MCP server on first
35
35
  # run; eval entries remain readable via list/recall regardless).
36
36
  _SCHEMA_SQL = """