tickmarkr 2.5.7 → 2.5.8
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/cli/commands/approve.js +8 -1
- package/dist/cli/commands/compile.js +7 -0
- package/dist/cli/commands/plan.d.ts +5 -0
- package/dist/cli/commands/plan.js +28 -23
- package/dist/cli/commands/resume.js +1 -1
- package/dist/cli/commands/run.js +1 -1
- package/dist/compile/native.js +68 -7
- package/dist/compile/retired-literals.d.ts +22 -0
- package/dist/compile/retired-literals.js +271 -0
- package/dist/drivers/index.d.ts +4 -2
- package/dist/drivers/index.js +54 -6
- package/dist/gates/run-gates.d.ts +2 -0
- package/dist/gates/run-gates.js +4 -2
- package/dist/gates/test-manifest.d.ts +9 -0
- package/dist/gates/test-manifest.js +9 -1
- package/dist/gates/test-reporter.js +22 -1
- package/dist/graph/schema.d.ts +28 -0
- package/dist/graph/schema.js +13 -1
- package/dist/run/daemon.d.ts +19 -0
- package/dist/run/daemon.js +280 -78
- package/dist/run/journal.d.ts +37 -1
- package/dist/run/journal.js +43 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +1 -0
- package/dist/run/repair-disposition.d.ts +41 -0
- package/dist/run/repair-disposition.js +77 -0
- package/dist/tui/cockpit/home-view.js +45 -30
- package/dist/tui/cockpit/live-store.d.ts +18 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +54 -0
- package/skills/tickmarkr-overseer/SKILL.md +107 -0
package/dist/run/journal.d.ts
CHANGED
|
@@ -132,6 +132,42 @@ export declare function repairReachSinceApproval(events: JournalEvent[], taskId:
|
|
|
132
132
|
export declare function repairsSinceApproval(events: JournalEvent[], taskId: string): number;
|
|
133
133
|
/** Recheck releases not yet enacted by a battery (or by a legacy worker launch). */
|
|
134
134
|
export declare function pendingRechecks(events: JournalEvent[]): Set<string>;
|
|
135
|
+
/**
|
|
136
|
+
* OBS-1075: what an accepted approval still authorises, typed by its release. `worker` funds one
|
|
137
|
+
* dispatch (plain, attempt-cap, review-upheld, scope-request); `battery` funds the recheck battery
|
|
138
|
+
* and no worker; `waiver` satisfies exactly its named gate; `inert` authorises nothing.
|
|
139
|
+
*/
|
|
140
|
+
export type PendingApprovalAction = {
|
|
141
|
+
taskId: string;
|
|
142
|
+
ts: string;
|
|
143
|
+
authority: "worker";
|
|
144
|
+
release: "plain" | typeof ATTEMPT_CAP_RELEASE | typeof REVIEW_UPHELD_RELEASE | "scope-request";
|
|
145
|
+
} | {
|
|
146
|
+
taskId: string;
|
|
147
|
+
ts: string;
|
|
148
|
+
authority: "battery";
|
|
149
|
+
release: typeof RECHECK_RELEASE;
|
|
150
|
+
} | {
|
|
151
|
+
taskId: string;
|
|
152
|
+
ts: string;
|
|
153
|
+
authority: "waiver";
|
|
154
|
+
release: typeof GATE_SATISFIED_RELEASE;
|
|
155
|
+
gate: GateName;
|
|
156
|
+
} | {
|
|
157
|
+
taskId: string;
|
|
158
|
+
ts: string;
|
|
159
|
+
authority: "inert";
|
|
160
|
+
release: unknown;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Per task, the newest task-approved row no later enactment has consumed. Pure: events in, typed
|
|
164
|
+
* actions out. Acceptance is not enactment and a run boundary is not enactment — only the row the
|
|
165
|
+
* release causes consumes it (ENACTED_BY), so an approval accepted just before an abnormal exit
|
|
166
|
+
* survives every restart until it is enacted. A later approval supersedes an earlier one. An unknown
|
|
167
|
+
* release, or a waiver without a known gate, is inert: it supersedes, authorises nothing, and no row
|
|
168
|
+
* enacts it.
|
|
169
|
+
*/
|
|
170
|
+
export declare function pendingApprovalActions(events: JournalEvent[]): Map<string, PendingApprovalAction>;
|
|
135
171
|
/**
|
|
136
172
|
* Why the last attempt failed, one row per journaled cause, in the daemon's own `source: details`
|
|
137
173
|
* shape. The daemon builds that brief in a loop-local variable, which dies with the process: a resumed
|
|
@@ -240,6 +276,7 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
240
276
|
"gate-fail": "gate-fail";
|
|
241
277
|
quota: "quota";
|
|
242
278
|
infra: "infra";
|
|
279
|
+
"scope-request": "scope-request";
|
|
243
280
|
dispatch: "dispatch";
|
|
244
281
|
"human-gate": "human-gate";
|
|
245
282
|
"reroute-exhausted": "reroute-exhausted";
|
|
@@ -247,7 +284,6 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
247
284
|
"merge-conflict": "merge-conflict";
|
|
248
285
|
"tip-moved": "tip-moved";
|
|
249
286
|
authoring: "authoring";
|
|
250
|
-
"scope-request": "scope-request";
|
|
251
287
|
"diff-cap": "diff-cap";
|
|
252
288
|
}>>;
|
|
253
289
|
tokens: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
package/dist/run/journal.js
CHANGED
|
@@ -620,6 +620,49 @@ export function pendingRechecks(events) {
|
|
|
620
620
|
}
|
|
621
621
|
return pending;
|
|
622
622
|
}
|
|
623
|
+
const ENACTED_BY = {
|
|
624
|
+
worker: ["task-dispatch", "worker-launch"],
|
|
625
|
+
battery: ["recheck-battery"],
|
|
626
|
+
waiver: ["worktree-recreation"],
|
|
627
|
+
};
|
|
628
|
+
/**
|
|
629
|
+
* Per task, the newest task-approved row no later enactment has consumed. Pure: events in, typed
|
|
630
|
+
* actions out. Acceptance is not enactment and a run boundary is not enactment — only the row the
|
|
631
|
+
* release causes consumes it (ENACTED_BY), so an approval accepted just before an abnormal exit
|
|
632
|
+
* survives every restart until it is enacted. A later approval supersedes an earlier one. An unknown
|
|
633
|
+
* release, or a waiver without a known gate, is inert: it supersedes, authorises nothing, and no row
|
|
634
|
+
* enacts it.
|
|
635
|
+
*/
|
|
636
|
+
export function pendingApprovalActions(events) {
|
|
637
|
+
const pending = new Map();
|
|
638
|
+
for (const e of events) {
|
|
639
|
+
if (!e.taskId)
|
|
640
|
+
continue;
|
|
641
|
+
if (e.event === "task-approved") {
|
|
642
|
+
pending.set(e.taskId, approvalAction(e.taskId, e));
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const action = pending.get(e.taskId);
|
|
646
|
+
if (action && action.authority !== "inert" && ENACTED_BY[action.authority].includes(e.event))
|
|
647
|
+
pending.delete(e.taskId);
|
|
648
|
+
}
|
|
649
|
+
return pending;
|
|
650
|
+
}
|
|
651
|
+
function approvalAction(taskId, e) {
|
|
652
|
+
const { release, gate } = e.data;
|
|
653
|
+
const base = { taskId, ts: e.ts };
|
|
654
|
+
if (release === undefined)
|
|
655
|
+
return { ...base, authority: "worker", release: "plain" };
|
|
656
|
+
if (release === ATTEMPT_CAP_RELEASE || release === REVIEW_UPHELD_RELEASE || release === "scope-request") {
|
|
657
|
+
return { ...base, authority: "worker", release };
|
|
658
|
+
}
|
|
659
|
+
if (release === RECHECK_RELEASE)
|
|
660
|
+
return { ...base, authority: "battery", release };
|
|
661
|
+
if (release === GATE_SATISFIED_RELEASE && typeof gate === "string" && GATE_NAMES.includes(gate)) {
|
|
662
|
+
return { ...base, authority: "waiver", release, gate: gate };
|
|
663
|
+
}
|
|
664
|
+
return { ...base, authority: "inert", release };
|
|
665
|
+
}
|
|
623
666
|
// Both retry decisions below govern exactly ONE dispatch: the next one. So both are read back from the
|
|
624
667
|
// journal at the moment that dispatch is built, never carried in a process variable — a stop between
|
|
625
668
|
// the decision and the dispatch (OBS-254's shape, one layer up) would otherwise send a normal prompt
|
package/dist/run/merge.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface TipVerifyResult {
|
|
|
12
12
|
spawnedCommand?: string;
|
|
13
13
|
/** Q121s: nonzero exit whose failures are ALL baseline-recorded — forgiven exactly as the battery forgives. */
|
|
14
14
|
forgiven?: boolean;
|
|
15
|
+
/** D-131: carried from a persisted per-gate verdict — this cycle did NOT execute the command. */
|
|
16
|
+
reused?: true;
|
|
15
17
|
/**
|
|
16
18
|
* OBS-534: what a nonzero exit is evidence OF, taken from the battery's own readers — `ceilingKillResult`
|
|
17
19
|
* for a kill, the shared runner classifier for everything else. `infra` means nothing was verified.
|
package/dist/run/merge.js
CHANGED
|
@@ -184,6 +184,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
|
|
|
184
184
|
gate,
|
|
185
185
|
cmd,
|
|
186
186
|
pass: hit.pass,
|
|
187
|
+
reused: true,
|
|
187
188
|
exitCode: hit.exitCode ?? 0,
|
|
188
189
|
fingerprints: hit.meta?.fingerprints ?? [],
|
|
189
190
|
...(hit.meta?.forgiven ? { forgiven: true } : {}),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { GateResult } from "../gates/types.js";
|
|
2
|
+
/** Pure decision table of the scope-red disposition (OBS-1077, OBS-1074 residual): no filesystem,
|
|
3
|
+
* process, clock or journal. The caller supplies the inventory and performs every side effect. */
|
|
4
|
+
export type RepairDispositionKind = "fund-repair" | "scope-request" | "authoring" | "none";
|
|
5
|
+
export interface RepairDisposition {
|
|
6
|
+
kind: RepairDispositionKind;
|
|
7
|
+
/** validated inventory paths only — the sole paths a caller may put in an executable command */
|
|
8
|
+
paths: string[];
|
|
9
|
+
reason: string;
|
|
10
|
+
/** which evidence decided: a gate name, "worker" for a refusal, absent for the ordinary disposition */
|
|
11
|
+
source?: string;
|
|
12
|
+
/** an independent classification that kept priority over path prose */
|
|
13
|
+
blocker?: "infra" | "scope-collateral";
|
|
14
|
+
/** candidates that bound to nothing, or to more than one path; evidence, never approval advice */
|
|
15
|
+
diagnostics: ScopeHintDiagnostic[];
|
|
16
|
+
}
|
|
17
|
+
export interface ScopeHintDiagnostic {
|
|
18
|
+
candidate: string;
|
|
19
|
+
kind: "unresolved" | "ambiguous";
|
|
20
|
+
matches: string[];
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ScopeHintResolution {
|
|
24
|
+
resolved: string[];
|
|
25
|
+
diagnostics: ScopeHintDiagnostic[];
|
|
26
|
+
}
|
|
27
|
+
export interface RepairDispositionInput {
|
|
28
|
+
/** the failing gate results (passing rows are ignored) */
|
|
29
|
+
results: readonly GateResult[];
|
|
30
|
+
files: readonly string[];
|
|
31
|
+
/** the worker's refusal summary — present only when the worker reported ok:false */
|
|
32
|
+
refusalSummary?: string;
|
|
33
|
+
/** task tree plus diff, deleted tracked paths included */
|
|
34
|
+
inventory: Iterable<string>;
|
|
35
|
+
}
|
|
36
|
+
/** Lexing never starts in the middle of a word (same lexer as the daemon's scope-red disposition). */
|
|
37
|
+
export declare const namedPaths: (text: string) => string[];
|
|
38
|
+
/** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
|
|
39
|
+
* path-suffix. Anything unbound or bound twice comes back as a non-executable diagnostic. */
|
|
40
|
+
export declare function resolveScopeHints(candidates: Iterable<string>, inventory: Iterable<string>): ScopeHintResolution;
|
|
41
|
+
export declare function classifyRepairDisposition(input: RepairDispositionInput): RepairDisposition;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { filesGlob } from "../graph/files-glob.js";
|
|
2
|
+
const failed = (g) => !(g.pass || g.meta?.skipped === true) || g.meta?.infra === true;
|
|
3
|
+
const REFUSAL_RE = /outside|out.of.scope|allowlist|scope expansion|not (?:in|own)|unowned/i;
|
|
4
|
+
/** Lexing never starts in the middle of a word (same lexer as the daemon's scope-red disposition). */
|
|
5
|
+
export const namedPaths = (text) => [...text.matchAll(/(?:^|[\s`'"])((?:[A-Za-z0-9_@.()[\]-]+\/)+[A-Za-z0-9_@.[\]-]+|[A-Za-z0-9_@-]+(?:\.[A-Za-z0-9_-]+)+)(?=$|[\s`'"),:;.!?])/g)]
|
|
6
|
+
.map((match) => match[1].replace(/^\.\//, "").replace(/\.$/, ""))
|
|
7
|
+
.filter((path) => !path.split("/").includes(".."));
|
|
8
|
+
/** Binds candidates to the SUPPLIED inventory and nothing else: an exact path, else a unique
|
|
9
|
+
* path-suffix. Anything unbound or bound twice comes back as a non-executable diagnostic. */
|
|
10
|
+
export function resolveScopeHints(candidates, inventory) {
|
|
11
|
+
const known = new Set(inventory);
|
|
12
|
+
const resolved = new Set();
|
|
13
|
+
const diagnostics = [];
|
|
14
|
+
for (const candidate of new Set(candidates)) {
|
|
15
|
+
if (known.has(candidate)) {
|
|
16
|
+
resolved.add(candidate);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const matches = [...known].filter((path) => path.endsWith(`/${candidate}`)).sort();
|
|
20
|
+
if (matches.length === 1)
|
|
21
|
+
resolved.add(matches[0]);
|
|
22
|
+
else
|
|
23
|
+
diagnostics.push(matches.length === 0
|
|
24
|
+
? { candidate, kind: "unresolved", matches, reason: `scope hint resolves to nothing in the task tree: ${candidate}` }
|
|
25
|
+
: { candidate, kind: "ambiguous", matches, reason: `scope hint ${candidate} matches ${matches.length} paths: ${matches.join(", ")}` });
|
|
26
|
+
}
|
|
27
|
+
return { resolved: [...resolved].sort(), diagnostics };
|
|
28
|
+
}
|
|
29
|
+
export function classifyRepairDisposition(input) {
|
|
30
|
+
const reds = input.results.filter(failed);
|
|
31
|
+
const none = (reason, extra = {}) => ({ kind: "none", paths: [], reason, diagnostics: [], ...extra });
|
|
32
|
+
// Independent classifications first: prose naming a path never erases them.
|
|
33
|
+
const infra = reds.find((g) => g.meta?.infra === true);
|
|
34
|
+
if (infra)
|
|
35
|
+
return none(`${infra.gate}: infra red keeps its own classification`, { source: infra.gate, blocker: "infra" });
|
|
36
|
+
const verdict = reds.find((g) => g.gate === "scope")?.meta?.collateral;
|
|
37
|
+
if (verdict) {
|
|
38
|
+
return verdict.authoring
|
|
39
|
+
? { kind: "authoring", paths: [...verdict.predicted], reason: verdict.repair, source: "scope", blocker: "scope-collateral", diagnostics: [] }
|
|
40
|
+
: none("scope: collateral verdict has unpredicted offenders — ordinary chargeable disposition", { source: "scope", blocker: "scope-collateral" });
|
|
41
|
+
}
|
|
42
|
+
if (input.files.length === 0)
|
|
43
|
+
return none("task declares no files[]");
|
|
44
|
+
const allowed = filesGlob([...input.files]);
|
|
45
|
+
const inventory = [...input.inventory];
|
|
46
|
+
const unowned = (text) => {
|
|
47
|
+
const r = resolveScopeHints(namedPaths(text).filter((path) => !allowed(path)), inventory);
|
|
48
|
+
return { paths: r.resolved.filter((path) => !allowed(path)), diagnostics: r.diagnostics };
|
|
49
|
+
};
|
|
50
|
+
const owned = (text) => resolveScopeHints(namedPaths(text), inventory).resolved.some(allowed);
|
|
51
|
+
const refusal = input.refusalSummary !== undefined && REFUSAL_RE.test(input.refusalSummary)
|
|
52
|
+
? unowned(input.refusalSummary) : { paths: [], diagnostics: [] };
|
|
53
|
+
// A test gate names where the ASSERTION lives — a detection site, not the code that needs fixing.
|
|
54
|
+
const sites = reds.filter((g) => g.gate === "test").map((g) => unowned(g.details));
|
|
55
|
+
const others = reds.filter((g) => g.gate !== "test");
|
|
56
|
+
const hints = others.map((g) => unowned(g.details));
|
|
57
|
+
const diagnostics = [refusal, ...sites, ...hints].flatMap((r) => r.diagnostics)
|
|
58
|
+
.filter((d, i, all) => all.findIndex((o) => o.candidate === d.candidate) === i);
|
|
59
|
+
const uniq = (rs) => [...new Set(rs.flatMap((r) => r.paths))].sort();
|
|
60
|
+
if (refusal.paths.length) {
|
|
61
|
+
return { kind: "scope-request", paths: refusal.paths, source: "worker", diagnostics,
|
|
62
|
+
reason: `worker refusal names unowned paths: ${refusal.paths.join(", ")}` };
|
|
63
|
+
}
|
|
64
|
+
const repair = uniq(hints);
|
|
65
|
+
if (repair.length && !others.some((g) => owned(g.details))) {
|
|
66
|
+
const review = others.some((g) => g.gate === "review");
|
|
67
|
+
return { kind: review ? "scope-request" : "authoring", paths: repair, source: others[0].gate, diagnostics,
|
|
68
|
+
reason: `files[] repair hint: ${repair.join(", ")}` };
|
|
69
|
+
}
|
|
70
|
+
const detection = uniq(sites);
|
|
71
|
+
if (detection.length) {
|
|
72
|
+
return { kind: "fund-repair", paths: detection, source: "test", diagnostics,
|
|
73
|
+
reason: `test gate names a detection site, not an out-of-scope repair: ${detection.join(", ")}` };
|
|
74
|
+
}
|
|
75
|
+
// Unknown attribution never blocks funding: the ordinary chargeable disposition applies.
|
|
76
|
+
return none("no attributable out-of-scope repair — ordinary chargeable disposition", { diagnostics });
|
|
77
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, useInput } from "ink";
|
|
3
|
-
import { useLayoutEffect, useState } from "react";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { GLYPHS } from "../../brand.js";
|
|
5
5
|
import { BodyText, CockpitGrid, Panel, ProgressMeter, Sparkline, StateGlyph, } from "./components.js";
|
|
6
6
|
import { ABSENT_FIELD, fieldReading } from "./derive.js";
|
|
@@ -146,75 +146,90 @@ export function HomeView({ model, width, focused = true, onOpenPark, onOpenDiagn
|
|
|
146
146
|
const maxOffset = Math.max(0, model.activity.length - 1);
|
|
147
147
|
const maxSelection = Math.max(0, model.activity.length - 1);
|
|
148
148
|
const safeOffset = Math.min(offset, maxOffset);
|
|
149
|
+
// Keep input ahead of the next render/effect subscription, as in EvidenceView.
|
|
150
|
+
// Render reconciles the refs with model updates; each move writes before setting state.
|
|
151
|
+
const sectionRef = useRef(effectiveSection);
|
|
152
|
+
const needsYouIndexRef = useRef(effectiveNeedsYouIndex);
|
|
153
|
+
const activityIndexRef = useRef(effectiveActivityIndex);
|
|
154
|
+
sectionRef.current = effectiveSection;
|
|
155
|
+
needsYouIndexRef.current = effectiveNeedsYouIndex;
|
|
156
|
+
activityIndexRef.current = effectiveActivityIndex;
|
|
157
|
+
const moveSection = (next) => {
|
|
158
|
+
sectionRef.current = next;
|
|
159
|
+
setSection(next);
|
|
160
|
+
};
|
|
161
|
+
const moveNeedsYou = (index) => {
|
|
162
|
+
needsYouIndexRef.current = index;
|
|
163
|
+
setNeedsYouIndex(index);
|
|
164
|
+
setSelectedNeedsYouId(model.needsYou[index]?.id);
|
|
165
|
+
};
|
|
166
|
+
const moveActivity = (index) => {
|
|
167
|
+
activityIndexRef.current = index;
|
|
168
|
+
setActivityIndex(index);
|
|
169
|
+
setSelectedEvidenceId(model.activity[index]?.evidence.id);
|
|
170
|
+
};
|
|
149
171
|
useInput((_input, key) => {
|
|
150
172
|
if (key.leftArrow && model.needsYou.length > 0) {
|
|
151
|
-
if (
|
|
152
|
-
const nextIndex = (
|
|
153
|
-
|
|
154
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
173
|
+
if (sectionRef.current === "needsYou") {
|
|
174
|
+
const nextIndex = (needsYouIndexRef.current + 1) % model.needsYou.length;
|
|
175
|
+
moveNeedsYou(nextIndex);
|
|
155
176
|
}
|
|
156
177
|
else {
|
|
157
|
-
|
|
178
|
+
moveSection("needsYou");
|
|
158
179
|
}
|
|
159
180
|
}
|
|
160
181
|
if (key.rightArrow)
|
|
161
|
-
|
|
182
|
+
moveSection("activity");
|
|
162
183
|
if (key.upArrow) {
|
|
163
|
-
if (
|
|
184
|
+
if (sectionRef.current === "needsYou") {
|
|
164
185
|
if (model.needsYou.length > 0) {
|
|
165
|
-
const nextIndex = (
|
|
166
|
-
|
|
167
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
186
|
+
const nextIndex = (needsYouIndexRef.current - 1 + model.needsYou.length) % model.needsYou.length;
|
|
187
|
+
moveNeedsYou(nextIndex);
|
|
168
188
|
}
|
|
169
189
|
}
|
|
170
190
|
else {
|
|
171
191
|
if (model.activity.length > 0) {
|
|
172
|
-
const nextIndex = Math.max(0,
|
|
173
|
-
|
|
174
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
192
|
+
const nextIndex = Math.max(0, activityIndexRef.current - 1);
|
|
193
|
+
moveActivity(nextIndex);
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
196
|
}
|
|
178
197
|
if (key.downArrow) {
|
|
179
|
-
if (
|
|
198
|
+
if (sectionRef.current === "needsYou") {
|
|
180
199
|
if (model.needsYou.length > 0) {
|
|
181
|
-
const nextIndex = (
|
|
182
|
-
|
|
183
|
-
setSelectedNeedsYouId(model.needsYou[nextIndex]?.id);
|
|
200
|
+
const nextIndex = (needsYouIndexRef.current + 1) % model.needsYou.length;
|
|
201
|
+
moveNeedsYou(nextIndex);
|
|
184
202
|
}
|
|
185
203
|
}
|
|
186
204
|
else {
|
|
187
205
|
if (model.activity.length > 0) {
|
|
188
|
-
const nextIndex = Math.min(maxSelection,
|
|
189
|
-
|
|
190
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
206
|
+
const nextIndex = Math.min(maxSelection, activityIndexRef.current + 1);
|
|
207
|
+
moveActivity(nextIndex);
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
}
|
|
194
211
|
if (key.pageDown) {
|
|
195
212
|
setOffset((o) => Math.max(0, o - HOME_ACTIVITY_VISIBLE_ROWS));
|
|
196
213
|
if (model.activity.length > 0) {
|
|
197
|
-
const nextIndex = Math.max(0,
|
|
198
|
-
|
|
199
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
214
|
+
const nextIndex = Math.max(0, activityIndexRef.current - HOME_ACTIVITY_VISIBLE_ROWS);
|
|
215
|
+
moveActivity(nextIndex);
|
|
200
216
|
}
|
|
201
217
|
}
|
|
202
218
|
if (key.pageUp) {
|
|
203
219
|
setOffset((o) => Math.min(maxOffset, o + HOME_ACTIVITY_VISIBLE_ROWS));
|
|
204
220
|
if (model.activity.length > 0) {
|
|
205
|
-
const nextIndex = Math.min(maxSelection,
|
|
206
|
-
|
|
207
|
-
setSelectedEvidenceId(model.activity[nextIndex]?.evidence.id);
|
|
221
|
+
const nextIndex = Math.min(maxSelection, activityIndexRef.current + HOME_ACTIVITY_VISIBLE_ROWS);
|
|
222
|
+
moveActivity(nextIndex);
|
|
208
223
|
}
|
|
209
224
|
}
|
|
210
225
|
if (key.return) {
|
|
211
|
-
if (
|
|
212
|
-
const target =
|
|
226
|
+
if (sectionRef.current === "needsYou") {
|
|
227
|
+
const target = selectNeedsYouTarget(model, needsYouIndexRef.current);
|
|
213
228
|
if (target)
|
|
214
229
|
(target.kind === "park" ? onOpenPark : onOpenDiagnostic)(target.id);
|
|
215
230
|
}
|
|
216
231
|
else {
|
|
217
|
-
const row = model.activity[
|
|
232
|
+
const row = model.activity[activityIndexRef.current];
|
|
218
233
|
if (row)
|
|
219
234
|
onOpenEvidence(row.evidence);
|
|
220
235
|
}
|
|
@@ -121,6 +121,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
121
121
|
oracle: "test";
|
|
122
122
|
test: string;
|
|
123
123
|
text?: string | undefined;
|
|
124
|
+
landing?: string | undefined;
|
|
124
125
|
} | {
|
|
125
126
|
oracle: "judge";
|
|
126
127
|
text: string;
|
|
@@ -138,6 +139,14 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
138
139
|
confidence?: number | undefined;
|
|
139
140
|
reason?: string | undefined;
|
|
140
141
|
}[] | undefined;
|
|
142
|
+
pins?: ({
|
|
143
|
+
kind: "literal";
|
|
144
|
+
text: string;
|
|
145
|
+
glob: string;
|
|
146
|
+
} | {
|
|
147
|
+
kind: "fixture";
|
|
148
|
+
paths: string[];
|
|
149
|
+
})[] | undefined;
|
|
141
150
|
routingHints?: {
|
|
142
151
|
pin?: {
|
|
143
152
|
via: string;
|
|
@@ -213,6 +222,7 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
213
222
|
oracle: "test";
|
|
214
223
|
test: string;
|
|
215
224
|
text?: string | undefined;
|
|
225
|
+
landing?: string | undefined;
|
|
216
226
|
} | {
|
|
217
227
|
oracle: "judge";
|
|
218
228
|
text: string;
|
|
@@ -230,6 +240,14 @@ export declare function createLiveStore(options: LiveStoreOptions): {
|
|
|
230
240
|
confidence?: number | undefined;
|
|
231
241
|
reason?: string | undefined;
|
|
232
242
|
}[] | undefined;
|
|
243
|
+
pins?: ({
|
|
244
|
+
kind: "literal";
|
|
245
|
+
text: string;
|
|
246
|
+
glob: string;
|
|
247
|
+
} | {
|
|
248
|
+
kind: "fixture";
|
|
249
|
+
paths: string[];
|
|
250
|
+
})[] | undefined;
|
|
233
251
|
routingHints?: {
|
|
234
252
|
pin?: {
|
|
235
253
|
via: string;
|
package/package.json
CHANGED
|
@@ -149,6 +149,10 @@
|
|
|
149
149
|
"text": {
|
|
150
150
|
"type": "string",
|
|
151
151
|
"minLength": 1
|
|
152
|
+
},
|
|
153
|
+
"landing": {
|
|
154
|
+
"type": "string",
|
|
155
|
+
"minLength": 1
|
|
152
156
|
}
|
|
153
157
|
},
|
|
154
158
|
"required": [
|
|
@@ -201,6 +205,56 @@
|
|
|
201
205
|
]
|
|
202
206
|
}
|
|
203
207
|
},
|
|
208
|
+
"pins": {
|
|
209
|
+
"type": "array",
|
|
210
|
+
"items": {
|
|
211
|
+
"oneOf": [
|
|
212
|
+
{
|
|
213
|
+
"type": "object",
|
|
214
|
+
"properties": {
|
|
215
|
+
"kind": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"const": "literal"
|
|
218
|
+
},
|
|
219
|
+
"text": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"minLength": 1
|
|
222
|
+
},
|
|
223
|
+
"glob": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"minLength": 1
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
"required": [
|
|
229
|
+
"kind",
|
|
230
|
+
"text",
|
|
231
|
+
"glob"
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "object",
|
|
236
|
+
"properties": {
|
|
237
|
+
"kind": {
|
|
238
|
+
"type": "string",
|
|
239
|
+
"const": "fixture"
|
|
240
|
+
},
|
|
241
|
+
"paths": {
|
|
242
|
+
"minItems": 1,
|
|
243
|
+
"type": "array",
|
|
244
|
+
"items": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"minLength": 1
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
"required": [
|
|
251
|
+
"kind",
|
|
252
|
+
"paths"
|
|
253
|
+
]
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
},
|
|
204
258
|
"gates": {
|
|
205
259
|
"default": [
|
|
206
260
|
"build",
|
|
@@ -288,6 +288,58 @@ journal tail to decide what happens next, or sweeping orphans — you have taken
|
|
|
288
288
|
3. **Gate a mid-run fix at the base, not at a summary (law 47 / OBS-909).** A fix landed on main while a
|
|
289
289
|
run is live is proved with `tickmarkr verify --base <main>`, never with a suite summary copied from a
|
|
290
290
|
different tree. A release proof runs every CI-ordered step — including lint — before its suite.
|
|
291
|
+
4. **Rescue a broken harness from OUTSIDE the run it broke — the daemon is NEVER asked to repair itself
|
|
292
|
+
as a task (v2.5.7 ledger D-59, D-64; OBS-1078).** When the harness is the defect — resume cannot be
|
|
293
|
+
relied on, a gate cannot be relied on, the binary every gate runs on is the thing that is wrong — no
|
|
294
|
+
task inside the run can fix it: the gates that would judge the repair are executed BY the suspect
|
|
295
|
+
binary, and the worktree the repair would run in is created by the same code. Dispatching
|
|
296
|
+
*"fix the daemon"* as a task of the run it broke buys a green gate from the defect and a repair nobody
|
|
297
|
+
can trust. The rescue route has three legs, each with its own record, and there is no fourth:
|
|
298
|
+
- **AN INDEPENDENTLY GATED EXTERNAL FIX LEG.** Its own checkout and branch off the run's base, its own
|
|
299
|
+
brief, its own `files[]`, its own battery — `tickmarkr verify --base <ref> --criteria <file>
|
|
300
|
+
--author <the seat that wrote it>` — plus the cross-vendor review its criterion names, all recorded
|
|
301
|
+
BEFORE the run consumes any of it. The leg's battery is scheduled at a boundary item 2 permits (the
|
|
302
|
+
run parked or ended, or the operator's recorded exception), and its reds are declared to the
|
|
303
|
+
contamination watcher in advance so they are classified instead of assumed.
|
|
304
|
+
- **AN EXPLICITLY RECORDED REBUILT BINARY AND RESTART — the provenance is written down in the same act,
|
|
305
|
+
or the rebuild did not happen.** Record: the fix leg's commit sha; the build command; the install
|
|
306
|
+
form (never `npm i -g .` on the repository directory — npm SYMLINKS a directory install and every
|
|
307
|
+
later build hot-swaps the machine-wide binary with no version change to notice it by); the
|
|
308
|
+
global-versus-repo **inode** comparison that proves a real install, which `tickmarkr version` cannot
|
|
309
|
+
do because it cannot go red when nothing is bumped; the version read-back; and the restart itself —
|
|
310
|
+
`tickmarkr resume <runId>`, the ORCHESTRATOR's command, never yours. An unrecorded rebuild leaves the
|
|
311
|
+
next seat arguing about which binary produced which journal rows, with nothing to read.
|
|
312
|
+
- **A SEPARATELY GATED UNION.** The rebuilt harness and the run's existing work are then gated
|
|
313
|
+
TOGETHER, on their own, at the one commit that carries both. A green fix leg beside a green run is
|
|
314
|
+
no proof of their union — see *a green leg beside a green parent*, under the release criterion
|
|
315
|
+
below — because the union's own conflict resolutions, ordering and collected-count changes are
|
|
316
|
+
exactly what neither green ever saw.
|
|
317
|
+
**NO BRANCH SURGERY: the run keeps its ORIGINAL base and its recorded baseline.** No rebase of the
|
|
318
|
+
integration branch, no re-cut `baseRef`, no re-baselining to make the rescue leg's diff read small.
|
|
319
|
+
Resume REPLAYS the journal's `baseRef`, so a rewritten one is a different run wearing the same id —
|
|
320
|
+
and a baseline re-recorded to suit the rescue launders every red the original baseline was there to
|
|
321
|
+
compare against.
|
|
322
|
+
5. **A rebuilt binary is NOT a rebuilt tree: a claimed fix arrival needs ANCESTRY EVIDENCE naming the
|
|
323
|
+
actual task subject (v2.5.7 ledger D-80; OBS-1078).** Rebuilding and reinstalling `dist` changes the
|
|
324
|
+
daemon the orchestrator launches and nothing else. It puts no test, fixture, schema or skill byte into
|
|
325
|
+
any task worktree or into the integration branch: those trees were checked out from the integration tip
|
|
326
|
+
BEFORE the fix leg landed, and git does not retro-fill a checkout that already exists. So *"the fix has
|
|
327
|
+
reached the task checkouts"* is never accepted on a version read-back, a rebuild log, or the fixer's
|
|
328
|
+
say-so — require ancestry evidence naming the ACTUAL subjects, and read it before the claim, not after:
|
|
329
|
+
- **the TASK subject** — that task worktree's own branch and HEAD sha (`git -C <task-worktree> rev-parse
|
|
330
|
+
--abbrev-ref HEAD` and `git -C <task-worktree> rev-parse HEAD`), with the fix proven an ancestor of
|
|
331
|
+
THAT commit: `git -C <task-worktree> merge-base --is-ancestor <fix-sha> HEAD`, whose exit code is the
|
|
332
|
+
evidence, recorded beside all three shas;
|
|
333
|
+
- **the INTEGRATION subject** — the integration-branch commit that worktree was created from, named by
|
|
334
|
+
sha from the journal's `task-dispatch` row or `tickmarkr status`; never "the run", and never the
|
|
335
|
+
branch name alone, which moves while you are reading it;
|
|
336
|
+
- **and, where the fix is a FILE the tree must hold** — a new test, a fixture, a schema — its presence
|
|
337
|
+
at that commit: `git -C <task-worktree> cat-file -e <task-head>:<path>`. A fix that lives only in the
|
|
338
|
+
daemon's `dist` cannot make a worker's missing oracle appear, and a worker red on that missing file is
|
|
339
|
+
still a plan defect, not a retry.
|
|
340
|
+
A fix present in the daemon and absent from the task trees has been INSTALLED, not ARRIVED: say which
|
|
341
|
+
of the two you mean, and name the commit each half was read against. Evidence rule 13's baseRef trap is
|
|
342
|
+
the same defect wearing a diff instead of a claim.
|
|
291
343
|
|
|
292
344
|
### What the ORCHESTRATOR does, and what you require of it
|
|
293
345
|
|
|
@@ -383,12 +435,67 @@ its own — **not because any instrument detected it.**
|
|
|
383
435
|
> covers — and a re-scope of any named subject voids it AUTOMATICALLY, with no ruling required.**
|
|
384
436
|
> A void condition that needs a ruling to fire is not a void condition; it is a second thing to forget.
|
|
385
437
|
|
|
438
|
+
**⚡ ONE QUALIFICATION, PROSPECTIVE ONLY: A FUTURE SEAL MAY DECLARE THAT AN AUDITED FILES-ONLY AMENDMENT
|
|
439
|
+
DOES NOT VOID IT — AND THAT DECLARATION HAS TO BE IN THE SEAL, WRITTEN AT ISSUE (v2.5.7 ledger D-80, D-83;
|
|
440
|
+
OBS-1078).** The automatic-void rule above is QUALIFIED for seals issued after this clause, never deleted:
|
|
441
|
+
it still fires on its own terms for every seal that does not carry the declaration. A requirements seal may
|
|
442
|
+
declare, prospectively, that exactly one narrow class of change does not void it — an **AUDITED FILES-ONLY
|
|
443
|
+
AMENDMENT**: a recorded amendment to a task's `files[]` and nothing else, which leaves all four of these
|
|
444
|
+
UNTOUCHED.
|
|
445
|
+
|
|
446
|
+
1. **the ACCEPTANCE TEXT** — every criterion's own words, byte for byte, a `test:` leaf title included;
|
|
447
|
+
2. **the TASK IDENTITY** — which task is which: its id, its shape, its deps, the suites it owns;
|
|
448
|
+
3. **the RUN IDENTITY** — the run, its base and its `baseRef`, the integration branch and its tip commit;
|
|
449
|
+
4. **the SAFETY REQUIREMENTS** — the gates, bounds, refusals and contamination rules the run is held to.
|
|
450
|
+
|
|
451
|
+
**Replay it both ways, because the clause is a procedure and not a mood:**
|
|
452
|
+
- *A seal carrying that declaration, then an audited amendment adding one owned path to a task's `files[]`,
|
|
453
|
+
with all four invariants read and found unchanged* → **THE SEAL IS KEPT.** No successor seal and no
|
|
454
|
+
re-grade of what it already sealed: the declaration was the seal's own pre-commitment about this exact
|
|
455
|
+
change, made before the change existed — the only moment such a commitment can honestly be made. Read the
|
|
456
|
+
four invariants BEFORE ruling, and record the read beside the amendment.
|
|
457
|
+
- *The same seal, then an amendment that changes the RUN IDENTITY — a new run id, a re-cut `baseRef`, a
|
|
458
|
+
different integration tip* → **THE SEAL IS VOID, on its own declaration's terms.** Run identity is one of
|
|
459
|
+
the four, so *"files-only"* does not describe that amendment; moving any one of the four puts the change
|
|
460
|
+
outside the declared class, and **any other change of any kind needs a SUCCESSOR SEAL** — a new document,
|
|
461
|
+
sealed before the work it grades, naming the subject as it now stands.
|
|
462
|
+
|
|
463
|
+
**Three limits, and each one has been argued around:**
|
|
464
|
+
- **The declaration is never retroactive and never inferred.** A seal issued BEFORE this change — one whose
|
|
465
|
+
own text does not carry the declaration — **stays governed by the existing void conditions above,
|
|
466
|
+
unmodified: any re-scope, narrowing, widening, split, merge or re-ownership of a named subject voids it
|
|
467
|
+
AUTOMATICALLY, with no ruling required.** So *"the acceptance text never changed, therefore the seal
|
|
468
|
+
still holds"* is **REFUSED** for such a seal once its SUBJECT has moved: unchanged acceptance text alone
|
|
469
|
+
preserves nothing over a changed subject, because a seal grades the subject it named, not the sentences
|
|
470
|
+
it happened to be written in. Retrofitting the declaration onto an already-sealed document is itself an
|
|
471
|
+
amendment to a sealed subject — write the successor seal instead.
|
|
472
|
+
- **A files-only clause confers no amendment authority.** It says nothing about whether the amendment is
|
|
473
|
+
LAWFUL: compile's unit bounds still refuse an over-bound `files[]`, and no seal can widen what compile
|
|
474
|
+
will certify.
|
|
475
|
+
- **AUDITED means recorded, or it did not happen.** The approval row carrying the amended files, the
|
|
476
|
+
before-and-after `files[]`, and the four-invariant read are what let the next seat replay your ruling
|
|
477
|
+
instead of trusting it. An unrecorded *"files-only"* amendment is indistinguishable from a re-scope, and
|
|
478
|
+
is voided as one.
|
|
479
|
+
|
|
386
480
|
**⚡ IDENTIFY THE SUBJECT BY WHAT THE CLAIM IS ABOUT. Half of the failure above was a category error, and
|
|
387
481
|
it is the cheap half to fix:** a graph hash identifies a **PLAN**, and a plan is recompiled, re-cut and
|
|
388
482
|
re-owned as a matter of course. **A criterion about a SHIPPED TREE names the COMMIT** — or the tag, or the
|
|
389
483
|
export tree hash — **never a graph hash, never a run id, never a task list.** Ask what a reader would have
|
|
390
484
|
to hold in their hand to check the clause: if it is bytes, name the bytes.
|
|
391
485
|
|
|
486
|
+
**⚡ A GREEN LEG BESIDE A GREEN PARENT IS NO PROOF OF THEIR UNION: THE RELEASE PROOF BINDS TO THE MERGED
|
|
487
|
+
CANDIDATE'S COMMIT, AND IS VOID THE MOMENT THAT SUBJECT MOVES (v2.5.7 ledger D-80, D-83; OBS-1078).** The
|
|
488
|
+
subject of a release proof is never *"the run"* and never *"the fix leg"* — it is the **MERGED candidate**:
|
|
489
|
+
the one commit that carries the run's work AND every leg merged into it. Name that commit's sha in the
|
|
490
|
+
proof, in the same act as the grade, and the proof is bound to that sha alone. **When the subject moves — a
|
|
491
|
+
later merge, a new integration tip, an amended or re-cut commit, a fresh export, a re-tag — the proof is
|
|
492
|
+
VOID, and the new subject is graded from scratch**, count oracle included, since the oracle is derived from
|
|
493
|
+
the merged tree and not from either parent. Two greens assembled into one verdict are two claims about two
|
|
494
|
+
trees that never contained each other: the union's own conflict resolutions, its ordering and its
|
|
495
|
+
collected-test total are exactly what neither green observed — which is why `RELEASING.md` step 4 matches
|
|
496
|
+
the run's head SHA to the mirror's `HEAD` before a single job log is graded. This is the void-condition duty
|
|
497
|
+
pointed at bytes: **a proof that names no commit cannot notice its subject leaving.**
|
|
498
|
+
|
|
392
499
|
**THE RECIPROCAL DUTY, and it is yours because you write both documents:** when you issue a ruling that
|
|
393
500
|
re-scopes, narrows, splits or re-owns anything, **the ruling must name every sealed document its subject
|
|
394
501
|
appears in** — and say, in the ruling, whether each one is now void. You are the only seat that can do
|