okstra 0.202.0 → 0.205.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 (263) hide show
  1. package/README.md +7 -6
  2. package/dist/cli-registry.mjs +7 -7
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/lifecycle/install.mjs +50 -124
  5. package/dist/commands/lifecycle/install.mjs.map +1 -1
  6. package/dist/commands/memory/memory.mjs +41 -8
  7. package/dist/commands/memory/memory.mjs.map +1 -1
  8. package/dist/lib/install-assets.mjs +3 -0
  9. package/dist/lib/install-assets.mjs.map +1 -1
  10. package/dist/lib/runtime-manifest.mjs +2 -1
  11. package/dist/lib/runtime-manifest.mjs.map +1 -1
  12. package/dist/lib/types.d.mts +2 -1
  13. package/docs/architecture/storage-model.md +14 -11
  14. package/docs/architecture.md +26 -20
  15. package/docs/cli.md +15 -12
  16. package/docs/contributor-change-matrix.md +3 -2
  17. package/docs/performance-improvement-plan-v2.md +3 -9
  18. package/docs/project-structure-overview.md +39 -11
  19. package/docs/task-process/README.md +1 -1
  20. package/docs/task-process/common-flow.md +1 -1
  21. package/docs/task-process/final-verification.md +3 -1
  22. package/docs/task-process/implementation-option-selection.md +1 -1
  23. package/docs/task-process/implementation.md +1 -1
  24. package/docs/task-process/release-handoff.md +36 -39
  25. package/package.json +1 -2
  26. package/runtime/BUILD.json +2 -2
  27. package/runtime/agents/common.json +28 -0
  28. package/runtime/agents/operations/code-review.json +6 -0
  29. package/runtime/agents/operations/report-translation.json +6 -0
  30. package/runtime/agents/operations/schedule-verification.json +6 -0
  31. package/runtime/agents/roles/analyser.json +18 -0
  32. package/runtime/agents/roles/critic.json +18 -0
  33. package/runtime/agents/roles/designer.json +18 -0
  34. package/runtime/agents/roles/implementer.json +20 -0
  35. package/runtime/agents/roles/leader.json +20 -0
  36. package/runtime/agents/roles/planner.json +18 -0
  37. package/runtime/agents/roles/report-writer.json +19 -0
  38. package/runtime/agents/roles/translator.json +19 -0
  39. package/runtime/agents/roles/verifier.json +18 -0
  40. package/runtime/bin/lib/okstra/usage.sh +5 -5
  41. package/runtime/prompts/duties/acceptance-critic.json +32 -0
  42. package/runtime/prompts/duties/acceptance-verifier.json +32 -0
  43. package/runtime/prompts/duties/analysis-worker.json +32 -0
  44. package/runtime/prompts/duties/code-reviewer.json +32 -0
  45. package/runtime/prompts/duties/diagnosis-worker.json +32 -0
  46. package/runtime/prompts/duties/direction-selection-worker.json +32 -0
  47. package/runtime/prompts/duties/discovery-worker.json +32 -0
  48. package/runtime/prompts/duties/implementation-executor.json +32 -0
  49. package/runtime/prompts/duties/implementation-verifier.json +32 -0
  50. package/runtime/prompts/duties/lead.json +32 -0
  51. package/runtime/prompts/duties/planning-worker.json +36 -0
  52. package/runtime/prompts/duties/report-writer.json +32 -0
  53. package/runtime/prompts/duties/reverification-worker.json +32 -0
  54. package/runtime/prompts/duties/schedule-verifier.json +32 -0
  55. package/runtime/prompts/duties/scope-critic.json +32 -0
  56. package/runtime/prompts/duties/technical-verification-worker.json +32 -0
  57. package/runtime/prompts/duties/translator.json +32 -0
  58. package/runtime/prompts/launch.template.md +2 -1
  59. package/runtime/prompts/lead/adapters/cmux.md +1 -1
  60. package/runtime/prompts/lead/convergence.md +4 -4
  61. package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
  62. package/runtime/prompts/lead/plan-body-verification.md +6 -6
  63. package/runtime/prompts/lead/report-writer.md +3 -3
  64. package/runtime/prompts/profiles/_common-contract.md +2 -2
  65. package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
  66. package/runtime/prompts/profiles/_implementation-executor.md +4 -1
  67. package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
  68. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  69. package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
  70. package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
  71. package/runtime/prompts/profiles/error-analysis.json +39 -0
  72. package/runtime/prompts/profiles/error-analysis.md +0 -25
  73. package/runtime/prompts/profiles/feature-analysis.json +31 -0
  74. package/runtime/prompts/profiles/feature-analysis.md +0 -20
  75. package/runtime/prompts/profiles/final-verification.json +30 -0
  76. package/runtime/prompts/profiles/final-verification.md +4 -23
  77. package/runtime/prompts/profiles/forbidden-actions.json +4 -3
  78. package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
  79. package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
  80. package/runtime/prompts/profiles/implementation-planning.json +40 -0
  81. package/runtime/prompts/profiles/implementation-planning.md +4 -29
  82. package/runtime/prompts/profiles/implementation.json +30 -0
  83. package/runtime/prompts/profiles/implementation.md +1 -20
  84. package/runtime/prompts/profiles/improvement-discovery.json +31 -0
  85. package/runtime/prompts/profiles/improvement-discovery.md +0 -20
  86. package/runtime/prompts/profiles/project-analysis.json +31 -0
  87. package/runtime/prompts/profiles/project-analysis.md +0 -20
  88. package/runtime/prompts/profiles/release-handoff.json +5 -0
  89. package/runtime/prompts/profiles/release-handoff.md +74 -74
  90. package/runtime/prompts/profiles/requirements-discovery.json +39 -0
  91. package/runtime/prompts/profiles/requirements-discovery.md +0 -25
  92. package/runtime/prompts/profiles/technical-verification.json +39 -0
  93. package/runtime/prompts/profiles/technical-verification.md +0 -25
  94. package/runtime/prompts/wizard/prompts.ko.json +14 -18
  95. package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
  96. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
  97. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
  98. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
  99. package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
  100. package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
  101. package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
  102. package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
  103. package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
  104. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
  105. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
  106. package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
  107. package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
  108. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
  109. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
  110. package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
  111. package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
  112. package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
  113. package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
  114. package/runtime/python/okstra_ctl/cmux.py +69 -20
  115. package/runtime/python/okstra_ctl/code_review_target.py +16 -8
  116. package/runtime/python/okstra_ctl/conformance.py +43 -0
  117. package/runtime/python/okstra_ctl/consumers.py +23 -8
  118. package/runtime/python/okstra_ctl/container.py +31 -8
  119. package/runtime/python/okstra_ctl/context_cost.py +11 -15
  120. package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
  121. package/runtime/python/okstra_ctl/convergence_engine.py +38 -0
  122. package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
  123. package/runtime/python/okstra_ctl/design_prep.py +34 -1
  124. package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
  125. package/runtime/python/okstra_ctl/domain/host.py +5 -0
  126. package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
  127. package/runtime/python/okstra_ctl/error_report.py +4 -3
  128. package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
  129. package/runtime/python/okstra_ctl/handoff.py +384 -286
  130. package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
  131. package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
  132. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
  133. package/runtime/python/okstra_ctl/lead_progress.py +1 -1
  134. package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
  135. package/runtime/python/okstra_ctl/manager_cli.py +92 -4
  136. package/runtime/python/okstra_ctl/manager_launch.py +1 -1
  137. package/runtime/python/okstra_ctl/manager_paths.py +14 -3
  138. package/runtime/python/okstra_ctl/manager_store.py +210 -3
  139. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  140. package/runtime/python/okstra_ctl/manager_view.py +2 -1
  141. package/runtime/python/okstra_ctl/model_discovery.py +30 -0
  142. package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
  143. package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
  144. package/runtime/python/okstra_ctl/models.py +1 -1
  145. package/runtime/python/okstra_ctl/next_phase.py +16 -6
  146. package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
  147. package/runtime/python/okstra_ctl/option_comparison.py +168 -0
  148. package/runtime/python/okstra_ctl/path_hints.py +9 -0
  149. package/runtime/python/okstra_ctl/paths.py +3 -0
  150. package/runtime/python/okstra_ctl/profile_show.py +42 -1
  151. package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
  152. package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
  153. package/runtime/python/okstra_ctl/render.py +79 -0
  154. package/runtime/python/okstra_ctl/report_contract.py +1 -1
  155. package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
  156. package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
  157. package/runtime/python/okstra_ctl/report_translation.py +2 -1
  158. package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
  159. package/runtime/python/okstra_ctl/role_requirements.py +142 -129
  160. package/runtime/python/okstra_ctl/rollup.py +3 -1
  161. package/runtime/python/okstra_ctl/run.py +76 -29
  162. package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
  163. package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
  164. package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
  165. package/runtime/python/okstra_ctl/stage_map.py +16 -2
  166. package/runtime/python/okstra_ctl/stage_targets.py +209 -43
  167. package/runtime/python/okstra_ctl/team.py +22 -13
  168. package/runtime/python/okstra_ctl/time_report.py +2 -1
  169. package/runtime/python/okstra_ctl/usage_report.py +3 -1
  170. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
  171. package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
  172. package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
  173. package/runtime/python/okstra_ctl/wizard/state.py +3 -5
  174. package/runtime/python/okstra_ctl/wizard/steps_plan.py +12 -21
  175. package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
  176. package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
  177. package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
  178. package/runtime/python/okstra_ctl/workflow.py +1 -1
  179. package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
  180. package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
  181. package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
  182. package/runtime/python/okstra_token_usage/pricing.py +6 -4
  183. package/runtime/schemas/agent-common-v1.schema.json +34 -0
  184. package/runtime/schemas/agent-duty-v1.schema.json +38 -0
  185. package/runtime/schemas/agent-operation-v1.schema.json +11 -0
  186. package/runtime/schemas/agent-profile-v1.schema.json +46 -0
  187. package/runtime/schemas/agent-role-v1.schema.json +29 -0
  188. package/runtime/schemas/final-report-v2.0.schema.json +118 -97
  189. package/runtime/schemas/final-report-v3.0.schema.json +118 -97
  190. package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
  191. package/runtime/skills/okstra-chat/SKILL.md +2 -2
  192. package/runtime/skills/okstra-code-review/SKILL.md +23 -9
  193. package/runtime/skills/okstra-container-build/SKILL.md +10 -10
  194. package/runtime/skills/okstra-inspect/SKILL.md +1 -1
  195. package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
  196. package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
  197. package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
  198. package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
  199. package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
  200. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  201. package/runtime/skills/okstra-inspect/facets/status.md +4 -3
  202. package/runtime/skills/okstra-inspect/facets/time.md +11 -10
  203. package/runtime/skills/okstra-manager/SKILL.md +18 -2
  204. package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
  205. package/runtime/skills/okstra-rollup/SKILL.md +5 -5
  206. package/runtime/skills/okstra-run/SKILL.md +31 -12
  207. package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
  208. package/runtime/skills/okstra-setup/SKILL.md +12 -10
  209. package/runtime/skills/okstra-setup/references/project-config.md +7 -6
  210. package/runtime/skills/okstra-usage/SKILL.md +1 -1
  211. package/runtime/skills/okstra-user-response/SKILL.md +1 -1
  212. package/runtime/templates/manager/view.template.html +1 -0
  213. package/runtime/templates/report-writer-prompt-preamble.md +8 -0
  214. package/runtime/templates/reports/brief.template.md +14 -4
  215. package/runtime/templates/reports/html/i18n/en.json +5 -4
  216. package/runtime/templates/reports/html/i18n/ko.json +5 -4
  217. package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
  218. package/runtime/templates/reports/i18n/en.json +1 -1
  219. package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
  220. package/runtime/templates/reports/release-handoff-input.template.md +6 -4
  221. package/runtime/templates/translator-prompt-preamble.md +36 -0
  222. package/runtime/validators/checks/validate-assets-01.py +7 -8
  223. package/runtime/validators/validate-brief.py +70 -0
  224. package/runtime/validators/validate-implementation-plan-stages.py +2 -1
  225. package/runtime/validators/validate-run.py +72 -15
  226. package/runtime/validators/validate-schedule.py +9 -0
  227. package/docs/for-ai/README.md +0 -68
  228. package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
  229. package/docs/for-ai/skills/okstra-chat.md +0 -34
  230. package/docs/for-ai/skills/okstra-code-review.md +0 -57
  231. package/docs/for-ai/skills/okstra-container-build.md +0 -129
  232. package/docs/for-ai/skills/okstra-inspect.md +0 -262
  233. package/docs/for-ai/skills/okstra-manager.md +0 -86
  234. package/docs/for-ai/skills/okstra-memory.md +0 -126
  235. package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
  236. package/docs/for-ai/skills/okstra-rollup.md +0 -114
  237. package/docs/for-ai/skills/okstra-run.md +0 -250
  238. package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
  239. package/docs/for-ai/skills/okstra-setup.md +0 -167
  240. package/docs/for-ai/skills/okstra-usage.md +0 -29
  241. package/docs/for-ai/skills/okstra-user-response.md +0 -72
  242. package/runtime/agents/workers/claude-worker.md +0 -128
  243. package/runtime/agents/workers/report-writer-worker.md +0 -37
  244. package/runtime/agents/workers/translator-worker.md +0 -63
  245. package/runtime/prompts/duties/acceptance-critic.md +0 -44
  246. package/runtime/prompts/duties/acceptance-verifier.md +0 -44
  247. package/runtime/prompts/duties/analysis-worker.md +0 -44
  248. package/runtime/prompts/duties/code-reviewer.md +0 -44
  249. package/runtime/prompts/duties/common.md +0 -39
  250. package/runtime/prompts/duties/diagnosis-worker.md +0 -44
  251. package/runtime/prompts/duties/direction-selection-worker.md +0 -44
  252. package/runtime/prompts/duties/discovery-worker.md +0 -44
  253. package/runtime/prompts/duties/implementation-executor.md +0 -44
  254. package/runtime/prompts/duties/implementation-verifier.md +0 -44
  255. package/runtime/prompts/duties/lead.md +0 -44
  256. package/runtime/prompts/duties/planning-worker.md +0 -52
  257. package/runtime/prompts/duties/report-writer.md +0 -44
  258. package/runtime/prompts/duties/reverification-worker.md +0 -44
  259. package/runtime/prompts/duties/schedule-verifier.md +0 -44
  260. package/runtime/prompts/duties/scope-critic.md +0 -44
  261. package/runtime/prompts/duties/technical-verification-worker.md +0 -44
  262. package/runtime/prompts/duties/translator.md +0 -44
  263. package/runtime/python/okstra_ctl/pane_title.py +0 -154
@@ -0,0 +1,108 @@
1
+ # Claude Code Worker Session Contract
2
+
3
+ This file is the host contract for a worker running **inside** a Claude Code
4
+ session (`runner=native-session`). The dispatch prompt names it as
5
+ `**Host Session Contract Path:**`. It owns only what is true because the worker
6
+ shares the lead's host session; the duty contract owns the role, the selected
7
+ preamble owns the audience procedure, and the task instructions own this run's
8
+ inputs and outputs. A `runner=cli-wrapper` worker runs as its own provider
9
+ process and never receives this file.
10
+
11
+ ## Dispatch verification
12
+
13
+ The summon message carried the absolute path of your dispatch prompt document,
14
+ not the prompt itself. You have already read that document; these checks confirm
15
+ you read the right one.
16
+
17
+ 1. Extract the absolute `Project Root` (a line starting with `**Project Root:**`
18
+ or `Project Root:`). If it is absent, return
19
+ `CLAUDE_PROJECT_ROOT_MISSING: absolute Project Root was not provided in the lead prompt`
20
+ and do nothing else.
21
+ 2. Check that the path you were summoned with matches the document's
22
+ `Assigned worker prompt history path:` header, resolving a relative header
23
+ value against `Project Root`. On a mismatch, return
24
+ `CLAUDE_PROMPT_PATH_MISSING: summon path does not match the document's assigned prompt history path`.
25
+ The lead persisted and verified that document before dispatch, so never
26
+ rewrite it.
27
+
28
+ ## Working directory and shell
29
+
30
+ Anchor every file operation to the absolute `Project Root`. Use absolute paths;
31
+ do not rely on inherited cwd, and never use `cd` to move the session.
32
+
33
+ - **Executor exception (implementation runs).** When you are the `Executor` and
34
+ the prompt provides an `EXECUTOR_WORKTREE_PATH` that differs from the
35
+ session's cwd, prefix cwd-sensitive Bash commands (`cargo *`, `npm *`,
36
+ `pnpm *`, `bun *`, `pytest`, `make *`, `go *`, and other toolchain
37
+ build/test commands) with `cd <EXECUTOR_WORKTREE_PATH> && ` in the same Bash
38
+ invocation. Pass the chained command directly to the Bash tool; do not wrap it
39
+ in `bash -lc "..."` or `bash -c "..."`, because the leading `cd` token has to
40
+ stay visible to the permission layer. The `cd` is scoped to that one subshell
41
+ and does not move the session, which is why it does not contradict the rule
42
+ above.
43
+ - **Verifier QA-gate exception.** A verifier may use the same
44
+ `cd <WORKTREE> && <cmd>` shape for the project-declared `qaCommands` (lint,
45
+ format, typecheck, test) from `project.json`, since those are cwd-sensitive by
46
+ nature. Outside the QA gate a verifier still reads by absolute path only.
47
+ - **No chaining beyond `cd && cmd`.** The permission matcher allows exactly the
48
+ two-segment shape. Appending a pipe, semicolon, redirect, or second `&&` — for
49
+ example `cd ... && cargo test ... 2>&1 | tail -20; echo "exit:$?"` —
50
+ disqualifies the prefix match and raises a permission prompt on every
51
+ dispatch. Let the host capture stdout, stderr, and the exit code natively. If
52
+ you genuinely need less output, run the command and read the result in a
53
+ separate call.
54
+ - **Commands must not be able to prompt.** Your Bash calls see the user's own
55
+ shell, where `cp`, `mv`, and `rm` are commonly aliased to their `-i` form. The
56
+ confirmation that alias raises has nobody to answer it, and the dispatch hangs
57
+ until it is killed. Invoke these as `command cp` / `command mv` /
58
+ `command rm`, which skips alias expansion. Do not reach for `-f` instead: it
59
+ also changes what the tool does on failure, and `rm -f` reports success on a
60
+ path that never existed.
61
+
62
+ ## MCP
63
+
64
+ The run's available MCP servers and tools are listed in the analysis packet's
65
+ `Available MCP Servers` section. When that section is absent or says none, treat
66
+ MCP as unavailable for this run and never infer tools from host configuration.
67
+ Call a listed tool directly by name (`mcp__<server>__<tool>`); do not shell out
68
+ via `claude --mcp-cli call ...` and do not run a tool name as a Bash command.
69
+ When a server you need is not listed, record `MCP not available for this run` in
70
+ your output rather than guessing a tool name.
71
+
72
+ ## Post-write gates (implementation executor)
73
+
74
+ An executor prompt carries two gate blocks the lead appends after the coding
75
+ preflight: `Pre-commit diff review sweep` and `Implementation self-check`.
76
+ Execute both and record their coverage lines exactly as the blocks specify. The
77
+ provider wrappers refuse to launch an executor prompt that lacks them
78
+ (`*_POSTWRITE_GATE_MISSING`); a worker in this session has no wrapper, so the
79
+ contract lands on you. If either block is missing, record a typed `tool-failure`
80
+ through `okstra error-log append-observed` and tell the lead to re-dispatch with
81
+ the blocks included.
82
+
83
+ ## Returning
84
+
85
+ The lead's dispatch returns as soon as the session is spawned and does not block
86
+ on you. It detects completion by polling your result file, so that file's
87
+ appearance at `**Result Path:**` is the only completion signal it has. Lingering
88
+ after the file is on disk extends the phase's wall-clock time for the whole run.
89
+
90
+ After the write succeeds:
91
+
92
+ 1. Return your final message immediately, beginning with your model identity as
93
+ the selected preamble specifies.
94
+ 2. Do not perform further `Read`, `Grep`, `Glob`, or MCP calls, and do not
95
+ re-review your own output.
96
+ 3. Do not rewrite the result file. If a correction is genuinely required, make a
97
+ single `Edit` and return.
98
+ 4. The one exception is recording a `tool-failure` when the failure happened
99
+ after the write; return immediately after that command.
100
+
101
+ **Enforced:** `validators/validate-run.py` `validate_team_state` fails a run
102
+ whose worker carries a terminal status with no saved result file at its assigned
103
+ Result Path.
104
+
105
+ If you catch yourself thinking "let me double-check section 3" after the write
106
+ succeeded, stop. Convergence and the report writer reconcile gaps across all
107
+ workers; extra depth in one worker at the cost of returning late is a net loss
108
+ for the run.
@@ -205,6 +205,7 @@ The worker's `--sandbox danger-full-access` flag only selects the child Codex po
205
205
  - For convergence reverify, consume the persisted round plan exactly. This adapter may map and transport each returned batch, but it cannot change batch membership and does not classify findings or branch on task type, provider, or model identity.
206
206
  - Do not invoke Claude Code team or subagent tools.
207
207
  - The prepared run manifest and team-state are the dispatch authority. A `runner=native-session` assignment stays in the current Codex host; a `runner=cli-wrapper` assignment uses the registered provider wrapper. Unsupported explicitly requested workers fail; an adapter must not silently change the roster.
208
+ - This host declares no worker session contract. A `runner=native-session` worker therefore receives no host session rules beyond its duty contract, the selected preamble, and the task instructions; the dispatch prompt carries no `**Host Session Contract Path:**` header. Declaring one means adding `workerSessionContract` to this adapter's `manifest.json` and descriptor — it is never installed into a host-global discovery path.
208
209
  - The report-writer follows its persisted provider, model, and runner assignment exactly. It has no Codex-only provider override or separate opt-in gate.
209
210
  - Reverify and critic retries invoke a fresh worker attempt and persist the core-supplied `dispatchKind` (`reverify-r<N>` or `critic`) in the dispatch record; never reuse a prior rollout as a new vote.
210
211
  - Native calls use only `hostModelValue`. Immediately before the host primitive, run `okstra agent-prompt record-dispatch` with the project root, run manifest, verified metadata path, and `--enforcement-mode host-native-spec-link-gate`; after the Result Path exists, run `okstra agent-prompt link-result` with `--dispatch-id <invocationId>:attempt-1` and that path before accepting it. CLI calls use only `modelExecutionValue` through `worker-dispatch`, which records its own dispatch. The native linkage proves association with a verified specification, not observed prompt delivery.
@@ -161,6 +161,8 @@ For a `host-text` mapping, render each numbered item as its option label followe
161
161
  | `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Use `okstra lead-progress append --phase <phase-id>` for a checkpoint and `okstra agent-activity append --kind <kind>` for an activity record; both resolve the ledger path from the run manifest. Emit the matching `PROGRESS:` line — the command prints it as `progressLine` — and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
162
162
  | `collect_usage` | Return explicit unavailable lead usage until Grok registers a session transcript or CLI usage artifact contract. |
163
163
 
164
+ - This host declares no worker session contract. A `runner=native-session` worker therefore receives no host session rules beyond its duty contract, the selected preamble, and the task instructions; the dispatch prompt carries no `**Host Session Contract Path:**` header. Declaring one means adding `workerSessionContract` to this adapter's `manifest.json` and descriptor — it is never installed into a host-global discovery path.
165
+
164
166
  ## Completion, cleanup, and resume
165
167
 
166
168
  - Do not infer the current host from an installed `grok` executable. The runtime must come from an explicit request or current-session declaration.
@@ -83,6 +83,8 @@ Render every numbered item as its option label followed by its description verba
83
83
  | `record_lead_event` | Append progress and activity records to the manifest-provided `leadEventsPath`. Use `okstra lead-progress append --phase <phase-id>` for a checkpoint and `okstra agent-activity append --kind <kind>` for an activity record; both resolve the ledger path from the run manifest. Emit the matching `PROGRESS:` line — the command prints it as `progressLine` — and, when an activity record is required, the immediately following `ACTIVITY:` line from the same structured fields. |
84
84
  | `collect_usage` | Return explicit unavailable lead usage until Kimi registers a session transcript or CLI usage artifact contract. |
85
85
 
86
+ - This host declares no worker session contract. A `runner=native-session` worker therefore receives no host session rules beyond its duty contract, the selected preamble, and the task instructions; the dispatch prompt carries no `**Host Session Contract Path:**` header. Declaring one means adding `workerSessionContract` to this adapter's `manifest.json` and descriptor — it is never installed into a host-global discovery path.
87
+
86
88
  ## Completion, cleanup, and resume
87
89
 
88
90
  - Do not infer the current host from an installed `kimi` executable. The runtime must come from an explicit request or current-session declaration.
@@ -33,9 +33,16 @@ ANTIGRAVITY = {
33
33
  "gemini-3.1-pro", "Gemini 3.1 Pro", "gemini-3.1-pro",
34
34
  aliases=("gemini 3.1 pro",),
35
35
  ),
36
+ "gemini-3.8-flash": ModelSpec(
37
+ "gemini-3.8-flash", "Gemini 3.8 Flash", "gemini-3.8-flash",
38
+ aliases=("gemini 3.8 flash",),
39
+ ),
40
+ # picker 에서는 감춘다: Flash 계열의 최신은 3.8 이다(`agy models` 2026-09-23
41
+ # 확인). 엔트리는 남긴다 — 이 모델로 디스패치된 과거 task 의 서빙 모델
42
+ # 관측값이 카탈로그에서 자기 행을 찾아야 게이트를 통과한다.
36
43
  "gemini-3.7-flash": ModelSpec(
37
44
  "gemini-3.7-flash", "Gemini 3.7 Flash", "gemini-3.7-flash",
38
- aliases=("gemini 3.7 flash",),
45
+ aliases=("gemini 3.7 flash",), selectable=False,
39
46
  ),
40
47
  }
41
48
 
@@ -41,6 +41,14 @@ CLAUDE = {
41
41
  "opus-5", "opus-5", "claude-opus-5", aliases=("claude-opus-5",),
42
42
  channel_family="opus", selectable=False,
43
43
  ),
44
+ # opus 채널이 지금 서빙하는 점-릴리스. 엔트리가 없으면 served-model 게이트가
45
+ # 관측값을 카탈로그에서 못 찾아 dispatch 가 exit 78 로 실패한다 — 실측
46
+ # 2026-09-23, 두 프로젝트에서 `claude-opus-5-5` 로 같은 실패(dev-10860
47
+ # stage-3·4·5). haiku-4-5-20251001 과 같은 이유의 엔트리다.
48
+ "opus-5-5": ModelSpec(
49
+ "opus-5-5", "opus-5-5", "claude-opus-5-5", aliases=("claude-opus-5-5",),
50
+ channel_family="opus", selectable=False,
51
+ ),
44
52
  "sonnet": ModelSpec(
45
53
  "sonnet", "sonnet", "sonnet", version_kind="channel", channel_family="sonnet"
46
54
  ),
@@ -17,9 +17,21 @@ import okstra_ctl.model_discovery as model_discovery
17
17
 
18
18
 
19
19
  # 모델 식별자와 순서는 Codex의 `~/.codex/models_cache.json`을 따른다
20
- # (2026-09-07 확인, 클라이언트 0.153.4): astra / sol / terra / luna.
21
- # `gpt-5.6` is not a slug that catalog offers at all, so it is gone from here;
22
- # its rate moved to `_LEGACY_CODEX_PRICING` so past runs still price.
20
+ # (2026-09-23 확인, 클라이언트 0.155.0): gpt-6 astra / sol / luna 와 5.6 계열의
21
+ # sol / terra / luna. `gpt-5.6` is not a slug that catalog offers at all, so it
22
+ # is gone from here; its rate moved to `_LEGACY_CODEX_PRICING` so past runs
23
+ # still price.
24
+ #
25
+ # 노출 규칙: 티어(astra·sol·terra·luna)마다, **이 계정이 실제로 서빙하는** 최신
26
+ # 세대 하나만 selectable 로 둔다. 카탈로그가 슬러그를 내놓는 것과 계정이 그것을
27
+ # 실행하는 것은 다르다 — 실측 2026-09-23(jobs dev-10860
28
+ # implementation-option-selection-002): ChatGPT 계정으로 로그인한 이 기계에서
29
+ # `gpt-6-sol` 디스패치가 두 번 다 400 으로 거절됐다
30
+ # ("The 'gpt-6-sol' model is not supported when using Codex with a ChatGPT
31
+ # account"). 같은 계정의 `models_cache.json` 도 6세대로는 astra 만 싣는다.
32
+ # 그래서 sol·luna 티어는 5.6 행이 선택 가능한 행이고, gpt-6 의 두 행은 엔트리만
33
+ # 남긴다 — 서빙하는 계정이 그 값을 쓰거나 과거 run 을 정산할 때 필요하다.
34
+ # astra 는 이 계정에서 실제로 돌아 gpt-6 행이 선택 가능하다.
23
35
  CODEX = {
24
36
  # 비용은 계정이 구독이든 API 든 공개 API 단가(입력·캐시 입력·출력 USD/1M)로
25
37
  # 추정한다 — 리포트가 답하는 것은 "얼마나 썼는가" 이지 "청구서에 얼마가
@@ -28,12 +40,17 @@ CODEX = {
28
40
  # 계상된다(실측 2026-09-08). 출처: OpenAI 표준 등급, 2026-09 기준
29
41
  # (morphllm.com/openai-api-pricing, cloudzero.com/blog/openai-pricing,
30
42
  # layer3labs.io/guides/gpt-6-astra-api-pricing).
43
+ # gpt-6 단가는 OpenAI 모델 문서의 표준 등급(2026-09-23 확인):
44
+ # developers.openai.com/api/docs/models/gpt-6-sol, .../gpt-6-luna.
31
45
  "gpt-6-astra": ModelSpec("gpt-6-astra", "gpt-6-astra", "gpt-6-astra", pricing=(10.0, 1.0, 50.0)),
32
- "gpt-5.6-sol": ModelSpec("gpt-5.6-sol", "gpt-5.6-sol", "gpt-5.6-sol", pricing=(5.0, 0.50, 30.0)),
46
+ # picker 에서는 감춘다: 이 계정이 400 으로 거절한다(위 노출 규칙). 엔트리는
47
+ # 남긴다 — 서빙하는 계정의 값이고, 과거 run 의 단가도 이 표에서 찾는다.
48
+ "gpt-6-sol": ModelSpec("gpt-6-sol", "gpt-6-sol", "gpt-6-sol", pricing=(2.0, 0.20, 10.0), selectable=False),
49
+ "gpt-6-luna": ModelSpec("gpt-6-luna", "gpt-6-luna", "gpt-6-luna", pricing=(0.10, 0.01, 0.50), selectable=False),
33
50
  "gpt-5.6-terra": ModelSpec("gpt-5.6-terra", "gpt-5.6-terra", "gpt-5.6-terra", pricing=(2.0, 0.20, 12.0)),
51
+ "gpt-5.6-sol": ModelSpec("gpt-5.6-sol", "gpt-5.6-sol", "gpt-5.6-sol", pricing=(5.0, 0.50, 30.0)),
34
52
  "gpt-5.6-luna": ModelSpec("gpt-5.6-luna", "gpt-5.6-luna", "gpt-5.6-luna", pricing=(0.20, 0.02, 1.20)),
35
- # picker 에서는 감춘다. 엔트리는 남긴다 — 과거 run 의 토큰 사용량을
36
- # 정산할 때 pricing 을 이 표에서 찾는다.
53
+ # picker 에서는 감춘다(사유는 위와 동일 — 더 이상 카탈로그가 내놓지 않는다).
37
54
  "gpt-5.4-mini": ModelSpec("gpt-5.4-mini", "gpt-5.4-mini", "gpt-5.4-mini", pricing=(0.75, 0.075, 4.50), selectable=False),
38
55
  "codex-auto-review": ModelSpec("codex-auto-review", "codex-auto-review", "codex-auto-review", selectable=False),
39
56
  }
@@ -21,7 +21,11 @@ from okstra_ctl.domain.worker_stream import StreamEvent, Text, ToolCall, ToolRes
21
21
 
22
22
 
23
23
  GROK = {
24
- "grok-4.6": ModelSpec("grok-4.6", "grok-4.6", "grok-4.6", pricing=(2.00, 0.50, 6.00)),
24
+ "grok-4.7": ModelSpec("grok-4.7", "grok-4.7", "grok-4.7", pricing=(2.00, 0.50, 6.00)),
25
+ # picker 에서는 감춘다: grok-4.7 이 같은 계열의 최신이다(`grok` CLI 1.0.40 의
26
+ # 기본값도 4.7, 2026-09-23 확인). 엔트리는 남긴다 — 과거 run 의 정산이 이
27
+ # 표의 단가를 읽고, 서빙 모델 게이트가 옛 관측값을 여기서 찾는다.
28
+ "grok-4.6": ModelSpec("grok-4.6", "grok-4.6", "grok-4.6", pricing=(2.00, 0.50, 6.00), selectable=False),
25
29
  # picker 에서는 감춘다(사유는 codex 의 gpt-5.4-mini 와 동일).
26
30
  "grok-build-0.1": ModelSpec("grok-build-0.1", "grok-build-0.1", "grok-build-0.1", pricing=(1.00, 0.20, 2.00), selectable=False),
27
31
  }
@@ -294,7 +298,7 @@ def create_provider() -> ProviderSpec:
294
298
  display_label="Grok",
295
299
  models=GROK,
296
300
  default_models={
297
- role: "grok-4.6"
301
+ role: "grok-4.7"
298
302
  for role in (
299
303
  "lead",
300
304
  "analyser",
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  from pathlib import PurePosixPath
13
13
  import re
14
14
  import tempfile
15
- from typing import Iterator, Literal, Mapping, get_args
15
+ from typing import Any, Iterator, Literal, Mapping, get_args
16
16
 
17
17
  from ..domain.role import RoleCatalogError, role_for_duty
18
18
  from ..json_boundary import (
@@ -44,34 +44,6 @@ AgentAudience = Literal[
44
44
  ]
45
45
 
46
46
  _SUPPORTED_AUDIENCES = frozenset(get_args(AgentAudience))
47
- # The section set IS the duty contract's shape: a duty author adding a role file
48
- # reads these names, and `_validate_duty_sections` refuses a file that misses one.
49
- # The check is structural — it proves every required section exists and carries
50
- # text, NOT that the text is a real contract. A one-line placeholder passes it;
51
- # what keeps a section substantive is review, and the rule that earns a section a
52
- # place here at all: a sentence that reads the same in another duty file belongs
53
- # in `common.md` or nowhere.
54
- COMMON_DUTY_SECTIONS = (
55
- "Assignment fidelity",
56
- "Required inputs",
57
- "Evidence first",
58
- "Authority and scope",
59
- "Collaboration and independence",
60
- "Instruction precedence",
61
- "Conflict handling",
62
- "Completion honesty",
63
- )
64
- ROLE_DUTY_SECTIONS = (
65
- "Responsibility",
66
- "Required conduct",
67
- "Decision principles",
68
- "Authority and boundaries",
69
- "Evidence standard",
70
- "Collaboration contract",
71
- "Completion criteria",
72
- "Forbidden conduct",
73
- "Blocked-state reporting",
74
- )
75
47
  _SLUG_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
76
48
  _DUTY_SECTION_RE = re.compile(r"(?m)^## ([^\n]+)\s*$")
77
49
  _TOP_LEVEL_KEYS = {
@@ -144,6 +116,8 @@ class DutyContract:
144
116
  applies_to: AgentAudience | None
145
117
  body: str
146
118
  source_path: Path
119
+ # 직무가 선언한 정본 역할. 공통 계약과 역할 계약 자신은 갖지 않는다.
120
+ role_id: str = ""
147
121
 
148
122
 
149
123
  @dataclass(frozen=True)
@@ -495,21 +469,102 @@ class _MaterializedInvocation:
495
469
  reservation: dict[str, object] | None
496
470
 
497
471
 
472
+
473
+ # 계약 JSON 을 프롬프트에 실을 Markdown 으로 바꾸는 고정 렌더러. 제목과 순서는
474
+ # 스키마 필드 순서에 고정되어 있고, 빈 배열도 섹션을 생략하지 않는다 — 생략하면
475
+ # 같은 계약이 호출마다 다른 다이제스트를 갖는다.
476
+ _COMMON_DUTY_SECTIONS_JSON = (
477
+ ("assignmentFidelity", "Assignment fidelity"),
478
+ ("requiredInputs", "Required inputs"),
479
+ ("evidenceFirst", "Evidence first"),
480
+ ("authorityAndScope", "Authority and scope"),
481
+ ("collaborationAndIndependence", "Collaboration and independence"),
482
+ ("instructionPrecedence", "Instruction precedence"),
483
+ ("conflictHandling", "Conflict handling"),
484
+ ("completionHonesty", "Completion honesty"),
485
+ )
486
+ _ROLE_CONTRACT_SECTIONS_JSON = (
487
+ ("identity", "Identity"),
488
+ ("responsibilities", "Responsibility"),
489
+ ("requiredCapabilities", "Required capabilities"),
490
+ ("prohibitions", "Prohibitions"),
491
+ )
492
+ _ROLE_DUTY_SECTIONS_JSON = (
493
+ ("responsibilities", "Responsibility"),
494
+ ("requiredConduct", "Required conduct"),
495
+ ("decisionPrinciples", "Decision principles"),
496
+ ("authorityAndBoundaries", "Authority and boundaries"),
497
+ ("evidenceStandards", "Evidence standard"),
498
+ ("collaborationContract", "Collaboration contract"),
499
+ ("completionCriteria", "Completion criteria"),
500
+ ("prohibitions", "Forbidden conduct"),
501
+ ("blockedStateReporting", "Blocked-state reporting"),
502
+ )
503
+
504
+
505
+ def _contract_title(contract_id: str) -> str:
506
+ return " ".join(part.capitalize() for part in contract_id.split("-"))
507
+
508
+
509
+ def _render_contract_body(
510
+ payload: Mapping[str, Any],
511
+ sections: tuple[tuple[str, str], ...],
512
+ heading: str,
513
+ path: Path,
514
+ ) -> str:
515
+ lines = [f"# {heading}", ""]
516
+ for key, title in sections:
517
+ values = payload.get(key)
518
+ if not isinstance(values, list) or not all(
519
+ isinstance(item, str) and item.strip() for item in values
520
+ ):
521
+ raise AgentInvocationError(f"invalid contract field {key!r}: {path}")
522
+ lines.append(f"## {title}")
523
+ lines.append("")
524
+ if values:
525
+ lines.extend(f"- {item}" for item in values)
526
+ else:
527
+ lines.append("- (none)")
528
+ lines.append("")
529
+ return "\n".join(lines).rstrip("\n") + "\n"
530
+
531
+
532
+ def _load_contract_payload(path: Path) -> Mapping[str, Any]:
533
+ if not path.is_file() and path.with_suffix(".md").is_file():
534
+ # 이 run 은 Markdown 직무를 동결했다(contractFormatVersion 1). 그 형식의
535
+ # 파서는 제거됐으므로 여기서 멈추고 원인을 이름한다 — 없는 파일 이름만
536
+ # 알리면 설치가 깨진 것처럼 읽힌다.
537
+ raise AgentInvocationError(
538
+ f"run froze markdown duty contracts and cannot be resumed: {path.parent}"
539
+ )
540
+ try:
541
+ payload = load_owned_object(path, artifact="agent contract")
542
+ except JsonBoundaryError as exc:
543
+ raise AgentInvocationError(f"invalid agent contract: {path}: {exc}") from exc
544
+ except OSError as exc:
545
+ raise AgentInvocationError(f"cannot read agent contract: {path}") from exc
546
+ if payload.get("schemaVersion") != "1.0":
547
+ raise AgentInvocationError(f"unsupported contract schemaVersion: {path}")
548
+ return payload
549
+
550
+
498
551
  def load_common_duty_contract(duty_root: Path) -> DutyContract:
499
552
  """Load the common fragment, which is deliberately outside the role catalog."""
500
- path = duty_root / "common.md"
501
- fields, body = _parse_duty_file(path)
502
- if set(fields) != {"id", "version", "kind"}:
503
- raise AgentInvocationError(f"invalid common duty frontmatter: {path}")
504
- if fields["id"] != "common" or fields["kind"] != "common":
505
- raise AgentInvocationError(f"invalid common duty frontmatter: {path}")
506
- _validate_duty_sections(body, COMMON_DUTY_SECTIONS, "common", path)
553
+ path = duty_root / "common.json"
554
+ payload = _load_contract_payload(path)
555
+ if payload.get("id") != "common":
556
+ raise AgentInvocationError(f"invalid common contract id: {path}")
507
557
  return DutyContract(
508
558
  id="common",
509
- version=_parse_version(fields["version"], path),
559
+ version=1,
510
560
  kind="common",
511
561
  applies_to=None,
512
- body=body,
562
+ body=_render_contract_body(
563
+ payload,
564
+ _COMMON_DUTY_SECTIONS_JSON,
565
+ "Common Agent Duty Contract",
566
+ path,
567
+ ),
513
568
  source_path=path,
514
569
  )
515
570
 
@@ -518,8 +573,8 @@ def load_duty_catalog(duty_root: Path) -> dict[AgentAudience, DutyContract]:
518
573
  """Load one role contract for every supported invocation audience."""
519
574
  catalog: dict[AgentAudience, DutyContract] = {}
520
575
  seen_ids: set[str] = set()
521
- for path in sorted(duty_root.glob("*.md")):
522
- if path.name == "common.md":
576
+ for path in sorted(duty_root.glob("*.json")):
577
+ if path.name == "common.json":
523
578
  continue
524
579
  duty = _load_role_duty(path)
525
580
  if duty.id in seen_ids:
@@ -538,18 +593,49 @@ def load_role_duty_contract(duty_root: Path, audience: AgentAudience) -> DutyCon
538
593
  """현재 호출의 지침만 읽어 이후 추가된 역할을 기존 실행에 요구하지 않는다."""
539
594
  if audience not in _SUPPORTED_AUDIENCES:
540
595
  raise AgentInvocationError(f"unknown duty audience: {audience}")
541
- duty = _load_role_duty(duty_root / f"{audience}.md")
596
+ duty = _load_role_duty(duty_root / f"{audience}.json")
542
597
  if duty.applies_to != audience:
543
598
  raise AgentInvocationError(f"duty audience does not match requested audience: {audience}")
544
599
  return duty
545
600
 
546
601
 
547
602
  def digest_duty_catalog(duty_root: Path) -> str:
548
- """Return the canonical digest of every duty file in a snapshot."""
549
- names = [path.relative_to(duty_root).as_posix() for path in duty_root.glob("*.md")]
603
+ """Return the canonical digest of every contract file in a snapshot.
604
+
605
+ 역할 계약은 하위 `roles/` 에 둔다 — `report-writer` 와 `translator` 는 직무
606
+ id 이면서 역할 id 라, 한 폴더에 두면 두 계약이 같은 파일 이름을 두고 겹친다.
607
+ """
608
+ names = [
609
+ path.relative_to(duty_root).as_posix()
610
+ for path in (*duty_root.glob("*.json"), *(duty_root / "roles").glob("*.json"))
611
+ ]
550
612
  return _digest_framed_files(duty_root, names)
551
613
 
552
614
 
615
+ def load_role_contract(duty_root: Path, role_id: str) -> DutyContract:
616
+ """이 호출의 역할 계약. 직무가 `roleId` 로 이름한 역할 하나만 읽는다."""
617
+ path = duty_root / "roles" / f"{role_id}.json"
618
+ payload = _load_contract_payload(path)
619
+ if payload.get("id") != role_id:
620
+ raise AgentInvocationError(f"role contract id does not match its file: {path}")
621
+ identity = payload.get("identity")
622
+ if not isinstance(identity, str) or not identity.strip():
623
+ raise AgentInvocationError(f"role contract has no identity: {path}")
624
+ return DutyContract(
625
+ id=role_id,
626
+ version=1,
627
+ kind="role",
628
+ applies_to=None,
629
+ body=_render_contract_body(
630
+ {**payload, "identity": [identity]},
631
+ _ROLE_CONTRACT_SECTIONS_JSON,
632
+ f"{_contract_title(role_id)} Role Contract",
633
+ path,
634
+ ),
635
+ source_path=path,
636
+ )
637
+
638
+
553
639
  def prepare_agent_invocation(
554
640
  request: AgentInvocationRequest,
555
641
  ) -> PreparedAgentInvocation:
@@ -1314,7 +1400,10 @@ def _render_prompt(
1314
1400
  if assignment.host_model_value is not None:
1315
1401
  header.append(f"**Host model value:** {assignment.host_model_value}")
1316
1402
  header.extend(delivery)
1317
- duty_body = f"{common.body.rstrip()}\n\n{duty.body.rstrip()}"
1403
+ role_contract = load_role_contract(request.duty_root, duty.role_id)
1404
+ duty_body = "\n\n".join(
1405
+ part.rstrip() for part in (common.body, role_contract.body, duty.body)
1406
+ )
1318
1407
  return (
1319
1408
  "\n".join(header)
1320
1409
  + "\n\n## Duty Contract\n\n"
@@ -1344,7 +1433,11 @@ def _invocation_digests(
1344
1433
  _source_payload(source) for source in request.instruction.source_paths
1345
1434
  ],
1346
1435
  }
1347
- selected_names = ["common.md", duty.source_path.name]
1436
+ selected_names = [
1437
+ "common.json",
1438
+ f"roles/{duty.role_id}.json",
1439
+ duty.source_path.name,
1440
+ ]
1348
1441
  return {
1349
1442
  "catalogDigest": digest_duty_catalog(request.duty_root),
1350
1443
  "assignmentDigest": _sha256(_canonical_json(assignment)),
@@ -2131,7 +2224,9 @@ def _verify_duty_snapshot(
2131
2224
  if actual_catalog != expected_catalog or digests["catalogDigest"] != actual_catalog:
2132
2225
  errors.append("catalog digest does not match run duty snapshot")
2133
2226
  return errors
2134
- actual_duty = _digest_framed_files(root, ["common.md", duty.source_path.name])
2227
+ actual_duty = _digest_framed_files(
2228
+ root, ["common.json", f"roles/{duty.role_id}.json", duty.source_path.name]
2229
+ )
2135
2230
  if digests["dutyDigest"] != actual_duty:
2136
2231
  errors.append("duty digest does not match run duty snapshot")
2137
2232
  return errors
@@ -2163,7 +2258,17 @@ def _verify_prompt_duty_body(
2163
2258
  rendered_duty, _task = remainder.split("\n\n## Task Instructions\n\n", 1)
2164
2259
  except (AgentInvocationError, OSError, UnicodeDecodeError, ValueError):
2165
2260
  return ["prompt duty contract does not match duty snapshot"]
2166
- expected = f"{common.body.rstrip()}\n\n{duty.body.rstrip()}"
2261
+ role_contract = load_role_contract(
2262
+ _project_path(
2263
+ project_root,
2264
+ str(metadata["contractSource"]["dutyRootPath"]),
2265
+ must_exist=True,
2266
+ ),
2267
+ duty.role_id,
2268
+ )
2269
+ expected = "\n\n".join(
2270
+ part.rstrip() for part in (common.body, role_contract.body, duty.body)
2271
+ )
2167
2272
  if rendered_duty != expected:
2168
2273
  return ["prompt duty contract does not match duty snapshot"]
2169
2274
  return []
@@ -2407,77 +2512,27 @@ def _deduplicate(errors: list[str]) -> list[str]:
2407
2512
 
2408
2513
 
2409
2514
  def _load_role_duty(path: Path) -> DutyContract:
2410
- fields, body = _parse_duty_file(path)
2411
- if set(fields) != {"id", "version", "kind", "appliesTo"}:
2412
- raise AgentInvocationError(f"invalid role duty frontmatter: {path}")
2413
- audience = fields["appliesTo"]
2414
- if audience not in _SUPPORTED_AUDIENCES:
2515
+ payload = _load_contract_payload(path)
2516
+ audience = payload.get("id")
2517
+ if not isinstance(audience, str) or audience not in _SUPPORTED_AUDIENCES:
2415
2518
  raise AgentInvocationError(f"unknown duty audience: {audience}")
2416
- if fields["kind"] != "role" or fields["id"] != audience:
2417
- raise AgentInvocationError(f"invalid role duty frontmatter: {path}")
2418
- _validate_duty_sections(body, ROLE_DUTY_SECTIONS, "role", path)
2519
+ if audience != path.stem:
2520
+ raise AgentInvocationError(f"duty id does not match its file name: {path}")
2521
+ if not isinstance(payload.get("roleId"), str) or not payload["roleId"]:
2522
+ raise AgentInvocationError(f"duty contract has no roleId: {path}")
2419
2523
  return DutyContract(
2420
- id=fields["id"],
2421
- version=_parse_version(fields["version"], path),
2524
+ role_id=payload["roleId"],
2525
+ id=audience,
2526
+ version=1,
2422
2527
  kind="role",
2423
2528
  applies_to=audience,
2424
- body=body,
2529
+ body=_render_contract_body(
2530
+ payload,
2531
+ _ROLE_DUTY_SECTIONS_JSON,
2532
+ f"{_contract_title(audience)} Duty Contract",
2533
+ path,
2534
+ ),
2425
2535
  source_path=path,
2426
2536
  )
2427
2537
 
2428
2538
 
2429
- def _parse_duty_file(path: Path) -> tuple[dict[str, str], str]:
2430
- try:
2431
- text = path.read_text(encoding="utf-8")
2432
- except OSError as exc:
2433
- raise AgentInvocationError(f"cannot read duty contract: {path}") from exc
2434
- lines = text.splitlines(keepends=True)
2435
- if not lines or lines[0].strip() != "---":
2436
- raise AgentInvocationError(f"missing duty frontmatter: {path}")
2437
- try:
2438
- end = next(index for index, line in enumerate(lines[1:], 1) if line.strip() == "---")
2439
- except StopIteration as exc:
2440
- raise AgentInvocationError(f"unterminated duty frontmatter: {path}") from exc
2441
- fields: dict[str, str] = {}
2442
- for line in lines[1:end]:
2443
- key, separator, value = line.partition(":")
2444
- if not separator or not key.strip() or key.strip() in fields:
2445
- raise AgentInvocationError(f"invalid duty frontmatter: {path}")
2446
- fields[key.strip()] = value.strip()
2447
- return fields, "".join(lines[end + 1 :]).lstrip("\n")
2448
-
2449
-
2450
- def _validate_duty_sections(
2451
- body: str,
2452
- required: tuple[str, ...],
2453
- kind: str,
2454
- path: Path,
2455
- ) -> None:
2456
- matches = list(_DUTY_SECTION_RE.finditer(body))
2457
- sections: dict[str, str] = {}
2458
- for index, match in enumerate(matches):
2459
- name = match.group(1).strip()
2460
- if name in sections:
2461
- raise AgentInvocationError(f"duplicate {kind} duty section {name}: {path}")
2462
- end = matches[index + 1].start() if index + 1 < len(matches) else len(body)
2463
- sections[name] = body[match.end() : end].strip()
2464
- missing = [name for name in required if name not in sections]
2465
- if missing:
2466
- raise AgentInvocationError(
2467
- f"missing {kind} duty sections: {', '.join(missing)}: {path}"
2468
- )
2469
- empty = [name for name in required if not sections[name]]
2470
- if empty:
2471
- raise AgentInvocationError(
2472
- f"empty {kind} duty sections: {', '.join(empty)}: {path}"
2473
- )
2474
-
2475
-
2476
- def _parse_version(value: str, path: Path) -> int:
2477
- try:
2478
- version = int(value)
2479
- except ValueError as exc:
2480
- raise AgentInvocationError(f"invalid duty version: {path}") from exc
2481
- if version < 1:
2482
- raise AgentInvocationError(f"invalid duty version: {path}")
2483
- return version