okstra 0.225.1 → 0.226.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 (67) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/commands/execute/render-bundle.mjs +5 -0
  4. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  5. package/docs/cli.md +17 -1
  6. package/docs/project-structure-overview.md +2 -0
  7. package/package.json +1 -1
  8. package/runtime/BUILD.json +2 -2
  9. package/runtime/bin/lib/okstra/cli.sh +5 -1
  10. package/runtime/bin/lib/okstra/globals.sh +1 -0
  11. package/runtime/bin/lib/okstra/usage.sh +3 -0
  12. package/runtime/bin/okstra.sh +2 -0
  13. package/runtime/prompts/duties/technical-verification-worker.json +2 -1
  14. package/runtime/prompts/lead/convergence.md +4 -1
  15. package/runtime/prompts/lead/phase-routing.md +7 -3
  16. package/runtime/prompts/lead/team-contract.md +1 -1
  17. package/runtime/prompts/wizard/prompts.ko.json +15 -0
  18. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +5 -0
  19. package/runtime/python/okstra_ctl/analysis_packet.py +108 -2
  20. package/runtime/python/okstra_ctl/approved_plan_stage_extract.py +206 -0
  21. package/runtime/python/okstra_ctl/brief_sources.py +200 -0
  22. package/runtime/python/okstra_ctl/business_flow/store.py +15 -2
  23. package/runtime/python/okstra_ctl/cmux.py +18 -3
  24. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -1
  25. package/runtime/python/okstra_ctl/design_surfaces.py +19 -7
  26. package/runtime/python/okstra_ctl/execution_mutation_audit.py +20 -18
  27. package/runtime/python/okstra_ctl/implementation_direction.py +10 -0
  28. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +41 -0
  29. package/runtime/python/okstra_ctl/manager_split.py +185 -27
  30. package/runtime/python/okstra_ctl/next_phase.py +3 -1
  31. package/runtime/python/okstra_ctl/phases/final_verification/prior_blockers.py +128 -0
  32. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +8 -5
  33. package/runtime/python/okstra_ctl/phases/final_verification/report_assets/final-verification.template.html +1 -1
  34. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +84 -0
  35. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -1
  36. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +13 -5
  37. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +1 -1
  38. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +67 -0
  39. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +4 -2
  40. package/runtime/python/okstra_ctl/phases/implementation_option_selection/report_assets/implementation-option-selection.template.html +2 -1
  41. package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +127 -18
  42. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
  43. package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +2 -2
  44. package/runtime/python/okstra_ctl/report_assembly.py +23 -2
  45. package/runtime/python/okstra_ctl/report_views.py +2 -1
  46. package/runtime/python/okstra_ctl/run.py +46 -1
  47. package/runtime/python/okstra_ctl/run_context.py +13 -8
  48. package/runtime/python/okstra_ctl/wizard/confirmation.py +6 -0
  49. package/runtime/python/okstra_ctl/wizard/ids.py +6 -0
  50. package/runtime/python/okstra_ctl/wizard/registry.py +21 -0
  51. package/runtime/python/okstra_ctl/wizard/render.py +4 -0
  52. package/runtime/python/okstra_ctl/wizard/sources.py +33 -11
  53. package/runtime/python/okstra_ctl/wizard/state.py +4 -0
  54. package/runtime/python/okstra_ctl/wizard/steps_options.py +74 -9
  55. package/runtime/python/okstra_ctl/worker_prompt_body.py +21 -1
  56. package/runtime/python/okstra_ctl/worker_prompt_headers.py +11 -4
  57. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
  58. package/runtime/schemas/final-report-v2.0.schema.json +74 -0
  59. package/runtime/schemas/final-report-v3.0.schema.json +99 -2
  60. package/runtime/skills/okstra-brief-gen/SKILL.md +26 -0
  61. package/runtime/skills/okstra-manager/SKILL.md +15 -2
  62. package/runtime/templates/reports/brief.template.md +10 -1
  63. package/runtime/templates/reports/html/i18n/en.json +8 -0
  64. package/runtime/templates/reports/html/i18n/ko.json +8 -0
  65. package/runtime/templates/worker-prompt-preamble.md +1 -1
  66. package/runtime/validators/validate-brief.py +39 -1
  67. package/runtime/validators/validate-run.py +5 -0
@@ -27,6 +27,8 @@ class WorkerPromptHeaderError(Exception):
27
27
  # skill catalogs, and similar non-okstra artifacts. Restating the scope inline
28
28
  # puts the rule in front of the worker before its first tool call, which is the
29
29
  # only point at which it can still win over those host instructions.
30
+ # 패킷의 페이지 읽기 규칙도 같은 이유로 여기 있다 — 패킷 안 목차는 파일을 연 뒤에야
31
+ # 보이는데, 통째 `cat` 은 그 전에 잘린다(실측 609KB 패킷, codex).
30
32
  READ_SCOPE_HEADER = (
31
33
  "**Read scope:** Read only the paths this prompt enumerates "
32
34
  "(`[Required reading]`, `## Inputs`, verification-target paths) plus "
@@ -39,7 +41,11 @@ READ_SCOPE_HEADER = (
39
41
  "NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, "
40
42
  "`SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an "
41
43
  "un-enumerated file seems essential, record it under *Missing Information "
42
- "or Assumptions* instead of reading it."
44
+ "or Assumptions* instead of reading it. Read the primary analysis packet "
45
+ "in pages of at most 200 lines (`sed -n '1,200p'`, then `'201,400p'`, …), "
46
+ "never with one whole-file `cat` or view: worker CLIs cut long output "
47
+ "without saying so, and the packet's `## Section Index` gives each "
48
+ "section's line range."
43
49
  )
44
50
  EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
45
51
 
@@ -63,12 +69,13 @@ EVIDENCE_CITATION_HEADER = (
63
69
  # 구현 워커는 명령 행을 쓰지 못한다.
64
70
  EVIDENCE_COMMANDS_HEADER = (
65
71
  "**Evidence commands:** Append one `- Evidence command: "
66
- "{\"command\":\"<exact command>\",\"cwd\":\"<project-root>\","
72
+ "{\"command\":\"<exact command>\",\"cwd\":\"<directory the command ran in>\","
67
73
  "\"exitCode\":0,\"outputSummary\":\"<one-line result>\"}` row to the audit "
68
74
  "sidecar for each command that produced or verified a conclusion. Do not "
69
75
  "record exploratory `rg`, `ls`, or file-opening commands. Write the command "
70
- "as you ran it. A value that must not be written down is yours to omit or "
71
- "pass as a `$VAR` reference."
76
+ "as you ran it. When it starts with `cd <dir> &&`, `cwd` is that `<dir>`, "
77
+ "not the shell's starting directory. A value that must not be written down "
78
+ "is yours to omit or pass as a `$VAR` reference."
72
79
  )
73
80
 
74
81
  # `agy`'s write tool validates the target against the Gemini artifact store
@@ -34,6 +34,9 @@ ERRORS_PATH_HEADERS = (
34
34
  # itself: the lead's launch prompt carries them, and no worker reads that.
35
35
  APPROVED_PLAN_HEADER = "**Approved plan:**"
36
36
  IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
37
+ # Executor-only and optional: a prompt published before it, or a run whose plan
38
+ # is a Markdown report, has no extract and the executor reads the plan itself.
39
+ APPROVED_PLAN_STAGE_EXTRACT_HEADER = "**Approved plan stage extract:**"
37
40
  # A stage's verifiers share one worktree and run as one batch. The Tier 3 script
38
41
  # (builds, fixed ports) and the self-mock mutation probe (edits sources in place)
39
42
  # must each run once, by the verifier named here.
@@ -10014,6 +10014,80 @@
10014
10014
  "implementation-option-selection",
10015
10015
  "implementation-planning"
10016
10016
  ]
10017
+ },
10018
+ "defectClass": {
10019
+ "$ref": "#/$defs/DefectClass"
10020
+ }
10021
+ }
10022
+ },
10023
+ "DefectClass": {
10024
+ "type": "object",
10025
+ "description": "The defect class a blocker belongs to: the shared mechanism, every location x case cell swept for it, and whether an earlier final-verification run of the same target already blocked on it. Required on every final-verification acceptance blocker by `phases/final_verification/validation.py`.",
10026
+ "required": [
10027
+ "mechanism",
10028
+ "cells",
10029
+ "recurrence"
10030
+ ],
10031
+ "additionalProperties": false,
10032
+ "properties": {
10033
+ "mechanism": {
10034
+ "type": "string",
10035
+ "minLength": 1
10036
+ },
10037
+ "cells": {
10038
+ "type": "array",
10039
+ "minItems": 1,
10040
+ "items": {
10041
+ "$ref": "#/$defs/DefectClassCell"
10042
+ }
10043
+ },
10044
+ "recurrence": {
10045
+ "enum": [
10046
+ "new",
10047
+ "recurring"
10048
+ ]
10049
+ },
10050
+ "priorBlockers": {
10051
+ "type": "array",
10052
+ "items": {
10053
+ "type": "string",
10054
+ "pattern": "^final-verification-\\d{3,} AB-\\d{3,}$"
10055
+ }
10056
+ }
10057
+ }
10058
+ },
10059
+ "DefectClassCell": {
10060
+ "type": "object",
10061
+ "required": [
10062
+ "location",
10063
+ "case",
10064
+ "verdict",
10065
+ "evidence"
10066
+ ],
10067
+ "additionalProperties": false,
10068
+ "properties": {
10069
+ "location": {
10070
+ "type": "string",
10071
+ "minLength": 1
10072
+ },
10073
+ "case": {
10074
+ "type": "string",
10075
+ "minLength": 1
10076
+ },
10077
+ "verdict": {
10078
+ "enum": [
10079
+ "defective",
10080
+ "sound",
10081
+ "unverified",
10082
+ "not-applicable"
10083
+ ]
10084
+ },
10085
+ "evidence": {
10086
+ "type": "string",
10087
+ "minLength": 1
10088
+ },
10089
+ "regressionTest": {
10090
+ "type": "string"
10017
10091
  }
10018
10092
  }
10019
10093
  },
@@ -3510,6 +3510,9 @@
3510
3510
  "$ref": "#/$defs/ImplementationOptionSafetyBlocker"
3511
3511
  }
3512
3512
  },
3513
+ "splitAcceptance": {
3514
+ "$ref": "#/$defs/ImplementationOptionSplitAcceptance"
3515
+ },
3513
3516
  "unresolvedFeasibilityFacts": {
3514
3517
  "type": "array",
3515
3518
  "items": {
@@ -3608,6 +3611,9 @@
3608
3611
  "$ref": "#/$defs/ImplementationOptionSafetyBlocker"
3609
3612
  }
3610
3613
  },
3614
+ "splitAcceptance": {
3615
+ "$ref": "#/$defs/ImplementationOptionSplitAcceptance"
3616
+ },
3611
3617
  "unresolvedFeasibilityFacts": {
3612
3618
  "type": "array",
3613
3619
  "items": {
@@ -3936,6 +3942,18 @@
3936
3942
  }
3937
3943
  }
3938
3944
  },
3945
+ "ImplementationOptionSplitAcceptance": {
3946
+ "type": "object",
3947
+ "description": "The user's accept-risk answer that settles a split feasibility vote. Report assembly checks that clarificationRef names a blocks: approval row answered with accept-risk.",
3948
+ "required": ["clarificationRef"],
3949
+ "additionalProperties": false,
3950
+ "properties": {
3951
+ "clarificationRef": {
3952
+ "type": "string",
3953
+ "pattern": "^C-[0-9]{3}$"
3954
+ }
3955
+ }
3956
+ },
3939
3957
  "ImplementationOptionUnresolvedFact": {
3940
3958
  "type": "object",
3941
3959
  "required": [
@@ -3947,8 +3965,13 @@
3947
3965
  "properties": {
3948
3966
  "resolutionKind": {
3949
3967
  "type": "string",
3950
- "enum": ["user-decision", "technical-verification"],
3951
- "description": "Optional for historical reads; blocked report publication requires explicit classification."
3968
+ "enum": ["user-decision", "technical-verification", "deferred-gate"],
3969
+ "description": "Optional for historical reads; blocked report publication requires explicit classification. deferred-gate marks a fact only a later gate can close (for example a deployed-environment measurement); it requires gate and does not block candidate validity."
3970
+ },
3971
+ "gate": {
3972
+ "type": "string",
3973
+ "minLength": 1,
3974
+ "description": "For deferred-gate facts: the gate that closes the fact, such as a brief External Gates entry or final-verification user-owned external QA."
3952
3975
  },
3953
3976
  "clarificationRefs": {
3954
3977
  "type": "array",
@@ -10480,6 +10503,80 @@
10480
10503
  "implementation-option-selection",
10481
10504
  "implementation-planning"
10482
10505
  ]
10506
+ },
10507
+ "defectClass": {
10508
+ "$ref": "#/$defs/DefectClass"
10509
+ }
10510
+ }
10511
+ },
10512
+ "DefectClass": {
10513
+ "type": "object",
10514
+ "description": "The defect class a blocker belongs to: the shared mechanism, every location x case cell swept for it, and whether an earlier final-verification run of the same target already blocked on it. Required on every final-verification acceptance blocker by `phases/final_verification/validation.py`.",
10515
+ "required": [
10516
+ "mechanism",
10517
+ "cells",
10518
+ "recurrence"
10519
+ ],
10520
+ "additionalProperties": false,
10521
+ "properties": {
10522
+ "mechanism": {
10523
+ "type": "string",
10524
+ "minLength": 1
10525
+ },
10526
+ "cells": {
10527
+ "type": "array",
10528
+ "minItems": 1,
10529
+ "items": {
10530
+ "$ref": "#/$defs/DefectClassCell"
10531
+ }
10532
+ },
10533
+ "recurrence": {
10534
+ "enum": [
10535
+ "new",
10536
+ "recurring"
10537
+ ]
10538
+ },
10539
+ "priorBlockers": {
10540
+ "type": "array",
10541
+ "items": {
10542
+ "type": "string",
10543
+ "pattern": "^final-verification-\\d{3,} AB-\\d{3,}$"
10544
+ }
10545
+ }
10546
+ }
10547
+ },
10548
+ "DefectClassCell": {
10549
+ "type": "object",
10550
+ "required": [
10551
+ "location",
10552
+ "case",
10553
+ "verdict",
10554
+ "evidence"
10555
+ ],
10556
+ "additionalProperties": false,
10557
+ "properties": {
10558
+ "location": {
10559
+ "type": "string",
10560
+ "minLength": 1
10561
+ },
10562
+ "case": {
10563
+ "type": "string",
10564
+ "minLength": 1
10565
+ },
10566
+ "verdict": {
10567
+ "enum": [
10568
+ "defective",
10569
+ "sound",
10570
+ "unverified",
10571
+ "not-applicable"
10572
+ ]
10573
+ },
10574
+ "evidence": {
10575
+ "type": "string",
10576
+ "minLength": 1
10577
+ },
10578
+ "regressionTest": {
10579
+ "type": "string"
10483
10580
  }
10484
10581
  }
10485
10582
  },
@@ -185,6 +185,23 @@ for every variant and every source type below.
185
185
  missing and where it lives. A bracketed note inside the captured block
186
186
  helps the reader, but it does not replace that row: the row is what a later
187
187
  phase sees when it asks whether the source arrived complete.
188
+ 4. **Every local document the brief is derived from is its own Source.** A
189
+ spec, plan or note under the project root that you read while drafting —
190
+ one the reporter named, or one you opened because a ticket or the
191
+ conversation pointed at it — gets its own `### Source N — file` entry,
192
+ not only a mention in Context. When git does not track the file (a
193
+ `.project-docs/` spec, for example), write the entry with
194
+ `- format: by-reference`, a project-relative `ref`, and an empty fence:
195
+ the task worktree is a checkout without that file, and prepare copies it
196
+ read-only into the analysis packet's `## Cited Local Documents` on every
197
+ run, so later revisions of the document reach the next phase. A tracked
198
+ file is in the worktree; capture it as-is per rule 1.
199
+
200
+ **Enforced:** `validators/validate-brief.py`
201
+ (`check_cited_local_documents`) refuses a brief that names a git-untracked
202
+ document under the project root outside `## Source Material` without such
203
+ an entry. `okstra brief-sources attach --project-root <PROJECT_ROOT>
204
+ --brief <path>` appends the missing entries by reference.
188
205
 
189
206
  ### 1.0. brief variant
190
207
 
@@ -235,6 +252,9 @@ each source's raw content **separately** under the `Source Material
235
252
  Take an absolute or project-relative path via free-text. Read the entire
236
253
  file. Re-ask if it doesn't exist. Captured content is preserved **verbatim**
237
254
  in Source Material.
255
+ When the file is under the project root and git does not track it, record
256
+ it by reference instead (capture contract rule 4): the entry's `ref` is the
257
+ project-relative path, `format: by-reference`, and the fence stays empty.
238
258
 
239
259
  ### 1b. `Issue tracker ticket`
240
260
 
@@ -1047,6 +1067,12 @@ Lenses` rules.
1047
1067
  2. If it exits non-zero, **fix the offending brief(s) and re-run**. Do NOT
1048
1068
  print the Step 7 message until the validator passes.
1049
1069
 
1070
+ A `local document ... has no ### Source N — file entry` failure is fixed
1071
+ by `okstra brief-sources attach --project-root <PROJECT_ROOT> --brief
1072
+ <path>`, which appends the entry and changes no other line. For a brief
1073
+ other than the one you just wrote, show the user the listed path and ask
1074
+ before running it.
1075
+
1050
1076
  3. If neither validator copy is present, fall back to a manual checklist:
1051
1077
  confirm each brief has all ten required frontmatter keys (`type`,
1052
1078
  `brief-id`, `parent-id`, `ticket-id`, `source-type`, `task-group`,
@@ -69,9 +69,11 @@ Use this when one Linear project, or one parent issue, covers work in several re
69
69
  3. For each issue, propose target projects from evidence: labels, team, repository names or paths in the body. Confirm with `prompt_user` (multi-select, recommended projects first, reason in each description).
70
70
  4. For each (issue, project) pair, draft the scope: the part of the issue this project implements. Confirm it with `prompt_user` offering one or two drafts plus a custom-input option. A pair without scope is rejected by the CLI.
71
71
  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:`.
72
+ List in `localSources` every local document under that project's root you read to author the pair (a spec, a plan, a note), as a path relative to the project root. Each becomes a `### Source N — file` entry with `format: by-reference`, so every run of the child copies the current file into its analysis packet; a task worktree is a checkout and lacks an untracked `.project-docs/` file. A path outside the project root or a missing file rejects the plan, and a brief whose text names an untracked document that `localSources` does not list fails `validate-brief`.
72
73
  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>`.
73
74
  - `split plan rejected` or `rendered briefs failed validate-brief`: fix the named fields and run it again. Nothing was written.
74
- - `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.
75
+ - To add issues or projects to an earlier split, add them to the kept `split-plan.json` content and run the split again without `--overwrite`. Each brief's status is `created`, `unchanged`, `refreshed` or `overwritten`. The CLI renders each existing brief from the kept plan and from the new plan, both with the brief's original `created` date. When the two differ only in `### Out of scope`, `## Related Artifacts` and `## Related Task Graph`, it removes and appends just the changed lines in those sections (`refreshed`) and keeps every other line, hand edits included. An edit to the pair's own `outOfScope` list is applied the same way. A brief the new plan does not change stays `unchanged`, even with hand edits.
76
+ - `briefs already exist with content this plan would change`: the plan changes a listed brief outside those sections, for example its own scope or Context. Show the listed paths and ask before adding `--overwrite`, which re-renders only those briefs and replaces their hand edits. A brief with no kept plan to compare against is also listed when its text differs.
75
77
  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.
76
78
 
77
79
  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.
@@ -90,7 +92,8 @@ Plan JSON (`schemaVersion` 1). `source.kind` is `project` or `issue`; `recommend
90
92
  "recommendedPhase": "requirements-discovery",
91
93
  "context": "<text>", "problem": "<text>", "desiredOutcome": "<text>",
92
94
  "expectedBehavior": [], "preservedBehavior": [], "expectedOutcome": [],
93
- "externalGates": [], "constraints": [], "openQuestions": []
95
+ "externalGates": [], "constraints": [], "openQuestions": [],
96
+ "localSources": [".project-docs/specs/<spec>.md"]
94
97
  }
95
98
  ]
96
99
  }
@@ -100,6 +103,16 @@ Plan JSON (`schemaVersion` 1). `source.kind` is `project` or `issue`; `recommend
100
103
 
101
104
  The CLI writes each brief to `<projectRoot>/.okstra/briefs/<group-slug>/<ticketId>-<file-title>.md` (`<group-slug>` is the task group lowercased, with each run of characters other than `a-z` and `0-9` replaced by `-`, so `Upload V2` becomes `upload-v2`) 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.
102
105
 
106
+ ## Upgrading an Existing Brief
107
+
108
+ A brief written before `localSources` existed may name a local document only in Context; `validate-brief` and every prepare of that child then refuse it. Do not hand-edit the brief and do not re-split to fix it: `--overwrite` re-renders the brief and replaces later edits. Run
109
+
110
+ ```bash
111
+ okstra brief-sources attach --project-root <project-root> --brief <brief-path>
112
+ ```
113
+
114
+ It appends one `### Source N — file` entry (`format: by-reference`) per cited untracked document at the end of Source Material and changes no other line, so `EB-` / `PB-` / `EO-` ids, External Gates and later edits stay. Show the user the printed paths. Add the same paths to the pair's `localSources` in the kept `split-plan.json` before any later re-split, so the re-rendered brief carries them.
115
+
103
116
  ## Overview Page
104
117
 
105
118
  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.
@@ -43,6 +43,15 @@ attachment returned as a link, an opaque embed node) gets a
43
43
  `conversion-block:` row in Open Questions naming what is missing. Heading was
44
44
  originally "Source Material (verbatim — do not modify)" — the parenthetical
45
45
  is a reviewer note, not body text.
46
+ `format: by-reference` applies only to a `file` source whose `ref` is a path
47
+ under the project root (absolute, or relative to the project root): leave
48
+ the fence empty, and prepare copies the file read-only into the analysis
49
+ packet's `## Cited Local Documents` on every run. Use it for an untracked
50
+ local document such as a `.project-docs/` spec, which the task worktree does
51
+ not contain. A file only mentioned in Context is not read, and
52
+ `validators/validate-brief.py` refuses a brief that names an untracked
53
+ document outside Source Material without its own `file` entry;
54
+ `okstra brief-sources attach` appends the missing entries.
46
55
  -->
47
56
 
48
57
  ### Source 1 — <type: file | linear | jira | github | notion | url | user-input>
@@ -50,7 +59,7 @@ is a reviewer note, not body text.
50
59
  - ref: <abs file path | LIN-1234 | https://... | "conversation synthesis">
51
60
  - fetched-via: <Read | mcp__linear__getIssue | mcp__notion__... | gh issue view | WebFetch | user-paste>
52
61
  - fetched-at: <YYYY-MM-DD HH:MM>
53
- - format: <as-is | "Jira ADF → Markdown (semantics preserved)" | "tool-truncated — missing body requested from reporter">
62
+ - format: <as-is | by-reference | "Jira ADF → Markdown (semantics preserved)" | "tool-truncated — missing body requested from reporter">
54
63
 
55
64
  ```
56
65
  <Paste the raw source here without changing a single character.>
@@ -451,6 +451,12 @@
451
451
  "what-blocks-acceptance": "What blocks acceptance",
452
452
  "nothing-is-holding-acceptance": "Nothing is holding acceptance.",
453
453
  "follow-up": "Follow-up:",
454
+ "defect-mechanism": "Mechanism:",
455
+ "recurring-class": "Recurring — an earlier run blocked on this class:",
456
+ "location": "Location",
457
+ "case": "Case",
458
+ "verdict": "Verdict",
459
+ "regression-test": "Regression test",
454
460
  "residual-risk": "Residual risk",
455
461
  "owner": "Owner:",
456
462
  "escalation-condition": "Escalation condition:",
@@ -521,6 +527,8 @@
521
527
  "safety-blockers": "Safety blockers",
522
528
  "technical-verification": "Technical verification pending",
523
529
  "user-decision": "User decision record",
530
+ "deferred-gate": "Closes at a later gate",
531
+ "split-acceptance": "Split vote accepted as a risk by the user",
524
532
  "classification-required": "Classification required before reassembly",
525
533
  "unresolved-facts": "Unresolved feasibility facts",
526
534
  "expected-outcome": "Expected outcome",
@@ -451,6 +451,12 @@
451
451
  "what-blocks-acceptance": "수용을 막는 것",
452
452
  "nothing-is-holding-acceptance": "수용을 막는 것이 없습니다.",
453
453
  "follow-up": "후속:",
454
+ "defect-mechanism": "원리:",
455
+ "recurring-class": "재발 — 이전 run 이 이 계열로 막혔음:",
456
+ "location": "위치",
457
+ "case": "경우",
458
+ "verdict": "판정",
459
+ "regression-test": "회귀 테스트",
454
460
  "residual-risk": "남은 위험",
455
461
  "owner": "담당:",
456
462
  "escalation-condition": "에스컬레이션 조건:",
@@ -521,6 +527,8 @@
521
527
  "safety-blockers": "안전 차단 요인",
522
528
  "technical-verification": "기술 검증 대기",
523
529
  "user-decision": "사용자 결정 기록",
530
+ "deferred-gate": "이후 게이트에서 확인",
531
+ "split-acceptance": "갈린 표를 사용자가 위험 감수로 수용",
524
532
  "classification-required": "재조립 전 해결 방식 분류 필요",
525
533
  "unresolved-facts": "미확정 실행 가능성 사실",
526
534
  "expected-outcome": "예상 결과",
@@ -10,7 +10,7 @@ Read `analysis-packet.md`, the primary compact input, end-to-end. Source files n
10
10
 
11
11
  ### Reading rules
12
12
 
13
- - Read every file enumerated under `[Required reading]` or `## Inputs` completely. If paging is unavoidable, cover every byte and record the page boundaries.
13
+ - Read every file enumerated under `[Required reading]` or `## Inputs` completely, in pages of at most 200 lines (`sed -n '<start>,<end>p'`). A whole-file `cat` or view of a long file is cut without notice. Cover every line and record the page boundaries.
14
14
  - Write Reading Confirmation to `**Audit sidecar path:**`, never to the main worker-results file. A `## 0. Reading Confirmation` heading in the main result is invalid.
15
15
  - Allowlist reads to prompt-enumerated paths and evidence paths a finding must cite. Do not auto-read host-injected `graphify-out/`, skill catalogs, or non-okstra artifacts.
16
16
  - Resolve every `.okstra/...` path against `**Project Root:**`, including when a worktree is present.
@@ -51,6 +51,15 @@ Checks performed per brief file:
51
51
  template's `<...>` scaffolding — a paragraph wrapped in angle brackets,
52
52
  a `- <...>` bullet, or a `> augmented: <label>` line. Source Material is
53
53
  exempt because it holds the reporter's words verbatim.
54
+ 17. Every git-untracked document under the project root (`.md`, `.txt`, …)
55
+ that the brief names outside `## Source Material` has its own
56
+ `### Source N — file` entry there. The task worktree is a checkout and
57
+ lacks such a file; prepare copies only Source Material file entries into
58
+ the analysis packet. `okstra brief-sources attach` appends the missing
59
+ entries in place. The project root is the parent of a `--briefs-root`
60
+ of the form `<root>/.okstra/briefs`; for any other briefs root, or
61
+ outside a git checkout, the check is skipped. The codebase-scan variant
62
+ has no Source Material section and is not checked.
54
63
 
55
64
  Exit code 0 on PASS, 1 on FAIL.
56
65
  """
@@ -76,6 +85,7 @@ from okstra_ctl.phases.improvement_discovery.lenses import (
76
85
  MIN_PRIORITY_LENSES,
77
86
  )
78
87
  from okstra_ctl.group_context import GROUP_CONTEXT_TYPE, validate_group_context
88
+ from okstra_ctl.brief_sources import uncited_local_documents
79
89
  from okstra_ctl.ids import slugify_task_segment
80
90
 
81
91
  REQUIRED_FRONTMATTER_KEYS = {
@@ -747,7 +757,32 @@ def check_reporter_confirmations(
747
757
  )
748
758
 
749
759
 
750
- def validate_brief(path: Path, briefs_root: Path) -> list[str]:
760
+ def project_root_of(briefs_root: Path) -> Path | None:
761
+ """`<root>/.okstra/briefs` 이면 `<root>`, 아니면 None."""
762
+ resolved = briefs_root.resolve()
763
+ if resolved.name == "briefs" and resolved.parent.name == ".okstra":
764
+ return resolved.parent.parent
765
+ return None
766
+
767
+
768
+ def check_cited_local_documents(
769
+ text: str, scope: str, project_root: Path | None, errors: list[str]
770
+ ) -> None:
771
+ if project_root is None or scope == "codebase":
772
+ return
773
+ for relative in uncited_local_documents(text, project_root):
774
+ errors.append(
775
+ f"local document {relative!r} is cited in the brief but has no "
776
+ "`### Source N — file` entry in Source Material; it is untracked, "
777
+ "so the task worktree lacks it and no run will show it to workers. "
778
+ f"Run `okstra brief-sources attach --project-root {project_root} "
779
+ "--brief <this brief>` to append it by reference"
780
+ )
781
+
782
+
783
+ def validate_brief(
784
+ path: Path, briefs_root: Path, project_root: Path | None = None
785
+ ) -> list[str]:
751
786
  text = strip_html_comments(path.read_text(encoding="utf-8"))
752
787
  errors: list[str] = []
753
788
 
@@ -795,6 +830,9 @@ def validate_brief(path: Path, briefs_root: Path) -> list[str]:
795
830
  check_end_state_sections(text, scope, errors)
796
831
  check_variant_required_sections(text, scope, errors)
797
832
  check_template_scaffold(text, scope, errors)
833
+ check_cited_local_documents(
834
+ text, scope, project_root or project_root_of(briefs_root), errors
835
+ )
798
836
 
799
837
  # 2. brief-id matches filename stem
800
838
  stem = path.stem
@@ -242,6 +242,7 @@ from okstra_ctl.report_routing import technical_verification_routing_errors
242
242
  from okstra_ctl.phases.implementation_option_selection.validation import ( # noqa: E402
243
243
  validate_blocked_answer_channel,
244
244
  validate_implementation_option_selection,
245
+ validate_split_acceptance,
245
246
  )
246
247
  from okstra_ctl.phases.implementation_planning.validation import (
247
248
  _validate_requirement_provenance, _validate_stage_has_requirement,
@@ -3197,6 +3198,10 @@ def validate_final_report_data(
3197
3198
  f"implementation-option-selection: {error}"
3198
3199
  for error in validate_blocked_answer_channel(data)
3199
3200
  )
3201
+ failures.extend(
3202
+ f"implementation-option-selection: {error}"
3203
+ for error in validate_split_acceptance(data)
3204
+ )
3200
3205
  elif task_type == "implementation":
3201
3206
  _validate_stage_carry_sidecar_exists(data, report_path, failures)
3202
3207
  _validate_lead_fixes(data, failures)