okstra 0.207.1 → 0.209.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 (133) hide show
  1. package/README.md +3 -2
  2. package/dist/cli-registry.mjs +6 -0
  3. package/dist/cli-registry.mjs.map +1 -1
  4. package/dist/commands/execute/render-bundle.mjs +1 -1
  5. package/dist/commands/lifecycle/doctor.mjs +1 -1
  6. package/dist/lib/skill-catalog.mjs +1 -0
  7. package/dist/lib/skill-catalog.mjs.map +1 -1
  8. package/docs/architecture/storage-model.md +14 -0
  9. package/docs/architecture.md +30 -9
  10. package/docs/cli.md +26 -22
  11. package/docs/contributor-change-matrix.md +1 -1
  12. package/docs/project-structure-overview.md +15 -8
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/operations/explain-flow.json +6 -0
  16. package/runtime/bin/lib/okstra/cli.sh +1 -5
  17. package/runtime/bin/lib/okstra/globals.sh +0 -2
  18. package/runtime/bin/lib/okstra/usage.sh +5 -3
  19. package/runtime/bin/okstra.sh +0 -2
  20. package/runtime/prompts/duties/business-flow-investigator.json +14 -0
  21. package/runtime/prompts/lead/context-loader.md +1 -1
  22. package/runtime/prompts/lead/convergence.md +22 -7
  23. package/runtime/prompts/lead/okstra-lead-contract.md +10 -6
  24. package/runtime/prompts/lead/report-writer.md +1 -1
  25. package/runtime/prompts/lead/team-contract.md +12 -17
  26. package/runtime/prompts/profiles/_common-contract.md +3 -3
  27. package/runtime/prompts/wizard/prompts.ko.json +0 -91
  28. package/runtime/python/okstra_ctl/adapters/providers/claude/adapter.py +6 -0
  29. package/runtime/python/okstra_ctl/agent/invocation.py +1 -1
  30. package/runtime/python/okstra_ctl/agent/prompt_cli/batch.py +1 -0
  31. package/runtime/python/okstra_ctl/agent/prompt_cli/cli.py +1 -0
  32. package/runtime/python/okstra_ctl/agent/prompt_cli/materialize.py +11 -125
  33. package/runtime/python/okstra_ctl/agent/standalone.py +183 -0
  34. package/runtime/python/okstra_ctl/analysis_packet.py +39 -8
  35. package/runtime/python/okstra_ctl/assignment_resolver.py +7 -1
  36. package/runtime/python/okstra_ctl/brief_frontmatter.py +10 -0
  37. package/runtime/python/okstra_ctl/business_flow/__init__.py +4 -0
  38. package/runtime/python/okstra_ctl/business_flow/cli.py +134 -0
  39. package/runtime/python/okstra_ctl/business_flow/contracts.py +268 -0
  40. package/runtime/python/okstra_ctl/business_flow/engine.py +518 -0
  41. package/runtime/python/okstra_ctl/business_flow/hooks.py +221 -0
  42. package/runtime/python/okstra_ctl/business_flow/invocation.py +170 -0
  43. package/runtime/python/okstra_ctl/business_flow/report.py +49 -0
  44. package/runtime/python/okstra_ctl/business_flow/source.py +206 -0
  45. package/runtime/python/okstra_ctl/business_flow/store.py +388 -0
  46. package/runtime/python/okstra_ctl/cmux.py +21 -16
  47. package/runtime/python/okstra_ctl/convergence.py +173 -2
  48. package/runtime/python/okstra_ctl/convergence_critic_verify_prompt.py +18 -0
  49. package/runtime/python/okstra_ctl/convergence_provenance.py +8 -0
  50. package/runtime/python/okstra_ctl/coverage_census.py +603 -0
  51. package/runtime/python/okstra_ctl/design_surfaces.py +4 -0
  52. package/runtime/python/okstra_ctl/direct_work.py +1 -1
  53. package/runtime/python/okstra_ctl/dispatch_core.py +7 -3
  54. package/runtime/python/okstra_ctl/doctor.py +12 -6
  55. package/runtime/python/okstra_ctl/domain/role.py +1 -0
  56. package/runtime/python/okstra_ctl/group_context.py +5 -4
  57. package/runtime/python/okstra_ctl/legacy_model_selection.py +7 -51
  58. package/runtime/python/okstra_ctl/manager_split.py +4 -1
  59. package/runtime/python/okstra_ctl/model_io/lines.py +1 -24
  60. package/runtime/python/okstra_ctl/model_io/renderers.py +54 -41
  61. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.json +1 -1
  62. package/runtime/python/okstra_ctl/phases/change_impact_analysis/profile.md +0 -8
  63. package/runtime/python/okstra_ctl/phases/error_analysis/profile.json +1 -1
  64. package/runtime/python/okstra_ctl/phases/error_analysis/profile.md +6 -8
  65. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.json +1 -1
  66. package/runtime/python/okstra_ctl/phases/feature_analysis/profile.md +0 -8
  67. package/runtime/python/okstra_ctl/phases/final_verification/profile.json +1 -1
  68. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +2 -8
  69. package/runtime/python/okstra_ctl/phases/implementation/boundary.json +1 -1
  70. package/runtime/python/okstra_ctl/phases/implementation/profile.json +1 -1
  71. package/runtime/python/okstra_ctl/phases/implementation/profile.md +0 -6
  72. package/runtime/python/okstra_ctl/phases/implementation/report_assets/implementation-input.template.md +1 -1
  73. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +4 -3
  74. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +1 -14
  75. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.json +1 -1
  76. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +5 -8
  77. package/runtime/python/okstra_ctl/phases/implementation_option_selection/spec.md +3 -3
  78. package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +15 -5
  79. package/runtime/python/okstra_ctl/phases/implementation_planning/authoring.py +9 -1
  80. package/runtime/python/okstra_ctl/phases/implementation_planning/boundary.json +1 -1
  81. package/runtime/python/okstra_ctl/phases/implementation_planning/instructions/plan-body-verification.md +4 -2
  82. package/runtime/python/okstra_ctl/phases/implementation_planning/plan_body.py +25 -9
  83. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.json +1 -1
  84. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +7 -10
  85. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.json +1 -1
  86. package/runtime/python/okstra_ctl/phases/improvement_discovery/profile.md +4 -11
  87. package/runtime/python/okstra_ctl/phases/project_analysis/profile.json +1 -1
  88. package/runtime/python/okstra_ctl/phases/project_analysis/profile.md +0 -8
  89. package/runtime/python/okstra_ctl/phases/release_handoff/profile.md +1 -1
  90. package/runtime/python/okstra_ctl/phases/release_handoff/spec.md +1 -1
  91. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.json +1 -1
  92. package/runtime/python/okstra_ctl/phases/requirements_discovery/profile.md +14 -8
  93. package/runtime/python/okstra_ctl/phases/requirements_discovery/spec.md +3 -3
  94. package/runtime/python/okstra_ctl/phases/technical_verification/profile.json +1 -1
  95. package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +0 -4
  96. package/runtime/python/okstra_ctl/plan_items.py +1 -1
  97. package/runtime/python/okstra_ctl/render.py +10 -43
  98. package/runtime/python/okstra_ctl/render_final_report.py +3 -0
  99. package/runtime/python/okstra_ctl/report_assembly.py +15 -1
  100. package/runtime/python/okstra_ctl/report_finalize.py +50 -0
  101. package/runtime/python/okstra_ctl/report_html/render.py +3 -0
  102. package/runtime/python/okstra_ctl/report_synthesis_packet.py +1 -2
  103. package/runtime/python/okstra_ctl/run.py +78 -409
  104. package/runtime/python/okstra_ctl/wizard/__init__.py +2 -24
  105. package/runtime/python/okstra_ctl/wizard/cli.py +3 -6
  106. package/runtime/python/okstra_ctl/wizard/confirmation.py +3 -35
  107. package/runtime/python/okstra_ctl/wizard/engine.py +2 -4
  108. package/runtime/python/okstra_ctl/wizard/ids.py +1 -88
  109. package/runtime/python/okstra_ctl/wizard/registry.py +36 -228
  110. package/runtime/python/okstra_ctl/wizard/render.py +2 -2
  111. package/runtime/python/okstra_ctl/wizard/roles.py +1 -3
  112. package/runtime/python/okstra_ctl/wizard/sources.py +9 -40
  113. package/runtime/python/okstra_ctl/wizard/state.py +36 -145
  114. package/runtime/python/okstra_ctl/wizard/statefile.py +27 -128
  115. package/runtime/python/okstra_ctl/wizard/steps_identity.py +22 -10
  116. package/runtime/python/okstra_ctl/wizard/steps_options.py +5 -4
  117. package/runtime/python/okstra_ctl/wizard/steps_roles.py +15 -565
  118. package/runtime/python/okstra_ctl/worker_prompt_policy.py +9 -2
  119. package/runtime/schemas/business-flow-v1.schema.json +847 -0
  120. package/runtime/schemas/convergence-groups-v2.0.schema.json +7 -0
  121. package/runtime/skills/okstra-explain-flow/SKILL.md +42 -0
  122. package/runtime/skills/okstra-inspect/facets/history.md +5 -5
  123. package/runtime/skills/okstra-run/SKILL.md +2 -2
  124. package/runtime/templates/reports/business-flow.template.md +106 -0
  125. package/runtime/templates/reports/html/base.template.html +14 -1
  126. package/runtime/templates/reports/html/business-flow.template.html +31 -0
  127. package/runtime/templates/reports/html/i18n/en.json +1 -0
  128. package/runtime/templates/reports/html/i18n/ko.json +1 -0
  129. package/runtime/templates/worker-prompt-preamble.md +11 -2
  130. package/runtime/validators/checks/validate-prompt-metadata-01.py +10 -10
  131. package/runtime/validators/validate-run.py +70 -21
  132. package/runtime/validators/validate_analysis_report.py +21 -21
  133. package/runtime/python/okstra_ctl/workers.py +0 -133
@@ -89,6 +89,13 @@
89
89
  "type": "array",
90
90
  "minItems": 1,
91
91
  "items": { "$ref": "#/$defs/SourceItem" }
92
+ },
93
+ "cellRefs": {
94
+ "description": "Coverage-census cells this finding answers (grouping Markdown `Cells:`). Optional: a finding outside the census has none.",
95
+ "type": "array",
96
+ "minItems": 1,
97
+ "uniqueItems": true,
98
+ "items": { "type": "string", "pattern": "^C-\\S+$" }
92
99
  }
93
100
  }
94
101
  },
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: okstra-explain-flow
3
+ description: Explain a product business flow across related projects, including inbound and outbound dependencies, business rules, states, errors, before/after changes, risks and side effects. Query or retry shared business explanations independently of okstra-run.
4
+ ---
5
+
6
+ # Explain a Business Flow
7
+
8
+ Use the user's business name or question as the investigation input. If the project is ambiguous, resolve the intended project before investigating. A task, brief, prior analysis or implementation is not required.
9
+
10
+ Run the shared runtime from the project:
11
+
12
+ ```sh
13
+ okstra explain-flow --question "<the user's business question>" --report-language <reader language> --host-runtime <current host runtime>
14
+ ```
15
+
16
+ Use `claude-code`, `codex`, or the configured external host identifier. The runtime owns operation/model resolution, auditable invocation preparation, provider execution, source snapshots, validated result publication, shared knowledge and reports. Do not dispatch an unaudited replacement investigator or rerun an entire task when only an explanation failed.
17
+
18
+ For relevant stored knowledge:
19
+
20
+ ```sh
21
+ okstra explain-flow knowledge --query "<business question>"
22
+ ```
23
+
24
+ For a failed or partial explanation:
25
+
26
+ ```sh
27
+ okstra explain-flow rerun --execution <execution-id>
28
+ ```
29
+
30
+ If the original task checkout has been removed, add `--source-root <available checkout>` to the retry. Preserve the original execution and its baseline.
31
+
32
+ For current status:
33
+
34
+ ```sh
35
+ okstra explain-flow status --execution <execution-id>
36
+ ```
37
+
38
+ Read the returned human report. Explain the business findings, verified scope and gaps in the user's language. Link the returned report paths; do not assemble a guessed path. Distinguish static source inspection from actual execution evidence, expected changes, historical source versions and unresolved conflicts. Never report missing access as no impact.
39
+
40
+ The investigation reads source, configuration, test bodies and explicitly supplied existing results. Its policy forbids executing tests, starting services, deploying, migrating or editing investigated projects. The report distinguishes that policy and the before/after source audit from an enforced write boundary. A report and a shared fact use the same accepted evidence. New runs retain prior reports and source baselines.
41
+
42
+ Enforcement: `business_flow.engine` validates the result schema, source evidence and source stability before contribution; `agent.invocation` verifies standalone completion; `business_flow.store` preserves claim provenance and source versions. These behaviors are covered by `tests/run/test_business_flow.py` and existing invocation/JSON-boundary contract tests.
@@ -59,10 +59,9 @@ Builds a fresh run — new run-seq, new manifest, new report — using parameter
59
59
  - `taskType` → `--task-type`
60
60
  - `taskBriefPath` → `--task-brief`
61
61
  3. Optional arguments (include only when present in source):
62
- - `Workers` → `--workers` (comma-separated provider ids; the projection already folds roster slot ids like `codex-verifier` down to their provider, so pass the line verbatim and never hand-assemble it from a roster)
63
- - `relatedTasks` → `--related-tasks`
64
- - model overrides → `--claude-model`, `--codex-model`, `--antigravity-model`
65
- - for `taskType: implementation`: `teamContract.executor.provider` → `--executor <claude|codex|antigravity>` when different from `claude`.
62
+ - each `Role count` line → one `--role-count <role>=<N>` (only roles whose count can change appear)
63
+ - each `Role model` line → one `--role-model <role>=<provider>/<model>`, in the printed order (pass the values verbatim; never rebuild them from a roster)
64
+ - `Related tasks` → `--related-tasks`
66
65
  4. **`taskType: implementation` only — resolve `--base-ref`:** do not inspect the worktree registry. Omit `--base-ref` to reuse an existing registration. If the launch reports that no worktree is registered and a base is required, ask the user before retrying.
67
66
  5. Display the assembled command:
68
67
  ```bash
@@ -72,7 +71,8 @@ Builds a fresh run — new run-seq, new manifest, new report — using parameter
72
71
  --task-id <task-id> \
73
72
  --task-type <task-type> \
74
73
  --task-brief <brief-path> \
75
- --workers <worker-list>
74
+ --role-count <role>=<N> \
75
+ --role-model <role>=<provider>/<model>
76
76
  ```
77
77
  6. Once the user confirms, execute it.
78
78
 
@@ -205,7 +205,7 @@ That is the entire interactive flow. The wizard handles:
205
205
  - base-ref pick + git rev-parse validation (skipped when reusing an active worktree),
206
206
  - `implementation`-only sub-flow: approved-plan path (frontmatter `approved: true` check) + stage pick (`auto` = the earliest incomplete stage whose dependencies are satisfied, or a specific stage number). Implementer slots use role-count / role-model like every other role (`executor` is only a compatibility alias for `implementer`). When an approved plan is selected and a `## PLAN DECISION` sidecar carrying `Status: approved`, exported from the report — matching the plan on source-report·seq — is detected in that run's sibling `user-responses/`, the approve-confirm step expands to 3 options (`yes_apply` recommended: approve + apply the option as exported / `yes` approve only / `no` abort) — `yes_apply` validates the option against the plan's `optionCandidates` before applying it via the existing approval·option path,
207
207
  - `release-handoff`-only sub-flow: after the approved plan auto-resolves, a `handoff_stage_pick` multi-select — choose the eligible stages to open a PR for, one PR per stage; the result goes out as render-args' `stages` key (csv; empty takes every eligible stage),
208
- - launch selection after identity/worktree steps: one screen per static role, in profile order. A role that can run several instances (`max > 1`) is a checkbox step `role-models:<role>` (`multi: true`) — the number of models checked is the number of instances, there is no separate count question; the label states the profile range and recommended count, every executable candidate is listed on that one screen (defaults first, the recommended set flagged), and when the list exceeds the host's native checkbox limit the runtime either splits it into several checkbox questions on one `pick_group` screen (interaction plan `native-group`; the question steps are `role-models:<role>#1`, `#2`, … and the answer is one JSON object keyed by them, each value a CSV) when the host's native question group holds every option, or returns the interaction plan `numbered-multi` — render the whole list, never a shortlist or pages. An optional role (`min = 0`, e.g. critic) carries a `추가 안 함` row. A fixed single role (`min = max = 1`, e.g. report-writer) is a single pick `role-model:<role>:1`, and a role capped at one model (`max = 1`, e.g. critic) is a single pick on its `role-models:<role>` step; both split the same way when they exceed the native option limit — the tabs are checkbox questions, the answer is the same keyed JSON object, and the wizard rejects a screen whose tabs together name more than one value. current-session lead is this session and is listed on the confirmation summary, not as a wizard step. The wizard does not fork on defaults-vs-customize, does not show a provider roster multi-pick, and does not offer a separate implementer-provider pick. Dynamic verifiers are not chosen at launch. `--workers` is compatibility-only, not a launch picker. Repeated `--role-count` / `--role-model` tokens on `renderArgv` are intentional,
208
+ - launch selection after identity/worktree steps: one screen per static role, in profile order. A role that can run several instances (`max > 1`) is a checkbox step `role-models:<role>` (`multi: true`) — the number of models checked is the number of instances, there is no separate count question; the label states the profile range and recommended count, every executable candidate is listed on that one screen (defaults first, the recommended set flagged), and when the list exceeds the host's native checkbox limit the runtime either splits it into several checkbox questions on one `pick_group` screen (interaction plan `native-group`; the question steps are `role-models:<role>#1`, `#2`, … and the answer is one JSON object keyed by them, each value a CSV) when the host's native question group holds every option, or returns the interaction plan `numbered-multi` — render the whole list, never a shortlist or pages. An optional role (`min = 0`, e.g. critic) carries a `추가 안 함` row. A fixed single role (`min = max = 1`, e.g. report-writer) is a single pick `role-model:<role>:1`, and a role capped at one model (`max = 1`, e.g. critic) is a single pick on its `role-models:<role>` step; both split the same way when they exceed the native option limit — the tabs are checkbox questions, the answer is the same keyed JSON object, and the wizard rejects a screen whose tabs together name more than one value. current-session lead is this session and is listed on the confirmation summary, not as a wizard step. The wizard does not fork on defaults-vs-customize, does not show a provider roster multi-pick, and does not offer a separate implementer-provider pick. Dynamic verifiers are not chosen at launch. Repeated `--role-count` / `--role-model` tokens on `renderArgv` are intentional,
209
209
  - **resume-clarification (in-session equivalent)** — there is no separate mode or flag matching the shell's `okstra.sh --resume-clarification`; two steps of the standard flow carry out its substance. (1) `reuse_previous` (yes/no to reuse the previous run's settings — in `requirements-discovery` / `error-analysis` / `implementation-planning` / `project-analysis` / `feature-analysis` / `change-impact-analysis`, only when prior run-inputs exist): YES prefills role-count·role-model·directive·related-tasks at once (and, for the analysis types, the target and evidence inputs). (2) `clarification_pick`: a `revision-requested` analysis report for the selected analysis type is recommended first; otherwise the **task-type's own** previous `final-report` is auto-recommended as the carry-in input (for `technical-verification`, the `implementation-option-selection` report), falling back to the newest by mtime across all phases when absent — except for `implementation-planning` and `technical-verification`, which get no cross-phase fallback. The approved plan is never recommended here. The same run's `user-responses/` sidecar (answers the user filled in) is attached alongside. The chosen path is passed to prepare as `--clarification-response` — the user makes the sidecar via the report's `Export user response`, places it in `runs/<task-type>/user-responses/`, and re-runs the same phase,
210
210
  - **re-verification scope (`reverify_scope_pick`, `implementation-planning` clarification re-runs only)** — asked right before `confirm` when the re-run is narrowable **or** an answered `C-NNN` traces to no stage. When every answered id traces to a stage: 3 options — `auto` (recommended — leave it to the lead's `okstra incremental-scope` decision) / `full` (re-verify every stage) / Enter directly (a stage-number CSV, validated against the prior report's Stage Map). When an id is unlinked, `auto` is omitted and the user names stages or picks `full`; that unlinked id does not freeze the run at full. The answer goes out as `--reverify-scope` and reaches the lead prompt as the `REVERIFY_SCOPE_MODE` / `REVERIFY_SCOPE_STAGES` tokens; it shapes that CLI's inputs rather than replacing the decision. The confirmation block's `reverify-scope` line names unlinked ids as needing stage numbers, not as a forced full re-run,
211
211
  - `release-handoff` PR template override + persist scope,
@@ -271,7 +271,7 @@ Before invoking it, follow the active host relay's execution-permission guidance
271
271
 
272
272
  Analysis sidetracks therefore forward wizard-owned tokens such as `--analysis-target "<value>"` and `--evidence-inputs "<value>"` when they are present. These are examples of the verbatim token rule, not a separate hard-coded argument list.
273
273
 
274
- Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderArgv` whose following value is the empty string MUST still be passed explicitly (e.g. `--workers ""`, `--directive ""`) — the wizard's intent is always "flag present with empty value", even where prepare's default happens to be the same empty value.
274
+ Step 3's empty-answer and escaping rules apply verbatim: every flag in `renderArgv` whose following value is the empty string MUST still be passed explicitly (e.g. `--related-tasks ""`, `--directive ""`) — the wizard's intent is always "flag present with empty value", even where prepare's default happens to be the same empty value.
275
275
 
276
276
  `renderArgv` already contains exactly one `--lead-runtime <host-runtime>` pair. Do not add or replace it. Do not enumerate a fixed provider list in this skill because the wizard and role model pool own the ordered tokens.
277
277
 
@@ -0,0 +1,106 @@
1
+ # {{ flow.title }}
2
+
3
+ {{ flow.summary }}
4
+
5
+ - Mode: `{{ execution.request.mode }}`
6
+ - Status: `{{ execution.status }}`
7
+ - Execution: `{{ execution.id }}`
8
+ {% if execution.request.baseline %}
9
+ - Before-state explanation: `{{ execution.request.baseline }}`
10
+ {% endif %}
11
+
12
+ ## Business terms
13
+ {% for row in flow.terms %}
14
+ - **{{ row.term }}**: {{ row.meaning }}
15
+ {% endfor %}
16
+
17
+ ## Business steps
18
+ {% for step in flow.steps %}
19
+ ### {{ loop.index }}. {{ step.title }}
20
+
21
+ {{ step.purpose }}
22
+
23
+ - Projects: {{ step.projectIds | join(', ') }}
24
+ {% for field in ['inputs', 'outputs', 'rules', 'states', 'failures', 'retries', 'recovery', 'risks', 'sideEffects'] %}
25
+
26
+ #### {{ field }}
27
+ {% for value in step[field] %}
28
+ - {{ value }}
29
+ {% endfor %}
30
+ {% endfor %}
31
+ {% if step.before %}
32
+
33
+ Before: {{ step.before }}
34
+ {% endif %}
35
+ {% if step.after %}
36
+
37
+ After: {{ step.after }}
38
+ {% endif %}
39
+
40
+ Claim keys: {{ step.factKeys | join(', ') }}
41
+ {% endfor %}
42
+
43
+ ## Related projects
44
+
45
+ | From | To | Relationship | Business data | Claim keys |
46
+ |---|---|---|---|---|
47
+ {% for row in flow.relationships %}
48
+ | {{ row.from | cell }} | {{ row.to | cell }} | {{ row.kind | cell }} | {{ row.data | cell }} | {{ row.factKeys | join(', ') | cell }} |
49
+ {% endfor %}
50
+
51
+ ## Business facts and evidence
52
+ {% for fact in flow.facts %}
53
+ ### {{ fact.key }}
54
+
55
+ {{ fact.value }}
56
+
57
+ Evidence level: `{{ fact.level }}`
58
+ {% for evidence in fact.evidence %}
59
+
60
+ - `{{ evidence.projectId }}:{{ evidence.path }}:{{ evidence.line }}-{{ evidence.endLine }}`
61
+
62
+ ```text
63
+ {{ evidence.excerpt }}
64
+ ```
65
+ {% endfor %}
66
+ {% for receipt in fact.get('executionEvidence', []) %}
67
+ - Existing execution record: `{{ receipt }}`
68
+ {% endfor %}
69
+ {% endfor %}
70
+
71
+ ## Source comparison
72
+ {% for row in execution.changes %}
73
+ - {{ row.projectId }}: {% if row.baselineAvailable %}baseline available{% else %}no comparison baseline{% endif %}
74
+ {% for path in row.paths %}
75
+ - `{{ path }}`
76
+ {% endfor %}
77
+ {% endfor %}
78
+
79
+ ## Shared knowledge versions and conflicts
80
+ {% for claim in claims %}
81
+ - `{{ claim.key }}`: {{ claim.value }} ({{ claim.level }}; {{ claim.status }})
82
+ - Claim: `{{ claim.id }}`; producers: {{ claim.producers }}
83
+ {% for projectId, state in claim.sourceStates.items() %}
84
+ - Source: `{{ projectId }}` at `{{ state.head }}`; fingerprint: `{{ state.digest }}`
85
+ {% endfor %}
86
+ {% endfor %}
87
+
88
+ ## Investigation limits
89
+
90
+ Policy: `{{ execution.readOnlyAudit.policy }}`; enforced boundary: `{{ execution.readOnlyAudit.boundary }}`; audit coverage: `{{ execution.readOnlyAudit.coverage }}`.
91
+ {% for surface in execution.readOnlyAudit.unobserved %}
92
+ - Unobserved: {{ surface }}
93
+ {% endfor %}
94
+ {% if execution.get('reconciliation') %}
95
+ - Reinvestigation: `{{ execution.reconciliation.executionId }}` ({{ execution.reconciliation.status }})
96
+ {% endif %}
97
+
98
+ ## Unverified segments
99
+ {% for gap in flow.gaps %}
100
+ - {{ gap }}
101
+ {% endfor %}
102
+
103
+ ## Unresolved business questions
104
+ {% for question in flow.questions %}
105
+ - {{ question }}
106
+ {% endfor %}
@@ -44,6 +44,17 @@
44
44
  </header>
45
45
  <main id="main-content" data-report-role="human-main">
46
46
  {% block human_content %}{% endblock %}
47
+ {% if businessFlow | default([]) %}
48
+ <section data-report-section="business-flow">
49
+ <h2>Business Flow Explanations</h2>
50
+ {% for row in businessFlow %}
51
+ <p>{{ row.mode }}: {{ row.status }}. {{ row.summary }}</p>
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 %}
55
+ {% endfor %}
56
+ </section>
57
+ {% endif %}
47
58
  {% if endStates %}
48
59
  <section data-report-section="brief-end-states">
49
60
  <h2>{{ t('base.brief-end-states') }}</h2>
@@ -54,9 +65,11 @@
54
65
  </table>
55
66
  </section>
56
67
  {% endif %}
57
- {% if crossVerification.get("consensus") or crossVerification.get("differences") %}
68
+ {% set no_cross_model_check = taskType != "release-handoff" and (crossVerification.get("roundHistory") or {}).get("round2SkippedReason") == "single-analyser-only" %}
69
+ {% if crossVerification.get("consensus") or crossVerification.get("differences") or no_cross_model_check %}
58
70
  <section data-report-section="cross-check">
59
71
  <h2>{{ t('base.cross-check') }}</h2>
72
+ {% if no_cross_model_check %}<p class="section-lede">{{ t('base.no-cross-model-check') }}</p>{% endif %}
60
73
  {% if crossVerification.get("consensus") %}
61
74
  <h3>{{ t('base.agreed-across-workers') }}</h3>
62
75
  <div class="summary-grid">{% for row in crossVerification.consensus %}<article class="summary-card" id="id-xv-{{ row.id }}"><p class="eyebrow">{{ row.id | inline_code }}</p><h3>{{ row.statement | inline_code }}</h3><p class="evidence-refs">{{ t('macros.layout.evidence') }} {{ row.evidence | inline_code }}</p></article>{% endfor %}</div>
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ execution.request.report_language }}">
3
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>{{ flow.title }}</title><style>{{ css | safe }}</style></head>
4
+ <body><main>
5
+ <h1>{{ flow.title }}</h1><p>{{ flow.summary }}</p>
6
+ <p>Mode: <code>{{ execution.request.mode }}</code> · Status: <code>{{ execution.status }}</code></p>
7
+ {% if execution.request.baseline %}<p>Before-state explanation: <code>{{ execution.request.baseline }}</code></p>{% endif %}
8
+ <h2>Business terms</h2><dl>{% for row in flow.terms %}<dt>{{ row.term }}</dt><dd>{{ row.meaning }}</dd>{% endfor %}</dl>
9
+ <h2>Business steps</h2>
10
+ {% for step in flow.steps %}
11
+ <section id="step-{{ loop.index }}"><h3>{{ loop.index }}. {{ step.title }}</h3><p>{{ step.purpose }}</p><p>Projects: {{ step.projectIds | join(', ') }}</p>
12
+ {% for field in ['inputs', 'outputs', 'rules', 'states', 'failures', 'retries', 'recovery', 'risks', 'sideEffects'] %}
13
+ <h4>{{ field }}</h4><ul>{% for value in step[field] %}<li>{{ value }}</li>{% endfor %}</ul>
14
+ {% endfor %}
15
+ {% if step.before %}<p>Before: {{ step.before }}</p>{% endif %}{% if step.after %}<p>After: {{ step.after }}</p>{% endif %}
16
+ <p>Claim keys: {{ step.factKeys | join(', ') }}</p></section>
17
+ {% endfor %}
18
+ <h2>Related projects</h2><table><thead><tr><th>From</th><th>To</th><th>Relationship</th><th>Business data</th><th>Claim keys</th></tr></thead><tbody>
19
+ {% for row in flow.relationships %}<tr><td>{{ row.from }}</td><td>{{ row.to }}</td><td>{{ row.kind }}</td><td>{{ row.data }}</td><td>{{ row.factKeys | join(', ') }}</td></tr>{% endfor %}
20
+ </tbody></table>
21
+ <h2>Business facts and evidence</h2>
22
+ {% for fact in flow.facts %}<section><h3>{{ fact.key }}</h3><p>{{ fact.value }}</p><p>Evidence level: <code>{{ fact.level }}</code></p>
23
+ {% for evidence in fact.evidence %}<p><code>{{ evidence.projectId }}:{{ evidence.path }}:{{ evidence.line }}-{{ evidence.endLine }}</code></p><pre><code>{{ evidence.excerpt }}</code></pre>{% endfor %}
24
+ {% for receipt in fact.get('executionEvidence', []) %}<p>Existing execution record: <code>{{ receipt }}</code></p>{% endfor %}</section>{% endfor %}
25
+ <h2>Source comparison</h2><ul>{% for row in execution.changes %}<li>{{ row.projectId }}: {% if row.baselineAvailable %}baseline available{% else %}no comparison baseline{% endif %}<ul>{% for path in row.paths %}<li><code>{{ path }}</code></li>{% endfor %}</ul></li>{% endfor %}</ul>
26
+ <h2>Unverified segments</h2><ul>{% for gap in flow.gaps %}<li>{{ gap }}</li>{% endfor %}</ul>
27
+ <h2>Shared knowledge versions and conflicts</h2><ul>{% for claim in claims %}<li>{{ claim.key }}: {{ claim.value }} ({{ claim.level }}; {{ claim.status }})<ul><li>Claim: <code>{{ claim.id }}</code>; producers: {{ claim.producers }}</li>{% for projectId, state in claim.sourceStates.items() %}<li>Source: {{ projectId }} at <code>{{ state.head }}</code>; fingerprint: <code>{{ state.digest }}</code></li>{% endfor %}</ul></li>{% endfor %}</ul>
28
+ <h2>Investigation limits</h2><p>Policy: {{ execution.readOnlyAudit.policy }}; enforced boundary: {{ execution.readOnlyAudit.boundary }}; audit coverage: {{ execution.readOnlyAudit.coverage }}.</p><ul>{% for surface in execution.readOnlyAudit.unobserved %}<li>Unobserved: {{ surface }}</li>{% endfor %}</ul>
29
+ {% if execution.get('reconciliation') %}<p>Reinvestigation: <code>{{ execution.reconciliation.executionId }}</code> ({{ execution.reconciliation.status }})</p>{% endif %}
30
+ <h2>Unresolved business questions</h2><ul>{% for question in flow.questions %}<li>{{ question }}</li>{% endfor %}</ul>
31
+ </main></body></html>
@@ -172,6 +172,7 @@
172
172
  "elapsed": "Elapsed",
173
173
  "evidence-ledger": "Evidence ledger",
174
174
  "cross-check": "Worker agreement and dissent",
175
+ "no-cross-model-check": "No cross-model check ran: fewer than two analysis workers were selected, so no other model verified these findings.",
175
176
  "agreed-across-workers": "Agreed across workers",
176
177
  "workers-disagreed": "Workers disagreed",
177
178
  "source": "Source",
@@ -172,6 +172,7 @@
172
172
  "elapsed": "소요 시간",
173
173
  "evidence-ledger": "근거 대장",
174
174
  "cross-check": "작업자 합의와 이견",
175
+ "no-cross-model-check": "교차 모델 검증을 하지 않았습니다. 분석 작업자를 두 명 미만으로 골라 다른 모델이 이 결과를 검증하지 않았습니다.",
175
176
  "agreed-across-workers": "작업자 간 합의",
176
177
  "workers-disagreed": "작업자 간 이견",
177
178
  "source": "출처",
@@ -40,9 +40,18 @@ Every analysis result starts with YAML frontmatter containing the task identity,
40
40
  3. Safe or Reasonable Areas
41
41
  4. Uncertain Points
42
42
  5. Recommended Next Actions
43
- 6. Specialization Lens (optional, additive only)
43
+ 6. Coverage Verdicts (when the packet carries `## Coverage Census`)
44
+ 7. Specialization Lens (optional, additive only)
44
45
 
45
- Every item has a worker-local ID and file:line evidence where code evidence exists. Sections 1–5 are the common core: feasibility, requirement interpretation, hidden assumptions, alternatives, and execution risk. Section 6 is the only legal home for specialization and is not consensus input.
46
+ Every item has a worker-local ID and file:line evidence where code evidence exists. Sections 1–5 are the common core: feasibility, requirement interpretation, hidden assumptions, alternatives, and execution risk. Section 7 is the only legal home for specialization and is not consensus input.
47
+
48
+ Section 6 judges every cell of the packet's `## Coverage Census`, one line per cell, using the cell id verbatim:
49
+
50
+ - `- <cell-id>: clean — <path:line evidence>`
51
+ - `- <cell-id>: finding <worker-local ID>` — the ID of an item in sections 1–5
52
+ - `- <cell-id>: n/a — <reason>`
53
+
54
+ A cell with no line, a `clean` without a `path:line` citation, an `n/a` without a reason, or a `finding` naming an ID your result does not contain counts as unjudged. The lead sends unjudged cells back to you once, in a `census-gapfill` dispatch; whatever stays unjudged after it is reported as a coverage warning. That dispatch narrows this contract: write only Findings (the new items its verdicts point to, or `- none`) and Coverage Verdicts for the cells it lists. When the packet has no `## Coverage Census`, omit section 6.
46
55
 
47
56
  ## Return message to the lead
48
57
 
@@ -22,30 +22,30 @@ def validate_prompt_contract(
22
22
  worker_ids: list[str],
23
23
  prompt_dir: str,
24
24
  prompt_map: object,
25
- required_worker_roles: object,
25
+ worker_role_rows: object,
26
26
  ) -> None:
27
27
  if not prompt_dir:
28
28
  errors.append(f"{prefix} is missing worker prompt directory metadata")
29
29
  if not isinstance(prompt_map, dict):
30
30
  errors.append(f"{prefix} worker prompt map is missing or invalid")
31
31
  prompt_map = {}
32
- if not isinstance(required_worker_roles, list):
33
- errors.append(f"{prefix} requiredWorkerRoles is missing or invalid")
34
- required_worker_roles = []
32
+ if not isinstance(worker_role_rows, list):
33
+ errors.append(f"{prefix} workerRoles is missing or invalid")
34
+ worker_role_rows = []
35
35
 
36
36
  expected_dir_prefix = prompt_dir.rstrip("/") + "/" if prompt_dir else ""
37
37
  for worker_id in worker_ids:
38
38
  if worker_id not in prompt_map:
39
39
  errors.append(f"{prefix} worker prompt map is missing selected worker: {worker_id}")
40
40
 
41
- for worker in required_worker_roles:
41
+ for worker in worker_role_rows:
42
42
  if not isinstance(worker, dict):
43
- errors.append(f"{prefix} requiredWorkerRoles contains a non-object entry")
43
+ errors.append(f"{prefix} workerRoles contains a non-object entry")
44
44
  continue
45
45
  worker_id = str(worker.get("workerId", "")).strip()
46
46
  prompt_relative = str(worker.get("promptPath", "")).strip()
47
47
  if not worker_id:
48
- errors.append(f"{prefix} requiredWorkerRoles contains an entry without workerId")
48
+ errors.append(f"{prefix} workerRoles contains an entry without workerId")
49
49
  continue
50
50
  if not prompt_relative:
51
51
  errors.append(
@@ -54,7 +54,7 @@ def validate_prompt_contract(
54
54
  continue
55
55
  if prompt_map.get(worker_id) != prompt_relative:
56
56
  errors.append(
57
- f"{prefix} worker prompt map does not match requiredWorkerRoles for {worker_id}"
57
+ f"{prefix} worker prompt map does not match workerRoles for {worker_id}"
58
58
  )
59
59
  if expected_dir_prefix and not prompt_relative.startswith(expected_dir_prefix):
60
60
  errors.append(
@@ -85,7 +85,7 @@ else:
85
85
  selected_workers,
86
86
  str(task_artifacts.get("workerPromptsDirectoryPath", "")).strip(),
87
87
  task_artifacts.get("workerPromptPathByWorkerId"),
88
- task_manifest.get("resultContract", {}).get("requiredWorkerRoles"),
88
+ task_manifest.get("resultContract", {}).get("workerRoles"),
89
89
  )
90
90
 
91
91
  if (
@@ -167,7 +167,7 @@ else:
167
167
  selected_workers,
168
168
  run_prompt_dir,
169
169
  run_manifest.get("workerPromptPathByWorkerId"),
170
- run_manifest.get("teamContract", {}).get("requiredWorkerRoles"),
170
+ run_manifest.get("teamContract", {}).get("workerRoles"),
171
171
  )
172
172
 
173
173
  team_state_relative_path = str(
@@ -55,6 +55,7 @@ from okstra_ctl.phases.implementation_planning.guidance import (
55
55
  _validate_rerun_guidance as _validate_rerun_guidance,
56
56
  _validate_approval_guidance as _validate_approval_guidance,
57
57
  )
58
+ from okstra_ctl.coverage_census import census_advisories, read_census_state
58
59
  from okstra_ctl.plan_approval import plan_is_approved as _report_already_approved
59
60
  from okstra_ctl.report_validation_identity import (
60
61
  _report_run_seq as _report_run_seq,
@@ -1248,18 +1249,18 @@ def extract_contract(
1248
1249
  if not isinstance(task_contract, dict):
1249
1250
  task_contract = {}
1250
1251
 
1251
- required_worker_roles = run_contract.get("requiredWorkerRoles")
1252
- if not isinstance(required_worker_roles, list):
1253
- required_worker_roles = task_contract.get("requiredWorkerRoles")
1254
- if not isinstance(required_worker_roles, list):
1255
- required_worker_roles = []
1256
- failures.append("requiredWorkerRoles is missing from run/task manifest")
1252
+ worker_role_rows = run_contract.get("workerRoles")
1253
+ if not isinstance(worker_role_rows, list):
1254
+ worker_role_rows = task_contract.get("workerRoles")
1255
+ if not isinstance(worker_role_rows, list):
1256
+ worker_role_rows = []
1257
+ failures.append("workerRoles is missing from run/task manifest")
1257
1258
 
1258
- optional_worker_roles = run_contract.get("optionalWorkerRoles")
1259
- if not isinstance(optional_worker_roles, list):
1260
- optional_worker_roles = task_contract.get("optionalWorkerRoles")
1261
- if not isinstance(optional_worker_roles, list):
1262
- optional_worker_roles = []
1259
+ critic_role_rows = run_contract.get("criticRoles")
1260
+ if not isinstance(critic_role_rows, list):
1261
+ critic_role_rows = task_contract.get("criticRoles")
1262
+ if not isinstance(critic_role_rows, list):
1263
+ critic_role_rows = []
1263
1264
 
1264
1265
  lead_role = (
1265
1266
  run_contract.get("leadRole")
@@ -1273,7 +1274,7 @@ def extract_contract(
1273
1274
  if not isinstance(required_agent_status_entries, list):
1274
1275
  required_agent_status_entries = [lead_role] + [
1275
1276
  item.get("role", "")
1276
- for item in required_worker_roles
1277
+ for item in worker_role_rows
1277
1278
  if isinstance(item, dict) and item.get("role")
1278
1279
  ]
1279
1280
 
@@ -1290,8 +1291,8 @@ def extract_contract(
1290
1291
  or task_contract.get("leadModelExecutionValue")
1291
1292
  or ""
1292
1293
  ),
1293
- "required_worker_roles": required_worker_roles,
1294
- "optional_worker_roles": optional_worker_roles,
1294
+ "worker_role_rows": worker_role_rows,
1295
+ "critic_role_rows": critic_role_rows,
1295
1296
  "required_agent_status_entries": [
1296
1297
  item
1297
1298
  for item in required_agent_status_entries
@@ -1460,7 +1461,7 @@ def _validate_initial_analysis_prompts(
1460
1461
  return
1461
1462
  run_manifest = data.get("runManifest") or {}
1462
1463
  team_contract = run_manifest.get("teamContract") or {}
1463
- selected_workers = team_contract.get("requiredWorkerRoles") or []
1464
+ selected_workers = team_contract.get("workerRoles") or []
1464
1465
  selected_worker_ids = [
1465
1466
  str(worker.get("workerId") or "").strip()
1466
1467
  for worker in selected_workers
@@ -1598,6 +1599,30 @@ def _validate_cmux_workers_were_dispatched_by_okstra(
1598
1599
  )
1599
1600
 
1600
1601
 
1602
+ _CROSS_VERIFICATION_ROLES = frozenset({"analyser", "designer", "planner", "verifier"})
1603
+
1604
+
1605
+ def single_verifier_advisories(run_manifest: dict) -> list[str]:
1606
+ """사용자가 한 명만 고른 교차 검증 역할은 다른 모델의 검증을 받지 않는다.
1607
+
1608
+ 사용자 선택이므로 run 을 막지 않고, 사람이 보도록 advisory 로만 남긴다.
1609
+ 재검증 칸(`sourceRoleExecutionRef` 가 있는 동적 역할)은 세지 않는다.
1610
+ """
1611
+ counts: dict[str, int] = {}
1612
+ for row in run_manifest.get("executionRoles") or []:
1613
+ if not isinstance(row, dict) or row.get("sourceRoleExecutionRef"):
1614
+ continue
1615
+ role = row.get("role")
1616
+ if role in _CROSS_VERIFICATION_ROLES:
1617
+ counts[role] = counts.get(role, 0) + 1
1618
+ return [
1619
+ f"no cross-model check: only one {role} was selected, so no other "
1620
+ "model verified its results"
1621
+ for role, count in sorted(counts.items())
1622
+ if count == 1
1623
+ ]
1624
+
1625
+
1601
1626
  def validate_team_state(
1602
1627
  team_state: dict,
1603
1628
  project_root: Path,
@@ -1702,24 +1727,24 @@ def validate_team_state(
1702
1727
 
1703
1728
  expected_workers: dict[str, dict] = {}
1704
1729
  # 선택은 배정 전의 선택이다. 배정된 비평 역할도 결과 또는 생략 사유가
1705
- # 있어야 하므로 필수 역할과 같은 상태 검사를 거친다.
1730
+ # 있어야 하므로 다른 선택 역할과 같은 상태 검사를 거친다.
1706
1731
  for worker in [
1707
- *contract["required_worker_roles"],
1708
- *contract.get("optional_worker_roles", []),
1732
+ *contract["worker_role_rows"],
1733
+ *contract.get("critic_role_rows", []),
1709
1734
  ]:
1710
1735
  if not isinstance(worker, dict):
1711
- failures.append("requiredWorkerRoles contains a non-object entry")
1736
+ failures.append("workerRoles contains a non-object entry")
1712
1737
  continue
1713
1738
  role = str(worker.get("role", "")).strip()
1714
1739
  if not role:
1715
- failures.append("requiredWorkerRoles contains an entry without role")
1740
+ failures.append("workerRoles contains an entry without role")
1716
1741
  continue
1717
1742
  expected_workers[role] = worker
1718
1743
 
1719
1744
  for role, expected in expected_workers.items():
1720
1745
  worker = by_role.get(role)
1721
1746
  if worker is None:
1722
- failures.append(f"missing required worker role: {role}")
1747
+ failures.append(f"missing selected worker role: {role}")
1723
1748
  continue
1724
1749
 
1725
1750
  expected_worker_id = expected.get("workerId")
@@ -3120,6 +3145,7 @@ def validate_final_report_data(
3120
3145
  print(f"validate-run: warning: {warning}", file=sys.stderr)
3121
3146
  # Phase-agnostic: the coverage critic runs in every finding-producing phase.
3122
3147
  _validate_unverified_critic_gaps_recorded(data, failures)
3148
+ _validate_census_coverage(data, manifest, project_root, failures)
3123
3149
  # Called here rather than from a task-type branch: four profiles raise
3124
3150
  # clarification rows, and the gate scopes itself by task type internally.
3125
3151
  _validate_clarification_options(data, failures)
@@ -4078,6 +4104,28 @@ def _normalize_report_contracts(raw_contracts: object) -> set[str]:
4078
4104
  }
4079
4105
 
4080
4106
 
4107
+ def _validate_census_coverage(
4108
+ data: Mapping[str, Any],
4109
+ manifest: Mapping[str, Any],
4110
+ project_root: Path | None,
4111
+ failures: list[str],
4112
+ ) -> None:
4113
+ """Coverage-census leftovers, reported and never blocking.
4114
+
4115
+ The user's rule for the census is that no cell state stops a run, so these
4116
+ strings carry no `blocking_checks` fragment and `partition` files them as
4117
+ `validate-run: advisory`.
4118
+ """
4119
+ if project_root is None:
4120
+ return
4121
+ state = read_census_state(project_root, manifest)
4122
+ if state is None:
4123
+ return
4124
+ failures.extend(
4125
+ census_advisories(state, data.get("missingInformation") or [])
4126
+ )
4127
+
4128
+
4081
4129
  CRITIC_UNVERIFIED_SOURCE = "critic-unverified"
4082
4130
 
4083
4131
 
@@ -5853,6 +5901,7 @@ def main() -> int:
5853
5901
  failures.extend(autofix_messages)
5854
5902
  _validate_execution_identity_v2(run_manifest, failures)
5855
5903
  contract = extract_contract(run_manifest, task_manifest, failures)
5904
+ advisories.extend(single_verifier_advisories(run_manifest))
5856
5905
  concurrent_run_authorized = bool(
5857
5906
  (run_manifest.get("concurrentRun") or {}).get("detected")
5858
5907
  )