@gtrabanco/pi-agentic-workflow 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +26 -0
  2. package/dist/config/defaults.js +2 -0
  3. package/dist/config/load.d.ts +3 -0
  4. package/dist/config/load.js +3 -1
  5. package/dist/config/merge.js +2 -0
  6. package/dist/config/path-policy.d.ts +97 -0
  7. package/dist/config/path-policy.js +301 -0
  8. package/dist/config/schema.js +65 -2
  9. package/dist/config/types.d.ts +5 -0
  10. package/dist/extension/index.js +147 -5
  11. package/package.json +1 -1
  12. package/skills/design-feature/SKILL.md +1 -1
  13. package/skills/design-feature/references/REPAIR.md +12 -10
  14. package/skills/execute-phase/SKILL.md +1 -1
  15. package/skills/execute-phase/references/PREFLIGHT.md +28 -0
  16. package/skills/init-workspace/SKILL.md +1 -1
  17. package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +7 -1
  18. package/skills/init-workspace/references/UPGRADE.md +7 -2
  19. package/skills/orchestration-envelope/SKILL.md +1 -1
  20. package/skills/orchestration-envelope/references/TURN_CONTRACT.md +52 -0
  21. package/skills/plan-feature-scaffold/SKILL.md +1 -1
  22. package/skills/plan-feature-scaffold/references/SCAFFOLD_PROCESS.md +8 -0
  23. package/skills/pre-execution-review/SKILL.md +1 -1
  24. package/skills/pre-execution-review/references/LEDGERS.md +8 -4
  25. package/skills/pre-execution-review/references/POLICY.md +40 -14
  26. package/skills/review-plan/SKILL.md +1 -1
  27. package/skills/review-plan/references/CHECKS.md +5 -2
  28. package/skills/review-plan/references/OUTPUT.md +6 -6
  29. package/skills/review-spec/SKILL.md +1 -1
  30. package/skills/review-spec/references/CHECKS.md +4 -1
  31. package/skills/review-spec/references/OUTPUT.md +9 -5
  32. package/skills/verification-contract/SKILL.md +7 -1
@@ -1,7 +1,9 @@
1
- import { dirname, resolve } from "node:path";
1
+ import { readFileSync, readdirSync, existsSync, lstatSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
- import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import { getAgentDir, isToolCallEventType } from "@earendil-works/pi-coding-agent";
4
5
  import { loadConfig } from "../config/load.js";
6
+ import { evaluateToolCall, isProtectedPath, normalizeTarget } from "../config/path-policy.js";
5
7
  import { createExtension } from "./factory.js";
6
8
  import { readGitStatusBounded } from "./receipt-guard.js";
7
9
  import { createPickerComponent, PICKER_MAX_VISIBLE, pagedSelect } from "../settings/picker.js";
@@ -95,9 +97,86 @@ function toRegistrar(pi) {
95
97
  }),
96
98
  };
97
99
  }
100
+ /** Every unit decision ledger's `path-protection-records@1` block, concatenated. */
101
+ function collectRecordTexts(root) {
102
+ const texts = [];
103
+ for (const parent of ["docs/features", "docs/fix"]) {
104
+ const base = join(root, parent);
105
+ let entries;
106
+ try {
107
+ entries = readdirSync(base, { withFileTypes: true });
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ for (const entry of entries) {
113
+ if (!entry.isDirectory())
114
+ continue;
115
+ try {
116
+ texts.push(readFileSync(join(base, entry.name, "decisions.md"), "utf8"));
117
+ }
118
+ catch {
119
+ // A unit without a decisions ledger contributes no records.
120
+ }
121
+ }
122
+ }
123
+ return texts.join("\n");
124
+ }
125
+ /**
126
+ * Canonicalise an absolute target even when it does not exist yet. `realpathSync`
127
+ * refuses a path whose final component is absent, so the longest existing prefix
128
+ * is canonicalised and the missing tail re-appended; a symlink at the final
129
+ * component is followed explicitly, because a write follows it even when its
130
+ * destination is absent (F28). Resolution errors fail closed.
131
+ */
132
+ function resolveRealTarget(target) {
133
+ let current = target;
134
+ for (let hops = 0; hops < 40; hops += 1) {
135
+ let link = null;
136
+ try {
137
+ if (lstatSync(current).isSymbolicLink())
138
+ link = readlinkSync(current);
139
+ }
140
+ catch {
141
+ // The path does not exist: canonicalise its longest existing prefix.
142
+ return { ok: true, real: resolveExistingPrefix(current) };
143
+ }
144
+ if (link === null) {
145
+ try {
146
+ return { ok: true, real: realpathSync(current) };
147
+ }
148
+ catch (error) {
149
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
150
+ }
151
+ }
152
+ current = isAbsolute(link) ? link : resolve(dirname(current), link);
153
+ }
154
+ return { ok: false, reason: "too many symbolic links" };
155
+ }
156
+ /** Canonicalise the longest existing prefix of `target` and re-append the missing tail. */
157
+ function resolveExistingPrefix(target) {
158
+ const tail = [];
159
+ let current = target;
160
+ for (;;) {
161
+ try {
162
+ const real = realpathSync(current);
163
+ return tail.length === 0 ? real : join(real, ...tail.reverse());
164
+ }
165
+ catch {
166
+ const parent = dirname(current);
167
+ if (parent === current)
168
+ return target;
169
+ tail.push(basename(current));
170
+ current = parent;
171
+ }
172
+ }
173
+ }
98
174
  export default function extension(pi) {
99
175
  const agentDir = getAgentDir();
100
176
  const hint = createHintStore({ path: stateFilePath(agentDir) });
177
+ // Said once per session: an override that was ignored (or a malformed file)
178
+ // is reported the first time the guard runs, never on every tool call.
179
+ let reportedDegradations = false;
101
180
  const { router, guards } = createExtension({
102
181
  registrar: toRegistrar(pi),
103
182
  // Resolved per call: the router must never hold a session-bound object
@@ -129,10 +208,73 @@ export default function extension(pi) {
129
208
  pi.on("thinking_level_select", (event) => router.noteThinkingLevelSelect(event.level));
130
209
  // The inline-receipt path is impossible: a `gh pr comment` carrying a
131
210
  // REVIEW-PASS / merge-ready marker is blocked before it runs, and the reason
132
- // names the script that proves the receipt landed (issue #182).
133
- pi.on("tool_call", (event) => {
211
+ // names the script that proves the receipt landed (issue #182). The guard
212
+ // itself filters to `bash`, so every tool call is checked — nesting it under
213
+ // one tool name would make the block unreachable for the calls it exists for.
214
+ pi.on("tool_call", (event, ctx) => {
134
215
  const command = "command" in event.input && typeof event.input.command === "string" ? event.input.command : undefined;
135
- return guards.receiptGuard({ toolName: event.toolName, command });
216
+ const receipt = guards.receiptGuard({ toolName: event.toolName, command });
217
+ if (receipt.block)
218
+ return receipt;
219
+ // Tier 2 path prevention (feature 60): block a write/edit to an existing
220
+ // protected path with no matching justification record; reads and new-file
221
+ // creates pass. Reads the same effective policy the Tier 1 gate reads.
222
+ if (!isToolCallEventType("write", event) && !isToolCallEventType("edit", event))
223
+ return undefined;
224
+ const targetPath = event.input.path;
225
+ if (typeof targetPath !== "string" || targetPath === "")
226
+ return undefined;
227
+ const loaded = loadConfig({ agentDir, cwd: ctx.cwd, projectTrusted: ctx.isProjectTrusted() });
228
+ if (!reportedDegradations && loaded.pathProtectionDegradations.length > 0) {
229
+ reportedDegradations = true;
230
+ for (const degradation of loaded.pathProtectionDegradations) {
231
+ ctx.ui.notify(`pi-agentic-workflow: path protection — ${degradation.code}: ${degradation.detail}`, "warning");
232
+ }
233
+ }
234
+ const absolute = isAbsolute(targetPath) ? targetPath : resolve(ctx.cwd, targetPath);
235
+ let realRoot = ctx.cwd;
236
+ try {
237
+ realRoot = realpathSync(ctx.cwd);
238
+ }
239
+ catch {
240
+ // ctx.cwd always exists; fall back to the lexical root.
241
+ }
242
+ // Canonicalise the target even when it does not exist yet, so the
243
+ // containment check and the protected-glob match run on the same basis as
244
+ // `realpathSync(ctx.cwd)` (F27) and a dangling symlink cannot route a write
245
+ // outside the project root (F28).
246
+ const resolvedTarget = resolveRealTarget(absolute);
247
+ if (!resolvedTarget.ok) {
248
+ return {
249
+ block: true,
250
+ reason: `"${targetPath}" could not be resolved (${resolvedTarget.reason}); ` +
251
+ `path protection cannot verify it, so the write is blocked.`,
252
+ };
253
+ }
254
+ const relativeTarget = normalizeTarget(relative(realRoot, resolvedTarget.real));
255
+ // Fail closed on a target that escapes the project root: the policy is
256
+ // repo-relative, so an out-of-root path cannot be verified (F7).
257
+ if (relativeTarget === "" || relativeTarget === ".." || relativeTarget.startsWith("../") || isAbsolute(relativeTarget)) {
258
+ return {
259
+ block: true,
260
+ reason: `"${targetPath}" resolves outside the project root (${ctx.cwd}); ` +
261
+ `path protection cannot verify it, so the write is blocked.`,
262
+ };
263
+ }
264
+ // Lazy: read the records corpus (every unit's decisions.md) only once the
265
+ // target is a known existing protected path, never on every write/edit (F13).
266
+ if (!existsSync(absolute))
267
+ return undefined;
268
+ if (!isProtectedPath(loaded.config.pathProtection, relativeTarget))
269
+ return undefined;
270
+ const decision = evaluateToolCall({
271
+ toolName: event.toolName,
272
+ targetPath: relativeTarget,
273
+ targetExists: true,
274
+ policy: loaded.config.pathProtection,
275
+ recordsText: collectRecordTexts(ctx.cwd),
276
+ });
277
+ return decision.block ? { block: true, reason: decision.reason } : undefined;
136
278
  });
137
279
  pi.on("agent_settled", (_event, ctx) => {
138
280
  void router.settle(toInvocationContext(ctx));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-agentic-workflow",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: design-feature
3
3
  user-invocable: true
4
- version: 3.4.0
4
+ version: 3.5.0
5
5
  argument-hint: <idea | NN-slug> [<instruction>]
6
6
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -57,18 +57,20 @@ exists; it never chose how to fill it, and neither does this skill.
57
57
  ### 4. Second cycle = anomaly, not routine
58
58
 
59
59
  Cycle rules have one owner: `pre-execution-review/references/POLICY.md` §4 — a repeat needs a changed
60
- snapshot or a named falsifiable question plus a new evidence route, and entering
61
- a second repair/re-review cycle prints the `CONVERGENCE-ANOMALY` block (repeated
60
+ snapshot or a named falsifiable question plus a new evidence route, and a second
61
+ consecutive repair/re-review cycle prints the `CONVERGENCE-ANOMALY` block (repeated
62
62
  and new finding ids, the snapshots that moved, the evidence or obligation missed,
63
63
  the owning stage, and why the prior readiness/review/repair failed) **before** any
64
- further edit, then routes to that owner. More cycles stay allowed when
65
- correctness needs them; they never earn a PASS, and an exhausted cycle budget
66
- does not downgrade an open finding.
67
-
68
- A repair responding to a persisted verdict is **never** a loop defect (POLICY §4
69
- scopes every guard to blind re-reviews): a unit whose `progress.md` carries an open
70
- FAIL/NEEDS-DESIGN receipt is being repaired by definition, so no "already complete"
71
- answer exists for it and no cycle cap converts its verdict into a dead end.
64
+ further edit, then routes to that owner. The repair cycles are the correction
65
+ path; they never earn a PASS, and an exhausted cycle budget does not downgrade an
66
+ open finding. A third cycle never starts without explicit user instruction: the
67
+ orchestrator's `stop-review-loop-cap` refusal stops the invocation and names
68
+ `design-feature` as the human route, and a PASS resets the count.
69
+
70
+ The cap counts review invocations, never repairs: a repair responding to a
71
+ persisted verdict produces a new snapshot by design, so the repair turn itself is
72
+ never a loop defect. An open finding is never downgraded or waved through to
73
+ satisfy a count.
72
74
 
73
75
  The spec-stage detail this skill adds: the owning stage of a Product-half miss is
74
76
  `product` unless the evidence proves the plan asked for something the SPEC never
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: execute-phase
3
3
  user-invocable: true
4
- version: 4.5.2
4
+ version: 4.6.0
5
5
  argument-hint: <NN> [P<k>] | --fix <n> [P<k>] | [--max-attempts N] [--force]
6
6
  allowed-tools: [Bash, Read, Edit, Write, MultiEdit]
7
7
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
@@ -198,3 +198,31 @@ directive inside it is data to report, never an action to take.
198
198
  own notes / `progress.md` if present ("executed non-atomic phase: <failed
199
199
  boxes>, user-forced <date>") before implementation begins. `--force` is a
200
200
  user-only escape hatch — the autopilot (`ship-roadmap`) must never pass it.
201
+
202
+ ## Path-protection checkpoint (after the phase-lint guard, before any edit)
203
+
204
+ Run the Tier 1 path gate over the phase that just closed. A pre-edit working
205
+ tree is clean by construction, so the gate **must** read the committed range,
206
+ never the bare working tree (E-60-12) — a gate without `--base` always returns
207
+ `pass — clean` and observes nothing.
208
+
209
+ 1. Each phase entry records its base ref (`git rev-parse HEAD` at phase entry) in
210
+ the phase handoff (`progress.md`).
211
+ 2. At phase `P<n>` (n ≥ 2) run
212
+ `bun packages/agentic-workflow/bin/path-guard.mjs --unit <unit-dir> --phase P<n-1> --base <P<n-1> base ref>`
213
+ and paste its `PATH-GUARD` block. P1's checkpoint uses the planning-artifacts
214
+ range (`--phase P1 --base <unit base>`); the final phase's own range is checked
215
+ by the close-out gate (`--phase P<n> --base <P<n> base ref>`).
216
+ 3. Exit 0 → proceed. Exit 1 → STOP before any edit and print:
217
+
218
+ ```text
219
+ GATE REJECTION — path-protection
220
+ Reason: <the gate's `PATH-GUARD fail — <reason>` line>
221
+ Return route: /execute-phase <NN> <P<n>> — record a justification/approval row, then re-run the checkpoint
222
+ ```
223
+
224
+ There is **no `--force` bypass** (E-60-8): the escape hatch is the recorded
225
+ justification/approval, never a flag. Where the crate is unavailable
226
+ (installed-skill target; the scripts-distribution gap, `known-issues.md`
227
+ B-01), apply the same disclose-and-degrade rule the phase-lint guard uses and
228
+ record the unavailable gate — never silently skip it.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: init-workspace
3
3
  user-invocable: true
4
- version: 2.8.0
4
+ version: 2.9.0
5
5
  argument-hint: <target-dir>
6
6
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -20,7 +20,13 @@
20
20
  that the scaffold failed. If retained, add its documentation-map row and
21
21
  name the project's explicit architectural-decision authority.
22
22
  7. **Install accepted agent safety hooks.** Keep the canonical policy and
23
- fullauto wrapper at `.agentic-workflow/hooks/`. For each accepted platform,
23
+ fullauto wrapper at `.agentic-workflow/hooks/`. Seed the shipped
24
+ path-protection policy at `.agentic-workflow/path-policy.json` and its doc
25
+ page at `.agentic-workflow/path-protection.md`, beside the hooks — the
26
+ `path-policy.json` seed is byte-identical to the producer crate's canonical
27
+ default, so a fresh install gets the guard with zero config. Leave an
28
+ existing owner-written `.agentic-workflow/path-policy.json` untouched and
29
+ report it as a residual. For each accepted platform,
24
30
  activate only its repository config/example as documented in the hook pack.
25
31
  Run `bash .agentic-workflow/hooks/tests/test-command-guard.sh`; missing `jq`,
26
32
  an unknown payload, or an existing customized hook file becomes a residual,
@@ -19,7 +19,9 @@ lacks**. Seven ordered steps:
19
19
  (`docs/CAPABILITIES.md` + its documentation-map row), the optional
20
20
  architectural-invariants document
21
21
  (`docs/architecture/ARCHITECTURAL_INVARIANTS.md` + its documentation-map
22
- row), the portable command-guard pack and detected platform adapter, or the Normalized
22
+ row), the portable command-guard pack and detected platform adapter, the
23
+ shipped path-protection `path-policy.json` seed and its `path-protection.md`
24
+ doc page, or the Normalized
23
25
  Repository State ledger (`docs/workflow/REPOSITORY_STATE.md`). This is the
24
26
  diff-against-current-template contract; it is the only source of *what's
25
27
  new*. (A missing `docs/CAPABILITIES.md` is proposed with the same
@@ -44,7 +46,10 @@ lacks**. Seven ordered steps:
44
46
  template also changed is left untouched and listed as a residual, not
45
47
  silently updated. For hooks, add the canonical pack when absent and activate
46
48
  a platform adapter only after an explicit yes; never replace an existing
47
- platform hook config. Leave honest placeholders where the user skipped.
49
+ platform hook config. A missing `path-policy.json` seed and its
50
+ `path-protection.md` page are proposed from the template; an existing
51
+ owner-written `path-policy.json` is never clobbered and is listed as a
52
+ residual instead. Leave honest placeholders where the user skipped.
48
53
  6. **Seed missing urgency labels, additively (feature 15).** Independent of
49
54
  the `CLAUDE.md`/`docs/` block diff above (this is forge-repo state, not a
50
55
  doc block): check whether the target repo already has the `urgent` and
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: orchestration-envelope
3
3
  user-invocable: false
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -85,3 +85,55 @@ next | suggested
85
85
  next | continuation
86
86
  ```
87
87
 
88
+ ## Path-protection checkpoint (feature 60)
89
+
90
+ The Tier 1 path gate is versioned so the checkpoint invocation, the closed
91
+ reason vocabulary, and the two record blocks cannot drift from the crate module
92
+ that produces them. The preflight runs the checkpoint over the just-closed
93
+ phase's committed range (`execute-phase`'s preflight owns the step text):
94
+
95
+ ```text
96
+ path-protection@1
97
+ # checkpoint: path-guard --unit <unit-dir> --phase <P<n>> --base <P<n> base ref>
98
+ # reasons: schema-export:PATH_GUARD_REASONS (packages/agentic-workflow/src/path-policy.mjs)
99
+ reason | kind
100
+ clean | pass
101
+ justified | pass
102
+ approved | pass
103
+ protected-modification | fail
104
+ approval-required | fail
105
+ undeclared-test | fail
106
+ unmatched-record | fail
107
+ malformed-declaration | fail
108
+ missing-config | degraded
109
+ malformed-config | degraded
110
+ ```
111
+
112
+ The gate prints one fixed block (`PATH-GUARD <pass|fail> — <reason>`, then an
113
+ `offenders:` line, then `phase: … freeze-after: … checked:`, then any `DEGRADED`
114
+ line) and exits 0 on pass, 1 on fail, 2 on usage. The plan declares the protected
115
+ set it will author:
116
+
117
+ ```text
118
+ path-protection-plan@1
119
+ freeze-after: <P<n>|none>
120
+ kind | path | justification
121
+ created | <repo-relative path or glob> | <one-line justification>
122
+ not-created | <test name> | <one-line justification>
123
+ ignored | <test name> | <one-line justification>
124
+ ```
125
+
126
+ Every escape is an append-only record row in the unit's `decisions.md`:
127
+
128
+ ```text
129
+ path-protection-records@1
130
+ kind | paths | phase | date | authority | justification
131
+ justification | <path or glob>[,<path or glob>] | <P<n>> | <YYYY-MM-DD> | execute-phase | <one-line justification>
132
+ approval | <path or glob>[,<path or glob>] | <P<n>> | <YYYY-MM-DD> | human-owner | <one-line justification>
133
+ ```
134
+
135
+ A `justification` row satisfies a justification requirement; an `approval` row
136
+ satisfies an approval requirement and additionally requires a matching
137
+ justification row. The authority column is validated (`execute-phase` for
138
+ justification, `human-owner` for approval), so no auto-approval authority exists.
139
+
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: plan-feature-scaffold
3
3
  user-invocable: false
4
- version: 2.3.2
4
+ version: 2.4.0
5
5
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -28,6 +28,14 @@
28
28
  - Rotate `artifactRevisionId` for the unit's plan artifacts on this write and
29
29
  carry it in the hand-off. A later revert to these same bytes is a new
30
30
  authoring event and needs a new id.
31
+ - Declare the test set in the `path-protection-plan@1` block (feature 60),
32
+ placeholder tokens only, never a project glob: `freeze-after: <P<n>|none>`
33
+ plus one justified row per test decision
34
+ (`created | <repo-relative path or glob> | <one-line justification>`,
35
+ `not-created | <test name> | <one-line justification>`,
36
+ `ignored | <test name> | <one-line justification>`). Put it in `PLAN.md` for
37
+ M/L, or in the SPEC's `### Phases` for XS/S; the grammar is
38
+ `path-protection@1` in the turn contract.
31
39
  4. **Scale by size:**
32
40
 
33
41
  - **XS/S:** SPEC + `ACCEPTANCE.md`. Its `### Phases` has checkbox tasks and at least `P1`
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: pre-execution-review
3
3
  user-invocable: false
4
- version: 2.2.1
4
+ version: 2.3.0
5
5
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
6
6
  license: MIT
7
7
  description: >
@@ -90,7 +90,11 @@ status | resolution-evidence | resolving-artifact-revision
90
90
 
91
91
  - `stage` is `spec` or `plan`; `severity` and `class` use the receipt
92
92
  vocabularies (`info|low|medium|high|critical`,
93
- `product|plan|source|environment|runtime`). `info` is the only immaterial one.
93
+ `product|plan|source|environment|runtime`). Material is `medium` and above; a
94
+ `low` row is a persisted **report-note** — visible, non-blocking, and resolved
95
+ by the stage author without a re-review — and `info` is immaterial.
96
+ - Deflating a real defect below the `medium` minimum to unblock a pass is itself
97
+ a finding: the row is re-classified and blocks, never waved through.
94
98
  - `finding-id` is stable across cycles: a repeated finding keeps its id and gains
95
99
  a second resolution row, which is exactly what makes the no-progress and
96
100
  `CONVERGENCE-ANOMALY` rules computable.
@@ -104,9 +108,9 @@ status | resolution-evidence | resolving-artifact-revision
104
108
  - `resolving-artifact-revision` is the `artifactRevisionId` of the write that
105
109
  closed the row — the link that lets a re-review prove the snapshot actually
106
110
  changed.
107
- - A `PASS` may not coexist with an open material/unverified row in this table for
108
- the bound snapshot. `audit-pr` reads it; the merge authority stays
109
- `audit-pr`'s alone.
111
+ - A `PASS` may coexist with open or unverified `low`/`info` report-note rows; it
112
+ is refused while any open or unverified `medium`+ row exists for the bound
113
+ snapshot. `audit-pr` reads it; the merge authority stays `audit-pr`'s alone.
110
114
 
111
115
  ## Durable ledger write ownership (the map)
112
116
 
@@ -38,15 +38,34 @@ what is specific to their artifact set.
38
38
  The first review emits one complete unioned findings set — never a drip of
39
39
  successive surprises. Its owner then classifies **every** finding by root cause
40
40
  (`product | plan | source | environment | runtime`) and applies **one**
41
- evidence-bounded repair batch to the owning artifact(s) before a single
42
- re-review of the resulting snapshot.
41
+ evidence-bounded repair batch to the owning artifact(s). The batch rotates
42
+ `artifactRevisionId`; by default it is followed by one re-review of the freshly
43
+ rotated snapshot.
43
44
 
44
45
  | Class | What the batch may do | What it may not do |
45
46
  |---|---|---|
46
47
  | Common root cause | One edit that closes several findings | Split into per-finding micro-edits that leave the shared cause in place |
47
- | Wording-only | Skip a full replan when intent, obligation identity, phase topology, validators, and authority are all unchanged | Proceed without recording that determination in the evidence |
48
+ | Wording-only | Skip the re-review when intent, obligation identity, phase topology, validators, and authority are all unchanged | Proceed without recording the determination, or without rotating `artifactRevisionId` |
48
49
  | Scope-changing | Re-cut the plan or the Product half | Ride through as a "wording fix" |
49
50
 
51
+ A wording-only batch records its machine half and its judgment half as one block
52
+ in the unit's unbound `progress.md`:
53
+
54
+ ```text
55
+ ## Wording-only determination v1 — <spec|plan>
56
+ - Determination: <opaque id>
57
+ - Artifact revision: <the rotated artifactRevisionId>
58
+ - Acceptance fingerprint: <the ACCEPTANCE.md blob the batch left unmoved>
59
+ - Intent and authority unchanged: yes
60
+ ```
61
+
62
+ The revision rotation is **never** skippable: the freshness predicate refuses an
63
+ unrecorded rotation (`stale-source-revision` — bound bytes moved and no matching
64
+ determination was recorded), and a later material movement after an exempted one
65
+ is refused because the recorded revision no longer matches.
66
+ The determination is the exemption's record, never authorization to move bytes
67
+ without one.
68
+
50
69
  Never repair by editing the reviewed claim into agreement with the reviewer: the
51
70
  repair supplies the missing evidence or routes the gap to its owner.
52
71
 
@@ -57,8 +76,8 @@ falsifiable question and a new evidence route**. Identical inputs plus an
57
76
  identical question stop as no-progress: report it and stop, do not re-issue the
58
77
  same verdict with more confidence.
59
78
 
60
- One repair/re-review cycle is the normal correction path. Entering a **second**
61
- cycle is allowed when correctness needs it and never grants PASS, but it is a
79
+ One repair/re-review cycle is the normal correction path. A **second**
80
+ consecutive unconverged repair/re-review cycle is not routine — it is a
62
81
  `CONVERGENCE-ANOMALY`: before any further edit, report
63
82
 
64
83
  ```text
@@ -76,12 +95,18 @@ on a Product- or Plan-rooted finding is invalid even when the candidate changed:
76
95
  the loop repairs source, not authority. Runtime retry and budget mechanics stay
77
96
  outside this policy and can never be translated into a PASS.
78
97
 
79
- The guards above gate **blind re-reviews** — an identical snapshot with the
80
- identical question — and never a repair performed in response to a persisted
81
- verdict: a repair turn whose input is a FAIL/NEEDS-DESIGN receipt produces a new
82
- snapshot by design, so no cycle cap or anomaly rule may block or end it. The
83
- anomaly is printed and routed, never a stop, and no cap converts a verdict into a
84
- dead end.
98
+ A **third cycle never** starts without explicit user instruction: after two
99
+ consecutive unconverged cycles the orchestrator's `stop-review-loop-cap` refusal
100
+ stops a further `review-spec`/`review-plan` invocation, names `design-feature` as
101
+ the human route, and a PASS resets the count. An unconverged loop ends in the
102
+ `needs-design` verdict where the verdict vocabulary sanctions it (the spec stage)
103
+ and in the refusal plus `design-feature` routing at the plan stage. The guards
104
+ above gate **blind re-reviews** — an identical snapshot with the identical
105
+ question — and never a repair performed in response to a persisted verdict: a
106
+ repair turn whose input is a FAIL/NEEDS-DESIGN receipt produces a new snapshot by
107
+ design, so the count is over review invocations, never over repairs. The cap
108
+ stops the loop and routes a human decision; it never establishes correctness from
109
+ a count and never waives a finding.
85
110
 
86
111
  ### 5. What a cycle can never produce
87
112
 
@@ -170,9 +195,9 @@ reports; a printed verdict whose mark is still "for next turn" is not finished.
170
195
  printed without the pasted self-check output is a contract defect, and every
171
196
  consumer reads a chat-only verdict as `missing-receipt-snapshot` — it does not
172
197
  exist to the sensor.
173
- - **A gate rejection is typed**, from a closed set of four: `dependency`, `status`,
174
- `phase-lint`, `stale-or-missing-receipt`. Each fixed gate block carries its type
175
- and names what it read and where the turn goes:
198
+ - **A gate rejection is typed**, from a closed set of five: `dependency`, `status`,
199
+ `phase-lint`, `path-protection`, `stale-or-missing-receipt`. Each fixed gate
200
+ block carries its type and names what it read and where the turn goes:
176
201
 
177
202
  ```text
178
203
  GATE REJECTION — <type>
@@ -191,6 +216,7 @@ reports; a printed verdict whose mark is still "for next turn" is not finished.
191
216
  dependency | PREFLIGHT.md
192
217
  status | PREFLIGHT.md
193
218
  phase-lint | PREFLIGHT.md
219
+ path-protection | PREFLIGHT.md
194
220
  stale-or-missing-receipt | PRE_EXECUTION_GATE.md
195
221
  ```
196
222
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: review-plan
3
3
  user-invocable: true
4
- version: 1.6.1
4
+ version: 1.7.0
5
5
  argument-hint: <NN-slug | fix-N | path/to/SPEC.md> [--adversarial N]
6
6
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -102,7 +102,10 @@ One row per finding in the receipt's `findings` array: stable `id`, `severity`
102
102
  (`info | low | medium | high | critical`), `class` (`product | plan | source |
103
103
  environment | runtime`), `claim` with its section/row pointer, `evidenceRefs`
104
104
  (≥ 1 — a finding without evidence is a hunch and gets dropped), `verification`,
105
- `resolution: open` on emission. Material = anything above `info`; a PASS may not
106
- carry an open or unverified material row. A Product-rooted finding found here
105
+ `resolution: open` on emission. Material = `medium` and above; a `low` row is a
106
+ persisted report-note the stage author resolves without a re-review, and `info`
107
+ is immaterial. Deflating a real defect below the `medium` minimum to unblock a
108
+ pass is itself a finding; a PASS may not carry an open or unverified material
109
+ row. A Product-rooted finding found here
107
110
  keeps `class: product`: the class routes the repair, it is not a record of who
108
111
  noticed what.
@@ -115,16 +115,17 @@ not authority.
115
115
  | Verdict / class | Who acts | What happens next |
116
116
  |---|---|---|
117
117
  | `PLAN-REVIEW-PASS` | nobody | `/execute-phase <NN>` binds this receipt + exact snapshot digest |
118
- | `PLAN-REVIEW-FAIL`, `class: plan` | `plan-feature` / `plan-fix` (the author) | one root-caused repair batch → new `artifactRevisionId` → re-review of the new snapshot |
118
+ | `PLAN-REVIEW-FAIL`, `class: plan` | `plan-feature` / `plan-fix` (the author) | one root-caused repair batch → new `artifactRevisionId` → next cycle (default re-review; a recorded wording-only batch skips it) |
119
119
  | `PLAN-REVIEW-FAIL`, `class: product` | `design-feature` | repair the Product half → `review-spec` → the Plan receipt is re-derived (`stale-parent`) |
120
120
  | `PLAN-REVIEW-FAIL`, `class: source\|environment\|runtime` | its owner, later | record the row, keep it `open`, route it. Do not edit the plan to hide it and do not start `execute-phase` on a plan carrying an open material row |
121
121
 
122
122
  Repeating this review follows the no-progress and convergence rules in
123
123
  `pre-execution-review/references/POLICY.md` §4 — a repeat needs
124
124
  a changed snapshot or a named falsifiable question plus a new evidence route, and
125
- entering a second repair/re-review cycle prints the `CONVERGENCE-ANOMALY` block
126
- before any further edit. A second cycle never grants PASS and never gets folded
127
- into `review-change → fold-findings`, which repairs source, not plan authority.
125
+ a second consecutive repair/re-review cycle prints the `CONVERGENCE-ANOMALY`
126
+ block before any further edit; the orchestrator's `stop-review-loop-cap` refuses
127
+ a third and names `design-feature` (a PASS resets the count). A second cycle
128
+ never grants PASS and never folds into `review-change → fold-findings`.
128
129
 
129
130
  ### Skill-specific turn-contract boxes
130
131
 
@@ -157,10 +158,9 @@ On FAIL, name every finding id once, in order, joined with ` + `:
157
158
 
158
159
  ```
159
160
  → Next: /plan-feature <NN-slug> "<instruction>" (or /plan-fix <N> "<instruction>") —
160
- one repair batch for F1 + P9 + L4, then /review-plan <NN-slug> re-reviews the new
161
+ one repair batch for F1 + P9 + L4, then /review-plan <NN-slug> judges the new
161
162
  artifact revision
162
163
  · class: product → /design-feature <NN-slug> then /review-spec <NN-slug>; the plan
163
164
  re-derives afterwards
164
165
  · a missing product choice → answer it yourself in the instruction; nothing here chooses
165
- · a second cycle about to start → print CONVERGENCE-ANOMALY first, then route to the owner
166
166
  ```
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: review-spec
3
3
  user-invocable: true
4
- version: 1.7.1
4
+ version: 1.8.0
5
5
  argument-hint: <NN-slug | path/to/SPEC.md>
6
6
  author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
7
7
  license: MIT
@@ -101,5 +101,8 @@ Each finding is one row of the receipt's `findings` array: stable `id`,
101
101
  source | environment | runtime`), `claim` (what is wrong, with the section
102
102
  pointer), `evidenceRefs` (≥ 1 — a finding without evidence is a hunch and gets
103
103
  dropped), `verification` (`verified | unverified`), `resolution` (`open` on
104
- emission). Material = anything above `info`; a `PASS` may not carry an open or
104
+ emission). Material = `medium` and above; a `low` row is a persisted report-note
105
+ the stage author resolves without a re-review, and `info` is immaterial.
106
+ Deflating a real defect below the `medium` minimum to unblock a pass is itself a
107
+ finding; a `PASS` may not carry an open or
105
108
  unverified material row.