okstra 0.210.0 → 0.212.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/dist/cli-registry.mjs +6 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/docs/cli.md +3 -2
- package/docs/project-structure-overview.md +2 -0
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/wizard/prompts.ko.json +3 -3
- package/runtime/python/okstra_ctl/locks.py +39 -0
- package/runtime/python/okstra_ctl/manager_launch.py +2 -1
- package/runtime/python/okstra_ctl/manager_paths.py +4 -0
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-deliverable.md +5 -4
- package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-verifier.md +9 -1
- package/runtime/python/okstra_ctl/phases/implementation/report.py +66 -19
- package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation.template.html +8 -7
- package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +6 -2
- package/runtime/python/okstra_ctl/report_html/run_usage.py +9 -0
- package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -8
- package/runtime/python/okstra_ctl/report_projections.py +18 -2
- package/runtime/python/okstra_ctl/report_synthesis_packet.py +19 -0
- package/runtime/python/okstra_ctl/report_translation.py +27 -6
- package/runtime/python/okstra_ctl/wizard/state.py +30 -1
- package/runtime/python/okstra_ctl/wizard/steps_options.py +19 -0
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +7 -1
- package/runtime/python/okstra_ctl/worktree_lock_cli.py +87 -0
- package/runtime/schemas/final-report-v3.0.schema.json +11 -0
- package/runtime/templates/reports/html/assets/base.css +4 -2
- package/runtime/templates/reports/html/base.template.html +14 -9
- package/runtime/templates/reports/html/i18n/en.json +91 -2
- package/runtime/templates/reports/html/i18n/ko.json +91 -2
- package/runtime/templates/reports/html/macros/forms.html +8 -7
|
@@ -33,6 +33,22 @@ _STATUS = {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
def _report_workers(team_state: Mapping[str, Any]) -> list[dict]:
|
|
37
|
+
"""리드 자신의 워커 항목을 뺀 워커 행.
|
|
38
|
+
|
|
39
|
+
team-state 는 리드를 `workerId: lead` 워커로도 적는다(agent 없음, 상태
|
|
40
|
+
`dispatched`). 리드 행은 `lead`·`leadUsage` 에서 따로 만들므로 이 항목을
|
|
41
|
+
표에 올리면 "Codex · lead · unknown · 미실행" 행이 하나 더 생긴다.
|
|
42
|
+
"""
|
|
43
|
+
return [
|
|
44
|
+
row for row in accounting_workers(team_state)
|
|
45
|
+
if "lead" not in {
|
|
46
|
+
str(row.get("workerId") or "").strip().lower(),
|
|
47
|
+
str(row.get("role") or "").strip().lower(),
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
def _text(value: object, fallback: str) -> str:
|
|
37
53
|
text = str(value or "").strip()
|
|
38
54
|
return text or fallback
|
|
@@ -148,7 +164,7 @@ def project_execution(
|
|
|
148
164
|
"role": "Okstra lead",
|
|
149
165
|
}
|
|
150
166
|
lead_usage = team_state.get("leadUsage")
|
|
151
|
-
worker_rows =
|
|
167
|
+
worker_rows = _report_workers(team_state)
|
|
152
168
|
result: dict[str, Any] = {
|
|
153
169
|
"executionStatus": [
|
|
154
170
|
_execution_row(
|
|
@@ -180,7 +196,7 @@ def _usage_row(summary: Mapping[str, Any], prefix: str, cost: object) -> dict[st
|
|
|
180
196
|
|
|
181
197
|
def _worker_details(team_state: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
182
198
|
result = []
|
|
183
|
-
for worker in
|
|
199
|
+
for worker in _report_workers(team_state):
|
|
184
200
|
if not isinstance(worker, Mapping):
|
|
185
201
|
continue
|
|
186
202
|
usage = worker.get("usage") if isinstance(worker.get("usage"), Mapping) else {}
|
|
@@ -295,6 +295,7 @@ class ReportSynthesisPacket:
|
|
|
295
295
|
"Do not invent a value when a source is missing or contradictory.",
|
|
296
296
|
"Write only the narrative, pointer, and audit artifacts named by the dispatch.",
|
|
297
297
|
*_INTERNAL_IDENTIFIER_INSTRUCTION,
|
|
298
|
+
*_READER_PROSE_INSTRUCTION,
|
|
298
299
|
*self._schema_instructions(),
|
|
299
300
|
*self._task_instructions(),
|
|
300
301
|
*self._carry_instructions(),
|
|
@@ -335,6 +336,7 @@ class ReportSynthesisPacket:
|
|
|
335
336
|
# 작성자가 실제로 읽는 것은 마크다운이다. JSON 정본에만 실으면 지시가
|
|
336
337
|
# 도달하지 않는다.
|
|
337
338
|
lines.extend(f"- {text}" for text in _INTERNAL_IDENTIFIER_INSTRUCTION)
|
|
339
|
+
lines.extend(f"- {text}" for text in _READER_PROSE_INSTRUCTION)
|
|
338
340
|
lines.extend(f"- {text}" for text in self._schema_instructions())
|
|
339
341
|
lines.extend(f"- {text}" for text in self._task_instructions())
|
|
340
342
|
lines.extend(f"- {text}" for text in self._carry_instructions())
|
|
@@ -573,6 +575,23 @@ _INTERNAL_IDENTIFIER_INSTRUCTION = (
|
|
|
573
575
|
)
|
|
574
576
|
|
|
575
577
|
|
|
578
|
+
# 리포트를 읽는 사람은 run 을 보지 않았다. 아래 규칙도 집행 코드가 없고(서사는
|
|
579
|
+
# 자유 서술이다) 저자 지시로만 전달한다 — dev-11126 #003 서사가 어긴 네 가지.
|
|
580
|
+
_READER_PROSE_INSTRUCTION = (
|
|
581
|
+
"Write every reader-facing field for a reader who did not watch the run: "
|
|
582
|
+
"each sentence must make sense without the worker results, the plan, or "
|
|
583
|
+
"the lead's notes.",
|
|
584
|
+
"Explain a plan- or run-internal id (`VC-`, `OOP-`, `P-Step-`, `OF-`, "
|
|
585
|
+
"`IO-`, `C-`) in words the first time a field uses it, or leave it out; "
|
|
586
|
+
"never let a bare id carry the meaning of a sentence.",
|
|
587
|
+
"Keep commit hashes and file paths out of narrative sentences; put them "
|
|
588
|
+
"only in evidence lines (`evidence`, `evidenceRefs` and other citation "
|
|
589
|
+
"fields).",
|
|
590
|
+
"Name a term by what it does (\"the function that decides whether the "
|
|
591
|
+
"server is local\") rather than by jargon (\"predicate\").",
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
576
595
|
CARRIED_STAGES_LABEL = "Carried plan stages"
|
|
577
596
|
|
|
578
597
|
|
|
@@ -14,7 +14,9 @@ reader merely reads is offered, and the translator decides.
|
|
|
14
14
|
|
|
15
15
|
When a key name carries load-bearing values in even one place, it counts as
|
|
16
16
|
structural everywhere. Missing a translation leaves English on the page;
|
|
17
|
-
translating an anchor id breaks the link.
|
|
17
|
+
translating an anchor id breaks the link. ``PROSE_FIELDS`` is the one
|
|
18
|
+
exception: a field named by its enclosing keys, where that key holds prose
|
|
19
|
+
even though the same name is an id or a path elsewhere.
|
|
18
20
|
"""
|
|
19
21
|
from __future__ import annotations
|
|
20
22
|
|
|
@@ -328,6 +330,23 @@ STRUCTURAL_KEYS = frozenset({
|
|
|
328
330
|
})
|
|
329
331
|
|
|
330
332
|
|
|
333
|
+
# Fields whose key is structural by name but holds a sentence at this position.
|
|
334
|
+
# Each tuple matches the tail of the key path with array indices dropped;
|
|
335
|
+
# `endStateCoverage[].coveredBy` is absent because there it is a linked id.
|
|
336
|
+
PROSE_FIELDS = frozenset({
|
|
337
|
+
("requirementCoverage", "coveredBy"),
|
|
338
|
+
("commitList", "files"),
|
|
339
|
+
("manualUserTest", "items", "target"),
|
|
340
|
+
("manualUserTest", "items", "steps"),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _is_prose_field(keys: tuple[str, ...]) -> bool:
|
|
345
|
+
if keys and keys[-1] in PROSE_KEYS:
|
|
346
|
+
return True
|
|
347
|
+
return any(keys[-len(field):] == field for field in PROSE_FIELDS)
|
|
348
|
+
|
|
349
|
+
|
|
331
350
|
# A key name says what a field is *for*; these say what one value actually is.
|
|
332
351
|
# `evidence` is the largest prose bucket in a real report and also holds bare
|
|
333
352
|
# `src/server.ts:33` citations — the key cannot separate them, the value can.
|
|
@@ -367,16 +386,18 @@ def unescape_token(token: str) -> str:
|
|
|
367
386
|
return token.replace("~1", "/").replace("~0", "~")
|
|
368
387
|
|
|
369
388
|
|
|
370
|
-
def _walk(node: Any, pointer: str,
|
|
389
|
+
def _walk(node: Any, pointer: str, keys: tuple[str, ...]) -> Iterator[tuple[str, str]]:
|
|
371
390
|
if isinstance(node, Mapping):
|
|
372
391
|
for child_key, value in node.items():
|
|
373
|
-
yield from _walk(
|
|
392
|
+
yield from _walk(
|
|
393
|
+
value, f"{pointer}/{escape_token(str(child_key))}", (*keys, str(child_key))
|
|
394
|
+
)
|
|
374
395
|
elif isinstance(node, (list, tuple)):
|
|
375
396
|
for index, value in enumerate(node):
|
|
376
397
|
# An array element inherits the key that named the array, so
|
|
377
398
|
# `evidence: [...]` translates each entry.
|
|
378
|
-
yield from _walk(value, f"{pointer}/{index}",
|
|
379
|
-
elif isinstance(node, str) and
|
|
399
|
+
yield from _walk(value, f"{pointer}/{index}", keys)
|
|
400
|
+
elif isinstance(node, str) and _is_prose_field(keys) and is_translatable_value(node):
|
|
380
401
|
yield pointer, node
|
|
381
402
|
|
|
382
403
|
|
|
@@ -386,7 +407,7 @@ def extract(data: Mapping[str, Any]) -> dict[str, str]:
|
|
|
386
407
|
The translator fills in this map rather than authoring pointers, so a
|
|
387
408
|
sidecar cannot cite a path the document does not have.
|
|
388
409
|
"""
|
|
389
|
-
return dict(_walk(data, "",
|
|
410
|
+
return dict(_walk(data, "", ()))
|
|
390
411
|
|
|
391
412
|
|
|
392
413
|
def group_translation_pointers(
|
|
@@ -377,6 +377,25 @@ def _initial_cross_verification_requirement(
|
|
|
377
377
|
), None)
|
|
378
378
|
|
|
379
379
|
|
|
380
|
+
def _assigned_models_by_role(payload: dict[str, Any]) -> dict[str, list[tuple[str, str]]]:
|
|
381
|
+
"""run-inputs 의 `workerAssignments` 를 역할별 (provider, model) 로.
|
|
382
|
+
|
|
383
|
+
지금 run-inputs 의 `workers` 는 provider 가 아니라 worker id(`claude-planner`)를
|
|
384
|
+
적는다. 그 값으로 `<provider>Model` 을 찾으면 아무 모델도 복원되지 않는다.
|
|
385
|
+
"""
|
|
386
|
+
rows = payload.get("workerAssignments")
|
|
387
|
+
assigned: dict[str, list[tuple[str, str]]] = {}
|
|
388
|
+
for row in rows if isinstance(rows, list) else ():
|
|
389
|
+
if not isinstance(row, dict):
|
|
390
|
+
continue
|
|
391
|
+
role, provider, model = row.get("role"), row.get("provider"), row.get("model")
|
|
392
|
+
if isinstance(role, str) and isinstance(provider, str) and provider:
|
|
393
|
+
assigned.setdefault(role, []).append(
|
|
394
|
+
(provider, model if isinstance(model, str) else "")
|
|
395
|
+
)
|
|
396
|
+
return assigned
|
|
397
|
+
|
|
398
|
+
|
|
380
399
|
def _role_selections_from_run_inputs(
|
|
381
400
|
state: WizardState,
|
|
382
401
|
payload: dict[str, Any],
|
|
@@ -385,7 +404,10 @@ def _role_selections_from_run_inputs(
|
|
|
385
404
|
profile = _load_role_profile_for_state(state)
|
|
386
405
|
pool = ModelPool.from_registry(default_provider_registry())
|
|
387
406
|
cross_requirement = _initial_cross_verification_requirement(profile)
|
|
407
|
+
assigned = _assigned_models_by_role(payload)
|
|
388
408
|
providers = _legacy_worker_providers(payload)
|
|
409
|
+
if cross_requirement is not None and cross_requirement.role in assigned:
|
|
410
|
+
providers = [provider for provider, _ in assigned[cross_requirement.role]]
|
|
389
411
|
if cross_requirement is not None:
|
|
390
412
|
selected_count = cross_requirement.recommended_count
|
|
391
413
|
if providers and cross_requirement.min_count < cross_requirement.max_count:
|
|
@@ -400,8 +422,13 @@ def _role_selections_from_run_inputs(
|
|
|
400
422
|
model.provider_id
|
|
401
423
|
for model in pool.default_candidates(cross_requirement.role)
|
|
402
424
|
]
|
|
425
|
+
assigned_models = [model for _, model in assigned.get(cross_requirement.role, ())]
|
|
403
426
|
for ordinal, provider in enumerate(providers[:selected_count], start=1):
|
|
404
|
-
raw_model =
|
|
427
|
+
raw_model = (
|
|
428
|
+
assigned_models[ordinal - 1]
|
|
429
|
+
if ordinal <= len(assigned_models)
|
|
430
|
+
else payload.get(f"{provider}Model", "")
|
|
431
|
+
)
|
|
405
432
|
model_ref = _legacy_model_ref(
|
|
406
433
|
pool,
|
|
407
434
|
provider,
|
|
@@ -433,6 +460,8 @@ def _role_selections_from_run_inputs(
|
|
|
433
460
|
if not requirement.dynamic and requirement.min_count > 0
|
|
434
461
|
}
|
|
435
462
|
for role, provider, raw_model in provider_rows:
|
|
463
|
+
if assigned.get(role):
|
|
464
|
+
provider, raw_model = assigned[role][0]
|
|
436
465
|
if role not in static_roles or not isinstance(provider, str) or not provider:
|
|
437
466
|
continue
|
|
438
467
|
model_ref = _legacy_model_ref(pool, provider, role, raw_model)
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Callable, Optional
|
|
7
8
|
|
|
8
9
|
from okstra_ctl.clarification_items import user_response_sidecars
|
|
10
|
+
from okstra_ctl.manager_paths import MANAGER_CONTEXT_DIRECTIVE_PREFIX
|
|
9
11
|
from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
|
|
10
12
|
from okstra_ctl.report_language import is_language_tag
|
|
11
13
|
from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
|
|
@@ -169,12 +171,29 @@ def _submit_optional_cached_pick(
|
|
|
169
171
|
)
|
|
170
172
|
|
|
171
173
|
|
|
174
|
+
def _directive_context_note(state: WizardState, directive: str) -> str:
|
|
175
|
+
"""manager directive 는 파일을 가리킬 뿐이라, 그 파일이 언제 쓰였는지 보여 준다.
|
|
176
|
+
|
|
177
|
+
직전 run 이후 정한 결정은 그 파일에 없다. 날짜가 없으면 사용자는 재사용할
|
|
178
|
+
directive 가 새 범위를 담는지 판단할 수 없었다(fontsninja dev-11127 re-plan).
|
|
179
|
+
"""
|
|
180
|
+
if not directive.startswith(MANAGER_CONTEXT_DIRECTIVE_PREFIX):
|
|
181
|
+
return ""
|
|
182
|
+
path = Path(directive[len(MANAGER_CONTEXT_DIRECTIVE_PREFIX):].strip()).expanduser()
|
|
183
|
+
labels = _p(state.workspace_root, "directive_pick")["labels"]
|
|
184
|
+
if not path.is_file():
|
|
185
|
+
return labels["context_missing"].format(path=path)
|
|
186
|
+
written = datetime.fromtimestamp(path.stat().st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
187
|
+
return labels["context_file"].format(path=path, date=written)
|
|
188
|
+
|
|
189
|
+
|
|
172
190
|
_DIRECTIVE_PICK_SPEC = _OptionalCachedPickSpec(
|
|
173
191
|
step=S_DIRECTIVE_PICK, prompt_key="directive_pick",
|
|
174
192
|
recommend_token=_REUSE_LAST_TOKEN, label_key="reuse_last", echo_suffix_key="reuse",
|
|
175
193
|
suggest=_suggest_last_directive, snippet_style="prefix",
|
|
176
194
|
cache_attr="last_directive_cached", target_attr="directive",
|
|
177
195
|
pending_attr="directive_pending_text",
|
|
196
|
+
recommend_note=_directive_context_note,
|
|
178
197
|
)
|
|
179
198
|
|
|
180
199
|
|
|
@@ -63,7 +63,13 @@ def _assignment_model(inputs: dict, worker_id: str, provider: str) -> str:
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def _build_reuse_previous(state: WizardState) -> Prompt:
|
|
66
|
-
|
|
66
|
+
directive = _latest_run_inputs(
|
|
67
|
+
state, phase_segment=slugify_task_segment(state.task_type)
|
|
68
|
+
).get("directive")
|
|
69
|
+
t = _p(
|
|
70
|
+
state.workspace_root, "reuse_previous",
|
|
71
|
+
directive=directive if isinstance(directive, str) and directive else "(없음)",
|
|
72
|
+
)
|
|
67
73
|
return Prompt(
|
|
68
74
|
step=S_REUSE_PREVIOUS, kind="pick",
|
|
69
75
|
label=t["label"],
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""`okstra worktree-lock` — 선언된 검증 명령을 worktree 단위 배타 락 안에서 실행한다."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from okstra_project.dirs import okstra_home
|
|
12
|
+
|
|
13
|
+
from .locks import worktree_exec_lock
|
|
14
|
+
|
|
15
|
+
_CLI_EPILOG = r"""Every verifier of a stage runs in the same worktree at the same time, and each
|
|
16
|
+
re-runs the plan's Tier 1 commands itself. A command that starts a server on a
|
|
17
|
+
fixed port (a dev server on 3000, a fixture API on 8899) then collides with the
|
|
18
|
+
same command from another verifier. This launcher runs the command unchanged,
|
|
19
|
+
one invocation at a time per worktree:
|
|
20
|
+
|
|
21
|
+
okstra worktree-lock --worktree <target.cwd> --command '<target.command>'
|
|
22
|
+
|
|
23
|
+
The command runs through $SHELL -c (or /bin/sh) with the worktree as its
|
|
24
|
+
working directory. Its stdout and stderr pass through untouched and its exit
|
|
25
|
+
code is this command's exit code; lock notices go to stderr only.
|
|
26
|
+
|
|
27
|
+
The lock file is <okstra home>/.locks/worktree-exec/<hash of the worktree
|
|
28
|
+
realpath>.lock, outside the worktree and the project's .okstra tree. When it
|
|
29
|
+
cannot be created or locked, the command still runs, unlocked, after a warning
|
|
30
|
+
on stderr.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _shell() -> str:
|
|
35
|
+
return os.environ.get("SHELL", "") or "/bin/sh"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _announce_wait(lockfile: Path) -> None:
|
|
39
|
+
sys.stderr.write(
|
|
40
|
+
f"okstra worktree-lock: another command holds {lockfile}; waiting\n"
|
|
41
|
+
)
|
|
42
|
+
sys.stderr.flush()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run(command: str, worktree: Path) -> int:
|
|
46
|
+
code = subprocess.run([_shell(), "-c", command], cwd=worktree, check=False).returncode
|
|
47
|
+
# 시그널로 죽은 자식은 음수를 준다. 셸이 보고하는 128+N 으로 맞춘다.
|
|
48
|
+
return 128 - code if code < 0 else code
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv: list[str] | None = None) -> int:
|
|
52
|
+
parser = argparse.ArgumentParser(
|
|
53
|
+
prog="okstra worktree-lock",
|
|
54
|
+
description="Run one declared shell command in a worktree under that worktree's exclusive lock.",
|
|
55
|
+
epilog=_CLI_EPILOG,
|
|
56
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--worktree", type=Path, required=True,
|
|
60
|
+
help="worktree the command runs in; also the lock key (resolved to its realpath)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--command", required=True,
|
|
64
|
+
help="the declared command, run verbatim through the shell",
|
|
65
|
+
)
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
worktree = args.worktree.resolve()
|
|
68
|
+
if not worktree.is_dir():
|
|
69
|
+
sys.stderr.write(f"okstra worktree-lock: not a directory: {worktree}\n")
|
|
70
|
+
return 2
|
|
71
|
+
with contextlib.ExitStack() as stack:
|
|
72
|
+
try:
|
|
73
|
+
stack.enter_context(
|
|
74
|
+
worktree_exec_lock(okstra_home(), worktree, on_wait=_announce_wait)
|
|
75
|
+
)
|
|
76
|
+
except OSError as exc:
|
|
77
|
+
# 잠그지 못해도 검증자의 재실행 자체를 막지 않는다. 동시 실행의 위험은
|
|
78
|
+
# 락 도입 전과 같고, 그 사실을 stderr 에 남긴다.
|
|
79
|
+
sys.stderr.write(
|
|
80
|
+
f"okstra worktree-lock: warning: cannot take the lock ({exc}); running unlocked\n"
|
|
81
|
+
)
|
|
82
|
+
sys.stderr.flush()
|
|
83
|
+
return _run(args.command, worktree)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
@@ -9949,6 +9949,10 @@
|
|
|
9949
9949
|
"planStep": {
|
|
9950
9950
|
"type": "string",
|
|
9951
9951
|
"minLength": 1
|
|
9952
|
+
},
|
|
9953
|
+
"summary": {
|
|
9954
|
+
"type": "string",
|
|
9955
|
+
"minLength": 1
|
|
9952
9956
|
}
|
|
9953
9957
|
}
|
|
9954
9958
|
},
|
|
@@ -10009,6 +10013,13 @@
|
|
|
10009
10013
|
},
|
|
10010
10014
|
"tddEvidence": {
|
|
10011
10015
|
"type": "string"
|
|
10016
|
+
},
|
|
10017
|
+
"checkId": {
|
|
10018
|
+
"type": "string",
|
|
10019
|
+
"pattern": "^VC-\\d{3,}$"
|
|
10020
|
+
},
|
|
10021
|
+
"advisory": {
|
|
10022
|
+
"type": "boolean"
|
|
10012
10023
|
}
|
|
10013
10024
|
}
|
|
10014
10025
|
},
|
|
@@ -12,7 +12,8 @@ body { margin: 0; background: Canvas; color: CanvasText; }
|
|
|
12
12
|
.report-meta dd { margin: 0; }
|
|
13
13
|
.lede { max-width: 70ch; font-size: clamp(1.05rem, 2vw, 1.35rem); line-height: 1.4; margin: 0 0 1rem; }
|
|
14
14
|
.eyebrow { text-transform: uppercase; letter-spacing: .08em; font-size: .78rem; color: GrayText; }
|
|
15
|
-
|
|
15
|
+
/* minmax(0, 1fr): auto 열은 가장 넓은 자식만큼 커져서 긴 토큰 하나가 모든 section 을 넓혔다. */
|
|
16
|
+
main { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1rem; padding-bottom: 3rem; }
|
|
16
17
|
section { padding: 1.4rem; border: 1px solid color-mix(in srgb, CanvasText 14%, transparent); border-radius: 16px; background: color-mix(in srgb, Canvas 94%, CanvasText 6%); }
|
|
17
18
|
h2 { margin-top: 0; font-size: 1.45rem; }
|
|
18
19
|
.report-section-toggle > summary { cursor: pointer; }
|
|
@@ -188,4 +189,5 @@ button[data-action="export-user-response"]:hover { background: color-mix(in srgb
|
|
|
188
189
|
/* A seven-column table needs 42em of floor, more than a phone can give. */
|
|
189
190
|
@media (max-width: 640px) { section { padding: 1rem; } .visualization { display: none; } th, td { min-width: 4.5em; } nav.report-index ol { columns: 1; } }
|
|
190
191
|
@media print { .skip-link, .back-to-top-wrap, script { display: none !important; } body { color: #000; background: #fff; } section { break-inside: avoid; border-color: #bbb; } .visualization-fallback { display: table; } }
|
|
191
|
-
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; padding: .1em .3em; border-radius: 4px; background: color-mix(in srgb, CanvasText 8%, Canvas); }
|
|
192
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; padding: .1em .3em; border-radius: 4px; background: color-mix(in srgb, CanvasText 8%, Canvas); overflow-wrap: anywhere; }
|
|
193
|
+
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
@@ -27,14 +27,12 @@
|
|
|
27
27
|
<thead><tr>
|
|
28
28
|
<th scope="col">{{ t('base.role') }}</th>
|
|
29
29
|
<th scope="col">{{ t('base.execution-label') }}</th>
|
|
30
|
-
<th scope="col">{{ t('base.status') }}</th>
|
|
31
30
|
</tr></thead>
|
|
32
31
|
<tbody>
|
|
33
32
|
{% for row in executionRoles %}
|
|
34
33
|
<tr>
|
|
35
|
-
<td>{{ row.role }}</td>
|
|
34
|
+
<td>{{ row.role | enum_label('executionRole') }}</td>
|
|
36
35
|
<td><code>{{ row.executionLabel }}</code></td>
|
|
37
|
-
<td>{{ row.status }}</td>
|
|
38
36
|
</tr>
|
|
39
37
|
{% endfor %}
|
|
40
38
|
</tbody>
|
|
@@ -45,14 +43,20 @@
|
|
|
45
43
|
<main id="main-content" data-report-role="human-main">
|
|
46
44
|
{% block human_content %}{% endblock %}
|
|
47
45
|
{% if businessFlow | default([]) %}
|
|
46
|
+
{% set flow_failed = businessFlow | selectattr('status', 'equalto', 'failed') | list %}
|
|
48
47
|
<section data-report-section="business-flow">
|
|
49
|
-
<h2>
|
|
50
|
-
{% for row in businessFlow %}
|
|
51
|
-
<p>{{ row.mode }}
|
|
52
|
-
<ul>{% for kind, path in row.reports.items() %}<li><a href="{{ path }}">{{ kind }}</a></li>{% endfor %}</ul>
|
|
53
|
-
{% if row.error %}<p>Explanation error: {{ row.error }}</p>{% endif %}
|
|
54
|
-
{% if row.id %}<p>Retry: <code>okstra explain-flow rerun --execution {{ row.id }}</code></p>{% endif %}
|
|
48
|
+
<h2>{{ t('base.business-flow') }}</h2>
|
|
49
|
+
{% for row in businessFlow if row.status != 'failed' %}
|
|
50
|
+
<p>{{ row.mode | enum_label('businessFlowMode') }} · {{ row.status | enum_label('businessFlowStatus') }}. {{ row.summary }}</p>
|
|
51
|
+
{% if row.reports %}<ul>{% for kind, path in row.reports.items() %}<li><a href="{{ path }}">{{ kind }}</a></li>{% endfor %}</ul>{% endif %}
|
|
55
52
|
{% endfor %}
|
|
53
|
+
{% if flow_failed %}
|
|
54
|
+
{# 실패 행의 오류·재실행 명령은 운영자용이라 접어 둔다. #}
|
|
55
|
+
<p>{{ t('base.business-flow-failed-count') | replace('{count}', flow_failed | length) }}</p>
|
|
56
|
+
<details><summary>{{ t('base.business-flow-failure-details') }}</summary>
|
|
57
|
+
<ul>{% for row in flow_failed %}<li>{{ row.mode | enum_label('businessFlowMode') }}{% if row.error %} · {{ t('base.business-flow-error') }} {{ row.error }}{% endif %}{% if row.id %} · {{ t('base.business-flow-rerun') }} <code>okstra explain-flow rerun --execution {{ row.id }}</code>{% endif %}</li>{% endfor %}</ul>
|
|
58
|
+
</details>
|
|
59
|
+
{% endif %}
|
|
56
60
|
</section>
|
|
57
61
|
{% endif %}
|
|
58
62
|
{% if endStates %}
|
|
@@ -146,6 +150,7 @@
|
|
|
146
150
|
<tr><th scope="row">{{ t('runUsage.row-workers') }}</th><td class="figure">{{ runUsage.worker.rawTokens }}</td><td class="figure">{{ runUsage.worker.cacheReadTokens }}</td><td class="figure">{{ runUsage.worker.billableTokens }}</td><td class="figure">{{ runUsage.worker.cost }}</td><td class="figure"></td></tr>
|
|
147
151
|
<tr class="grand-total"><th scope="row">{{ t('runUsage.row-total') }}</th><td class="figure">{{ runUsage.grand.rawTokens }}</td><td class="figure">{{ runUsage.grand.cacheReadTokens }}</td><td class="figure">{{ runUsage.grand.billableTokens }}</td><td class="figure">{{ runUsage.grand.cost }}</td><td class="figure"></td></tr>
|
|
148
152
|
{% if runUsage.cliCost %}<tr><th scope="row">{{ t('runUsage.row-cli') }}</th><td class="figure"></td><td class="figure"></td><td class="figure"></td><td class="figure">{{ runUsage.cliCost }}</td><td class="figure"></td></tr>{% endif %}
|
|
153
|
+
{% if runUsage.grandWithCli %}<tr class="grand-total"><th scope="row">{{ t('runUsage.row-total-with-cli') }}</th><td class="figure"></td><td class="figure"></td><td class="figure"></td><td class="figure">{{ runUsage.grandWithCli }}</td><td class="figure"></td></tr>{% endif %}
|
|
149
154
|
{% if runUsage.taskCumulative %}<tr class="task-cumulative"><th scope="row">{{ t('runUsage.row-task-cumulative') | replace('{runs}', runUsage.taskCumulative.runCount) }}</th><td class="figure">{{ runUsage.taskCumulative.rawTokens }}</td><td class="figure">{{ runUsage.taskCumulative.cacheReadTokens }}</td><td class="figure">{{ runUsage.taskCumulative.billableTokens }}</td><td class="figure">{{ runUsage.taskCumulative.cost }}</td><td class="figure"></td></tr>{% endif %}
|
|
150
155
|
</tfoot>
|
|
151
156
|
</table>
|
|
@@ -147,6 +147,74 @@
|
|
|
147
147
|
"deferred": "Deferred",
|
|
148
148
|
"not-applicable": "Not applicable",
|
|
149
149
|
"blocked": "Blocked"
|
|
150
|
+
},
|
|
151
|
+
"clarificationKind": {
|
|
152
|
+
"material": "Missing information",
|
|
153
|
+
"decision": "Your decision",
|
|
154
|
+
"data-point": "A fact to confirm"
|
|
155
|
+
},
|
|
156
|
+
"clarificationStatus": {
|
|
157
|
+
"open": "Open",
|
|
158
|
+
"answered": "Answered",
|
|
159
|
+
"resolved": "Resolved",
|
|
160
|
+
"obsolete": "No longer relevant"
|
|
161
|
+
},
|
|
162
|
+
"approvalDisposition": {
|
|
163
|
+
"select": "Choose this option",
|
|
164
|
+
"accept-risk": "Accept the risk",
|
|
165
|
+
"request-revision": "Request a revision",
|
|
166
|
+
"reject": "Reject"
|
|
167
|
+
},
|
|
168
|
+
"approvalClassification": {
|
|
169
|
+
"user-decision": "Needs your decision",
|
|
170
|
+
"noncritical-dissent": "Non-critical disagreement",
|
|
171
|
+
"correctness-critical": "Affects correctness"
|
|
172
|
+
},
|
|
173
|
+
"clarificationScope": {
|
|
174
|
+
"in-repo": "This repository only",
|
|
175
|
+
"cross-repo": "Other repositories too",
|
|
176
|
+
"new-schema": "New schema",
|
|
177
|
+
"deferrable": "Can be deferred"
|
|
178
|
+
},
|
|
179
|
+
"executionRole": {
|
|
180
|
+
"leader": "Lead",
|
|
181
|
+
"analyser": "Analyser",
|
|
182
|
+
"critic": "Critic",
|
|
183
|
+
"designer": "Designer",
|
|
184
|
+
"implementer": "Implementer",
|
|
185
|
+
"planner": "Planner",
|
|
186
|
+
"report-writer": "Report writer",
|
|
187
|
+
"translator": "Translator",
|
|
188
|
+
"verifier": "Verifier"
|
|
189
|
+
},
|
|
190
|
+
"requirementStatus": {
|
|
191
|
+
"covered": "Covered",
|
|
192
|
+
"gap": "Not covered",
|
|
193
|
+
"documented-deviation": "Approved deviation",
|
|
194
|
+
"blocked": "Blocked"
|
|
195
|
+
},
|
|
196
|
+
"rollbackCategory": {
|
|
197
|
+
"Pure code": "Code only",
|
|
198
|
+
"Feature-flag-gated": "Behind a feature flag",
|
|
199
|
+
"Schema/config/stateful": "Schema, config or stored state"
|
|
200
|
+
},
|
|
201
|
+
"rollbackResult": {
|
|
202
|
+
"ok": "Passed",
|
|
203
|
+
"not-applicable": "Not applicable",
|
|
204
|
+
"advisory — human-run": "Advisory: a person runs it"
|
|
205
|
+
},
|
|
206
|
+
"businessFlowMode": {
|
|
207
|
+
"pre": "Before the change",
|
|
208
|
+
"contribute": "This phase's contribution",
|
|
209
|
+
"post": "After the change",
|
|
210
|
+
"unknown": "Unknown phase"
|
|
211
|
+
},
|
|
212
|
+
"businessFlowStatus": {
|
|
213
|
+
"prepared": "Prepared",
|
|
214
|
+
"running": "Running",
|
|
215
|
+
"failed": "Failed",
|
|
216
|
+
"partial": "Partly explained",
|
|
217
|
+
"complete": "Explained"
|
|
150
218
|
}
|
|
151
219
|
},
|
|
152
220
|
"enumHint": {
|
|
@@ -194,7 +262,12 @@
|
|
|
194
262
|
"agent-activity": "Agent activity",
|
|
195
263
|
"agent-activity-intro": "What each agent was asked to do in this run and how it ended. Decision cards and prose cite these rows by id.",
|
|
196
264
|
"agent": "Agent",
|
|
197
|
-
"activity": "Activity"
|
|
265
|
+
"activity": "Activity",
|
|
266
|
+
"business-flow": "Business flow explanations",
|
|
267
|
+
"business-flow-failed-count": "{count} business flow explanations failed",
|
|
268
|
+
"business-flow-failure-details": "Failure details and rerun commands",
|
|
269
|
+
"business-flow-error": "Error:",
|
|
270
|
+
"business-flow-rerun": "Rerun:"
|
|
198
271
|
},
|
|
199
272
|
"runUsage": {
|
|
200
273
|
"heading": "What this run cost",
|
|
@@ -214,7 +287,8 @@
|
|
|
214
287
|
"row-total": "Total",
|
|
215
288
|
"row-cli": "CLI calls (billed separately)",
|
|
216
289
|
"cache-read-tokens": "Cache re-reads",
|
|
217
|
-
"row-task-cumulative": "This task so far ({runs} runs)"
|
|
290
|
+
"row-task-cumulative": "This task so far ({runs} runs)",
|
|
291
|
+
"row-total-with-cli": "Total including CLI calls"
|
|
218
292
|
},
|
|
219
293
|
"macros": {
|
|
220
294
|
"forms": {
|
|
@@ -585,6 +659,21 @@
|
|
|
585
659
|
"result": "Result",
|
|
586
660
|
"no-rollback-check-was-recorded": "No rollback check was recorded.",
|
|
587
661
|
"stage-carry-over": "Stage carry-over",
|
|
662
|
+
"carry-sidecar": "Sidecar",
|
|
663
|
+
"carry-commit-range": "Commit range",
|
|
664
|
+
"carry-steps": "Steps",
|
|
665
|
+
"carry-files-changed": "Files changed",
|
|
666
|
+
"carry-raw": "Sidecar JSON as written",
|
|
667
|
+
"file": "File",
|
|
668
|
+
"change": "Change",
|
|
669
|
+
"lines": "Lines",
|
|
670
|
+
"plan-step": "Plan step",
|
|
671
|
+
"action-created": "Created",
|
|
672
|
+
"action-modified": "Modified",
|
|
673
|
+
"action-deleted": "Deleted",
|
|
674
|
+
"what-it-does": "What it does",
|
|
675
|
+
"advisory": "advisory",
|
|
676
|
+
"entry-count": "{count} entries",
|
|
588
677
|
"manual-user-test": "Manual user test",
|
|
589
678
|
"environment": "Environment:",
|
|
590
679
|
"expected": "Expected:",
|