okstra 0.179.2 → 0.183.2
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 +1 -1
- package/dist/cli-registry.mjs +30 -2
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/execute/incremental-carry.mjs +9 -8
- package/dist/commands/execute/incremental-carry.mjs.map +1 -1
- package/dist/commands/execute/plan-verify.mjs +3 -1
- package/dist/commands/execute/plan-verify.mjs.map +1 -1
- package/dist/commands/execute/render-bundle.d.mts +4 -2
- package/dist/commands/execute/render-bundle.mjs +46 -5
- package/dist/commands/execute/render-bundle.mjs.map +1 -1
- package/dist/commands/execute/run.mjs +11 -3
- package/dist/commands/execute/run.mjs.map +1 -1
- package/dist/commands/inspect/model-io.d.mts +1 -0
- package/dist/commands/inspect/model-io.mjs +25 -0
- package/dist/commands/inspect/model-io.mjs.map +1 -0
- package/dist/commands/inspect/stage-map.mjs +29 -8
- package/dist/commands/inspect/stage-map.mjs.map +1 -1
- package/dist/commands/inspect/task-list.mjs +52 -6
- package/dist/commands/inspect/task-list.mjs.map +1 -1
- package/dist/commands/inspect/user-response.mjs +14 -4
- package/dist/commands/inspect/user-response.mjs.map +1 -1
- package/dist/commands/lifecycle/check-project.d.mts +1 -0
- package/dist/commands/lifecycle/check-project.mjs +69 -50
- package/dist/commands/lifecycle/check-project.mjs.map +1 -1
- package/dist/commands/lifecycle/contract-check.d.mts +1 -0
- package/dist/commands/lifecycle/contract-check.mjs +18 -0
- package/dist/commands/lifecycle/contract-check.mjs.map +1 -0
- package/dist/commands/lifecycle/preflight.mjs +154 -51
- package/dist/commands/lifecycle/preflight.mjs.map +1 -1
- package/dist/commands/pr/pr.d.mts +1 -0
- package/dist/commands/pr/pr.mjs +19 -1
- package/dist/commands/pr/pr.mjs.map +1 -1
- package/dist/commands/report/agent-activity.mjs +2 -2
- package/dist/commands/report/approval-decision.d.mts +1 -0
- package/dist/commands/report/approval-decision.mjs +21 -0
- package/dist/commands/report/approval-decision.mjs.map +1 -0
- package/dist/commands/report/design-snapshot.d.mts +1 -0
- package/dist/commands/report/design-snapshot.mjs +19 -0
- package/dist/commands/report/design-snapshot.mjs.map +1 -0
- package/dist/commands/report/translate.mjs +3 -0
- package/dist/commands/report/translate.mjs.map +1 -1
- package/dist/lib/host-registry-client.mjs +13 -9
- package/dist/lib/host-registry-client.mjs.map +1 -1
- package/docs/architecture/storage-model.md +1 -1
- package/docs/architecture.md +21 -10
- package/docs/cli.md +36 -20
- package/docs/container.md +6 -4
- package/docs/contributor-change-matrix.md +1 -1
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-brief-gen.md +5 -3
- package/docs/for-ai/skills/okstra-code-review.md +4 -4
- package/docs/for-ai/skills/okstra-container-build.md +20 -17
- package/docs/for-ai/skills/okstra-inspect.md +20 -23
- package/docs/for-ai/skills/okstra-manager.md +19 -18
- package/docs/for-ai/skills/okstra-memory.md +2 -2
- package/docs/for-ai/skills/okstra-pr-gen.md +3 -3
- package/docs/for-ai/skills/okstra-rollup.md +14 -13
- package/docs/for-ai/skills/okstra-run.md +7 -3
- package/docs/for-ai/skills/okstra-schedule-gen.md +15 -18
- package/docs/for-ai/skills/okstra-setup.md +7 -7
- package/docs/for-ai/skills/okstra-usage.md +5 -4
- package/docs/for-ai/skills/okstra-user-response.md +50 -32
- package/docs/project-structure-overview.md +43 -31
- package/docs/task-process/implementation-planning.md +2 -2
- package/docs/task-process/implementation.md +7 -4
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/claude-worker.md +4 -4
- package/runtime/agents/workers/report-writer-worker.md +15 -164
- package/runtime/agents/workers/translator-worker.md +5 -13
- package/runtime/bin/okstra-error-log.py +51 -11
- package/runtime/bin/okstra-report-translate.py +210 -23
- package/runtime/prompts/host-orchestration/implementation.md +1 -1
- package/runtime/prompts/launch.template.md +9 -12
- package/runtime/prompts/lead/adapters/cmux.md +1 -1
- package/runtime/prompts/lead/context-loader.md +41 -141
- package/runtime/prompts/lead/convergence.md +10 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +40 -49
- package/runtime/prompts/lead/plan-body-verification.md +217 -23
- package/runtime/prompts/lead/report-writer.md +81 -421
- package/runtime/prompts/lead/team-contract.md +9 -54
- package/runtime/prompts/profiles/_clarification-recommendation.md +5 -4
- package/runtime/prompts/profiles/_coding-conventions-preflight.md +3 -2
- package/runtime/prompts/profiles/_common-contract.md +4 -4
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -3
- package/runtime/prompts/profiles/change-impact-analysis.md +1 -1
- package/runtime/prompts/profiles/error-analysis.md +1 -1
- package/runtime/prompts/profiles/feature-analysis.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +20 -12
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/prompts/profiles/project-analysis.md +1 -1
- package/runtime/prompts/profiles/release-handoff.md +0 -1
- package/runtime/prompts/profiles/requirements-discovery.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +0 -1
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +69 -17
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +13 -4
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +8 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +50 -5
- package/runtime/python/okstra_ctl/adapters/hosts/external/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +66 -5
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +70 -2
- package/runtime/python/okstra_ctl/agent_activity.py +141 -38
- package/runtime/python/okstra_ctl/agent_invocation.py +19 -6
- package/runtime/python/okstra_ctl/agent_prompt_cli.py +70 -23
- package/runtime/python/okstra_ctl/analysis_inputs.py +5 -4
- package/runtime/python/okstra_ctl/analysis_packet.py +124 -3
- package/runtime/python/okstra_ctl/approval_decisions.py +328 -0
- package/runtime/python/okstra_ctl/attempt_evidence.py +2 -2
- package/runtime/python/okstra_ctl/backfill.py +13 -10
- package/runtime/python/okstra_ctl/batch.py +2 -4
- package/runtime/python/okstra_ctl/build_tools.py +6 -3
- package/runtime/python/okstra_ctl/claim_reproduction.py +101 -0
- package/runtime/python/okstra_ctl/clarification_items.py +27 -13
- package/runtime/python/okstra_ctl/cmux.py +130 -52
- package/runtime/python/okstra_ctl/code_review_target.py +34 -8
- package/runtime/python/okstra_ctl/consumers.py +5 -4
- package/runtime/python/okstra_ctl/container.py +103 -8
- package/runtime/python/okstra_ctl/context_cost.py +2 -1
- package/runtime/python/okstra_ctl/contract_graph.py +497 -0
- package/runtime/python/okstra_ctl/contract_graph_cli.py +62 -0
- package/runtime/python/okstra_ctl/convergence.py +338 -17
- package/runtime/python/okstra_ctl/convergence_engine.py +10 -18
- package/runtime/python/okstra_ctl/convergence_provenance.py +58 -8
- package/runtime/python/okstra_ctl/convergence_store.py +55 -34
- package/runtime/python/okstra_ctl/design_prep.py +7 -4
- package/runtime/python/okstra_ctl/design_snapshot.py +134 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +97 -69
- package/runtime/python/okstra_ctl/dispatch_state.py +163 -63
- package/runtime/python/okstra_ctl/doctor.py +6 -3
- package/runtime/python/okstra_ctl/domain/worker_presentation.py +70 -9
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +16 -30
- package/runtime/python/okstra_ctl/error_log_write.py +35 -30
- package/runtime/python/okstra_ctl/error_report.py +26 -1
- package/runtime/python/okstra_ctl/error_zip.py +27 -5
- package/runtime/python/okstra_ctl/execution_identity.py +3 -2
- package/runtime/python/okstra_ctl/execution_manifest.py +7 -4
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +6 -2
- package/runtime/python/okstra_ctl/final_report_schema.py +26 -17
- package/runtime/python/okstra_ctl/fix_cycles.py +2 -2
- package/runtime/python/okstra_ctl/fixed_text.py +39 -0
- package/runtime/python/okstra_ctl/git_reconcile.py +41 -9
- package/runtime/python/okstra_ctl/handoff.py +5 -4
- package/runtime/python/okstra_ctl/i18n.py +4 -2
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -14
- package/runtime/python/okstra_ctl/implementation_outcome.py +4 -7
- package/runtime/python/okstra_ctl/incremental_carry.py +130 -17
- package/runtime/python/okstra_ctl/incremental_scope.py +12 -4
- package/runtime/python/okstra_ctl/index.py +8 -11
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +79 -7
- package/runtime/python/okstra_ctl/invocation.py +3 -6
- package/runtime/python/okstra_ctl/json_boundary.py +366 -0
- package/runtime/python/okstra_ctl/json_registry.py +10 -12
- package/runtime/python/okstra_ctl/jsonl.py +19 -2
- package/runtime/python/okstra_ctl/lead_events.py +33 -1
- package/runtime/python/okstra_ctl/listing.py +3 -3
- package/runtime/python/okstra_ctl/log_report.py +24 -2
- package/runtime/python/okstra_ctl/manager_cli.py +92 -7
- package/runtime/python/okstra_ctl/manager_store.py +12 -10
- package/runtime/python/okstra_ctl/material.py +5 -1
- package/runtime/python/okstra_ctl/migrate.py +29 -25
- package/runtime/python/okstra_ctl/model_cli.py +3 -15
- package/runtime/python/okstra_ctl/model_io_cli.py +1051 -0
- package/runtime/python/okstra_ctl/mutation_probe.py +13 -4
- package/runtime/python/okstra_ctl/pane_reclaim.py +3 -2
- package/runtime/python/okstra_ctl/path_hints.py +12 -0
- package/runtime/python/okstra_ctl/paths.py +21 -0
- package/runtime/python/okstra_ctl/plan_items.py +115 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +626 -34
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +2 -1
- package/runtime/python/okstra_ctl/pr_template.py +3 -2
- package/runtime/python/okstra_ctl/project_meta.py +5 -7
- package/runtime/python/okstra_ctl/recap.py +5 -4
- package/runtime/python/okstra_ctl/reconcile.py +21 -27
- package/runtime/python/okstra_ctl/registry/host_discovery.py +3 -2
- package/runtime/python/okstra_ctl/registry/provider_registry.py +3 -2
- package/runtime/python/okstra_ctl/render.py +74 -16
- package/runtime/python/okstra_ctl/render_final_report.py +10 -8
- package/runtime/python/okstra_ctl/report_assembly.py +509 -0
- package/runtime/python/okstra_ctl/report_contract.py +2 -1
- package/runtime/python/okstra_ctl/report_finalize.py +67 -32
- package/runtime/python/okstra_ctl/report_html/render.py +3 -2
- package/runtime/python/okstra_ctl/report_inputs.py +72 -0
- package/runtime/python/okstra_ctl/report_language.py +3 -2
- package/runtime/python/okstra_ctl/report_markdown.py +81 -8
- package/runtime/python/okstra_ctl/report_narrative.py +351 -0
- package/runtime/python/okstra_ctl/report_projections.py +265 -0
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +518 -0
- package/runtime/python/okstra_ctl/report_views.py +3 -2
- package/runtime/python/okstra_ctl/rollup.py +65 -4
- package/runtime/python/okstra_ctl/run.py +184 -65
- package/runtime/python/okstra_ctl/run_audit.py +3 -2
- package/runtime/python/okstra_ctl/run_context.py +6 -9
- package/runtime/python/okstra_ctl/run_index_row.py +2 -8
- package/runtime/python/okstra_ctl/schedule_semantics.py +5 -2
- package/runtime/python/okstra_ctl/schema_excerpt.py +15 -8
- package/runtime/python/okstra_ctl/session_transcript.py +27 -1
- package/runtime/python/okstra_ctl/set_work_status.py +64 -38
- package/runtime/python/okstra_ctl/stage_fix_carry.py +8 -6
- package/runtime/python/okstra_ctl/stage_ledger.py +132 -18
- package/runtime/python/okstra_ctl/stage_map.py +96 -28
- package/runtime/python/okstra_ctl/stage_targets.py +3 -4
- package/runtime/python/okstra_ctl/team.py +3 -2
- package/runtime/python/okstra_ctl/team_reconcile.py +11 -2
- package/runtime/python/okstra_ctl/time_report.py +51 -4
- package/runtime/python/okstra_ctl/usage_identity.py +2 -1
- package/runtime/python/okstra_ctl/usage_report.py +58 -4
- package/runtime/python/okstra_ctl/user_response.py +1431 -66
- package/runtime/python/okstra_ctl/wizard.py +11 -44
- package/runtime/python/okstra_ctl/work_categories.py +3 -2
- package/runtime/python/okstra_ctl/worker_dispatch.py +5 -2
- package/runtime/python/okstra_ctl/worker_prompt_body.py +49 -3
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +3 -2
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +31 -3
- package/runtime/python/okstra_ctl/worker_runner.py +14 -12
- package/runtime/python/okstra_ctl/workflow.py +2 -1
- package/runtime/python/okstra_ctl/worktree.py +3 -2
- package/runtime/python/okstra_ctl/wrapper_status.py +4 -2
- package/runtime/python/okstra_ctl/write_policy.py +4 -2
- package/runtime/python/okstra_token_usage/antigravity.py +39 -12
- package/runtime/python/okstra_token_usage/collect.py +90 -38
- package/runtime/python/okstra_token_usage/grok.py +127 -0
- package/runtime/schemas/final-report-v2.0.schema.json +9 -0
- package/runtime/schemas/final-report-v3.0.schema.json +10219 -0
- package/runtime/schemas/report-narrative-v3.0.schema.json +30 -0
- package/runtime/schemas/report-synthesis-packet-v1.0.schema.json +140 -0
- package/runtime/skills/okstra-brief-gen/SKILL.md +9 -7
- package/runtime/skills/okstra-code-review/SKILL.md +21 -11
- package/runtime/skills/okstra-container-build/SKILL.md +18 -18
- package/runtime/skills/okstra-inspect/SKILL.md +12 -11
- package/runtime/skills/okstra-inspect/facets/error-zip.md +8 -8
- package/runtime/skills/okstra-inspect/facets/errors.md +2 -2
- package/runtime/skills/okstra-inspect/facets/history.md +9 -14
- package/runtime/skills/okstra-inspect/facets/logs.md +2 -2
- package/runtime/skills/okstra-inspect/facets/recap.md +5 -5
- package/runtime/skills/okstra-inspect/facets/report.md +6 -10
- package/runtime/skills/okstra-inspect/facets/status.md +9 -8
- package/runtime/skills/okstra-inspect/facets/time.md +3 -3
- package/runtime/skills/okstra-manager/SKILL.md +16 -14
- package/runtime/skills/okstra-memory/SKILL.md +3 -3
- package/runtime/skills/okstra-pr-gen/SKILL.md +5 -4
- package/runtime/skills/okstra-rollup/SKILL.md +6 -16
- package/runtime/skills/okstra-run/SKILL.md +7 -7
- package/runtime/skills/okstra-schedule-gen/SKILL.md +21 -17
- package/runtime/skills/okstra-setup/SKILL.md +21 -13
- package/runtime/skills/okstra-setup/references/project-config.md +2 -2
- package/runtime/skills/okstra-usage/SKILL.md +10 -10
- package/runtime/skills/okstra-user-response/SKILL.md +78 -107
- package/runtime/templates/report-writer-prompt-preamble.md +29 -19
- package/runtime/templates/reports/html/macros/forms.html +6 -4
- package/runtime/templates/reports/schedule.template.md +4 -4
- package/runtime/templates/worker-error-contract.md +17 -29
- package/runtime/validators/validate-run.py +636 -72
- package/runtime/validators/validate_session_conformance.py +43 -7
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Phase 7 report post-processing — the single reference point.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
The order is load-bearing
|
|
7
|
-
|
|
3
|
+
Contract 3.0 collects usage into team state, assembles the role-owned inputs
|
|
4
|
+
into final-report data.json once, then verifies and renders that published
|
|
5
|
+
record. Contract 2.0 keeps its historical in-place projection sequence for
|
|
6
|
+
read-only compatibility. The order is load-bearing: rendering before assembly
|
|
7
|
+
or usage collection publishes stale derived views, and validating before
|
|
8
|
+
rendering trips the report-views contract.
|
|
8
9
|
|
|
9
10
|
`token-usage` is the one step whose failure does not stop the sequence, which
|
|
10
11
|
deliberately accepts that first state: its input is the lead session log, so a
|
|
@@ -39,10 +40,12 @@ from typing import Any, Callable, Mapping, Sequence
|
|
|
39
40
|
|
|
40
41
|
from .agent_activity import ActivityProjectionError, project_agent_activity
|
|
41
42
|
from .report_contract import apply_execution_roles
|
|
43
|
+
from .report_assembly import ReportAssemblyError, assemble_report
|
|
42
44
|
from .dispatch_state import DispatchError, link_agent_dispatch_result
|
|
43
45
|
from .final_report_paths import final_report_data_path, final_report_markdown_path
|
|
44
46
|
from .paths import task_dir, task_manifest_file
|
|
45
47
|
from .release_gate import release_handoff_allowed
|
|
48
|
+
from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
46
49
|
from .stage_integrate import IntegrateError
|
|
47
50
|
from .stage_targets import (
|
|
48
51
|
StageTargetError,
|
|
@@ -74,6 +77,16 @@ STEP_ORDER = (
|
|
|
74
77
|
STEP_TEARDOWN_STAGES,
|
|
75
78
|
)
|
|
76
79
|
|
|
80
|
+
V3_STEP_ORDER = (
|
|
81
|
+
STEP_TOKEN_USAGE,
|
|
82
|
+
STEP_PROJECT_ACTIVITY,
|
|
83
|
+
STEP_CHECK_SOURCE,
|
|
84
|
+
STEP_RENDER_VIEWS,
|
|
85
|
+
STEP_SPAWN_FOLLOWUPS,
|
|
86
|
+
STEP_VALIDATE_RUN,
|
|
87
|
+
STEP_TEARDOWN_STAGES,
|
|
88
|
+
)
|
|
89
|
+
|
|
77
90
|
|
|
78
91
|
class FinalizeError(Exception):
|
|
79
92
|
"""Raised when the Phase 7 sequence cannot be assembled."""
|
|
@@ -226,6 +239,7 @@ class FinalizeContext:
|
|
|
226
239
|
task_id: str
|
|
227
240
|
seq: str
|
|
228
241
|
final_status_path: Path | None
|
|
242
|
+
report_contract_version: str = "2.0"
|
|
229
243
|
|
|
230
244
|
@property
|
|
231
245
|
def markdown_path(self) -> Path:
|
|
@@ -257,13 +271,16 @@ class FinalizeContext:
|
|
|
257
271
|
final_status_path=resolve_optional_path(
|
|
258
272
|
project_root, manifest.get("finalStatusPath")
|
|
259
273
|
),
|
|
274
|
+
report_contract_version=str(
|
|
275
|
+
manifest.get("reportContractVersion") or "2.0"
|
|
276
|
+
),
|
|
260
277
|
)
|
|
261
278
|
|
|
262
279
|
|
|
263
280
|
def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
264
281
|
"""Assemble the ordered Phase 7 argv list. Order is contractual."""
|
|
265
282
|
markdown_path = ctx.markdown_path
|
|
266
|
-
|
|
283
|
+
commands = [
|
|
267
284
|
(
|
|
268
285
|
STEP_PROJECT_ACTIVITY,
|
|
269
286
|
[
|
|
@@ -344,6 +361,16 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
344
361
|
["<in-process>", "teardown-stages", str(ctx.data_path)],
|
|
345
362
|
),
|
|
346
363
|
]
|
|
364
|
+
if ctx.report_contract_version != "3.0":
|
|
365
|
+
return commands
|
|
366
|
+
by_name = dict(commands)
|
|
367
|
+
usage = by_name[STEP_TOKEN_USAGE]
|
|
368
|
+
marker = usage.index("--substitute-data")
|
|
369
|
+
by_name[STEP_TOKEN_USAGE] = usage[:marker]
|
|
370
|
+
by_name[STEP_PROJECT_ACTIVITY] = [
|
|
371
|
+
"<in-process>", "report-assembly", str(ctx.manifest_path), str(ctx.data_path)
|
|
372
|
+
]
|
|
373
|
+
return [(name, by_name[name]) for name in V3_STEP_ORDER]
|
|
347
374
|
|
|
348
375
|
|
|
349
376
|
def _teardown_stage_worktrees(
|
|
@@ -363,8 +390,8 @@ def _teardown_stage_worktrees(
|
|
|
363
390
|
if ctx.task_type != "final-verification":
|
|
364
391
|
return _done({"skipped": "not a final-verification run"})
|
|
365
392
|
try:
|
|
366
|
-
data =
|
|
367
|
-
except
|
|
393
|
+
data = load_owned_object(Path(ctx.data_path), artifact="final report record")
|
|
394
|
+
except JsonBoundaryError as exc:
|
|
368
395
|
return subprocess.CompletedProcess(
|
|
369
396
|
command, 1, "", f"cannot read final-report data.json: {exc}")
|
|
370
397
|
if data.get("verificationScope") != "whole-task":
|
|
@@ -441,14 +468,16 @@ def run_finalize(
|
|
|
441
468
|
steps: list[dict[str, Any]] = []
|
|
442
469
|
deferred = ""
|
|
443
470
|
try:
|
|
444
|
-
|
|
471
|
+
if ctx.report_contract_version != "3.0":
|
|
472
|
+
write_execution_roles(ctx)
|
|
445
473
|
commands = build_commands(ctx)
|
|
446
474
|
except FinalizeError as exc:
|
|
447
475
|
return {"ok": False, "reason": str(exc), "steps": steps}
|
|
448
476
|
|
|
449
477
|
if only:
|
|
450
478
|
selected = set(only)
|
|
451
|
-
|
|
479
|
+
contract_order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
|
|
480
|
+
unknown = sorted(selected - set(contract_order))
|
|
452
481
|
if unknown:
|
|
453
482
|
return {
|
|
454
483
|
"ok": False,
|
|
@@ -462,12 +491,17 @@ def run_finalize(
|
|
|
462
491
|
before_step(name)
|
|
463
492
|
if name == STEP_PROJECT_ACTIVITY:
|
|
464
493
|
try:
|
|
465
|
-
|
|
466
|
-
ctx.project_root,
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
494
|
+
if ctx.report_contract_version == "3.0":
|
|
495
|
+
assembled = assemble_report(ctx.project_root, ctx.manifest_path)
|
|
496
|
+
count = len(assembled.get("agentActivity") or [])
|
|
497
|
+
else:
|
|
498
|
+
rows = project_agent_activity(
|
|
499
|
+
ctx.project_root,
|
|
500
|
+
ctx.manifest_path,
|
|
501
|
+
ctx.data_path,
|
|
502
|
+
)
|
|
503
|
+
count = len(rows)
|
|
504
|
+
except (ActivityProjectionError, ReportAssemblyError) as exc:
|
|
471
505
|
result = subprocess.CompletedProcess(
|
|
472
506
|
command,
|
|
473
507
|
1,
|
|
@@ -478,7 +512,7 @@ def run_finalize(
|
|
|
478
512
|
result = subprocess.CompletedProcess(
|
|
479
513
|
command,
|
|
480
514
|
0,
|
|
481
|
-
json.dumps({"count":
|
|
515
|
+
json.dumps({"count": count}),
|
|
482
516
|
"",
|
|
483
517
|
)
|
|
484
518
|
else:
|
|
@@ -530,9 +564,7 @@ def write_execution_roles(ctx: FinalizeContext) -> None:
|
|
|
530
564
|
updated = apply_execution_roles(dict(data), dict(manifest))
|
|
531
565
|
if updated == data:
|
|
532
566
|
return
|
|
533
|
-
|
|
534
|
-
tmp.write_text(json.dumps(updated, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
535
|
-
tmp.replace(ctx.data_path)
|
|
567
|
+
write_owned_object_atomic(ctx.data_path, updated, artifact="final report record")
|
|
536
568
|
|
|
537
569
|
|
|
538
570
|
def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
|
|
@@ -571,11 +603,9 @@ def _link_lead_result_for_validation(ctx: FinalizeContext) -> None:
|
|
|
571
603
|
|
|
572
604
|
def _load_manifest(path: Path) -> Mapping[str, Any]:
|
|
573
605
|
try:
|
|
574
|
-
payload =
|
|
575
|
-
except
|
|
576
|
-
raise FinalizeError(f"run manifest not
|
|
577
|
-
except json.JSONDecodeError as exc:
|
|
578
|
-
raise FinalizeError(f"run manifest is not valid JSON: {path} ({exc})") from exc
|
|
606
|
+
payload = load_owned_object(path, artifact="run manifest")
|
|
607
|
+
except JsonBoundaryError as exc:
|
|
608
|
+
raise FinalizeError(f"run manifest is not valid: {path} ({exc})") from exc
|
|
579
609
|
if not isinstance(payload, Mapping):
|
|
580
610
|
raise FinalizeError(f"run manifest is not a JSON object: {path}")
|
|
581
611
|
return payload
|
|
@@ -633,7 +663,9 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
633
663
|
return parser
|
|
634
664
|
|
|
635
665
|
|
|
636
|
-
def _step_summary_lines(
|
|
666
|
+
def _step_summary_lines(
|
|
667
|
+
result: Mapping[str, Any], order: Sequence[str] = STEP_ORDER,
|
|
668
|
+
) -> list[str]:
|
|
637
669
|
"""One human-readable line per step, so the outcome is legible without
|
|
638
670
|
parsing the JSON payload."""
|
|
639
671
|
lines = []
|
|
@@ -641,13 +673,15 @@ def _step_summary_lines(result: Mapping[str, Any]) -> list[str]:
|
|
|
641
673
|
code = step.get("exitCode")
|
|
642
674
|
mark = "ok " if code == 0 else "FAIL"
|
|
643
675
|
lines.append(f" [{mark}] {step.get('name')} (exit {code})")
|
|
644
|
-
for name in
|
|
676
|
+
for name in order:
|
|
645
677
|
if not any(s.get("name") == name for s in (result.get("steps") or [])):
|
|
646
678
|
lines.append(f" [skip] {name}")
|
|
647
679
|
return lines
|
|
648
680
|
|
|
649
681
|
|
|
650
|
-
def _recovery_step_names(
|
|
682
|
+
def _recovery_step_names(
|
|
683
|
+
result: Mapping[str, Any], order: Sequence[str] = STEP_ORDER,
|
|
684
|
+
) -> list[str]:
|
|
651
685
|
"""The steps a retry has to re-run: every step from the earliest failure on.
|
|
652
686
|
|
|
653
687
|
Naming only the failed steps would prescribe half a recovery. `token-usage`
|
|
@@ -664,9 +698,9 @@ def _recovery_step_names(result: Mapping[str, Any]) -> list[str]:
|
|
|
664
698
|
for step in (result.get("steps") or [])
|
|
665
699
|
if step.get("exitCode") != 0
|
|
666
700
|
}
|
|
667
|
-
for index, name in enumerate(
|
|
701
|
+
for index, name in enumerate(order):
|
|
668
702
|
if name in failed:
|
|
669
|
-
return list(
|
|
703
|
+
return list(order[index:])
|
|
670
704
|
return []
|
|
671
705
|
|
|
672
706
|
|
|
@@ -682,13 +716,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
682
716
|
# sequence because the token-usage step below reads `leadSessionIds`.
|
|
683
717
|
observe_lead_session(ctx.project_root, ctx.team_state_path)
|
|
684
718
|
result = run_finalize(ctx, only=args.only or None)
|
|
719
|
+
order = V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
|
|
685
720
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
686
721
|
print("finalize steps:", file=sys.stderr)
|
|
687
|
-
for line in _step_summary_lines(result):
|
|
722
|
+
for line in _step_summary_lines(result, order):
|
|
688
723
|
print(line, file=sys.stderr)
|
|
689
724
|
if not result["ok"]:
|
|
690
725
|
print(f"error: {result['reason']}", file=sys.stderr)
|
|
691
|
-
recovery = _recovery_step_names(result)
|
|
726
|
+
recovery = _recovery_step_names(result, order)
|
|
692
727
|
if recovery:
|
|
693
728
|
flags = " ".join(f"--only {name}" for name in recovery)
|
|
694
729
|
print(
|
|
@@ -15,6 +15,7 @@ from ..i18n import HTML_DICTIONARY_REL, load_dictionary, make_jinja_global
|
|
|
15
15
|
from ..report_translation import overlay
|
|
16
16
|
from ..report_view_artifacts import user_responses_dir_for_report
|
|
17
17
|
from ..usage_cells import format_duration_ms
|
|
18
|
+
from ..json_boundary import load_owned_object
|
|
18
19
|
from .common import anchor_index
|
|
19
20
|
from .filters import (
|
|
20
21
|
code_evidence,
|
|
@@ -87,7 +88,7 @@ def _localize(data: dict, data_path: Path) -> tuple[dict, str]:
|
|
|
87
88
|
"rendering the English source\n"
|
|
88
89
|
)
|
|
89
90
|
return data, lang
|
|
90
|
-
payload =
|
|
91
|
+
payload = load_owned_object(sidecar_file, artifact="translation sidecar")
|
|
91
92
|
strings = payload.get("strings")
|
|
92
93
|
if not isinstance(strings, dict):
|
|
93
94
|
raise HtmlRenderError(f"translation sidecar has no 'strings' object: {sidecar_file}")
|
|
@@ -120,7 +121,7 @@ def render_v2_html_view(
|
|
|
120
121
|
no reader ever compared, which made a derived artifact a precondition for
|
|
121
122
|
another derived artifact.
|
|
122
123
|
"""
|
|
123
|
-
data =
|
|
124
|
+
data = load_owned_object(data_path, artifact="final report record")
|
|
124
125
|
errors = validate(data, load_schema_for_data(data))
|
|
125
126
|
if errors:
|
|
126
127
|
raise HtmlRenderError("invalid v2 final-report data: " + "; ".join(errors[:5]))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""리포트 계약 3.0의 역할별 입력 경로와 단일 소유자 레지스트리."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Mapping
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
REPORT_CONTRACT_V3 = "3.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReportInputError(ValueError):
|
|
13
|
+
"""3.0 입력 경로나 소유자가 매니페스트에서 해소되지 않았다."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class ReportInputPath:
|
|
18
|
+
key: str
|
|
19
|
+
owner: str
|
|
20
|
+
path: Path
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_INPUT_FIELDS = (
|
|
24
|
+
("narrative", "report-writer", "reportNarrativePath"),
|
|
25
|
+
("approval-decisions", "lead", "approvalDecisionsPath"),
|
|
26
|
+
("agent-activity", "activity-ledger", "leadEventsPath"),
|
|
27
|
+
("execution-status", "team-state", "teamStatePath"),
|
|
28
|
+
("convergence", "convergence", "convergenceStatePath"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_PLANNING_INPUT_FIELDS = (
|
|
32
|
+
("design-preparation", "design-surface-detector", "designPreparationPath"),
|
|
33
|
+
("plan-body-verification", "convergence", "planBodyVerificationPath"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def uses_report_contract_v3(manifest: Mapping[str, Any]) -> bool:
|
|
38
|
+
return str(manifest.get("reportContractVersion") or "").strip() == REPORT_CONTRACT_V3
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _project_path(project_root: Path, value: object, field: str) -> Path:
|
|
42
|
+
if not isinstance(value, str) or not value.strip():
|
|
43
|
+
raise ReportInputError(f"report contract 3.0 requires {field}")
|
|
44
|
+
path = Path(value)
|
|
45
|
+
return path if path.is_absolute() else project_root / path
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def report_narrative_path(
|
|
49
|
+
project_root: Path, manifest: Mapping[str, Any],
|
|
50
|
+
) -> Path:
|
|
51
|
+
if not uses_report_contract_v3(manifest):
|
|
52
|
+
raise ReportInputError("report narrative path belongs to report contract 3.0")
|
|
53
|
+
return _project_path(project_root, manifest.get("reportNarrativePath"), "reportNarrativePath")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def report_input_paths(
|
|
57
|
+
project_root: Path, manifest: Mapping[str, Any],
|
|
58
|
+
) -> tuple[ReportInputPath, ...]:
|
|
59
|
+
if not uses_report_contract_v3(manifest):
|
|
60
|
+
raise ReportInputError("role-owned report inputs require report contract 3.0")
|
|
61
|
+
fields = _INPUT_FIELDS + (
|
|
62
|
+
_PLANNING_INPUT_FIELDS
|
|
63
|
+
if manifest.get("taskType") == "implementation-planning"
|
|
64
|
+
else ()
|
|
65
|
+
)
|
|
66
|
+
rows = tuple(
|
|
67
|
+
ReportInputPath(key, owner, _project_path(project_root, manifest.get(field), field))
|
|
68
|
+
for key, owner, field in fields
|
|
69
|
+
)
|
|
70
|
+
if len({row.key for row in rows}) != len(rows):
|
|
71
|
+
raise ReportInputError("report input keys must be unique")
|
|
72
|
+
return rows
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Mapping
|
|
13
13
|
|
|
14
14
|
from .worker_prompt_body import instruction_path
|
|
15
|
+
from .json_boundary import JsonBoundaryError, load_owned_object
|
|
15
16
|
|
|
16
17
|
REPORT_LANGUAGE_VALUES = {"en", "ko", "auto"}
|
|
17
18
|
|
|
@@ -50,8 +51,8 @@ def _read_report_language(path: Path, label: str) -> str:
|
|
|
50
51
|
if not path.is_file():
|
|
51
52
|
return ""
|
|
52
53
|
try:
|
|
53
|
-
payload =
|
|
54
|
-
except
|
|
54
|
+
payload = load_owned_object(path, artifact=f"{label} configuration")
|
|
55
|
+
except JsonBoundaryError as exc:
|
|
55
56
|
raise ReportLanguageError(f"{label} config is unreadable: {path}") from exc
|
|
56
57
|
if not isinstance(payload, Mapping):
|
|
57
58
|
raise ReportLanguageError(f"{label} config must be a JSON object: {path}")
|
|
@@ -95,6 +95,17 @@ def humanise(key: str) -> str:
|
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def label_key(label: str) -> str:
|
|
99
|
+
"""라벨 대조용 정규화 표기.
|
|
100
|
+
|
|
101
|
+
`humanise` 는 `_ACRONYMS` 표에 있는 낱말만 대문자로 올린다(`ticketId` ->
|
|
102
|
+
`Ticket ID`). 그 표는 저작자에게 전달되지 않으므로 `Ticket Id` 라고 쓰면
|
|
103
|
+
글자 하나 때문에 리포트 전체가 되돌아온다. 대소문자와 낱말 구분 문자를
|
|
104
|
+
지운 표기로 맞춰, 약어 표를 모르고도 쓸 수 있게 한다.
|
|
105
|
+
"""
|
|
106
|
+
return re.sub(r"[^a-z0-9]", "", str(label).lower())
|
|
107
|
+
|
|
108
|
+
|
|
98
109
|
class SchemaIndex:
|
|
99
110
|
"""Property-order lookup over the final-report schema.
|
|
100
111
|
|
|
@@ -105,6 +116,7 @@ class SchemaIndex:
|
|
|
105
116
|
|
|
106
117
|
def __init__(self, schema: Any) -> None:
|
|
107
118
|
self._defs = schema.get("$defs", {}) if isinstance(schema, dict) else {}
|
|
119
|
+
self._schema = schema if isinstance(schema, dict) else {}
|
|
108
120
|
|
|
109
121
|
def resolve(self, node: Any) -> dict:
|
|
110
122
|
seen: set[str] = set()
|
|
@@ -118,10 +130,21 @@ class SchemaIndex:
|
|
|
118
130
|
|
|
119
131
|
def _branches(self, node: Any) -> list[dict]:
|
|
120
132
|
resolved = self.resolve(node)
|
|
121
|
-
branches = [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
133
|
+
branches: list[dict] = []
|
|
134
|
+
pending = [resolved]
|
|
135
|
+
seen: set[int] = set()
|
|
136
|
+
while pending:
|
|
137
|
+
branch = pending.pop(0)
|
|
138
|
+
identity = id(branch)
|
|
139
|
+
if identity in seen:
|
|
140
|
+
continue
|
|
141
|
+
seen.add(identity)
|
|
142
|
+
branches.append(branch)
|
|
143
|
+
for keyword in ("allOf", "oneOf", "anyOf"):
|
|
144
|
+
pending.extend(
|
|
145
|
+
self.resolve(candidate)
|
|
146
|
+
for candidate in branch.get(keyword) or ()
|
|
147
|
+
)
|
|
125
148
|
return branches
|
|
126
149
|
|
|
127
150
|
def key_order(self, node: Any) -> list[str]:
|
|
@@ -133,17 +156,67 @@ class SchemaIndex:
|
|
|
133
156
|
return order
|
|
134
157
|
|
|
135
158
|
def child(self, node: Any, key: str) -> dict:
|
|
159
|
+
candidates: list[dict] = []
|
|
136
160
|
for branch in self._branches(node):
|
|
137
161
|
candidate = (branch.get("properties") or {}).get(key)
|
|
138
162
|
if candidate is not None:
|
|
139
|
-
|
|
140
|
-
return {}
|
|
163
|
+
candidates.append(self.resolve(candidate))
|
|
164
|
+
return max(candidates, key=_schema_detail, default={})
|
|
141
165
|
|
|
142
166
|
def item(self, node: Any) -> dict:
|
|
167
|
+
candidates: list[dict] = []
|
|
143
168
|
for branch in self._branches(node):
|
|
144
169
|
if "items" in branch:
|
|
145
|
-
|
|
146
|
-
return {}
|
|
170
|
+
candidates.append(self.resolve(branch["items"]))
|
|
171
|
+
return max(candidates, key=_schema_detail, default={})
|
|
172
|
+
|
|
173
|
+
def keys_for_label(self, label: str) -> list[str]:
|
|
174
|
+
"""전체 스키마에서 사람이 읽는 표기가 일치하는 고유 키 후보."""
|
|
175
|
+
wanted = label_key(label)
|
|
176
|
+
found: set[str] = set()
|
|
177
|
+
pending: list[Any] = [self._schema]
|
|
178
|
+
seen: set[int] = set()
|
|
179
|
+
while pending:
|
|
180
|
+
node = pending.pop()
|
|
181
|
+
if isinstance(node, list):
|
|
182
|
+
pending.extend(node)
|
|
183
|
+
continue
|
|
184
|
+
if not isinstance(node, dict) or id(node) in seen:
|
|
185
|
+
continue
|
|
186
|
+
seen.add(id(node))
|
|
187
|
+
for key in (node.get("properties") or {}):
|
|
188
|
+
if label_key(humanise(key)) == wanted:
|
|
189
|
+
found.add(key)
|
|
190
|
+
pending.extend(node.values())
|
|
191
|
+
return sorted(found)
|
|
192
|
+
|
|
193
|
+
def schema_for_key(self, key: str) -> dict:
|
|
194
|
+
"""전체 스키마에서 이 키를 가장 구체적으로 설명하는 후보."""
|
|
195
|
+
found: list[dict] = []
|
|
196
|
+
pending: list[Any] = [self._schema]
|
|
197
|
+
seen: set[int] = set()
|
|
198
|
+
while pending:
|
|
199
|
+
node = pending.pop()
|
|
200
|
+
if isinstance(node, list):
|
|
201
|
+
pending.extend(node)
|
|
202
|
+
continue
|
|
203
|
+
if not isinstance(node, dict) or id(node) in seen:
|
|
204
|
+
continue
|
|
205
|
+
seen.add(id(node))
|
|
206
|
+
candidate = (node.get("properties") or {}).get(key)
|
|
207
|
+
if isinstance(candidate, dict):
|
|
208
|
+
found.append(self.resolve(candidate))
|
|
209
|
+
pending.extend(node.values())
|
|
210
|
+
return max(found, key=_schema_detail, default={})
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _schema_detail(node: dict) -> tuple[int, int]:
|
|
214
|
+
"""구체적인 분기 스키마가 빈 호환 스키마보다 먼저 선택되게 한다."""
|
|
215
|
+
structural = sum(
|
|
216
|
+
1 for key in ("properties", "items", "required", "enum", "const", "type")
|
|
217
|
+
if key in node
|
|
218
|
+
)
|
|
219
|
+
return structural, len(str(node))
|
|
147
220
|
|
|
148
221
|
|
|
149
222
|
def _is_scalar(value: Any) -> bool:
|