okstra 0.167.0 → 0.169.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 +6 -5
- package/docs/architecture/storage-model.md +57 -1
- package/docs/architecture.md +70 -2
- package/docs/cli.md +8 -4
- package/docs/for-ai/skills/okstra-code-review.md +3 -2
- package/docs/for-ai/skills/okstra-schedule-gen.md +3 -1
- package/docs/pr-template-usage.md +10 -6
- package/docs/project-structure-overview.md +14 -11
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +6 -5
- package/runtime/agents/workers/report-writer-worker.md +9 -4
- package/runtime/agents/workers/translator-worker.md +6 -4
- package/runtime/prompts/coding-preflight/architectures/hexagonal.md +27 -1
- package/runtime/prompts/coding-preflight/clean-code.md +13 -0
- package/runtime/prompts/duties/acceptance-critic.md +24 -0
- package/runtime/prompts/duties/acceptance-verifier.md +24 -0
- package/runtime/prompts/duties/analysis-worker.md +24 -0
- package/runtime/prompts/duties/code-reviewer.md +24 -0
- package/runtime/prompts/duties/common.md +35 -0
- package/runtime/prompts/duties/implementation-executor.md +24 -0
- package/runtime/prompts/duties/implementation-verifier.md +24 -0
- package/runtime/prompts/duties/lead.md +24 -0
- package/runtime/prompts/duties/report-writer.md +24 -0
- package/runtime/prompts/duties/reverification-worker.md +24 -0
- package/runtime/prompts/duties/schedule-verifier.md +24 -0
- package/runtime/prompts/duties/scope-critic.md +24 -0
- package/runtime/prompts/duties/translator.md +24 -0
- package/runtime/prompts/lead/convergence.md +51 -7
- package/runtime/prompts/lead/okstra-lead-contract.md +10 -20
- package/runtime/prompts/lead/plan-body-verification.md +16 -1
- package/runtime/prompts/lead/report-writer.md +20 -5
- package/runtime/prompts/lead/team-contract.md +13 -13
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation.md +4 -2
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +8 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +33 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +13 -12
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +3 -2
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +3 -3
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +2 -1
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +6 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +2 -1
- package/runtime/python/okstra_ctl/agent_invocation.py +1502 -0
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +788 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +2 -107
- package/runtime/python/okstra_ctl/context_cost.py +46 -5
- package/runtime/python/okstra_ctl/dispatch_core.py +312 -37
- package/runtime/python/okstra_ctl/dispatch_state.py +461 -36
- package/runtime/python/okstra_ctl/doctor.py +150 -16
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +87 -9
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +214 -23
- package/runtime/python/okstra_ctl/path_hints.py +26 -0
- package/runtime/python/okstra_ctl/paths.py +20 -0
- package/runtime/python/okstra_ctl/ports/__init__.py +8 -0
- package/runtime/python/okstra_ctl/ports/host.py +3 -0
- package/runtime/python/okstra_ctl/ports/host_model.py +60 -0
- package/runtime/python/okstra_ctl/pr_template.py +3 -6
- package/runtime/python/okstra_ctl/registry/host_registry.py +5 -0
- package/runtime/python/okstra_ctl/render.py +217 -12
- package/runtime/python/okstra_ctl/report_finalize.py +44 -0
- package/runtime/python/okstra_ctl/run.py +368 -51
- package/runtime/python/okstra_ctl/session.py +16 -12
- package/runtime/python/okstra_ctl/team.py +11 -11
- package/runtime/python/okstra_ctl/worker_dispatch.py +104 -0
- package/runtime/python/okstra_ctl/worker_prompt_body.py +5 -38
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +32 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +38 -1
- package/runtime/skills/okstra-code-review/SKILL.md +22 -3
- package/runtime/skills/okstra-run/SKILL.md +16 -1
- package/runtime/skills/okstra-schedule-gen/SKILL.md +15 -1
- package/runtime/templates/implementation-worker-preamble.md +0 -10
- package/runtime/templates/report-writer-prompt-preamble.md +0 -9
- package/runtime/templates/reports/settings.template.json +0 -11
- package/runtime/templates/worker-prompt-preamble.md +0 -10
- package/runtime/validators/lib/fixtures.sh +93 -0
- package/runtime/validators/lib/validate-assets.sh +0 -8
- package/runtime/validators/validate-run.py +182 -0
- package/src/cli-registry.mjs +14 -0
- package/src/commands/execute/agent-prompt.mjs +25 -0
- package/src/commands/execute/codex-dispatch.mjs +6 -63
- package/src/commands/execute/worker-dispatch.mjs +76 -0
- package/src/commands/lifecycle/doctor.mjs +18 -3
- package/src/commands/lifecycle/install.mjs +33 -15
- package/src/commands/lifecycle/uninstall.mjs +4 -3
- package/src/lib/install-assets.mjs +9 -0
- package/runtime/agents/workers/antigravity-worker.md +0 -259
- package/runtime/agents/workers/codex-worker.md +0 -259
- package/runtime/agents/workers/grok-worker.md +0 -259
- package/runtime/agents/workers/kimi-worker.md +0 -259
- package/runtime/prompts/coding-preflight/scripts/preedit-check.sh +0 -79
- package/runtime/templates/operating-standard.md +0 -22
- package/src/lib/worker-agent-render.mjs +0 -50
- /package/runtime/templates/{prd → pr}/pr-body.template.md +0 -0
|
@@ -108,6 +108,11 @@ from okstra_ctl.worker_prompt_contract import ( # noqa: E402
|
|
|
108
108
|
PromptRecord,
|
|
109
109
|
validate_initial_prompt_records,
|
|
110
110
|
)
|
|
111
|
+
from okstra_ctl.agent_invocation import ( # noqa: E402
|
|
112
|
+
AgentInvocationError,
|
|
113
|
+
agent_model_assignment_from_payload,
|
|
114
|
+
verify_agent_invocation,
|
|
115
|
+
)
|
|
111
116
|
from okstra_ctl.worker_audit_ledger import ( # noqa: E402
|
|
112
117
|
READING_CONFIRMATION_HEADING_RE,
|
|
113
118
|
check_worker_results_audit,
|
|
@@ -124,6 +129,176 @@ from okstra_ctl.convergence_provenance import ( # noqa: E402
|
|
|
124
129
|
TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
|
|
125
130
|
ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
|
|
126
131
|
WORKER_DISPATCH_MODES = {"cli-wrapper", "mixed", "tmux-pane"}
|
|
132
|
+
_AGENT_DISPATCH_DIGEST_KEYS = (
|
|
133
|
+
"catalogDigest",
|
|
134
|
+
"assignmentDigest",
|
|
135
|
+
"dutyDigest",
|
|
136
|
+
"instructionDigest",
|
|
137
|
+
"promptDigest",
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_agent_dispatch_contract(
|
|
142
|
+
*,
|
|
143
|
+
project_root: Path,
|
|
144
|
+
run_manifest_path: Path,
|
|
145
|
+
run_manifest: Mapping[str, Any],
|
|
146
|
+
team_state: Mapping[str, Any],
|
|
147
|
+
failures: list[str],
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Validate invocation-to-dispatch and result associations for new runs."""
|
|
150
|
+
contract = run_manifest.get("agentContract")
|
|
151
|
+
if not isinstance(contract, Mapping) or contract.get("schemaVersion") != 1:
|
|
152
|
+
return
|
|
153
|
+
assignments = run_manifest.get("invocationAssignments")
|
|
154
|
+
if not isinstance(assignments, Mapping):
|
|
155
|
+
failures.append("agent invocation metadata is missing: invocationAssignments")
|
|
156
|
+
return
|
|
157
|
+
worker_dispatches = [
|
|
158
|
+
row for row in (team_state.get("workerDispatches") or [])
|
|
159
|
+
if isinstance(row, Mapping) and row.get("invocationId")
|
|
160
|
+
]
|
|
161
|
+
agent_dispatches = [
|
|
162
|
+
row for row in (team_state.get("agentDispatches") or [])
|
|
163
|
+
if isinstance(row, Mapping)
|
|
164
|
+
]
|
|
165
|
+
dispatches = [*worker_dispatches, *agent_dispatches]
|
|
166
|
+
ids: dict[str, Mapping[str, Any]] = {}
|
|
167
|
+
for row in dispatches:
|
|
168
|
+
dispatch_id = str(row.get("dispatchId") or "").strip()
|
|
169
|
+
missing = [
|
|
170
|
+
key for key in (
|
|
171
|
+
"dispatchId", "workerId", "audience", "invocationId",
|
|
172
|
+
"assignmentRef", "promptMetadataPath", "modelExecutionValue",
|
|
173
|
+
"enforcementMode", *_AGENT_DISPATCH_DIGEST_KEYS,
|
|
174
|
+
)
|
|
175
|
+
if not str(row.get(key) or "").strip()
|
|
176
|
+
]
|
|
177
|
+
if missing:
|
|
178
|
+
failures.append(
|
|
179
|
+
"agent invocation metadata is missing from dispatch record: "
|
|
180
|
+
+ ", ".join(missing)
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
if dispatch_id in ids:
|
|
184
|
+
failures.append(f"agent dispatch ID is duplicated: {dispatch_id}")
|
|
185
|
+
continue
|
|
186
|
+
ids[dispatch_id] = row
|
|
187
|
+
assignment_ref = str(row["assignmentRef"])
|
|
188
|
+
try:
|
|
189
|
+
assignment = agent_model_assignment_from_payload(
|
|
190
|
+
assignments.get(assignment_ref)
|
|
191
|
+
)
|
|
192
|
+
except AgentInvocationError as exc:
|
|
193
|
+
failures.append(f"agent dispatch {dispatch_id}: {exc}")
|
|
194
|
+
continue
|
|
195
|
+
metadata_path = _resolve_prompt_record_path(
|
|
196
|
+
project_root, str(row["promptMetadataPath"])
|
|
197
|
+
)
|
|
198
|
+
errors = verify_agent_invocation(
|
|
199
|
+
metadata_path,
|
|
200
|
+
project_root=project_root,
|
|
201
|
+
expected_run_manifest_path=run_manifest_path,
|
|
202
|
+
expected_assignment=assignment,
|
|
203
|
+
expected_invocation_id=str(row["invocationId"]),
|
|
204
|
+
expected_worker_id=str(row["workerId"]),
|
|
205
|
+
expected_assignment_ref=assignment_ref,
|
|
206
|
+
expected_audience=str(row["audience"]),
|
|
207
|
+
)
|
|
208
|
+
failures.extend(
|
|
209
|
+
f"agent dispatch {dispatch_id}: {error}" for error in errors
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
213
|
+
digests = metadata["digests"]
|
|
214
|
+
except (OSError, json.JSONDecodeError, KeyError, TypeError):
|
|
215
|
+
digests = {}
|
|
216
|
+
for key in _AGENT_DISPATCH_DIGEST_KEYS:
|
|
217
|
+
if row.get(key) != digests.get(key):
|
|
218
|
+
failures.append(
|
|
219
|
+
f"agent dispatch {dispatch_id}: {key} does not match metadata"
|
|
220
|
+
)
|
|
221
|
+
if row.get("modelExecutionValue") != assignment.model_execution_value:
|
|
222
|
+
failures.append(
|
|
223
|
+
f"agent dispatch {dispatch_id}: modelExecutionValue does not match assignment"
|
|
224
|
+
)
|
|
225
|
+
if row.get("hostModelValue") != assignment.host_model_value:
|
|
226
|
+
failures.append(
|
|
227
|
+
f"agent dispatch {dispatch_id}: hostModelValue does not match assignment"
|
|
228
|
+
)
|
|
229
|
+
enforcement = row.get("enforcementMode")
|
|
230
|
+
if enforcement not in {
|
|
231
|
+
"core-pre-dispatch", "host-native-spec-link-gate",
|
|
232
|
+
}:
|
|
233
|
+
failures.append(
|
|
234
|
+
f"agent dispatch {dispatch_id}: enforcementMode is invalid"
|
|
235
|
+
)
|
|
236
|
+
if (
|
|
237
|
+
enforcement == "host-native-spec-link-gate"
|
|
238
|
+
and row.get("promptDeliveryVerified") is not False
|
|
239
|
+
):
|
|
240
|
+
failures.append(
|
|
241
|
+
"host-native lead delivery cannot be marked verified"
|
|
242
|
+
if row.get("audience") == "lead"
|
|
243
|
+
else "host-native prompt delivery cannot be marked verified"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
lead_ids = {
|
|
247
|
+
dispatch_id for dispatch_id, row in ids.items()
|
|
248
|
+
if row.get("audience") == "lead" and row.get("workerId") == "lead"
|
|
249
|
+
}
|
|
250
|
+
if not lead_ids:
|
|
251
|
+
failures.append("accepted lead result has no lead dispatch record")
|
|
252
|
+
|
|
253
|
+
links = [
|
|
254
|
+
row for row in (team_state.get("agentResultLinks") or [])
|
|
255
|
+
if isinstance(row, Mapping)
|
|
256
|
+
]
|
|
257
|
+
paths: dict[str, str] = {}
|
|
258
|
+
dispatch_link_counts: dict[str, int] = {}
|
|
259
|
+
for link in links:
|
|
260
|
+
dispatch_id = str(link.get("dispatchId") or "").strip()
|
|
261
|
+
result_path = str(link.get("resultPath") or "").strip()
|
|
262
|
+
if dispatch_id not in ids or not result_path:
|
|
263
|
+
failures.append("agent result link has no matching dispatch record")
|
|
264
|
+
continue
|
|
265
|
+
dispatch_link_counts[dispatch_id] = dispatch_link_counts.get(dispatch_id, 0) + 1
|
|
266
|
+
previous = paths.setdefault(result_path, dispatch_id)
|
|
267
|
+
if previous != dispatch_id:
|
|
268
|
+
failures.append(
|
|
269
|
+
f"agent result is linked to multiple dispatches: {result_path}"
|
|
270
|
+
)
|
|
271
|
+
for dispatch_id, count in dispatch_link_counts.items():
|
|
272
|
+
if count > 1:
|
|
273
|
+
failures.append(
|
|
274
|
+
f"agent dispatch is linked to multiple accepted results: {dispatch_id}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
for worker in team_state.get("workers") or []:
|
|
278
|
+
if not isinstance(worker, Mapping) or worker.get("status") != "completed":
|
|
279
|
+
continue
|
|
280
|
+
worker_id = str(worker.get("workerId") or "").strip()
|
|
281
|
+
worker_result = str(worker.get("resultPath") or "").strip()
|
|
282
|
+
worker_dispatch_ids = {
|
|
283
|
+
dispatch_id for dispatch_id, row in ids.items()
|
|
284
|
+
if row.get("workerId") == worker_id
|
|
285
|
+
}
|
|
286
|
+
if worker_id and not worker_dispatch_ids:
|
|
287
|
+
failures.append(
|
|
288
|
+
f"accepted LLM result has no agent invocation record: {worker_id}"
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
matching = [
|
|
292
|
+
link for link in links
|
|
293
|
+
if link.get("dispatchId") in worker_dispatch_ids
|
|
294
|
+
and link.get("resultPath") == worker_result
|
|
295
|
+
]
|
|
296
|
+
if worker_id and len(matching) != 1:
|
|
297
|
+
failures.append(
|
|
298
|
+
f"accepted LLM result is not linked to its own dispatch: {worker_id}"
|
|
299
|
+
)
|
|
300
|
+
if lead_ids and not any(link.get("dispatchId") in lead_ids for link in links):
|
|
301
|
+
failures.append("accepted lead result has no dispatch result link")
|
|
127
302
|
|
|
128
303
|
|
|
129
304
|
def utc_now() -> str:
|
|
@@ -7805,6 +7980,13 @@ def main() -> int:
|
|
|
7805
7980
|
failures,
|
|
7806
7981
|
concurrent_run_authorized=concurrent_run_authorized,
|
|
7807
7982
|
)
|
|
7983
|
+
_validate_agent_dispatch_contract(
|
|
7984
|
+
project_root=project_root,
|
|
7985
|
+
run_manifest_path=run_manifest_path,
|
|
7986
|
+
run_manifest=run_manifest,
|
|
7987
|
+
team_state=team_state,
|
|
7988
|
+
failures=failures,
|
|
7989
|
+
)
|
|
7808
7990
|
# Schema validation runs BEFORE markdown substring checks: if the
|
|
7809
7991
|
# data.json is well-formed, the rendered markdown is guaranteed to
|
|
7810
7992
|
# contain every required section. Substring checks below are a
|
package/src/cli-registry.mjs
CHANGED
|
@@ -80,6 +80,20 @@ export const COMMAND_REGISTRY = [
|
|
|
80
80
|
category: "admin",
|
|
81
81
|
summary: ["Extract and validate deterministic plan-body items"],
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
name: "agent-prompt",
|
|
85
|
+
module: "./commands/execute/agent-prompt.mjs",
|
|
86
|
+
export: "run",
|
|
87
|
+
category: "admin",
|
|
88
|
+
summary: ["Materialize and verify auditable agent invocations"],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "worker-dispatch",
|
|
92
|
+
module: "./commands/execute/worker-dispatch.mjs",
|
|
93
|
+
export: "run",
|
|
94
|
+
category: "admin",
|
|
95
|
+
summary: ["Dispatch verified CLI-backed worker assignments"],
|
|
96
|
+
},
|
|
83
97
|
{
|
|
84
98
|
name: "plan-verify",
|
|
85
99
|
module: "./commands/execute/plan-verify.mjs",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
|
|
3
|
+
const USAGE = `okstra agent-prompt — materialize and verify auditable agent invocations
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
okstra agent-prompt materialize [options]
|
|
7
|
+
okstra agent-prompt verify [options]
|
|
8
|
+
okstra agent-prompt materialize-result [options]
|
|
9
|
+
okstra agent-prompt complete [options]
|
|
10
|
+
okstra agent-prompt verify-completion [options]
|
|
11
|
+
okstra agent-prompt record-dispatch [options]
|
|
12
|
+
okstra agent-prompt link-result [options]
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export async function run(args) {
|
|
16
|
+
if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
|
|
17
|
+
process.stdout.write(USAGE);
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
const { code } = await runPythonModule({
|
|
21
|
+
module: "okstra_ctl.agent_prompt_cli",
|
|
22
|
+
args,
|
|
23
|
+
});
|
|
24
|
+
return code ?? 1;
|
|
25
|
+
}
|
|
@@ -1,67 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
buildWorkerDispatchArgs,
|
|
3
|
+
runWorkerDispatch,
|
|
4
|
+
} from "./worker-dispatch.mjs";
|
|
3
5
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
okstra codex-dispatch --project-root <dir> --run-manifest <path> \\
|
|
8
|
-
[--workers <cli-assigned-worker-ids>] [--dry-run] \\
|
|
9
|
-
[--idle-timeout-seconds <n>]
|
|
10
|
-
|
|
11
|
-
This command reads an existing run manifest produced by 'okstra codex-run' and
|
|
12
|
-
dispatches only assignments whose persisted runner is 'cli-wrapper'. When
|
|
13
|
-
--workers is omitted, it selects that subset from the run roster. Each worker,
|
|
14
|
-
including report-writer, uses its persisted provider, model, and registered
|
|
15
|
-
wrapper. Native-session assignments remain owned by the current Codex host;
|
|
16
|
-
explicitly requesting one here fails.
|
|
17
|
-
|
|
18
|
-
Missing worker prompt files are generated automatically from the run manifest
|
|
19
|
-
and active-run-context. Existing prompt files are never overwritten.
|
|
20
|
-
When the Codex report-writer path completes, this command also runs the
|
|
21
|
-
idempotent post-report steps: token-usage substitution, render-views,
|
|
22
|
-
spawn-followups, and validate-run.
|
|
23
|
-
|
|
24
|
-
--workspace-root and --okstra-bin are owned by this command.
|
|
25
|
-
`;
|
|
26
|
-
|
|
27
|
-
const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
|
|
28
|
-
|
|
29
|
-
function isOwnedFlag(arg) {
|
|
30
|
-
if (OWNED_FLAGS.has(arg)) return true;
|
|
31
|
-
return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function buildCodexDispatchArgs(args, paths) {
|
|
35
|
-
return [
|
|
36
|
-
"--workspace-root", paths.workspace,
|
|
37
|
-
"--okstra-bin", paths.bin,
|
|
38
|
-
...args,
|
|
39
|
-
];
|
|
40
|
-
}
|
|
6
|
+
export const buildCodexDispatchArgs = buildWorkerDispatchArgs;
|
|
41
7
|
|
|
42
8
|
export async function run(args) {
|
|
43
|
-
|
|
44
|
-
process.stdout.write(USAGE);
|
|
45
|
-
return 0;
|
|
46
|
-
}
|
|
47
|
-
if (args.length === 0) {
|
|
48
|
-
process.stdout.write(USAGE);
|
|
49
|
-
return 2;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const forbidden = args.find(isOwnedFlag);
|
|
53
|
-
if (forbidden) {
|
|
54
|
-
process.stderr.write(
|
|
55
|
-
`error: ${forbidden} is set by 'okstra codex-dispatch' itself — remove it from your args\n`,
|
|
56
|
-
);
|
|
57
|
-
return 2;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const paths = await resolvePaths();
|
|
61
|
-
const result = await runPythonModule({
|
|
62
|
-
module: "okstra_ctl.codex_dispatch",
|
|
63
|
-
args: buildCodexDispatchArgs(args, paths),
|
|
64
|
-
stdio: "inherit-stdout",
|
|
65
|
-
});
|
|
66
|
-
return result.code;
|
|
9
|
+
return runWorkerDispatch(args, { commandName: "codex-dispatch" });
|
|
67
10
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { runPythonModule } from "../../lib/python-helper.mjs";
|
|
2
|
+
import { resolvePaths } from "../../lib/paths.mjs";
|
|
3
|
+
|
|
4
|
+
export const USAGE = `okstra worker-dispatch — dispatch verified CLI-backed workers
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
okstra worker-dispatch --project-root <dir> --run-manifest <path> \\
|
|
8
|
+
[--workers <cli-assigned-worker-ids>] [--dry-run] \\
|
|
9
|
+
[--idle-timeout-seconds <n>]
|
|
10
|
+
|
|
11
|
+
This command reads a prepared run manifest and dispatches only assignments
|
|
12
|
+
whose persisted runner is 'cli-wrapper'. It verifies each invocation contract
|
|
13
|
+
before execution and uses the persisted provider, model, and registered CLI.
|
|
14
|
+
Native-session assignments remain owned by the active host session.
|
|
15
|
+
|
|
16
|
+
Missing worker prompt files are generated automatically from the immutable run snapshot.
|
|
17
|
+
Existing prompt and metadata files are never overwritten.
|
|
18
|
+
When report-writer completes, this command also runs the idempotent post-report
|
|
19
|
+
steps: token-usage substitution, render-views, spawn-followups, and validate-run.
|
|
20
|
+
|
|
21
|
+
--workspace-root and --okstra-bin are owned by this command.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const OWNED_FLAGS = new Set(["--workspace-root", "--okstra-bin"]);
|
|
25
|
+
|
|
26
|
+
function isOwnedFlag(arg) {
|
|
27
|
+
if (OWNED_FLAGS.has(arg)) return true;
|
|
28
|
+
return [...OWNED_FLAGS].some((flag) => arg.startsWith(`${flag}=`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildWorkerDispatchArgs(args, paths) {
|
|
32
|
+
return [
|
|
33
|
+
"--workspace-root", paths.workspace,
|
|
34
|
+
"--okstra-bin", paths.bin,
|
|
35
|
+
...args,
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function runWorkerDispatch(args, { commandName = "worker-dispatch" } = {}) {
|
|
40
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
41
|
+
process.stdout.write(
|
|
42
|
+
commandName === "worker-dispatch"
|
|
43
|
+
? USAGE
|
|
44
|
+
: USAGE.replaceAll("worker-dispatch", commandName),
|
|
45
|
+
);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
if (args.length === 0) {
|
|
49
|
+
process.stdout.write(
|
|
50
|
+
commandName === "worker-dispatch"
|
|
51
|
+
? USAGE
|
|
52
|
+
: USAGE.replaceAll("worker-dispatch", commandName),
|
|
53
|
+
);
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const forbidden = args.find(isOwnedFlag);
|
|
58
|
+
if (forbidden) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`error: ${forbidden} is set by 'okstra ${commandName}' itself — remove it from your args\n`,
|
|
61
|
+
);
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const paths = await resolvePaths();
|
|
66
|
+
const result = await runPythonModule({
|
|
67
|
+
module: "okstra_ctl.worker_dispatch",
|
|
68
|
+
args: buildWorkerDispatchArgs(args, paths),
|
|
69
|
+
stdio: "inherit-stdout",
|
|
70
|
+
});
|
|
71
|
+
return result.code;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function run(args) {
|
|
75
|
+
return runWorkerDispatch(args);
|
|
76
|
+
}
|
|
@@ -88,6 +88,16 @@ export function phaseDiagnosticsToChecks(phase, phaseDiagnostics) {
|
|
|
88
88
|
}));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
export function buildPhaseDiagnosticsArgs(phase, paths, resolvedRuntime) {
|
|
92
|
+
return [
|
|
93
|
+
phase,
|
|
94
|
+
process.cwd(),
|
|
95
|
+
paths.workspace,
|
|
96
|
+
homedir(),
|
|
97
|
+
resolvedRuntime || "",
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
|
|
91
101
|
async function checkPython3() {
|
|
92
102
|
const r = await runProcess("python3", ["--version"]);
|
|
93
103
|
if (r.code !== 0) return { ok: false, detail: `python3 not found: ${r.stderr.trim() || "missing binary"}` };
|
|
@@ -140,7 +150,7 @@ async function check(name, fn) {
|
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
|
|
143
|
-
async function loadPhaseDiagnostics(phase, paths) {
|
|
153
|
+
async function loadPhaseDiagnostics(phase, paths, resolvedRuntime) {
|
|
144
154
|
const script = [
|
|
145
155
|
"import json, sys",
|
|
146
156
|
"from okstra_ctl.doctor import phase_diagnostics",
|
|
@@ -149,6 +159,7 @@ async function loadPhaseDiagnostics(phase, paths) {
|
|
|
149
159
|
" cwd=sys.argv[2],",
|
|
150
160
|
" workspace_root=sys.argv[3],",
|
|
151
161
|
" home=sys.argv[4],",
|
|
162
|
+
" host_runtime=sys.argv[5],",
|
|
152
163
|
")",
|
|
153
164
|
"print(json.dumps(payload, ensure_ascii=False))",
|
|
154
165
|
].join("\n");
|
|
@@ -159,7 +170,7 @@ async function loadPhaseDiagnostics(phase, paths) {
|
|
|
159
170
|
// never exists and report a false "missing agent".
|
|
160
171
|
const result = await runPythonSnippet({
|
|
161
172
|
script,
|
|
162
|
-
args:
|
|
173
|
+
args: buildPhaseDiagnosticsArgs(phase, paths, resolvedRuntime),
|
|
163
174
|
});
|
|
164
175
|
if (result.code !== 0 && !result.stdout.trim()) {
|
|
165
176
|
return {
|
|
@@ -249,7 +260,11 @@ export async function run(args) {
|
|
|
249
260
|
|
|
250
261
|
let phaseDiagnostics = null;
|
|
251
262
|
if (opts.phase) {
|
|
252
|
-
phaseDiagnostics = await loadPhaseDiagnostics(
|
|
263
|
+
phaseDiagnostics = await loadPhaseDiagnostics(
|
|
264
|
+
opts.phase,
|
|
265
|
+
paths,
|
|
266
|
+
resolvedRuntime,
|
|
267
|
+
);
|
|
253
268
|
if (phaseDiagnostics.usageError) {
|
|
254
269
|
process.stderr.write(`error: ${phaseDiagnostics.reason}\n`);
|
|
255
270
|
return 2;
|
|
@@ -10,9 +10,9 @@ import { normalizeHostRequest, resolveRuntime } from "../../lib/host-registry-cl
|
|
|
10
10
|
import { OBSOLETE_SKILL_NAMES, USER_SKILL_NAMES } from "../../lib/skill-catalog.mjs";
|
|
11
11
|
import {
|
|
12
12
|
AGENTS_MANIFEST_REL,
|
|
13
|
+
LEGACY_TRANSPORT_AGENT_NAMES,
|
|
13
14
|
SKILLS_MANIFEST_REL,
|
|
14
15
|
} from "../../lib/install-assets.mjs";
|
|
15
|
-
import { renderWorkerAgentDefinitions } from "../../lib/worker-agent-render.mjs";
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
const USER_HOME = homedir();
|
|
@@ -389,6 +389,7 @@ async function installLinkMode(repoPath, paths, opts) {
|
|
|
389
389
|
|
|
390
390
|
let agentResult = { installed: [] };
|
|
391
391
|
if (skillTargets.some((target) => target.provider === "claude")) {
|
|
392
|
+
await pruneOwnedLegacyTransportAgents(paths.home, { dryRun, quiet });
|
|
392
393
|
agentResult = await installAgentsLink(repoAbs, { dryRun, quiet });
|
|
393
394
|
}
|
|
394
395
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun });
|
|
@@ -493,6 +494,35 @@ async function writeAgentsManifest(home, names, opts) {
|
|
|
493
494
|
);
|
|
494
495
|
}
|
|
495
496
|
|
|
497
|
+
async function pruneOwnedLegacyTransportAgents(home, opts) {
|
|
498
|
+
const { dryRun = false, quiet = false } = opts ?? {};
|
|
499
|
+
let previous = [];
|
|
500
|
+
try {
|
|
501
|
+
const data = JSON.parse(
|
|
502
|
+
await fs.readFile(join(home, AGENTS_MANIFEST_REL), "utf8"),
|
|
503
|
+
);
|
|
504
|
+
if (Array.isArray(data?.agents)) previous = data.agents;
|
|
505
|
+
} catch (err) {
|
|
506
|
+
if (err.code !== "ENOENT" && !(err instanceof SyntaxError)) throw err;
|
|
507
|
+
}
|
|
508
|
+
const ownedLegacy = LEGACY_TRANSPORT_AGENT_NAMES.filter((name) => (
|
|
509
|
+
previous.includes(name)
|
|
510
|
+
));
|
|
511
|
+
for (const name of ownedLegacy) {
|
|
512
|
+
const target = join(CLAUDE_AGENTS_DIR, name);
|
|
513
|
+
if (dryRun) {
|
|
514
|
+
process.stdout.write(`[dry-run] remove retired agent ${target}\n`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
await fs.unlink(target);
|
|
519
|
+
if (!quiet) process.stdout.write(` agents/${name}: removed retired wrapper\n`);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err.code !== "ENOENT") throw err;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
496
526
|
async function writeRuntimeManifest(home, runtime, opts) {
|
|
497
527
|
const { dryRun = false, resolution = null, installedAssets = null } = opts ?? {};
|
|
498
528
|
const data = buildRuntimeManifest(
|
|
@@ -574,11 +604,7 @@ async function installAgentsLink(repoAbs, opts) {
|
|
|
574
604
|
const { dryRun, quiet } = opts;
|
|
575
605
|
const srcRoot = join(repoAbs, "agents", "workers");
|
|
576
606
|
const sourceNames = await listWorkerFiles(srcRoot);
|
|
577
|
-
const
|
|
578
|
-
const names = [
|
|
579
|
-
...sourceNames,
|
|
580
|
-
...renderedDefinitions.map(({ name }) => name),
|
|
581
|
-
].sort();
|
|
607
|
+
const names = [...sourceNames].sort();
|
|
582
608
|
if (names.length === 0) {
|
|
583
609
|
if (!quiet) process.stdout.write(" agents: <repo>/agents/workers missing — skipped\n");
|
|
584
610
|
return { installed: [] };
|
|
@@ -590,15 +616,6 @@ async function installAgentsLink(repoAbs, opts) {
|
|
|
590
616
|
const action = await ensureSymlink(src, dst, { dryRun });
|
|
591
617
|
if (!quiet) process.stdout.write(` agents/${name}: ${action}\n`);
|
|
592
618
|
}
|
|
593
|
-
for (const { name, content } of renderedDefinitions) {
|
|
594
|
-
const dst = join(CLAUDE_AGENTS_DIR, name);
|
|
595
|
-
if (dryRun) {
|
|
596
|
-
process.stdout.write(`[dry-run] render ${name} -> ${dst}\n`);
|
|
597
|
-
} else {
|
|
598
|
-
await writeFileAtomic(dst, content, 0o644);
|
|
599
|
-
}
|
|
600
|
-
if (!quiet) process.stdout.write(` agents/${name}: rendered\n`);
|
|
601
|
-
}
|
|
602
619
|
return { installed: names };
|
|
603
620
|
}
|
|
604
621
|
|
|
@@ -900,6 +917,7 @@ export async function runInstall(args) {
|
|
|
900
917
|
|
|
901
918
|
let agentResult = { installed: [] };
|
|
902
919
|
if (skillTargets.some((target) => target.provider === "claude")) {
|
|
920
|
+
await pruneOwnedLegacyTransportAgents(paths.home, opts);
|
|
903
921
|
agentResult = await installAgentsCopy(runtimeRoot, opts);
|
|
904
922
|
}
|
|
905
923
|
await writeAgentsManifest(paths.home, agentResult.installed, { dryRun: opts.dryRun });
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
AGENTS_MANIFEST_REL,
|
|
12
12
|
INSTALLED_FILES,
|
|
13
13
|
INSTALLED_TREES,
|
|
14
|
+
LEGACY_TRANSPORT_AGENT_NAMES,
|
|
14
15
|
SKILLS_MANIFEST_REL,
|
|
15
16
|
} from "../../lib/install-assets.mjs";
|
|
16
17
|
|
|
@@ -23,11 +24,11 @@ export const FALLBACK_SKILL_NAMES = [
|
|
|
23
24
|
...new Set([...USER_SKILL_NAMES, ...OBSOLETE_SKILL_NAMES]),
|
|
24
25
|
];
|
|
25
26
|
|
|
26
|
-
const FALLBACK_AGENT_NAMES = [
|
|
27
|
+
export const FALLBACK_AGENT_NAMES = [
|
|
27
28
|
"claude-worker.md",
|
|
28
|
-
"codex-worker.md",
|
|
29
|
-
"antigravity-worker.md",
|
|
30
29
|
"report-writer-worker.md",
|
|
30
|
+
"translator-worker.md",
|
|
31
|
+
...LEGACY_TRANSPORT_AGENT_NAMES,
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
const USER_HOME = homedir();
|
|
@@ -35,3 +35,12 @@ export const INSTALLED_FILES = [
|
|
|
35
35
|
|
|
36
36
|
export const SKILLS_MANIFEST_REL = "installed-skills.json";
|
|
37
37
|
export const AGENTS_MANIFEST_REL = "installed-agents.json";
|
|
38
|
+
|
|
39
|
+
// Exact names of the retired LLM transport wrappers. Install removes an entry
|
|
40
|
+
// only when the previous agent manifest proves that Okstra installed it.
|
|
41
|
+
export const LEGACY_TRANSPORT_AGENT_NAMES = Object.freeze([
|
|
42
|
+
"codex-worker.md",
|
|
43
|
+
"antigravity-worker.md",
|
|
44
|
+
"grok-worker.md",
|
|
45
|
+
"kimi-worker.md",
|
|
46
|
+
]);
|