okstra 0.199.1 → 0.199.2
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/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/adapters/cmux.md +2 -0
- package/runtime/prompts/lead/okstra-lead-contract.md +1 -1
- package/runtime/prompts/lead/report-writer.md +8 -7
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -1
- package/runtime/prompts/profiles/forbidden-actions.json +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +5 -1
- package/runtime/python/okstra_ctl/adapters/hosts/codex/relay.md +8 -0
- package/runtime/python/okstra_ctl/agent/prompt_cli/dynamic_verifier.py +5 -1
- package/runtime/python/okstra_ctl/agent/prompt_cli/run_identity.py +7 -1
- package/runtime/python/okstra_ctl/conformance.py +7 -0
- package/runtime/python/okstra_ctl/convergence_store.py +16 -1
- package/runtime/python/okstra_ctl/error_log_core.py +2 -1
- package/runtime/python/okstra_ctl/error_report.py +20 -1
- package/runtime/python/okstra_ctl/implementation_direction.py +22 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +16 -0
- package/runtime/python/okstra_ctl/qa_commands.py +26 -2
- package/runtime/python/okstra_ctl/report_finalize.py +20 -0
- package/runtime/python/okstra_ctl/run.py +10 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +13 -0
- package/runtime/python/okstra_ctl/worker_prompt_policy.py +10 -0
- package/runtime/skills/okstra-inspect/SKILL.md +4 -2
- package/runtime/skills/okstra-run/SKILL.md +1 -1
- package/runtime/validators/forbidden_actions.py +3 -0
- package/runtime/validators/validate-run.py +76 -3
- package/runtime/validators/validate_session_conformance.py +5 -0
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -57,7 +57,7 @@ For every other task type:
|
|
|
57
57
|
- Pointer `status: terminal` → the lifecycle ends here. Say the task is finished and quote the pointer's `rationale`. Do not propose a run and do not send the user to `/okstra-inspect`: the decision is already made, so there is nothing to inspect. If this run registered follow-up tasks, name them and the command that starts one.
|
|
58
58
|
- Pointer `status: blocked` → the pointer's `rationale` names what is in the way and what to run. Quote it and issue that command — `/okstra-user-response` for the `C-NNN` ids it lists, or `/okstra-run` for the phase it names.
|
|
59
59
|
- Pointer `status: pending` with a `rationale` → the user's own input is what comes next. Quote the `rationale` and issue the command it names. Do not re-run the phase that just completed, and do not send the user to `/okstra-inspect`: a finished phase has nothing to inspect, and re-running it discards the result the user is being asked to act on.
|
|
60
|
-
- Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run
|
|
60
|
+
- Phase 7 `validate-run` failed → one line naming the blocking cause, then use the blocked pointer's `phase` as the recovery target with `/okstra-run`; re-run the current phase only when that target is empty. Only a failure matching the blocking allowlist (`okstra_ctl.blocking_checks`) reaches this row; every other finding was demoted to an advisory, printed as `validate-run: advisory — <finding>`, and the run passed. Name those advisories in one line and take the command from the matching pointer row above — an advisory is not a reason to re-run.
|
|
61
61
|
- Otherwise → `/okstra-inspect status` for this task.
|
|
62
62
|
|
|
63
63
|
Name every file in this reply as a markdown link — `[<what it is>](<path>)`, path inside the parentheses — so the user can open it. `reportPaths.markdown` in the `report-finalize` result already carries this run's report, report record, and team state that way. Commands stay in backticks.
|
|
@@ -6,6 +6,8 @@ This adapter maps the neutral Okstra lead operations to a cmux session, where Ok
|
|
|
6
6
|
|
|
7
7
|
It overrides only the worker-dispatch portion of the selected host relay, not the host or lead contract. Your own runtime still decides how you read files, ask the user, and record your session — this file only decides how workers are started, awaited, and reclaimed.
|
|
8
8
|
|
|
9
|
+
Keep the selected host relay's dispatch permission guidance when calling `okstra team dispatch`. For a restricted Codex lead, its `Permission at the dispatch boundary` guidance applies to every live dispatch, including reverify and critic jobs; selecting cmux does not grant execution privileges.
|
|
10
|
+
|
|
9
11
|
## Capability declaration
|
|
10
12
|
|
|
11
13
|
| Field | Value |
|
|
@@ -512,7 +512,7 @@ For every other task type:
|
|
|
512
512
|
- Pointer `status: terminal` and the result carries `nextInGroup` → the task is finished and the task-group has a task not yet started: say this task is finished, then close on `/okstra-run` for `nextInGroup.briefId` from its `brief` path — the group's start order is the brief ordinal, and `nextCommand.note` already names the task and its brief.
|
|
513
513
|
- Pointer `status: blocked` → the pointer's `rationale` names what is in the way and what to run. Quote it and issue that command — `/okstra-user-response` for the `C-NNN` ids it lists, or `/okstra-run` for the phase it names.
|
|
514
514
|
- Pointer `status: pending` with a `rationale` → the user's own input is what comes next. Quote the `rationale` and issue the command it names. Do not re-run the phase that just completed, and do not send the user to `/okstra-inspect`: a finished phase has nothing to inspect, and re-running it discards the result the user is being asked to act on.
|
|
515
|
-
- Phase 7 `validate-run` failed → one line naming the blocking cause, then `/okstra-run
|
|
515
|
+
- Phase 7 `validate-run` failed → one line naming the blocking cause, then use the blocked pointer's `phase` as the recovery target with `/okstra-run`; re-run the current phase only when that target is empty. Only a failure matching the blocking allowlist (`okstra_ctl.blocking_checks`) reaches this row; every other finding was demoted to an advisory, printed as `validate-run: advisory — <finding>`, and the run passed. Name those advisories in one line and take the command from the matching pointer row above — an advisory is not a reason to re-run.
|
|
516
516
|
- Otherwise → `/okstra-inspect status` for this task.
|
|
517
517
|
|
|
518
518
|
When the host native picker is available and two of those rows could apply, ask with that picker (recommended first). Do not end the turn after the status dump.
|
|
@@ -123,16 +123,17 @@ For historical schema-v1 Markdown only, the following heading table remains a re
|
|
|
123
123
|
|
|
124
124
|
**Enforced:** `okstra_ctl.report_finalize.V3_STEP_ORDER` is the order — `report-finalize` runs the steps from that tuple, so the sequence cannot be reordered by a caller. Running the steps by hand is what this rule forbids, and that path is not reachable through the CLI.
|
|
125
125
|
|
|
126
|
-
Do not run the
|
|
126
|
+
Do not run the nine steps below manually. Invoke `okstra report-finalize`; contract 3.0 runs them in this order:
|
|
127
127
|
|
|
128
128
|
1. **`token-usage`** — collect usage into team state without touching the final record.
|
|
129
129
|
2. **`project-activity`** — report assembly validates every owner input and publishes the final record once.
|
|
130
|
-
3. **`
|
|
131
|
-
4. **`
|
|
132
|
-
5. **`
|
|
133
|
-
6. **`
|
|
134
|
-
7. **`
|
|
135
|
-
8. **`
|
|
130
|
+
3. **`preflight`** — run the same schema and conformance checks used by final validation before translation, without writing run status. Structural failure skips translation; rendering and final validation still record the failure. `--only translate` also includes this check.
|
|
131
|
+
4. **`translate`** — for a non-English `reportLanguage`, materialize and dispatch the translator worker and require its `*.i18n.<lang>.json` sidecar; a no-op for English or when the sidecar already exists.
|
|
132
|
+
5. **`render-views`** — render the Markdown reading copy and human HTML, with the translation sidecar overlaid.
|
|
133
|
+
6. **`spawn-followups`** — materialize registered follow-up tasks.
|
|
134
|
+
7. **`validate-run`** — validate the record, views, run manifest, and team state.
|
|
135
|
+
8. **`record-group-memory`** — write this run's conclusion (headline, decisions, watch-outs, open follow-ups, record path, next phase) and the group's start order into the task-group's `group-context.md` okstra region, creating the file when the group has none; skipped, like teardown, when an earlier step failed. Sibling tasks read it as `## Task-Group Memory`.
|
|
136
|
+
9. **`teardown-stages`** — remove eligible stage worktrees after successful validation.
|
|
136
137
|
|
|
137
138
|
After `report-finalize` returns, the lead — not the report writer — closes the run with the launch prompt's User closeout: one command the user can run now.
|
|
138
139
|
|
|
@@ -9,12 +9,14 @@ at Phase 5, BEFORE constructing the verifier worker dispatch prompts.
|
|
|
9
9
|
|
|
10
10
|
- Every verdict comes from a fresh session with no shared context, never from the session that wrote the diff. Verifiers MUST NOT call Edit, Write, or any Bash command that mutates files outside the run's artifact directories. If a verifier wants a fix, it records the recommendation in its worker result; it does not apply the fix itself.
|
|
11
11
|
- Session isolation is the primary self-review safeguard: each verifier is a separate invocation with its own context window. Reusing the executor's model is acceptable. The model comes from the run's stored assignment.
|
|
12
|
-
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced.
|
|
12
|
+
- Verifiers read from the SAME working tree path the Executor used so they observe the exact diff the Executor produced. Source files, lockfiles, Git state, and shared links remain read-only. Declared verification commands may create their normal worktree-local build/cache outputs and install dependencies with a frozen lockfile. This is the bounded exception to the preceding write restriction; it does not permit source repairs, moving shared links, or redirecting build outputs outside the worktree. Run-owned logs remain in the run artifact directories.
|
|
13
13
|
|
|
14
14
|
**Enforced:** `_validate_verifier_command_log_is_read_only` in `validators/validate-run.py` scans every `verifierResults[].readOnlyCommandLog` for source-mutating commands (`sed -i`, `git checkout --`/`restore`/`reset --hard`/`stash`/`clean`/`apply`, `patch -p`, `rm -rf`, `truncate`). Read-only forms (`git stash list`, `git clean --dry-run`, `git apply --check`) pass.
|
|
15
15
|
|
|
16
16
|
## Verifier QA duties (independent re-run mandate)
|
|
17
17
|
|
|
18
|
+
`okstra_ctl.qa_commands.find_unfrozen_installs` rejects unfrozen npm/pnpm/yarn/bun installs in both the plan preflight and the verifier command log. If an approved command conflicts with these restrictions or cannot run in the provisioned worktree, report the exact checklist row and route to `implementation-planning`; do not substitute a command during verification.
|
|
19
|
+
|
|
18
20
|
Every verifier acts as a QA gate, not just a diff reviewer. Trusting the executor's reported evidence is forbidden — verifiers MUST reproduce it themselves from the same worktree path the executor used.
|
|
19
21
|
|
|
20
22
|
**Enforced:** `_validate_verifier_reran_independently` in `validators/validate-run.py` fails an empty `independentValidationRerun`. What you wrote in it is not pattern-matched — the wording check that used to reject phrasings citing the executor is gone, because it fired on reports that had reproduced the run and merely said so. Write the command you ran and its outcome; the Discrepancy rule below is what makes you name the divergence.
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"implementation-planning": [
|
|
49
49
|
"source code edits of any kind (Edit/Write on project source files is forbidden)",
|
|
50
|
-
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`)
|
|
50
|
+
"file writes outside the run`s artifact directories (`reports/`, `prompts/`, `state/`, `manifests/`, `worker-results/`, `status/`, `sessions/`), including task-root QA scripts, manifest, and tsconfig (planning declares conformance commands and required dependencies; implementation writes these files); in particular, do not write to `docs/superpowers/specs/` or `docs/superpowers/plans/`",
|
|
51
51
|
"executing builds, migrations, deployments, or any state-mutating command",
|
|
52
52
|
"starting `implementation` inside this run (must be a separate run authorised by an approved deliverable from this phase), even if the user says \"다음 단계 진행해\"",
|
|
53
53
|
"dispatching parallel sub-agents beyond the required worker roster (okstra owns worker fan-out)",
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Implementation Planning Profile
|
|
2
2
|
|
|
3
|
+
Validation commands are executable inputs: preserve their newlines, quotes, and code bodies. Use frozen dependency installation (`npm ci`, `pnpm install --frozen-lockfile`, or the package manager's equivalent). Name QA scripts with absolute task-artifact paths when a command runs from a stage worktree; `.okstra/tasks/...` relative to that worktree does not point to the project task. `stage_validation_executability_errors` enforces command restrictions at planning validation, implementation entry, and report preflight, including advisory plan-body runs.
|
|
4
|
+
|
|
5
|
+
Plan for the actual worktree layout before approval. Shared documentation directories can be links to the main checkout. Choose a build command compatible with those links (for example, an installed Next.js version may provide `next build --webpack`); verify the available option rather than assuming it. Do not plan for a verifier to move links or repair its environment. Compare negative-case assertions with the brief and the script body: a requirement to cache existing assets does not establish that missing assets should be cached. Record any changed expectation in a new plan revision; preserve the earlier approved plan.
|
|
6
|
+
|
|
3
7
|
```yaml
|
|
4
8
|
roles:
|
|
5
9
|
- role: planner
|
|
@@ -185,7 +189,7 @@ roles:
|
|
|
185
189
|
Layout split (the implementer writes these, not this phase): executable scripts (conformance + any real-IO test) live under `<task_root>/qa/scripts/`; data sidecars (`conformance-manifest.json`, `result-*.json`) stay at the `qa/` root. This declaration is enforced at four layers: `validators/validate-implementation-plan-stages.py` check **S11** forces every stage to carry one of the two lines; at the planning boundary `validators/validate-run.py` `_validate_planning_conformance_declared` accepts a well-formed `Conformance tests:` line even when the script file and manifest entry are absent (malformed `requires` still fails); the matching `implementation` stage run that inherited `Conformance tests:` fails closed when the script file is missing (`_validate_conformance`); and the manifest JSON structure — including each entry's `script` living under `qa/scripts/` and a `runCommand` that does not change cwd — is enforced by `validate_conformance_manifest` when the implementer writes the entry.
|
|
186
190
|
- `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
|
|
187
191
|
- `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
|
|
188
|
-
- **Run-executable only (BLOCKING).** A `validationChecklist` row that carries `stageRefs` gates that stage's carry, so it MUST be executable by the implementation run itself — no deployment, no manual walk-through, no observation "by hand", no credentials the run does not hold. The implementation phase forbids deploys and holds no deploy credentials, so such a row can never pass and blocks a stage with zero code defects (observed: dev-10341 VC-013/VC-014). Manual or deployed-environment verification belongs in the brief's `External Gates` and final-verification's user-owned external QA — record it there without `stageRefs`. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`
|
|
192
|
+
- **Run-executable only (BLOCKING).** A `validationChecklist` row that carries `stageRefs` gates that stage's carry, so it MUST be executable by the implementation run itself — no deployment, no manual walk-through, no observation "by hand", no credentials the run does not hold. The implementation phase forbids deploys and holds no deploy credentials, so such a row can never pass and blocks a stage with zero code defects (observed: dev-10341 VC-013/VC-014). Manual or deployed-environment verification belongs in the brief's `External Gates` and final-verification's user-owned external QA — record it there without `stageRefs`. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`stage_validation_executability_errors`) rejects stage-gating rows whose observation text requires a manual or deployed-environment step.
|
|
189
193
|
- **Dependency precondition (stages that run the project toolchain).** The planning worktree is created without installed dependencies, so a stage whose steps call `npm` / `yarn` / `pytest` / `cargo` / equivalent cannot have those commands succeed at plan time — they exit `127`, not RED/GREEN. Declare the install **once** as a `phase: pre` row in `### Validation Checklist` (e.g. `VC-008 — the implementation run's stage worktree has workspace dependencies installed`) and have every such stage's `Stage Validation` cite that `VC-NNN` in its `pre:` line. Do not repeat the install commands per stage, and do not silently assume the tooling is present: a plan that never states the precondition produces steps whose commands never resolve, which the §5.5.9 round then reports as unverifiable. **Enforced (advisory):** `validators/validate-run.py` `_detect_missing_dependency_precondition` warns when a toolchain-invoking stage cites no `VC-NNN`, or cites one that is not `phase: pre`. Whether the cited row genuinely covers dependencies is a §5.5.9 judgement, not a machine check. Detection uses the token allowlist in `scripts/okstra_ctl/build_tools.py`; a project overrides it with `buildToolTokens` in `.okstra/project.json`.
|
|
190
194
|
- **Vertical-slice-first partition rule (1st-class):** the grouping anchor is a **thin end-to-end vertical slice** — one stage delivers a single user-observable increment, crossing whatever layers are needed (data → service → API → UI) to make that one increment work. File/module proximity is demoted to the **intra-slice grouping rule**: within a slice, keep steps touching the same file/directory/module together so the diff, PR, and rollback unit stay cohesive. **Horizontal layer-splitting is forbidden** — never carve "the DB layer" into one stage and "the service layer" into the next; that produces stages that ship no standalone user value. A stage is split ONLY when (a) a real `depends-on` data/contract dependency exists, (b) effective steps would exceed 8, or (c) it is a distinct vertical slice (a different user-value increment). Maximising the number of parallel stages is NOT a reason to split — parallelism is an emergent property of independent stages, never a partitioning goal. **Config propagation is not a slice (BLOCKING):** a dependent stage whose planned paths are all configuration/CI files (`Dockerfile`, `docker-compose*`, `*.yml` / `*.yaml`, `.env*`) merely propagates a value another stage introduced — it ships no user-value increment of its own. Fold that work into the introducing stage as its own step(s) and separate it by **commits**, never by stages: each extra stage costs a full implementation run (executor + verifiers, 30min+ observed) for a few config lines. A first stage with no dependency is exempt — a task whose whole scope is configuration is legitimate. **Enforced:** `okstra_ctl.implementation_direction.validate_selected_direction_plan` (`_micro_stage_fold_errors`).
|
|
191
195
|
- **Project-boundary partition rule (hard boundary):** a *project* boundary is either (a) a different repository / `PROJECT_ROOT`, or (b) a different top-level independently-deployable module within one repo. A stage maps to a single worktree on one repo/branch, so **no stage may contain edits belonging to more than one project** — this is a hard split that overrides the ≤8-step merging allowance; never co-locate two projects' changes in one stage to save a stage. Two cases:
|
|
@@ -178,6 +178,14 @@ Display the question only through the selected tool. Do not print it or its opti
|
|
|
178
178
|
|
|
179
179
|
## Codex dispatch details
|
|
180
180
|
|
|
181
|
+
### Permission at the dispatch boundary
|
|
182
|
+
|
|
183
|
+
Before a live CLI worker dispatch, check the current host's declared sandbox and approval policy. In a restricted Codex session, request the host's supported execution permission on the dispatch command itself (`sandbox_permissions: "require_escalated"` for `exec_command` when that mechanism is available and permitted). Scope the request to the prepared run and explain that it starts the selected worker processes. Follow the host's approval decision; a wizard confirmation or a successful preflight does not grant this permission.
|
|
184
|
+
|
|
185
|
+
Apply this guidance to both `okstra worker-dispatch` and the cmux override's `okstra team dispatch`, including initial workers, reverify, critic, report-writer, and retries. Read-only previews do not start workers. A session already authorized to execute outside the sandbox does not need another request. When the host prohibits escalation, preserve the pending invocation and report the execution constraint instead of trying an alternate launcher or weakening host controls.
|
|
186
|
+
|
|
187
|
+
The worker's `--sandbox danger-full-access` flag only selects the child Codex policy; it does not remove restrictions inherited from the parent process. If startup reports `Operation not permitted`, retain the exact error and distinguish initialization failure from a worker verdict. Retry only through the permitted host mechanism after the execution conditions change, within the existing retry limit; do not repeat the same restricted invocation. This is host-call guidance, not a permission grant or a runtime-enforced check.
|
|
188
|
+
|
|
181
189
|
- For convergence reverify, consume the persisted round plan exactly. This adapter may map and transport each returned batch, but it cannot change batch membership and does not classify findings or branch on task type, provider, or model identity.
|
|
182
190
|
- Do not invoke Claude Code team or subagent tools.
|
|
183
191
|
- The prepared run manifest and team-state are the dispatch authority. A `runner=native-session` assignment stays in the current Codex host; a `runner=cli-wrapper` assignment uses the registered provider wrapper. Unsupported explicitly requested workers fail; an adapter must not silently change the roster.
|
|
@@ -28,6 +28,7 @@ from ...convergence_store import (
|
|
|
28
28
|
from ...worker_prompt_policy import (
|
|
29
29
|
CRITIC_VERIFY_DISPATCH_KIND,
|
|
30
30
|
PLAN_VERIFY_DISPATCH_KIND_PREFIX,
|
|
31
|
+
is_plan_critic_verification,
|
|
31
32
|
verification_dispatch_round,
|
|
32
33
|
)
|
|
33
34
|
from .inputs import AgentPromptCliError
|
|
@@ -81,7 +82,10 @@ def _dynamic_verifier_source(
|
|
|
81
82
|
and manifest.get("executionIdentityVersion") == 2
|
|
82
83
|
)
|
|
83
84
|
is_reverify = (
|
|
84
|
-
args.assignment_ref.startswith("reverify/")
|
|
85
|
+
(args.assignment_ref.startswith("reverify/") or is_plan_critic_verification(
|
|
86
|
+
task_type=str(manifest.get("taskType", "")),
|
|
87
|
+
assignment_ref=args.assignment_ref, dispatch_kind=args.dispatch_kind,
|
|
88
|
+
))
|
|
85
89
|
and args.audience == "reverification-worker"
|
|
86
90
|
)
|
|
87
91
|
source = args.source_role_execution_ref
|
|
@@ -10,6 +10,7 @@ from typing import Any, Mapping
|
|
|
10
10
|
|
|
11
11
|
from ...worker_prompt_policy import (
|
|
12
12
|
CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT,
|
|
13
|
+
is_plan_critic_verification,
|
|
13
14
|
resolve_prompt_plan_for_manifest,
|
|
14
15
|
)
|
|
15
16
|
from .inputs import AgentPromptCliError
|
|
@@ -35,7 +36,12 @@ def _validate_run_identity(
|
|
|
35
36
|
elif assignment_ref.startswith("critic/"):
|
|
36
37
|
scope = assignment_ref.split("/", 1)[1]
|
|
37
38
|
expected = CRITIC_DUTY_BY_ASSIGNMENT_SEGMENT.get(scope, "")
|
|
38
|
-
if
|
|
39
|
+
if is_plan_critic_verification(
|
|
40
|
+
task_type=str(manifest.get("taskType", "")),
|
|
41
|
+
assignment_ref=assignment_ref, dispatch_kind=dispatch_kind,
|
|
42
|
+
):
|
|
43
|
+
expected = "reverification-worker"
|
|
44
|
+
elif not expected or dispatch_kind != "critic":
|
|
39
45
|
raise AgentPromptCliError("critic assignment identity is invalid")
|
|
40
46
|
# provider 대조는 여기 두지 않는다. critic 의 `worker_id` 는 배정 참조의
|
|
41
47
|
# 마지막 마디(`scope` / `acceptance`)이고 provider 이름이 아니다
|
|
@@ -61,6 +61,13 @@ def normalize_conformance_script(
|
|
|
61
61
|
value = script[len(prefix):] if script.startswith(prefix) else script
|
|
62
62
|
while value.startswith("./"):
|
|
63
63
|
value = value[2:]
|
|
64
|
+
if task_root is not None and value.startswith(".okstra/"):
|
|
65
|
+
for parent in task_root.resolve().parents:
|
|
66
|
+
if parent.name == ".okstra" and task_root.resolve().is_relative_to(
|
|
67
|
+
parent / "tasks"
|
|
68
|
+
):
|
|
69
|
+
value = str((parent.parent / value).resolve())
|
|
70
|
+
break
|
|
64
71
|
if task_root is not None and value.startswith("/"):
|
|
65
72
|
try:
|
|
66
73
|
return Path(value).resolve().relative_to(task_root.resolve()).as_posix()
|
|
@@ -17,6 +17,7 @@ from .json_boundary import JsonBoundaryError, load_owned_object, write_owned_obj
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
DYNAMIC_VERIFIER_SOURCE_ROLES = frozenset({
|
|
20
|
+
"critic",
|
|
20
21
|
"analyser",
|
|
21
22
|
"designer",
|
|
22
23
|
"planner",
|
|
@@ -163,12 +164,14 @@ def reserve_dynamic_verifier(
|
|
|
163
164
|
manifest = read_execution_manifest(manifest_path)
|
|
164
165
|
if manifest.legacy:
|
|
165
166
|
raise ExecutionManifestError("cannot reserve a dynamic verifier in v1 data")
|
|
167
|
+
authority = load_owned_json_object(manifest_path)
|
|
166
168
|
_validate_dynamic_verifier_request(
|
|
167
169
|
manifest.role_executions,
|
|
168
170
|
source_role_execution_ref,
|
|
169
171
|
duty_id,
|
|
172
|
+
task_type=str(authority.get("taskType", "")),
|
|
173
|
+
dispatch_kind=dispatch_kind or f"reverify-r{round_number}",
|
|
170
174
|
)
|
|
171
|
-
authority = load_owned_json_object(manifest_path)
|
|
172
175
|
if authority.get("taskKey") not in (None, task_key):
|
|
173
176
|
raise ExecutionManifestError("dynamic verifier taskKey does not match manifest")
|
|
174
177
|
project_root = _manifest_project_root(authority, manifest_path)
|
|
@@ -219,7 +222,12 @@ def _validate_dynamic_verifier_request(
|
|
|
219
222
|
roles: tuple[RoleExecution, ...],
|
|
220
223
|
source_role_execution_ref: str,
|
|
221
224
|
duty_id: str,
|
|
225
|
+
*,
|
|
226
|
+
task_type: str,
|
|
227
|
+
dispatch_kind: str,
|
|
222
228
|
) -> None:
|
|
229
|
+
from .worker_prompt_policy import is_plan_verify_dispatch_kind
|
|
230
|
+
|
|
223
231
|
try:
|
|
224
232
|
duty_role = role_for_duty(duty_id)
|
|
225
233
|
except RoleCatalogError as exc:
|
|
@@ -238,6 +246,13 @@ def _validate_dynamic_verifier_request(
|
|
|
238
246
|
raise ExecutionManifestError(
|
|
239
247
|
f"dynamic verifier source role is not eligible: {source.role}"
|
|
240
248
|
)
|
|
249
|
+
if source is not None and source.role == "critic" and not (
|
|
250
|
+
task_type == "implementation-planning"
|
|
251
|
+
and is_plan_verify_dispatch_kind(dispatch_kind)
|
|
252
|
+
):
|
|
253
|
+
raise ExecutionManifestError(
|
|
254
|
+
"critic source role is eligible only for planning verification"
|
|
255
|
+
)
|
|
241
256
|
|
|
242
257
|
|
|
243
258
|
def _manifest_project_root(
|
|
@@ -31,7 +31,7 @@ def parse_records(paths: list[Path]) -> tuple[list[dict], int]:
|
|
|
31
31
|
except (OSError, UnicodeDecodeError):
|
|
32
32
|
skipped += 1
|
|
33
33
|
continue
|
|
34
|
-
for line in text.splitlines():
|
|
34
|
+
for line_number, line in enumerate(text.splitlines(), 1):
|
|
35
35
|
line = line.strip()
|
|
36
36
|
if not line:
|
|
37
37
|
continue
|
|
@@ -42,6 +42,7 @@ def parse_records(paths: list[Path]) -> tuple[list[dict], int]:
|
|
|
42
42
|
continue
|
|
43
43
|
if isinstance(rec, dict):
|
|
44
44
|
rec["_sourceLog"] = str(path)
|
|
45
|
+
rec["_sourceLine"] = line_number
|
|
45
46
|
records.append(rec)
|
|
46
47
|
else:
|
|
47
48
|
skipped += 1
|
|
@@ -100,12 +100,31 @@ def prior_run_error_digest(
|
|
|
100
100
|
)
|
|
101
101
|
],
|
|
102
102
|
)
|
|
103
|
+
examples: dict[tuple[str, str], str] = {}
|
|
104
|
+
for record in carried:
|
|
105
|
+
message = str(record.get("message") or "")[:1200]
|
|
106
|
+
command = str(record.get("command") or "")[:800]
|
|
107
|
+
if message or command:
|
|
108
|
+
examples[(message, command)] = (
|
|
109
|
+
f"{record['_sourceLog']}:{record['_sourceLine']}"
|
|
110
|
+
)
|
|
111
|
+
details = (
|
|
112
|
+
_md_table(
|
|
113
|
+
["Message", "Command", "Source"],
|
|
114
|
+
[
|
|
115
|
+
[message, command, source]
|
|
116
|
+
for (message, command), source in list(examples.items())[-20:]
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
if examples
|
|
120
|
+
else ""
|
|
121
|
+
)
|
|
103
122
|
return (
|
|
104
123
|
"# Prior-Run Errors\n\n"
|
|
105
124
|
"`contract-violation` and `tool-failure` records this task's earlier "
|
|
106
125
|
"runs wrote. These are not findings about the work — they are traps "
|
|
107
126
|
"that fired before and can fire again in this run.\n\n"
|
|
108
|
-
f"{table}\n"
|
|
127
|
+
f"{table}\n\n{details}\n"
|
|
109
128
|
)
|
|
110
129
|
|
|
111
130
|
|
|
@@ -31,6 +31,7 @@ from .json_boundary import (
|
|
|
31
31
|
write_owned_object_atomic,
|
|
32
32
|
)
|
|
33
33
|
from .report_views import normalize_direction_selection_identity
|
|
34
|
+
from .qa_commands import find_denied_tokens
|
|
34
35
|
from .scope_provenance import brief_end_state_id_sequence
|
|
35
36
|
from .user_response import UserResponseError, parse_direction_selection
|
|
36
37
|
|
|
@@ -814,14 +815,31 @@ _MANUAL_VALIDATION_MARKERS = (
|
|
|
814
815
|
)
|
|
815
816
|
|
|
816
817
|
|
|
817
|
-
def
|
|
818
|
+
def stage_validation_executability_errors(
|
|
818
819
|
planning: Mapping[str, Any],
|
|
819
820
|
) -> list[str]:
|
|
820
821
|
failures: list[str] = []
|
|
821
822
|
for row in planning.get("validationChecklist") or ():
|
|
822
|
-
if not isinstance(row, Mapping)
|
|
823
|
+
if not isinstance(row, Mapping):
|
|
823
824
|
continue
|
|
824
|
-
|
|
825
|
+
command = str(row.get("commandOrObservation") or "")
|
|
826
|
+
denied = find_denied_tokens(command)
|
|
827
|
+
if re.search(r"(?<![\w/])\.?/?\.okstra/tasks/", command):
|
|
828
|
+
denied.append(
|
|
829
|
+
"project-relative QA path (use the absolute task artifact path; keep worktree cwd)"
|
|
830
|
+
)
|
|
831
|
+
if re.search(r">\s*/(?:tmp|private/tmp|var/tmp)/", command):
|
|
832
|
+
denied.append(
|
|
833
|
+
"output outside the worktree (use a worktree-local output path)"
|
|
834
|
+
)
|
|
835
|
+
if denied:
|
|
836
|
+
failures.append(
|
|
837
|
+
f"validationChecklist {row.get('id')} conflicts with verifier command rules: "
|
|
838
|
+
f"{'; '.join(denied)}; correct and approve the plan before implementation"
|
|
839
|
+
)
|
|
840
|
+
if not row.get("stageRefs"):
|
|
841
|
+
continue
|
|
842
|
+
lowered = command.lower()
|
|
825
843
|
matched = [
|
|
826
844
|
marker for marker in _MANUAL_VALIDATION_MARKERS if marker in lowered
|
|
827
845
|
]
|
|
@@ -947,6 +965,6 @@ def validate_selected_direction_plan(
|
|
|
947
965
|
planning, brief_end_state_id_sequence(Path(brief_path))
|
|
948
966
|
)
|
|
949
967
|
)
|
|
950
|
-
failures.extend(
|
|
968
|
+
failures.extend(stage_validation_executability_errors(planning))
|
|
951
969
|
failures.extend(_micro_stage_fold_errors(planning))
|
|
952
970
|
return failures
|
|
@@ -469,6 +469,18 @@ def _label(key: str) -> str:
|
|
|
469
469
|
return " ".join(part.capitalize() for part in key.replace("Id", " ID").split())
|
|
470
470
|
|
|
471
471
|
|
|
472
|
+
def _render_literal(label: str, value: str, indent: str = "") -> list[str]:
|
|
473
|
+
"""명령·코드의 줄바꿈을 보존하고 본문보다 긴 울타리로 감싼다."""
|
|
474
|
+
fence = "`" * max(
|
|
475
|
+
3, 1 + max((len(part) for part in re.findall(r"`+", value)), default=0)
|
|
476
|
+
)
|
|
477
|
+
body = "".join(indent + " " + row for row in value.splitlines(keepends=True))
|
|
478
|
+
ending = "" if value.endswith("\n") else "\n"
|
|
479
|
+
return [
|
|
480
|
+
f"{indent}- {label}:\n\n{indent} {fence}text\n{body}{ending}{indent} {fence}\n"
|
|
481
|
+
]
|
|
482
|
+
|
|
483
|
+
|
|
472
484
|
def _render_object_list(key: str, value: object) -> list[str]:
|
|
473
485
|
if not isinstance(value, list):
|
|
474
486
|
raise PlanItemContractError(f"plan item {key} must be an array")
|
|
@@ -491,6 +503,8 @@ def _render_object_list(key: str, value: object) -> list[str]:
|
|
|
491
503
|
raise PlanItemContractError(
|
|
492
504
|
f"plan item {field_path} must be scalar"
|
|
493
505
|
)
|
|
506
|
+
elif field == "details" and isinstance(field_value, str):
|
|
507
|
+
rows.extend(_render_literal(f"`{field_path}`", field_value, " "))
|
|
494
508
|
else:
|
|
495
509
|
rows.append(
|
|
496
510
|
f" - `{field_path}`: `{scalar(field_value)}`\n"
|
|
@@ -542,6 +556,8 @@ def _render_scalar_or_list(key: str, value: object, indent: str = "") -> list[st
|
|
|
542
556
|
return rows
|
|
543
557
|
if isinstance(value, Mapping):
|
|
544
558
|
raise PlanItemContractError(f"plan item {key} must be scalar")
|
|
559
|
+
if key in {"command", "commandOrObservation"} and isinstance(value, str):
|
|
560
|
+
return _render_literal(_label(key), value, indent)
|
|
545
561
|
return [indent + line(_label(key), value)]
|
|
546
562
|
|
|
547
563
|
|
|
@@ -72,6 +72,31 @@ def _has_insta_update_set(cmd: str) -> bool:
|
|
|
72
72
|
return match.group(1).lower() != "no"
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def find_unfrozen_installs(cmd: str) -> list[str]:
|
|
76
|
+
"""계획과 실행 기록에서 같은 잠금파일 고정 조건을 판정한다."""
|
|
77
|
+
found: list[str] = []
|
|
78
|
+
if _has_npm_install_without_ci(cmd):
|
|
79
|
+
found.append("npm install (use 'npm ci' instead)")
|
|
80
|
+
for match in re.finditer(r"\b(pnpm|yarn|bun)\s+(?:install|i)\b([^;&|\n]*)", cmd):
|
|
81
|
+
flags = match.group(2).split()
|
|
82
|
+
frozen = "--frozen-lockfile" in flags or (
|
|
83
|
+
match.group(1) == "yarn" and "--immutable" in flags
|
|
84
|
+
)
|
|
85
|
+
disabled = any(
|
|
86
|
+
flag
|
|
87
|
+
in {
|
|
88
|
+
"--no-frozen-lockfile",
|
|
89
|
+
"--frozen-lockfile=false",
|
|
90
|
+
"--no-immutable",
|
|
91
|
+
"--immutable=false",
|
|
92
|
+
}
|
|
93
|
+
for flag in flags
|
|
94
|
+
)
|
|
95
|
+
if not frozen or disabled:
|
|
96
|
+
found.append(f"{match.group(1)} install (requires a frozen lockfile)")
|
|
97
|
+
return found
|
|
98
|
+
|
|
99
|
+
|
|
75
100
|
def find_denied_tokens(cmd: str) -> list[str]:
|
|
76
101
|
"""`cmd` 안에 포함된 모든 denied 토큰 목록을 반환. 비어 있으면 안전."""
|
|
77
102
|
if not isinstance(cmd, str):
|
|
@@ -84,8 +109,7 @@ def find_denied_tokens(cmd: str) -> list[str]:
|
|
|
84
109
|
for sub in _DENIED_SUBSTRINGS:
|
|
85
110
|
if sub in cmd:
|
|
86
111
|
found.append(sub)
|
|
87
|
-
|
|
88
|
-
found.append("npm install (use 'npm ci' instead)")
|
|
112
|
+
found.extend(find_unfrozen_installs(cmd))
|
|
89
113
|
if _has_insta_update_set(cmd):
|
|
90
114
|
found.append("INSTA_UPDATE=<not-no>")
|
|
91
115
|
return found
|
|
@@ -93,6 +93,7 @@ STEP_SPAWN_FOLLOWUPS = "spawn-followups"
|
|
|
93
93
|
STEP_VALIDATE_RUN = "validate-run"
|
|
94
94
|
STEP_RECORD_GROUP_MEMORY = "record-group-memory"
|
|
95
95
|
STEP_TEARDOWN_STAGES = "teardown-stages"
|
|
96
|
+
STEP_PREFLIGHT = "preflight"
|
|
96
97
|
|
|
97
98
|
STEP_ORDER = (
|
|
98
99
|
STEP_PROJECT_ACTIVITY,
|
|
@@ -112,6 +113,7 @@ STEP_ORDER = (
|
|
|
112
113
|
V3_STEP_ORDER = (
|
|
113
114
|
STEP_TOKEN_USAGE,
|
|
114
115
|
STEP_PROJECT_ACTIVITY,
|
|
116
|
+
STEP_PREFLIGHT,
|
|
115
117
|
# Before the render that overlays its sidecar.
|
|
116
118
|
STEP_TRANSLATE,
|
|
117
119
|
STEP_RENDER_VIEWS,
|
|
@@ -401,6 +403,11 @@ def build_commands(ctx: FinalizeContext) -> list[tuple[str, list[str]]]:
|
|
|
401
403
|
if ctx.report_contract_version != "3.0":
|
|
402
404
|
return commands
|
|
403
405
|
by_name = dict(commands)
|
|
406
|
+
by_name[STEP_PREFLIGHT] = [
|
|
407
|
+
*_validate_run_command(ctx, ctx.data_path),
|
|
408
|
+
"--section",
|
|
409
|
+
"preflight",
|
|
410
|
+
]
|
|
404
411
|
usage = by_name[STEP_TOKEN_USAGE]
|
|
405
412
|
marker = usage.index("--substitute-data")
|
|
406
413
|
by_name[STEP_TOKEN_USAGE] = usage[:marker]
|
|
@@ -585,6 +592,8 @@ def run_finalize(
|
|
|
585
592
|
|
|
586
593
|
if only:
|
|
587
594
|
selected = set(only)
|
|
595
|
+
if ctx.report_contract_version == "3.0" and STEP_TRANSLATE in selected:
|
|
596
|
+
selected.add(STEP_PREFLIGHT)
|
|
588
597
|
contract_order = (
|
|
589
598
|
V3_STEP_ORDER if ctx.report_contract_version == "3.0" else STEP_ORDER
|
|
590
599
|
)
|
|
@@ -603,15 +612,20 @@ def run_finalize(
|
|
|
603
612
|
|
|
604
613
|
first_failure = ""
|
|
605
614
|
validated = False
|
|
615
|
+
preflight_failed = False
|
|
606
616
|
for name, command in commands:
|
|
607
617
|
# 실패한 시퀀스가 worktree 를 거두면 재작업 대상이 사라지고, 검증 안 된
|
|
608
618
|
# 레코드를 기억하면 형제 task 가 그것을 결론으로 읽는다.
|
|
609
619
|
if name in _SKIPPED_AFTER_FAILURE and first_failure:
|
|
610
620
|
continue
|
|
621
|
+
if name == STEP_TRANSLATE and preflight_failed:
|
|
622
|
+
continue
|
|
611
623
|
if before_step is not None:
|
|
612
624
|
before_step(name)
|
|
613
625
|
result = _run_finalize_step(ctx, name, command)
|
|
614
626
|
steps.append(step_payload(name, command, result))
|
|
627
|
+
if name == STEP_PREFLIGHT:
|
|
628
|
+
preflight_failed = result.returncode != 0
|
|
615
629
|
if result.returncode != 0 and not first_failure:
|
|
616
630
|
first_failure = f"{name} failed with exit code {result.returncode}"
|
|
617
631
|
if name == STEP_VALIDATE_RUN and result.returncode == 0:
|
|
@@ -1082,6 +1096,12 @@ def closeout_command(result: Mapping[str, Any]) -> dict[str, str]:
|
|
|
1082
1096
|
if isinstance(step, Mapping)
|
|
1083
1097
|
)
|
|
1084
1098
|
if failed_validate:
|
|
1099
|
+
recovery = promote_next_phase(result.get("nextRecommendedPhase"))
|
|
1100
|
+
if recovery["status"] == STATUS_BLOCKED and recovery["phase"]:
|
|
1101
|
+
return {
|
|
1102
|
+
"command": f"/okstra-run → {recovery['phase']}",
|
|
1103
|
+
"note": recovery["rationale"],
|
|
1104
|
+
}
|
|
1085
1105
|
return {
|
|
1086
1106
|
"command": "/okstra-run",
|
|
1087
1107
|
"note": "validate-run failed — name the blocking cause in one line, "
|
|
@@ -394,6 +394,16 @@ def _validate_approved_plan_conformance(path: Path) -> None:
|
|
|
394
394
|
if loaded is None:
|
|
395
395
|
return
|
|
396
396
|
data_path, data = loaded
|
|
397
|
+
from .implementation_direction import stage_validation_executability_errors
|
|
398
|
+
|
|
399
|
+
command_errors = stage_validation_executability_errors(
|
|
400
|
+
data.get("implementationPlanning") or {}
|
|
401
|
+
)
|
|
402
|
+
if command_errors:
|
|
403
|
+
raise PrepareError(
|
|
404
|
+
f"approved plan command preflight failed: {data_path}\n"
|
|
405
|
+
+ "\n".join(command_errors)
|
|
406
|
+
)
|
|
397
407
|
bad = malformed_conformance_stages(data)
|
|
398
408
|
if not bad:
|
|
399
409
|
return
|
|
@@ -391,6 +391,19 @@ def complete_reverify_instruction(
|
|
|
391
391
|
if not body.lstrip().startswith(("## ", "**")):
|
|
392
392
|
body = "## Instructions\n\n" + body
|
|
393
393
|
body = "\n".join(prefix) + "\n\n" + body
|
|
394
|
+
if task_type == "implementation-planning":
|
|
395
|
+
# 저장된 금지 목록은 감사 대조용으로 유지하되 폐기된 작성 의무를 정정한다.
|
|
396
|
+
body = body.rstrip() + (
|
|
397
|
+
"\n\n## Planning conformance ownership\n\n"
|
|
398
|
+
"Planning declares conformance commands and required dependencies; "
|
|
399
|
+
"implementation writes the QA scripts, manifest, and tsconfig. "
|
|
400
|
+
"Do not create those files during planning. Their absence before "
|
|
401
|
+
"implementation is not a planning defect by itself. This phase "
|
|
402
|
+
"ownership supersedes any legacy requirement in the frozen Forbidden "
|
|
403
|
+
"actions block saying this phase MUST write those artifacts. "
|
|
404
|
+
"Verify that the plan assigns their creation to implementation and "
|
|
405
|
+
"provides executable commands and required dependencies.\n"
|
|
406
|
+
)
|
|
394
407
|
if _OUTPUT_CONTRACT_HEADING not in body:
|
|
395
408
|
body = body.rstrip() + "\n\n" + output_contract.strip() + "\n"
|
|
396
409
|
errors = _validate_output_contract_block(body)
|
|
@@ -121,6 +121,16 @@ def is_plan_verify_dispatch_kind(dispatch_kind: str) -> bool:
|
|
|
121
121
|
return _numbered_round(dispatch_kind, PLAN_VERIFY_DISPATCH_KIND_PREFIX) is not None
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def is_plan_critic_verification(
|
|
125
|
+
*, task_type: str, assignment_ref: str, dispatch_kind: str,
|
|
126
|
+
) -> bool:
|
|
127
|
+
return (
|
|
128
|
+
task_type == "implementation-planning"
|
|
129
|
+
and assignment_ref == "critic/scope"
|
|
130
|
+
and is_plan_verify_dispatch_kind(dispatch_kind)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
124
134
|
def verification_dispatch_round(dispatch_kind: str) -> int | None:
|
|
125
135
|
"""검증 kind 가 적는 라운드 번호. 번호 없는 kind(`critic-verify`)는 1,
|
|
126
136
|
검증 kind 가 아니거나 번호가 깨졌으면 None.
|
|
@@ -28,19 +28,21 @@ Single read-side entry point for okstra runtime inspection plus the one status m
|
|
|
28
28
|
|
|
29
29
|
## Step 0: Preflight (shared)
|
|
30
30
|
|
|
31
|
+
Resolve `<host-runtime>` from the launcher's `OKSTRA_RUNTIME_HOST` when present; otherwise use the registered host ID declared by the current harness (`codex` in Codex, `claude-code` in Claude Code). This follows `okstra-run`'s host selection rule. Do not infer the host from worker models, installed executables, or `PATH`. If neither source identifies the host, report that it is unknown instead of substituting Claude Code. Keep the same resolved host when retrying against another project directory.
|
|
32
|
+
|
|
31
33
|
<!-- BEGIN FRAGMENT: bash-invocation-rule -->
|
|
32
34
|
Run one Bash tool call, starting with the literal token `okstra` (never wrapped in `if`/`eval`/`export`/`$(...)`/`VAR=...`/`||`/`&&`/`npx` — a non-literal leading token defeats the `Bash(okstra:*)` permission match):
|
|
33
35
|
<!-- END FRAGMENT: bash-invocation-rule -->
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
okstra preflight --runtime
|
|
38
|
+
okstra preflight --runtime <host-runtime>
|
|
37
39
|
```
|
|
38
40
|
|
|
39
41
|
The project check only sees the cwd of the Bash call. When the user is asking about a project that is **not** the cwd (a sibling repo, a monorepo subdir, or a project named explicitly in the request), the bare form can report `Okstra preflight: failed` — a false negative, not a missing setup; do not hard-stop on it.
|
|
40
42
|
|
|
41
43
|
Branch on the fixed first line:
|
|
42
44
|
- `Okstra preflight: ready` → carry `Project root` as a literal string; it is the base for every sub-command step below.
|
|
43
|
-
- `Okstra preflight: failed` → before concluding "no setup", ask whether the user pointed at a specific project directory. If they did, re-run targeting it: `okstra preflight --runtime
|
|
45
|
+
- `Okstra preflight: failed` → before concluding "no setup", ask whether the user pointed at a specific project directory. If they did, re-run targeting it: `okstra preflight --runtime <host-runtime> --cwd <that-dir>` (`--cwd` is the sanctioned way to target a project — a leading `cd` would break the permission match). Only if this also fails do you show `Reason` and `Recovery`, then stop.
|
|
44
46
|
|
|
45
47
|
<!-- BEGIN FRAGMENT: preflight-outdated-cli -->
|
|
46
48
|
If the call fails with `unknown command: preflight`, the `okstra` binary on PATH predates this skill — tell the user to update it (`npm i -g okstra@latest`), then stop (`/okstra-setup` does not update the binary).
|
|
@@ -475,4 +475,4 @@ Do not read the wizard state file directly. `okstra wizard outcome` exposes any
|
|
|
475
475
|
- Echo each captured answer (`result.echo`) on one short line so the user sees what was registered.
|
|
476
476
|
- Name every file you show the user as a markdown link — `[<what it is>](<path>)`, with the path inside the parentheses. That is the only form the host renders as clickable; a path in backticks is text the user has to copy out. The `report-finalize` result's `reportPaths.markdown` carries the run's report, report record, and team state already in that form. Commands stay in backticks — a link is for a file, not for something to run.
|
|
477
477
|
- Never invent identity; if a `text` prompt returns an empty answer where the wizard rejects it, the user must retry.
|
|
478
|
-
- After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. When the lead (or this skill, after the lead returns) reports the run over, close with the user's next action — one command they can run now. A prohibition is not a next action. Take the pointer from the `report-finalize` result's top-level `nextRecommendedPhase` (`phase`, `status`, `rationale`; also on stderr as `next phase status:` / `next phase:` / `next phase rationale:`) — do not re-derive it from the report, and treat a `nextRecommendedPhaseError` as "pointer unreadable", said in one line before the `validate-run` branch. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead. After `implementation-planning`, open `blocks: approval` rows → `/okstra-user-response`. A recorded `accept-risk` / `select` / `answer` is not an open blocker. No open approval blocker → `/okstra-run` → `implementation` or `--approve` (do not start another planning run; do not say `/okstra-inspect`). For every other task type, quote the pointer's `rationale` in every branch — that sentence is the report's own reason and it is what the user asked to be analysed. Pointer `status: ready` → `/okstra-run` for that phase; `status: terminal` → say the task is finished, name any follow-up tasks this run registered, and do not say `/okstra-inspect`; `status: blocked` → issue the command the `rationale` calls for (`/okstra-user-response` for the `C-NNN` ids, `/okstra-run` for the phase it names); `validate-run` failed → one-line cause then `/okstra-run
|
|
478
|
+
- After Step 6, begin the lead workflow without re-summarizing the skill itself. For a single run, the end of Step 6 is the end of the run — but in an unattended chain where `orchestration.chainStages` has 2+ elements, repeat Step 6 per stage until Step 7's queue is empty (or it stops at a "not ready" / exception gate), then finish. When the lead (or this skill, after the lead returns) reports the run over, close with the user's next action — one command they can run now. A prohibition is not a next action. Take the pointer from the `report-finalize` result's top-level `nextRecommendedPhase` (`phase`, `status`, `rationale`; also on stderr as `next phase status:` / `next phase:` / `next phase rationale:`) — do not re-derive it from the report, and treat a `nextRecommendedPhaseError` as "pointer unreadable", said in one line before the `validate-run` branch. The same result also carries `nextCommand` — `{command, note}`, the table below already applied to this run. When `command` is non-empty it is the close; when it is empty the `note` says what to do with the `rationale` instead. After `implementation-planning`, open `blocks: approval` rows → `/okstra-user-response`. A recorded `accept-risk` / `select` / `answer` is not an open blocker. No open approval blocker → `/okstra-run` → `implementation` or `--approve` (do not start another planning run; do not say `/okstra-inspect`). For every other task type, quote the pointer's `rationale` in every branch — that sentence is the report's own reason and it is what the user asked to be analysed. Pointer `status: ready` → `/okstra-run` for that phase; `status: terminal` → say the task is finished, name any follow-up tasks this run registered, and do not say `/okstra-inspect`; `status: blocked` → issue the command the `rationale` calls for (`/okstra-user-response` for the `C-NNN` ids, `/okstra-run` for the phase it names); `validate-run` failed → one-line cause then `/okstra-run` for the blocked pointer's `phase` when present, otherwise the current phase; otherwise `/okstra-inspect status`.
|
|
@@ -108,11 +108,14 @@ def verifier_mutation_hits(command_log: str) -> list[tuple[str, str]]:
|
|
|
108
108
|
로그는 자유 문자열이라 줄 단위로 본다 — 어느 명령이 문제인지 그대로 보고해야
|
|
109
109
|
작성자가 고칠 자리를 안다. heredoc 본문은 기록되는 데이터이므로 걷어낸다.
|
|
110
110
|
"""
|
|
111
|
+
from okstra_ctl.qa_commands import find_unfrozen_installs
|
|
112
|
+
|
|
111
113
|
hits: list[tuple[str, str]] = []
|
|
112
114
|
for raw in strip_heredoc_bodies(command_log or "").splitlines():
|
|
113
115
|
line = raw.strip()
|
|
114
116
|
if not line or line.startswith("#"):
|
|
115
117
|
continue
|
|
118
|
+
hits.extend((label, line) for label in find_unfrozen_installs(line))
|
|
116
119
|
for label, pattern in (*_VERIFIER_MUTATION, *_VERIFIER_SOURCE_MUTATION):
|
|
117
120
|
if pattern.search(line):
|
|
118
121
|
hits.append((label, line))
|
|
@@ -902,11 +902,22 @@ def update_workflow_metadata(
|
|
|
902
902
|
if current_phase:
|
|
903
903
|
phase_states[current_phase] = current_phase_state
|
|
904
904
|
last_completed_phase = workflow.get("lastCompletedPhase", "")
|
|
905
|
+
recovery = next_phase.project(report_data or {})
|
|
906
|
+
recovery_phase = recovery["phase"]
|
|
907
|
+
can_backtrack = (
|
|
908
|
+
current_phase in PHASE_SEQUENCE
|
|
909
|
+
and recovery_phase in PHASE_SEQUENCE
|
|
910
|
+
and PHASE_SEQUENCE.index(recovery_phase)
|
|
911
|
+
< PHASE_SEQUENCE.index(current_phase)
|
|
912
|
+
)
|
|
905
913
|
next_recommended_phase = next_phase.make(
|
|
914
|
+
phase=recovery_phase if can_backtrack else "",
|
|
906
915
|
status=next_phase.STATUS_BLOCKED,
|
|
907
916
|
rationale=_blocked_rationale(
|
|
908
|
-
current_phase,
|
|
909
|
-
|
|
917
|
+
recovery_phase if can_backtrack else current_phase,
|
|
918
|
+
recovery["rationale"]
|
|
919
|
+
if can_backtrack
|
|
920
|
+
else inherited_pointer["rationale"],
|
|
910
921
|
failures,
|
|
911
922
|
run_manifest_path=str(run_manifest.get("runManifestPath") or ""),
|
|
912
923
|
),
|
|
@@ -2197,6 +2208,12 @@ def _validate_planning_conformance_declared(
|
|
|
2197
2208
|
if not isinstance(ip, dict):
|
|
2198
2209
|
return
|
|
2199
2210
|
_planning_conformance_declarations(ip.get("stages"), failures)
|
|
2211
|
+
if ip.get("planningContract") != "selected-direction":
|
|
2212
|
+
from okstra_ctl.implementation_direction import (
|
|
2213
|
+
stage_validation_executability_errors,
|
|
2214
|
+
)
|
|
2215
|
+
|
|
2216
|
+
failures.extend(stage_validation_executability_errors(ip))
|
|
2200
2217
|
for conflict in exempt_stage_surface_conflicts(data, surface_patterns):
|
|
2201
2218
|
failures.append(
|
|
2202
2219
|
"conformance gate BLOCKING: stage "
|
|
@@ -9363,13 +9380,62 @@ def _data_schema_failures(data: dict) -> list[str]:
|
|
|
9363
9380
|
]
|
|
9364
9381
|
|
|
9365
9382
|
|
|
9383
|
+
def run_preflight(report_path: Path, run_manifest_path: Path) -> int:
|
|
9384
|
+
"""번역·표시·상태 갱신 전에 정본의 구조와 적합성만 검사한다."""
|
|
9385
|
+
try:
|
|
9386
|
+
data = load_json(_data_path_for(report_path))
|
|
9387
|
+
manifest = load_json(run_manifest_path)
|
|
9388
|
+
if not isinstance(data, dict) or not isinstance(manifest, dict):
|
|
9389
|
+
raise TypeError("preflight requires report and run-manifest JSON objects")
|
|
9390
|
+
except (OSError, ValueError, TypeError) as exc:
|
|
9391
|
+
print(json.dumps({"ok": False, "failures": [str(exc)]}))
|
|
9392
|
+
return 2
|
|
9393
|
+
failures = _data_schema_failures(data)
|
|
9394
|
+
task_type = manifest.get("taskType")
|
|
9395
|
+
warnings: list[str] = []
|
|
9396
|
+
if task_type in ("implementation", "final-verification"):
|
|
9397
|
+
project_root = Path(
|
|
9398
|
+
str(manifest.get("projectRoot") or run_manifest_path.parent)
|
|
9399
|
+
)
|
|
9400
|
+
warnings = _validate_conformance(
|
|
9401
|
+
report_path,
|
|
9402
|
+
failures,
|
|
9403
|
+
surface_patterns=_project_surface_patterns(project_root),
|
|
9404
|
+
approved_plan_path=_approved_plan_path_from_run_inputs(
|
|
9405
|
+
run_manifest_path, failures
|
|
9406
|
+
),
|
|
9407
|
+
)
|
|
9408
|
+
elif task_type == "implementation-planning":
|
|
9409
|
+
from okstra_ctl.implementation_direction import (
|
|
9410
|
+
stage_validation_executability_errors,
|
|
9411
|
+
)
|
|
9412
|
+
|
|
9413
|
+
failures.extend(
|
|
9414
|
+
stage_validation_executability_errors(
|
|
9415
|
+
data.get("implementationPlanning") or {}
|
|
9416
|
+
)
|
|
9417
|
+
)
|
|
9418
|
+
_validate_planning_conformance_declared(report_path, failures)
|
|
9419
|
+
print(
|
|
9420
|
+
json.dumps(
|
|
9421
|
+
{
|
|
9422
|
+
"ok": not failures,
|
|
9423
|
+
"failures": list(dict.fromkeys(failures)),
|
|
9424
|
+
"warnings": warnings,
|
|
9425
|
+
},
|
|
9426
|
+
ensure_ascii=False,
|
|
9427
|
+
)
|
|
9428
|
+
)
|
|
9429
|
+
return 2 if failures else 0
|
|
9430
|
+
|
|
9431
|
+
|
|
9366
9432
|
def main() -> int:
|
|
9367
9433
|
parser = argparse.ArgumentParser(
|
|
9368
9434
|
description="Validate okstra run contract artifacts."
|
|
9369
9435
|
)
|
|
9370
9436
|
parser.add_argument(
|
|
9371
9437
|
"--section",
|
|
9372
|
-
choices=(SECTION_FULL, SECTION_PLAN_BODY),
|
|
9438
|
+
choices=(SECTION_FULL, SECTION_PLAN_BODY, "preflight"),
|
|
9373
9439
|
default=SECTION_FULL,
|
|
9374
9440
|
help=(
|
|
9375
9441
|
"Which contract surface to validate. `full` (default) validates the "
|
|
@@ -9415,6 +9481,13 @@ def main() -> int:
|
|
|
9415
9481
|
)
|
|
9416
9482
|
args = parser.parse_args()
|
|
9417
9483
|
|
|
9484
|
+
if args.section == "preflight":
|
|
9485
|
+
if not args.report or not args.run_manifest:
|
|
9486
|
+
parser.error("--section preflight requires --report and --run-manifest")
|
|
9487
|
+
return run_preflight(
|
|
9488
|
+
Path(args.report).resolve(), Path(args.run_manifest).resolve()
|
|
9489
|
+
)
|
|
9490
|
+
|
|
9418
9491
|
if args.section == SECTION_PLAN_BODY:
|
|
9419
9492
|
if args.narrative and args.state and not args.report:
|
|
9420
9493
|
return run_plan_body_inputs(
|
|
@@ -561,6 +561,11 @@ def _progress_line_from_event(event) -> tuple[str, str, str] | None:
|
|
|
561
561
|
message = details.get("message")
|
|
562
562
|
tail = f" {message}" if isinstance(message, str) and message else ""
|
|
563
563
|
line = f"PROGRESS: {phase}{tail}"
|
|
564
|
+
worker = details.get("worker")
|
|
565
|
+
if isinstance(worker, str) and worker.strip():
|
|
566
|
+
from okstra_ctl.lead_progress import render_progress_line
|
|
567
|
+
|
|
568
|
+
line = render_progress_line(phase, [("worker", worker), ("detail", line)])
|
|
564
569
|
return (event.timestamp, phase, line)
|
|
565
570
|
|
|
566
571
|
|