okstra 0.184.0 → 0.185.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli-registry.mjs +9 -0
- package/dist/cli-registry.mjs.map +1 -1
- package/dist/commands/chat/chat.d.mts +1 -0
- package/dist/commands/chat/chat.mjs +385 -0
- package/dist/commands/chat/chat.mjs.map +1 -0
- package/dist/lib/skill-catalog.mjs +1 -0
- package/dist/lib/skill-catalog.mjs.map +1 -1
- package/docs/architecture.md +8 -6
- package/docs/cli.md +2 -1
- package/docs/for-ai/README.md +4 -2
- package/docs/for-ai/skills/okstra-chat.md +28 -0
- package/docs/for-ai/skills/okstra-inspect.md +1 -1
- package/docs/for-ai/skills/okstra-run.md +2 -2
- package/docs/for-ai/skills/okstra-user-response.md +10 -8
- package/docs/project-structure-overview.md +5 -4
- package/docs/task-process/README.md +1 -1
- package/docs/task-process/implementation-planning.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
- package/runtime/prompts/lead/report-writer.md +1 -1
- package/runtime/prompts/profiles/_clarification-recommendation.md +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +2 -1
- package/runtime/prompts/wizard/prompts.ko.json +2 -0
- package/runtime/python/okstra_ctl/adapters/hosts/claude-code/relay.md +2 -2
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +1 -1
- package/runtime/python/okstra_ctl/adapters/hosts/grok/relay.md +1 -1
- package/runtime/python/okstra_ctl/next_phase.py +67 -4
- package/runtime/python/okstra_ctl/user_response.py +147 -37
- package/runtime/python/okstra_ctl/wizard.py +13 -0
- package/runtime/skills/okstra-chat/SKILL.md +104 -0
- package/runtime/skills/okstra-inspect/facets/status.md +6 -5
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- package/runtime/skills/okstra-user-response/SKILL.md +50 -16
- package/runtime/validators/validate-run.py +90 -15
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# okstra-chat AI Manual
|
|
2
|
+
|
|
3
|
+
## Source
|
|
4
|
+
|
|
5
|
+
- Skill source: [`skills/okstra-chat/SKILL.md`](../../../skills/okstra-chat/SKILL.md)
|
|
6
|
+
- CLI: [`src/commands/chat/chat.mts`](../../../src/commands/chat/chat.mts)
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Global rooms under the okstra home so host sessions from different providers can send and read messages. Not a project `.okstra/` artifact.
|
|
11
|
+
|
|
12
|
+
## CLI
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
okstra chat rooms
|
|
16
|
+
okstra chat create --room <name>
|
|
17
|
+
okstra chat join --room <name> --name <display>
|
|
18
|
+
okstra chat members --room <name>
|
|
19
|
+
okstra chat send --room <name> --as <display> --to <all|name> --body <text>
|
|
20
|
+
okstra chat unread --room <name> --as <display>
|
|
21
|
+
okstra chat inbox --room <name> --as <display>
|
|
22
|
+
okstra chat log --room <name> --as <display>
|
|
23
|
+
okstra chat ack --room <name> --as <display> --through <id>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Display names are typed. The CLI does not generate them.
|
|
27
|
+
|
|
28
|
+
Read the fixed text rows. Do not parse JSON.
|
|
@@ -87,7 +87,7 @@ Keep the table narrow.
|
|
|
87
87
|
- workStatus
|
|
88
88
|
- Next
|
|
89
89
|
|
|
90
|
-
`nextRecommendedPhase` is an object `{phase, status, rationale}`. The Next cell is its `phase`, or `--` when that is empty. If `awaitingApproval`, or `nextRecommendedPhase.status` is anything but `ready`, add a marker to the Next cell and explain it.
|
|
90
|
+
`nextRecommendedPhase` is an object `{phase, status, rationale}`. The Next cell is its `phase`, or `--` when that is empty. If `awaitingApproval`, or `nextRecommendedPhase.status` is anything but `ready`, add a marker to the Next cell and explain it. When `awaitingApproval` is true, the next human action is to approve the plan (`okstra-run` → `implementation`, or `--approve`); do not re-run `implementation-planning`. When the pointer is `blocked` after planning, send the user to `okstra-user-response` on the named `C-NNN` ids first.
|
|
91
91
|
|
|
92
92
|
### Specific task
|
|
93
93
|
|
|
@@ -213,7 +213,7 @@ Read the scope and path from the persist action of `okstra wizard outcome`, not
|
|
|
213
213
|
|
|
214
214
|
## Okstra lead takeover
|
|
215
215
|
|
|
216
|
-
After render-bundle, read `<INSTRUCTION_SET_PATH>/lead-execution-prompt.md` verbatim and proceed from Phase 1 in that prompt's order.
|
|
216
|
+
After render-bundle, read `<INSTRUCTION_SET_PATH>/lead-execution-prompt.md` verbatim and proceed from Phase 1 in that prompt's order. Before any in-run approval or clarification question, follow the lead contract "User confirmation before an approval blocker": read cited plan items, worker findings, and files, then ask in the user's language with each option's outcome.
|
|
217
217
|
|
|
218
218
|
Inform the user on one line.
|
|
219
219
|
|
|
@@ -221,7 +221,7 @@ Inform the user on one line.
|
|
|
221
221
|
Took over as Okstra lead (`<host-runtime>`) for `<taskKey>` (`<task-type>`). Run dir: `<RUN_DIR_RELATIVE_PATH>`. Beginning Phase 1 (context loading).
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
For a single-element chain, the end of Step 6 is the end of the run. Step 7 below applies only when the `chain-stages` CSV has 2 or more elements.
|
|
224
|
+
For a single-element chain, the end of Step 6 is the end of the run. Step 7 below applies only when the `chain-stages` CSV has 2 or more elements. After an `implementation-planning` run, if `workflow.awaitingApproval` is true, tell the user to approve the plan; do not start another planning run. If the pointer is `blocked`, send the user to `okstra-user-response` first.
|
|
225
225
|
|
|
226
226
|
## implementation unattended chaining (chain-stages)
|
|
227
227
|
|
|
@@ -15,7 +15,7 @@ The model-facing reads are fixed text. Do not use the automation-compatible JSON
|
|
|
15
15
|
| Command | Purpose |
|
|
16
16
|
|---|---|
|
|
17
17
|
| `okstra user-response list-view --home <home> --project <projectId> --limit 3` | List tasks that still need clarification answers or a plan decision. |
|
|
18
|
-
| `okstra user-response show-view --report <reportPath> --project-root <projectRoot>` | Show open questions, choices, impact, approval context, plan candidates, and current state after validating project ownership. |
|
|
18
|
+
| `okstra user-response show-view --report <reportPath> --project-root <projectRoot>` | Show open questions, choices, impact, why asked, linked plan items, cited artifacts, approval context, plan candidates, and current state after validating project ownership. |
|
|
19
19
|
| `okstra user-response begin --report <reportPath> --task-key <taskKey>` | Open a typed transaction and return an opaque id. |
|
|
20
20
|
| `okstra user-response answer ...` | Add one validated answer to the draft. |
|
|
21
21
|
| `okstra user-response plan-decision ...` | Record an explicit plan decision. |
|
|
@@ -26,14 +26,16 @@ The legacy `list` and `show` JSON commands remain for automation compatibility.
|
|
|
26
26
|
|
|
27
27
|
## Flow
|
|
28
28
|
|
|
29
|
-
1. Run `okstra preflight --runtime
|
|
30
|
-
2.
|
|
31
|
-
3.
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
34
|
-
6.
|
|
29
|
+
1. Run `okstra preflight --runtime <host-runtime>` for the current harness. On `Okstra preflight: failed`, show `Reason` and `Recovery`, then stop. On `Okstra preflight: ready`, carry `Project root`, `Project ID`, `Runtime`, and `Relay contract`, then run `okstra paths --field home`.
|
|
30
|
+
2. Read the relay `Wizard interaction relay` JSON. When `native-single` is available and the option count fits `nativeLimits`, call `interactions.native-single.function` (`AskUserQuestion` / `ask_user_question` / `request_user_input` from that field). Do not print a numbered list in chat while the native tool is available. Otherwise render a numbered Markdown list. Do not substitute one host function name for another.
|
|
31
|
+
3. Select a task from `list-view` through that host picker.
|
|
32
|
+
4. Read only `show-view --report <reportPath> --project-root <projectRoot>` for report facts. The view also prints `Why asked`, `Linked plan items`, and `Cited artifacts`.
|
|
33
|
+
5. Read every cited `path:line` under the project root and every linked plan-item definition before asking. Do not search beyond that list. Investigation explains; it never changes `options[]`.
|
|
34
|
+
6. Ask one open clarification at a time in the user's language through the host picker. The question body is why, what is already decided, the fork, and what stays blocked. Do not lead with `Kind`, `Blocks`, `Expected form`, or `C-NNN`. Keep the row id in parentheses at the end.
|
|
35
|
+
7. Echo the complete response and confirm through the host picker (`Record as shown` / `Change an answer`). Do not ask them to type `confirmed`.
|
|
36
|
+
8. Begin the transaction, add answers and decisions, then finalize it.
|
|
35
37
|
|
|
36
|
-
Each `options[]` row displays `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`. Contract 3.0 additionally displays `reach`, `scopeEffects`, and row-level `approvalContext`.
|
|
38
|
+
Each `options[]` row displays `{role, answer, rationale, scopeImpact, addedWork, directionChange, disposition}`. Contract 3.0 additionally displays `reach`, `scopeEffects`, and row-level `approvalContext`. Option descriptions use this order: If you pick this (`addedWork`). What it reverses (`directionChange`). Scope (`reach` or `scopeImpact`). Why it is on the board (`rationale`). If the view says `not stated in the report`, repeat that text and never invent a value. A quote from a cited file may follow those axes; it does not replace them.
|
|
37
39
|
|
|
38
40
|
When the user selects a predefined option, pass only the fixed view's one-based option number. Python resolves that option's `disposition`, answer, reach, and scope effects from the validated report:
|
|
39
41
|
|
|
@@ -137,7 +137,7 @@ Runtime/install asset changes follow this checklist:
|
|
|
137
137
|
- `runtime/python/*` → `~/.okstra/lib/python/`
|
|
138
138
|
- `runtime/bin/*` → `~/.okstra/bin/`
|
|
139
139
|
- `runtime/templates/*` → `~/.okstra/templates/`
|
|
140
|
-
- `runtime/skills/<name>` (the
|
|
140
|
+
- `runtime/skills/<name>` (the fourteen user-facing skills only) → `~/.agents/skills` always, plus `~/.claude/skills` when `~/.claude` exists
|
|
141
141
|
- `runtime/prompts/*` → `~/.okstra/prompts/` (lead contracts under `prompts/lead/`, coding-preflight pack under `prompts/coding-preflight/`)
|
|
142
142
|
- the native Claude execution adapters in `runtime/agents/workers/` → `~/.claude/agents/` when `~/.claude` exists; retired provider transport-agent files are removed only when the prior install manifest owned them
|
|
143
143
|
- install manifests → `~/.okstra/installed-skills.json` (target-aware), `~/.okstra/installed-agents.json`
|
|
@@ -318,7 +318,7 @@ Important modules:
|
|
|
318
318
|
| `codex_dispatch.py` | Compatibility adapter delegating `okstra codex-dispatch` to the provider-neutral `worker_dispatch` path |
|
|
319
319
|
| `analysis_packet.py` | assembles the compact analysis-worker input packet for a task run from worker-owned profile sections; report/lead procedure stays outside the packet |
|
|
320
320
|
| `analysis_inputs.py` | shared input boundary for `project-analysis`, `feature-analysis`, and `change-impact-analysis` — validates evidence-report identity and review status, enforces the type-to-type relation allowlist, computes `exact`/`stale` freshness, and resolves free-text or `PF-NNN` feature targets for both wizard and prepare paths |
|
|
321
|
-
| `user_response.py` | parses clarification/approval responses and the analysis-review sidecar; `parse_analysis_review` validates accepted, revision-requested, and rejected decisions plus their affected IDs and reason |
|
|
321
|
+
| `user_response.py` | parses clarification/approval responses and the analysis-review sidecar; `parse_analysis_review` validates accepted, revision-requested, and rejected decisions plus their affected IDs and reason; `format_show_view` prints why-asked, linked plan items, and cited artifacts for the in-session picker |
|
|
322
322
|
| `context_cost.py` | read-side context-cost estimator for a prepared okstra task bundle (the `okstra context-cost` backend) |
|
|
323
323
|
| `schema_excerpt.py` | generates a task-type-scoped excerpt of the final-report schema — a schema reduction to inject into the worker/lead prompt |
|
|
324
324
|
| `work_categories.py` | requirements-discovery work-category (domain) **SSOT** (`is_valid_category`) — the work-category allowlist is defined only here |
|
|
@@ -436,7 +436,7 @@ Optional (v1.0 backward-compatible) top-level keys:
|
|
|
436
436
|
|
|
437
437
|
### 4.10 `skills/`
|
|
438
438
|
|
|
439
|
-
|
|
439
|
+
14 user-facing skills (the only skills `okstra install` copies). The list SSOT is `USER_SKILL_NAMES` in `src/lib/skill-catalog.mts`; the Claude plugin manifest and the installer both derive from it.
|
|
440
440
|
|
|
441
441
|
Boilerplate shared by several skills (bash invocation rule, outdated-CLI preflight, python bootstrap note) is kept canonical in `skills/_fragments/*.md` and expanded in place inside each `SKILL.md` between `<!-- BEGIN FRAGMENT: <name> -->` / `<!-- END FRAGMENT: <name> -->` markers by `tools/sync-skill-fragments.mjs` (`--check` fails on drift). Sources stay fully expanded, so `runtime/` and installed copies remain self-contained; the guards are `tests-js/skill-fragments.test.mjs` and `tests/contract/test_prompt_fragment_ownership.py`.
|
|
442
442
|
|
|
@@ -445,6 +445,7 @@ Boilerplate shared by several skills (bash invocation rule, outdated-CLI preflig
|
|
|
445
445
|
| `okstra-brief-gen` | yes | Produce task brief from ticket/doc/link/conversation |
|
|
446
446
|
| `okstra-run` | yes | Start/resume an okstra task in the current registered host session |
|
|
447
447
|
| `okstra-memory` | yes | Store/search/archive global conversation memory under `~/.okstra/memory-book` |
|
|
448
|
+
| `okstra-chat` | yes | Global rooms under `~/.okstra/chat` so host sessions from different providers can send and read addressed messages |
|
|
448
449
|
| `okstra-inspect` | yes | Unified read-side — sub-commands `status` (lifecycle + workStatus), `history` (past runs / re-run / resume), `report` (find final-report), `time` (elapsed-time breakdown), `logs` (wrapper log inventory + cleanup), `cost` (task bundle context/read cost), `errors` (error-log aggregation), `error-zip` (anonymized cross-project error bundle), `run-audit` (progress-invariant audit over run artifacts), `recap` (cross-run phase recap). `SKILL.md` is a thin core (preflight + dispatch table + shared rules) and each sub-command body lives in `skills/okstra-inspect/facets/<sub-command>.md`, lazily read only after dispatch resolves; the 1:1 match between dispatch rows and facet files is enforced by `tests/contract/test_okstra_inspect_facets.py` |
|
|
449
450
|
| `okstra-rollup` | yes | Cross-task roll-up — aggregate runs/time/errors across a task-group (or whole project) and synthesize a digest from the report files |
|
|
450
451
|
| `okstra-usage` | yes | Read-only project usage snapshot — aggregate recent run coverage, tokens, known cost, CPU, and wall-clock time by task type (default: 30 days) |
|
|
@@ -453,7 +454,7 @@ Boilerplate shared by several skills (bash invocation rule, outdated-CLI preflig
|
|
|
453
454
|
| `okstra-pr-gen` | yes | Register PR body templates under `~/.okstra/template/pr/` and generate a PR description from a branch diff (drives `okstra pr template` / `branches` / `gen`). **Global skill** — needs a Git repo, not `<PROJECT_ROOT>/.okstra/project.json` |
|
|
454
455
|
| `okstra-manager` | yes | Coordinate cross-project okstra tasks through manager-owned plans, assignments, sync snapshots, status, and child launch context packets |
|
|
455
456
|
| `okstra-setup` | yes | Install/check runtime and register project |
|
|
456
|
-
| `okstra-user-response` | yes | Submit answers to a run's open clarification items through the installed response helper without hand-editing artifacts |
|
|
457
|
+
| `okstra-user-response` | yes | Submit answers to a run's open clarification items through the installed response helper without hand-editing artifacts. The skill reads cited context from `show-view` before asking, uses the host native selection UI, and each option names the outcome of picking it |
|
|
457
458
|
| `okstra-code-review` | yes | Census-based code review of a diff — one okstra `implementation` stage, or any branch — against this project's coding-preflight rules. The result file goes under the task bundle (`code-reviews/stage-<NN>.md`) in stage mode and under `.project-docs/code-reviews/<branch>/` in branch mode; `okstra code-review target` resolves both |
|
|
458
459
|
|
|
459
460
|
> The former internal skills — `context-loader`, `team-contract`, `convergence`, `report-writer`, `coding-preflight` — and the lead contract are no longer skills. They ship as runtime resources under `prompts/lead/*` and `prompts/coding-preflight/*` (installed to `~/.okstra/prompts/`); see §4.11.
|
|
@@ -76,7 +76,7 @@ The last column is the `workflow.nextRecommendedPhase` pointer Phase 7 leaves be
|
|
|
76
76
|
| `requirements-discovery` | common questions only | profile/brief/base-ref exist | multi-worker analysis, convergence 1 round default | `ready` at `error-analysis` or `implementation-option-selection`; `pending` when neither is settled |
|
|
77
77
|
| `error-analysis` | common questions only | profile/brief/base-ref exist | multi-worker analysis, convergence 2 rounds default | `ready` at `implementation-option-selection`, or at `error-analysis` while the investigation continues |
|
|
78
78
|
| `implementation-option-selection` | comparison or preselected-validation context | stable brief IDs and at least three analysers | read-only candidate validation, exact coverage, separate direction confirmation | `ready` at `implementation-planning`, `pending` on `pending-direction-selection`, or `blocked` |
|
|
79
|
-
| `implementation-planning` | selected-direction report for a new plan | selection report/sidecar/digest or same-task planning rerun | one-direction realization + Phase 6 plan-body verification | `ready` at `implementation` on `plan-ready` (the
|
|
79
|
+
| `implementation-planning` | selected-direction report for a new plan | selection report/sidecar/digest or same-task planning rerun | one-direction realization + Phase 6 plan-body verification | `ready` at `implementation` on approvable `plan-ready` (`awaitingApproval` until the user flips `approved`); `blocked` when the gate is blocking or a `Blocks=approval` row is open; `ready` at `implementation-option-selection` on `direction-invalidated` |
|
|
80
80
|
| `implementation` | approved plan, stage multi-pick, executor | approved marker, Stage Lifecycle Snapshot, stage-key reservation, QA command deny-list | one run = one stage; executor writes in isolated stage worktree, verifiers read-only | `ready` at the stage report's `routingRecommendation.target` — `final-verification` on a clean stage |
|
|
81
81
|
| `final-verification` | approved plan, stage pick (whole-task or single-stage) | `VERIFICATION_TARGET` resolved; whole-task auto integration/teardown or single-stage worktree reuse | whole-task may integrate stages first; analyser verification itself is read-only | `ready` at `release-handoff` on an `accepted` verdict, otherwise at the phase owning the defect; `terminal` on `done` |
|
|
82
82
|
| `release-handoff` | handoff scope (stage-group or whole-task), PR template override/scope | Stage Lifecycle Snapshot eligibility, generated `release-handoff-input.md`, empty worker roster | single-lead; whole-task PR or stage-group collector branch/PR | always `terminal` |
|
|
@@ -62,7 +62,7 @@ sequenceDiagram
|
|
|
62
62
|
|
|
63
63
|
Prepare rejects a new plan without a selected-direction report. Comparison mode requires a valid `DIRECTION SELECTION` sidecar, while preselected-validation mode uses the confirmed upstream direction without one. The normalized snapshot binds the source report, source-data digest, option ID, direction body, requirements, and invariants.
|
|
64
64
|
|
|
65
|
-
Prepare does not name the next phase. It carries the inherited `workflow.nextRecommendedPhase` forward and lowers a `ready` pointer to `pending`, because this run has not finished and a `ready` pointer would read as an invitation to start the following phase. The pointer becomes `ready` at `implementation`
|
|
65
|
+
Prepare does not name the next phase. It carries the inherited `workflow.nextRecommendedPhase` forward and lowers a `ready` pointer to `pending`, because this run has not finished and a `ready` pointer would read as an invitation to start the following phase. The pointer becomes `ready` at `implementation` when Phase 7 projects an approvable `plan-ready` outcome; `workflow.awaitingApproval` is then true until the user flips `frontmatter.approved`. A blocking plan-body gate or an open `Blocks=approval` row projects `blocked` instead, so inspect and the wizard ask the user to answer those rows rather than start implementation or loop planning.
|
|
66
66
|
|
|
67
67
|
## 4. lead execution flow
|
|
68
68
|
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -133,10 +133,11 @@ This is not a phase. It fires wherever the blocker surfaces — during intake wh
|
|
|
133
133
|
|
|
134
134
|
The sequence is fixed:
|
|
135
135
|
|
|
136
|
-
1.
|
|
137
|
-
2.
|
|
138
|
-
3.
|
|
139
|
-
4.
|
|
136
|
+
1. Investigate before asking. Read every cited plan item, worker finding, and `path:line` the question depends on. You are not ready to ask while any option's outcome cannot be named as a concrete change: the extra work it creates, the already-decided thing it reverses, and which files or stages it touches. If a citation cannot be read, say so in the question; do not invent the missing fact.
|
|
137
|
+
2. Emit `PROGRESS: user-confirm <C-NNN> <the question, one line>` with the id the row would carry.
|
|
138
|
+
3. Ask in the user's language through the selected adapter's `prompt_user` mapping. Do not lead with `C-NNN`, `Kind`, `Blocks`, or `expectedForm`. The question body is why this is being asked, what is already decided, the fork, and each option as "if you pick this, then …". Keep the report-owned impact axes (reach or scope, added work, direction change). One question at a time. When that mapping names a native question function and the option count fits the relay `nativeLimits`, call that function with `{label, description}` options. Do not print a numbered list in chat while the native tool is available. Numbered text is only for a text-only relay or a prompt that does not fit `nativeLimits`.
|
|
139
|
+
4. On an answer — record the raw text in the row's `userInput`, set `status: answered` and `userConfirmation: asked-and-answered`, and apply the selected disposition in this run.
|
|
140
|
+
5. Only when asking fails does the row stay open: `asked-awaiting` when the user has not answered, `deferred-no-interactive-session` when this run has no user to ask.
|
|
140
141
|
|
|
141
142
|
For report contract v3 `implementation-planning`, record active approval decisions only through `okstra approval-decision`; report assembly derives each report row's status, resolution, and backtraces from that lead-owned ledger plus the activity ledger. Classify a user-owned selection as `user-decision`, a surviving non-correctness majority disagreement as `noncritical-dissent`, and a cited path/symbol mismatch, `P-Req-*` coverage mismatch, or independent Requirement Coverage blocker as `correctness-critical`. `select` is limited to `user-decision`, `accept-risk` is limited to `noncritical-dissent`, and `request-revision` / `reject` are available to all three classifications. `correctness-critical` never offers or records `accept-risk`. Contract v2 remains read-only compatible; do not create a new v2 report. **Enforced:** `scripts/okstra_ctl/approval_decisions.py` rejects invalid option/disposition combinations, and `validators/validate-run.py` `_validate_v3_approval_context` recomputes report backtraces.
|
|
142
143
|
|
|
@@ -424,7 +425,7 @@ jq -s 'group_by(.errorType) | map({type: .[0].errorType, count: length})' <runDi
|
|
|
424
425
|
|
|
425
426
|
The errors log is informational. Its presence/absence does not affect the final verdict. Do not block report writing on it.
|
|
426
427
|
|
|
427
|
-
After persistence, reply briefly in the resolved Report Language with: completion status, the human report path, the report record path, team-state path, validator result, resume command path, any remaining blocker. **Lead this reply with the run's task identity** — state `<task-group>/<task-id>` (or the full `taskKey`) first, so the reader knows which task the reply is about. **Every run-artifact path in this reply MUST be task-qualified** — report the human report as `.okstra/tasks/<task-group>/<task-id>/runs/<task-type>/reports/final-report-<task-type>-<seq>.html` rooted at the task bundle, NOT the bare `runs/<task-type>/reports/...` form (byte-for-byte identical across every task of the same task-type, so it cannot identify the task). Under that, cite the report record (`.data.json`) and one line to render the full reading copy: `okstra render-final-report <task-qualified data.json>`. The same task-qualified rule applies to the team-state path, resume command path, and any other run-artifact path this reply cites.
|
|
428
|
+
After persistence, reply briefly in the resolved Report Language with: completion status, the human report path, the report record path, team-state path, validator result, resume command path, any remaining blocker. For `implementation-planning` that left `workflow.awaitingApproval` true, the first sentence after the task identity tells the user to approve this plan (`okstra-run` → `implementation`, which asks `approve_plan_confirm`, or `--approve`) and does not propose another planning run. For a `blocked` pointer after planning, the first sentence names the open `C-NNN` ids and sends the user to `okstra-user-response`; do not start implementation and do not re-run planning until those answers exist. **Lead this reply with the run's task identity** — state `<task-group>/<task-id>` (or the full `taskKey`) first, so the reader knows which task the reply is about. **Every run-artifact path in this reply MUST be task-qualified** — report the human report as `.okstra/tasks/<task-group>/<task-id>/runs/<task-type>/reports/final-report-<task-type>-<seq>.html` rooted at the task bundle, NOT the bare `runs/<task-type>/reports/...` form (byte-for-byte identical across every task of the same task-type, so it cannot identify the task). Under that, cite the report record (`.data.json`) and one line to render the full reading copy: `okstra render-final-report <task-qualified data.json>`. The same task-qualified rule applies to the team-state path, resume command path, and any other run-artifact path this reply cites.
|
|
428
429
|
|
|
429
430
|
## Run-scoped worker-resource lifecycle
|
|
430
431
|
|
|
@@ -112,7 +112,7 @@ Never dispatch the translator before report assembly and `check-source`. For a n
|
|
|
112
112
|
|
|
113
113
|
`workflow.nextRecommendedPhase` has `phase`, `status`, and `rationale`. The status vocabulary is `ready`, `pending`, `blocked`, and `terminal`.
|
|
114
114
|
|
|
115
|
-
`phase` is non-empty only for an authored `ready` pointer. `prepare` may lower `ready` to `pending` while retaining `phase` in `scripts/okstra_ctl/render.py::_derive_next_recommended_phase`; readers must use `status` for launchability. When `finalVerification.routingRecommendation.target` is `release-handoff(stage-group)`, write `phase` as `release-handoff`.
|
|
115
|
+
`phase` is non-empty only for an authored `ready` pointer. `prepare` may lower `ready` to `pending` while retaining `phase` in `scripts/okstra_ctl/render.py::_derive_next_recommended_phase`; readers must use `status` for launchability. When `finalVerification.routingRecommendation.target` is `release-handoff(stage-group)`, write `phase` as `release-handoff`. When `implementationPlanning.outcome` is `plan-ready` and the plan-body gate is `blocked-by-disagreement` or `aborted-non-result`, or a `Blocks=approval` row is still `open`/`answered`, write `status` `blocked` (empty `phase`) — do not write `ready` at `implementation`. When that outcome is `plan-ready` and the plan is approvable, write `ready` at `implementation`; the user still has to approve it separately.
|
|
116
116
|
|
|
117
117
|
## Compatibility
|
|
118
118
|
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
- `rationale` — one sentence on why this option is on the board.
|
|
5
5
|
- `reach` — exactly one of `in-repo` or `cross-repo`.
|
|
6
6
|
- `scopeEffects` — optional tokens drawn from `{new-schema, deferrable}`.
|
|
7
|
-
- `addedWork` — one sentence naming the work this choice creates that the other choices do not. Name the
|
|
8
|
-
- `directionChange` — one sentence naming what this choice reverses: an approved plan item, a recorded decision, an earlier answer. When it reverses nothing, say so.
|
|
7
|
+
- `addedWork` — one sentence naming the work this choice creates that the other choices do not. Name the files, stages, or commands; do not substitute a cost adjective.
|
|
8
|
+
- `directionChange` — one sentence naming what this choice reverses: an approved plan item, a recorded decision, an earlier answer. Name that item. When it reverses nothing, say so.
|
|
9
9
|
- `disposition` — the effect of selecting the option. Use `select` for `user-decision`, `accept-risk` for `noncritical-dissent`, and `request-revision` or `reject` when the option sends the plan back. `correctness-critical` never offers `accept-risk`.
|
|
10
10
|
- report assembly derives `approvalContext`, status, and resolution. `approvalContext` contains only `classification`, `unblockCondition`, and `recommendedDisposition`; it never copies plan or activity identifiers.
|
|
11
11
|
- the three impact fields answer three different questions — how far the change reaches, what new work it creates, and what it overturns. Someone choosing between options needs all three, so never fold them into one sentence: whichever axis is easiest to write would silently stand in for the other two.
|
|
@@ -195,7 +195,8 @@ roles:
|
|
|
195
195
|
- dependency / migration risk assessment (ordering constraints, data backfills, feature-flag prerequisites, repo-internal sequencing)
|
|
196
196
|
- **Cross-Project Dependencies (conditionally required):** when the plan depends on work in another project / repo / published package, add (a) a `kind: cross-project` DM row to `dependencyMigrationRisk`, and (b) a matching `XP-NNN` row to `crossProjectDependencies`. An upstream-precondition row must have concrete `requiredWork` / `verificationSignal` / `howToStart` — `validators/validate-run.py` enforces that a DM `cross-project` ⇒ at least one `direction: upstream-precondition` XP row, and the schema enforces non-empty row fields. A cross-project dependency is recorded as this structured precondition, not as a soft Recommended Next Step. A single-project plan uses an empty array.
|
|
197
197
|
- **recommendedNextSteps policy:** keep the substance of cross-project preconditions/carries in `crossProjectDependencies`, and put in `§3 Recommended Next Steps` only a pointer to that section (`§5.4 Cross-Project Dependencies`) — no double recording.
|
|
198
|
-
- **Resuming from an approval blocker (BLOCKING).** When this report carries any `blocks: approval` clarification, one `recommendedNextSteps` entry MUST name the `--answered-clarifications` re-run, in its `text` or in one of its `commands`. The reader of the report is the person who has to act, and until now the mechanics of resuming lived only in the lead prompt — which is read after the next run has already started. Point them at `okstra recap assemble`, which prints the answered ids, the exact flag value, the sidecar paths, and whether the re-verification would fall back to full. Do not restate those values here: they are unknown while you write, because the user has not answered yet. **Enforced:** `validators/validate-run.py` `_validate_rerun_guidance`.
|
|
198
|
+
- **Resuming from an approval blocker (BLOCKING).** When this report carries any `blocks: approval` clarification, one `recommendedNextSteps` entry MUST name the `--answered-clarifications` re-run, in its `text` or in one of its `commands`. The reader of the report is the person who has to act, and until now the mechanics of resuming lived only in the lead prompt — which is read after the next run has already started. Point them at `okstra recap assemble`, which prints the answered ids, the exact flag value, the sidecar paths, and whether the re-verification would fall back to full. Do not restate those values here: they are unknown while you write, because the user has not answered yet. The first human action is answering those rows (`okstra-user-response`); do not tell the reader to start another planning run before the answers exist. **Enforced:** `validators/validate-run.py` `_validate_rerun_guidance`.
|
|
199
|
+
- **Asking for approval (BLOCKING).** When `outcome` is `plan-ready`, the gate is `passed` or `passed-with-dissent`, and no `blocks: approval` row is open, one `recommendedNextSteps` entry MUST tell the reader to approve (`--approve` or the in-session wizard). Do not recommend another `implementation-planning` run. **Enforced:** `validators/validate-run.py` `_validate_approval_guidance`.
|
|
199
200
|
- validation checklist (pre / mid / post) — each item is an exact command or observable outcome
|
|
200
201
|
- rollback strategy — exact revert path (commits, flags, migrations) and the signal that triggers rollback
|
|
201
202
|
- **Requirement admissibility (scope boundary):** a brief line becomes a Requirement Coverage row only when **a stage can satisfy it by changing files in this repository** — source, tests, config, or deployment *manifest files*. A line whose satisfaction needs a person's approval, a ticket status change, or an action against live infrastructure (applying a manifest, a cutover, creating a dashboard or alert, validating in staging/production) is NOT a requirement for this phase: it belongs to the brief's `## External Gates`, and this plan neither creates a stage for it nor cites it in coverage. Briefs generated by `okstra-brief-gen` pre-split these into the end-state sections `## Expected Behavior` / `## Preserved Behavior` / `## Expected Outcome` (admissible) and `## External Gates` (not); when reading an older brief that carries a raw Definition-of-Done checklist, apply the same test line by line. The boundary is the *action*, not the topic — "add the flag to `values-prod.yaml`" is admissible, "apply that manifest to prod" is not. Planning an operational stage this phase cannot execute (see the run-scope rule above forbidding deployments) produces steps whose commands never resolve, which the §5.5.9 gate then correctly blocks — the plan must not create that deadlock in the first place.
|
|
@@ -81,7 +81,9 @@
|
|
|
81
81
|
"echo_template": "task-type: {value}",
|
|
82
82
|
"options": {
|
|
83
83
|
"_RECOMMENDED_SUFFIX": " (recommended)",
|
|
84
|
+
"_APPROVE_SUFFIX": " (recommended · 계획 승인 후 구현)",
|
|
84
85
|
"_RERUN_SUFFIX": " (현재 phase 재실행)",
|
|
86
|
+
"_BLOCKED_RERUN_SUFFIX": " (현재 phase 재실행 — 열린 명료화에 답한 뒤)",
|
|
85
87
|
"_NEXT_SUFFIX": " (다음 단계)",
|
|
86
88
|
"__free_input__": "직접 입력"
|
|
87
89
|
},
|
|
@@ -140,7 +140,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
140
140
|
|---|---|
|
|
141
141
|
| `read_artifacts` | Use the host file-read primitive and preserve the core contract's read order. |
|
|
142
142
|
| `write_artifact` | Use the host file-write primitive only for paths authorized by the active lifecycle phase. |
|
|
143
|
-
| `prompt_user` | Use
|
|
143
|
+
| `prompt_user` | Use `AskUserQuestion` for approvals and clarifications that fit `nativeLimits`. Do not print a numbered list in chat while that tool is available. Do not infer an answer from silence. |
|
|
144
144
|
| `dispatch_worker` | First verify the materialized invocation metadata. Dispatch `runner=native-session` through `Agent(name: "<role>", run_in_background: true)` without `team_name`, passing the verified final prompt and `hostModelValue`. Dispatch `runner=cli-wrapper` with the deterministic shell command `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; never wrap that process in another `Agent(...)` call. **Not in a cmux run:** when the run manifest's `terminalBackend` is `cmux-pane`, `prompts/lead/adapters/cmux.md` overrides this row. |
|
|
145
145
|
| `await_workers` | Arm one background shell poll for the pending Result Paths; the spawn acknowledgement is not completion. |
|
|
146
146
|
| `redispatch_worker` | Materialize and verify a fresh invocation, then use a fresh native `Agent(...)` session or `okstra worker-dispatch` attempt according to the persisted runner. |
|
|
@@ -207,7 +207,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
207
207
|
- Collect and persist token usage before any live-roster cleanup, including cleanup between batches and the run-end shutdown sequence.
|
|
208
208
|
- Before each new worker batch (and before the next phase's render-bundle), close the panes of the dispatches that finished in the prior round, in two passes. First count: `okstra team reclaim --project-root "<PROJECT_ROOT>" --run-manifest "<RUN_MANIFEST_PATH>" --dry-run` closes nothing and prints one `<paneId>\t<kind>` line per pane it would close — count those lines as `<n>`. Then run the same command **without** `--dry-run` to close them, and emit the neutral contract's `PROGRESS: phase-batch-cleanup panes=<n>` checkpoint with that count. Call both passes after collecting that round's results and token usage and before the next dispatch. The command reads each dispatch's recorded status, so an in-progress worker keeps its pane whichever moment you call it — you do not scope the pass by hand. It closes only the panes okstra opened and recorded; a pane the harness opened for itself carries no recorded id and is not okstra's to close. A `cli-wrapper` run holds no pane at all, so `<n>` is `0` — still emit the checkpoint.
|
|
209
209
|
- Reclaiming a pane does not stop the worker's background task. Every `dispatch_worker` Agent runs with `run_in_background: true`, so a worker whose result is already collected stays a live background task for the rest of the session — that residue is what fills the harness's exit-time `Background work is running` list. At the same batch boundary, right after the pane reclaim, call `TaskStop(task_id: "<name>")` once per worker of the completed batch, passing the exact `name` used at dispatch (`<workerId>-worker`, `<workerId>-worker-reverify-r<N>`, `<provider>-worker-critic`, `report-writer`). Stop only workers whose results were already collected — never an in-flight worker, never the lead, and keep `report-writer` while it is in flight, matching the pane pass's `--keep report-writer-worker`. `TaskStop` on an already-finished task is a no-op; treat a failure as benign, record nothing, and continue the boundary. This runs in a non-tmux session too, where the pane passes no-op but the background tasks still exist.
|
|
210
|
-
- Before any `prompt_user`/`AskUserQuestion` that follows worker dispatch — an approval, clarification, or decision gate — run the same two passes used at a round boundary: `okstra team reclaim … --dry-run` to count `<n>`, then the same command without `--dry-run` to close, and emit `PROGRESS: phase-gate-cleanup panes=<n>`. Then `TaskStop(task_id: "<name>")` each completed worker, exactly as at a batch boundary. A bare `TaskStop` idles the roster task and closes no pane, so it is never cleanup on its own. This keeps the user from being shown a gate while finished worker panes are still open.
|
|
210
|
+
- Before any `prompt_user`/`AskUserQuestion` that follows worker dispatch — an approval, clarification, or decision gate — run the same two passes used at a round boundary: `okstra team reclaim … --dry-run` to count `<n>`, then the same command without `--dry-run` to close, and emit `PROGRESS: phase-gate-cleanup panes=<n>`. Then `TaskStop(task_id: "<name>")` each completed worker, exactly as at a batch boundary. A bare `TaskStop` idles the roster task and closes no pane, so it is never cleanup on its own. This keeps the user from being shown a gate while finished worker panes are still open. After that cleanup, follow the lead contract "User confirmation before an approval blocker": read cited plan items, worker findings, and files before asking, and ask in the user's language with each option's outcome.
|
|
211
211
|
- After batch cleanup, record the current live session generation with `okstra token-usage "<TEAM_STATE_PATH>" --record-observed-session --project-root "<PROJECT_ROOT>"`. This protects usage accounting when Claude Code re-issues the session id after resume or compaction.
|
|
212
212
|
- Claude Code cannot delete the implicit team or surgically remove an idle roster entry. Explain that teammates may remain visible until session end and, when needed, give the manual action `Delete team <teamName> in Teams/FleetView`.
|
|
213
213
|
- The `SessionEnd` hook runs `$HOME/.okstra/bin/okstra-team-reconcile.sh --session-end` as the safety net for the current live session.
|
|
@@ -121,7 +121,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
121
121
|
|---|---|
|
|
122
122
|
| `read_artifacts` | Read the manifest-provided paths through the current host's file interface. |
|
|
123
123
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
124
|
-
| `prompt_user` | Use `request_user_input` for approvals and clarifications that fit two or three options
|
|
124
|
+
| `prompt_user` | Use `request_user_input` for approvals and clarifications that fit `nativeLimits` (two or three options). Do not print a numbered list in chat while that tool is available. Otherwise ask through host text and stop until an explicit answer arrives. |
|
|
125
125
|
| `dispatch_worker` | Verify each materialized invocation first. Dispatch `runner=native-session` with the current Codex host's primitive, the returned `promptPath`, and `hostModelValue`. Pass `runner=cli-wrapper` assignments to `okstra worker-dispatch --project-root <root> --run-manifest <path> --workers <ids>`; use `--dry-run` first when required. **Not in a cmux run:** when `terminalBackend` is `cmux-pane`, the cmux adapter overrides this row. |
|
|
126
126
|
| `await_workers` | Await native host workers through the host primitive and CLI workers through synchronous dispatch, then verify team-state terminal records and Result Paths for both. |
|
|
127
127
|
| `redispatch_worker` | Materialize and verify a fresh invocation, then start a fresh native worker or `okstra worker-dispatch` attempt according to the persisted runner. |
|
|
@@ -136,7 +136,7 @@ For a `host-text` mapping, render each numbered item as its option label followe
|
|
|
136
136
|
|---|---|
|
|
137
137
|
| `read_artifacts` | Read the manifest-provided paths through the current Grok host file interface. |
|
|
138
138
|
| `write_artifact` | Write only core-authorized `.okstra/` artifacts and preserve their schemas. |
|
|
139
|
-
| `prompt_user` | Use `ask_user_question` for approvals and clarifications
|
|
139
|
+
| `prompt_user` | Use `ask_user_question` for approvals and clarifications that fit `nativeLimits`. Do not print a numbered list in chat while that tool is available. Do not infer an answer from silence. |
|
|
140
140
|
| `dispatch_worker` | Verify the materialized invocation. Use the host primitive with `promptPath` and `hostModelValue` for `native-session`; use deterministic `okstra worker-dispatch` with `modelExecutionValue` for `cli-wrapper`. **Not in a cmux run:** the cmux adapter overrides this row. |
|
|
141
141
|
| `await_workers` | Await through the selected common dispatch backend, then verify terminal state and Result Paths. |
|
|
142
142
|
| `redispatch_worker` | Start a fresh attempt from the persisted assignment and record the supplied dispatch kind. |
|
|
@@ -9,6 +9,8 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
from typing import Any, Mapping
|
|
11
11
|
|
|
12
|
+
from okstra_ctl.clarification_items import APPROVAL_BLOCKS, UNRESOLVED_STATUSES
|
|
13
|
+
|
|
12
14
|
STATUS_READY = "ready"
|
|
13
15
|
STATUS_PENDING = "pending"
|
|
14
16
|
STATUS_BLOCKED = "blocked"
|
|
@@ -69,6 +71,12 @@ _OPTION_SELECTION_NON_PHASE = {
|
|
|
69
71
|
"blocked": STATUS_BLOCKED,
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
# run.py BLOCKING_PLAN_BODY_GATES 와 같아야 한다. next_phase 는 run 을
|
|
75
|
+
# 가져오지 않는다 — wizard 가 둘 다 import 해서 순환이 생긴다.
|
|
76
|
+
_BLOCKING_PLAN_GATES = frozenset(
|
|
77
|
+
{"blocked-by-disagreement", "aborted-non-result"}
|
|
78
|
+
)
|
|
79
|
+
|
|
72
80
|
# final-verification 의 routing enum 중 phase 이름이 아니라 phase 에 붙은 범위
|
|
73
81
|
# 한정자인 값 → 실제로 실행할 phase. `release-handoff(stage-group)` 은 넘길 stage
|
|
74
82
|
# 묶음을 좁힌다는 뜻이지 다른 phase 가 아니다. 범위는 위저드의 handoff_stage_pick
|
|
@@ -210,13 +218,68 @@ def _from_option_selection(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
|
210
218
|
return make(phase=routing, status=STATUS_READY)
|
|
211
219
|
|
|
212
220
|
|
|
221
|
+
def _unresolved_approval_ids(report_data: Mapping[str, Any]) -> list[str]:
|
|
222
|
+
rows = report_data.get("clarificationItems")
|
|
223
|
+
if not isinstance(rows, list):
|
|
224
|
+
return []
|
|
225
|
+
ids: list[str] = []
|
|
226
|
+
for row in rows:
|
|
227
|
+
if not isinstance(row, Mapping):
|
|
228
|
+
continue
|
|
229
|
+
blocks = str(row.get("blocks") or "").strip().lower()
|
|
230
|
+
status = str(row.get("status") or "").strip().lower()
|
|
231
|
+
row_id = row.get("id")
|
|
232
|
+
if (
|
|
233
|
+
blocks in APPROVAL_BLOCKS
|
|
234
|
+
and status in UNRESOLVED_STATUSES
|
|
235
|
+
and isinstance(row_id, str)
|
|
236
|
+
and row_id
|
|
237
|
+
):
|
|
238
|
+
ids.append(row_id)
|
|
239
|
+
return ids
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _planning_approval_block_reason(
|
|
243
|
+
report_data: Mapping[str, Any], planning: Mapping[str, Any]
|
|
244
|
+
) -> str:
|
|
245
|
+
"""plan-ready 인데 승인할 수 없으면 근거, 아니면 빈 문자열.
|
|
246
|
+
|
|
247
|
+
자문 게이트(`passed-with-dissent`)와 재현 실패 `has-dissent` 는 여기 안
|
|
248
|
+
들어온다. 차단은 `blocked-by-disagreement` / `aborted-non-result` 와
|
|
249
|
+
`Status` 가 open/answered 인 `Blocks=approval` 행뿐이다.
|
|
250
|
+
"""
|
|
251
|
+
ids = _unresolved_approval_ids(report_data)
|
|
252
|
+
if ids:
|
|
253
|
+
listed = ", ".join(ids)
|
|
254
|
+
return (
|
|
255
|
+
f"{listed} 가 Blocks=approval 로 열려 승인할 수 없습니다. "
|
|
256
|
+
"okstra-user-response 로 답한 뒤 그 답을 가지고 계획 단계를 "
|
|
257
|
+
"재개하세요. 구현을 시작하거나, 답을 쓰기 전에 계획 단계를 "
|
|
258
|
+
"다시 돌리지 마세요."
|
|
259
|
+
)
|
|
260
|
+
verification = planning.get("planBodyVerification")
|
|
261
|
+
gate = ""
|
|
262
|
+
if isinstance(verification, Mapping):
|
|
263
|
+
gate = str(verification.get("gateResult") or "").strip().lower()
|
|
264
|
+
if gate in _BLOCKING_PLAN_GATES:
|
|
265
|
+
return (
|
|
266
|
+
f"계획 본문 게이트가 `{gate}` 이라 승인할 수 없습니다. "
|
|
267
|
+
"구현을 시작하거나 계획 단계를 바로 다시 돌리지 마세요."
|
|
268
|
+
)
|
|
269
|
+
return ""
|
|
270
|
+
|
|
271
|
+
|
|
213
272
|
def _from_planning(report_data: Mapping[str, Any]) -> dict[str, str]:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return make(phase="implementation", status=STATUS_READY)
|
|
273
|
+
planning = _block(report_data, "implementationPlanning")
|
|
274
|
+
outcome = str(planning.get("outcome") or "")
|
|
217
275
|
if outcome == "direction-invalidated":
|
|
218
276
|
return make(phase="implementation-option-selection", status=STATUS_READY)
|
|
219
|
-
|
|
277
|
+
if outcome != "plan-ready":
|
|
278
|
+
return make(status=STATUS_PENDING)
|
|
279
|
+
blocked_reason = _planning_approval_block_reason(report_data, planning)
|
|
280
|
+
if blocked_reason:
|
|
281
|
+
return make(status=STATUS_BLOCKED, rationale=blocked_reason)
|
|
282
|
+
return make(phase="implementation", status=STATUS_READY)
|
|
220
283
|
|
|
221
284
|
|
|
222
285
|
def _from_target(report_data: Mapping[str, Any], key: str) -> dict[str, str]:
|