tickmarkr 1.93.0 → 1.96.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/run/merge.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { shq } from "../adapters/types.js";
4
- import { classifyFailureOutput, fingerprint, freshFailures } from "../gates/baseline.js";
4
+ import { ceilingKillResult, classifyFailureOutput, effectiveCeilingMs, fingerprint, freshFailures, } from "../gates/baseline.js";
5
5
  import { tickmarkrDir } from "../graph/graph.js";
6
6
  import { gitHead, linkNodeModules, resolveIntegrationBranch, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
7
7
  export function integrationBranch(cfg, runId) {
@@ -53,21 +53,55 @@ export async function mergeTask(intWt, taskBranch, message, gatedCommit) {
53
53
  export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
54
54
  const results = [];
55
55
  for (const [gate, cmd] of Object.entries(commands)) {
56
- const r = await sh(cmd, intWt);
56
+ const entry = baseline?.commands[gate];
57
+ // OBS-534: the ceiling is the BATTERY's, derived by effectiveCeilingMs from the same baseline entry
58
+ // this loop already reads for forgiveness two lines down — never the flat DEFAULT_SHELL_TIMEOUT_MS
59
+ // `sh` defaults to. A suite whose capture measured 600007ms carries a recorded 1800021ms ceiling;
60
+ // running it under 600000ms here SIGKILLed a green tip three times while every per-task gate passed.
61
+ const ceilingMs = effectiveCeilingMs(entry);
62
+ const r = await sh(cmd, intWt, ceilingMs);
57
63
  const raw = r.stdout + "\n" + r.stderr;
58
64
  const stripped = raw.split(intWt).join("");
59
- const entry = baseline?.commands[gate];
65
+ const artifact = join(runDir, `tip-verify-${gate}.log`);
66
+ // Battery parity on the ceiling too (baseline.ts Q24): the kill is read BEFORE the exit code is
67
+ // interpreted at all. A SIGKILLed battery never returned a verdict, so no line of its partial
68
+ // output is one — fingerprinting it is what produced the `<unrecognized failure output>` an
69
+ // operator cannot act on. The kill reader's own text (ceiling + elapsed) and cause replace it.
70
+ const killed = ceilingKillResult(gate, r, ceilingMs);
71
+ if (killed) {
72
+ writeFileSync(artifact, raw);
73
+ results.push({
74
+ gate,
75
+ cmd,
76
+ pass: false,
77
+ exitCode: r.code,
78
+ fingerprints: [],
79
+ details: killed.details,
80
+ cause: killed.meta?.classification,
81
+ artifact,
82
+ });
83
+ continue;
84
+ }
60
85
  const { failing, unreadable } = freshFailures(entry, stripped);
86
+ const cause = r.code === 0 ? undefined : classifyFailureOutput(stripped);
61
87
  // `?? 1` is the battery's own default (baseline.ts compareToBaseline): an exitCode-less legacy
62
88
  // entry reads as red-at-baseline there, so it must read the same here or old baselines silently
63
- // lose forgiveness. Battery parity on the infra rule too (T9): infrastructure-only output means
64
- // the runner never completed a suite — nothing was verified, so nothing is forgivable, however
65
- // familiar its fingerprints. Stricter-than-battery edge kept: unreadable output never forgives.
66
- const forgiven = r.code !== 0 && entry !== undefined && (entry.exitCode ?? 1) !== 0
67
- && failing.length === 0 && !unreadable && classifyFailureOutput(stripped) !== "infra";
89
+ // lose forgiveness. OBS-534 (T2): a capture killed at its ceiling now records a CAUSE and no
90
+ // verdict, and that default must not launder the missing exit code back into red-at-baseline.
91
+ // Only a recorded verdict is forgivable, so this reads the same predicate the battery does
92
+ // (baseline.ts `baselineRed`) — `infra` first, the legacy default only after it. freshFailures
93
+ // already drops the killed capture's flushed fingerprints, but it cannot close this alone: an
94
+ // output whose only shape is a diagnostic HEADING (vitest's "Unhandled Errors" banner) is
95
+ // fingerprintable yet OBS-42-exempt from rejecting, so `failing` comes back empty, `unreadable`
96
+ // false and the cause reads "regression" — every other guard satisfied, and a real red forgiven
97
+ // against a capture that never finished asking the question.
98
+ const baselineRed = entry !== undefined && entry.infra !== true && (entry.exitCode ?? 1) !== 0;
99
+ // Battery parity on the infra rule too (T9): infrastructure-only output means the runner never
100
+ // completed a suite — nothing was verified, so nothing is forgivable, however familiar its
101
+ // fingerprints. Stricter-than-battery edge kept: unreadable output never forgives.
102
+ const forgiven = r.code !== 0 && baselineRed && failing.length === 0 && !unreadable && cause !== "infra";
68
103
  const pass = r.code === 0 || forgiven;
69
- const artifact = pass ? undefined : join(runDir, `tip-verify-${gate}.log`);
70
- if (artifact)
104
+ if (!pass)
71
105
  writeFileSync(artifact, raw);
72
106
  results.push({
73
107
  gate,
@@ -79,7 +113,8 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
79
113
  : forgiven ? `exit ${r.code} but only baseline-recorded failures (forgiven vs baseline)`
80
114
  : `exit ${r.code}`,
81
115
  ...(forgiven ? { forgiven: true } : {}),
82
- ...(artifact ? { artifact } : {}),
116
+ ...(cause ? { cause } : {}),
117
+ ...(pass ? {} : { artifact }),
83
118
  });
84
119
  }
85
120
  return results;
@@ -20,7 +20,7 @@ Implement the first objective sentence. Additional prose that is not the title.
20
20
  </objective>
21
21
 
22
22
  <context>
23
- @.planning/PROJECT.md
23
+ @fixtures/gsd-sample/PROJECT.md
24
24
  @$HOME/.claude/get-shit-done/workflows/execute-plan.md
25
25
  @~/somewhere/outside.md
26
26
  </context>
@@ -0,0 +1,19 @@
1
+ # Sample project — vendored GSD fixture input
2
+
3
+ This file exists so the sibling phase's `07-01-PLAN.md` can cite a repo-relative `<context>` path that
4
+ is **part of the fixture itself**. It previously cited `.planning/PROJECT.md`, which resolved only
5
+ inside the private development checkout: the public export strips `.planning/` at any depth, so the
6
+ compiler's context-reachability refusal (v1.96 T3) correctly failed eight tests on the exported tree
7
+ and the release ritual's pre-tag proof caught it. A vendored fixture must carry its own inputs.
8
+
9
+ Nothing here is read by an assertion — the fixture only needs the path to resolve. The prose stands in
10
+ for the project brief a real GSD plan would point a worker at.
11
+
12
+ ## Objective
13
+
14
+ Ship a small feature end to end, with each plan in the phase owning one objective sentence.
15
+
16
+ ## Constraints
17
+
18
+ - One plan, one objective.
19
+ - A plan's `<context>` block promises the worker can read every repo-relative path it lists.
@@ -0,0 +1,68 @@
1
+ <!-- tickmarkr:spec -->
2
+ <!-- provenance: the shape of .planning/phases/99-arabic-coverage as compiled on 2026-08-18 into
3
+ run-20260818-185710-0000000000000011 (41 tasks). OBS-535's fix — files[] is write scope, not
4
+ payload — was measured against that graph (41 unreadable-payload lints to 0; 31 overflow lints
5
+ when the window is forced to 50k) and against nothing in this repo, because no fixture carried
6
+ its shape. This one does, at five tasks instead of forty-one.
7
+
8
+ The four shape features that made every task on that graph skip its context-window comparison,
9
+ each present below:
10
+ 1. a brace-glob write scope (`scripts/{a,b}`) — a set, not a document; unmeasurable as one file
11
+ 2. an output declared in files[] (`*-SUMMARY.md`) — absent from the base tree BY CONSTRUCTION
12
+ 3. a wave-1 artifact consumed by a TRANSITIVE dependent (T3 reads what T1 writes, via T2)
13
+ 4. the same artifact cited by a task with NO producer upstream (T4) — the actionable class,
14
+ which on the real graph was 17 tasks citing a self-gitignored tree (RULING-P99-14)
15
+ T5 adds the class no commit can ever satisfy: a ref under a gitignored directory.
16
+
17
+ Compile this fixture from a NON-REPO directory. compileNative's context reachability check
18
+ (native.ts:684) fails open when git cannot answer, and T3/T4/T5 deliberately cite paths absent
19
+ from any base tree — that absence is the fixture's whole subject. -->
20
+
21
+ ## T1: Land the instruments the later waves measure with
22
+ - goal: Write the two audit instruments and this task's own summary, so a later wave has something to read
23
+ - shape: implement
24
+ - files: scripts/{audit-strict.mjs,census.mjs}, .planning/payload-shape/T1-SUMMARY.md
25
+ - context: docs/payload-shape/PLAN.md
26
+ - complexity: 3
27
+ - acceptance:
28
+ - command: node -e "process.exit(0)"
29
+ - judge: both instruments exist and the summary records what they measured
30
+
31
+ ## T2: Rewrite the localisation sources the instruments flag
32
+ - goal: Apply the instrument's findings across the localisation sources named in the write scope
33
+ - shape: implement
34
+ - deps: T1
35
+ - files: src/i18n/{ar/common.json,en/common.json,ar/dossier.json,en/dossier.json,ar/intake.json,en/intake.json,ar/tasks.json,en/tasks.json,ar/settings.json,en/settings.json}, .planning/payload-shape/T2-SUMMARY.md
36
+ - context: docs/payload-shape/PLAN.md, scripts/audit-strict.mjs
37
+ - complexity: 5
38
+ - acceptance:
39
+ - judge: every key src/i18n/ar/common.json shares with its en counterpart carries a non-empty Arabic value, and .planning/payload-shape/T2-SUMMARY.md records the count it rewrote
40
+
41
+ ## T3: Close the census the second instrument opens
42
+ - goal: Read the census instrument written two waves back and close its remaining senses
43
+ - shape: implement
44
+ - deps: T2
45
+ - files: src/i18n/{ar/common.json,en/common.json}, .planning/payload-shape/T3-SUMMARY.md
46
+ - context: docs/payload-shape/PLAN.md, scripts/census.mjs
47
+ - complexity: 4
48
+ - acceptance:
49
+ - judge: the census reports no unresolved sense for any rewritten key
50
+
51
+ ## T4: Report on the audit without depending on the task that writes it
52
+ - goal: Summarise the audit's findings for the operator record
53
+ - shape: chore
54
+ - files: .planning/payload-shape/T4-SUMMARY.md
55
+ - context: docs/payload-shape/PLAN.md, scripts/audit-strict.mjs
56
+ - complexity: 2
57
+ - acceptance:
58
+ - judge: .planning/payload-shape/T4-SUMMARY.md states both the flagged-site count and the total key count it was measured against
59
+
60
+ ## T5: Apply the standing ruling to the rewritten sources
61
+ - goal: Enforce the ruling's terminology decisions across the sources T2 rewrote
62
+ - shape: chore
63
+ - deps: T2, T3
64
+ - files: src/i18n/{ar/common.json,en/common.json}
65
+ - context: docs/payload-shape/PLAN.md, .state/RULING-TERMINOLOGY.md
66
+ - complexity: 2
67
+ - acceptance:
68
+ - judge: no file under src/i18n/ar contains the string "Dossier" or "Engagement" in Latin script
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.93.0",
3
+ "version": "1.96.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -63,15 +63,14 @@ through brief lineage. **An executor choice nobody made is still an executor cho
63
63
  tab OVERSEER; create ONE tab ORCHESTRATOR.
64
64
  **FIVE-TAB CANON (standing operator layout — corrected three times on 2026-07-27, layout approved
65
65
  2026-07-29, re-earned 2026-08-17):**
66
- - `OVERSEER` — you, plus a visible pipeline watch pane running the SHIPPED surface (`tickmarkr ui`,
67
- newest-run default; never a hand-rolled status loop — the operator flagged an ad-hoc shell-loop
68
- watcher as "strange watch design"). A supervision watcher buried in an invisible background task
69
- reads as "no watcher" to the operator even when armed.
70
- - `ORCH` — the orchestrator, the run's watch board (split `--direction right`, ~0.6 to the board —
71
- never `down`; the board needs full height for its task matrix), and the raw journal feed. **Nothing
72
- else, ever: a work seat NEVER splits into the ORCH tab**, even when width arithmetic allows a second
73
- column. Operator verbatim: *"in orch tab should be the orch and the watcher only."* Re-earned
74
- 2026-08-17: a planning seat split beside the orchestrator, and the operator caught it, again.
66
+ - `OVERSEER` — you. Do not add a second live run surface: the daemon self-places the shipped board
67
+ beside the supervising seat that invokes the run.
68
+ - `ORCH` — the orchestrator and the daemon-placed, run-id-pinned shipped board beside it. **Look for
69
+ that `role: "watch"` pane; never hand-place or hand-roll a live run surface. Nothing else, ever: a
70
+ work seat NEVER splits into the ORCH tab.** Operator verbatim: *"in orch tab should be the orch and
71
+ the watcher only."* Re-earned 2026-08-17: a planning seat split beside the orchestrator, and the
72
+ operator caught it, again. The daemon owns the side placement and board-first width allocation;
73
+ neither the worker-pane halving floor nor an overseer split command places this pane.
75
74
  - Worker/seat tabs — tickmarkr opens ONE TAB PER TASK itself; GSD-leg seats get the same treatment
76
75
  (own tab, or a shared WORKERS tab), never the ORCH tab.
77
76
  - `CONSULT · <topic>` — ONE shared tab for ALL consultants of a round, side-by-side splits; never one
@@ -102,7 +101,7 @@ through brief lineage. **An executor choice nobody made is still an executor cho
102
101
  and only then delete. A guard's home must outlive the mission that earned it. The project ledger does
103
102
  NOT count as that home — `CLAUDE.md` itself says planning records are read-only archives and current
104
103
  guidance belongs in the memory file or the shipped docs.
105
- 4. Arm the watcher (Supervision). Report the hierarchy map (pane ids + names) to the user.
104
+ 4. Arm the watcher and your own supervision beat (Supervision). Report the hierarchy map (pane ids + names) to the user.
106
105
 
107
106
  ## Supervising tickmarkr as the executor — WHO DOES WHAT
108
107
 
@@ -114,7 +113,7 @@ and the first thing to get right is that **almost none of it is yours**.
114
113
  | | ORCHESTRATOR | OVERSEER |
115
114
  |---|---|---|
116
115
  | `compile` · `plan` · `run` · `resume` | **owns** | never |
117
- | journal watchers, live surface, dialog watchers | **owns** | watches the ORCHESTRATOR, not the run |
116
+ | journal and dialog watchers; verifying the daemon's live surface | **owns** | watches the ORCHESTRATOR, not the run |
118
117
  | orphan sweeps, worker pane hygiene | **owns** | — |
119
118
  | reading a gate failure and assembling its evidence | **owns** | reads the file it writes |
120
119
  | **deciding** a gate, spend, or ship | never | **owns** |
@@ -134,10 +133,11 @@ journal tail to decide what happens next, or sweeping orphans — you have taken
134
133
 
135
134
  ### What the ORCHESTRATOR does, and what you require of it
136
135
 
137
- - **A live surface.** `tickmarkr run` is stdout-silent until run-end by design, and the run spawns its own
138
- watch board (`role: "watch"`, one per run) — **look for that pane before building anything.** Do NOT use
139
- `tickmarkr status --watch` as the surface: `status <runId>` has reported the WRONG run, so a board built
140
- on it shows a previous milestone's numbers under the current run's id.
136
+ - **The live surface arrives with the run.** `tickmarkr run` is stdout-silent until run-end by design;
137
+ its daemon self-places one shipped `role: "watch"` board beside the supervising seat and pins that
138
+ board to the daemon's run id. **Look for the matching daemon-placed pane.** If it is absent, treat that
139
+ as a daemon/run liveness fault and use the normal recovery path; never hand-place, hand-roll, or launch
140
+ a replacement live surface.
141
141
  - **The journal is the source of truth**, not panes. Watchers go on `run-end` / `task-human` /
142
142
  `task-failed` / `consult-verdict`; never sleep-poll inside an agent turn. **Never key a watcher on an
143
143
  agent's `done`** — that is turn end and fires the moment a seat finishes acknowledging you.
@@ -377,6 +377,26 @@ they are left implicit:
377
377
 
378
378
  ## Supervision watcher
379
379
 
380
+ **Arm your OWN tier first, in the same call chain that arms everything else.** `status` derives each
381
+ tier's state from a beat file the tier itself writes, so a seat that never beats reads `ABSENT` — and
382
+ `ABSENT` means *never armed*, which is a lie about a seat that is working the run. Measured on the P99
383
+ run: `orchestrator ARMED / overseer ABSENT / watch ABSENT` for the whole milestone, with a live overseer
384
+ watching it. Two thirds of that line were constants, not measurements.
385
+
386
+ The beat is one shipped command and the loop is yours, run from the repo root as its own
387
+ `run_in_background` Bash call:
388
+
389
+ ```bash
390
+ cd <repo> && while :; do tickmarkr beat overseer; sleep 10; done # 10s = SUPERVISION_BEAT_MS
391
+ tickmarkr beat overseer --stand-down # at stand-down, in the same act
392
+ ```
393
+
394
+ One beat per invocation, deliberately: the loop is what proves the seat is alive, so a command that
395
+ kept beating on its own would keep reporting a dead seat as healthy. Stop the loop — or die — and the
396
+ tier ages to `STALE` (never `ABSENT`) within six beats, which is the state that says *armed, then lost*.
397
+ Stand down explicitly when you hand off, or a deliberate exit reads as a death. Same rule as rule 29
398
+ below, now with a conventional path the other tier already reads: `tickmarkr status` shows it.
399
+
380
400
  Arm the bundled watcher as its OWN Bash call with `run_in_background` — chaining it after other commands
381
401
  with `&` orphans it from the wake chain. It prints one wake reason and exits; re-arm after every wake.
382
402
 
@@ -448,9 +468,9 @@ herdr agent wait <name> --until blocked --timeout <ms> # run_in_background
448
468
  ⚠ **Its exit status is not evidence.** That command exits **0 on timeout** and **0 when the pane is gone**,
449
469
  exactly as it does on a real block — so confirm every wake by READING the pane before acting on it.
450
470
 
451
- **And note what no watcher can cover:** the adopted supervision design gives this seat zero watchers and
452
- wakes it on *product-owned signals* that do not exist until the `supervision-heartbeat` work ships. Until
453
- then the seat improvises, and an improvised set is where a whole failure class hides. A **host** permission
471
+ **And note what no watcher can cover:** the supervision beat above proves this seat is ARMED and nothing
472
+ more — it is a liveness claim, not a wake signal, so the seat still improvises the wakes, and an improvised
473
+ set is where a whole failure class hides. A **host** permission
454
474
  modal is invisible to tickmarkr entirely, so no `src/**` change closes that one — it is covered here or
455
475
  nowhere.
456
476
 
@@ -531,6 +551,10 @@ orchestrator turn boundary.
531
551
  safe 53 → floor 108"*), and it splits right only while `paneWidth/2 ≥ 108 + 2` (`herdr.ts:494`),
532
552
  otherwise **down**. Apply the same test by hand: `herdr pane layout --pane <id>`, halve the width,
533
553
  and if the halves fall under the floor, split `--direction down`.
554
+ - **The daemon-placed ORCH board is outside this manual split rule.** The halving bound protects
555
+ worker-pane trailers; the daemon, not the overseer, places the run-id-pinned shipped board beside
556
+ the supervising seat and owns its board-first width allocation. Look for that pane and do not split,
557
+ place, or recreate it.
534
558
  - **Binary splits cannot produce an even 3-column row at any width.** 220 goes to 110/55/55 whichever
535
559
  pane you split. **At a 220-col terminal the width-derived cap is TWO side-by-side panes**; a third
536
560
  seat goes below one of them, or into its own tab. "Three panes" is a *height* heuristic