okstra 0.201.2 → 0.202.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.
Files changed (49) hide show
  1. package/bin/okstra +1 -1
  2. package/dist/commands/lifecycle/setup.mjs +15 -0
  3. package/dist/commands/lifecycle/setup.mjs.map +1 -1
  4. package/dist/lib/citation-guidance.d.mts +21 -0
  5. package/dist/lib/citation-guidance.mjs +79 -0
  6. package/dist/lib/citation-guidance.mjs.map +1 -0
  7. package/docs/architecture/storage-model.md +4 -0
  8. package/docs/architecture.md +1 -1
  9. package/docs/cli.md +2 -2
  10. package/docs/for-ai/skills/okstra-manager.md +21 -4
  11. package/docs/for-ai/skills/okstra-setup.md +9 -0
  12. package/docs/project-structure-overview.md +11 -5
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/prompts/launch.template.md +1 -1
  16. package/runtime/prompts/lead/okstra-lead-contract.md +15 -3
  17. package/runtime/prompts/lead/team-contract.md +2 -0
  18. package/runtime/prompts/profiles/_implementation-executor.md +2 -2
  19. package/runtime/prompts/profiles/_implementation-verifier.md +7 -1
  20. package/runtime/prompts/profiles/implementation-planning.md +2 -0
  21. package/runtime/python/okstra_ctl/agent/evidence_recovery.py +78 -0
  22. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -6
  23. package/runtime/python/okstra_ctl/convergence_provenance.py +75 -18
  24. package/runtime/python/okstra_ctl/dispatch_core.py +91 -17
  25. package/runtime/python/okstra_ctl/execution_identity.py +9 -2
  26. package/runtime/python/okstra_ctl/execution_manifest.py +6 -2
  27. package/runtime/python/okstra_ctl/execution_mutation_audit.py +134 -65
  28. package/runtime/python/okstra_ctl/manager_cli.py +85 -12
  29. package/runtime/python/okstra_ctl/manager_launch.py +40 -18
  30. package/runtime/python/okstra_ctl/manager_paths.py +8 -0
  31. package/runtime/python/okstra_ctl/manager_split.py +474 -0
  32. package/runtime/python/okstra_ctl/manager_store.py +121 -18
  33. package/runtime/python/okstra_ctl/manager_sync.py +33 -15
  34. package/runtime/python/okstra_ctl/manager_view.py +216 -0
  35. package/runtime/python/okstra_ctl/plan_items_cli.py +6 -1
  36. package/runtime/python/okstra_ctl/qa_commands.py +15 -0
  37. package/runtime/python/okstra_ctl/report_finalize.py +13 -6
  38. package/runtime/python/okstra_ctl/report_html/view_models/final_verification.py +2 -21
  39. package/runtime/python/okstra_ctl/report_html/visualizations.py +0 -5
  40. package/runtime/python/okstra_ctl/verification_target.py +13 -2
  41. package/runtime/skills/okstra-manager/SKILL.md +53 -4
  42. package/runtime/skills/okstra-run/SKILL.md +3 -1
  43. package/runtime/skills/okstra-setup/SKILL.md +9 -0
  44. package/runtime/templates/manager/view.template.html +108 -0
  45. package/runtime/templates/reports/html/i18n/en.json +2 -0
  46. package/runtime/templates/reports/html/i18n/ko.json +2 -0
  47. package/runtime/templates/reports/html/tasks/final-verification.template.html +2 -2
  48. package/runtime/validators/validate-brief.py +7 -2
  49. package/runtime/validators/validate-run.py +7 -15
@@ -13,13 +13,14 @@ import hashlib
13
13
  import argparse
14
14
  import json
15
15
  import re
16
+ import shlex
16
17
  import subprocess
17
18
  from pathlib import Path
18
19
 
19
20
  from .execution_mutation_audit import source_content_snapshot
20
21
  from .json_boundary import load_owned_object
21
22
  from .path_hints import hydrate_active_run_context
22
- from .qa_commands import verification_command_defects
23
+ from .qa_commands import split_verification_command, verification_command_defects
23
24
 
24
25
  TARGET_FIELD_RES = {
25
26
  "scope": re.compile(r"\*\*Verification scope:\*\*\s*`([^`]*)`"),
@@ -95,6 +96,13 @@ def capture_verification_target(
95
96
  if not target or not Path(target).is_absolute():
96
97
  raise ValueError("active run context has no absolute verification worktree")
97
98
  worktree = Path(target).resolve()
99
+ declared_command = command
100
+ command, annotation = split_verification_command(command)
101
+ prefix = re.match(r"^\s*cd\s+('[^']*'|\"[^\"]*\"|[^\s;&|]+)\s*&&\s*", command)
102
+ if prefix is not None:
103
+ directory = shlex.split(prefix.group(1))[0]
104
+ if Path(directory).is_absolute() and Path(directory).resolve() == worktree:
105
+ command = command[prefix.end():]
98
106
  defects = verification_command_defects(command)
99
107
  if defects:
100
108
  raise ValueError("; ".join(defects))
@@ -105,11 +113,14 @@ def capture_verification_target(
105
113
  raise ValueError(f"verification target mismatch: expected HEAD {expected_head}, actual {head}, root {actual_root}")
106
114
  files = source_content_snapshot(worktree, frozenset({".okstra"}))
107
115
  digest = hashlib.sha256(json.dumps(files, sort_keys=True).encode()).hexdigest()
108
- return {
116
+ result = {
109
117
  "schemaVersion": "1.0", "runManifest": str(manifest_path),
110
118
  "taskKey": manifest["taskKey"], "cwd": str(worktree),
111
119
  "head": head, "sourceDigest": digest, "command": command,
112
120
  }
121
+ if declared_command != command:
122
+ result.update(declaredCommand=declared_command, commandAnnotation=annotation)
123
+ return result
113
124
 
114
125
 
115
126
  def main(argv: list[str] | None = None) -> int:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: okstra-manager
3
- description: Use when the user wants to manage okstra work across multiple project roots, register or discover projects under a manager, create or update a shared manager task, sync project child-task status into manager state, or launch a child task from manager context. Trigger words include "okstra manager", "okstra-manager", "multiple projects", "cross-project", "group projects together", "manager task".
3
+ description: Use when the user wants to manage okstra work across multiple project roots, register or discover projects under a manager, create or update a shared manager task, sync project child-task status into manager state, split a Linear project or issue into per-project scoped briefs, or launch a child task from manager context. Trigger words include "okstra manager", "okstra-manager", "multiple projects", "cross-project", "group projects together", "manager task", "split this Linear project", "brief per project".
4
4
  ---
5
5
 
6
6
  # OKSTRA Manager
@@ -28,19 +28,68 @@ okstra manager task sync --manager-id <manager-id> --task-group <task-group> --t
28
28
  okstra manager task assign --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --project-id <project-id> [--child-task-id <child-task-id>] [--role <role>] [--tag <tag>] [--assignment <text>]
29
29
  okstra manager task note --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --scope <shared|project> [--project-id <project-id>] --body <text>
30
30
  okstra manager task run --manager-id <manager-id> --project-id <project-id> --task-group <task-group> --task-id <task-id> [--child-task-id <child-task-id>]
31
+ okstra manager task split --manager-id <manager-id> --task-group <task-group> --task-id <task-id> --plan <split-plan.json> [--overwrite]
32
+ okstra manager list managers
33
+ okstra manager list projects --manager-id <manager-id>
34
+ okstra manager list tasks --manager-id <manager-id>
35
+ okstra manager view --manager-id <manager-id>
31
36
  ```
32
37
 
33
38
  - Public child task identity is `project-id:task-group:task-id`.
34
39
  - `okstra manager new task --task ...` should prefer the full child key form above. The CLI also accepts shorthand under the command's `--task-group`, but the full key is the public form to show users.
35
40
  - When a manager task id differs from the actual child task id for a specific project, pass `--child-task-id <child-task-id>` to `task assign` and `task run`.
36
41
 
42
+ ## Tracker Split
43
+
44
+ Use this when one Linear project, or one parent issue, covers work in several registered projects and each project needs its own brief. A Linear project can point at several projects, and one issue can too; every (issue, project) pair gets its own brief and child task.
45
+
46
+ 1. Run `okstra manager list projects --manager-id <manager-id>`. These are the only valid targets. Register a missing repo with `okstra manager new project ...` first.
47
+ 2. Fetch from the Linear MCP: the project (`get_project`) and its issues (`list_issues` filtered by that project), then each issue with `get_issue`, including sub-issues and blocking or related links. If the Linear tools are missing, load them once with `ToolSearch`; if they are still missing, ask the user to paste the bodies. Never invent ticket content.
48
+ 3. For each issue, propose target projects from evidence: labels, team, repository names or paths in the body. Confirm with `AskUserQuestion` (multi-select, recommended projects first, reason in each description).
49
+ 4. For each (issue, project) pair, draft the scope: the part of the issue this project implements. Confirm it with `AskUserQuestion` offering one or two drafts plus a custom-input option. A pair without scope is rejected by the CLI.
50
+ 5. Author each pair's brief fields under the okstra-brief-gen brief contract: bodies verbatim, `EB-NNN` / `PB-NNN` / `EO-NNN` items as `<id> <observable condition> — verify: <how>`, anything without an observation method under `externalGates`, and `openQuestions` rows prefixed `general:`, `terminology:`, `intent-check:`, `conversion-block:` or `adr-candidate:`.
51
+ 6. Write the plan JSON below with the Write tool to a scratch path outside the project roots, then run `okstra manager task split ... --plan <path>`.
52
+ - `split plan rejected` or `rendered briefs failed validate-brief`: fix the named fields and run it again. Nothing was written.
53
+ - `briefs already exist with different content`: show the listed paths and ask before adding `--overwrite`, which replaces hand edits. Adding issues to an earlier split changes every brief's Related Task Graph, so existing briefs are listed too.
54
+ 7. Each `Brief N task key` is `<project-id>:<task-group>:<child-task-id>`. Start one with `okstra manager task run ... --project-id <project-id> --child-task-id <child-task-id>` and follow the Child Launch Rule. The packet already carries `--task-brief`, and `--task-type` until the child task exists.
55
+
56
+ Plan JSON (`schemaVersion` 1). `source.kind` is `project` or `issue`; `recommendedPhase` is `requirements-discovery` (default), `error-analysis` or `improvement-discovery`; `relations[].relation` uses the brief Related Task Graph values; list fields may be omitted.
57
+
58
+ ```json
59
+ {
60
+ "schemaVersion": 1,
61
+ "source": {"kind": "project", "ref": "<Linear URL>", "title": "<title>", "fetchedVia": "<tool>", "fetchedAt": "YYYY-MM-DD HH:MM", "body": "<verbatim>"},
62
+ "issues": [
63
+ {
64
+ "ticketId": "LIN-12", "title": "<title>", "url": "<Linear URL>", "fetchedVia": "<tool>", "fetchedAt": "YYYY-MM-DD HH:MM", "body": "<verbatim>",
65
+ "relations": [{"relation": "blocked-by", "to": "LIN-11", "source": "<where the link came from>", "impact": "<what the next phase must keep>"}],
66
+ "assignments": [
67
+ {
68
+ "projectId": "<registered project id>", "scope": ["<work in this project>"], "outOfScope": [],
69
+ "recommendedPhase": "requirements-discovery",
70
+ "context": "<text>", "problem": "<text>", "desiredOutcome": "<text>",
71
+ "expectedBehavior": [], "preservedBehavior": [], "expectedOutcome": [],
72
+ "externalGates": [], "constraints": [], "openQuestions": []
73
+ }
74
+ ]
75
+ }
76
+ ]
77
+ }
78
+ ```
79
+
80
+ The CLI writes each brief to `<projectRoot>/.okstra/briefs/<task-group>/<ticketId>-<file-title>.md` with a `## Project Scope` section: this project's scope, `outOfScope`, and the scope of every other project the same issue went to. It validates every brief with the brief validator before writing any file, registers the child tasks, and keeps the plan at `split-plan.json` in the manager task directory.
81
+
82
+ ## Overview Page
83
+
84
+ When the user wants to see a manager at a glance, run `okstra manager task sync ...` for each task listed by `okstra manager list tasks ...` whose snapshot must be current, then run `okstra manager view --manager-id <manager-id>` and give the user the returned `View URL`. The page reads manager files only, so each task shows the state of its last sync and prints its own sync command.
85
+
37
86
  ## Child Launch Rule
38
87
 
39
88
  For launching child work:
40
89
 
41
90
  1. Run `okstra manager task sync ...` if the manager snapshot must be refreshed first.
42
91
  2. Run `okstra manager task run ...`.
43
- 3. Read the returned fixed fields `Backend`, `Worker dispatch backend`, `Project root`, `Context path`, and every numbered `Run arg N`.
44
- 4. Use that launch packet for the host-native child lead handoff.
92
+ 3. Read the returned fixed fields `Backend`, `Worker dispatch backend`, `Project root`, `Context path`, `Run command`, every numbered `Run arg N`, and `Shell command`.
93
+ 4. Give the user the `Shell command` line verbatim and tell them to run it in a new terminal. It starts the installed launcher in the child project: the launcher fills the task type and brief from the child task manifest or asks for them, prepares the run with the manager context directive, and starts the child lead as a separate host process (`Backend` `spawn-process`).
45
94
 
46
- The launch packet remains the source of truth. Do not rebuild child args by hand.
95
+ Do not run the `Shell command` through this session's Bash tool: it opens an interactive host session. Do not rebuild it from `Run arg N` or swap in `okstra run`, which drops the task inputs and the directive for a lead host.
@@ -244,6 +244,8 @@ Output: `{ok: true, outcome: {renderArgv: ["--lead-runtime", "...", ...], render
244
244
 
245
245
  `userAuthorization`, when present, preserves the confirmation prompt actually emitted by this wizard and the relayed `proceed` response. Its `--user-authorization-json` token carries that record into the run manifest; preserve it unchanged. Do not synthesize a receipt for older confirmed states. Include the translator's disclosed provider/model and report material when citing this record in host execution requests, including `report-finalize`. The record documents task scope; it does not grant host execution privileges.
246
246
 
247
+ Before retrying a worker in an existing run, inspect its last invocation attempt: await running work; append an attempt only after `failed-no-mutation` within the retry budget. Preserve every other terminal attempt. `okstra team dispatch` routes the legacy declaration-only implementer failure to a separate same-model evidence-recovery invocation; collect its result before independent verification. Other terminal failures need `okstra agent-prompt materialize` with a new `--invocation-id`, distinct `--prompt`/`--result` paths, and unchanged assignment/model for a bounded correction. Do not restart completed implementation or request renewed approval for unchanged scope. Enforcement: `dispatch_core._next_attempt` and `execution_manifest._validate_next_attempt`.
248
+
247
249
  Run every `outcome.persistActions[]` entry BEFORE `render-bundle`. The only supported action is:
248
250
 
249
251
  ```json
@@ -489,6 +491,6 @@ Follow the rendered launch prompt's "Progress, remaining work, and recommendatio
489
491
  ## Output Rules
490
492
 
491
493
  - Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
492
- - Name every file you show the user as a markdown link — `[<what it is>](<path>)`, with the path inside the parentheses. That is the only form the host renders as clickable; a path in backticks is text the user has to copy out. The `report-finalize` result's `reportPaths.markdown` carries the run's report, report record, and team state already in that form. Commands stay in backticks — a link is for a file, not for something to run.
494
+ - Name every file you show the user as a markdown link — `[<short label>](<absolute path>)`, with the absolute path inside the parentheses and a short label naming what the file is. A relative destination does not open from an external program. That is the only form the host renders as clickable; a path in backticks is text the user has to copy out. The `report-finalize` result's `reportPaths.markdown` carries the run's report, report record, and team state already in that form. Commands stay in backticks — a link is for a file, not for something to run.
493
495
  - Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
494
496
  - After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, finish after Step 6 and any same-run recovery, unless the user has already authorized continuing the task through further phases. In an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. When `report-finalize` returns `recovery.mode: same-run`, continue the authorized corrections in this run and execute `recovery.resumeCommand` before closeout; preserve approvals and model choices without reopening the wizard. The command and owner issues are supplied by `report_finalize._finalize_recovery`. When the lead (or this skill, after the lead returns) reports a successfully finalized run over, close with the user's next action — one command they can run now. A prohibition is not a next action. Take the pointer from the `report-finalize` result's top-level `nextRecommendedPhase` (`phase`, `status`, `rationale`; also on stderr as `next phase status:` / `next phase:` / `next phase rationale:`) — do not re-derive it from the report, and treat a `nextRecommendedPhaseError` as "pointer unreadable", said in one line before the `validate-run` branch. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead. After `implementation-planning`, open `blocks: approval` rows → `/okstra-user-response`. A recorded `accept-risk` / `select` / `answer` is not an open blocker. No open approval blocker → `/okstra-run` → `implementation` or `--approve` (do not start another planning run; do not say `/okstra-inspect`). For every other task type, quote the pointer's `rationale` in every branch — that sentence is the report's own reason and it is what the user asked to be analysed. Pointer `status: ready` → `/okstra-run` for that phase; `status: terminal` → say the task is finished, name any follow-up tasks this run registered, and do not say `/okstra-inspect`; `status: blocked` → issue the command the `rationale` calls for (`/okstra-user-response` for the `C-NNN` ids, `/okstra-run` for the phase it names); `validate-run` failed with `recovery.mode: phase-reentry` → name the cause and use `nextCommand` for the recorded earlier phase; otherwise `/okstra-inspect status`.
@@ -140,6 +140,15 @@ Then create the file — paste the literal `projectRoot` from Step 2 and the lit
140
140
  okstra setup --yes --project-root /abs/path/to/projectRoot --project-id my-project-id
141
141
  ```
142
142
 
143
+ `okstra setup` also refreshes the okstra-managed citation-guidance block in
144
+ `<PROJECT_ROOT>/CLAUDE.md` and `AGENTS.md` when those files already exist (it never
145
+ creates them). The block tells agents not to carry okstra-internal references — report
146
+ section numbers, `C-NNN` clarification ids, run/stage ids, `.okstra/...` paths — into
147
+ writing that is not an okstra report, where the reader cannot resolve them. The command
148
+ reports the files it touched in its JSON `citationGuidance` array; a failure there is a
149
+ `warning:` line, not a non-zero exit. Tell the user which guidance files were updated so
150
+ they can review the appended block.
151
+
143
152
  ## Step 3.5 (optional): project customisation
144
153
 
145
154
  The built-in defaults work for most projects — skip straight to Step 4
@@ -0,0 +1,108 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f7f7f5;
10
+ --panel: #ffffff;
11
+ --text: #1f2328;
12
+ --muted: #5f6670;
13
+ --line: #d9dbde;
14
+ --planned: #6e7781;
15
+ --missing: #9a6700;
16
+ --running: #0969da;
17
+ --done: #1a7f37;
18
+ --blocked: #cf222e;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root {
22
+ --bg: #111418;
23
+ --panel: #1a1f25;
24
+ --text: #e6e8eb;
25
+ --muted: #9aa3ad;
26
+ --line: #2f363e;
27
+ --planned: #8b949e;
28
+ --missing: #d29922;
29
+ --running: #58a6ff;
30
+ --done: #3fb950;
31
+ --blocked: #f85149;
32
+ }
33
+ }
34
+ * { box-sizing: border-box; }
35
+ body {
36
+ margin: 0;
37
+ background: var(--bg);
38
+ color: var(--text);
39
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
40
+ }
41
+ main { max-width: 1200px; margin: 0 auto; padding: 24px 16px 48px; }
42
+ header h1 { margin: 0 0 4px; font-size: 22px; }
43
+ .meta { color: var(--muted); font-size: 13px; }
44
+ section { margin-top: 28px; }
45
+ h2 { font-size: 17px; margin: 0 0 10px; }
46
+ h3 { font-size: 15px; margin: 0; }
47
+ .panel {
48
+ background: var(--panel);
49
+ border: 1px solid var(--line);
50
+ border-radius: 8px;
51
+ padding: 16px;
52
+ margin-bottom: 16px;
53
+ }
54
+ .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 10px; }
55
+ .tile { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
56
+ .tile .count { font-size: 24px; font-weight: 600; }
57
+ .tile .label { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
58
+ .scroll { overflow-x: auto; }
59
+ table { width: 100%; border-collapse: collapse; }
60
+ th, td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
61
+ th { color: var(--muted); font-weight: 600; font-size: 12px; white-space: nowrap; }
62
+ .wrap { display: inline-block; min-width: 220px; }
63
+ code.id { white-space: nowrap; word-break: normal; }
64
+ code { font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; word-break: break-all; }
65
+ .chip {
66
+ display: inline-block;
67
+ padding: 1px 8px;
68
+ border-radius: 999px;
69
+ border: 1px solid currentColor;
70
+ font-size: 12px;
71
+ white-space: nowrap;
72
+ }
73
+ .chip.planned { color: var(--planned); }
74
+ .chip.missing { color: var(--missing); }
75
+ .chip.running { color: var(--running); }
76
+ .chip.done { color: var(--done); }
77
+ .chip.blocked { color: var(--blocked); }
78
+ .task-head { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: baseline; margin-bottom: 8px; }
79
+ .counts { display: flex; flex-wrap: wrap; gap: 6px; }
80
+ .hint { color: var(--muted); font-size: 12px; margin: 6px 0 12px; }
81
+ details { margin-top: 10px; }
82
+ summary { cursor: pointer; color: var(--muted); }
83
+ ul.plain { margin: 6px 0 0; padding-left: 18px; }
84
+ .empty { color: var(--muted); }
85
+ a { color: var(--running); }
86
+ </style>
87
+ </head>
88
+ <body>
89
+ <main>
90
+ <header>
91
+ <h1>{{TITLE}}</h1>
92
+ <div class="meta">{{META}}</div>
93
+ </header>
94
+ <section>
95
+ <h2>Summary</h2>
96
+ {{SUMMARY}}
97
+ </section>
98
+ <section>
99
+ <h2>Projects</h2>
100
+ {{PROJECTS}}
101
+ </section>
102
+ <section>
103
+ <h2>Manager tasks</h2>
104
+ {{TASKS}}
105
+ </section>
106
+ </main>
107
+ </body>
108
+ </html>
@@ -371,6 +371,8 @@
371
371
  "final-verification": {
372
372
  "verification-verdict": "Verification verdict",
373
373
  "result-per-requirement": "Result per requirement",
374
+ "requirement": "Requirement",
375
+ "evidence": "Evidence",
374
376
  "what-the-change-added": "What the change added",
375
377
  "traced-to": "Serves",
376
378
  "callers": "Callers",
@@ -371,6 +371,8 @@
371
371
  "final-verification": {
372
372
  "verification-verdict": "검증 판정",
373
373
  "result-per-requirement": "요구사항별 결과",
374
+ "requirement": "요구사항",
375
+ "evidence": "근거",
374
376
  "what-the-change-added": "이 변경이 추가한 것",
375
377
  "traced-to": "봉사하는 요구사항",
376
378
  "callers": "호출자",
@@ -1,6 +1,5 @@
1
1
  {% extends "html/base.template.html" %}
2
2
  {% from "html/macros/layout.html" import narrative as render_narrative, row_key %}
3
- {% from "html/macros/visualizations.html" import figure %}
4
3
 
5
4
  {% block human_content %}
6
5
  <section data-report-section="verification-verdict">
@@ -22,7 +21,8 @@
22
21
  <section data-report-section="requirement-results" data-report-field="finalVerification.validationEvidence">
23
22
  <h2>{{ t('tasks.final-verification.result-per-requirement') }}</h2>
24
23
  {{ render_narrative(narrative.coverageExplanation, "finalVerification.userNarrative.coverageExplanation") }}
25
- {{ figure(coverageFigure, anchor_nodes=true) }}
24
+ <div class="table-scroll"><table><thead><tr><th>{{ t('macros.layout.id') }}</th><th>{{ t('tasks.final-verification.requirement') }}</th><th>{{ t('macros.layout.status') }}</th><th>{{ t('tasks.final-verification.evidence') }}</th></tr></thead>
25
+ <tbody>{% for row in final.validationEvidence %}<tr id="id-{{ row.id }}"><td>{{ row.id | inline_code }}</td><td>{{ row.requirement | inline_code }}</td><td><span class="status status-{{ row.status }}">{{ row.status }}</span></td><td>{{ row.artifact | inline_code }}</td></tr>{% endfor %}</tbody></table></div>
26
26
  </section>
27
27
 
28
28
  <section data-report-section="added-surface" data-report-field="finalVerification.addedSurfaceAudit">
@@ -104,6 +104,10 @@ AUGMENTATION_LABELS = {
104
104
 
105
105
  REPORTER_CONFIRMATION_VALUES = {"complete", "partial", "pending", "skipped"}
106
106
 
107
+ # okstra-manager `task split` renders the same brief contract for each project
108
+ # it splits a tracker issue into.
109
+ GENERATORS = {"okstra-brief-gen", "okstra-manager"}
110
+
107
111
  SCOPE_VALUES = {"reporter-input", "codebase"}
108
112
 
109
113
  TASK_GRAPH_HEADER = ["From", "Relation", "To", "Direction", "Source", "Impact"]
@@ -693,9 +697,10 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
693
697
  if fm.get("type") != "brief":
694
698
  errors.append(f"frontmatter type must be 'brief', got {fm.get('type')!r}")
695
699
 
696
- if fm.get("generator") != "okstra-brief-gen":
700
+ if fm.get("generator") not in GENERATORS:
697
701
  errors.append(
698
- f"frontmatter generator must be 'okstra-brief-gen', got {fm.get('generator')!r}"
702
+ f"frontmatter generator must be one of {sorted(GENERATORS)}, "
703
+ f"got {fm.get('generator')!r}"
699
704
  )
700
705
 
701
706
  if fm.get("reporter-confirmations") not in REPORTER_CONFIRMATION_VALUES:
@@ -5443,19 +5443,8 @@ def _validate_plan_body_state_rounds(
5443
5443
  )
5444
5444
 
5445
5445
 
5446
- def _validate_out_of_plan_edits_are_real(data: dict, failures: list[str]) -> None:
5447
- """선언한 out-of-plan 편집은 실제로 바뀐 파일이어야 한다.
5448
-
5449
- 입력 템플릿의 §"Forbidden In This Run" 은 계획 목록 밖에서 건드린 파일을 전부
5450
- `Out-of-plan edits` 에 적으라고 요구한다. 그 목록이 실제 diff 와 무관하면 기록은
5451
- 검토자에게 성실해 보이는 문자열일 뿐이다 — 실제로 바꾼 적 없는 파일이 적혀
5452
- 있으면 나머지 행도 믿을 근거가 없다.
5453
-
5454
- 계획 목록 자체와의 대조(계획 밖 파일이 여기 빠졌는지)는 하지 않는다. 그러려면
5455
- `approvedPlanReference.planFile` 이 가리키는 다른 리포트를 열어 stage 범위까지
5456
- 풀어야 하고, 그 경로 해소를 검증할 실물 run 이 저장소에 없다. 여기서는 리포트
5457
- 안에서 닫히는 방향만 본다.
5458
- """
5446
+ def _warn_out_of_plan_edits_not_in_diff(data: dict, warnings: list[str]) -> None:
5447
+ """소스 차이 목록은 별도 QA 산출물의 변경 여부를 증명하지 못한다."""
5459
5448
  implementation = data.get("implementation")
5460
5449
  if not isinstance(implementation, dict):
5461
5450
  return
@@ -5470,7 +5459,7 @@ def _validate_out_of_plan_edits_are_real(data: dict, failures: list[str]) -> Non
5470
5459
  continue
5471
5460
  target = row.get("file")
5472
5461
  if isinstance(target, str) and target and target not in changed:
5473
- failures.append(
5462
+ warnings.append(
5474
5463
  f"out-of-plan-edit: {row.get('id') or 'OOP-???'} 가 `{target}` 을 "
5475
5464
  "계획 밖 편집으로 신고했지만 diffSummary 에 그 파일이 없다"
5476
5465
  )
@@ -9755,7 +9744,10 @@ def main() -> int:
9755
9744
  _validate_verifier_command_log_is_read_only(validation_data, failures)
9756
9745
  _validate_verifier_reran_independently(validation_data, failures)
9757
9746
  _validate_verifier_discrepancy_is_not_passed(validation_data, failures)
9758
- _validate_out_of_plan_edits_are_real(validation_data, failures)
9747
+ declaration_warnings: list[str] = []
9748
+ _warn_out_of_plan_edits_not_in_diff(validation_data, declaration_warnings)
9749
+ for warning in declaration_warnings:
9750
+ print(f"validate-run: warning: {warning}", file=sys.stderr)
9759
9751
  if task_type == "implementation-planning":
9760
9752
  _validate_plan_body_state_file(
9761
9753
  validation_data,