tickmarkr 2.7.5 → 2.7.7
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/init.js +1 -1
- package/dist/cli/commands/report.js +4 -2
- package/dist/cli/commands/status.js +8 -5
- package/dist/compile/native.js +9 -7
- package/dist/compile/prd.js +4 -3
- package/dist/compile/speckit.js +3 -2
- package/dist/gates/acceptance.js +5 -4
- package/dist/gates/artifact-manifest.js +17 -13
- package/dist/gates/git-paths.d.ts +6 -0
- package/dist/gates/git-paths.js +22 -0
- package/dist/run/beat-lifecycle.js +1 -1
- package/dist/run/daemon.js +8 -4
- package/dist/run/journal.d.ts +12 -0
- package/dist/run/journal.js +41 -3
- package/dist/run/launchservices-check.js +1 -1
- package/dist/run/lease.js +2 -2
- package/dist/run/stall.js +3 -2
- package/dist/run/suite-census.js +2 -2
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +27 -6
- package/skills/tickmarkr-overseer/scripts/watch-launch.sh +11 -3
|
@@ -111,7 +111,7 @@ Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version\`, re
|
|
|
111
111
|
|
|
112
112
|
### Tip-verify-before-green
|
|
113
113
|
|
|
114
|
-
A run is green only when ALL of these hold: the run-end event exists in the journal, tip verify is not "failed", the run-end summary's \`failed\`, \`human\`, \`blocked\` and \`pending\` buckets are every one of them empty, AND the owed checks in CURRENT \`tickmarkr status <runId>\` are outstanding empty AND known (\`outstanding 0\`; \`outstanding unknown\` is never empty). The shorter "run-end plus tip verify" form is NOT sufficient: a run ending \`done=[T1,T3,T4] human=[T2] tipVerify=passed\` is a closed PARTIAL run — three of four delivered, one parked — and calling it green is how a park becomes invisible. Empty execution buckets are not sufficient either (D-660): a run ending with every execution bucket empty and \`tipVerify=passed\` after an operator waived one review still owes that accepted-risk review check — status reads \`outstanding 1 (T7 review)\`, and \`tickmarkr run\` and \`resume\` exit 0 on execution alone — so it is execution complete, not green. Read the debt from CURRENT status, never from the run-end record: a \`tickmarkr verify --record <runId>\` discharge that lands after run-end moves current status to \`outstanding 0\` while the historical run-end row still carries 1, and only then is that run green; a legacy waiver without a recorded obligation reads \`outstanding unknown\` and is never green. Never report green to the operator, tab titles, or records until every clause holds; when one does not, name the bucket that is not empty, the outstanding checks, or the unknown debt.
|
|
114
|
+
A run is green only when ALL of these hold: the run-end event exists in the journal and is not fatal, tip verify is not "failed", the run-end summary's \`failed\`, \`human\`, \`blocked\` and \`pending\` buckets are every one of them empty, AND the owed checks in CURRENT \`tickmarkr status <runId>\` are outstanding empty AND known (\`outstanding 0\`; \`outstanding unknown\` is never empty). The shorter "run-end plus tip verify" form is NOT sufficient: a run ending \`done=[T1,T3,T4] human=[T2] tipVerify=passed\` is a closed PARTIAL run — three of four delivered, one parked — and calling it green is how a park becomes invisible. Empty execution buckets are not sufficient either (D-660): a run ending with every execution bucket empty and \`tipVerify=passed\` after an operator waived one review still owes that accepted-risk review check — status reads \`outstanding 1 (T7 review)\`, and \`tickmarkr run\` and \`resume\` exit 0 on execution alone — so it is execution complete, not green. Read the debt from CURRENT status, never from the run-end record: a \`tickmarkr verify --record <runId>\` discharge that lands after run-end moves current status to \`outstanding 0\` while the historical run-end row still carries 1, and only then is that run green; a legacy waiver without a recorded obligation reads \`outstanding unknown\` and is never green. A fatal run-end is never green either: the crash record (\`fatal: true\`) names its phase and error, every task it left running or gated sits in \`pending\` or \`blocked\`, and even with every bucket empty status and report lead with the crash and read \`outstanding unknown\` until the run is resumed; from then on the resumed engagement owns the debt. Never report green to the operator, tab titles, or records until every clause holds; when one does not, name the bucket that is not empty, the outstanding checks, or the unknown debt.
|
|
115
115
|
|
|
116
116
|
### Verified handoffs
|
|
117
117
|
|
|
@@ -11,7 +11,7 @@ import { compareRuns } from "../../report/compare.js";
|
|
|
11
11
|
import { estimateCosts } from "../../report/cost.js";
|
|
12
12
|
import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChannelTokens, formatTokenUsage as fields, totalTokens as total, } from "../../report/operator-record.js";
|
|
13
13
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
14
|
-
import { effectiveEvents, foldOwedChecks, Journal, loadRoutingProfile, OWED_DISCHARGE_EVENT, } from "../../run/journal.js";
|
|
14
|
+
import { effectiveEvents, fatalRunEndCause, foldOwedChecks, Journal, loadRoutingProfile, OWED_DISCHARGE_EVENT, } from "../../run/journal.js";
|
|
15
15
|
import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
|
|
16
16
|
import { formatSpan, WALL_PRIORITY, WALL_PRIORITY_TEXT, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
|
|
17
17
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
@@ -241,8 +241,10 @@ const timeLead = (events) => {
|
|
|
241
241
|
* comparability or supersession note); the rest is folded here from the journal and the current fold.
|
|
242
242
|
*/
|
|
243
243
|
export function leadLines(events, owed, finished) {
|
|
244
|
+
// A fatal run-end leads with its crash: no tally after it may read as a finished run.
|
|
245
|
+
const crash = fatalRunEndCause(events);
|
|
244
246
|
return [
|
|
245
|
-
`finished ${[finished.tally, finished.tip, firstPassText(endToEndFirstPass(events)), ...(finished.notes ?? [])].join(" · ")}`,
|
|
247
|
+
`finished ${[...(crash ? [`run crashed — ${crash}`] : []), finished.tally, finished.tip, firstPassText(endToEndFirstPass(events)), ...(finished.notes ?? [])].join(" · ")}`,
|
|
246
248
|
timeLead(events),
|
|
247
249
|
needsYouText(events, owed),
|
|
248
250
|
];
|
|
@@ -13,7 +13,7 @@ import { projectOperatorSummary } from "../../run/operator-summary.js";
|
|
|
13
13
|
import { trackJournalRows } from "../../run/protocol.js";
|
|
14
14
|
import { newestPark, permittedDecisionVerbs } from "./approve.js";
|
|
15
15
|
import { currentOwed, endToEndFirstPass, firstPassText, leadLines, leadRows, needsYouText, wallText } from "./report.js";
|
|
16
|
-
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
|
|
16
|
+
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, fatalRunEndCause, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, visibleControls, } from "../../run/journal.js";
|
|
17
17
|
import { isPidLive, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
18
18
|
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
19
19
|
import { desiredPanes } from "../../run/reconcile.js";
|
|
@@ -649,10 +649,11 @@ const terminalFailureCause = (events) => {
|
|
|
649
649
|
const e = events[i];
|
|
650
650
|
if (e.event !== "run-end" || typeof e.data.error !== "string")
|
|
651
651
|
continue;
|
|
652
|
-
|
|
653
|
-
|
|
652
|
+
// the recorded error is data: its terminal controls print escaped, never executed (the journal keeps them)
|
|
653
|
+
const phase = typeof e.data.phase === "string" && visibleControls(e.data.phase)
|
|
654
|
+
? `${visibleControls(e.data.phase)} failed`
|
|
654
655
|
: "run failed";
|
|
655
|
-
return `${phase}: ${e.data.error
|
|
656
|
+
return `${phase}: ${visibleControls(e.data.error)}`;
|
|
656
657
|
}
|
|
657
658
|
return undefined;
|
|
658
659
|
};
|
|
@@ -1429,8 +1430,10 @@ const oneLine = (cwd, namedRunId) => {
|
|
|
1429
1430
|
// CG1: the lead's facts in one line — lineage first pass, the wall window, and what needs you from
|
|
1430
1431
|
// the CURRENT fold, so a discharge after run-end moves this line too.
|
|
1431
1432
|
const { events } = record;
|
|
1433
|
+
// A fatal run-end leads right after the run id: no completion fraction or verify state reads first.
|
|
1434
|
+
const crash = fatalRunEndCause(events);
|
|
1432
1435
|
return sanitizeTaskText([
|
|
1433
|
-
record.runId, ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)),
|
|
1436
|
+
record.runId, ...(crash ? [`run crashed — ${crash}`] : []), ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)),
|
|
1434
1437
|
].join(" · "));
|
|
1435
1438
|
};
|
|
1436
1439
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
package/dist/compile/native.js
CHANGED
|
@@ -179,12 +179,14 @@ export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
|
179
179
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
180
180
|
export const TICKMARKR_NATIVE_MARKER = /^<!--\s*tickmarkr:spec(?:\s+v1)?\s*-->\s*$/m;
|
|
181
181
|
export const NATIVE_MARKER = new RegExp(`^<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec(?:\\s+v1)?\\s*-->\\s*$`, "m");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
182
|
+
// Queue row 130: the rest of a spec line is [^\r\n], never `.` — `.` misses U+2028/U+2029, and an acceptance item holding
|
|
183
|
+
// one was glued onto its neighbour as a plain judge string, losing its own test: oracle (PRD and Spec Kit dropped it).
|
|
184
|
+
const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
|
|
185
|
+
const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
|
|
186
|
+
const NESTED_RE = /^\s+- ([^\r\n]+)$/;
|
|
185
187
|
// v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
|
|
186
188
|
// without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
|
|
187
|
-
const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(
|
|
189
|
+
const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*([^\\r\\n]*)$`);
|
|
188
190
|
const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout", "pins", "outofscope"]);
|
|
189
191
|
const listItems = (draft) => draft.list === "acceptance" ? draft.acceptanceRaw
|
|
190
192
|
: draft.list === "pins" ? draft.pinsRaw
|
|
@@ -199,7 +201,7 @@ const PIN_GLOB_SEP = "| glob:";
|
|
|
199
201
|
const LANDING_SEP = "| suite:";
|
|
200
202
|
function parsePin(task, raw, index, pathSet) {
|
|
201
203
|
const bad = (detail) => invalid(task, "pins", `item ${index + 1} (${JSON.stringify(raw)}) ${detail}`);
|
|
202
|
-
const typed = raw.match(/^(\w+):\s*(
|
|
204
|
+
const typed = raw.match(/^(\w+):\s*([^\r\n]*)$/);
|
|
203
205
|
if (typed?.[1] === "fixture") {
|
|
204
206
|
const paths = pathSet(typed[2]);
|
|
205
207
|
if (!paths.length)
|
|
@@ -275,7 +277,7 @@ function testsAtHead(root) {
|
|
|
275
277
|
}
|
|
276
278
|
const bodies = new Map();
|
|
277
279
|
for (const line of grep.stdout.split("\n")) {
|
|
278
|
-
const match = line.match(/^HEAD:(tests
|
|
280
|
+
const match = line.match(/^HEAD:(tests\/[^\r\n]*\.test\.ts):\d+:([^\r\n]*)$/);
|
|
279
281
|
if (!match)
|
|
280
282
|
continue;
|
|
281
283
|
const lines = bodies.get(match[1]) ?? [];
|
|
@@ -544,7 +546,7 @@ export function compileNative(file, options = {}) {
|
|
|
544
546
|
}
|
|
545
547
|
const draft = drafts.at(-1);
|
|
546
548
|
if (!draft) {
|
|
547
|
-
const base = line.match(/^base:\s*(
|
|
549
|
+
const base = line.match(/^base:\s*([^\r\n]*)$/);
|
|
548
550
|
if (base) {
|
|
549
551
|
const value = base[1].trim();
|
|
550
552
|
if (!value) {
|
package/dist/compile/prd.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { validateGraph } from "../graph/schema.js";
|
|
3
3
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
// Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
|
|
5
|
+
const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
|
|
6
|
+
const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
|
|
7
|
+
const NESTED_RE = /^\s+- ([^\r\n]+)$/;
|
|
7
8
|
export function compilePrd(file) {
|
|
8
9
|
if (!existsSync(file))
|
|
9
10
|
throw new CompileError(`no such PRD file: ${file}`);
|
package/dist/compile/speckit.js
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateGraph } from "../graph/schema.js";
|
|
4
4
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
// Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
|
|
6
|
+
const TASK_RE = /^- \[( |x)\] (T\d+)\s*(\[P\])?\s*([^\r\n]+)$/;
|
|
7
|
+
const SUB_RE = /^\s+- (\w+):\s*([^\r\n]+)$/;
|
|
7
8
|
export function compileSpecKit(dir) {
|
|
8
9
|
const path = join(dir, "tasks.md");
|
|
9
10
|
if (!existsSync(path)) {
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -307,13 +307,14 @@ function compressLines(lines) {
|
|
|
307
307
|
}
|
|
308
308
|
return parts.join(", ");
|
|
309
309
|
}
|
|
310
|
-
|
|
310
|
+
// Queue rows 128/129: "\n"-only line semantics — the m flag would also split a line at U+2028/U+2029.
|
|
311
|
+
const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
|
|
311
312
|
// the a-side path of a whole-file deletion section, or null if this section is not one.
|
|
312
313
|
function deletedPath(section) {
|
|
313
|
-
if (
|
|
314
|
+
if (!/(?<=^|\n)deleted file mode /.test(section))
|
|
314
315
|
return null;
|
|
315
|
-
const oldPath =
|
|
316
|
-
??
|
|
316
|
+
const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
|
|
317
|
+
?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
|
|
317
318
|
if (!oldPath || oldPath === "/dev/null")
|
|
318
319
|
return null;
|
|
319
320
|
const unquoted = oldPath.startsWith('"') && oldPath.endsWith('"') ? oldPath.slice(1, -1) : oldPath;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* path receives capture accounting only when its manifest row names a
|
|
4
4
|
* registered producer and repeats that producer's current provenance exactly.
|
|
5
5
|
*/
|
|
6
|
-
import { diffSidePath, gitHeaderPaths, unquoteGitPath } from "./git-paths.js";
|
|
6
|
+
import { diffSidePath, gitHeaderPaths, quoteGitPath, unquoteGitPath } from "./git-paths.js";
|
|
7
7
|
const GOLDEN_PROVENANCE = {
|
|
8
8
|
source: "src/tui/cockpit/capture.ts",
|
|
9
9
|
entrypoint: "regenerateGoldenFrames",
|
|
@@ -191,17 +191,21 @@ export function classifyArtifactPath(path, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
191
191
|
provenance: producer.provenance,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
// Queue rows 128/129: patch text is split into lines at "\n" only. Under the m flag JS `^` and `$` also break at
|
|
195
|
+
// U+2028/U+2029 (and `.` never matches them), so an added line holding one could forge a header line; these patterns
|
|
196
|
+
// anchor on "\n" instead, and a name written back into the text is quoted (quoteGitPath) so it cannot end a line.
|
|
197
|
+
const SET_ASIDE_RECEIPT = /(?<=^|\n)set aside: regenerable capture ([^\r\n]+?) — \d+ bytes withheld\b/;
|
|
195
198
|
/** The path named by a citable capture receipt, or null when there is none. */
|
|
196
199
|
export function setAsideReceiptPath(section) {
|
|
197
|
-
|
|
200
|
+
const path = SET_ASIDE_RECEIPT.exec(section)?.[1];
|
|
201
|
+
return path === undefined ? null : unquoteGitPath(path);
|
|
198
202
|
}
|
|
199
|
-
const DIFF_SECTIONS = /(
|
|
203
|
+
const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
|
|
200
204
|
function deletedPath(section) {
|
|
201
|
-
if (
|
|
205
|
+
if (!/(?<=^|\n)deleted file mode /.test(section))
|
|
202
206
|
return null;
|
|
203
|
-
const oldPath =
|
|
204
|
-
??
|
|
207
|
+
const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
|
|
208
|
+
?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
|
|
205
209
|
return oldPath ? diffSidePath(oldPath) : null;
|
|
206
210
|
}
|
|
207
211
|
/**
|
|
@@ -216,7 +220,7 @@ export function reviewableLogicDiff(diff) {
|
|
|
216
220
|
const path = deletedPath(section);
|
|
217
221
|
if (!path || isProtectedEvidence(path))
|
|
218
222
|
return section;
|
|
219
|
-
return `deleted file: ${path}\n`;
|
|
223
|
+
return `deleted file: ${quoteGitPath(path)}\n`;
|
|
220
224
|
}).join("");
|
|
221
225
|
}
|
|
222
226
|
function parseContentSection(section) {
|
|
@@ -240,12 +244,12 @@ function parseContentSection(section) {
|
|
|
240
244
|
function sectionPaths(section, parsed) {
|
|
241
245
|
if (parsed)
|
|
242
246
|
return [...new Set(parsed.sides.filter((path) => path !== null))];
|
|
243
|
-
const renamedFrom =
|
|
244
|
-
const renamedTo =
|
|
247
|
+
const renamedFrom = /(?<=^|\n)rename from ([^\r\n]+)/.exec(section)?.[1];
|
|
248
|
+
const renamedTo = /(?<=^|\n)rename to ([^\r\n]+)/.exec(section)?.[1];
|
|
245
249
|
if (renamedFrom || renamedTo) {
|
|
246
250
|
return [...new Set([renamedFrom, renamedTo].filter((path) => path !== undefined).map(unquoteGitPath))];
|
|
247
251
|
}
|
|
248
|
-
const header =
|
|
252
|
+
const header = /(?<=^|\n)diff --git ([^\r\n]+)/.exec(section)?.[1];
|
|
249
253
|
return header ? gitHeaderPaths(header) : [];
|
|
250
254
|
}
|
|
251
255
|
function hunkPayload(body, sign) {
|
|
@@ -293,7 +297,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
293
297
|
sections: [],
|
|
294
298
|
};
|
|
295
299
|
}
|
|
296
|
-
const rawSections = diff.split(
|
|
300
|
+
const rawSections = diff.split(DIFF_SECTIONS);
|
|
297
301
|
const kindOnly = kindOnlyPaths(rawSections);
|
|
298
302
|
const rendered = [];
|
|
299
303
|
const measurements = [];
|
|
@@ -321,7 +325,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
321
325
|
const withheld = parsed.lines.slice(parsed.minus).join("\n");
|
|
322
326
|
const captureBytes = Buffer.byteLength(withheld, "utf8");
|
|
323
327
|
const receiptPath = paths.at(-1);
|
|
324
|
-
const receipt = `set aside: regenerable capture ${receiptPath} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
328
|
+
const receipt = `set aside: regenerable capture ${quoteGitPath(receiptPath)} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
325
329
|
const compact = `${parsed.lines.slice(0, parsed.minus).join("\n")}\n${receipt}\n`;
|
|
326
330
|
rendered.push(compact);
|
|
327
331
|
measurements.push({
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** A path exactly as git printed it, quoted or not, decoded to the name on disk. Unquoted input is returned as is. */
|
|
2
2
|
export declare function unquoteGitPath(raw: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
|
|
5
|
+
* A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
|
|
6
|
+
* escaped, every other character kept), so no name can end a line; any other name is returned as is.
|
|
7
|
+
*/
|
|
8
|
+
export declare function quoteGitPath(path: string): string;
|
|
3
9
|
/**
|
|
4
10
|
* One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
|
|
5
11
|
* a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
|
package/dist/gates/git-paths.js
CHANGED
|
@@ -34,6 +34,28 @@ export function unquoteGitPath(raw) {
|
|
|
34
34
|
}
|
|
35
35
|
return Buffer.from(bytes).toString("utf8");
|
|
36
36
|
}
|
|
37
|
+
const NAMED = Object.fromEntries(Object.entries(ESCAPES).map(([name, code]) => [code, name]));
|
|
38
|
+
/**
|
|
39
|
+
* Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
|
|
40
|
+
* A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
|
|
41
|
+
* escaped, every other character kept), so no name can end a line; any other name is returned as is.
|
|
42
|
+
*/
|
|
43
|
+
export function quoteGitPath(path) {
|
|
44
|
+
if (!/[\x00-\x1f\x7f"\\\u2028\u2029]/.test(path))
|
|
45
|
+
return path;
|
|
46
|
+
let quoted = "";
|
|
47
|
+
for (const ch of path) {
|
|
48
|
+
const code = ch.codePointAt(0);
|
|
49
|
+
if (NAMED[code] !== undefined)
|
|
50
|
+
quoted += `\\${NAMED[code]}`;
|
|
51
|
+
else if (code < 0x20 || code === 0x7f || code === 0x2028 || code === 0x2029) {
|
|
52
|
+
quoted += [...Buffer.from(ch, "utf8")].map((byte) => `\\${byte.toString(8).padStart(3, "0")}`).join("");
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
quoted += ch;
|
|
56
|
+
}
|
|
57
|
+
return `"${quoted}"`;
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
|
|
39
61
|
* a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
|
|
@@ -104,7 +104,7 @@ export function readProcessIdentity(pid, host = {}) {
|
|
|
104
104
|
const ps = spawnSync("ps", ["-ww", "-o", "pgid=", "-o", "stat=", "-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
105
105
|
encoding: "utf8", env: { ...process.env, LC_ALL: "C" },
|
|
106
106
|
});
|
|
107
|
-
const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s
|
|
107
|
+
const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s*$/s.exec(ps.stdout ?? "");
|
|
108
108
|
if (ps.status !== 0 || !row)
|
|
109
109
|
return confirmedDead(pid) ? "DEAD" : "UNKNOWN";
|
|
110
110
|
if (row[2].startsWith("Z"))
|
package/dist/run/daemon.js
CHANGED
|
@@ -1244,7 +1244,7 @@ export async function observeWorkerProcessTree(marker, cwd) {
|
|
|
1244
1244
|
return "unmeasurable";
|
|
1245
1245
|
const rows = [];
|
|
1246
1246
|
for (const line of snapshot.stdout.split("\n")) {
|
|
1247
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(.*)
|
|
1247
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/s.exec(line);
|
|
1248
1248
|
if (match)
|
|
1249
1249
|
rows.push({ pid: match[1], ppid: match[2], command: match[3] });
|
|
1250
1250
|
}
|
|
@@ -1272,7 +1272,7 @@ const pathAtOrBelow = (root, candidate) => {
|
|
|
1272
1272
|
export function countLiveSuites(snapshot, repoRoot, daemonPid, cwdForPid, suiteParentForPid) {
|
|
1273
1273
|
const rows = [];
|
|
1274
1274
|
for (const line of snapshot.split("\n")) {
|
|
1275
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
1275
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
1276
1276
|
if (match && !match[3].startsWith("Z")) {
|
|
1277
1277
|
rows.push({ pid: Number(match[1]), ppid: Number(match[2]), command: match[4] });
|
|
1278
1278
|
}
|
|
@@ -1858,14 +1858,18 @@ export function recordFatalRunEnd(journal, runId, branch, err, graph, phase = "s
|
|
|
1858
1858
|
catch (readErr) {
|
|
1859
1859
|
console.error(`tickmarkr ${runId}: journal read failed while recording the fatal run-end (${readErr instanceof Error ? readErr.message : String(readErr)}) — original error: ${original}`);
|
|
1860
1860
|
}
|
|
1861
|
+
// A crash can land mid-dispatch. Resume re-pends every dispatched-but-unfinished task, so a running or
|
|
1862
|
+
// gated task is bucketed as the pending task resume will make it — pending, or blocked under a parked
|
|
1863
|
+
// dependency — and no task falls outside the five buckets every reader knows.
|
|
1864
|
+
const repended = graph && { ...graph, tasks: graph.tasks.map((t) => t.status === "running" || t.status === "gated" ? { ...t, status: "pending" } : t) };
|
|
1861
1865
|
const record = {
|
|
1862
1866
|
runId,
|
|
1863
1867
|
branch,
|
|
1864
1868
|
done: graph?.tasks.filter((t) => t.status === "done").map((t) => t.id) ?? [],
|
|
1865
1869
|
failed: graph?.tasks.filter((t) => t.status === "failed").map((t) => t.id) ?? [],
|
|
1866
1870
|
human: graph?.tasks.filter((t) => t.status === "human").map((t) => t.id) ?? [],
|
|
1867
|
-
blocked:
|
|
1868
|
-
pending:
|
|
1871
|
+
blocked: repended ? blockedTasks(repended).map((t) => t.id) : [],
|
|
1872
|
+
pending: repended ? pendingTasks(repended).map((t) => t.id) : [],
|
|
1869
1873
|
phase,
|
|
1870
1874
|
fatal: true,
|
|
1871
1875
|
error: original,
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -191,6 +191,13 @@ export declare const GATE_FINGERPRINT_CAP = 2;
|
|
|
191
191
|
export declare const receiptOrigin: (data: Record<string, unknown>) => string | undefined;
|
|
192
192
|
export declare function identicalGateFailures(events: JournalEvent[], taskId: string, gate: string, normalized: string): number;
|
|
193
193
|
export declare const readableExcerpt: (text: string) => string;
|
|
194
|
+
/**
|
|
195
|
+
* One-line display text for a recorded crash: every C0 control but tab and newline, DEL and every C1
|
|
196
|
+
* control prints as its visible escape (`\x1b`, `\u009b`), so no 7-bit or 8-bit OSC/CSI/ST sequence
|
|
197
|
+
* reaches a terminal and the reader still sees the text carried one; tabs and line breaks collapse to
|
|
198
|
+
* one space. The journal row keeps the raw bytes.
|
|
199
|
+
*/
|
|
200
|
+
export declare const visibleControls: (text: string) => string;
|
|
194
201
|
/** What each funded repair's next battery actually reached. A repair spends its budget only
|
|
195
202
|
* when that battery reaches one of the gates it was funded to fix; an earlier red is evidence that
|
|
196
203
|
* this repair never got its funded turn, not a charge against the repair ladder. */
|
|
@@ -379,6 +386,11 @@ export interface InterruptedAttempt {
|
|
|
379
386
|
}
|
|
380
387
|
export declare function resumeHarvestAuthor(events: JournalEvent[], taskId: string): Assignment | undefined;
|
|
381
388
|
export declare function interruptedAttempt(events: JournalEvent[], taskId: string): InterruptedAttempt | undefined;
|
|
389
|
+
/**
|
|
390
|
+
* The crash a FATAL run-end records, when it closes the newest engagement: `<phase> failed: <error>`.
|
|
391
|
+
* A later run-start or run-resume supersedes it; a normal run-end is no crash.
|
|
392
|
+
*/
|
|
393
|
+
export declare function fatalRunEndCause(events: readonly JournalEvent[]): string | undefined;
|
|
382
394
|
export declare function runHasEnded(events: JournalEvent[]): boolean;
|
|
383
395
|
/** OBS-53: classify worker-result failures so retries and routing see the true signal, not one lumped bucket. */
|
|
384
396
|
export declare function classifyWorkerResultCause(opts: {
|
package/dist/run/journal.js
CHANGED
|
@@ -763,6 +763,16 @@ export function identicalGateFailures(events, taskId, gate, normalized) {
|
|
|
763
763
|
// the failure fingerprint keep the raw text.
|
|
764
764
|
const TERMINAL_CONTROL_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[ -/]*[0-~]?|[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
765
765
|
export const readableExcerpt = (text) => text.replace(TERMINAL_CONTROL_RE, "");
|
|
766
|
+
/**
|
|
767
|
+
* One-line display text for a recorded crash: every C0 control but tab and newline, DEL and every C1
|
|
768
|
+
* control prints as its visible escape (`\x1b`, `\u009b`), so no 7-bit or 8-bit OSC/CSI/ST sequence
|
|
769
|
+
* reaches a terminal and the reader still sees the text carried one; tabs and line breaks collapse to
|
|
770
|
+
* one space. The journal row keeps the raw bytes.
|
|
771
|
+
*/
|
|
772
|
+
export const visibleControls = (text) => text.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/gu, (c) => {
|
|
773
|
+
const code = c.charCodeAt(0).toString(16);
|
|
774
|
+
return c < "\x80" ? `\\x${code.padStart(2, "0")}` : `\\u${code.padStart(4, "0")}`;
|
|
775
|
+
}).replace(/\s+/gu, " ").trim();
|
|
766
776
|
export function repairReachSinceApproval(events, taskId) {
|
|
767
777
|
const repairs = [];
|
|
768
778
|
for (const e of effectiveEvents(events)) { // OBS-1178: a refused or unsound approval resets no repair history
|
|
@@ -1367,6 +1377,28 @@ export function interruptedAttempt(events, taskId) {
|
|
|
1367
1377
|
}
|
|
1368
1378
|
// Runs can end and later resume in the same journal. The newest lifecycle marker decides whether
|
|
1369
1379
|
// an unresolved task is still recoverable by this live daemon or belongs to an ended run.
|
|
1380
|
+
/**
|
|
1381
|
+
* The crash a FATAL run-end records, when it closes the newest engagement: `<phase> failed: <error>`.
|
|
1382
|
+
* A later run-start or run-resume supersedes it; a normal run-end is no crash.
|
|
1383
|
+
*/
|
|
1384
|
+
export function fatalRunEndCause(events) {
|
|
1385
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
1386
|
+
const e = events[i];
|
|
1387
|
+
if (e.event === "run-start" || e.event === "run-resume")
|
|
1388
|
+
return undefined;
|
|
1389
|
+
if (e.event !== "run-end")
|
|
1390
|
+
continue;
|
|
1391
|
+
if (e.data.fatal !== true)
|
|
1392
|
+
return undefined;
|
|
1393
|
+
// Display text only: terminal controls in a recorded error (OSC title, CSI clear) print escaped,
|
|
1394
|
+
// never executed, and the journal row keeps the raw bytes.
|
|
1395
|
+
const shown = (v) => typeof v === "string" ? visibleControls(v) : "";
|
|
1396
|
+
const phase = shown(e.data.phase) || "run";
|
|
1397
|
+
const error = shown(e.data.error);
|
|
1398
|
+
return `${phase} failed: ${error || "unrecorded error"}`;
|
|
1399
|
+
}
|
|
1400
|
+
return undefined;
|
|
1401
|
+
}
|
|
1370
1402
|
export function runHasEnded(events) {
|
|
1371
1403
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
1372
1404
|
const event = events[i].event;
|
|
@@ -1943,7 +1975,9 @@ export class Journal {
|
|
|
1943
1975
|
// C1: the terminal record carries the owed-check fold as of this append, revalidated from
|
|
1944
1976
|
// each discharge's artifact — a reopened journal re-reduces, it never copies a prior total.
|
|
1945
1977
|
// Canonical (validated) rows are strict and keep their schema; the live producer is the tuple.
|
|
1946
|
-
|
|
1978
|
+
// A fatal record folds over itself, so its own owedChecks already read unknown with the crash.
|
|
1979
|
+
const folded = !decisionRow && data.fatal === true ? [...priorEvents, { ts: new Date().toISOString(), event, data }] : priorEvents;
|
|
1980
|
+
return decisionRow ? reduced : { ...reduced, owedChecks: foldOwedChecks(folded, join(this.dir, "..", "..", "..")) };
|
|
1947
1981
|
})()
|
|
1948
1982
|
: event === "resume-restore" && rowTaskId && upheldFeedbackByTask(effectiveEvents(priorEvents)).has(rowTaskId)
|
|
1949
1983
|
? {
|
|
@@ -2701,12 +2735,16 @@ function dischargeProblem(row, o, events, cwd, memo) {
|
|
|
2701
2735
|
* across folds; every artifact, hash, criteria and reviewer condition is still re-read on every fold.
|
|
2702
2736
|
*/
|
|
2703
2737
|
export function foldOwedChecks(events, cwd, memo) {
|
|
2738
|
+
let fold;
|
|
2704
2739
|
try {
|
|
2705
|
-
|
|
2740
|
+
fold = foldOwedChecksOrThrow(events, cwd, memo);
|
|
2706
2741
|
}
|
|
2707
2742
|
catch (error) {
|
|
2708
|
-
|
|
2743
|
+
fold = { known: false, debt: "unknown", outstanding: [], acceptedRisk: [], discharged: [], unknown: [{ reason: `owed-check fold failed: ${String(error).split("\n")[0]}` }] };
|
|
2709
2744
|
}
|
|
2745
|
+
// A fatal run-end never finished its checks: its debt is unknown and the crash leads the reasons, never 0.
|
|
2746
|
+
const fatal = fatalRunEndCause(events);
|
|
2747
|
+
return fatal ? { ...fold, known: false, debt: "unknown", unknown: [{ reason: `fatal run-end — ${fatal}` }, ...fold.unknown] } : fold;
|
|
2710
2748
|
}
|
|
2711
2749
|
function foldOwedChecksOrThrow(events, cwd, memo) {
|
|
2712
2750
|
// v2.6.7 T4 (G2): checkout trust is re-derived at the fold boundary, before a memoised proof can answer —
|
|
@@ -14,7 +14,7 @@ import { basename } from "node:path";
|
|
|
14
14
|
export const LAUNCHSERVICES_PROBE_CEILING_MS = 5_000;
|
|
15
15
|
// lstart in the C locale: "Wed Sep 30 21:54:33 2026"; comm is the executable path on Darwin.
|
|
16
16
|
const PS_ARGS = ["-A", "-o", "pid=,lstart=,comm="];
|
|
17
|
-
const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s
|
|
17
|
+
const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/s;
|
|
18
18
|
const MAX_PS_BYTES = 4 * 1024 * 1024;
|
|
19
19
|
/** Exactly one launchservicesd row, or null (absent, ambiguous or malformed output is unknown). */
|
|
20
20
|
export function parsePsIdentity(stdout) {
|
package/dist/run/lease.js
CHANGED
|
@@ -330,7 +330,7 @@ export async function describeLeaseBlockers(pids) {
|
|
|
330
330
|
done("");
|
|
331
331
|
}
|
|
332
332
|
});
|
|
333
|
-
const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)
|
|
333
|
+
const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)$/s.exec(line); return row ? [[Number(row[1]), row[2].trim()]] : []; }));
|
|
334
334
|
const cwds = await import("./suite-census.js").then(({ batchedProcessCwds }) => batchedProcessCwds(pids)).catch(() => undefined);
|
|
335
335
|
return pids.map((pid) => {
|
|
336
336
|
const cwd = cwds?.get(pid);
|
|
@@ -363,7 +363,7 @@ export async function protectedProcesses(holder, tokenEnv = REPOSITORY_LEASE_TOK
|
|
|
363
363
|
return !leader || sameProcessBirth(processBirth(pid), holder.rootBirths?.[pid]);
|
|
364
364
|
}));
|
|
365
365
|
const parsed = lines.flatMap(line => {
|
|
366
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
366
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
367
367
|
return match ? [{ pid: Number(match[1]), parent: Number(match[2]), group: Number(match[3]), stat: match[4], command: match[5] }] : [];
|
|
368
368
|
});
|
|
369
369
|
const parents = new Map(parsed.map(row => [row.pid, row.parent]));
|
package/dist/run/stall.js
CHANGED
|
@@ -118,7 +118,8 @@ export const workerReapHost = {
|
|
|
118
118
|
if (!lines.length || lines.length > 20_000)
|
|
119
119
|
return undefined;
|
|
120
120
|
for (const line of lines) {
|
|
121
|
-
|
|
121
|
+
// Queue row 127: the s flag — ps prints an argv's U+2028/U+2029 raw, and `.` alone never matches them.
|
|
122
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.*)$/s.exec(line);
|
|
122
123
|
if (!m)
|
|
123
124
|
return undefined;
|
|
124
125
|
if (m[5].startsWith("Z"))
|
|
@@ -418,7 +419,7 @@ async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
|
|
|
418
419
|
continue;
|
|
419
420
|
if (group !== undefined && Number(ids[3]) === group)
|
|
420
421
|
grouped.push(ids[1]);
|
|
421
|
-
const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)
|
|
422
|
+
const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/s.exec(line);
|
|
422
423
|
if (!m)
|
|
423
424
|
continue;
|
|
424
425
|
const cpu = parsePsCpu(m[3]);
|
package/dist/run/suite-census.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function batchedProcessCwds(pids, exec = probeExec, proc = procRead
|
|
|
56
56
|
export function runnerPids(snapshot) {
|
|
57
57
|
const pids = [];
|
|
58
58
|
for (const line of snapshot.split("\n")) {
|
|
59
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
59
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
60
60
|
if (match && !match[3].startsWith("Z") && isRunnerCommand(match[4]))
|
|
61
61
|
pids.push(Number(match[1]));
|
|
62
62
|
}
|
|
@@ -95,7 +95,7 @@ export async function batchedProcessProbes(pids, exec = probeExec, proc = procRe
|
|
|
95
95
|
if (rest.length === 0)
|
|
96
96
|
return;
|
|
97
97
|
for (const line of (await ask("ps", ["eww", "-p", rest.join(","), "-o", "pid=,command="])).split("\n")) {
|
|
98
|
-
const row = /^\s*(\d+)\s+(.*)
|
|
98
|
+
const row = /^\s*(\d+)\s+(.*)$/s.exec(line);
|
|
99
99
|
const value = row ? parentIn(row[2]) : undefined;
|
|
100
100
|
if (row && value)
|
|
101
101
|
parents.set(Number(row[1]), Number(value));
|
package/package.json
CHANGED
|
@@ -70,6 +70,18 @@ since_arm() { tail -n +$((base + 1)) "$1" 2>/dev/null; }
|
|
|
70
70
|
|
|
71
71
|
field() { printf '%s' "$2" | sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" | head -1; }
|
|
72
72
|
bucket() { printf '%s' "$2" | sed -n "s/.*\"$1\":\[\([^]]*\)\].*/\1/p" | head -1; }
|
|
73
|
+
# A row's data string value DECODED by JSON.parse (an escaped `\"` never cuts a quoted ref in a git error
|
|
74
|
+
# short), printed on one line as the product's own crash reason is (visibleControls): every C0 control
|
|
75
|
+
# but tab and newline, DEL and every C1 control prints as its visible escape (`\x1b`, `\u009b`), so a
|
|
76
|
+
# recorded 7-bit or 8-bit OSC/CSI/ST never reaches the terminal; tabs and line breaks collapse to a space.
|
|
77
|
+
jstr() {
|
|
78
|
+
printf '%s' "$2" | node -e '
|
|
79
|
+
let v; try { const row = JSON.parse(require("fs").readFileSync(0, "utf8")); v = (row.data ?? row)[process.argv[1]]; } catch {}
|
|
80
|
+
if (typeof v === "string") process.stdout.write(v.replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, (c) => {
|
|
81
|
+
const h = c.charCodeAt(0).toString(16);
|
|
82
|
+
return c < "\x80" ? "\\x" + h.padStart(2, "0") : "\\u" + h.padStart(4, "0");
|
|
83
|
+
}).replace(/\s+/g, " ").trim());' "$1" 2>/dev/null
|
|
84
|
+
}
|
|
73
85
|
|
|
74
86
|
report() {
|
|
75
87
|
# $3 is the row's physical journal line: with its ts it is the token a decision binds to (OBS-1178)
|
|
@@ -77,8 +89,13 @@ report() {
|
|
|
77
89
|
ev=$(field event "$line")
|
|
78
90
|
case "$ev" in
|
|
79
91
|
run-end)
|
|
80
|
-
local tv done failed human blocked pending verdict
|
|
92
|
+
local tv done failed human blocked pending verdict fatal=""
|
|
81
93
|
tv=$(field tipVerify "$line")
|
|
94
|
+
# A FATAL run-end is a crash, never green — even with every bucket empty (its tasks never finished).
|
|
95
|
+
case "$line" in *'"fatal":true'*)
|
|
96
|
+
local err; err=$(jstr error "$line")
|
|
97
|
+
fatal="$(jstr phase "$line") failed: ${err:0:160}" ;;
|
|
98
|
+
esac
|
|
82
99
|
done=$(bucket done "$line"); failed=$(bucket failed "$line")
|
|
83
100
|
human=$(bucket human "$line"); blocked=$(bucket blocked "$line")
|
|
84
101
|
pending=$(bucket pending "$line")
|
|
@@ -87,16 +104,20 @@ report() {
|
|
|
87
104
|
# clause open: a waived review is still owed (D-660), and a `verify --record` discharge that lands
|
|
88
105
|
# AFTER run-end moves only CURRENT status. This row is history, so the best it can say is
|
|
89
106
|
# EXECUTION COMPLETE; green is read from CURRENT `tickmarkr status <runId>` at `outstanding 0`.
|
|
90
|
-
if [ "$tv" != "failed" ] && [ -z "$failed$human$blocked$pending" ]; then
|
|
107
|
+
if [ -z "$fatal" ] && [ "$tv" != "failed" ] && [ -z "$failed$human$blocked$pending" ]; then
|
|
91
108
|
verdict="EXECUTION COMPLETE"
|
|
92
109
|
else
|
|
93
110
|
verdict="NOT GREEN"
|
|
94
111
|
fi
|
|
95
|
-
echo "RUN_END $run — $verdict (tipVerify=${tv:-unknown})"
|
|
112
|
+
echo "RUN_END $run — $verdict (${fatal:+fatal: $fatal; }tipVerify=${tv:-unknown})"
|
|
96
113
|
echo " done=[${done}] failed=[${failed}] human=[${human}] blocked=[${blocked}] pending=[${pending}]"
|
|
97
|
-
[ "$
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
if [ -n "$fatal" ]; then
|
|
115
|
+
echo " the run CRASHED ($fatal) — a fatal run-end is never green; its debt reads unknown until the run is resumed, and from then on the resumed engagement owns it"
|
|
116
|
+
elif [ "$verdict" = "EXECUTION COMPLETE" ]; then
|
|
117
|
+
echo " execution buckets empty and tip verify is not failed — not yet green: read CURRENT \`tickmarkr status $run\`; only \`outstanding 0\` is green, \`outstanding N (...)\` and \`outstanding unknown\` are not, and this run-end record's own count is historical"
|
|
118
|
+
else
|
|
119
|
+
echo " a non-empty bucket or a failed tip above is the reason; name it, never report this run as green"
|
|
120
|
+
fi
|
|
100
121
|
;;
|
|
101
122
|
task-human)
|
|
102
123
|
echo "TASK_HUMAN $(field taskId "$line") — $run"
|
|
@@ -15,15 +15,23 @@
|
|
|
15
15
|
# never hours; 900 is a generous default for a 7-task spec)
|
|
16
16
|
# <overseer-pane> the pane that must hear about it (herdr pane id), e.g. wZ:p18S
|
|
17
17
|
# [poll-s] poll interval, default 15
|
|
18
|
-
# exit 0 LAUNCH_OK (lock
|
|
18
|
+
# exit 0 LAUNCH_OK (a lock held by a LIVE pid; prints its contents) · exit 3 LAUNCH_OVERDUE (delivered) · exit 64 usage
|
|
19
|
+
#
|
|
20
|
+
# A lock is a launch only while its holder lives: a lock left by a crashed daemon (dead pid, or no pid
|
|
21
|
+
# at all) is stale, proves nothing about the new GO, and the watch keeps waiting for a live holder.
|
|
22
|
+
# `ps -p` answers for another user's process too, so EPERM never reads as death (lock.ts isPidLive).
|
|
19
23
|
set -u
|
|
20
24
|
LOCK="${1:-}"; DEADLINE="${2:-}"; PANE="${3:-}"; POLL="${4:-15}"
|
|
21
25
|
[ -n "$LOCK" ] && [ -n "$DEADLINE" ] && [ -n "$PANE" ] || { echo "usage: watch-launch.sh <lock-path> <deadline-s> <overseer-pane> [poll-s]" >&2; exit 64; }
|
|
22
26
|
start=$(date +%s)
|
|
23
27
|
while :; do
|
|
24
28
|
if [ -f "$LOCK" ]; then
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
body=$(tr -d '\n' < "$LOCK" 2>/dev/null)
|
|
30
|
+
pid=$(printf '%s' "$body" | sed -n 's/.*"pid":\([0-9][0-9]*\).*/\1/p')
|
|
31
|
+
if [ -n "$pid" ] && ps -p "$pid" >/dev/null 2>&1; then
|
|
32
|
+
printf 'LAUNCH_OK %s %s\n' "$(date -u +%H:%M:%SZ)" "$body"
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
27
35
|
fi
|
|
28
36
|
now=$(date +%s)
|
|
29
37
|
if [ $((now - start)) -ge "$DEADLINE" ]; then
|