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.
- package/README.md +7 -6
- package/dist/cli-registry.mjs +7 -7
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/lifecycle/install.mjs +50 -124
- package/dist/commands/lifecycle/install.mjs.map +1 -1
- package/dist/commands/memory/memory.mjs +41 -8
- package/dist/commands/memory/memory.mjs.map +1 -1
- package/dist/lib/install-assets.mjs +3 -0
- package/dist/lib/install-assets.mjs.map +1 -1
- package/dist/lib/runtime-manifest.mjs +2 -1
- package/dist/lib/runtime-manifest.mjs.map +1 -1
- package/dist/lib/types.d.mts +2 -1
- package/docs/architecture/storage-model.md +14 -11
- package/docs/architecture.md +26 -20
- package/docs/cli.md +15 -12
- package/docs/contributor-change-matrix.md +3 -2
- package/docs/performance-improvement-plan-v2.md +3 -9
- package/docs/project-structure-overview.md +39 -11
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/common-flow.md +1 -1
- package/docs/task-process/final-verification.md +3 -1
- package/docs/task-process/implementation-option-selection.md +1 -1
- package/docs/task-process/implementation.md +1 -1
- package/docs/task-process/release-handoff.md +36 -39
- package/package.json +1 -2
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/common.json +28 -0
- package/runtime/agents/operations/code-review.json +6 -0
- package/runtime/agents/operations/report-translation.json +6 -0
- package/runtime/agents/operations/schedule-verification.json +6 -0
- package/runtime/agents/roles/analyser.json +18 -0
- package/runtime/agents/roles/critic.json +18 -0
- package/runtime/agents/roles/designer.json +18 -0
- package/runtime/agents/roles/implementer.json +20 -0
- package/runtime/agents/roles/leader.json +20 -0
- package/runtime/agents/roles/planner.json +18 -0
- package/runtime/agents/roles/report-writer.json +19 -0
- package/runtime/agents/roles/translator.json +19 -0
- package/runtime/agents/roles/verifier.json +18 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/prompts/duties/acceptance-critic.json +32 -0
- package/runtime/prompts/duties/acceptance-verifier.json +32 -0
- package/runtime/prompts/duties/analysis-worker.json +32 -0
- package/runtime/prompts/duties/code-reviewer.json +32 -0
- package/runtime/prompts/duties/diagnosis-worker.json +32 -0
- package/runtime/prompts/duties/direction-selection-worker.json +32 -0
- package/runtime/prompts/duties/discovery-worker.json +32 -0
- package/runtime/prompts/duties/implementation-executor.json +32 -0
- package/runtime/prompts/duties/implementation-verifier.json +32 -0
- package/runtime/prompts/duties/lead.json +32 -0
- package/runtime/prompts/duties/planning-worker.json +36 -0
- package/runtime/prompts/duties/report-writer.json +32 -0
- package/runtime/prompts/duties/reverification-worker.json +32 -0
- package/runtime/prompts/duties/schedule-verifier.json +32 -0
- package/runtime/prompts/duties/scope-critic.json +32 -0
- package/runtime/prompts/duties/technical-verification-worker.json +32 -0
- package/runtime/prompts/duties/translator.json +32 -0
- package/runtime/prompts/launch.template.md +2 -1
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/convergence.md +4 -4
- package/runtime/prompts/lead/okstra-lead-contract.md +115 -6
- package/runtime/prompts/lead/plan-body-verification.md +6 -6
- package/runtime/prompts/lead/report-writer.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +2 -2
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +4 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
- package/runtime/prompts/profiles/change-impact-analysis.json +31 -0
- package/runtime/prompts/profiles/change-impact-analysis.md +0 -20
- package/runtime/prompts/profiles/error-analysis.json +39 -0
- package/runtime/prompts/profiles/error-analysis.md +0 -25
- package/runtime/prompts/profiles/feature-analysis.json +31 -0
- package/runtime/prompts/profiles/feature-analysis.md +0 -20
- package/runtime/prompts/profiles/final-verification.json +30 -0
- package/runtime/prompts/profiles/final-verification.md +4 -23
- package/runtime/prompts/profiles/forbidden-actions.json +4 -3
- package/runtime/prompts/profiles/implementation-option-selection.json +31 -0
- package/runtime/prompts/profiles/implementation-option-selection.md +0 -20
- package/runtime/prompts/profiles/implementation-planning.json +40 -0
- package/runtime/prompts/profiles/implementation-planning.md +4 -29
- package/runtime/prompts/profiles/implementation.json +30 -0
- package/runtime/prompts/profiles/implementation.md +1 -20
- package/runtime/prompts/profiles/improvement-discovery.json +31 -0
- package/runtime/prompts/profiles/improvement-discovery.md +0 -20
- package/runtime/prompts/profiles/project-analysis.json +31 -0
- package/runtime/prompts/profiles/project-analysis.md +0 -20
- package/runtime/prompts/profiles/release-handoff.json +5 -0
- package/runtime/prompts/profiles/release-handoff.md +74 -74
- package/runtime/prompts/profiles/requirements-discovery.json +39 -0
- package/runtime/prompts/profiles/requirements-discovery.md +0 -25
- package/runtime/prompts/profiles/technical-verification.json +39 -0
- package/runtime/prompts/profiles/technical-verification.md +0 -25
- package/runtime/prompts/wizard/prompts.ko.json +14 -18
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +3 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +4 -3
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/worker-session.md +108 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +8 -1
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +23 -6
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +6 -2
- package/runtime/python/okstra_ctl/agent/invocation.py +168 -113
- package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +120 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +107 -2
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +0 -49
- package/runtime/python/okstra_ctl/analysis_packet.py +4 -1
- package/runtime/python/okstra_ctl/application/open_worker.py +6 -1
- package/runtime/python/okstra_ctl/assignment_resolver.py +16 -5
- package/runtime/python/okstra_ctl/cmux.py +69 -20
- package/runtime/python/okstra_ctl/code_review_target.py +16 -8
- package/runtime/python/okstra_ctl/conformance.py +43 -0
- package/runtime/python/okstra_ctl/consumers.py +23 -8
- package/runtime/python/okstra_ctl/container.py +31 -8
- package/runtime/python/okstra_ctl/context_cost.py +11 -15
- package/runtime/python/okstra_ctl/contract_refreeze.py +156 -0
- package/runtime/python/okstra_ctl/convergence_engine.py +38 -0
- package/runtime/python/okstra_ctl/convergence_provenance.py +7 -1
- package/runtime/python/okstra_ctl/design_prep.py +34 -1
- package/runtime/python/okstra_ctl/dispatch_core.py +53 -27
- package/runtime/python/okstra_ctl/domain/host.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +10 -0
- package/runtime/python/okstra_ctl/error_report.py +4 -3
- package/runtime/python/okstra_ctl/execution_manifest.py +71 -18
- package/runtime/python/okstra_ctl/handoff.py +384 -286
- package/runtime/python/okstra_ctl/handoff_verification.py +25 -6
- package/runtime/python/okstra_ctl/implementation_stage.py +9 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +113 -0
- package/runtime/python/okstra_ctl/lead_progress.py +1 -1
- package/runtime/python/okstra_ctl/legacy_model_selection.py +2 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -4
- package/runtime/python/okstra_ctl/manager_launch.py +1 -1
- package/runtime/python/okstra_ctl/manager_paths.py +14 -3
- package/runtime/python/okstra_ctl/manager_store.py +210 -3
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/manager_view.py +2 -1
- package/runtime/python/okstra_ctl/model_discovery.py +30 -0
- package/runtime/python/okstra_ctl/model_io/lines.py +14 -1
- package/runtime/python/okstra_ctl/model_io/renderers.py +4 -3
- package/runtime/python/okstra_ctl/models.py +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +16 -6
- package/runtime/python/okstra_ctl/operation_invocation.py +86 -0
- package/runtime/python/okstra_ctl/option_comparison.py +168 -0
- package/runtime/python/okstra_ctl/path_hints.py +9 -0
- package/runtime/python/okstra_ctl/paths.py +3 -0
- package/runtime/python/okstra_ctl/profile_show.py +42 -1
- package/runtime/python/okstra_ctl/registry/host_discovery.py +20 -12
- package/runtime/python/okstra_ctl/registry/host_registry.py +11 -0
- package/runtime/python/okstra_ctl/render.py +79 -0
- package/runtime/python/okstra_ctl/report_contract.py +1 -1
- package/runtime/python/okstra_ctl/report_html/view_models/release_handoff.py +21 -3
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +177 -17
- package/runtime/python/okstra_ctl/report_translation.py +2 -1
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +69 -9
- package/runtime/python/okstra_ctl/role_requirements.py +142 -129
- package/runtime/python/okstra_ctl/rollup.py +3 -1
- package/runtime/python/okstra_ctl/run.py +76 -29
- package/runtime/python/okstra_ctl/schedule_semantics.py +17 -6
- package/runtime/python/okstra_ctl/stage_fix_carry.py +23 -4
- package/runtime/python/okstra_ctl/stage_integrate.py +178 -18
- package/runtime/python/okstra_ctl/stage_map.py +16 -2
- package/runtime/python/okstra_ctl/stage_targets.py +209 -43
- package/runtime/python/okstra_ctl/team.py +22 -13
- package/runtime/python/okstra_ctl/time_report.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +3 -1
- package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -9
- package/runtime/python/okstra_ctl/wizard/ids.py +1 -1
- package/runtime/python/okstra_ctl/wizard/registry.py +1 -1
- package/runtime/python/okstra_ctl/wizard/state.py +3 -5
- package/runtime/python/okstra_ctl/wizard/steps_plan.py +12 -21
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +5 -1
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +35 -7
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +66 -48
- package/runtime/python/okstra_ctl/workflow.py +1 -1
- package/runtime/python/okstra_ctl/worktree/__init__.py +3 -1
- package/runtime/python/okstra_ctl/worktree/naming.py +9 -0
- package/runtime/python/okstra_ctl/worktree_registry.py +38 -9
- package/runtime/python/okstra_token_usage/pricing.py +6 -4
- package/runtime/schemas/agent-common-v1.schema.json +34 -0
- package/runtime/schemas/agent-duty-v1.schema.json +38 -0
- package/runtime/schemas/agent-operation-v1.schema.json +11 -0
- package/runtime/schemas/agent-profile-v1.schema.json +46 -0
- package/runtime/schemas/agent-role-v1.schema.json +29 -0
- package/runtime/schemas/final-report-v2.0.schema.json +118 -97
- package/runtime/schemas/final-report-v3.0.schema.json +118 -97
- package/runtime/skills/okstra-brief-gen/SKILL.md +84 -4
- package/runtime/skills/okstra-chat/SKILL.md +2 -2
- package/runtime/skills/okstra-code-review/SKILL.md +23 -9
- package/runtime/skills/okstra-container-build/SKILL.md +10 -10
- package/runtime/skills/okstra-inspect/SKILL.md +1 -1
- package/runtime/skills/okstra-inspect/facets/cost.md +1 -1
- package/runtime/skills/okstra-inspect/facets/error-zip.md +9 -9
- package/runtime/skills/okstra-inspect/facets/errors.md +16 -16
- package/runtime/skills/okstra-inspect/facets/logs.md +7 -7
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -2
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +4 -3
- package/runtime/skills/okstra-inspect/facets/time.md +11 -10
- package/runtime/skills/okstra-manager/SKILL.md +18 -2
- package/runtime/skills/okstra-pr-gen/SKILL.md +6 -5
- package/runtime/skills/okstra-rollup/SKILL.md +5 -5
- package/runtime/skills/okstra-run/SKILL.md +31 -12
- package/runtime/skills/okstra-schedule-gen/SKILL.md +19 -14
- package/runtime/skills/okstra-setup/SKILL.md +12 -10
- package/runtime/skills/okstra-setup/references/project-config.md +7 -6
- package/runtime/skills/okstra-usage/SKILL.md +1 -1
- package/runtime/skills/okstra-user-response/SKILL.md +1 -1
- package/runtime/templates/manager/view.template.html +1 -0
- package/runtime/templates/report-writer-prompt-preamble.md +8 -0
- package/runtime/templates/reports/brief.template.md +14 -4
- package/runtime/templates/reports/html/i18n/en.json +5 -4
- package/runtime/templates/reports/html/i18n/ko.json +5 -4
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +8 -5
- package/runtime/templates/reports/i18n/en.json +1 -1
- package/runtime/templates/reports/md/tasks/release-handoff.template.md +1 -1
- package/runtime/templates/reports/release-handoff-input.template.md +6 -4
- package/runtime/templates/translator-prompt-preamble.md +36 -0
- package/runtime/validators/checks/validate-assets-01.py +7 -8
- package/runtime/validators/validate-brief.py +70 -0
- package/runtime/validators/validate-implementation-plan-stages.py +2 -1
- package/runtime/validators/validate-run.py +72 -15
- package/runtime/validators/validate-schedule.py +9 -0
- package/docs/for-ai/README.md +0 -68
- package/docs/for-ai/skills/okstra-brief-gen.md +0 -262
- package/docs/for-ai/skills/okstra-chat.md +0 -34
- package/docs/for-ai/skills/okstra-code-review.md +0 -57
- package/docs/for-ai/skills/okstra-container-build.md +0 -129
- package/docs/for-ai/skills/okstra-inspect.md +0 -262
- package/docs/for-ai/skills/okstra-manager.md +0 -86
- package/docs/for-ai/skills/okstra-memory.md +0 -126
- package/docs/for-ai/skills/okstra-pr-gen.md +0 -49
- package/docs/for-ai/skills/okstra-rollup.md +0 -114
- package/docs/for-ai/skills/okstra-run.md +0 -250
- package/docs/for-ai/skills/okstra-schedule-gen.md +0 -240
- package/docs/for-ai/skills/okstra-setup.md +0 -167
- package/docs/for-ai/skills/okstra-usage.md +0 -29
- package/docs/for-ai/skills/okstra-user-response.md +0 -72
- package/runtime/agents/workers/claude-worker.md +0 -128
- package/runtime/agents/workers/report-writer-worker.md +0 -37
- package/runtime/agents/workers/translator-worker.md +0 -63
- package/runtime/prompts/duties/acceptance-critic.md +0 -44
- package/runtime/prompts/duties/acceptance-verifier.md +0 -44
- package/runtime/prompts/duties/analysis-worker.md +0 -44
- package/runtime/prompts/duties/code-reviewer.md +0 -44
- package/runtime/prompts/duties/common.md +0 -39
- package/runtime/prompts/duties/diagnosis-worker.md +0 -44
- package/runtime/prompts/duties/direction-selection-worker.md +0 -44
- package/runtime/prompts/duties/discovery-worker.md +0 -44
- package/runtime/prompts/duties/implementation-executor.md +0 -44
- package/runtime/prompts/duties/implementation-verifier.md +0 -44
- package/runtime/prompts/duties/lead.md +0 -44
- package/runtime/prompts/duties/planning-worker.md +0 -52
- package/runtime/prompts/duties/report-writer.md +0 -44
- package/runtime/prompts/duties/reverification-worker.md +0 -44
- package/runtime/prompts/duties/schedule-verifier.md +0 -44
- package/runtime/prompts/duties/scope-critic.md +0 -44
- package/runtime/prompts/duties/technical-verification-worker.md +0 -44
- package/runtime/prompts/duties/translator.md +0 -44
- 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-
|
|
21
|
-
# `gpt-5.6` is not a slug that catalog offers at all, so it
|
|
22
|
-
# its rate moved to `_LEGACY_CODEX_PRICING` so past runs
|
|
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
|
-
|
|
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 에서는
|
|
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.
|
|
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.
|
|
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.
|
|
501
|
-
|
|
502
|
-
if
|
|
503
|
-
raise AgentInvocationError(f"invalid common
|
|
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=
|
|
559
|
+
version=1,
|
|
510
560
|
kind="common",
|
|
511
561
|
applies_to=None,
|
|
512
|
-
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("*.
|
|
522
|
-
if path.name == "common.
|
|
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}.
|
|
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
|
|
549
|
-
|
|
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
|
-
|
|
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 = [
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
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
|
|
2417
|
-
raise AgentInvocationError(f"
|
|
2418
|
-
|
|
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
|
-
|
|
2421
|
-
|
|
2524
|
+
role_id=payload["roleId"],
|
|
2525
|
+
id=audience,
|
|
2526
|
+
version=1,
|
|
2422
2527
|
kind="role",
|
|
2423
2528
|
applies_to=audience,
|
|
2424
|
-
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
|