okstra 0.163.2 → 0.165.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 +8 -6
- package/docs/architecture.md +24 -15
- package/docs/cli.md +15 -8
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-inspect.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +2 -2
- package/docs/project-structure-overview.md +22 -14
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/antigravity-worker.md +9 -7
- package/runtime/agents/workers/claude-worker.md +1 -0
- package/runtime/agents/workers/codex-worker.md +9 -7
- package/runtime/agents/workers/grok-worker.md +6 -4
- package/runtime/agents/workers/kimi-worker.md +6 -4
- package/runtime/bin/lib/okstra/cli.sh +5 -0
- package/runtime/bin/lib/okstra/globals.sh +2 -0
- package/runtime/bin/lib/okstra/usage.sh +5 -5
- package/runtime/bin/okstra-antigravity-exec.sh +1 -340
- package/runtime/bin/okstra-claude-exec.sh +1 -178
- package/runtime/bin/okstra-codex-exec.sh +1 -467
- package/runtime/bin/okstra-provider-exec.py +165 -190
- package/runtime/bin/okstra-trace-cleanup.sh +14 -7
- package/runtime/bin/okstra-wrapper-status.py +26 -19
- package/runtime/bin/okstra.sh +87 -91
- package/runtime/prompts/lead/adapters/cmux.md +2 -2
- package/runtime/prompts/lead/convergence.md +36 -8
- package/runtime/prompts/lead/okstra-lead-contract.md +24 -1
- package/runtime/prompts/lead/plan-body-verification.md +9 -1
- package/runtime/prompts/lead/report-writer.md +1 -0
- package/runtime/prompts/lead/team-contract.md +3 -3
- package/runtime/prompts/profiles/_common-contract.md +9 -1
- package/runtime/prompts/profiles/_coverage-critic.md +1 -1
- package/runtime/prompts/profiles/_implementation-diff-review.md +3 -1
- package/runtime/prompts/profiles/_implementation-self-check.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/implementation-planning.md +6 -4
- package/runtime/python/okstra_ctl/adapters/accounting/__init__.py +11 -0
- package/runtime/python/okstra_ctl/adapters/accounting/claude_jsonl.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/cli_artifact.py +17 -0
- package/runtime/python/okstra_ctl/adapters/accounting/unavailable.py +19 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/__init__.py +92 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cli_wrapper.py +54 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/cmux.py +68 -0
- package/runtime/python/okstra_ctl/adapters/dispatch/native_team.py +13 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/antigravity/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/antigravity.md → python/okstra_ctl/adapters/hosts/antigravity/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/capability_adapter.py +292 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/adapter.py +120 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/claude-code.md → python/okstra_ctl/adapters/hosts/claude-code/relay.md} +112 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/adapter.py +60 -0
- package/runtime/python/okstra_ctl/adapters/hosts/codex/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/codex.md → python/okstra_ctl/adapters/hosts/codex/relay.md} +52 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/adapter.py +72 -0
- package/runtime/python/okstra_ctl/adapters/hosts/external/manifest.json +1 -0
- package/runtime/{prompts/lead/adapters/external.md → python/okstra_ctl/adapters/hosts/external/relay.md} +53 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/adapter.py +63 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/hosts/kimi/relay.md +90 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/adapter.py +183 -0
- package/runtime/python/okstra_ctl/adapters/providers/antigravity/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +110 -0
- package/runtime/python/okstra_ctl/adapters/providers/claude/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +84 -0
- package/runtime/python/okstra_ctl/adapters/providers/codex/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/adapter.py +76 -0
- package/runtime/python/okstra_ctl/adapters/providers/grok/manifest.json +1 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/adapter.py +80 -0
- package/runtime/python/okstra_ctl/adapters/providers/kimi/manifest.json +1 -0
- package/runtime/python/okstra_ctl/application/__init__.py +1 -0
- package/runtime/python/okstra_ctl/application/advance_wizard.py +25 -0
- package/runtime/python/okstra_ctl/application/collect_usage.py +15 -0
- package/runtime/python/okstra_ctl/application/dispatch_assignments.py +15 -0
- package/runtime/python/okstra_ctl/application/resolve_assignment.py +93 -0
- package/runtime/python/okstra_ctl/application/resume_run.py +21 -0
- package/runtime/python/okstra_ctl/application/start_run.py +21 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +49 -826
- package/runtime/python/okstra_ctl/dispatch_core.py +244 -29
- package/runtime/python/okstra_ctl/dispatch_state.py +27 -0
- package/runtime/python/okstra_ctl/domain/__init__.py +34 -0
- package/runtime/python/okstra_ctl/domain/host.py +100 -0
- package/runtime/python/okstra_ctl/domain/provider.py +70 -0
- package/runtime/python/okstra_ctl/domain/wizard/__init__.py +19 -0
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +140 -0
- package/runtime/python/okstra_ctl/domain/worker_exec.py +102 -0
- package/runtime/python/okstra_ctl/domain/worker_role.py +34 -0
- package/runtime/python/okstra_ctl/domain/worker_stream.py +261 -0
- package/runtime/python/okstra_ctl/entrypoints/__init__.py +1 -0
- package/runtime/python/okstra_ctl/entrypoints/hosts.py +334 -0
- package/runtime/python/okstra_ctl/incremental_scope.py +16 -4
- package/runtime/python/okstra_ctl/models.py +54 -269
- package/runtime/python/okstra_ctl/ports/__init__.py +15 -0
- package/runtime/python/okstra_ctl/ports/host.py +32 -0
- package/runtime/python/okstra_ctl/ports/interaction.py +15 -0
- package/runtime/python/okstra_ctl/ports/lead_session.py +25 -0
- package/runtime/python/okstra_ctl/ports/usage_accounting.py +23 -0
- package/runtime/python/okstra_ctl/ports/worker_dispatch.py +32 -0
- package/runtime/python/okstra_ctl/registry/__init__.py +13 -0
- package/runtime/python/okstra_ctl/registry/factory_loader.py +32 -0
- package/runtime/python/okstra_ctl/registry/host_discovery.py +124 -0
- package/runtime/python/okstra_ctl/registry/host_registry.py +365 -0
- package/runtime/python/okstra_ctl/registry/provider_registry.py +149 -0
- package/runtime/python/okstra_ctl/render.py +145 -47
- package/runtime/python/okstra_ctl/report_html/common.py +71 -25
- package/runtime/python/okstra_ctl/report_html/models.py +5 -0
- package/runtime/python/okstra_ctl/report_html/render.py +1 -1
- package/runtime/python/okstra_ctl/report_html/run_usage.py +19 -0
- package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +14 -0
- package/runtime/python/okstra_ctl/report_views.py +44 -16
- package/runtime/python/okstra_ctl/run.py +80 -58
- package/runtime/python/okstra_ctl/session.py +1 -1
- package/runtime/python/okstra_ctl/stage_citations.py +52 -15
- package/runtime/python/okstra_ctl/team.py +44 -32
- package/runtime/python/okstra_ctl/user_response.py +45 -29
- package/runtime/python/okstra_ctl/wizard.py +175 -73
- package/runtime/python/okstra_ctl/worker_audit_ledger.py +29 -4
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -3
- package/runtime/python/okstra_ctl/worker_request.py +140 -0
- package/runtime/python/okstra_ctl/worker_runner.py +622 -0
- package/runtime/python/okstra_token_usage/collect.py +42 -7
- package/runtime/python/okstra_token_usage/report.py +42 -0
- package/runtime/python/okstra_token_usage/task_totals.py +88 -0
- package/runtime/schemas/final-report-v1.0.schema.json +4040 -1066
- package/runtime/schemas/final-report-v2.0.schema.json +5673 -1412
- package/runtime/skills/okstra-inspect/SKILL.md +1 -2
- package/runtime/skills/okstra-inspect/facets/logs.md +5 -5
- package/runtime/skills/okstra-inspect/facets/run-audit.md +3 -3
- package/runtime/skills/okstra-run/SKILL.md +74 -29
- package/runtime/skills/okstra-user-response/SKILL.md +15 -5
- package/runtime/templates/implementation-worker-preamble.md +1 -1
- package/runtime/templates/report-writer-prompt-preamble.md +1 -0
- package/runtime/templates/reports/final-report.template.md +3 -3
- package/runtime/templates/reports/html/assets/base.css +8 -4
- package/runtime/templates/reports/html/base.template.html +12 -6
- package/runtime/templates/reports/html/i18n/en.json +32 -7
- package/runtime/templates/reports/html/i18n/ko.json +32 -7
- package/runtime/templates/reports/html/macros/forms.html +9 -3
- package/runtime/templates/reports/html/tasks/implementation-planning.template.html +14 -19
- package/runtime/templates/reports/report.js +59 -26
- package/runtime/templates/reports/user-response.template.md +12 -8
- package/runtime/templates/worker-prompt-preamble.md +1 -1
- package/runtime/validators/validate-implementation-plan-stages.py +17 -22
- package/runtime/validators/validate-report-views.py +0 -39
- package/runtime/validators/validate-run.py +96 -14
- package/runtime/validators/validate_session_conformance.py +69 -3
- package/src/cli-registry.mjs +0 -7
- package/src/commands/execute/render-bundle.mjs +4 -4
- package/src/commands/execute/run.mjs +8 -25
- package/src/commands/execute/wizard.mjs +33 -13
- package/src/commands/lifecycle/doctor.mjs +10 -10
- package/src/commands/lifecycle/install.mjs +53 -30
- package/src/commands/lifecycle/preflight.mjs +14 -4
- package/src/lib/host-registry-client.mjs +176 -0
- package/src/lib/runtime-manifest.mjs +6 -8
- package/runtime/bin/okstra-wrapper-agy-stream.py +0 -61
- package/runtime/python/okstra_ctl/error_issue.py +0 -640
- package/runtime/python/okstra_ctl/issue_signals.py +0 -186
- package/runtime/python/okstra_ctl/lead_runtime.py +0 -115
- package/runtime/python/okstra_ctl/runner_resolution.py +0 -103
- package/runtime/skills/okstra-inspect/facets/error-issue.md +0 -77
- package/src/commands/inspect/error-issue.mjs +0 -27
- package/src/lib/runtime-readiness.mjs +0 -90
- package/src/lib/runtime-resolver.mjs +0 -123
|
@@ -30,11 +30,18 @@ from okstra_project.dirs import TASK_MANIFEST_FILENAME, OKSTRA_DIR_NAME, project
|
|
|
30
30
|
# 이는 silent drift 위험이 있어 SSOT import 로 통합한다.
|
|
31
31
|
from . import fix_cycles
|
|
32
32
|
from .analysis_inputs import ANALYSIS_TASK_TYPES
|
|
33
|
+
from .application.resolve_assignment import (
|
|
34
|
+
resolve_assignment_runner,
|
|
35
|
+
resolve_lead_provider,
|
|
36
|
+
)
|
|
37
|
+
from .application.dispatch_assignments import dispatch_assignments
|
|
38
|
+
from .adapters.dispatch import default_worker_dispatch_port, with_worker_dispatch
|
|
39
|
+
from .adapters.dispatch.cmux import dispatch_port_for_terminal_backend
|
|
33
40
|
from .paths import okstra_home
|
|
34
|
-
from .dispatch_state import BACKEND_CMUX_PANE
|
|
35
|
-
from .lead_runtime import lead_runtime_info, with_cmux_dispatch
|
|
36
41
|
from .models import UnknownProviderError, provider_ids, provider_spec
|
|
37
|
-
from .
|
|
42
|
+
from .ports.worker_dispatch import WorkerDispatchRequest
|
|
43
|
+
from .registry.host_registry import default_host_registry
|
|
44
|
+
from .registry.provider_registry import default_provider_registry
|
|
38
45
|
from .path_hints import compact_active_run_context, hydrate_run_context
|
|
39
46
|
from .paths import task_timeline_file
|
|
40
47
|
from .worker_artifacts import artifacts_from_context
|
|
@@ -80,27 +87,49 @@ def _lead_runtime(ctx: dict) -> str:
|
|
|
80
87
|
return ctx.get("LEAD_RUNTIME", "") or "claude-code"
|
|
81
88
|
|
|
82
89
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
def _lead_descriptor(ctx: dict):
|
|
91
|
+
return default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _worker_dispatch_plan(ctx: dict):
|
|
95
|
+
host_adapter = default_host_registry().resolve(_lead_runtime(ctx))
|
|
96
|
+
fallback_port = default_worker_dispatch_port(host_adapter.descriptor)
|
|
97
|
+
port = dispatch_port_for_terminal_backend(
|
|
98
|
+
ctx.get("TERMINAL_BACKEND", ""),
|
|
99
|
+
fallback_port,
|
|
100
|
+
)
|
|
101
|
+
decorated = with_worker_dispatch(host_adapter, port)
|
|
102
|
+
request = WorkerDispatchRequest(
|
|
103
|
+
project_root=Path(ctx.get("PROJECT_ROOT", "")),
|
|
104
|
+
run_manifest=Path(ctx.get("RUN_MANIFEST_PATH", "")),
|
|
105
|
+
)
|
|
106
|
+
return dispatch_assignments(request, decorated.worker_dispatch())
|
|
88
107
|
|
|
89
108
|
|
|
90
109
|
def _lead_agent(ctx: dict) -> str:
|
|
91
|
-
return
|
|
110
|
+
return _lead_descriptor(ctx).agent_id
|
|
92
111
|
|
|
93
112
|
|
|
94
113
|
def _lead_agent_label(ctx: dict) -> str:
|
|
95
|
-
return
|
|
114
|
+
return _lead_descriptor(ctx).agent_label
|
|
96
115
|
|
|
97
116
|
|
|
98
117
|
def _lead_role(ctx: dict) -> str:
|
|
99
|
-
return
|
|
118
|
+
return _lead_descriptor(ctx).role
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _lead_adapter(ctx: dict, plan) -> dict:
|
|
122
|
+
descriptor = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
123
|
+
return {
|
|
124
|
+
"name": plan.adapter_name or descriptor.id,
|
|
125
|
+
"dispatchMode": plan.dispatch_mode or descriptor.dispatch_mode,
|
|
126
|
+
"sessionAccounting": descriptor.session_accounting,
|
|
127
|
+
}
|
|
100
128
|
|
|
101
129
|
|
|
102
|
-
def
|
|
103
|
-
|
|
130
|
+
def _lead_adapter_contract(ctx: dict, plan) -> str:
|
|
131
|
+
descriptor = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
132
|
+
return plan.relay_contract or descriptor.relay_contract
|
|
104
133
|
|
|
105
134
|
|
|
106
135
|
def _runtime_resolution(ctx: dict) -> dict:
|
|
@@ -257,31 +286,71 @@ def _resolve_workers(ctx: dict) -> list[str]:
|
|
|
257
286
|
]
|
|
258
287
|
|
|
259
288
|
|
|
260
|
-
def _assignment_runner_for_ctx(
|
|
261
|
-
|
|
262
|
-
|
|
289
|
+
def _assignment_runner_for_ctx(
|
|
290
|
+
ctx: dict,
|
|
291
|
+
provider: str,
|
|
292
|
+
role: str,
|
|
293
|
+
requested_runner: str = "",
|
|
294
|
+
) -> str:
|
|
295
|
+
host = default_host_registry().resolve(_lead_runtime(ctx)).descriptor
|
|
296
|
+
spec = default_provider_registry().resolve(provider)
|
|
297
|
+
return resolve_assignment_runner(
|
|
298
|
+
host=host,
|
|
299
|
+
provider=spec,
|
|
300
|
+
role=role,
|
|
301
|
+
requested_runner=requested_runner,
|
|
302
|
+
).runner
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _resolved_lead_assignment(
|
|
306
|
+
ctx: dict,
|
|
307
|
+
requested_provider: str,
|
|
308
|
+
requested_runner: str = "",
|
|
309
|
+
):
|
|
310
|
+
host_registry = default_host_registry()
|
|
311
|
+
provider_registry = default_provider_registry()
|
|
312
|
+
assignment = resolve_lead_provider(
|
|
313
|
+
host_id=_lead_runtime(ctx),
|
|
314
|
+
requested_provider=requested_provider,
|
|
315
|
+
host_registry=host_registry,
|
|
316
|
+
provider_registry=provider_registry,
|
|
317
|
+
)
|
|
318
|
+
if requested_runner:
|
|
319
|
+
resolution = resolve_assignment_runner(
|
|
320
|
+
host=host_registry.resolve(_lead_runtime(ctx)).descriptor,
|
|
321
|
+
provider=provider_registry.resolve(assignment.provider),
|
|
322
|
+
role="lead",
|
|
323
|
+
requested_runner=requested_runner,
|
|
324
|
+
)
|
|
325
|
+
return assignment.provider, resolution.runner
|
|
326
|
+
return assignment.provider, assignment.runner
|
|
263
327
|
|
|
264
328
|
|
|
265
329
|
def _lead_assignment(ctx: dict) -> dict:
|
|
330
|
+
parsed = None
|
|
266
331
|
raw = ctx.get("LEAD_ASSIGNMENT_JSON", "")
|
|
267
332
|
if raw:
|
|
268
333
|
try:
|
|
269
|
-
|
|
270
|
-
if isinstance(
|
|
271
|
-
|
|
334
|
+
candidate = json.loads(raw)
|
|
335
|
+
if isinstance(candidate, dict):
|
|
336
|
+
parsed = candidate
|
|
272
337
|
except json.JSONDecodeError:
|
|
273
338
|
pass
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
or "
|
|
339
|
+
source = parsed or {}
|
|
340
|
+
provider, runner = _resolved_lead_assignment(
|
|
341
|
+
ctx,
|
|
342
|
+
source.get("provider", "") or ctx.get("LEAD_PROVIDER", ""),
|
|
343
|
+
source.get("runner", ""),
|
|
278
344
|
)
|
|
279
345
|
return {
|
|
346
|
+
**source,
|
|
280
347
|
"role": "lead",
|
|
281
348
|
"provider": provider,
|
|
282
|
-
"model": ctx.get("LEAD_MODEL", ""),
|
|
283
|
-
"modelExecutionValue":
|
|
284
|
-
|
|
349
|
+
"model": source.get("model", ctx.get("LEAD_MODEL", "")),
|
|
350
|
+
"modelExecutionValue": source.get(
|
|
351
|
+
"modelExecutionValue", ctx.get("LEAD_MODEL_EXECUTION_VALUE", "")
|
|
352
|
+
),
|
|
353
|
+
"runner": runner,
|
|
285
354
|
}
|
|
286
355
|
|
|
287
356
|
|
|
@@ -291,7 +360,7 @@ def _worker_assignments(ctx: dict) -> list[dict]:
|
|
|
291
360
|
try:
|
|
292
361
|
parsed = json.loads(raw)
|
|
293
362
|
if isinstance(parsed, list) and all(isinstance(row, dict) for row in parsed):
|
|
294
|
-
return parsed
|
|
363
|
+
return [_resolved_worker_assignment(ctx, row) for row in parsed]
|
|
295
364
|
except json.JSONDecodeError:
|
|
296
365
|
pass
|
|
297
366
|
catalog = _worker_catalog(ctx, include_assignments=False)
|
|
@@ -302,12 +371,39 @@ def _worker_assignments(ctx: dict) -> list[dict]:
|
|
|
302
371
|
"provider": catalog[worker_id]["agent"],
|
|
303
372
|
"model": catalog[worker_id]["model"],
|
|
304
373
|
"modelExecutionValue": catalog[worker_id]["modelExecutionValue"],
|
|
305
|
-
"runner": _assignment_runner_for_ctx(
|
|
374
|
+
"runner": _assignment_runner_for_ctx(
|
|
375
|
+
ctx,
|
|
376
|
+
catalog[worker_id]["agent"],
|
|
377
|
+
"report-writer" if worker_id == "report-writer" else "analyser",
|
|
378
|
+
),
|
|
306
379
|
}
|
|
307
380
|
for worker_id in _resolve_workers(ctx)
|
|
308
381
|
]
|
|
309
382
|
|
|
310
383
|
|
|
384
|
+
def _resolved_worker_assignment(ctx: dict, source: dict) -> dict:
|
|
385
|
+
worker_id = source.get("workerId", "")
|
|
386
|
+
role = source.get("role", "") or (
|
|
387
|
+
"report-writer" if worker_id == "report-writer" else "analyser"
|
|
388
|
+
)
|
|
389
|
+
provider = source.get("provider", "") or _fallback_worker_provider(
|
|
390
|
+
ctx, worker_id
|
|
391
|
+
)
|
|
392
|
+
runner = _assignment_runner_for_ctx(
|
|
393
|
+
ctx,
|
|
394
|
+
provider,
|
|
395
|
+
role,
|
|
396
|
+
source.get("runner", ""),
|
|
397
|
+
)
|
|
398
|
+
return {
|
|
399
|
+
**source,
|
|
400
|
+
"workerId": worker_id,
|
|
401
|
+
"role": role,
|
|
402
|
+
"provider": provider,
|
|
403
|
+
"runner": runner,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
311
407
|
def _worker_catalog(ctx: dict, *, include_assignments: bool = True) -> dict:
|
|
312
408
|
assignments = {} if not include_assignments else {
|
|
313
409
|
row.get("workerId"): row for row in _worker_assignments(ctx)
|
|
@@ -329,7 +425,11 @@ def _worker_catalog(ctx: dict, *, include_assignments: bool = True) -> dict:
|
|
|
329
425
|
"agentLabel": provider_info.display_label,
|
|
330
426
|
"provider": provider,
|
|
331
427
|
"runner": assignment.get(
|
|
332
|
-
"runner", _assignment_runner_for_ctx(
|
|
428
|
+
"runner", _assignment_runner_for_ctx(
|
|
429
|
+
ctx,
|
|
430
|
+
provider,
|
|
431
|
+
"report-writer" if worker_id == "report-writer" else "analyser",
|
|
432
|
+
),
|
|
333
433
|
),
|
|
334
434
|
"model": assignment.get("model", model),
|
|
335
435
|
"modelExecutionValue": assignment.get("modelExecutionValue", execution),
|
|
@@ -551,6 +651,7 @@ def render_active_run_context(active_context_path: str, ctx: dict) -> None:
|
|
|
551
651
|
def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
552
652
|
selected = _resolve_workers(ctx)
|
|
553
653
|
catalog = _worker_catalog(ctx)
|
|
654
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
554
655
|
workers = []
|
|
555
656
|
for w in selected:
|
|
556
657
|
m = catalog[w]
|
|
@@ -577,7 +678,7 @@ def render_team_state(team_state_path: str, ctx: dict) -> None:
|
|
|
577
678
|
"leadRuntime": _lead_runtime(ctx),
|
|
578
679
|
"leadAssignment": _lead_assignment(ctx),
|
|
579
680
|
"workerAssignments": _worker_assignments(ctx),
|
|
580
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
681
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
581
682
|
"leadEventsPath": ctx.get("LEAD_EVENTS_RELATIVE_PATH", ""),
|
|
582
683
|
"runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
|
|
583
684
|
"workflowState": ctx.get("CURRENT_RUN_STATUS", ""),
|
|
@@ -1112,6 +1213,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1112
1213
|
except Exception:
|
|
1113
1214
|
existing = {}
|
|
1114
1215
|
reviewers = _resolve_workers(ctx)
|
|
1216
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1115
1217
|
catalog = _worker_catalog(ctx)
|
|
1116
1218
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
1117
1219
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
@@ -1170,7 +1272,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1170
1272
|
"leadRuntime": _lead_runtime(ctx),
|
|
1171
1273
|
"leadAssignment": _lead_assignment(ctx),
|
|
1172
1274
|
"workerAssignments": _worker_assignments(ctx),
|
|
1173
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
1275
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
1174
1276
|
"workCategory": work_category,
|
|
1175
1277
|
# user-managed status (set-work-status CLI) — carried across re-renders
|
|
1176
1278
|
"workStatus": existing.get("workStatus", ""),
|
|
@@ -1308,7 +1410,7 @@ def render_task_manifest(manifest_path: str, ctx: dict) -> None:
|
|
|
1308
1410
|
"leadSession": {
|
|
1309
1411
|
"sessionId": ctx.get("CLAUDE_SESSION_ID", ""),
|
|
1310
1412
|
"resumeCommandPath": ctx.get("CLAUDE_RESUME_COMMAND_RELATIVE_PATH", ""),
|
|
1311
|
-
"accounting":
|
|
1413
|
+
"accounting": _lead_descriptor(ctx).session_accounting,
|
|
1312
1414
|
},
|
|
1313
1415
|
"convergence": convergence_block,
|
|
1314
1416
|
"createdAt": existing.get("createdAt") or ctx.get("RUN_TIMESTAMP_ISO", ""),
|
|
@@ -1418,6 +1520,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1418
1520
|
except Exception:
|
|
1419
1521
|
task_manifest = {}
|
|
1420
1522
|
reviewers = _resolve_workers(ctx)
|
|
1523
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1421
1524
|
catalog = _worker_catalog(ctx)
|
|
1422
1525
|
required_worker_roles = _required_worker_roles(ctx, reviewers)
|
|
1423
1526
|
worker_prompt_paths = {item: catalog[item]["promptPath"] for item in reviewers}
|
|
@@ -1452,7 +1555,7 @@ def render_run_manifest(run_manifest_path: str, ctx: dict) -> None:
|
|
|
1452
1555
|
"runtimeResolution": _runtime_resolution(ctx),
|
|
1453
1556
|
"leadAssignment": _lead_assignment(ctx),
|
|
1454
1557
|
"workerAssignments": _worker_assignments(ctx),
|
|
1455
|
-
"leadAdapter": _lead_adapter(ctx),
|
|
1558
|
+
"leadAdapter": _lead_adapter(ctx, worker_dispatch_plan),
|
|
1456
1559
|
"workCategory": task_manifest.get(
|
|
1457
1560
|
"workCategory", ctx.get("WORKFLOW_WORK_CATEGORY", "unknown")
|
|
1458
1561
|
),
|
|
@@ -1957,6 +2060,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1957
2060
|
selected = _resolve_workers(ctx)
|
|
1958
2061
|
catalog = _worker_catalog(ctx)
|
|
1959
2062
|
lead_runtime = _lead_runtime(ctx)
|
|
2063
|
+
worker_dispatch_plan = _worker_dispatch_plan(ctx)
|
|
1960
2064
|
lead_role = _lead_role(ctx)
|
|
1961
2065
|
lead_agent_label = _lead_agent_label(ctx)
|
|
1962
2066
|
lead_model = ctx.get("LEAD_MODEL", "")
|
|
@@ -1966,7 +2070,7 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
1966
2070
|
home_prompts / "lead" / "okstra-lead-contract.md"
|
|
1967
2071
|
)
|
|
1968
2072
|
adapter_contract_path = ctx.get("OKSTRA_LEAD_ADAPTER_CONTRACT_PATH") or str(
|
|
1969
|
-
home_prompts /
|
|
2073
|
+
home_prompts / _lead_adapter_contract(ctx, worker_dispatch_plan)
|
|
1970
2074
|
)
|
|
1971
2075
|
context_loader_path = ctx.get("OKSTRA_CONTEXT_LOADER_PATH") or str(
|
|
1972
2076
|
home_prompts / "lead" / "context-loader.md"
|
|
@@ -2163,27 +2267,21 @@ def inject_lead_prompt_computed_tokens(ctx: dict) -> None:
|
|
|
2163
2267
|
"schema and write rules. Do not re-inline either contract."
|
|
2164
2268
|
)
|
|
2165
2269
|
|
|
2166
|
-
|
|
2270
|
+
lead_descriptor = _lead_descriptor(ctx)
|
|
2271
|
+
if lead_descriptor.has_claude_session:
|
|
2167
2272
|
lead_session_block = (
|
|
2168
2273
|
"## Session\n"
|
|
2169
2274
|
"\n"
|
|
2170
|
-
"-
|
|
2171
|
-
"-
|
|
2172
|
-
)
|
|
2173
|
-
elif lead_runtime == "codex":
|
|
2174
|
-
lead_session_block = (
|
|
2175
|
-
"## Session\n"
|
|
2176
|
-
"\n"
|
|
2177
|
-
"- Lead runtime: `codex`\n"
|
|
2178
|
-
"- Codex run resume is artifact/checkpoint based; `okstra codex-run` "
|
|
2179
|
-
"does not create a Claude session id."
|
|
2275
|
+
f"- Session ID: `{ctx.get('CLAUDE_SESSION_ID', '')}`\n"
|
|
2276
|
+
f"- Resume: `{ctx.get('CLAUDE_RESUME_COMMAND_RELATIVE_PATH', '')}`"
|
|
2180
2277
|
)
|
|
2181
2278
|
else:
|
|
2182
2279
|
lead_session_block = (
|
|
2183
2280
|
"## Session\n"
|
|
2184
2281
|
"\n"
|
|
2185
|
-
f"-
|
|
2186
|
-
f"-
|
|
2282
|
+
f"- Lead runtime: `{lead_descriptor.id}`\n"
|
|
2283
|
+
f"- Lead session accounting: {lead_descriptor.session_accounting}\n"
|
|
2284
|
+
"- Resume behavior is provided by the selected host lead-session port."
|
|
2187
2285
|
)
|
|
2188
2286
|
|
|
2189
2287
|
# Compute results (deterministic from ctx, 덮어쓰기)
|
|
@@ -5,21 +5,36 @@ import re
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
# Every block whose rows prose cites but no section of its own renders, with
|
|
8
|
-
# the keys each one names its content and its
|
|
9
|
-
# all of them would misread `crossVerification.consensus`, whose
|
|
10
|
-
# holds provenance while `evidence.primary` uses the same word
|
|
11
|
-
# so the block a row came from is named here rather than guessed.
|
|
8
|
+
# the keys each one names its content, its provenance, and its confidence by. A
|
|
9
|
+
# key hunt across all of them would misread `crossVerification.consensus`, whose
|
|
10
|
+
# `evidence` key holds provenance while `evidence.primary` uses the same word
|
|
11
|
+
# for content — so the block a row came from is named here rather than guessed.
|
|
12
|
+
#
|
|
13
|
+
# Source and confidence are separate columns because most blocks carry only one
|
|
14
|
+
# of the two: `evidence.primary` cites a file and never rates itself, while
|
|
15
|
+
# `evidence.secondary` rates itself and cites nothing. Folding them into one
|
|
16
|
+
# field is what made the ledger label every row "source · confidence" and then
|
|
17
|
+
# print a single value under it.
|
|
18
|
+
#
|
|
19
|
+
# `endStateCoverage` is deliberately absent. Its rows hold an id pair
|
|
20
|
+
# (`EB-001` covered by `R-001`) rather than a statement, so the ledger rendered
|
|
21
|
+
# them as an id with no body, and the requirement-coverage table already names
|
|
22
|
+
# every one of them in its Source column.
|
|
23
|
+
#
|
|
24
|
+
# The kind each row carries is a vocabulary key, not the words the reader sees:
|
|
25
|
+
# the labels live in the i18n `ledgerKind` table so they arrive in the reader's
|
|
26
|
+
# language. `evidence.primary` was previously stamped "unclassified", which
|
|
27
|
+
# named the renderer's own indecision rather than anything about the row.
|
|
12
28
|
_LEDGER_BLOCKS = (
|
|
13
|
-
(("evidence", "primary"), "evidence", "source", "
|
|
14
|
-
(("evidence", "secondary"), "hypothesis", "confidence", "hypothesis"),
|
|
15
|
-
(("analysisCommon", "confirmedFacts"), "statement", "", "confirmed
|
|
16
|
-
(("analysisCommon", "inferences"), "statement", "confidence", "inference"),
|
|
17
|
-
(("analysisCommon", "unknowns"), "question", "reason", "unknown"),
|
|
18
|
-
(("crossVerification", "consensus"), "statement", "evidence", "cross-check
|
|
19
|
-
(("crossVerification", "differences"), "disagreement", "workersPosition", "cross-check
|
|
20
|
-
(("missingInformation",), "item", "risk", "missing
|
|
21
|
-
(("
|
|
22
|
-
(("followUpTasks",), "title", "reason", "follow-up"),
|
|
29
|
+
(("evidence", "primary"), "evidence", "source", "", "code-evidence"),
|
|
30
|
+
(("evidence", "secondary"), "hypothesis", "", "confidence", "hypothesis"),
|
|
31
|
+
(("analysisCommon", "confirmedFacts"), "statement", "", "", "confirmed-fact"),
|
|
32
|
+
(("analysisCommon", "inferences"), "statement", "", "confidence", "inference"),
|
|
33
|
+
(("analysisCommon", "unknowns"), "question", "reason", "", "unknown"),
|
|
34
|
+
(("crossVerification", "consensus"), "statement", "evidence", "", "cross-check-consensus"),
|
|
35
|
+
(("crossVerification", "differences"), "disagreement", "workersPosition", "", "cross-check-dissent"),
|
|
36
|
+
(("missingInformation",), "item", "risk", "", "missing-information"),
|
|
37
|
+
(("followUpTasks",), "title", "reason", "", "follow-up"),
|
|
23
38
|
)
|
|
24
39
|
|
|
25
40
|
|
|
@@ -32,23 +47,39 @@ def _dig(data: dict, path: tuple[str, ...]) -> list:
|
|
|
32
47
|
return node if isinstance(node, list) else []
|
|
33
48
|
|
|
34
49
|
|
|
35
|
-
def
|
|
50
|
+
def _joined(value: object) -> str:
|
|
51
|
+
"""One reader-facing string for a cell a block may fill with either a
|
|
52
|
+
sentence or a list of citations. `str()` on a list prints Python's own
|
|
53
|
+
repr — quotes, brackets and all — straight into the report.
|
|
54
|
+
"""
|
|
55
|
+
if isinstance(value, (list, tuple)):
|
|
56
|
+
return " · ".join(str(item) for item in value if item)
|
|
57
|
+
return str(value or "")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _ledger_row(
|
|
61
|
+
row: dict, text_key: str, source_key: str, confidence_key: str, kind: str
|
|
62
|
+
) -> dict[str, object]:
|
|
36
63
|
return {
|
|
37
64
|
"id": row.get("id", ""),
|
|
38
65
|
"kind": kind,
|
|
39
|
-
"text":
|
|
66
|
+
"text": _joined(row.get(text_key)),
|
|
40
67
|
"codeEvidence": row.get("currentCodeEvidence") or [],
|
|
41
|
-
"source":
|
|
68
|
+
"source": _joined(row.get(source_key)) if source_key else "",
|
|
69
|
+
"confidence": _joined(row.get(confidence_key)) if confidence_key else "",
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
|
|
45
|
-
def _own_section_ids(data: dict) -> set[str]:
|
|
73
|
+
def _own_section_ids(data: dict, omitted_fields: tuple[str, ...] = ()) -> set[str]:
|
|
46
74
|
"""Ids a section of the report already renders and anchors.
|
|
47
75
|
|
|
48
76
|
The ledger is the fallback home for a cited row, so it must not claim an id
|
|
49
77
|
that has one — two elements with the same anchor send half the links to the
|
|
50
78
|
wrong place. `crossVerification.consensus` numbers its rows `C-001` in some
|
|
51
79
|
runs, exactly where a clarification lives.
|
|
80
|
+
|
|
81
|
+
`omitted_fields` names task-block fields the HTML template does not render,
|
|
82
|
+
so their ids do not count as anchored.
|
|
52
83
|
"""
|
|
53
84
|
from ..report_contract import TASK_TYPE_DATA_PROPERTY
|
|
54
85
|
|
|
@@ -56,20 +87,34 @@ def _own_section_ids(data: dict) -> set[str]:
|
|
|
56
87
|
_collect_ids(data.get("clarificationItems", []), found)
|
|
57
88
|
property_name = TASK_TYPE_DATA_PROPERTY.get(data.get("header", {}).get("taskType", ""))
|
|
58
89
|
if property_name:
|
|
59
|
-
|
|
90
|
+
block = data.get(property_name, {})
|
|
91
|
+
if omitted_fields and isinstance(block, dict):
|
|
92
|
+
block = {
|
|
93
|
+
key: value for key, value in block.items() if key not in omitted_fields
|
|
94
|
+
}
|
|
95
|
+
_collect_ids(block, found)
|
|
60
96
|
return found
|
|
61
97
|
|
|
62
98
|
|
|
63
99
|
def evidence_index(data: dict) -> dict[str, object]:
|
|
100
|
+
"""The rows the ledger carries, keyed by id.
|
|
101
|
+
|
|
102
|
+
A row whose statement came out empty is dropped rather than listed: the
|
|
103
|
+
ledger exists so a cited id resolves to something the reader can read, and
|
|
104
|
+
an id above a blank line resolves to nothing.
|
|
105
|
+
"""
|
|
64
106
|
owned = _own_section_ids(data)
|
|
65
107
|
rows: dict[str, object] = {}
|
|
66
|
-
for path, text_key, source_key, kind in _LEDGER_BLOCKS:
|
|
108
|
+
for path, text_key, source_key, confidence_key, kind in _LEDGER_BLOCKS:
|
|
67
109
|
for row in _dig(data, path):
|
|
68
110
|
if not isinstance(row, dict):
|
|
69
111
|
continue
|
|
70
112
|
row_id = row.get("id")
|
|
71
|
-
if row_id
|
|
72
|
-
|
|
113
|
+
if not row_id or row_id in owned or row_id in rows:
|
|
114
|
+
continue
|
|
115
|
+
entry = _ledger_row(row, text_key, source_key, confidence_key, kind)
|
|
116
|
+
if entry["text"]:
|
|
117
|
+
rows[row_id] = entry
|
|
73
118
|
return rows
|
|
74
119
|
|
|
75
120
|
|
|
@@ -88,7 +133,7 @@ def _collect_ids(value: object, found: set[str]) -> None:
|
|
|
88
133
|
_ROW_ID = re.compile(r"[A-Z]{1,3}-\d+")
|
|
89
134
|
|
|
90
135
|
|
|
91
|
-
def anchor_index(data: dict) -> dict[str, str]:
|
|
136
|
+
def anchor_index(data: dict, omitted_fields: tuple[str, ...] = ()) -> dict[str, str]:
|
|
92
137
|
"""Map every row a reader can reach to the anchor name that lands on it.
|
|
93
138
|
|
|
94
139
|
Prose cites ids across section boundaries — a hotspot names a
|
|
@@ -98,9 +143,10 @@ def anchor_index(data: dict) -> dict[str, str]:
|
|
|
98
143
|
|
|
99
144
|
It stops there. `summary` is the AI-facing digest and
|
|
100
145
|
`analysisCommon.scope` describes the analysis target rather than listing
|
|
101
|
-
rows; neither renders, so a link to one would land nowhere.
|
|
146
|
+
rows; neither renders, so a link to one would land nowhere. Same for the
|
|
147
|
+
blocks a template declares in `omitted_fields`.
|
|
102
148
|
"""
|
|
103
|
-
found = _own_section_ids(data) | set(evidence_index(data))
|
|
149
|
+
found = _own_section_ids(data, omitted_fields) | set(evidence_index(data))
|
|
104
150
|
return {row_id: f"id-{row_id}" for row_id in sorted(found) if _ROW_ID.fullmatch(row_id)}
|
|
105
151
|
|
|
106
152
|
|
|
@@ -66,6 +66,11 @@ class HumanReportView:
|
|
|
66
66
|
template_name: str
|
|
67
67
|
context: dict[str, object]
|
|
68
68
|
figures: tuple[FigureModel, ...]
|
|
69
|
+
# Task-block fields this template leaves out of the HTML. Their rows still
|
|
70
|
+
# exist in the data and the markdown, so prose keeps citing their ids — but
|
|
71
|
+
# an anchor to a row this document never renders scrolls nowhere, which
|
|
72
|
+
# reads as a broken report rather than as a deliberate omission.
|
|
73
|
+
omitted_fields: tuple[str, ...] = ()
|
|
69
74
|
|
|
70
75
|
|
|
71
76
|
@dataclass(frozen=True)
|
|
@@ -130,7 +130,7 @@ def render_v2_html_view(
|
|
|
130
130
|
env.policies["json.dumps_kwargs"] = {"sort_keys": True, "ensure_ascii": False}
|
|
131
131
|
# Binding the index here is what lets a template cite an id without
|
|
132
132
|
# threading the index through every macro and call site.
|
|
133
|
-
anchors = anchor_index(data)
|
|
133
|
+
anchors = anchor_index(data, view.omitted_fields)
|
|
134
134
|
chrome = load_dictionary(lang, HTML_DICTIONARY_REL)
|
|
135
135
|
translate = make_jinja_global(chrome)
|
|
136
136
|
env.globals["t"] = translate
|
|
@@ -23,6 +23,7 @@ def _totals_row(row: object) -> dict[str, str]:
|
|
|
23
23
|
values = row if isinstance(row, dict) else {}
|
|
24
24
|
return {
|
|
25
25
|
"rawTokens": format_int(values.get("totalTokens")),
|
|
26
|
+
"cacheReadTokens": format_int(values.get("cacheReadTokens")),
|
|
26
27
|
"billableTokens": format_int(values.get("billableTokens")),
|
|
27
28
|
"cost": format_usd(values.get("costUsd")),
|
|
28
29
|
}
|
|
@@ -43,6 +44,7 @@ def _agent_row(row: dict) -> dict[str, str]:
|
|
|
43
44
|
"model": str(row.get("model") or ""),
|
|
44
45
|
"status": str(row.get("status") or ""),
|
|
45
46
|
"rawTokens": format_int(row.get("totalTokens")),
|
|
47
|
+
"cacheReadTokens": format_int(row.get("cacheReadTokens")),
|
|
46
48
|
"billableTokens": format_int(row.get("billableTokens")),
|
|
47
49
|
"cost": format_usd(row.get("costUsd")),
|
|
48
50
|
"duration": format_duration_ms(row.get("durationMs")),
|
|
@@ -70,15 +72,31 @@ def _unaccounted(rows: list[dict], grand: dict) -> dict[str, str] | None:
|
|
|
70
72
|
gap = grand_tokens - _sum(rows, "totalTokens")
|
|
71
73
|
if gap <= 0:
|
|
72
74
|
return None
|
|
75
|
+
cache_gap = (_number(grand.get("cacheReadTokens")) or 0) - _sum(rows, "cacheReadTokens")
|
|
73
76
|
billable_gap = (_number(grand.get("billableTokens")) or 0) - _sum(rows, "billableTokens")
|
|
74
77
|
cost_gap = (_number(grand.get("costUsd")) or 0) - _sum(rows, "costUsd")
|
|
75
78
|
return {
|
|
76
79
|
"rawTokens": format_int(gap),
|
|
80
|
+
"cacheReadTokens": format_int(max(0, cache_gap)),
|
|
77
81
|
"billableTokens": format_int(max(0, billable_gap)),
|
|
78
82
|
"cost": format_usd(max(0.0, cost_gap)),
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
|
|
86
|
+
def _task_cumulative(row: object) -> dict[str, str] | None:
|
|
87
|
+
"""What the task has spent over every run, when more than this one exists.
|
|
88
|
+
|
|
89
|
+
A single-run task would repeat the grand total word for word, and a reader
|
|
90
|
+
seeing the same figure twice reads it as a second charge.
|
|
91
|
+
"""
|
|
92
|
+
if not isinstance(row, dict):
|
|
93
|
+
return None
|
|
94
|
+
run_count = _number(row.get("runCount")) or 0
|
|
95
|
+
if run_count < 2:
|
|
96
|
+
return None
|
|
97
|
+
return {**_totals_row(row), "runCount": format_int(run_count)}
|
|
98
|
+
|
|
99
|
+
|
|
82
100
|
_MEASURED_KEYS = ("totalTokens", "billableTokens", "costUsd", "durationMs")
|
|
83
101
|
|
|
84
102
|
|
|
@@ -106,5 +124,6 @@ def run_usage(data: dict) -> dict[str, object] | None:
|
|
|
106
124
|
"rows": [_agent_row(row) for row in rows],
|
|
107
125
|
"unaccounted": _unaccounted(rows, usage.get("grand") or {}),
|
|
108
126
|
**totals,
|
|
127
|
+
"taskCumulative": _task_cumulative(usage.get("taskCumulative")),
|
|
109
128
|
"cliCost": format_usd(cli_cost) if cli_cost else "",
|
|
110
129
|
}
|
|
@@ -74,6 +74,19 @@ def _stage_figure(planning: dict):
|
|
|
74
74
|
return stage_map_figure(nodes=nodes, edges=edges, title="Implementation stage dependencies")
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
# Blocks the approver's view leaves to the markdown report: the checklist the
|
|
78
|
+
# implementer works from, the dependency and rollback tables an incident reader
|
|
79
|
+
# needs, and the verification rounds the audit trail keeps. Declared here so the
|
|
80
|
+
# ids they carry stop being anchor targets in this document.
|
|
81
|
+
_OMITTED_FIELDS = (
|
|
82
|
+
"validationChecklist",
|
|
83
|
+
"crossProjectDependencies",
|
|
84
|
+
"dependencyMigrationRisk",
|
|
85
|
+
"rollbackStrategy",
|
|
86
|
+
"planBodyVerification",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
77
90
|
def build_implementation_planning_view(data: dict) -> HumanReportView:
|
|
78
91
|
planning = data["implementationPlanning"]
|
|
79
92
|
figure = _stage_figure(planning)
|
|
@@ -97,4 +110,5 @@ def build_implementation_planning_view(data: dict) -> HumanReportView:
|
|
|
97
110
|
"html/tasks/implementation-planning.template.html",
|
|
98
111
|
context,
|
|
99
112
|
(figure,),
|
|
113
|
+
_OMITTED_FIELDS,
|
|
100
114
|
)
|