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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Provider-neutral dispatcher for verified CLI-backed worker assignments."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from . import dispatch_core
|
|
11
|
+
from .adapters.dispatch.cli_wrapper import CliWrapperDispatchPort
|
|
12
|
+
from .application.dispatch_assignments import dispatch_assignments
|
|
13
|
+
from .dispatch_state import DispatchError
|
|
14
|
+
from .models import provider_wrappers
|
|
15
|
+
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
SUPPORTED_CLI_WORKERS = provider_wrappers("analyser")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
22
|
+
args = _parser().parse_args(argv)
|
|
23
|
+
try:
|
|
24
|
+
if args.jobs_file and args.workers:
|
|
25
|
+
raise DispatchError("--jobs-file and --workers cannot be combined")
|
|
26
|
+
request = _dispatch_request(args)
|
|
27
|
+
port = CliWrapperDispatchPort(
|
|
28
|
+
supported_worker_wrappers=SUPPORTED_CLI_WORKERS,
|
|
29
|
+
unsupported_worker_label="worker-dispatch",
|
|
30
|
+
required_lead_runtime=None,
|
|
31
|
+
cli_wrapper_assignments_only=True,
|
|
32
|
+
default_provider_by_worker_id={"report-writer": "claude"},
|
|
33
|
+
)
|
|
34
|
+
plan = dispatch_assignments(request, port)
|
|
35
|
+
backend_plan = plan.backend_plan
|
|
36
|
+
if backend_plan is None:
|
|
37
|
+
raise DispatchError("CLI wrapper dispatch did not produce a backend plan")
|
|
38
|
+
if args.dry_run:
|
|
39
|
+
_print_json(_backend_payload(backend_plan, dry_run=True))
|
|
40
|
+
return 0
|
|
41
|
+
code = dispatch_core.dispatch_cli_wrapper_plan(backend_plan)
|
|
42
|
+
_print_json({**_backend_payload(backend_plan, dry_run=False), "exitCode": code})
|
|
43
|
+
return code
|
|
44
|
+
except DispatchError as exc:
|
|
45
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
46
|
+
return 2
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _dispatch_request(args: argparse.Namespace) -> WorkerDispatchRequest:
|
|
50
|
+
return WorkerDispatchRequest(
|
|
51
|
+
project_root=Path(args.project_root),
|
|
52
|
+
run_manifest=Path(args.run_manifest),
|
|
53
|
+
workspace_root=Path(args.workspace_root),
|
|
54
|
+
okstra_bin=Path(args.okstra_bin) if args.okstra_bin else None,
|
|
55
|
+
requested_workers=tuple(_parse_workers(args.workers)),
|
|
56
|
+
idle_timeout_seconds=_parse_idle_timeout(args.idle_timeout_seconds),
|
|
57
|
+
dispatch_kind=args.dispatch_kind,
|
|
58
|
+
jobs_file=Path(args.jobs_file) if args.jobs_file else None,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _backend_payload(backend_plan, *, dry_run: bool) -> dict[str, Any]:
|
|
63
|
+
payload = backend_plan.to_payload(dry_run=dry_run)
|
|
64
|
+
payload.pop("workerDispatches", None)
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parser() -> argparse.ArgumentParser:
|
|
69
|
+
parser = argparse.ArgumentParser(
|
|
70
|
+
prog="okstra worker-dispatch",
|
|
71
|
+
description="Dispatch verified CLI-backed workers for a prepared run.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument("--project-root", required=True)
|
|
74
|
+
parser.add_argument("--run-manifest", required=True)
|
|
75
|
+
parser.add_argument("--workspace-root", required=True)
|
|
76
|
+
parser.add_argument("--okstra-bin", default="")
|
|
77
|
+
parser.add_argument("--workers", default="")
|
|
78
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
79
|
+
parser.add_argument("--idle-timeout-seconds", default="600")
|
|
80
|
+
parser.add_argument("--dispatch-kind", default="initial")
|
|
81
|
+
parser.add_argument("--jobs-file", default="")
|
|
82
|
+
return parser
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_workers(raw: str) -> list[str]:
|
|
86
|
+
return [item.strip() for item in raw.split(",") if item.strip()]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_idle_timeout(raw: str) -> int:
|
|
90
|
+
try:
|
|
91
|
+
value = int(raw)
|
|
92
|
+
except ValueError as exc:
|
|
93
|
+
raise DispatchError("--idle-timeout-seconds must be an integer") from exc
|
|
94
|
+
if value < 0:
|
|
95
|
+
raise DispatchError("--idle-timeout-seconds must be non-negative")
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _print_json(payload: Mapping[str, Any]) -> None:
|
|
100
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -11,42 +11,25 @@ ANALYSIS_WORKER_LABELS = {
|
|
|
11
11
|
"codex": "Codex worker",
|
|
12
12
|
"antigravity": "Antigravity worker",
|
|
13
13
|
}
|
|
14
|
-
# An implementation audience shares this body but not its premise: the executor
|
|
15
|
-
# owns the diff and the verifier grades it, so neither is producing one of the
|
|
16
|
-
# independent findings that cross-verification triangulates.
|
|
17
|
-
ROLE_STATEMENTS = {
|
|
18
|
-
"executor": (
|
|
19
|
-
"You are the Executor for this implementation stage — the only worker "
|
|
20
|
-
"permitted to mutate project files. Carry the stage end to end and "
|
|
21
|
-
"produce the worker result."
|
|
22
|
-
),
|
|
23
|
-
"verifier": (
|
|
24
|
-
"You are a verifier for this implementation stage. Reproduce its QA "
|
|
25
|
-
"yourself, stay read-only on project files, and return an independent "
|
|
26
|
-
"verdict."
|
|
27
|
-
),
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
14
|
def analysis_prompt_body(
|
|
32
15
|
manifest: Mapping[str, Any],
|
|
33
16
|
active_context: Mapping[str, Any],
|
|
34
17
|
worker_id: str,
|
|
35
18
|
model: str,
|
|
36
|
-
role: str,
|
|
37
19
|
plan: PromptPlan,
|
|
38
20
|
) -> list[str]:
|
|
39
21
|
"""Render the provider-neutral body for an initial analysis audience."""
|
|
40
22
|
label = ANALYSIS_WORKER_LABELS.get(worker_id, f"{worker_id} worker")
|
|
23
|
+
pane_role = {
|
|
24
|
+
"implementation-executor": "executor",
|
|
25
|
+
"implementation-verifier": "verifier",
|
|
26
|
+
}.get(plan.audience, "worker")
|
|
41
27
|
return [
|
|
42
28
|
f"**Model:** {label}, {model}",
|
|
43
|
-
f"**Pane role:** {
|
|
29
|
+
f"**Pane role:** {pane_role}",
|
|
44
30
|
"",
|
|
45
31
|
f"# {label} Dispatch",
|
|
46
32
|
"",
|
|
47
|
-
"## Role",
|
|
48
|
-
_role_statement(label, role),
|
|
49
|
-
"",
|
|
50
33
|
"## Task",
|
|
51
34
|
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
52
35
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
@@ -62,16 +45,6 @@ def analysis_prompt_body(
|
|
|
62
45
|
"- Write the audit sidecar to Audit sidecar path as the preamble requires.",
|
|
63
46
|
"- Cite evidence with file paths and line numbers whenever you make a claim.",
|
|
64
47
|
]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _role_statement(label: str, role: str) -> str:
|
|
68
|
-
return ROLE_STATEMENTS.get(
|
|
69
|
-
role,
|
|
70
|
-
f"You are the {label} for okstra cross-verification. "
|
|
71
|
-
"Produce an independent worker result.",
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
|
|
75
48
|
def analysis_input_lines(
|
|
76
49
|
manifest: Mapping[str, Any],
|
|
77
50
|
active_context: Mapping[str, Any],
|
|
@@ -108,12 +81,6 @@ def report_writer_prompt_body(
|
|
|
108
81
|
"",
|
|
109
82
|
"# Report Writer Worker Dispatch",
|
|
110
83
|
"",
|
|
111
|
-
"## Role",
|
|
112
|
-
(
|
|
113
|
-
"You are the Report writer worker. Author the final-report data.json, "
|
|
114
|
-
"its rendered Markdown sibling, and the worker-result pointer."
|
|
115
|
-
),
|
|
116
|
-
"",
|
|
117
84
|
"## Task",
|
|
118
85
|
f"- Task key: `{_require_string(manifest, 'taskKey')}`",
|
|
119
86
|
f"- Task type: `{_require_string(manifest, 'taskType')}`",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Compact initial final-verification prompt contract."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import json
|
|
4
5
|
import re
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from pathlib import Path
|
|
@@ -81,6 +82,11 @@ _WORKER_SPECIFIC_PREFIXES = (
|
|
|
81
82
|
"**File write mode:**",
|
|
82
83
|
"**Model:**",
|
|
83
84
|
"**Pane role:**",
|
|
85
|
+
"**Provider:**",
|
|
86
|
+
"**Model execution value:**",
|
|
87
|
+
"**Runner:**",
|
|
88
|
+
"**Host runtime:**",
|
|
89
|
+
"**Host model value:**",
|
|
84
90
|
)
|
|
85
91
|
_WORKER_LABEL_RE = re.compile(
|
|
86
92
|
r"\b(?:Claude|Codex|Antigravity) worker\b",
|
|
@@ -95,6 +101,8 @@ class PromptRecord:
|
|
|
95
101
|
path: Path
|
|
96
102
|
expected_model: str | None = None
|
|
97
103
|
expected_delivery_mode: str | None = None
|
|
104
|
+
metadata_path: Path | None = None
|
|
105
|
+
expected_duty_audience: str | None = None
|
|
98
106
|
|
|
99
107
|
|
|
100
108
|
def validate_final_verification_initial_prompt(text: str) -> list[str]:
|
|
@@ -305,7 +313,8 @@ def _validate_model_header(text: str, expected_model: str | None) -> list[str]:
|
|
|
305
313
|
"""
|
|
306
314
|
if expected_model is None:
|
|
307
315
|
return []
|
|
308
|
-
|
|
316
|
+
task_text = text.split("\n\n## Task Instructions\n\n", 1)[-1]
|
|
317
|
+
model = _model_value(_header_values(task_text, MODEL_HEADER))
|
|
309
318
|
if model is None:
|
|
310
319
|
return ["exactly one non-empty **Model:** <label>, <model> header is required"]
|
|
311
320
|
if model != expected_model:
|
|
@@ -314,13 +323,34 @@ def _validate_model_header(text: str, expected_model: str | None) -> list[str]:
|
|
|
314
323
|
|
|
315
324
|
|
|
316
325
|
def _validate_record_metadata(text: str, record: PromptRecord) -> list[str]:
|
|
317
|
-
|
|
326
|
+
errors = [
|
|
318
327
|
*_validate_delivery_mode(
|
|
319
328
|
_header_values(text, PROMPT_DELIVERY_MODE_HEADER),
|
|
320
329
|
record.expected_delivery_mode,
|
|
321
330
|
),
|
|
322
331
|
*_validate_model_header(text, record.expected_model),
|
|
323
332
|
]
|
|
333
|
+
if record.expected_duty_audience is None:
|
|
334
|
+
return errors
|
|
335
|
+
if record.metadata_path is None:
|
|
336
|
+
return [*errors, "agent invocation metadata path is required"]
|
|
337
|
+
try:
|
|
338
|
+
metadata = json.loads(
|
|
339
|
+
record.metadata_path.read_text(encoding="utf-8")
|
|
340
|
+
)
|
|
341
|
+
except (OSError, UnicodeError, ValueError):
|
|
342
|
+
return [*errors, "agent invocation metadata is invalid"]
|
|
343
|
+
duty = metadata.get("dutyContract") if isinstance(metadata, dict) else None
|
|
344
|
+
if (
|
|
345
|
+
not isinstance(duty, dict)
|
|
346
|
+
or metadata.get("audience") != record.expected_duty_audience
|
|
347
|
+
or duty.get("id") != record.expected_duty_audience
|
|
348
|
+
):
|
|
349
|
+
errors.append(
|
|
350
|
+
"agent invocation duty does not match expected audience: "
|
|
351
|
+
+ record.expected_duty_audience
|
|
352
|
+
)
|
|
353
|
+
return errors
|
|
324
354
|
|
|
325
355
|
|
|
326
356
|
def _validate_evidence_ledger_header(
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from typing import Any, Literal, Mapping
|
|
6
6
|
|
|
7
|
+
from .agent_invocation import AgentAudience
|
|
7
8
|
from .analysis_inputs import ANALYSIS_TASK_TYPES
|
|
8
9
|
|
|
9
10
|
|
|
@@ -12,6 +13,7 @@ PromptAudience = Literal[
|
|
|
12
13
|
"implementation-executor",
|
|
13
14
|
"implementation-verifier",
|
|
14
15
|
"report-writer",
|
|
16
|
+
"translator",
|
|
15
17
|
"reverify",
|
|
16
18
|
"lead-only",
|
|
17
19
|
]
|
|
@@ -68,6 +70,7 @@ WORKER_ERROR_CONTRACT_FILENAME = "worker-error-contract.md"
|
|
|
68
70
|
@dataclass(frozen=True)
|
|
69
71
|
class PromptPlan:
|
|
70
72
|
audience: PromptAudience
|
|
73
|
+
duty_audience: AgentAudience
|
|
71
74
|
equality_group: str | None
|
|
72
75
|
packet_only: bool
|
|
73
76
|
allow_coding_preflight: bool
|
|
@@ -90,6 +93,10 @@ def resolve_prompt_plan(
|
|
|
90
93
|
return _plan("lead-only")
|
|
91
94
|
if not worker_id.strip():
|
|
92
95
|
raise ValueError("worker ID is required")
|
|
96
|
+
if worker_id == "translator" or dispatch_kind == "translator":
|
|
97
|
+
if worker_id != "translator" or dispatch_kind != "translator":
|
|
98
|
+
raise ValueError("translator worker and dispatch kind must match")
|
|
99
|
+
return _plan("translator", duty_audience="translator")
|
|
93
100
|
if dispatch_kind.startswith("reverify-r"):
|
|
94
101
|
return _plan("reverify")
|
|
95
102
|
# A critic pass keeps the full analysis contract — worker anchor headers, the
|
|
@@ -99,6 +106,15 @@ def resolve_prompt_plan(
|
|
|
99
106
|
# analysis prompts makes `validate_analysis_prompt_set` compare two prompts
|
|
100
107
|
# that are *supposed* to differ and fail every critic-enabled run.
|
|
101
108
|
analysis_equality_group = None if dispatch_kind == "critic" else "analysis-core"
|
|
109
|
+
analysis_duty: AgentAudience = (
|
|
110
|
+
"acceptance-critic"
|
|
111
|
+
if dispatch_kind == "critic" and task_type == "final-verification"
|
|
112
|
+
else "scope-critic"
|
|
113
|
+
if dispatch_kind == "critic"
|
|
114
|
+
else "acceptance-verifier"
|
|
115
|
+
if task_type == "final-verification"
|
|
116
|
+
else "analysis-worker"
|
|
117
|
+
)
|
|
102
118
|
if task_type == "implementation" and not executor_worker_id:
|
|
103
119
|
raise ValueError("implementation executor worker ID is required")
|
|
104
120
|
if worker_id == "report-writer":
|
|
@@ -119,6 +135,7 @@ def resolve_prompt_plan(
|
|
|
119
135
|
if task_type == "final-verification":
|
|
120
136
|
return _plan(
|
|
121
137
|
"analysis",
|
|
138
|
+
duty_audience=analysis_duty,
|
|
122
139
|
equality_group=analysis_equality_group,
|
|
123
140
|
packet_only=True,
|
|
124
141
|
required_headers=FINAL_VERIFICATION_HEADERS,
|
|
@@ -128,11 +145,17 @@ def resolve_prompt_plan(
|
|
|
128
145
|
if task_type == "improvement-discovery":
|
|
129
146
|
return _plan(
|
|
130
147
|
"analysis",
|
|
148
|
+
duty_audience=analysis_duty,
|
|
131
149
|
equality_group=analysis_equality_group,
|
|
132
150
|
packet_only=True,
|
|
133
151
|
required_headers=(GRILLING_LOG_HEADER,),
|
|
134
152
|
)
|
|
135
|
-
return _plan(
|
|
153
|
+
return _plan(
|
|
154
|
+
"analysis",
|
|
155
|
+
duty_audience=analysis_duty,
|
|
156
|
+
equality_group=analysis_equality_group,
|
|
157
|
+
packet_only=True,
|
|
158
|
+
)
|
|
136
159
|
|
|
137
160
|
|
|
138
161
|
def resolve_prompt_plan_for_manifest(
|
|
@@ -154,6 +177,7 @@ def resolve_prompt_plan_for_manifest(
|
|
|
154
177
|
def _plan(
|
|
155
178
|
audience: PromptAudience,
|
|
156
179
|
*,
|
|
180
|
+
duty_audience: AgentAudience | None = None,
|
|
157
181
|
equality_group: str | None = None,
|
|
158
182
|
packet_only: bool = False,
|
|
159
183
|
allow_coding_preflight: bool = False,
|
|
@@ -163,6 +187,7 @@ def _plan(
|
|
|
163
187
|
) -> PromptPlan:
|
|
164
188
|
return PromptPlan(
|
|
165
189
|
audience=audience,
|
|
190
|
+
duty_audience=duty_audience or _duty_audience(audience),
|
|
166
191
|
equality_group=equality_group,
|
|
167
192
|
packet_only=packet_only,
|
|
168
193
|
allow_coding_preflight=allow_coding_preflight,
|
|
@@ -172,6 +197,18 @@ def _plan(
|
|
|
172
197
|
)
|
|
173
198
|
|
|
174
199
|
|
|
200
|
+
def _duty_audience(audience: PromptAudience) -> AgentAudience:
|
|
201
|
+
return {
|
|
202
|
+
"analysis": "analysis-worker",
|
|
203
|
+
"implementation-executor": "implementation-executor",
|
|
204
|
+
"implementation-verifier": "implementation-verifier",
|
|
205
|
+
"report-writer": "report-writer",
|
|
206
|
+
"translator": "translator",
|
|
207
|
+
"reverify": "reverification-worker",
|
|
208
|
+
"lead-only": "lead",
|
|
209
|
+
}[audience]
|
|
210
|
+
|
|
211
|
+
|
|
175
212
|
def _worker_facing_headers(
|
|
176
213
|
audience: PromptAudience,
|
|
177
214
|
required_headers: tuple[str, ...],
|
|
@@ -107,9 +107,19 @@ okstra code-review target --branch <name> [--base <ref>] --project-root <project
|
|
|
107
107
|
|
|
108
108
|
A large census is never truncated. Report the cell count and confirm before dispatching — a silent cut is a false "I looked at everything" signal.
|
|
109
109
|
|
|
110
|
-
## Step 3 —
|
|
110
|
+
## Step 3 — Materialize and dispatch four reviewers in parallel
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
Every reviewer and later gap-fill is a separate auditable standalone invocation. Before dispatch, create
|
|
113
|
+
`.okstra/agent-invocations/code-review/<invocation-id>.instructions.md` from that reviewer's brief, then run
|
|
114
|
+
`okstra agent-prompt materialize` with `--purpose code-review`, `--audience code-reviewer`, and the canonical
|
|
115
|
+
`.prompt.md` path beside it. Pass the current host runtime, selected provider, and `--model-role analyser`;
|
|
116
|
+
the returned assignment is authoritative. Run `okstra agent-prompt verify` against the returned
|
|
117
|
+
`metadataPath` before invoking any model.
|
|
118
|
+
|
|
119
|
+
For a native host call, pass the verified prompt body and `hostModelValue`. For a deterministic provider
|
|
120
|
+
process, run `okstra worker-dispatch` with the verified prompt path and `modelExecutionValue`; never
|
|
121
|
+
substitute one model value for the other. Dispatch the four verified calls in parallel when the host supports
|
|
122
|
+
it. Every brief carries:
|
|
113
123
|
|
|
114
124
|
- the diff, plus the work directory path so the reviewer can read whole files for context
|
|
115
125
|
- the project layout in one or two lines (where source, tests, and — if the routing found one — domain / ports / adapters live)
|
|
@@ -130,9 +140,18 @@ Axis scope — the `Reads` column names that group's rules, and a brief never re
|
|
|
130
140
|
|
|
131
141
|
Say it plainly in every brief: **the census is law** — work the cells exactly as given, return a verdict for every one, never re-derive the list. There is no length budget; dropping a finding to stay brief is the failure this skill exists to prevent.
|
|
132
142
|
|
|
143
|
+
Capture each raw model return under `.okstra/agent-invocations/code-review/.tmp/`. Publish it with
|
|
144
|
+
`okstra agent-prompt materialize-result`, publish the immutable completion marker with
|
|
145
|
+
`okstra agent-prompt complete`, then run `okstra agent-prompt verify-completion`. Parse only the
|
|
146
|
+
`returnedBody` emitted by that last command. A model response without a verified completion is not a
|
|
147
|
+
review result and cannot contribute a verdict.
|
|
148
|
+
|
|
133
149
|
## Step 3.5 — Audit the coverage
|
|
134
150
|
|
|
135
|
-
Diff each reviewer's
|
|
151
|
+
Diff each reviewer's verified `returnedBody` cells against the slice you handed it. Any cell without a verdict
|
|
152
|
+
→ dispatch **one gap-fill invocation per axis**, carrying only the missing cells and the same brief. Each
|
|
153
|
+
gap-fill uses a new invocation ID and repeats the full materialize → verify → dispatch → materialize-result →
|
|
154
|
+
complete → verify-completion boundary from Step 3. Repeat until every cell of every axis has a verdict.
|
|
136
155
|
|
|
137
156
|
A missing verdict is unfinished work, never an implicit `clean`. Do not start Step 4 while a single cell is unaccounted for.
|
|
138
157
|
|
|
@@ -258,7 +258,22 @@ okstra render-bundle \
|
|
|
258
258
|
--<each-remaining-renderArgs-key> "<corresponding-value>"
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
`render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT` and `
|
|
261
|
+
`render-bundle` auto-supplies `--workspace-root` and forces `--render-only`. Stdout prints `okstra task root:`, `okstra instruction-set:`, `okstra run manifest:`, and the full rendered lead prompt. Parse the labelled lines for `TASK_ROOT`, `INSTRUCTION_SET_PATH`, and `RUN_MANIFEST_PATH`. Also watch for an optional `okstra concurrent-run stages:` label line — present only when a concurrent run is detected (see "Concurrent-run detection branch" below).
|
|
262
|
+
|
|
263
|
+
Before acting as the lead, read `resources.leadPromptMetadataPath` from the run
|
|
264
|
+
manifest and execute the host-native specification-link gate:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
okstra agent-prompt record-dispatch \
|
|
268
|
+
--project-root <projectRoot> \
|
|
269
|
+
--run-manifest <RUN_MANIFEST_PATH> \
|
|
270
|
+
--metadata <projectRoot/resources.leadPromptMetadataPath> \
|
|
271
|
+
--enforcement-mode host-native-spec-link-gate
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Do not continue from the rendered prompt if this command fails. This record
|
|
275
|
+
associates the current-session lead with a verified invocation specification;
|
|
276
|
+
it does not claim that the host exposed or attested the delivered prompt bytes.
|
|
262
277
|
|
|
263
278
|
The python function underneath is mutex-protected (`~/.okstra/.locks/<task-key>.lock`), writes `run-context-*.json` + `run-inputs-*.json` + all manifests + discovery files, and registers the run in `~/.okstra/recent.jsonl` with status `prepared`.
|
|
264
279
|
|
|
@@ -221,7 +221,16 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
|
|
|
221
221
|
```
|
|
222
222
|
|
|
223
223
|
3. **Run the deterministic gate first.** Execute `python3 ~/.okstra/lib/validators/validate-schedule.py <draft> --selection-json <selection>`. Do not dispatch the narrative verifier when this exits non-zero.
|
|
224
|
-
4. **Run the independent LLM verifier second.** Only after the deterministic gate passes,
|
|
224
|
+
4. **Run the independent LLM verifier second.** Only after the deterministic gate passes, create
|
|
225
|
+
`.okstra/agent-invocations/schedule-verification/<invocation-id>.instructions.md` containing the draft,
|
|
226
|
+
selection JSON, and checks below, but none of the lead's reasoning. Materialize its sibling `.prompt.md`
|
|
227
|
+
with `okstra agent-prompt materialize`, `--purpose schedule-verification`,
|
|
228
|
+
`--audience schedule-verifier`, the current host runtime, the selected provider, and
|
|
229
|
+
`--model-role analyser`. Run `okstra agent-prompt verify` against the returned `metadataPath` before
|
|
230
|
+
dispatch. A native host call receives the verified prompt body and `hostModelValue`; a deterministic
|
|
231
|
+
provider process runs `okstra worker-dispatch` with the prompt path and `modelExecutionValue`. These
|
|
232
|
+
values are not interchangeable.
|
|
233
|
+
The verifier returns `pass` plus concrete findings. Give it these checks:
|
|
225
234
|
- **Executable ordering** — stage sequence, `Depends On`, and Gantt bar positions tell the same story; nothing depends on something scheduled after it.
|
|
226
235
|
- **Arithmetic** — the Work Breakdown `Days` column sums to the At a Glance `Days` cell and to the `Effort sum` line.
|
|
227
236
|
- **Engineering-only scope** — no approval gate, permission check, stakeholder alignment or decision checklist.
|
|
@@ -231,6 +240,11 @@ When you do skip, insert in the section's position exactly: `> _Gantt Chart skip
|
|
|
231
240
|
- **One language.** Table cells and stage titles follow the schedule's prose language; only headings and field labels stay English literals.
|
|
232
241
|
|
|
233
242
|
These last three are the checks no validator can make. Do not accept a `pass` that skipped them.
|
|
243
|
+
|
|
244
|
+
Capture the raw return under `.okstra/agent-invocations/schedule-verification/.tmp/`, then run
|
|
245
|
+
`okstra agent-prompt materialize-result`, `okstra agent-prompt complete`, and
|
|
246
|
+
`okstra agent-prompt verify-completion` in that order. Parse only the `returnedBody` emitted by
|
|
247
|
+
`verify-completion`; an inline or unverified response cannot pass this gate.
|
|
234
248
|
5. **Revise from the first gate after every change.** If either gate requests a change, the lead revises the same draft in place, then starts again at the deterministic `--selection-json` gate. Allow **Max 2 revise cycles** total across both gates.
|
|
235
249
|
6. **Gate publication.** Only the same draft that passes both gates may be promoted in Step 6. If it still fails after the second revision, do not write the final file; remove the paired staging artifacts and report the residual findings in Korean.
|
|
236
250
|
|
|
@@ -2,16 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
This file is shared by the `implementation-executor` and `implementation-verifier` audiences. Read it end-to-end, then read the role sidecar enumerated by the prompt. Before work, also read the shared file named by `**Worker Error Contract Path:**`; error rules live only there.
|
|
4
4
|
|
|
5
|
-
## Operating standard (read before any output)
|
|
6
|
-
|
|
7
|
-
Work like a senior engineer who owns this result, not a commentator on it.
|
|
8
|
-
- Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
|
|
9
|
-
- Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
|
|
10
|
-
- Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
|
|
11
|
-
- Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
|
|
12
|
-
- Fit what's here — match the surrounding code and prose; size the response to the request.
|
|
13
|
-
- Hold your own line — reason from your independent angle; do not drift toward the other workers' likely answers or echo the user. Triangulation fails if your view isn't genuinely yours.
|
|
14
|
-
|
|
15
5
|
## Required reading
|
|
16
6
|
|
|
17
7
|
- Read the implementation role sidecar named by the prompt end-to-end. The executor sidecar owns mutation behavior; the verifier sidecar owns read-only review behavior.
|
|
@@ -2,15 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the audience-specific contract for `report-writer`. Read it end-to-end. Before work, also read the shared file named by `**Worker Error Contract Path:**`; error rules live only there.
|
|
4
4
|
|
|
5
|
-
## Operating standard (read before any output)
|
|
6
|
-
|
|
7
|
-
Work like a senior engineer who owns this result, not a commentator on it.
|
|
8
|
-
- Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
|
|
9
|
-
- Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
|
|
10
|
-
- Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
|
|
11
|
-
- Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
|
|
12
|
-
- Fit what's here — match the surrounding code and prose; size the response to the request.
|
|
13
|
-
|
|
14
5
|
## Required reading
|
|
15
6
|
|
|
16
7
|
Read every input enumerated by the Phase 6 dispatch end-to-end: task/analysis inputs, worker results, convergence state, the instruction-set-local `final-report-template.md`, and the task-type excerpt `final-report-schema.json`. Do not pull the full repository template or schema when the scoped instruction-set copies are provided.
|
|
@@ -49,17 +49,6 @@
|
|
|
49
49
|
]
|
|
50
50
|
}
|
|
51
51
|
],
|
|
52
|
-
"PreToolUse": [
|
|
53
|
-
{
|
|
54
|
-
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
|
|
55
|
-
"hooks": [
|
|
56
|
-
{
|
|
57
|
-
"type": "command",
|
|
58
|
-
"command": "bash \"$HOME/.okstra/prompts/coding-preflight/scripts/preedit-check.sh\""
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
],
|
|
63
52
|
"SessionEnd": [
|
|
64
53
|
{
|
|
65
54
|
"hooks": [
|
|
@@ -2,16 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the audience-specific contract for initial `analysis` workers. Read it end-to-end from the path selected by the dispatch prompt. Before doing work, also read the shared file named by `**Worker Error Contract Path:**`; that file is the only source for error-sidecar rules.
|
|
4
4
|
|
|
5
|
-
## Operating standard (read before any output)
|
|
6
|
-
|
|
7
|
-
Work like a senior engineer who owns this result, not a commentator on it.
|
|
8
|
-
- Evidence over assertion — back every claim with a file:line, or mark it an explicit assumption. Never state the unverified as fact.
|
|
9
|
-
- Read before you reason — read each required input end to end; when you lack basis, write "insufficient evidence" instead of filling the gap plausibly.
|
|
10
|
-
- Shortest sound path — chase the most likely cause first; don't re-verify what is settled or pad with restatement.
|
|
11
|
-
- Decide, don't survey — when options exist, give the trade-off and one recommendation, not an exhaustive list.
|
|
12
|
-
- Fit what's here — match the surrounding code and prose; size the response to the request.
|
|
13
|
-
- Hold your own line — reason from your independent angle; do not drift toward the other workers' likely answers or echo the user. Triangulation fails if your view isn't genuinely yours.
|
|
14
|
-
|
|
15
5
|
## Required reading
|
|
16
6
|
|
|
17
7
|
Read `analysis-packet.md`, the primary compact input, end-to-end. Source files named as fallback/evidence paths are read only when a finding needs to verify a citation, fill a packet gap, or resolve ambiguity. Do not read `final-report-template.md` or `final-report-schema.json`; analysis workers produce findings, not the final report.
|
|
@@ -526,5 +526,98 @@ if lead_sid:
|
|
|
526
526
|
write_json(team_state_path, team_state)
|
|
527
527
|
write_json(run_manifest_path, run_manifest)
|
|
528
528
|
write_json(task_manifest_path, task_manifest)
|
|
529
|
+
|
|
530
|
+
# The synthetic fixture accepts lead and completed-worker results, so it must
|
|
531
|
+
# create the same verified invocation records and result links as a real run.
|
|
532
|
+
# Keep the deliberately omitted worker prompt absent: the first validator pass
|
|
533
|
+
# still exercises the historical prompt-history failure before the helper
|
|
534
|
+
# restores that file.
|
|
535
|
+
if WORKSPACE_ROOT:
|
|
536
|
+
from okstra_ctl.agent_invocation import (
|
|
537
|
+
AgentInstruction,
|
|
538
|
+
AgentInstructionSource,
|
|
539
|
+
AgentInvocationRequest,
|
|
540
|
+
agent_model_assignment_from_payload,
|
|
541
|
+
prepare_agent_invocation,
|
|
542
|
+
)
|
|
543
|
+
from okstra_ctl.dispatch_state import (
|
|
544
|
+
link_agent_dispatch_result,
|
|
545
|
+
record_verified_agent_dispatch,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
lead_record = record_verified_agent_dispatch(
|
|
549
|
+
project_root=project_root,
|
|
550
|
+
run_manifest_path=run_manifest_path,
|
|
551
|
+
metadata_path=project_root / run_manifest["leadPromptMetadataPath"],
|
|
552
|
+
enforcement_mode="host-native-spec-link-gate",
|
|
553
|
+
)
|
|
554
|
+
link_agent_dispatch_result(
|
|
555
|
+
project_root=project_root,
|
|
556
|
+
run_manifest_path=run_manifest_path,
|
|
557
|
+
dispatch_id=lead_record["dispatchId"],
|
|
558
|
+
result_path=report_path,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
assignments = run_manifest["invocationAssignments"]
|
|
562
|
+
contract = run_manifest["agentContract"]
|
|
563
|
+
reservation_root = project_root / contract["invocationReservationRootPath"]
|
|
564
|
+
for worker in team_state.get("workers", []):
|
|
565
|
+
if not isinstance(worker, dict) or worker.get("status") != "completed":
|
|
566
|
+
continue
|
|
567
|
+
worker_id = str(worker.get("workerId") or "").strip()
|
|
568
|
+
prompt_relative = str(worker.get("promptPath") or "").strip()
|
|
569
|
+
result_relative = str(worker.get("resultPath") or "").strip()
|
|
570
|
+
if not worker_id or not prompt_relative or not result_relative:
|
|
571
|
+
continue
|
|
572
|
+
source_prompt = project_root / prompt_relative
|
|
573
|
+
if not source_prompt.is_file():
|
|
574
|
+
continue
|
|
575
|
+
assignment_ref = f"initial/{worker_id}"
|
|
576
|
+
invocation_id = f"validation-fixture-{worker_id}"
|
|
577
|
+
invocation_prompt = reservation_root / f"{invocation_id}.prompt.md"
|
|
578
|
+
prepared = prepare_agent_invocation(AgentInvocationRequest(
|
|
579
|
+
invocation_id=invocation_id,
|
|
580
|
+
worker_id=worker_id,
|
|
581
|
+
audience=(
|
|
582
|
+
"report-writer" if worker_id == "report-writer"
|
|
583
|
+
else "analysis-worker"
|
|
584
|
+
),
|
|
585
|
+
assignment_ref=assignment_ref,
|
|
586
|
+
purpose=None,
|
|
587
|
+
assignment=agent_model_assignment_from_payload(
|
|
588
|
+
assignments[assignment_ref]
|
|
589
|
+
),
|
|
590
|
+
instruction=AgentInstruction(
|
|
591
|
+
anchor_lines=(),
|
|
592
|
+
body=source_prompt.read_text(encoding="utf-8"),
|
|
593
|
+
source_paths=(
|
|
594
|
+
AgentInstructionSource("project", prompt_relative),
|
|
595
|
+
),
|
|
596
|
+
),
|
|
597
|
+
project_root=project_root,
|
|
598
|
+
run_manifest_path=run_manifest_path,
|
|
599
|
+
duty_root=project_root / contract["dutyRootPath"],
|
|
600
|
+
prompt_path=invocation_prompt,
|
|
601
|
+
metadata_path=invocation_prompt.with_name(
|
|
602
|
+
invocation_prompt.name + ".meta.json"
|
|
603
|
+
),
|
|
604
|
+
dispatch_kind="validation-fixture",
|
|
605
|
+
))
|
|
606
|
+
dispatch = record_verified_agent_dispatch(
|
|
607
|
+
project_root=project_root,
|
|
608
|
+
run_manifest_path=run_manifest_path,
|
|
609
|
+
metadata_path=prepared.metadata_path,
|
|
610
|
+
enforcement_mode=(
|
|
611
|
+
"host-native-spec-link-gate"
|
|
612
|
+
if prepared.assignment.runner == "native-session"
|
|
613
|
+
else "core-pre-dispatch"
|
|
614
|
+
),
|
|
615
|
+
)
|
|
616
|
+
link_agent_dispatch_result(
|
|
617
|
+
project_root=project_root,
|
|
618
|
+
run_manifest_path=run_manifest_path,
|
|
619
|
+
dispatch_id=dispatch["dispatchId"],
|
|
620
|
+
result_path=project_root / result_relative,
|
|
621
|
+
)
|
|
529
622
|
PY
|
|
530
623
|
}
|
|
@@ -41,20 +41,12 @@ def check(source_path: Path, target_path: Path) -> None:
|
|
|
41
41
|
|
|
42
42
|
# 1. Worker agent files: agents/workers/*-worker.md -> runtime/agents/workers/*-worker.md
|
|
43
43
|
#
|
|
44
|
-
# `_cli-wrapper-template.md` is a build-time render INPUT, not a shipped
|
|
45
|
-
# artifact — tools/build.mjs renders it (with each *.params.json) into
|
|
46
|
-
# codex-worker.md / antigravity-worker.md and excludes the template itself from the
|
|
47
|
-
# runtime payload. Skip it here so parity is checked only on shipped files.
|
|
48
|
-
# Keep this set in sync with TEMPLATE_INPUT_BASENAMES in tools/build.mjs.
|
|
49
|
-
template_input_basenames = {"_cli-wrapper-template.md"}
|
|
50
44
|
workers_source = agents_source_root / "workers"
|
|
51
45
|
workers_target = runtime_root / "agents" / "workers"
|
|
52
46
|
if not workers_source.is_dir():
|
|
53
47
|
errors.append(f"missing agents/workers source directory: {workers_source}")
|
|
54
48
|
else:
|
|
55
49
|
for source_path in sorted(workers_source.glob("*.md")):
|
|
56
|
-
if source_path.name in template_input_basenames:
|
|
57
|
-
continue
|
|
58
50
|
check(source_path, workers_target / source_path.name)
|
|
59
51
|
|
|
60
52
|
# 2. Lead contract + internal lead resources: prompts/lead/* + prompts/coding-preflight/*
|