@popovych.co/witness 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gate.js CHANGED
@@ -431,6 +431,7 @@ export async function runGate(ctx, gateName, target, flags) {
431
431
  ...(dropped.length ? { skipped: dropped } : {}),
432
432
  ...(outcome === 'stopped' && standing ? { standing } : {}),
433
433
  ...(input.artifactSha ? { artifact_sha: input.artifactSha } : {}),
434
+ ...(input.diffSha ? { diff_sha: input.diffSha } : {}),
434
435
  checks: input.checks,
435
436
  ...(verdicts.length ? { verdicts } : {}),
436
437
  ...(malformed.length ? { malformed } : {}),
@@ -10,7 +10,7 @@ import { latestRecap, readStream } from '../journal.js';
10
10
  import { findById } from '../scan.js';
11
11
  import { changedFiles, changedTestSpecs, diffBase, evidenceForDiff, runRegression, screensDir } from '../evidence.js';
12
12
  import { runCriteria } from '../criteria.js';
13
- import { effortOf, implementReviewedSha } from '../reviewed.js';
13
+ import { diffReviewedSha, effortOf, implementReviewedSha } from '../reviewed.js';
14
14
  import { worktreePath } from '../worktree.js';
15
15
  import { registerGate } from '../gate.js';
16
16
  export function codePromptBody(wt, base, files, header) {
@@ -146,6 +146,8 @@ registerGate({
146
146
  return ok({
147
147
  class: (recap?.class ?? 'feature'),
148
148
  reviewedSha: implementReviewedSha(wt, base, plan),
149
+ // the diff term of the line above, so a lapse can attribute itself (row 135)
150
+ diffSha: diffReviewedSha(wt, base),
149
151
  artifactSha: canonicalSha(plan.meta, plan.body),
150
152
  reviewed: { kind: 'tree', root: wt, files },
151
153
  promptBody: codePromptBody(wt, base, files, `### Plan under implementation: ${planId}\n${serializeDoc({ meta: plan.meta, body: plan.body })}`),
@@ -7,11 +7,11 @@ import { primaryRoot } from '../gitio.js';
7
7
  import { findById, loadCanon } from '../scan.js';
8
8
  import { designArtifactCurrent, designPending, designUnseen } from '../design.js';
9
9
  import { effortAbandoned, effortStreams, latestRecap, readStream } from '../journal.js';
10
- import { effortOf, effortReviewedSha, effortSpecs, effortWrites, implementReviewedSha, planPairSha } from '../reviewed.js';
10
+ import { diffReviewedSha, effortOf, effortReviewedSha, effortSpecs, effortWrites, implementReviewedSha, planPairSha } from '../reviewed.js';
11
11
  import { changedFiles, diffBase, evidenceForDiff, isTestPath } from '../evidence.js';
12
12
  import { SESSION_DEFAULT, stagePin } from '../model.js';
13
13
  import { handoffLine, relayLine, resolveDriver, resolveJudge } from '../harness.js';
14
- import { worktreeFlow, worktreePath } from '../worktree.js';
14
+ import { atHome, worktreeFlow, worktreePath } from '../worktree.js';
15
15
  import { lazyStamp } from '../stamp.js';
16
16
  import { ok, refuse, renderRefusal, v } from '../refusal.js';
17
17
  import { kv, rows } from '../toon.js';
@@ -86,20 +86,33 @@ export function authoringOwed(entries, gate, currentSha) {
86
86
  const recent = lastDisposition(entries, gate, entries.lastIndexOf(last));
87
87
  return recent !== undefined && ['revise', 'revise-upstream'].includes(recent.decision);
88
88
  }
89
- // D75 re-arms a gate whose reviewed sha has moved — under row 96 that is the diff moving
90
- // or the base moving, no longer any file in the worktree. Correct either way, but the row
91
- // it produces is a bare `gate implement`, indistinguishable from a CLI stuck on a stale
92
- // answer. A human who just watched that gate pass reads it as the latter, and
93
- // with a second session in the worktree answering `ship` the pair looks like a deadlock
94
- // with no error anywhere. The lapse is a fact the CLI already knows; say it.
95
- function lapseNote(entries, gate, currentSha) {
89
+ // A cap with its remainder counted, never a silent truncation: a note that lists six of
90
+ // nineteen paths and says so is a report, one that lists six and stops is a wrong answer.
91
+ const LAPSE_PATHS = 6;
92
+ function lapseNote(entries, gate, currentSha,
93
+ // A THUNK: the cause costs extra git invocations, and `next` runs every turn while a
94
+ // lapse is rare. Nothing below the guards may be paid for on the settled path.
95
+ cause) {
96
96
  const last = lastGateRun(entries, gate);
97
97
  if (!last || currentSha === undefined || last.reviewed_sha === currentSha)
98
98
  return undefined;
99
99
  // sha-free: asks "was it ever settled", which is the only thing that can lapse
100
100
  if (!gateSettled(entries, gate))
101
101
  return undefined;
102
- return `${gate} approval lapsed — judged @${last.reviewed_sha.slice(0, 7)}, worktree now @${currentSha.slice(0, 7)} — re-gate to judge the current tree`;
102
+ const shas = `judged @${last.reviewed_sha.slice(0, 7)}, now @${currentSha.slice(0, 7)}`;
103
+ const c = cause();
104
+ if (c === undefined || last.diff_sha === undefined) {
105
+ return `${gate} approval lapsed — ${shas} — re-gate to judge the current tree`;
106
+ }
107
+ if (last.diff_sha === c.diffSha) {
108
+ return `${gate} approval lapsed — the plan was re-authored, the worktree is unchanged (${shas}) — re-gate to judge the current plan`;
109
+ }
110
+ const shown = c.changed.slice(0, LAPSE_PATHS).join(' ');
111
+ const rest = c.changed.length - LAPSE_PATHS;
112
+ const paths = c.changed.length === 0
113
+ ? ''
114
+ : ` · changed vs base: ${shown}${rest > 0 ? ` (+${rest} more)` : ''}`;
115
+ return `${gate} approval lapsed — the worktree moved (${shas})${paths} — re-gate to judge the current tree`;
103
116
  }
104
117
  // Row 105. `appendKind` keys on harness, so a cross-harness run is `fresh` and re-invokes
105
118
  // the whole battery — while roundsSinceApprove counts every non-malformed run with no
@@ -220,7 +233,10 @@ export function flowAction(root, cfg, plan, judge) {
220
233
  }
221
234
  return {
222
235
  line: `witness gate implement ${id}`, target: id, ...inWorktree,
223
- ...noteOf(lapseNote(entries, 'implement', diffSha), judgeNote(entries, 'implement', judge)),
236
+ ...noteOf(lapseNote(entries, 'implement', diffSha,
237
+ // `files` is the list flowAction already computed above — the cause costs one
238
+ // hash pass, not a second walk of the diff
239
+ () => (baseR.ok ? { diffSha: diffReviewedSha(wt, baseR.value), changed: files } : undefined)), judgeNote(entries, 'implement', judge)),
224
240
  };
225
241
  }
226
242
  // How far along a flow is — the drain order when several are actionable. Most-advanced
@@ -724,10 +740,22 @@ export async function run(ctx, argv) {
724
740
  ctx.out(kv('note', action.note));
725
741
  if (action.block)
726
742
  action.block.forEach((l) => ctx.out(l));
743
+ // Whether this session is already in `home:` is a fact the CLI holds both halves of, so
744
+ // it answers it here rather than printing the handoff and leaving the comparison to the
745
+ // model. A `run:` line that cds to the directory it was printed in is row 129's defect —
746
+ // a rendered command that does not run — and it is what let the engine bounce a human
747
+ // between two checkouts with nothing changing in between.
748
+ //
749
+ // NOT rows 116-118's reverted band-aid. That fix made `next` REFUSE a handoff on a
750
+ // version-skew diagnosis owned by `check`. This withholds no knowledge and changes no
751
+ // routing answer: the stage, the target and the home are identical either way, and only
752
+ // the instruction to a session that is already there stops being printed.
727
753
  if (action.home) {
728
754
  ctx.out(kv('home', action.home));
729
- ctx.out(kv('run', handoffLine(harness, action.home, action.model)));
730
- ctx.out(kv('relay', relayLine(harness)));
755
+ if (!atHome(ctx.cwd, action.home)) {
756
+ ctx.out(kv('run', handoffLine(harness, action.home, action.model)));
757
+ ctx.out(kv('relay', relayLine(harness)));
758
+ }
731
759
  }
732
760
  return EXIT.OK;
733
761
  }
package/dist/worktree.js CHANGED
@@ -1,5 +1,5 @@
1
- import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { isAbsolute, join, sep } from 'node:path';
1
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
2
+ import { isAbsolute, join, resolve, sep } from 'node:path';
3
3
  import { git, stateDirs, tryGit } from './gitio.js';
4
4
  import { ok, refuse, v } from './refusal.js';
5
5
  export function worktreesDir(root) {
@@ -24,6 +24,26 @@ export function worktreeFlow(cwd, root) {
24
24
  // bare join with no slugification, and doc ids are /^[a-z0-9-]+$/ (dsl.ts).
25
25
  return rest.length > 0 && !rest.includes(sep) ? rest : undefined;
26
26
  }
27
+ // Whether the session asking is already in `home`. A PATH comparison, not a string one:
28
+ // `primaryRoot` answers with git's physical path (`rev-parse --show-toplevel` resolves
29
+ // symlinks) while `ctx.cwd` is whatever the human typed, and on macOS every `/tmp` and
30
+ // `/var` path is a symlink — so a raw `===` reports a session sitting in its own home as a
31
+ // session that must be relocated, which is the handoff loop this predicate exists to close.
32
+ //
33
+ // realpathSync throws on a path that does not exist; `resolve` is the fallback because a
34
+ // home that is not on disk is not one you are standing in, and the comparison must still
35
+ // answer rather than throw inside `next`'s print block.
36
+ export function atHome(cwd, home) {
37
+ const real = (p) => {
38
+ try {
39
+ return realpathSync(p);
40
+ }
41
+ catch {
42
+ return resolve(p);
43
+ }
44
+ };
45
+ return real(cwd) === real(home);
46
+ }
27
47
  export function branchName(planId) {
28
48
  return `witness/${planId}`;
29
49
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@popovych.co/witness",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Evidence-gated spec-driven development pipeline for AI coding agents: brainstorm to PR in six stages, where specs are state, plans are motion, and no transition passes without evidence. Drives from Claude Code or Pi.",
5
5
  "keywords": [
6
6
  "spec-driven-development",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "witness",
3
3
  "description": "Spec-driven development with evidence gates: brainstorm → PR through six stages and five gates. Skills author, the pinned witness CLI writes state and witnesses evidence; gates stop where judgment is owed.",
4
- "version": "0.12.0",
4
+ "version": "0.13.0",
5
5
  "author": {
6
6
  "name": "Kostiantyn Popovych"
7
7
  },
@@ -10,7 +10,7 @@ The CLI decides, you act. If `$ARGUMENTS` contains `--manual`, append `--manual`
10
10
  Resolve the CLI once:
11
11
 
12
12
  ```bash
13
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
14
14
  ```
15
15
 
16
16
  If the repo has no `witness.config.yaml`, run `$WITNESS init` first (one bootstrap commit), then proceed. If init or any verb refuses on a dirty tree, the human commits or cleans it — **never `git commit` yourself, anywhere, at any stage**: the CLI makes its own state commits, and the ship phase makes the sole code commit.
@@ -20,14 +20,14 @@ If the repo has no `witness.config.yaml`, run `$WITNESS init` first (one bootstr
20
20
  Every turn is `witness next`, read, act on the first matching row, repeat:
21
21
 
22
22
  1. Run `$WITNESS next`.
23
- 2. Read its TOON lines: `next:` (a command line), and optional `stage:`, `target:`, `note:`, `home:` (the directory this action's session belongs in), `run:` (the paste-ready handoff command), `relay:` (how a session continues in a fresh context in the same `home:`). `run:` and `relay:` are rendered by the CLI for the harness you are running on — never rewrite them, never substitute a command you remember.
23
+ 2. Read its TOON lines: `next:` (a command line), and optional `stage:`, `target:`, `note:`, `home:` (the directory this action's session belongs in), `run:` (the paste-ready handoff command), `relay:` (how a session continues in a fresh context in the same `home:`). **`run:` and `relay:` are printed only when `home:` is a different session from yours** — the CLI compares it against your cwd, resolving symlinks, so you never make that comparison yourself. `run:` and `relay:` are rendered by the CLI for the harness you are running on — never rewrite them, never substitute a command you remember.
24
24
  3. Act on the **first** matching row, then go to 1:
25
25
 
26
26
  | Signal | Action |
27
27
  |---|---|
28
28
  | A `choose:` block is printed (`note:` says `N ready — ranked below`) | Several specs are ready and the CLI has ranked them. Render the whole block verbatim and in full — the recommendation, its why, each alternative's when and tradeoff, and the `judge-first:` line — then ask the human which to plan. Then use the `witness-plan` skill with the id they name. Never collapse the block to a bare list of ids: the ranking and its costs are the part they are deciding with. |
29
29
  | `next:` names `witness decide` | A gate is stopped and the decision is the human's. Run `$WITNESS decide <gate> <target> --show`, render everything it emits verbatim and in full — the checks, the findings, and its ranked options — and **END YOUR TURN**. Never substitute a command set you remember: it is wrong at the round bound, in the reopened and stale states, and wherever a repair grant is unspent. Never run `--approve`, `--revise` or `--stop` on your own judgment. |
30
- | `home:` present and ≠ your cwd | This stage belongs to a different session. Print the `run:` line verbatim for the human (if this session is `--manual`-armed, change the argument to `'/witness --manual'`), say that work continues in the fresh session, and **END YOUR TURN**. Never use a stage skill or run the `next:` command from the wrong `home:` — a fresh session is the execution model, and it is the only one. |
30
+ | a `run:` line is printed | This stage belongs to a different session; the CLI already compared `home:` against your cwd. Print the `run:` line verbatim for the human (if this session is `--manual`-armed, change the argument to `'/witness --manual'`), say that work continues in the fresh session, and **END YOUR TURN**. Never use a stage skill or run the `next:` command from the wrong `home:` — a fresh session is the execution model, and it is the only one. A `home:` line with **no** `run:` under it means you are already there: read on and act. |
31
31
  | `stage: brainstorm` | Use the `witness-brainstorm` skill. |
32
32
  | `stage: decompose` | Use the `witness-decompose` skill with `target` (the effort slug). |
33
33
  | `stage: design` | Use the `witness-design` skill with `target` (the spec id). |
@@ -3,4 +3,4 @@
3
3
  # context). Silent everywhere that isn't a witness repo; failures swallowed:
4
4
  # session start must never hang or die on this.
5
5
  [ -f witness.config.yaml ] || exit 0
6
- ${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0} 2>/dev/null || true
6
+ ${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0} 2>/dev/null || true
@@ -15,7 +15,7 @@ witness: ends in a confirmed scope recap, writes no document. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -10,7 +10,7 @@ description: Slice a witness effort's confirmed recap into vertical spec slices,
10
10
  Resolve the CLI once per session:
11
11
 
12
12
  ```bash
13
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
13
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
14
14
  ```
15
15
 
16
16
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -16,7 +16,7 @@ No text copied verbatim; the design-critic lens is new material. -->
16
16
  Resolve the CLI once per session:
17
17
 
18
18
  ```bash
19
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
19
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
20
20
  ```
21
21
 
22
22
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ tests carry the parent spec's tag in their NAME. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ parent's criteria. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.
@@ -15,7 +15,7 @@ model can. See NOTICE.md. -->
15
15
  Resolve the CLI once per session:
16
16
 
17
17
  ```bash
18
- WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.12.0}"
18
+ WITNESS="${WITNESS_BIN:-npx -y @popovych.co/witness@0.13.0}"
19
19
  ```
20
20
 
21
21
  - **Render the CLI's decision output verbatim and in full — every line, unmodified.** Never print a command set you remember; never recompose, reformat, summarise or reorder what the CLI emitted. Which decisions are live, how they rank, and what each costs are the CLI's answers, and they change with the round, the bound, the repair grant and the content sha — a remembered set is wrong in more states than it is right.