tickmarkr 2.1.8 → 2.2.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/cli/commands/approve.d.ts +24 -0
- package/dist/cli/commands/approve.js +121 -34
- package/dist/compile/collateral.js +149 -4
- package/dist/compile/native.js +16 -0
- package/dist/drivers/herdr.d.ts +7 -5
- package/dist/drivers/herdr.js +142 -11
- package/dist/drivers/types.d.ts +5 -6
- package/dist/drivers/types.js +2 -48
- package/dist/run/daemon.d.ts +10 -6
- package/dist/run/daemon.js +275 -22
- package/dist/run/journal.d.ts +16 -0
- package/dist/run/journal.js +51 -6
- package/dist/tui/cockpit/setup-cockpit.d.ts +8 -10
- package/dist/tui/cockpit/setup-cockpit.js +92 -53
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +1 -1
- package/skills/tickmarkr-loop/SKILL.md +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +202 -24
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +109 -4
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +137 -0
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
export declare const APPROVAL_DISPOSITIONS: readonly ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
2
|
+
export type ApprovalDisposition = (typeof APPROVAL_DISPOSITIONS)[number];
|
|
3
|
+
/**
|
|
4
|
+
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
5
|
+
* this approval ends on. One table, because two surfaces say it: this command's own message, and the
|
|
6
|
+
* setup cockpit's pre-write confirm inset, which predicts the effect BEFORE the write. A prediction
|
|
7
|
+
* that can drift from the write is worth less than none, so neither surface writes the phrase itself.
|
|
8
|
+
*/
|
|
9
|
+
export declare const APPROVAL_ENACTS: Record<ApprovalDisposition, string>;
|
|
10
|
+
export declare function approvalDispositionForRelease(release: unknown): ApprovalDisposition;
|
|
1
11
|
export type ApprovalStatus = "deferred-live" | "recorded-no-owner";
|
|
12
|
+
/** Which run, and whether a LIVE daemon owns THAT run — the two facts an enactment sentence needs. */
|
|
13
|
+
export interface ApprovalRunOwner {
|
|
14
|
+
runId: string;
|
|
15
|
+
live: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** The same read `approve` performs, for surfaces that must predict an enactment before writing. */
|
|
18
|
+
export declare function approvalRunOwner(cwd: string, runId: string): ApprovalRunOwner;
|
|
19
|
+
/**
|
|
20
|
+
* The one sentence that says who enacts this release and what it buys. A live owner's approval is
|
|
21
|
+
* already scheduled — it rides that daemon's next task boundary — so it must NOT be told to resume:
|
|
22
|
+
* a second run in the same repository is forbidden, and it would contend for the live daemon's
|
|
23
|
+
* graph.lock over an approval that has already dispatched.
|
|
24
|
+
*/
|
|
25
|
+
export declare function approvalEnactment(token: ApprovalDisposition, run: ApprovalRunOwner): string;
|
|
2
26
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
3
27
|
export declare function approve(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -1,12 +1,59 @@
|
|
|
1
1
|
import { userInfo } from "node:os";
|
|
2
2
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
3
3
|
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, RECHECK_RELEASE, REVIEW_UPHELD_RELEASE } from "../../run/journal.js";
|
|
4
|
+
export const APPROVAL_DISPOSITIONS = ["dispatch", "waive-gate", "re-dispatch", "fund-fixed-attempt", "fresh-budget"];
|
|
5
|
+
/**
|
|
6
|
+
* What each disposition's release actually buys — the clause every operator-facing sentence about
|
|
7
|
+
* this approval ends on. One table, because two surfaces say it: this command's own message, and the
|
|
8
|
+
* setup cockpit's pre-write confirm inset, which predicts the effect BEFORE the write. A prediction
|
|
9
|
+
* that can drift from the write is worth less than none, so neither surface writes the phrase itself.
|
|
10
|
+
*/
|
|
11
|
+
export const APPROVAL_ENACTS = {
|
|
12
|
+
dispatch: "dispatch it",
|
|
13
|
+
"waive-gate": "continue past the approved gate",
|
|
14
|
+
"re-dispatch": "re-dispatch against the full gate suite",
|
|
15
|
+
"fund-fixed-attempt": "dispatch a fixed attempt carrying the findings",
|
|
16
|
+
"fresh-budget": "dispatch it on a fresh attempt budget",
|
|
17
|
+
};
|
|
18
|
+
export function approvalDispositionForRelease(release) {
|
|
19
|
+
if (release === GATE_SATISFIED_RELEASE)
|
|
20
|
+
return "waive-gate";
|
|
21
|
+
if (release === RECHECK_RELEASE)
|
|
22
|
+
return "re-dispatch";
|
|
23
|
+
if (release === REVIEW_UPHELD_RELEASE)
|
|
24
|
+
return "fund-fixed-attempt";
|
|
25
|
+
if (release === ATTEMPT_CAP_RELEASE)
|
|
26
|
+
return "fresh-budget";
|
|
27
|
+
return "dispatch";
|
|
28
|
+
}
|
|
4
29
|
import { acquireApprovalSerialization, runLockOwner } from "../../run/lock.js";
|
|
30
|
+
// THE liveness rule, written once. The lock is REPOSITORY-wide, so a live owner of some OTHER run is
|
|
31
|
+
// not an owner of this one and sweeps none of its approvals — claiming otherwise is the same
|
|
32
|
+
// falsehood in a new shape. Liveness itself comes from lock.ts's runLockOwner (the same inspect() the
|
|
33
|
+
// acquire/unlock decision table uses), never a second `process.kill(pid, 0)`, and never the lock
|
|
34
|
+
// FILE's presence: a stale lock whose recorded pid is dead is not a live run.
|
|
35
|
+
const ownedByLiveDaemon = (owner, runId) => ({ runId, live: owner?.live === true && owner.runId === runId });
|
|
36
|
+
/** The same read `approve` performs, for surfaces that must predict an enactment before writing. */
|
|
37
|
+
export function approvalRunOwner(cwd, runId) {
|
|
38
|
+
return ownedByLiveDaemon(runLockOwner(cwd), runId);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The one sentence that says who enacts this release and what it buys. A live owner's approval is
|
|
42
|
+
* already scheduled — it rides that daemon's next task boundary — so it must NOT be told to resume:
|
|
43
|
+
* a second run in the same repository is forbidden, and it would contend for the live daemon's
|
|
44
|
+
* graph.lock over an approval that has already dispatched.
|
|
45
|
+
*/
|
|
46
|
+
export function approvalEnactment(token, run) {
|
|
47
|
+
return run.live
|
|
48
|
+
? `the live daemon enacts this at its next task boundary — it will ${APPROVAL_ENACTS[token]}`
|
|
49
|
+
: `run \`tickmarkr resume ${run.runId}\` to ${APPROVAL_ENACTS[token]}`;
|
|
50
|
+
}
|
|
5
51
|
/** The production command registered in COMMANDS; its returned bytes are what the CLI prints. */
|
|
6
52
|
export async function approve(argv, cwd = process.cwd()) {
|
|
7
|
-
const { runId, taskId, by, reason, uphold, recheck, reviewRoundCeiling } = parseArgs(argv);
|
|
8
|
-
|
|
9
|
-
|
|
53
|
+
const { runId, taskId, by, reason, waive, uphold, recheck, reviewRoundCeiling } = parseArgs(argv);
|
|
54
|
+
const decisions = [waive, uphold, recheck].filter(Boolean).length;
|
|
55
|
+
if (decisions > 1)
|
|
56
|
+
throw new Error("--waive, --uphold and --recheck are different decisions — pass one");
|
|
10
57
|
const serialization = await acquireApprovalSerialization(cwd, runId);
|
|
11
58
|
try {
|
|
12
59
|
// Journal.open throws `no journal for <runId> at <dir>` on an unknown run — that IS the refusal.
|
|
@@ -30,13 +77,15 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
30
77
|
}
|
|
31
78
|
}
|
|
32
79
|
const lastHuman = events[lastHumanIndex];
|
|
80
|
+
const capPark = lastHuman?.data.kind === ATTEMPT_CAP_RELEASE;
|
|
81
|
+
const gateFailPark = lastHuman?.data.kind === "gate-fail";
|
|
82
|
+
const failedGate = gateFailPark ? failedGateForNewestPark(events, taskId, lastHumanIndex) : undefined;
|
|
83
|
+
if (gateFailPark && !failedGate) {
|
|
84
|
+
throw new Error(`task ${taskId} is parked on gate-fail but has no failed gate result on the newest park — refusing to infer one`);
|
|
85
|
+
}
|
|
33
86
|
if (uphold) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const lastFailed = events.slice(0, lastHumanIndex).reverse().find((e) => e.event === "gate-result" && e.taskId === taskId && e.data.pass === false
|
|
37
|
-
&& typeof e.data.gate === "string" && GATE_NAMES.includes(e.data.gate))?.data.gate;
|
|
38
|
-
if (lastFailed !== "review") {
|
|
39
|
-
throw new Error(`--uphold applies to a review rejection; ${taskId}'s last failed gate is ${lastFailed ?? "none"} — refusing`);
|
|
87
|
+
if (!gateFailPark || failedGate !== "review") {
|
|
88
|
+
throw new Error(`--uphold applies to a review gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
40
89
|
}
|
|
41
90
|
journal.append("task-approved", taskId, {
|
|
42
91
|
by,
|
|
@@ -46,15 +95,11 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
46
95
|
gate: "review",
|
|
47
96
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
48
97
|
});
|
|
49
|
-
return disposition(cwd, runId, `upheld the reviewer for ${taskId} in ${runId} — by ${by}
|
|
98
|
+
return disposition(cwd, runId, "fund-fixed-attempt", `upheld the reviewer for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
50
99
|
}
|
|
51
|
-
const capPark = lastHuman?.data.kind === ATTEMPT_CAP_RELEASE;
|
|
52
|
-
const gateFailPark = lastHuman?.data.kind === "gate-fail";
|
|
53
100
|
if (recheck) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!gateFailPark) {
|
|
57
|
-
throw new Error(`--recheck applies to a gate-fail park; ${taskId}'s park kind is ${lastHuman?.data.kind ?? "none"} — refusing`);
|
|
101
|
+
if (!gateFailPark || !failedGate) {
|
|
102
|
+
throw new Error(`--recheck applies to a gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
58
103
|
}
|
|
59
104
|
journal.append("task-approved", taskId, {
|
|
60
105
|
by,
|
|
@@ -63,14 +108,27 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
63
108
|
release: RECHECK_RELEASE,
|
|
64
109
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
65
110
|
});
|
|
66
|
-
return disposition(cwd, runId, `re-checking ${taskId} in ${runId} — by ${by};
|
|
111
|
+
return disposition(cwd, runId, "re-dispatch", `re-checking ${taskId} in ${runId} — by ${by}; failed gate ${failedGate}; no gate marked satisfied`, serialization.contended);
|
|
67
112
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
113
|
+
if (waive) {
|
|
114
|
+
if (!gateFailPark || !failedGate) {
|
|
115
|
+
throw new Error(`--waive applies to a gate-fail park; ${taskId}'s newest park is ${String(lastHuman?.data.kind ?? "none")} with failed gate ${failedGate ?? "none"} — refusing`);
|
|
116
|
+
}
|
|
117
|
+
journal.append("task-approved", taskId, {
|
|
118
|
+
by,
|
|
119
|
+
...(reason ? { reason } : {}),
|
|
120
|
+
via: "cli",
|
|
121
|
+
release: GATE_SATISFIED_RELEASE,
|
|
122
|
+
gate: failedGate,
|
|
123
|
+
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
124
|
+
});
|
|
125
|
+
return disposition(cwd, runId, "waive-gate", `waived failed gate ${failedGate} for ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
126
|
+
}
|
|
127
|
+
if (gateFailPark) {
|
|
128
|
+
const choices = [`--waive (disposition waive-gate)`, `--recheck (disposition re-dispatch)`];
|
|
129
|
+
if (failedGate === "review")
|
|
130
|
+
choices.push(`--uphold (disposition fund-fixed-attempt)`);
|
|
131
|
+
throw new Error(`task ${taskId} is parked on failed gate ${failedGate}; plain approve has disposition only for non-gate parks — pass ${choices.join(" or ")}`);
|
|
74
132
|
}
|
|
75
133
|
journal.append("task-approved", taskId, {
|
|
76
134
|
by,
|
|
@@ -78,9 +136,9 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
78
136
|
via: "cli",
|
|
79
137
|
...(reviewRoundCeiling === undefined ? {} : { reviewRoundCeiling }),
|
|
80
138
|
...(capPark ? { release: ATTEMPT_CAP_RELEASE } : {}),
|
|
81
|
-
...(failedGate ? { release: GATE_SATISFIED_RELEASE, gate: failedGate } : {}),
|
|
82
139
|
});
|
|
83
|
-
|
|
140
|
+
const token = capPark ? "fresh-budget" : "dispatch";
|
|
141
|
+
return disposition(cwd, runId, token, `approved ${taskId} in ${runId} — by ${by}`, serialization.contended);
|
|
84
142
|
}
|
|
85
143
|
finally {
|
|
86
144
|
serialization.release();
|
|
@@ -89,29 +147,41 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
89
147
|
// The status is command OUTPUT, not a typed sibling result: the registered approve function returns
|
|
90
148
|
// one primitive string and the dispatcher prints those bytes unchanged. A compact sentinel followed
|
|
91
149
|
// by JSON makes the contract unambiguous to machines without widening the shared command result type.
|
|
92
|
-
// No-lock approvals keep their historical one-line result for the cockpit; a dead recorded owner
|
|
93
|
-
// a command delayed behind terminalization
|
|
94
|
-
|
|
150
|
+
// No-lock approvals keep their historical one-line result for the cockpit; a dead recorded owner, a
|
|
151
|
+
// live owner executing some OTHER run, and a command delayed behind terminalization all emit
|
|
152
|
+
// recorded-no-owner — none of them is a daemon that will sweep this run's approvals.
|
|
153
|
+
//
|
|
154
|
+
// The ENACTMENT half of the message is completed here because only here is liveness known: the call
|
|
155
|
+
// sites carry the decision, not the answer to who will act on it. `deferred-live` keeps its v1.89
|
|
156
|
+
// token — machine consumers parse it — while its TEXT now names the boundary sweep, and the `resume`
|
|
157
|
+
// field it used to carry unconditionally is withheld from the live branch it would misdirect.
|
|
158
|
+
function disposition(cwd, runId, token, message, contended) {
|
|
95
159
|
const owner = runLockOwner(cwd);
|
|
160
|
+
const run = ownedByLiveDaemon(owner, runId);
|
|
96
161
|
const resume = `tickmarkr resume ${runId}`;
|
|
162
|
+
const out = `approval disposition ${token}: ${message}; ${approvalEnactment(token, run)}`;
|
|
97
163
|
if (!owner && !contended)
|
|
98
|
-
return
|
|
99
|
-
const status =
|
|
164
|
+
return out;
|
|
165
|
+
const status = run.live ? "deferred-live" : "recorded-no-owner";
|
|
100
166
|
const record = {
|
|
101
167
|
status,
|
|
102
|
-
|
|
168
|
+
disposition: token,
|
|
169
|
+
// The recovery command is claimed only where it IS the enactment. A live owner's approval is
|
|
170
|
+
// already scheduled, and a resume would contend for that daemon's graph.lock.
|
|
171
|
+
...(run.live ? {} : { resume }),
|
|
103
172
|
...(owner?.pid === undefined ? {} : { ownerPid: owner.pid }),
|
|
104
173
|
...(owner?.runId === undefined ? {} : { ownerRunId: owner.runId }),
|
|
105
174
|
};
|
|
106
|
-
return `${
|
|
175
|
+
return `${out}\nTICKMARKR_APPROVAL ${JSON.stringify(record)}`;
|
|
107
176
|
}
|
|
108
|
-
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
177
|
+
const USAGE = "usage: tickmarkr approve <run-id> <task-id> [--waive|--uphold|--recheck] [--review-rounds <positive-integer>] [--by <name>] [--reason <text>]";
|
|
109
178
|
// hand-parsed argv — no CLI framework (house style). Positionals are runId then taskId; decision,
|
|
110
179
|
// ceiling, actor and reason are flags. Throws usage on missing positionals (mirrors resume.ts/unlock.ts).
|
|
111
180
|
function parseArgs(argv) {
|
|
112
181
|
const positionals = [];
|
|
113
182
|
let by;
|
|
114
183
|
let reason;
|
|
184
|
+
let waive = false;
|
|
115
185
|
let uphold = false;
|
|
116
186
|
let recheck = false;
|
|
117
187
|
let reviewRoundCeiling;
|
|
@@ -127,6 +197,9 @@ function parseArgs(argv) {
|
|
|
127
197
|
if (!reason)
|
|
128
198
|
throw new Error(USAGE);
|
|
129
199
|
}
|
|
200
|
+
else if (a === "--waive") {
|
|
201
|
+
waive = true;
|
|
202
|
+
}
|
|
130
203
|
else if (a === "--uphold") {
|
|
131
204
|
uphold = true;
|
|
132
205
|
}
|
|
@@ -150,5 +223,19 @@ function parseArgs(argv) {
|
|
|
150
223
|
if (!runId || !taskId) {
|
|
151
224
|
throw new Error(USAGE);
|
|
152
225
|
}
|
|
153
|
-
return { runId, taskId, by: by ?? userInfo().username, reason, uphold, recheck, reviewRoundCeiling };
|
|
226
|
+
return { runId, taskId, by: by ?? userInfo().username, reason, waive, uphold, recheck, reviewRoundCeiling };
|
|
227
|
+
}
|
|
228
|
+
function failedGateForNewestPark(events, taskId, lastHumanIndex) {
|
|
229
|
+
for (let i = lastHumanIndex - 1; i >= 0; i -= 1) {
|
|
230
|
+
const event = events[i];
|
|
231
|
+
if (event.taskId !== taskId)
|
|
232
|
+
continue;
|
|
233
|
+
if (event.event === "task-approved" || event.event === "task-human" || event.event === "task-dispatch")
|
|
234
|
+
return undefined;
|
|
235
|
+
if (event.event === "gate-result" && event.data.pass === false
|
|
236
|
+
&& typeof event.data.gate === "string" && GATE_NAMES.includes(event.data.gate)) {
|
|
237
|
+
return event.data.gate;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
154
241
|
}
|
|
@@ -262,6 +262,148 @@ export function criterionReadsOnly(text, target) {
|
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
const ARCH_PAGES = ["docs/codebase/ARCHITECTURE.md", "docs/codebase/STRUCTURE.md"];
|
|
265
|
+
// files[] is deliberately small, but a brace range can still be broad. If a pattern exceeds
|
|
266
|
+
// this advisory budget, fail open instead of printing an unexpanded brace fragment as a directory.
|
|
267
|
+
const MAX_BRACE_EXPANSIONS = 256;
|
|
268
|
+
const RANGE_CHAR_START = 32;
|
|
269
|
+
const RANGE_CHAR_END = 126;
|
|
270
|
+
function isEscaped(input, index) {
|
|
271
|
+
let backslashes = 0;
|
|
272
|
+
for (let i = index - 1; i >= 0 && input[i] === "\\"; i--)
|
|
273
|
+
backslashes++;
|
|
274
|
+
return backslashes % 2 === 1;
|
|
275
|
+
}
|
|
276
|
+
function unescapeGlobLiteral(input) {
|
|
277
|
+
return input.replace(/\\(.)/g, "$1");
|
|
278
|
+
}
|
|
279
|
+
function rangeParts(body) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
let depth = 0;
|
|
282
|
+
let start = 0;
|
|
283
|
+
let sawRange = false;
|
|
284
|
+
for (let i = 0; i < body.length - 1; i++) {
|
|
285
|
+
if (isEscaped(body, i))
|
|
286
|
+
continue;
|
|
287
|
+
const ch = body[i];
|
|
288
|
+
if (ch === "{") {
|
|
289
|
+
depth++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (ch === "}") {
|
|
293
|
+
depth = Math.max(0, depth - 1);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (ch !== "." || body[i + 1] !== "." || isEscaped(body, i + 1) || depth !== 0)
|
|
297
|
+
continue;
|
|
298
|
+
parts.push(body.slice(start, i));
|
|
299
|
+
i++;
|
|
300
|
+
start = i + 1;
|
|
301
|
+
sawRange = true;
|
|
302
|
+
}
|
|
303
|
+
if (!sawRange)
|
|
304
|
+
return undefined;
|
|
305
|
+
parts.push(body.slice(start));
|
|
306
|
+
return parts;
|
|
307
|
+
}
|
|
308
|
+
function rangeAlternatives(body) {
|
|
309
|
+
const parts = rangeParts(body);
|
|
310
|
+
if (!parts)
|
|
311
|
+
return undefined;
|
|
312
|
+
const source = `[${parts.map(unescapeGlobLiteral).sort().join("-")}]`;
|
|
313
|
+
let re;
|
|
314
|
+
try {
|
|
315
|
+
re = new RegExp(`^${source}$`);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const out = [];
|
|
321
|
+
for (let code = RANGE_CHAR_START; code <= RANGE_CHAR_END; code++) {
|
|
322
|
+
const ch = String.fromCharCode(code);
|
|
323
|
+
if (re.test(ch))
|
|
324
|
+
out.push(ch);
|
|
325
|
+
if (out.length > MAX_BRACE_EXPANSIONS)
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function braceAlternatives(body) {
|
|
331
|
+
const alternatives = [];
|
|
332
|
+
let depth = 0;
|
|
333
|
+
let start = 0;
|
|
334
|
+
let hasComma = false;
|
|
335
|
+
for (let i = 0; i < body.length; i++) {
|
|
336
|
+
if (isEscaped(body, i))
|
|
337
|
+
continue;
|
|
338
|
+
const ch = body[i];
|
|
339
|
+
if (ch === "{") {
|
|
340
|
+
depth++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (ch === "}") {
|
|
344
|
+
depth = Math.max(0, depth - 1);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (ch !== "," || depth !== 0)
|
|
348
|
+
continue;
|
|
349
|
+
alternatives.push(body.slice(start, i));
|
|
350
|
+
start = i + 1;
|
|
351
|
+
hasComma = true;
|
|
352
|
+
}
|
|
353
|
+
if (hasComma) {
|
|
354
|
+
alternatives.push(body.slice(start));
|
|
355
|
+
return alternatives;
|
|
356
|
+
}
|
|
357
|
+
return rangeAlternatives(body);
|
|
358
|
+
}
|
|
359
|
+
function expandableBrace(pattern) {
|
|
360
|
+
const opens = [];
|
|
361
|
+
let unsupported;
|
|
362
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
363
|
+
if (isEscaped(pattern, i))
|
|
364
|
+
continue;
|
|
365
|
+
if (pattern[i] === "{") {
|
|
366
|
+
opens.push(i);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (pattern[i] !== "}" || !opens.length)
|
|
370
|
+
continue;
|
|
371
|
+
const start = opens.pop();
|
|
372
|
+
const alternatives = braceAlternatives(pattern.slice(start + 1, i));
|
|
373
|
+
if (alternatives === undefined)
|
|
374
|
+
continue;
|
|
375
|
+
if (alternatives.length === 0) {
|
|
376
|
+
unsupported ??= { start, end: i, alternatives };
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
return { start, end: i, alternatives };
|
|
380
|
+
}
|
|
381
|
+
return unsupported;
|
|
382
|
+
}
|
|
383
|
+
function expandFilesPattern(pattern) {
|
|
384
|
+
const pending = [pattern];
|
|
385
|
+
const expanded = new Set();
|
|
386
|
+
let visited = 0;
|
|
387
|
+
while (pending.length) {
|
|
388
|
+
const candidate = pending.pop();
|
|
389
|
+
if (++visited > MAX_BRACE_EXPANSIONS)
|
|
390
|
+
return [];
|
|
391
|
+
const brace = expandableBrace(candidate);
|
|
392
|
+
if (!brace) {
|
|
393
|
+
expanded.add(unescapeGlobLiteral(candidate));
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (brace.alternatives.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
if (pending.length + brace.alternatives.length > MAX_BRACE_EXPANSIONS)
|
|
399
|
+
return [];
|
|
400
|
+
for (const alternative of brace.alternatives) {
|
|
401
|
+
pending.push(candidate.slice(0, brace.start) + alternative + candidate.slice(brace.end + 1));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const scoped = filesGlob(pattern);
|
|
405
|
+
return [...expanded].filter((candidate) => scoped(candidate));
|
|
406
|
+
}
|
|
265
407
|
function topLevelSrcDir(file) {
|
|
266
408
|
const parts = file.replace(/^\.\//, "").replace(/\/+$/, "").split("/");
|
|
267
409
|
if (parts[0] !== "src" || parts.length < 2 || !parts[1])
|
|
@@ -293,10 +435,13 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
293
435
|
const scoped = filesGlob(t.files.map((f) => f.replace(/^\.\//, "")));
|
|
294
436
|
const newDirs = new Set();
|
|
295
437
|
for (const f of t.files) {
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
438
|
+
const normalized = f.replace(/^\.\//, "");
|
|
439
|
+
for (const expanded of expandFilesPattern(normalized)) {
|
|
440
|
+
const dir = topLevelSrcDir(expanded);
|
|
441
|
+
if (!dir || existing.has(dir))
|
|
442
|
+
continue;
|
|
443
|
+
newDirs.add(`src/${dir}/`);
|
|
444
|
+
}
|
|
300
445
|
}
|
|
301
446
|
if (!newDirs.size)
|
|
302
447
|
continue;
|
package/dist/compile/native.js
CHANGED
|
@@ -802,6 +802,22 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
802
802
|
A text sweep produces a candidate list; only running the change enumerates the real blocker set.
|
|
803
803
|
Keep the candidates for scope, then execute the production path and full gates before declaring the
|
|
804
804
|
set closed — this milestone paid a halted run to learn that the two populations are not identical.
|
|
805
|
+
- SPIKE-THE-CONTRACT-THEN-SCOPE trigger question: COULD A TEST THIS TASK DOES NOT OWN BE ASSERTING THE
|
|
806
|
+
THING I AM CHANGING? "I'D HAVE TO GREP TO KNOW" IS YES. This applies to observable contracts:
|
|
807
|
+
execution order, event-stream order, diagnostics/output sets, CLI surface, serialised formats, or
|
|
808
|
+
timing measurements. If yes, implement the change as a throwaway spike, run the full suite, read the
|
|
809
|
+
reds, THEN scope files[].
|
|
810
|
+
- Caveat: a spike measures ONE implementation. It converts unknown collateral into
|
|
811
|
+
measured-for-one-specimen collateral; it does NOT make its reds the closed blocker set for every
|
|
812
|
+
route. A worker taking a different route can still red on unowned collateral; that remains a PLAN
|
|
813
|
+
DEFECT, NEVER A RETRY.
|
|
814
|
+
- Measured price: 518 s implement + 831 s suite = 1,348 s ≈ 22.5 min. "Far cheaper than the alternative"
|
|
815
|
+
is WITHDRAWN on the direct leg: a direct failed run died at about 20 min, so on the direct leg the
|
|
816
|
+
two are EQUAL. The spike only pays when it avoids downstream halt, sweep, re-scope, rulings, and
|
|
817
|
+
another compile+plan.
|
|
818
|
+
- Do NOT run the spike for a change that is purely additive and unwinds cheaply. The rule is bounded by
|
|
819
|
+
the expense of a late defect, not by novelty; where the defect would surface and fix cheaply, the spike
|
|
820
|
+
is pure overhead.
|
|
805
821
|
|
|
806
822
|
WHICH SIDE OF A RUN INHERITS ENVIRONMENT — AND IT DEPENDS ON THE DRIVER (OBS-542):
|
|
807
823
|
- Gate commands and "command:"/"test:" oracles INHERIT THE DAEMON'S ENVIRONMENT. They are children of
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JournalEvent } from "../run/journal.js";
|
|
2
|
-
import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "./types.js";
|
|
2
|
+
import { type ExecutorDriver, type NotifyOpts, type PanesToCloseOpts, type Slot, type SlotOpts } from "./types.js";
|
|
3
3
|
export declare const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
4
4
|
export declare const TRAILER_WIDTH_MARGIN = 2;
|
|
5
5
|
export declare const DELIVERY_ATTEMPTS = 3;
|
|
@@ -70,6 +70,11 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
70
70
|
private watches;
|
|
71
71
|
constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource, journal?: DriverJournal | undefined);
|
|
72
72
|
private appendDispatchRetry;
|
|
73
|
+
private openRunJournal;
|
|
74
|
+
private liveSupervisionSeats;
|
|
75
|
+
private journalReconcile;
|
|
76
|
+
private paneReconcileData;
|
|
77
|
+
private parsePaneList;
|
|
73
78
|
/** v1.99 T2: bind this driver's own journal writes to the run's live narration sink. */
|
|
74
79
|
narrateWith(narrate: (event: JournalEvent) => void): void;
|
|
75
80
|
private serial;
|
|
@@ -123,9 +128,6 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
123
128
|
private watchSlot;
|
|
124
129
|
private discardSplit;
|
|
125
130
|
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
126
|
-
reconcile(desired: Set<string>, runId: string, opts?:
|
|
127
|
-
spareLiveLlm?: boolean;
|
|
128
|
-
endedRunIds?: Set<string>;
|
|
129
|
-
}): Promise<void>;
|
|
131
|
+
reconcile(desired: Set<string>, runId: string, opts?: PanesToCloseOpts): Promise<void>;
|
|
130
132
|
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
131
133
|
}
|