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
|
@@ -11,6 +11,16 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any, Callable, Literal, Mapping, Sequence
|
|
12
12
|
|
|
13
13
|
from .final_report_paths import final_report_data_path
|
|
14
|
+
from .agent_invocation import (
|
|
15
|
+
AgentInstruction,
|
|
16
|
+
AgentInstructionSource,
|
|
17
|
+
AgentInvocationError,
|
|
18
|
+
AgentInvocationRequest,
|
|
19
|
+
agent_model_assignment_from_payload,
|
|
20
|
+
compose_agent_prompt,
|
|
21
|
+
prepare_agent_invocation,
|
|
22
|
+
verify_agent_invocation,
|
|
23
|
+
)
|
|
14
24
|
from .path_hints import hydrate_active_run_context
|
|
15
25
|
from .report_language import resolve_report_language
|
|
16
26
|
from .worker_prompt_body import (
|
|
@@ -101,6 +111,30 @@ _CLARIFICATION_AUTHORITY_INTRO = (
|
|
|
101
111
|
_CLARIFICATION_SOURCE_PREFIX = "Source:"
|
|
102
112
|
_REQUIRED_PROMPT_RESOURCES_HEADING = "## Required prompt resources"
|
|
103
113
|
_HTML_COMMENTS = re.compile(r"<!--.*?-->\n?", re.DOTALL)
|
|
114
|
+
_AGENT_CONTRACT_KEYS = {
|
|
115
|
+
"schemaVersion",
|
|
116
|
+
"dutyRootPath",
|
|
117
|
+
"catalogDigest",
|
|
118
|
+
"invocationReservationRootPath",
|
|
119
|
+
"allowedAudiences",
|
|
120
|
+
"authorizedPaths",
|
|
121
|
+
}
|
|
122
|
+
_MODEL_ASSIGNMENT_KEYS = {
|
|
123
|
+
"provider",
|
|
124
|
+
"model",
|
|
125
|
+
"modelExecutionValue",
|
|
126
|
+
"runner",
|
|
127
|
+
"hostRuntime",
|
|
128
|
+
"hostModelValue",
|
|
129
|
+
}
|
|
130
|
+
_RUN_INVOCATION_PATH_KEYS = {
|
|
131
|
+
"runManifestPath",
|
|
132
|
+
"workerPromptsDirectoryPath",
|
|
133
|
+
"leadInstructionsPath",
|
|
134
|
+
"leadExecutionPromptPath",
|
|
135
|
+
"leadPromptMetadataPath",
|
|
136
|
+
"invocationReservationRootPath",
|
|
137
|
+
}
|
|
104
138
|
|
|
105
139
|
|
|
106
140
|
@dataclass(frozen=True)
|
|
@@ -198,6 +232,7 @@ def _load_materialization_context(
|
|
|
198
232
|
)
|
|
199
233
|
manifest_path = _resolve_input_path(project_root, request.run_manifest_path)
|
|
200
234
|
manifest = _load_json_object(manifest_path, "run manifest")
|
|
235
|
+
_validate_invocation_manifest_authority(manifest)
|
|
201
236
|
team_state = _load_referenced_json(project_root, manifest, "teamStatePath")
|
|
202
237
|
active_context = hydrate_active_run_context(
|
|
203
238
|
_load_referenced_json(
|
|
@@ -215,6 +250,39 @@ def _load_materialization_context(
|
|
|
215
250
|
)
|
|
216
251
|
|
|
217
252
|
|
|
253
|
+
def _validate_invocation_manifest_authority(
|
|
254
|
+
manifest: Mapping[str, Any],
|
|
255
|
+
) -> None:
|
|
256
|
+
has_contract = "agentContract" in manifest
|
|
257
|
+
has_assignments = "invocationAssignments" in manifest
|
|
258
|
+
if not has_contract and not has_assignments:
|
|
259
|
+
return
|
|
260
|
+
contract = manifest.get("agentContract")
|
|
261
|
+
assignments = manifest.get("invocationAssignments")
|
|
262
|
+
complete = (
|
|
263
|
+
isinstance(contract, Mapping)
|
|
264
|
+
and set(contract) == _AGENT_CONTRACT_KEYS
|
|
265
|
+
and contract.get("schemaVersion") == 1
|
|
266
|
+
and isinstance(assignments, Mapping)
|
|
267
|
+
and bool(assignments)
|
|
268
|
+
and all(
|
|
269
|
+
isinstance(item, Mapping)
|
|
270
|
+
and set(item) == _MODEL_ASSIGNMENT_KEYS
|
|
271
|
+
for item in assignments.values()
|
|
272
|
+
)
|
|
273
|
+
and all(
|
|
274
|
+
isinstance(manifest.get(key), str)
|
|
275
|
+
and bool(manifest.get(key))
|
|
276
|
+
for key in _RUN_INVOCATION_PATH_KEYS
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
if not complete:
|
|
280
|
+
raise InitialPromptMaterializationError(
|
|
281
|
+
"required_input_missing",
|
|
282
|
+
"new invocation manifest is incomplete",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
218
286
|
def _resolve_prompt_item(
|
|
219
287
|
context: _MaterializationContext,
|
|
220
288
|
worker: InitialPromptWorkerRequest,
|
|
@@ -312,12 +380,23 @@ def _render_prompt(
|
|
|
312
380
|
context: _MaterializationContext,
|
|
313
381
|
item: _PromptItem,
|
|
314
382
|
) -> str:
|
|
383
|
+
request = _agent_invocation_request(context, item)
|
|
384
|
+
if request is not None:
|
|
385
|
+
return compose_agent_prompt(request)
|
|
386
|
+
anchors, body = _render_prompt_parts(context, item)
|
|
387
|
+
return "\n".join([*anchors, *body]).rstrip() + "\n"
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _render_prompt_parts(
|
|
391
|
+
context: _MaterializationContext,
|
|
392
|
+
item: _PromptItem,
|
|
393
|
+
) -> tuple[list[str], list[str]]:
|
|
315
394
|
worker = item.worker
|
|
316
395
|
state = _worker_state(context.team_state, worker.worker_id)
|
|
317
396
|
prompt_rel = _project_relative(context.project_root, item.final_path)
|
|
318
397
|
result_rel, audit_source_rel = _result_paths(context, state, worker.worker_id)
|
|
319
398
|
try:
|
|
320
|
-
|
|
399
|
+
anchors = worker_prompt_headers(
|
|
321
400
|
project_root=context.project_root,
|
|
322
401
|
prompt_rel=prompt_rel,
|
|
323
402
|
result_rel=result_rel,
|
|
@@ -327,12 +406,12 @@ def _render_prompt(
|
|
|
327
406
|
manifest=context.manifest,
|
|
328
407
|
active_context=context.active_context,
|
|
329
408
|
)
|
|
330
|
-
|
|
409
|
+
anchors.append(
|
|
331
410
|
f"**Prompt Delivery Mode:** {context.request.delivery_mode.value}"
|
|
332
411
|
)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
412
|
+
anchors.extend(_implementation_anchor_lines(context, item.plan))
|
|
413
|
+
body = _prompt_body(context, state, worker, item.plan)
|
|
414
|
+
body.extend(_resource_lines(context, item))
|
|
336
415
|
except InitialPromptMaterializationError:
|
|
337
416
|
raise
|
|
338
417
|
except Exception as exc:
|
|
@@ -340,7 +419,62 @@ def _render_prompt(
|
|
|
340
419
|
"render_failed",
|
|
341
420
|
f"cannot compose initial prompt for {worker.worker_id}: {exc}",
|
|
342
421
|
) from exc
|
|
343
|
-
return
|
|
422
|
+
return anchors, body
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _agent_invocation_request(
|
|
426
|
+
context: _MaterializationContext,
|
|
427
|
+
item: _PromptItem,
|
|
428
|
+
) -> AgentInvocationRequest | None:
|
|
429
|
+
contract = context.manifest.get("agentContract")
|
|
430
|
+
if not isinstance(contract, Mapping):
|
|
431
|
+
return None
|
|
432
|
+
assignment_ref = f"initial/{item.worker.worker_id}"
|
|
433
|
+
assignments = context.manifest.get("invocationAssignments")
|
|
434
|
+
payload = assignments.get(assignment_ref) if isinstance(assignments, Mapping) else None
|
|
435
|
+
try:
|
|
436
|
+
assignment = agent_model_assignment_from_payload(payload)
|
|
437
|
+
except AgentInvocationError as exc:
|
|
438
|
+
raise InitialPromptMaterializationError(
|
|
439
|
+
"required_input_missing", str(exc)
|
|
440
|
+
) from exc
|
|
441
|
+
if assignment.model_execution_value != item.worker.model:
|
|
442
|
+
raise InitialPromptMaterializationError(
|
|
443
|
+
"required_input_missing",
|
|
444
|
+
f"requested model does not match {assignment_ref}",
|
|
445
|
+
)
|
|
446
|
+
anchors, body = _render_prompt_parts(context, item)
|
|
447
|
+
source_paths = (
|
|
448
|
+
AgentInstructionSource(
|
|
449
|
+
"project",
|
|
450
|
+
_project_relative(context.project_root, context.request.run_manifest_path),
|
|
451
|
+
),
|
|
452
|
+
AgentInstructionSource(
|
|
453
|
+
"project", _required_string(context.manifest, "activeRunContextPath")
|
|
454
|
+
),
|
|
455
|
+
AgentInstructionSource(
|
|
456
|
+
"project", _required_string(context.manifest, "teamStatePath")
|
|
457
|
+
),
|
|
458
|
+
)
|
|
459
|
+
return AgentInvocationRequest(
|
|
460
|
+
invocation_id=f"initial-{item.worker.worker_id}",
|
|
461
|
+
worker_id=item.worker.worker_id,
|
|
462
|
+
audience=item.plan.duty_audience,
|
|
463
|
+
assignment_ref=assignment_ref,
|
|
464
|
+
purpose=None,
|
|
465
|
+
assignment=assignment,
|
|
466
|
+
instruction=AgentInstruction(
|
|
467
|
+
anchor_lines=tuple(anchors),
|
|
468
|
+
body="\n".join(body).rstrip() + "\n",
|
|
469
|
+
source_paths=source_paths,
|
|
470
|
+
),
|
|
471
|
+
project_root=context.project_root,
|
|
472
|
+
run_manifest_path=context.request.run_manifest_path,
|
|
473
|
+
duty_root=context.project_root / _required_string(contract, "dutyRootPath"),
|
|
474
|
+
prompt_path=item.final_path,
|
|
475
|
+
metadata_path=item.final_path.with_name(item.final_path.name + ".meta.json"),
|
|
476
|
+
dispatch_kind="initial",
|
|
477
|
+
)
|
|
344
478
|
|
|
345
479
|
|
|
346
480
|
def _prompt_body(
|
|
@@ -366,7 +500,6 @@ def _prompt_body(
|
|
|
366
500
|
context.active_context,
|
|
367
501
|
worker.worker_id,
|
|
368
502
|
worker.model,
|
|
369
|
-
_worker_role(plan),
|
|
370
503
|
plan,
|
|
371
504
|
)
|
|
372
505
|
|
|
@@ -607,15 +740,30 @@ def _publish_or_reuse(
|
|
|
607
740
|
) -> _PublishedPrompt:
|
|
608
741
|
if item.existed:
|
|
609
742
|
return _published_prompt(item, existed=True)
|
|
610
|
-
|
|
743
|
+
invocation = _agent_invocation_request(context, item)
|
|
744
|
+
if invocation is None:
|
|
745
|
+
temp_path = _required_temp_path(item)
|
|
746
|
+
try:
|
|
747
|
+
os.link(temp_path, item.final_path)
|
|
748
|
+
except FileExistsError:
|
|
749
|
+
_validate_existing_prompt(context, item)
|
|
750
|
+
return _published_prompt(item, existed=True)
|
|
751
|
+
except OSError as exc:
|
|
752
|
+
raise InitialPromptMaterializationError(
|
|
753
|
+
"publication_failed",
|
|
754
|
+
f"cannot publish initial prompt {item.final_path}: {exc}",
|
|
755
|
+
) from exc
|
|
756
|
+
return _published_prompt(item, existed=False)
|
|
611
757
|
try:
|
|
612
|
-
|
|
613
|
-
except
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
758
|
+
prepare_agent_invocation(invocation)
|
|
759
|
+
except AgentInvocationError as exc:
|
|
760
|
+
reason: MaterializationReason = (
|
|
761
|
+
"existing_prompt_conflict"
|
|
762
|
+
if exc.reason == "existing_invocation_conflict"
|
|
763
|
+
else "publication_failed"
|
|
764
|
+
)
|
|
617
765
|
raise InitialPromptMaterializationError(
|
|
618
|
-
|
|
766
|
+
reason,
|
|
619
767
|
f"cannot publish initial prompt {item.final_path}: {exc}",
|
|
620
768
|
) from exc
|
|
621
769
|
return _published_prompt(item, existed=False)
|
|
@@ -625,6 +773,7 @@ def _validate_existing_prompt(
|
|
|
625
773
|
context: _MaterializationContext,
|
|
626
774
|
item: _PromptItem,
|
|
627
775
|
) -> None:
|
|
776
|
+
expects_invocation = isinstance(context.manifest.get("agentContract"), Mapping)
|
|
628
777
|
text = _require_readable_prompt(
|
|
629
778
|
item.final_path,
|
|
630
779
|
item.worker.worker_id,
|
|
@@ -636,6 +785,10 @@ def _validate_existing_prompt(
|
|
|
636
785
|
item.worker.worker_id,
|
|
637
786
|
"initial",
|
|
638
787
|
item.final_path,
|
|
788
|
+
metadata_path=_metadata_path(item.final_path) if expects_invocation else None,
|
|
789
|
+
expected_duty_audience=(
|
|
790
|
+
item.plan.duty_audience if expects_invocation else None
|
|
791
|
+
),
|
|
639
792
|
)
|
|
640
793
|
basic_errors = validate_initial_prompt_records(
|
|
641
794
|
manifest=context.manifest,
|
|
@@ -653,6 +806,10 @@ def _validate_existing_prompt(
|
|
|
653
806
|
item.final_path,
|
|
654
807
|
expected_model=item.worker.model,
|
|
655
808
|
expected_delivery_mode=context.request.delivery_mode.value,
|
|
809
|
+
metadata_path=_metadata_path(item.final_path) if expects_invocation else None,
|
|
810
|
+
expected_duty_audience=(
|
|
811
|
+
item.plan.duty_audience if expects_invocation else None
|
|
812
|
+
),
|
|
656
813
|
)
|
|
657
814
|
expected_errors = validate_initial_prompt_records(
|
|
658
815
|
manifest=context.manifest,
|
|
@@ -665,6 +822,23 @@ def _validate_existing_prompt(
|
|
|
665
822
|
reason,
|
|
666
823
|
"existing initial prompt metadata: " + "; ".join(expected_errors),
|
|
667
824
|
)
|
|
825
|
+
invocation = _agent_invocation_request(context, item)
|
|
826
|
+
if invocation is not None:
|
|
827
|
+
errors = verify_agent_invocation(
|
|
828
|
+
invocation.metadata_path,
|
|
829
|
+
project_root=context.project_root,
|
|
830
|
+
expected_run_manifest_path=context.request.run_manifest_path,
|
|
831
|
+
expected_assignment=invocation.assignment,
|
|
832
|
+
expected_invocation_id=invocation.invocation_id,
|
|
833
|
+
expected_worker_id=invocation.worker_id,
|
|
834
|
+
expected_assignment_ref=invocation.assignment_ref,
|
|
835
|
+
expected_audience=invocation.audience,
|
|
836
|
+
)
|
|
837
|
+
if errors:
|
|
838
|
+
raise InitialPromptMaterializationError(
|
|
839
|
+
"existing_prompt_invalid",
|
|
840
|
+
"existing initial prompt invocation: " + "; ".join(errors),
|
|
841
|
+
)
|
|
668
842
|
|
|
669
843
|
|
|
670
844
|
def _validate_existing_clarification_metadata(
|
|
@@ -854,12 +1028,20 @@ def _item_record(
|
|
|
854
1028
|
item: _PromptItem,
|
|
855
1029
|
) -> PromptRecord:
|
|
856
1030
|
path = item.final_path if item.existed else _required_temp_path(item)
|
|
1031
|
+
expects_invocation = (
|
|
1032
|
+
item.existed
|
|
1033
|
+
and isinstance(context.manifest.get("agentContract"), Mapping)
|
|
1034
|
+
)
|
|
857
1035
|
return PromptRecord(
|
|
858
1036
|
item.worker.worker_id,
|
|
859
1037
|
"initial",
|
|
860
1038
|
path,
|
|
861
1039
|
expected_model=item.worker.model,
|
|
862
1040
|
expected_delivery_mode=context.request.delivery_mode.value,
|
|
1041
|
+
metadata_path=_metadata_path(item.final_path) if expects_invocation else None,
|
|
1042
|
+
expected_duty_audience=(
|
|
1043
|
+
item.plan.duty_audience if expects_invocation else None
|
|
1044
|
+
),
|
|
863
1045
|
)
|
|
864
1046
|
|
|
865
1047
|
|
|
@@ -867,15 +1049,24 @@ def _published_record(
|
|
|
867
1049
|
context: _MaterializationContext,
|
|
868
1050
|
prompt: _PublishedPrompt,
|
|
869
1051
|
) -> PromptRecord:
|
|
1052
|
+
expects_invocation = isinstance(context.manifest.get("agentContract"), Mapping)
|
|
870
1053
|
return PromptRecord(
|
|
871
1054
|
prompt.worker.worker_id,
|
|
872
1055
|
"initial",
|
|
873
1056
|
prompt.path,
|
|
874
1057
|
expected_model=prompt.worker.model,
|
|
875
1058
|
expected_delivery_mode=context.request.delivery_mode.value,
|
|
1059
|
+
metadata_path=_metadata_path(prompt.path) if expects_invocation else None,
|
|
1060
|
+
expected_duty_audience=(
|
|
1061
|
+
prompt.plan.duty_audience if expects_invocation else None
|
|
1062
|
+
),
|
|
876
1063
|
)
|
|
877
1064
|
|
|
878
1065
|
|
|
1066
|
+
def _metadata_path(prompt_path: Path) -> Path:
|
|
1067
|
+
return prompt_path.with_name(prompt_path.name + ".meta.json")
|
|
1068
|
+
|
|
1069
|
+
|
|
879
1070
|
def _worker_prompt_path(
|
|
880
1071
|
context: _MaterializationContext,
|
|
881
1072
|
worker_id: str,
|
|
@@ -1094,14 +1285,6 @@ def _validate_worker_request(worker: InitialPromptWorkerRequest) -> None:
|
|
|
1094
1285
|
)
|
|
1095
1286
|
|
|
1096
1287
|
|
|
1097
|
-
def _worker_role(plan: PromptPlan) -> str:
|
|
1098
|
-
if plan.audience == "implementation-executor":
|
|
1099
|
-
return "executor"
|
|
1100
|
-
if plan.audience == "implementation-verifier":
|
|
1101
|
-
return "verifier"
|
|
1102
|
-
return "worker"
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
1288
|
def _existing_metadata_reason(
|
|
1106
1289
|
errors: Sequence[str],
|
|
1107
1290
|
) -> MaterializationReason:
|
|
@@ -1164,7 +1347,15 @@ def _required_temp_path(item: _PromptItem) -> Path:
|
|
|
1164
1347
|
|
|
1165
1348
|
|
|
1166
1349
|
def _project_relative(project_root: Path, path: Path) -> str:
|
|
1167
|
-
|
|
1350
|
+
# macOS commonly exposes the same temporary directory as both `/var/...`
|
|
1351
|
+
# and `/private/var/...`; project-root resolution already canonicalizes the
|
|
1352
|
+
# former. Resolve the candidate too so provenance does not reject an alias
|
|
1353
|
+
# for the same directory. `strict=False` is required for a prompt that has
|
|
1354
|
+
# not been published yet, while still resolving every existing parent and
|
|
1355
|
+
# therefore preserving the symlink-escape check in `relative_to`.
|
|
1356
|
+
return path.resolve(strict=False).relative_to(
|
|
1357
|
+
project_root.resolve(strict=True)
|
|
1358
|
+
).as_posix()
|
|
1168
1359
|
|
|
1169
1360
|
|
|
1170
1361
|
def _resolve_input_path(project_root: Path, path: Path) -> Path:
|
|
@@ -60,6 +60,7 @@ def compact_active_run_context(
|
|
|
60
60
|
"stage": ctx.get("RUN_STAGE", ""),
|
|
61
61
|
"fixRunCarry": ctx.get("FIX_RUN_CONTEXT", ""),
|
|
62
62
|
},
|
|
63
|
+
"agentContract": dict(_mapping(payload.get("agentContract"))),
|
|
63
64
|
"inputs": _compact_active_inputs(payload),
|
|
64
65
|
"workers": _compact_active_workers(payload),
|
|
65
66
|
"executorWorktree": dict(_mapping(payload.get("executorWorktree"))),
|
|
@@ -97,6 +98,7 @@ def hydrate_active_run_context(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
97
98
|
"task": _hydrate_active_task(payload, ctx),
|
|
98
99
|
"workflow": dict(_mapping(payload.get("workflow"))),
|
|
99
100
|
"run": _hydrate_active_run(payload, ctx),
|
|
101
|
+
"agentContract": dict(_mapping(payload.get("agentContract"))),
|
|
100
102
|
"instructionSet": _hydrate_active_instruction_set(payload, ctx),
|
|
101
103
|
"workers": _hydrate_active_workers(payload, ctx),
|
|
102
104
|
"errorLogs": _hydrate_active_error_logs(ctx),
|
|
@@ -416,6 +418,12 @@ def _run_files(
|
|
|
416
418
|
"run_context_file": run_manifests / f"run-context-{task_type_segment}-{sequences['manifests']}.json",
|
|
417
419
|
"run_inputs_file": run_manifests / f"run-inputs-{task_type_segment}-{sequences['manifests']}.json",
|
|
418
420
|
"run_prompt_snapshot": run_prompts / f"lead-execution-prompt{suffixes['prompts']}.md",
|
|
421
|
+
"duty_contract_root": run_prompts / f"duty-contracts{suffixes['prompts']}",
|
|
422
|
+
"lead_instructions": run_prompts / f"lead-instructions{suffixes['prompts']}.md",
|
|
423
|
+
"lead_prompt_metadata": (
|
|
424
|
+
run_prompts / f"lead-execution-prompt{suffixes['prompts']}.md.meta.json"
|
|
425
|
+
),
|
|
426
|
+
"invocation_reservation_root": run_prompts / ".agent-invocations",
|
|
419
427
|
"claude_worker_prompt": run_prompts / f"claude-worker-prompt{suffixes['prompts']}.md",
|
|
420
428
|
"codex_worker_prompt": run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md",
|
|
421
429
|
"antigravity_worker_prompt": run_prompts / f"antigravity-worker-prompt{suffixes['prompts']}.md",
|
|
@@ -535,6 +543,10 @@ def _absolute_run_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
535
543
|
"RUN_MANIFEST_PATH": str(paths["run_manifest_file"]),
|
|
536
544
|
"RUN_CONTEXT_FILE": str(paths["run_context_file"]),
|
|
537
545
|
"RUN_PROMPT_SNAPSHOT_FILE": str(paths["run_prompt_snapshot"]),
|
|
546
|
+
"DUTY_CONTRACT_ROOT": str(paths["duty_contract_root"]),
|
|
547
|
+
"LEAD_INSTRUCTIONS_PATH": str(paths["lead_instructions"]),
|
|
548
|
+
"LEAD_PROMPT_METADATA_PATH": str(paths["lead_prompt_metadata"]),
|
|
549
|
+
"INVOCATION_RESERVATION_ROOT": str(paths["invocation_reservation_root"]),
|
|
538
550
|
"FINAL_REPORT_PATH": str(paths["final_report"]),
|
|
539
551
|
"CONVERGENCE_STATE_PATH": str(paths["convergence_state"]),
|
|
540
552
|
"FINAL_STATUS_PATH": str(paths["final_status"]),
|
|
@@ -569,6 +581,8 @@ def _filename_fields(paths: Mapping[str, Any]) -> dict[str, str]:
|
|
|
569
581
|
return {
|
|
570
582
|
"RUN_MANIFEST_FILENAME": Path(paths["run_manifest_file"]).name,
|
|
571
583
|
"RUN_PROMPT_SNAPSHOT_FILENAME": Path(paths["run_prompt_snapshot"]).name,
|
|
584
|
+
"LEAD_INSTRUCTIONS_FILENAME": Path(paths["lead_instructions"]).name,
|
|
585
|
+
"LEAD_PROMPT_METADATA_FILENAME": Path(paths["lead_prompt_metadata"]).name,
|
|
572
586
|
"FINAL_REPORT_FILENAME": Path(paths["final_report"]).name,
|
|
573
587
|
"FINAL_STATUS_FILENAME": Path(paths["final_status"]).name,
|
|
574
588
|
"CLAUDE_RESUME_COMMAND_FILENAME": Path(paths["claude_resume_command"]).name,
|
|
@@ -621,6 +635,18 @@ def _relative_file_fields(project_root: Path, paths: Mapping[str, Any]) -> dict[
|
|
|
621
635
|
"RUN_CONTEXT_RELATIVE_PATH": _rel(project_root, paths["run_context_file"]),
|
|
622
636
|
"RUN_INPUTS_RELATIVE_PATH": _rel(project_root, paths["run_inputs_file"]),
|
|
623
637
|
"RUN_PROMPT_SNAPSHOT_RELATIVE_PATH": _rel(project_root, paths["run_prompt_snapshot"]),
|
|
638
|
+
"DUTY_CONTRACT_ROOT_RELATIVE_PATH": _rel(
|
|
639
|
+
project_root, paths["duty_contract_root"]
|
|
640
|
+
),
|
|
641
|
+
"LEAD_INSTRUCTIONS_RELATIVE_PATH": _rel(
|
|
642
|
+
project_root, paths["lead_instructions"]
|
|
643
|
+
),
|
|
644
|
+
"LEAD_PROMPT_METADATA_RELATIVE_PATH": _rel(
|
|
645
|
+
project_root, paths["lead_prompt_metadata"]
|
|
646
|
+
),
|
|
647
|
+
"INVOCATION_RESERVATION_ROOT_RELATIVE_PATH": _rel(
|
|
648
|
+
project_root, paths["invocation_reservation_root"]
|
|
649
|
+
),
|
|
624
650
|
"CLAUDE_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["claude_worker_prompt"]),
|
|
625
651
|
"CODEX_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["codex_worker_prompt"]),
|
|
626
652
|
"ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH": _rel(project_root, paths["antigravity_worker_prompt"]),
|
|
@@ -524,6 +524,16 @@ def compute_run_paths(
|
|
|
524
524
|
|
|
525
525
|
run_manifest_file = run_manifests / f"run-manifest{suffixes['manifests']}.json"
|
|
526
526
|
run_prompt_snapshot = run_prompts / f"lead-execution-prompt{suffixes['prompts']}.md"
|
|
527
|
+
duty_contract_root = (
|
|
528
|
+
run_prompts / f"duty-contracts{suffixes['prompts']}"
|
|
529
|
+
)
|
|
530
|
+
lead_instructions = (
|
|
531
|
+
run_prompts / f"lead-instructions{suffixes['prompts']}.md"
|
|
532
|
+
)
|
|
533
|
+
lead_prompt_metadata = run_prompt_snapshot.with_name(
|
|
534
|
+
run_prompt_snapshot.name + ".meta.json"
|
|
535
|
+
)
|
|
536
|
+
invocation_reservation_root = run_prompts / ".agent-invocations"
|
|
527
537
|
claude_worker_prompt = run_prompts / f"claude-worker-prompt{suffixes['prompts']}.md"
|
|
528
538
|
codex_worker_prompt = run_prompts / f"codex-worker-prompt{suffixes['prompts']}.md"
|
|
529
539
|
antigravity_worker_prompt = run_prompts / f"antigravity-worker-prompt{suffixes['prompts']}.md"
|
|
@@ -625,6 +635,10 @@ def compute_run_paths(
|
|
|
625
635
|
"RUN_CARRY_PATH": str(run_carry),
|
|
626
636
|
"RUN_MANIFEST_PATH": str(run_manifest_file),
|
|
627
637
|
"RUN_PROMPT_SNAPSHOT_FILE": str(run_prompt_snapshot),
|
|
638
|
+
"DUTY_CONTRACT_ROOT": str(duty_contract_root),
|
|
639
|
+
"LEAD_INSTRUCTIONS_PATH": str(lead_instructions),
|
|
640
|
+
"LEAD_PROMPT_METADATA_PATH": str(lead_prompt_metadata),
|
|
641
|
+
"INVOCATION_RESERVATION_ROOT": str(invocation_reservation_root),
|
|
628
642
|
"CLAUDE_WORKER_PROMPT_FILE": str(claude_worker_prompt),
|
|
629
643
|
"CODEX_WORKER_PROMPT_FILE": str(codex_worker_prompt),
|
|
630
644
|
"ANTIGRAVITY_WORKER_PROMPT_FILE": str(antigravity_worker_prompt),
|
|
@@ -652,6 +666,8 @@ def compute_run_paths(
|
|
|
652
666
|
"RUN_VALIDATOR_PATH": str(run_validator_script),
|
|
653
667
|
"RUN_MANIFEST_FILENAME": run_manifest_file.name,
|
|
654
668
|
"RUN_PROMPT_SNAPSHOT_FILENAME": run_prompt_snapshot.name,
|
|
669
|
+
"LEAD_INSTRUCTIONS_FILENAME": lead_instructions.name,
|
|
670
|
+
"LEAD_PROMPT_METADATA_FILENAME": lead_prompt_metadata.name,
|
|
655
671
|
"FINAL_REPORT_FILENAME": final_report.name,
|
|
656
672
|
"FINAL_STATUS_FILENAME": final_status.name,
|
|
657
673
|
"CLAUDE_RESUME_COMMAND_FILENAME": claude_resume_command.name,
|
|
@@ -688,6 +704,10 @@ def compute_run_paths(
|
|
|
688
704
|
("RUN_SESSIONS_RELATIVE_PATH", run_sessions),
|
|
689
705
|
("RUN_MANIFEST_RELATIVE_PATH", run_manifest_file),
|
|
690
706
|
("RUN_PROMPT_SNAPSHOT_RELATIVE_PATH", run_prompt_snapshot),
|
|
707
|
+
("DUTY_CONTRACT_ROOT_RELATIVE_PATH", duty_contract_root),
|
|
708
|
+
("LEAD_INSTRUCTIONS_RELATIVE_PATH", lead_instructions),
|
|
709
|
+
("LEAD_PROMPT_METADATA_RELATIVE_PATH", lead_prompt_metadata),
|
|
710
|
+
("INVOCATION_RESERVATION_ROOT_RELATIVE_PATH", invocation_reservation_root),
|
|
691
711
|
("CLAUDE_WORKER_PROMPT_RELATIVE_PATH", claude_worker_prompt),
|
|
692
712
|
("CODEX_WORKER_PROMPT_RELATIVE_PATH", codex_worker_prompt),
|
|
693
713
|
("ANTIGRAVITY_WORKER_PROMPT_RELATIVE_PATH", antigravity_worker_prompt),
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Host-facing ports consumed by application use cases."""
|
|
2
2
|
|
|
3
3
|
from .host import HostAdapter
|
|
4
|
+
from .host_model import (
|
|
5
|
+
HostModelBindingError,
|
|
6
|
+
HostModelBindingPort,
|
|
7
|
+
HostModelBindingRequest,
|
|
8
|
+
)
|
|
4
9
|
from .interaction import InteractionPort
|
|
5
10
|
from .lead_session import LeadSessionPort
|
|
6
11
|
from .usage_accounting import UsageAccountingPort
|
|
@@ -8,6 +13,9 @@ from .worker_dispatch import WorkerDispatchPort
|
|
|
8
13
|
|
|
9
14
|
__all__ = (
|
|
10
15
|
"HostAdapter",
|
|
16
|
+
"HostModelBindingError",
|
|
17
|
+
"HostModelBindingPort",
|
|
18
|
+
"HostModelBindingRequest",
|
|
11
19
|
"InteractionPort",
|
|
12
20
|
"LeadSessionPort",
|
|
13
21
|
"UsageAccountingPort",
|
|
@@ -11,6 +11,7 @@ from ..domain.host import (
|
|
|
11
11
|
HostSessionContext,
|
|
12
12
|
)
|
|
13
13
|
from .interaction import InteractionPort
|
|
14
|
+
from .host_model import HostModelBindingPort
|
|
14
15
|
from .lead_session import LeadSessionPort
|
|
15
16
|
from .usage_accounting import UsageAccountingPort
|
|
16
17
|
from .worker_dispatch import WorkerDispatchPort
|
|
@@ -25,6 +26,8 @@ class HostAdapter(Protocol):
|
|
|
25
26
|
|
|
26
27
|
def interaction(self) -> InteractionPort: ...
|
|
27
28
|
|
|
29
|
+
def host_model(self) -> HostModelBindingPort: ...
|
|
30
|
+
|
|
28
31
|
def lead_session(self) -> LeadSessionPort: ...
|
|
29
32
|
|
|
30
33
|
def worker_dispatch(self) -> WorkerDispatchPort: ...
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Host-owned binding from provider model values to native call arguments."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HostModelBindingError(ValueError):
|
|
9
|
+
"""Raised when a host has no explicit native model mapping."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class HostModelBindingRequest:
|
|
14
|
+
host_runtime: str
|
|
15
|
+
provider: str
|
|
16
|
+
model: str
|
|
17
|
+
model_execution_value: str
|
|
18
|
+
runner: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HostModelBindingPort(Protocol):
|
|
22
|
+
def resolve(self, request: HostModelBindingRequest) -> str | None: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FailClosedHostModelBindingPort:
|
|
26
|
+
def __init__(self, host_runtime: str) -> None:
|
|
27
|
+
self._host_runtime = host_runtime
|
|
28
|
+
|
|
29
|
+
def resolve(self, request: HostModelBindingRequest) -> str | None:
|
|
30
|
+
if request.runner == "cli-wrapper":
|
|
31
|
+
return None
|
|
32
|
+
raise HostModelBindingError(
|
|
33
|
+
"unsupported host model: "
|
|
34
|
+
f"host={self._host_runtime!r}, provider={request.provider!r}, "
|
|
35
|
+
f"model={request.model_execution_value!r}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NativeExecutionValueHostModelBindingPort:
|
|
40
|
+
"""Explicit policy for hosts whose native API accepts provider CLI values."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, host_runtime: str, native_provider: str) -> None:
|
|
43
|
+
self._host_runtime = host_runtime
|
|
44
|
+
self._native_provider = native_provider
|
|
45
|
+
|
|
46
|
+
def resolve(self, request: HostModelBindingRequest) -> str | None:
|
|
47
|
+
if request.runner == "cli-wrapper":
|
|
48
|
+
return None
|
|
49
|
+
if (
|
|
50
|
+
request.runner != "native-session"
|
|
51
|
+
or request.host_runtime != self._host_runtime
|
|
52
|
+
or request.provider != self._native_provider
|
|
53
|
+
or not request.model_execution_value.strip()
|
|
54
|
+
):
|
|
55
|
+
raise HostModelBindingError(
|
|
56
|
+
"unsupported host model: "
|
|
57
|
+
f"host={request.host_runtime!r}, provider={request.provider!r}, "
|
|
58
|
+
f"model={request.model_execution_value!r}"
|
|
59
|
+
)
|
|
60
|
+
return request.model_execution_value
|
|
@@ -6,8 +6,8 @@ release-handoff 단계에서 lead 가 PR 본문을 작성할 때 사용하는
|
|
|
6
6
|
1. per-run override (okstra-run Step 6 에서 입력)
|
|
7
7
|
2. project: <project_root>/.okstra/project.json 의 ``prTemplatePath``
|
|
8
8
|
3. global: ~/.okstra/config.json 의 ``prTemplatePath``
|
|
9
|
-
4. default: ``$
|
|
10
|
-
|
|
9
|
+
4. default: ``$OKSTRA_SKILLS_DIR/okstra-run/templates/pr-body.template.md``
|
|
10
|
+
(env 가 설정된 경우에만) → ``$OKSTRA_HOME/templates/pr/pr-body.template.md``
|
|
11
11
|
|
|
12
12
|
경로는 절대경로 또는 ``~`` 시작 경로를 권장한다. 상대경로일 경우 project
|
|
13
13
|
스코프는 ``project_root`` 기준, override 는 호출자 cwd 기준으로 해석한다.
|
|
@@ -41,10 +41,7 @@ def _default_template_candidates() -> list[Path]:
|
|
|
41
41
|
env_dir = os.environ.get("OKSTRA_SKILLS_DIR", "").strip()
|
|
42
42
|
if env_dir:
|
|
43
43
|
out.append(Path(env_dir) / "okstra-run" / "templates" / _DEFAULT_FILENAME)
|
|
44
|
-
out.append(okstra_home() / "templates" / "
|
|
45
|
-
out.append(
|
|
46
|
-
Path.home() / ".claude" / "skills" / "okstra-run" / "templates" / _DEFAULT_FILENAME
|
|
47
|
-
)
|
|
44
|
+
out.append(okstra_home() / "templates" / "pr" / _DEFAULT_FILENAME)
|
|
48
45
|
return out
|
|
49
46
|
|
|
50
47
|
|
|
@@ -26,12 +26,14 @@ _ADAPTER_METHODS = (
|
|
|
26
26
|
"detect",
|
|
27
27
|
"probe",
|
|
28
28
|
"interaction",
|
|
29
|
+
"host_model",
|
|
29
30
|
"lead_session",
|
|
30
31
|
"worker_dispatch",
|
|
31
32
|
"usage_accounting",
|
|
32
33
|
)
|
|
33
34
|
_PORT_METHODS = {
|
|
34
35
|
"interaction": ("plan",),
|
|
36
|
+
"host_model": ("resolve",),
|
|
35
37
|
"lead_session": ("build_start", "build_resume"),
|
|
36
38
|
"worker_dispatch": ("build_plan",),
|
|
37
39
|
"usage_accounting": ("collect",),
|
|
@@ -52,6 +54,9 @@ class _RegisteredHostAdapter:
|
|
|
52
54
|
def interaction(self):
|
|
53
55
|
return self._adapter.interaction()
|
|
54
56
|
|
|
57
|
+
def host_model(self):
|
|
58
|
+
return self._adapter.host_model()
|
|
59
|
+
|
|
55
60
|
def lead_session(self):
|
|
56
61
|
return self._adapter.lead_session()
|
|
57
62
|
|