okstra 0.166.0 → 0.166.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/docs/cli.md +1 -1
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/profiles/improvement-discovery.md +1 -1
- package/runtime/python/okstra_ctl/analysis_packet.py +13 -0
- package/runtime/python/okstra_ctl/conformance.py +23 -0
- package/runtime/python/okstra_ctl/initial_prompt_materialization.py +24 -0
- package/runtime/python/okstra_ctl/path_hints.py +5 -3
- package/runtime/python/okstra_ctl/render.py +7 -0
- package/runtime/python/okstra_ctl/stage_fix_carry.py +4 -4
- package/runtime/schemas/final-report-v2.0.schema.json +102 -1
- package/runtime/templates/reports/final-report.template.md +17 -2
- package/runtime/templates/reports/html/tasks/release-handoff.template.html +2 -1
- package/src/commands/lifecycle/install.mjs +17 -1
package/docs/cli.md
CHANGED
|
@@ -130,7 +130,7 @@ For standard values and phase-specific responsibilities, see [Task type](#--task
|
|
|
130
130
|
- `out-of-scope`: optional.
|
|
131
131
|
- `candidate-cap`: 1–12; default 8.
|
|
132
132
|
- Output: the `## 5.9 Improvement Candidates` table with 11 columns: Cand ID / Lens / Title / Scope / Severity / Effort / Consensus / Source workers / Recommended next-phase / Expected behavior after / Evidence.
|
|
133
|
-
- Verdict Token: `candidates-ready` / `no-candidates`
|
|
133
|
+
- Verdict Token: `analysis-complete` / `analysis-partial` / `blocked` — the shared analysis enum, which is what `schemas/final-report-v2.0.schema.json` admits. Finding no candidates is not a verdict: the run stays `analysis-complete` and records an empty candidate set with a `no-candidate` row per lens. (`candidates-ready` / `no-candidates` appear only in a schema-v1 legacy report's `## 7. Final Verdict`.)
|
|
134
134
|
- Routing: there is no automatic spin-off. The user selects candidates and starts each under a new task ID with `requirements-discovery`, `implementation-planning`, or `error-analysis`.
|
|
135
135
|
- Workers: claude + codex + antigravity + report-writer are all required.
|
|
136
136
|
- Primary-pass assignment: selected analyser instances are enumerated in `requiredWorkerRoles` order, then the lead rotates the primary pass across the resolved priority lenses. Provider/model names do not affect the order, and every analyser still covers every resolved lens after its primary pass.
|
package/package.json
CHANGED
package/runtime/BUILD.json
CHANGED
|
@@ -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`.
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
- v1 legacy branch: when validating or rerendering an existing schema-v1 report, preserve its `## 5.9 Improvement Candidates` table and legacy Markdown contract; do not rewrite that historical data into v2 implicitly.
|
|
55
55
|
- the `## 5.9 Improvement Candidates` table populated with rows that obey the 11-column schema from `validators/validate_improvement_report.py` (Cand ID `I-NNN`, Lens from whitelist, Title, Scope ⊆ scan-scope, Severity, Effort, Consensus, Source workers `<worker>:<id>` from {claude, codex, antigravity}, Recommended next-phase ∈ {requirements-discovery, implementation-planning, error-analysis}, Expected behavior after, Evidence as path:line list). `Expected behavior after` states, in one observable sentence, what becomes different once the candidate is applied — it is the seed of the downstream brief's `EB-NNN` / `EO-NNN`. A candidate you cannot write this cell for is a preference, not a finding: drop it rather than filling the cell with a restatement of the title.
|
|
56
56
|
- `Consensus` cells in `## 5.9 Improvement Candidates` use the table enum exactly: `full`, `partial`, `contested`, `worker-unique`. Map convergence's `full-consensus` / `partial-consensus` labels to `full` / `partial` before writing the table.
|
|
57
|
-
-
|
|
57
|
+
- Verdict Token — **branch-specific, and the two branches do not share a vocabulary.** On the current v2 branch use the shared analysis enum: `analysis-complete` when every resolved lens was examined, `analysis-partial` when one could not be, `blocked` when the scan itself could not run. `schemas/final-report-v2.0.schema.json` admits only those three for `verdictCard.verdictToken` / `finalVerdict.verdictToken`, so a v2 report carrying `candidates-ready` fails Phase 7. **Finding no candidates is not a verdict**: it is an empty `candidates[]` plus a `lensCoverage[]` row per lens with `status: no-candidate` and its evidence-backed rationale — the verdict stays `analysis-complete`. `candidates-ready` / `no-candidates` belong to the v1 legacy `## 7. Final Verdict` Markdown alone, where `validators/validate_improvement_report.py` enforces them. Both branches: Direction `routing`; Next Step "ask the user to select K candidates (see the ## 5.9 table)".
|
|
58
58
|
- `## 3. Recommended Next Steps` first entry summarises per-candidate routing and proposes new task-key names of the form `<task-group>/imp-<Cand-ID>`
|
|
59
59
|
- author the shared schema-v2 report fields plus `improvementDiscovery.candidates[]`, `improvementDiscovery.lensCoverage[]`, `improvementDiscovery.selectionLimit`, and `improvementDiscovery.userNarrative` in data.json. `candidates[]` carries the same 11 logical fields described above; `lensCoverage[]` records either candidate IDs or an evidence-backed no-candidate rationale for every resolved lens. `schemas/final-report-v2.0.schema.json` and `validators/validate_improvement_report.py` enforce this contract. The renderers independently derive AI handoff Markdown and human HTML; never author a free-form report.
|
|
60
60
|
- Clarification request policy (phase-specific addenda — shared policy is in `_common-contract.md`):
|
|
@@ -44,7 +44,13 @@ PROFILE_SECTIONS = (
|
|
|
44
44
|
"Non-goals",
|
|
45
45
|
)
|
|
46
46
|
WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
47
|
+
# `Brief consumption` carries the reporter-confirmation rules — do not infer an
|
|
48
|
+
# unconfirmed `intent-check:` / `conversion-block:`, carry it forward as a
|
|
49
|
+
# blocker instead. Those bind the analysis worker, and the packet is the only
|
|
50
|
+
# profile text a worker receives, so a phase that omits the section ships a
|
|
51
|
+
# worker that can turn an unconfirmed intent into a settled requirement.
|
|
47
52
|
"requirements-discovery": (
|
|
53
|
+
"Brief consumption",
|
|
48
54
|
"Worker discovery procedure",
|
|
49
55
|
),
|
|
50
56
|
"error-analysis": (
|
|
@@ -52,6 +58,7 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
|
52
58
|
"Worker diagnosis procedure",
|
|
53
59
|
),
|
|
54
60
|
"implementation-planning": (
|
|
61
|
+
"Brief consumption",
|
|
55
62
|
"Worker planning procedure",
|
|
56
63
|
"Pre-planning context exploration",
|
|
57
64
|
"Design principles applied when scoring options",
|
|
@@ -60,7 +67,13 @@ WORKER_PROFILE_SECTIONS_BY_TASK_TYPE = {
|
|
|
60
67
|
"Worker verification procedure",
|
|
61
68
|
),
|
|
62
69
|
"improvement-discovery": (
|
|
70
|
+
"Brief consumption",
|
|
63
71
|
"Worker candidate procedure",
|
|
72
|
+
# Every analyser examines every resolved lens but leads with its own, and
|
|
73
|
+
# keeps duplicate / conflicting relations. Held only in the lead's profile,
|
|
74
|
+
# the workers converge on the same easy lens and the cross-check thins out
|
|
75
|
+
# while the aggregate still looks complete.
|
|
76
|
+
"Worker diversity rule",
|
|
64
77
|
),
|
|
65
78
|
}
|
|
66
79
|
CLARIFICATION_SECTIONS = (
|
|
@@ -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`
|
|
190
|
-
#
|
|
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
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
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
|
|
|
@@ -726,9 +726,10 @@
|
|
|
726
726
|
"type": "object",
|
|
727
727
|
"description": "RENDER_IF taskType == release-handoff. §5.6 deliverables.",
|
|
728
728
|
"required": [
|
|
729
|
-
"
|
|
729
|
+
"handoffScope",
|
|
730
730
|
"featureBranchState",
|
|
731
731
|
"userSelections",
|
|
732
|
+
"localCheckoutOutcome",
|
|
732
733
|
"executedCommands",
|
|
733
734
|
"commitList",
|
|
734
735
|
"mergeConflictProbe",
|
|
@@ -737,6 +738,37 @@
|
|
|
737
738
|
"userNarrative"
|
|
738
739
|
],
|
|
739
740
|
"additionalProperties": false,
|
|
741
|
+
"allOf": [
|
|
742
|
+
{
|
|
743
|
+
"if": {
|
|
744
|
+
"properties": {
|
|
745
|
+
"handoffScope": {
|
|
746
|
+
"properties": {
|
|
747
|
+
"mode": {
|
|
748
|
+
"const": "stage-group"
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
"required": [
|
|
752
|
+
"mode"
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
"required": [
|
|
757
|
+
"handoffScope"
|
|
758
|
+
]
|
|
759
|
+
},
|
|
760
|
+
"then": {
|
|
761
|
+
"required": [
|
|
762
|
+
"sourceVerificationReports"
|
|
763
|
+
]
|
|
764
|
+
},
|
|
765
|
+
"else": {
|
|
766
|
+
"required": [
|
|
767
|
+
"sourceVerificationReport"
|
|
768
|
+
]
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
],
|
|
740
772
|
"properties": {
|
|
741
773
|
"handoffScope": {
|
|
742
774
|
"type": "object",
|
|
@@ -761,11 +793,25 @@
|
|
|
761
793
|
},
|
|
762
794
|
"collectorBranch": {
|
|
763
795
|
"type": "string"
|
|
796
|
+
},
|
|
797
|
+
"mergeCommits": {
|
|
798
|
+
"type": "array",
|
|
799
|
+
"description": "Assemble's merge commit SHAs, in the order the stages were merged into the collector branch. Without them the collector branch's contents cannot be traced back to the stages that produced them once the branch moves on.",
|
|
800
|
+
"items": {
|
|
801
|
+
"type": "string",
|
|
802
|
+
"minLength": 7
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
"dependencyClosureVerdict": {
|
|
806
|
+
"type": "string",
|
|
807
|
+
"minLength": 1,
|
|
808
|
+
"description": "Whether the selected stages are closed under their dependencies, and on what basis. A stage-group PR that ships a stage without its predecessor is the failure this records."
|
|
764
809
|
}
|
|
765
810
|
}
|
|
766
811
|
},
|
|
767
812
|
"sourceVerificationReport": {
|
|
768
813
|
"type": "object",
|
|
814
|
+
"description": "whole-task mode: the one final-verification report this handoff ships. Required unless handoffScope.mode is stage-group.",
|
|
769
815
|
"required": [
|
|
770
816
|
"path",
|
|
771
817
|
"verdictTokenQuote"
|
|
@@ -782,6 +828,59 @@
|
|
|
782
828
|
}
|
|
783
829
|
}
|
|
784
830
|
},
|
|
831
|
+
"sourceVerificationReports": {
|
|
832
|
+
"type": "array",
|
|
833
|
+
"description": "stage-group mode: one row per selected stage, because that handoff ships several verified stages and a single report path cannot say which stage each verdict belongs to.",
|
|
834
|
+
"minItems": 1,
|
|
835
|
+
"items": {
|
|
836
|
+
"type": "object",
|
|
837
|
+
"required": [
|
|
838
|
+
"stage",
|
|
839
|
+
"path",
|
|
840
|
+
"verdictTokenQuote"
|
|
841
|
+
],
|
|
842
|
+
"additionalProperties": false,
|
|
843
|
+
"properties": {
|
|
844
|
+
"stage": {
|
|
845
|
+
"type": "integer",
|
|
846
|
+
"minimum": 1
|
|
847
|
+
},
|
|
848
|
+
"path": {
|
|
849
|
+
"type": "string",
|
|
850
|
+
"minLength": 1
|
|
851
|
+
},
|
|
852
|
+
"verdictTokenQuote": {
|
|
853
|
+
"type": "string",
|
|
854
|
+
"minLength": 1
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
"localCheckoutOutcome": {
|
|
860
|
+
"type": "object",
|
|
861
|
+
"description": "What the `local checkout` action did, including when it was not taken. `removedWorktree` is the command's own field: a non-empty path means the okstra worktree was removed, an empty string means it was already gone and the branch survived. Recording a removal that did not happen is the error this shape exists to prevent.",
|
|
862
|
+
"required": [
|
|
863
|
+
"status"
|
|
864
|
+
],
|
|
865
|
+
"additionalProperties": false,
|
|
866
|
+
"properties": {
|
|
867
|
+
"status": {
|
|
868
|
+
"enum": [
|
|
869
|
+
"not-run",
|
|
870
|
+
"checked-out"
|
|
871
|
+
]
|
|
872
|
+
},
|
|
873
|
+
"branch": {
|
|
874
|
+
"type": "string"
|
|
875
|
+
},
|
|
876
|
+
"mainWorktreePath": {
|
|
877
|
+
"type": "string"
|
|
878
|
+
},
|
|
879
|
+
"removedWorktree": {
|
|
880
|
+
"type": "string"
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
785
884
|
"featureBranchState": {
|
|
786
885
|
"type": "object",
|
|
787
886
|
"required": [
|
|
@@ -835,7 +934,9 @@
|
|
|
835
934
|
]
|
|
836
935
|
},
|
|
837
936
|
"h3": {
|
|
937
|
+
"description": "The PR title/body confirmation. `not-run` when H1 was `local checkout` or `skip` — those routes never reach the question, and inventing an answer for a question that was never asked is the failure this value prevents.",
|
|
838
938
|
"enum": [
|
|
939
|
+
"not-run",
|
|
839
940
|
"use as-is",
|
|
840
941
|
"edit then proceed",
|
|
841
942
|
"cancel"
|
|
@@ -544,13 +544,22 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
544
544
|
- Mode: `{{ releaseHandoff.handoffScope.mode | mdcell }}`
|
|
545
545
|
{% if releaseHandoff.handoffScope.stages %}- Stages: {{ releaseHandoff.handoffScope.stages | join(', ') | mdcell }}
|
|
546
546
|
{% endif %}{% if releaseHandoff.handoffScope.collectorBranch %}- Collector branch: `{{ releaseHandoff.handoffScope.collectorBranch | mdcell }}`
|
|
547
|
+
{% endif %}{% if releaseHandoff.handoffScope.mergeCommits %}- Assemble merge commits: {{ releaseHandoff.handoffScope.mergeCommits | join(', ') | mdcell }}
|
|
548
|
+
{% endif %}{% if releaseHandoff.handoffScope.dependencyClosureVerdict %}- Dependency closure: {{ releaseHandoff.handoffScope.dependencyClosureVerdict | mdcell }}
|
|
547
549
|
{% endif %}
|
|
548
550
|
{% endif %}
|
|
549
551
|
### 5.6.1 Source Verification Report
|
|
550
|
-
|
|
552
|
+
{% if releaseHandoff.sourceVerificationReports %}
|
|
553
|
+
| Stage | Report path (project-relative) | Quoted `Verdict Token` row |
|
|
554
|
+
|-------|--------------------------------|----------------------------|
|
|
555
|
+
{% for row in releaseHandoff.sourceVerificationReports -%}
|
|
556
|
+
| {{ row.stage | mdcell }} | `{{ row.path | mdcell }}` | {{ row.verdictTokenQuote | mdcell }} |
|
|
557
|
+
{% endfor %}
|
|
558
|
+
{%- else %}
|
|
551
559
|
- Path (project-relative): `{{ releaseHandoff.sourceVerificationReport.path }}`
|
|
552
560
|
- Quoted `Verdict Token` row from that report's `## 7.` table:
|
|
553
561
|
> {{ releaseHandoff.sourceVerificationReport.verdictTokenQuote | mdquote(2) }}
|
|
562
|
+
{% endif %}
|
|
554
563
|
|
|
555
564
|
### 5.6.2 Feature Branch & Working-Tree State{% if t("releaseHandoff.branchStateAside") != "captured at run start" %} ({{ t("releaseHandoff.branchStateAside") }}){% endif %}
|
|
556
565
|
|
|
@@ -571,7 +580,7 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
571
580
|
{% endif %}
|
|
572
581
|
| H2 | {{ t("releaseHandoff.h2Body") }} | `{{ (releaseHandoff.userSelections.h2 or t("releaseHandoff.h2DefaultLabel")) | mdcell }}` | {{ t("releaseHandoff.h2OptionsLabel") }} |
|
|
573
582
|
| H2b | Merge conflict probe | `{{ (releaseHandoff.userSelections.h2b or releaseHandoff.mergeConflictProbe.kind) | mdcell }}` | `not-run` / `clean` / `proceed anyway` / `change base branch` / `cancel` |
|
|
574
|
-
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `use as-is` / `edit then proceed` / `cancel` |
|
|
583
|
+
| H3 | {{ t("releaseHandoff.h3Body") }} | `{{ releaseHandoff.userSelections.h3 | mdcell }}` | `not-run` / `use as-is` / `edit then proceed` / `cancel` |
|
|
575
584
|
|
|
576
585
|
### 5.6.4 Executed Commands
|
|
577
586
|
|
|
@@ -623,6 +632,12 @@ Carried-forward plan items retain their prior verdicts verbatim; each such item
|
|
|
623
632
|
|
|
624
633
|
{{ releaseHandoff.routingRecommendation }}
|
|
625
634
|
|
|
635
|
+
### 5.6.9 Local Checkout Outcome
|
|
636
|
+
|
|
637
|
+
- Status: `{{ releaseHandoff.localCheckoutOutcome.status | mdcell }}`
|
|
638
|
+
{% if releaseHandoff.localCheckoutOutcome.status == 'checked-out' %}- Checked out `{{ releaseHandoff.localCheckoutOutcome.branch | mdcell }}` into `{{ releaseHandoff.localCheckoutOutcome.mainWorktreePath | mdcell }}`
|
|
639
|
+
- okstra worktree: {% if releaseHandoff.localCheckoutOutcome.removedWorktree %}`{{ releaseHandoff.localCheckoutOutcome.removedWorktree | mdcell }}` removed{% else %}none removed — already torn down, so the branch survived{% endif %}
|
|
640
|
+
{% endif %}
|
|
626
641
|
{% endif %}
|
|
627
642
|
{% if header.taskType == 'implementation' %}
|
|
628
643
|
## 5.7 Implementation Deliverables
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
<h2>{{ t('tasks.release-handoff.commits-delivered') }}</h2>
|
|
40
40
|
{% if handoff.commitList is mapping %}<p>{{ t('tasks.release-handoff.there-are-no-commits') }}</p>{% else %}
|
|
41
41
|
<table><thead><tr><th>{{ t('tasks.release-handoff.commit') }}</th><th>{{ t('tasks.release-handoff.subject') }}</th><th>{{ t('tasks.release-handoff.files') }}</th></tr></thead><tbody>{% for row in handoff.commitList %}<tr>{{ row_key(pairs=[("SHA", row.shortSha), ("Order", loop.index)]) }}<td>{{ row.subject | inline_code }}</td><td>{% for file in row.files %}<code>{{ file }}</code>{% if not loop.last %} {% endif %}{% endfor %}</td></tr>{% endfor %}</tbody></table>{% endif %}
|
|
42
|
-
<p data-report-field="releaseHandoff.
|
|
42
|
+
{% if handoff.sourceVerificationReports %}<p data-report-field="releaseHandoff.sourceVerificationReports"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong>{% for row in handoff.sourceVerificationReports %} — stage {{ row.stage }}: <code>{{ row.path }}</code> · {{ row.verdictTokenQuote | inline_code }}{% endfor %}</p>
|
|
43
|
+
{% else %}<p data-report-field="releaseHandoff.sourceVerificationReport"><strong>{{ t('tasks.release-handoff.the-verification-report-this-rests-on') }}</strong> — <code>{{ handoff.sourceVerificationReport.path }}</code> · {{ handoff.sourceVerificationReport.verdictTokenQuote | inline_code }}</p>{% endif %}
|
|
43
44
|
</section>
|
|
44
45
|
|
|
45
46
|
<section data-report-section="next-action">
|
|
@@ -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
|
-
|
|
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`);
|