okstra 0.166.0 → 0.166.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okstra",
3
- "version": "0.166.0",
3
+ "version": "0.166.1",
4
4
  "description": "Host-aware multi-provider cross-verification orchestrator runtime and agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "devonshin",
@@ -1,5 +1,5 @@
1
1
  {
2
- "package": "0.166.0",
3
- "builtAt": "2026-08-11T10:29:58.327Z",
2
+ "package": "0.166.1",
3
+ "builtAt": "2026-08-11T12:28:22.915Z",
4
4
  "repoRoot": "/home/runner/work/okstra/okstra"
5
5
  }
@@ -132,7 +132,7 @@
132
132
  unavailable environment is a user-owned follow-up, never a plan approval or
133
133
  later run blocker. `requires=[]` and `requires=[io]` remain blocking.
134
134
  Remote IO should also declare `external`.
135
- The manifest lives at the **task level** (`<task_root>/qa/`, path token `TASK_QA_PATH`) and is shared across planning → implementation → final-verification. Layout split: 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` fails when a stage that declared `Conformance tests:` has no matching `-stage-<N>` entry in the shared manifest (a declaration that was never materialized); the manifest JSON structure — including each entry's `script` living under `qa/scripts/` — is enforced by `validate_conformance_manifest` (called from both the run path and validate-run); and the result policy is evaluated by `conformance.py` and `validate-run.py`.
135
+ The manifest lives at the **task level** (`<task_root>/qa/`, path token `TASK_QA_PATH`) and is shared across planning → implementation → final-verification. The verifier runs `runCommand` from the **worktree cwd**, and that cwd is the tree under test — it is what makes the script see this stage's diff. So `runCommand` MUST NOT repoint it: a leading `cd <checkout> &&` sends the script at whichever tree it names, which at implementation time is a checkout without the stage's changes and at final-verification is not the integrated tree either. Absolute paths are fine and usually necessary — the script and its `tsconfig` live under `<task_root>/qa/scripts/`, i.e. under `.okstra/`, and a worktree does not carry `.okstra/` (worker preamble: "`.okstra/**` artifacts remain anchored at `**Project Root:**`; the worktree may not contain them"). Point at those by absolute path; leave the cwd alone. **Enforced:** `scripts/okstra_ctl/conformance.py` `_check_entry` rejects a `runCommand` whose first word in any `&&` / `;` segment changes directory. Layout split: 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` fails when a stage that declared `Conformance tests:` has no matching `-stage-<N>` entry in the shared manifest (a declaration that was never materialized); the manifest JSON structure — including each entry's `script` living under `qa/scripts/` — is enforced by `validate_conformance_manifest` (called from both the run path and validate-run); and the result policy is evaluated by `conformance.py` and `validate-run.py`.
136
136
  - `### Stage Exit Contract` — predicted added/modified files, newly exposed identifiers/types/endpoints, downstream-usable resources.
137
137
  - `### Stage Validation` — pre / mid / post exact commands or observable outcomes for this stage only.
138
138
  - **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`.
@@ -14,6 +14,10 @@ import fnmatch
14
14
  import re
15
15
  from dataclasses import dataclass
16
16
 
17
+ # 셸에서 프로세스의 cwd 를 바꾸는 명령. verifier 가 워크트리 cwd 에서 실행하는
18
+ # 계약이 이것들로 무력화된다.
19
+ _CWD_CHANGING_COMMANDS: frozenset[str] = frozenset({"cd", "pushd", "popd", "chdir"})
20
+
17
21
  # diff 가 건드린 표면과 대조할 capability 태그 화이트리스트.
18
22
  CAPABILITY_WHITELIST: tuple[str, ...] = ("db", "io", "http", "external")
19
23
  EXTERNAL_ADVISORY_CAPABILITIES: frozenset[str] = frozenset(
@@ -89,6 +93,25 @@ def _check_entry(entry: object, idx: int, errors: list[str]) -> None:
89
93
  if isinstance(script, str) and script.strip() and "qa/scripts/" not in script:
90
94
  errors.append(f"{path}.script must live under the task qa scripts dir (qa/scripts/), got {script!r}")
91
95
  _check_nonempty_str(entry.get("runCommand"), f"{path}.runCommand", errors)
96
+ run_command = entry.get("runCommand")
97
+ # 이 명령은 워크트리 cwd 에서 verbatim 실행되고(_implementation-verifier.md
98
+ # "Otherwise run runCommand in the worktree cwd"), **그 cwd 가 곧 검사 대상**이다.
99
+ # 스크립트·tsconfig 는 `.okstra/` 아래 사는데 워크트리에는 `.okstra/` 가 없으므로
100
+ # (implementation-worker-preamble.md "the worktree may not contain them")
101
+ # 그것들을 절대경로로 가리키는 것은 정상이고 사실상 필수다. 금지되는 것은 cwd
102
+ # 를 옮기는 일뿐이다 — 선행 `cd <메인 체크아웃>` 은 stage diff 가 없는 트리에서
103
+ # 검사를 돌려 미변경 코드를 통과시킨다.
104
+ if isinstance(run_command, str):
105
+ for segment in re.split(r"&&|\|\||;|\|", run_command):
106
+ words = segment.split()
107
+ if words and words[0] in _CWD_CHANGING_COMMANDS:
108
+ errors.append(
109
+ f"{path}.runCommand must run in the worktree cwd — that cwd is "
110
+ f"the tree under test; a leading `{words[0]}` repoints it, so "
111
+ "the script checks whichever checkout it lands in instead of "
112
+ "this stage's diff"
113
+ )
114
+ break
92
115
  _check_nonempty_str(entry.get("passContract"), f"{path}.passContract", errors)
93
116
  req_ids = entry.get("requirementIds")
94
117
  if (
@@ -392,9 +392,33 @@ def _resource_lines(
392
392
  )
393
393
  if clarification:
394
394
  bodies.append(clarification)
395
+ carry = _stage_fix_carry_body(context, item.plan)
396
+ if carry:
397
+ bodies.append(carry)
395
398
  return ["", "\n\n".join(bodies)]
396
399
 
397
400
 
401
+ def _stage_fix_carry_body(
402
+ context: _MaterializationContext,
403
+ plan: PromptPlan,
404
+ ) -> str:
405
+ """The fix-run carry block, for the two audiences that act on it.
406
+
407
+ The executor's scope is the carried blocking findings, and the verifier MUST
408
+ cite each of them as resolved or still-failing — a fix-run verifier result
409
+ citing none is recorded as a contract violation. Both facts live in the
410
+ rendered analysis profile, which no CLI worker can read, so the block used to
411
+ reach them only if the lead transcribed it by hand. A missed transcription
412
+ made the worker answerable for a list it was never given.
413
+ """
414
+ if plan.audience not in ("implementation-executor", "implementation-verifier"):
415
+ return ""
416
+ run = context.active_context.get("run")
417
+ if not isinstance(run, Mapping):
418
+ return ""
419
+ return _string_value(run.get("fixRunCarry")).strip()
420
+
421
+
398
422
  def _resolve_clarification_input(
399
423
  context: _MaterializationContext,
400
424
  plan: PromptPlan,
@@ -58,6 +58,7 @@ def compact_active_run_context(
58
58
  "workflow": dict(_mapping(payload.get("workflow"))),
59
59
  "run": {
60
60
  "stage": ctx.get("RUN_STAGE", ""),
61
+ "fixRunCarry": ctx.get("FIX_RUN_CONTEXT", ""),
61
62
  },
62
63
  "inputs": _compact_active_inputs(payload),
63
64
  "workers": _compact_active_workers(payload),
@@ -186,12 +187,13 @@ def _hydrate_active_run(
186
187
  payload: Mapping[str, Any],
187
188
  ctx: Mapping[str, str],
188
189
  ) -> dict[str, str]:
189
- # `stage` is a run input, not a derived path, so pathHints cannot rebuild
190
- # itit survives the round trip only by being read back off the compact
191
- # payload.
190
+ # `stage` and `fixRunCarry` are run inputs / derived content, not paths, so
191
+ # pathHints cannot rebuild them they survive the round trip only by being
192
+ # read back off the compact payload.
192
193
  run = _mapping(payload.get("run"))
193
194
  return {
194
195
  "stage": str(run.get("stage", "") or ""),
196
+ "fixRunCarry": str(run.get("fixRunCarry", "") or ""),
195
197
  "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
196
198
  "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
197
199
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
@@ -511,6 +511,13 @@ def _active_run(ctx: dict) -> dict:
511
511
  # stage from `consumers.jsonl`. Feeds the implementation prompt anchor
512
512
  # in `initial_prompt_materialization`.
513
513
  "stage": ctx.get("RUN_STAGE", ""),
514
+ # A fix run's carried findings decide what the executor fixes and what
515
+ # the verifier MUST cite as resolved / still-failing. The rendered
516
+ # analysis profile holds the same block, but no CLI worker can read that
517
+ # file, so this copy is what `initial_prompt_materialization` inlines
518
+ # into their prompts. Derived once at prepare: re-deriving it later would
519
+ # read a worktree HEAD the executor may have already moved.
520
+ "fixRunCarry": ctx.get("FIX_RUN_CONTEXT", ""),
514
521
  "runDirectoryPath": ctx.get("RUN_DIR_RELATIVE_PATH", ""),
515
522
  "runManifestPath": ctx.get("RUN_MANIFEST_RELATIVE_PATH", ""),
516
523
  "teamStatePath": ctx.get("TEAM_STATE_RELATIVE_PATH", ""),
@@ -45,10 +45,10 @@ class StageFixCarry:
45
45
  "Scope rules for this fix run live in the implementation sidecars "
46
46
  "(`_implementation-verifier.md` § Fix-run incremental scope, "
47
47
  "`report-writer.md` § Fix-run incremental authoring).",
48
- "Lead duties: transcribe this block verbatim into every CLI "
49
- "executor/verifier prompt (CLI processes cannot read this file), "
50
- "and pass the previous data.json path above to the report-writer "
51
- "dispatch so it authors incrementally.",
48
+ "This block is inlined into the executor and verifier prompts by "
49
+ "`initial_prompt_materialization`; do not transcribe it by hand. "
50
+ "Lead duties: pass the previous data.json path above to the "
51
+ "report-writer dispatch so it authors incrementally.",
52
52
  ])
53
53
  return "\n".join(lines)
54
54
 
@@ -915,7 +915,14 @@ export async function runInstall(args) {
915
915
  await writeFileAtomic(join(paths.home, "version"), paths.package + "\n", 0o644);
916
916
  }
917
917
  if (!opts.quiet) {
918
- process.stdout.write(` version stamp: ${paths.package}\n`);
918
+ // The source is already named at the top of the run; what the stamp line
919
+ // could not say is whether this install moved anything. `copied=0 skipped=N`
920
+ // plus a bare version reads identically for "already current" and "an older
921
+ // payload just landed over a newer home" — the latter cost a full run,
922
+ // caught only by hand-diffing installed files.
923
+ process.stdout.write(
924
+ ` version stamp: ${formatVersionTransition(paths.version, paths.package)}\n`,
925
+ );
919
926
  process.stdout.write("done.\n");
920
927
  process.stdout.write(
921
928
  "\nNext step: register the current project.\n" +
@@ -1011,6 +1018,15 @@ async function agentDriftReasons(paths) {
1011
1018
  return reasons;
1012
1019
  }
1013
1020
 
1021
+ // What the stamp did, not just where it landed: `0.165.3 -> 0.166.0` and
1022
+ // `0.166.0 (unchanged)` are the two cases a bare version line cannot tell apart,
1023
+ // and they mean opposite things about whether the install did anything.
1024
+ export function formatVersionTransition(previous, next) {
1025
+ const from = String(previous ?? "").trim();
1026
+ if (!from) return next;
1027
+ return from === next ? `${next} (unchanged)` : `${from} -> ${next}`;
1028
+ }
1029
+
1014
1030
  function summarise(label, result, target) {
1015
1031
  if (result.missingSource) {
1016
1032
  process.stdout.write(` ${label}: source directory missing — skipped\n`);