tickmarkr 2.8.5 → 2.8.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/adapters/codex-commit-check.d.ts +4 -3
- package/dist/adapters/codex-commit-check.js +42 -28
- package/dist/adapters/codex.d.ts +2 -2
- package/dist/adapters/codex.js +38 -32
- package/dist/adapters/grok.js +5 -9
- package/dist/adapters/model-lints.js +4 -3
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +10 -1
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/approve.js +9 -28
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +38 -20
- package/dist/cli/commands/status.js +9 -6
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/compile/gsd.js +4 -3
- package/dist/compile/native.js +8 -6
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/baseline.js +51 -10
- package/dist/gates/cache.js +2 -4
- package/dist/graph/schema.js +3 -1
- package/dist/run/daemon.js +147 -35
- package/dist/run/git-trust.d.ts +11 -0
- package/dist/run/git-trust.js +1 -1
- package/dist/run/git.d.ts +18 -1
- package/dist/run/git.js +73 -2
- package/dist/run/journal.d.ts +15 -1
- package/dist/run/journal.js +43 -6
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +2 -2
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- package/skills/tickmarkr-overseer/SKILL.md +10 -8
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
1
|
import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,7 +10,8 @@ import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
|
|
|
11
10
|
import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
|
|
12
11
|
import { stateDirName, tickmarkrDir } from "../../graph/graph.js";
|
|
13
12
|
import { orcaHostDetected } from "../../drivers/index.js";
|
|
14
|
-
import { WORKTREES_DIR } from "../../run/git.js";
|
|
13
|
+
import { gitSync, WORKTREES_DIR } from "../../run/git.js";
|
|
14
|
+
import { GitTrustRefusal } from "../../run/git-trust.js";
|
|
15
15
|
import { Journal } from "../../run/journal.js";
|
|
16
16
|
import { runLockOwner, runLockRunId, runStatusLine } from "../../run/lock.js";
|
|
17
17
|
import { doctor } from "./doctor.js";
|
|
@@ -107,7 +107,11 @@ Outside multi-agent environments, run the loop directly.
|
|
|
107
107
|
|
|
108
108
|
### Version preflight
|
|
109
109
|
|
|
110
|
-
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version
|
|
110
|
+
Before \`tickmarkr compile\` or \`tickmarkr run\`: run \`tickmarkr version --check\` and stop on a non-zero exit. In the tickmarkr source repository (\`package.json\` named \`tickmarkr\`), stop if the versions differ anywhere (major, minor, or patch); the binary and repository must agree on the entire version, so binary \`2.1.0\` versus repository \`2.1.1\` is a stop. In a consumer repository, the check compares installed agent guidance with this binary's packaged block and skills, never the app's manifest version; refresh drifted guidance with \`tickmarkr init --agent --force\`. Tell the operator to update or link the correct binary when the source version differs. Never proceed on hope — stale binaries silently skip daemon gates. Also verify no run is live in this repository before starting one: lead with this repository's \`.tickmarkr/graph.lock\`, read its holder pid, and treat it as live until \`kill -0 <pid>\` proves that holder dead. Never require a machine-wide process pattern to be empty: a lawful run in another repository — or the probing shell's own argv — can match. If you use a process probe as secondary evidence, exclude the probing process, resolve every candidate's own cwd (for example with \`lsof -a -p <pid> -d cwd\`), and count only candidates whose cwd is this repository root.
|
|
111
|
+
|
|
112
|
+
### Seat hygiene
|
|
113
|
+
|
|
114
|
+
In a multi-tier session, the tiers clear each other at 50% context or more, after writing a handoff. A new spec or run is a mission boundary: write the handoff and clear before taking the next mission. Copy this duty into every brief and handoff. On a single-tier plain host, write your handoff and ask the operator to clear the session.
|
|
111
115
|
|
|
112
116
|
### Tip-verify-before-green
|
|
113
117
|
|
|
@@ -184,7 +188,7 @@ function skillFileList(root, prefix = "") {
|
|
|
184
188
|
}
|
|
185
189
|
// Drift is only computed for skills that ARE installed. A skill never installed is not drift — `--agent`
|
|
186
190
|
// installs it and says so, and nagging about it would bury the case this exists for.
|
|
187
|
-
function skillDrift(cwd) {
|
|
191
|
+
export function skillDrift(cwd) {
|
|
188
192
|
const drifted = [];
|
|
189
193
|
for (const { skillsDir } of hostTargets(cwd)) {
|
|
190
194
|
for (const skill of AGENT_SKILLS) {
|
|
@@ -215,7 +219,7 @@ function skillDrift(cwd) {
|
|
|
215
219
|
// superseded preflight rules (patch-version drift tolerated, a machine-wide process match required)
|
|
216
220
|
// through the very release that fixed the tracked copy, and a consultation was launched under the stale
|
|
217
221
|
// text with nobody able to see it. Same rule as skillDrift: a doc that was never written is not drift.
|
|
218
|
-
const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
222
|
+
export const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter((docPath) => {
|
|
219
223
|
if (!existsSync(docPath))
|
|
220
224
|
return false;
|
|
221
225
|
const current = readFileSync(docPath, "utf8");
|
|
@@ -227,6 +231,12 @@ const docsDrift = (cwd) => hostTargets(cwd).map(({ docPath }) => docPath).filter
|
|
|
227
231
|
return true; // a truncated block is stale by definition
|
|
228
232
|
return current.slice(begin, end + DOCS_END.length) !== AGENT_DOCS.trimEnd();
|
|
229
233
|
});
|
|
234
|
+
export function installedAgentGuidance(cwd) {
|
|
235
|
+
return hostTargets(cwd).flatMap(({ skillsDir, docPath }) => [
|
|
236
|
+
...AGENT_SKILLS.map((skill) => join(skillsDir, skill, "SKILL.md")).filter((path) => existsSync(path)),
|
|
237
|
+
...(existsSync(docPath) && readFileSync(docPath, "utf8").includes(DOCS_BEGIN) ? [docPath] : []),
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
230
240
|
const describeDrift = (d) => [
|
|
231
241
|
d.missing.length ? `missing ${d.missing.join(", ")}` : "",
|
|
232
242
|
d.modified.length ? `modified ${d.modified.join(", ")}` : "",
|
|
@@ -256,13 +266,8 @@ function gitTracked(cwd, path) {
|
|
|
256
266
|
const rel = relative(cwd, path);
|
|
257
267
|
if (rel.startsWith(".."))
|
|
258
268
|
return false;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
269
|
+
// queue row 2 item b: protected git; a trust refusal throws, an ordinary failure still reads as untracked
|
|
270
|
+
return gitSync(cwd, ["ls-files", "--error-unmatch", "--", rel]).status === 0;
|
|
266
271
|
}
|
|
267
272
|
async function installAgentFiles(cwd, force, docs, notes) {
|
|
268
273
|
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
@@ -375,19 +380,29 @@ function isPathInside(parentDir, candidatePath) {
|
|
|
375
380
|
}
|
|
376
381
|
/**
|
|
377
382
|
* The ONLY way the --remove path runs git: GIT_DIR, GIT_WORK_TREE and GIT_COMMON_DIR are stripped so the repository acted on
|
|
378
|
-
* is always cwd's own, and LC_ALL=C keeps stderr parseable.
|
|
383
|
+
* is always cwd's own, and LC_ALL=C keeps stderr parseable. Queue row 2 item b: it runs through the protected path (gitSync),
|
|
384
|
+
* whose GitTrustRefusal propagates unchanged; a failed git throws an error carrying its status and stderr.
|
|
379
385
|
*/
|
|
380
386
|
function removeGit(cwd, args) {
|
|
381
387
|
const { GIT_DIR: _dir, GIT_WORK_TREE: _workTree, GIT_COMMON_DIR: _commonDir, ...env } = process.env;
|
|
382
|
-
|
|
388
|
+
const r = gitSync(cwd, args, { env: { ...env, LC_ALL: "C" } });
|
|
389
|
+
if (r.error)
|
|
390
|
+
throw r.error;
|
|
391
|
+
if (r.status !== 0)
|
|
392
|
+
throw Object.assign(new Error(`git ${args.join(" ")} failed (${r.status ?? r.signal})`), { status: r.status, stderr: r.stderr });
|
|
393
|
+
return r.stdout;
|
|
383
394
|
}
|
|
395
|
+
/** A trust refusal is never one of the --remove path's fail-open answers. */
|
|
396
|
+
const rethrowRefusal = (error) => { if (error instanceof GitTrustRefusal)
|
|
397
|
+
throw error; };
|
|
384
398
|
function isLinkedWorktree(cwd) {
|
|
385
399
|
try {
|
|
386
400
|
const gitDir = removeGit(cwd, ["rev-parse", "--git-dir"]).trim();
|
|
387
401
|
const gitCommonDir = removeGit(cwd, ["rev-parse", "--git-common-dir"]).trim();
|
|
388
402
|
return resolve(cwd, gitDir) !== resolve(cwd, gitCommonDir);
|
|
389
403
|
}
|
|
390
|
-
catch {
|
|
404
|
+
catch (error) {
|
|
405
|
+
rethrowRefusal(error);
|
|
391
406
|
return false;
|
|
392
407
|
}
|
|
393
408
|
}
|
|
@@ -405,7 +420,8 @@ function checkLiveLock(cwd) {
|
|
|
405
420
|
return mainOwner;
|
|
406
421
|
}
|
|
407
422
|
}
|
|
408
|
-
catch {
|
|
423
|
+
catch (error) {
|
|
424
|
+
rethrowRefusal(error);
|
|
409
425
|
// not a git repo
|
|
410
426
|
}
|
|
411
427
|
return undefined;
|
|
@@ -420,6 +436,7 @@ function isGitRepo(cwd) {
|
|
|
420
436
|
return true;
|
|
421
437
|
}
|
|
422
438
|
catch (err) {
|
|
439
|
+
rethrowRefusal(err);
|
|
423
440
|
const e = err;
|
|
424
441
|
const stderr = String(e.stderr ?? "").trim();
|
|
425
442
|
if (e.status === 128 && stderr.startsWith("fatal: not a git repository (or any of the parent directories)"))
|
|
@@ -433,6 +450,7 @@ function git(cwd, args, message) {
|
|
|
433
450
|
return removeGit(cwd, args);
|
|
434
451
|
}
|
|
435
452
|
catch (err) {
|
|
453
|
+
rethrowRefusal(err);
|
|
436
454
|
const stderr = String(err.stderr ?? "").trim();
|
|
437
455
|
throw new Error(stderr ? `${message}: ${stderr}` : message);
|
|
438
456
|
}
|
|
@@ -522,16 +540,16 @@ function handleRemove(cwd) {
|
|
|
522
540
|
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/tickmarkr/"]);
|
|
523
541
|
branches = out.split("\n").map((b) => b.trim()).filter(Boolean);
|
|
524
542
|
}
|
|
525
|
-
catch {
|
|
526
|
-
|
|
543
|
+
catch (error) {
|
|
544
|
+
rethrowRefusal(error);
|
|
527
545
|
}
|
|
528
546
|
let preservedRefs = [];
|
|
529
547
|
try {
|
|
530
548
|
const out = removeGit(cwd, ["for-each-ref", "--format=%(refname)", "refs/tickmarkr/preserved/"]);
|
|
531
549
|
preservedRefs = out.split("\n").map((r) => r.trim()).filter(Boolean);
|
|
532
550
|
}
|
|
533
|
-
catch {
|
|
534
|
-
|
|
551
|
+
catch (error) {
|
|
552
|
+
rethrowRefusal(error);
|
|
535
553
|
}
|
|
536
554
|
for (const b of branches) {
|
|
537
555
|
notes.push(`left branch: ${b}`);
|
|
@@ -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, fatalRunEndCause, recordedTaskFailureKind, runHasEnded,
|
|
16
|
+
import { Journal, bindingToken, effectiveEvents, formatJournalNarration, parseJournalText, physicalLine, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, fatalRunEndCause, recordedTaskFailureKind, runHasEnded, restoresUpheldFeedback, 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";
|
|
@@ -1070,15 +1070,18 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1070
1070
|
}).map(sanitizeTaskText)
|
|
1071
1071
|
: [];
|
|
1072
1072
|
// OBS-738: recovery facts stay on the journal's two established reducers. The prefix fold keeps an
|
|
1073
|
-
// older journal equally readable by asking
|
|
1073
|
+
// older journal equally readable by asking restoresUpheldFeedback what was funded at that exact
|
|
1074
1074
|
// restore boundary; current resume-restore rows additionally carry the same task for narration.
|
|
1075
1075
|
// OBS-1178: both folds read decisions through the one decision fold over the whole journal.
|
|
1076
|
+
// Queue row 225 (D-2111): only each task's NEWEST restore speaks — never a historical set.
|
|
1076
1077
|
const decided = effectiveEvents(events);
|
|
1077
1078
|
const preservedRefs = preservedRefsByTask(events);
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1079
|
+
const newestRestores = new Map();
|
|
1080
|
+
decided.forEach((event, index) => { if (event.event === "resume-restore" && event.taskId)
|
|
1081
|
+
newestRestores.set(event.taskId, index); });
|
|
1082
|
+
const restoredUpheldFeedback = new Set([...newestRestores]
|
|
1083
|
+
.filter(([taskId, index]) => restoresUpheldFeedback(decided.slice(0, index), taskId))
|
|
1084
|
+
.map(([taskId]) => taskId));
|
|
1082
1085
|
// OBS-1178: a failed task's newest task-failed row is the token its recheck binds to. Only an
|
|
1083
1086
|
// effective decision consumes it — a refused or unsound recheck leaves the task failed and the token shown.
|
|
1084
1087
|
const failureTokens = new Map();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function distFingerprint(dir?: string): string;
|
|
2
|
-
export declare function version(argv?: string[], dir?: string): Promise<string>;
|
|
2
|
+
export declare function version(argv?: string[], dir?: string, cwd?: string): Promise<string>;
|
|
@@ -23,7 +23,41 @@ export function distFingerprint(dir = distDir) {
|
|
|
23
23
|
hash.update(relative(dir, file)).update("\0").update(readFileSync(file)).update("\0");
|
|
24
24
|
return hash.digest("hex");
|
|
25
25
|
}
|
|
26
|
-
export async function version(argv = [], dir = distDir) {
|
|
26
|
+
export async function version(argv = [], dir = distDir, cwd = process.cwd()) {
|
|
27
27
|
const { version: v } = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
28
|
-
|
|
28
|
+
const stamp = argv.includes("--dist") ? `${v} dist:${resolve(dir)} fingerprint:${distFingerprint(dir)}` : v;
|
|
29
|
+
if (!argv.includes("--check"))
|
|
30
|
+
return stamp;
|
|
31
|
+
let manifest;
|
|
32
|
+
try {
|
|
33
|
+
manifest = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch { /* no source manifest — consumer guidance check */ }
|
|
36
|
+
if (manifest?.name === "tickmarkr") {
|
|
37
|
+
const repoVersion = manifest.version ?? "missing";
|
|
38
|
+
const equal = v === repoVersion;
|
|
39
|
+
const out = `tickmarkr binary ${stamp}: compared tickmarkr source repository package.json version ${repoVersion} — ${equal ? "entire version matches" : "versions differ; update or link the correct binary"}`;
|
|
40
|
+
if (!equal)
|
|
41
|
+
throw new Error(out);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
const { docsDrift, installedAgentGuidance, skillDrift } = await import("./init.js");
|
|
45
|
+
const installed = installedAgentGuidance(cwd);
|
|
46
|
+
if (!installed.length)
|
|
47
|
+
return `tickmarkr binary ${stamp}: no installed agent guidance to compare`;
|
|
48
|
+
const drift = [
|
|
49
|
+
...skillDrift(cwd).flatMap(({ dir: skillDir, missing, modified }) => [
|
|
50
|
+
...missing.map((file) => `missing ${relative(cwd, join(skillDir, file))}`),
|
|
51
|
+
...modified.map((file) => `modified ${relative(cwd, join(skillDir, file))}`),
|
|
52
|
+
]),
|
|
53
|
+
...docsDrift(cwd).map((file) => `stale agent-docs block in ${relative(cwd, file)}`),
|
|
54
|
+
];
|
|
55
|
+
const out = [
|
|
56
|
+
`tickmarkr binary ${stamp}: compared installed agent guidance with this binary's packaged block and skills:`,
|
|
57
|
+
...installed.map((file) => ` ${relative(cwd, file)}`),
|
|
58
|
+
...(drift.length ? [...drift, "refresh: tickmarkr init --agent --force"] : ["installed guidance matches"]),
|
|
59
|
+
].join("\n");
|
|
60
|
+
if (drift.length)
|
|
61
|
+
throw new Error(out);
|
|
62
|
+
return out;
|
|
29
63
|
}
|
package/dist/cli/help.d.ts
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -169,9 +169,12 @@ export const COMMAND_HELP = {
|
|
|
169
169
|
],
|
|
170
170
|
},
|
|
171
171
|
version: {
|
|
172
|
-
usage: "version [--dist]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
-
options: {
|
|
174
|
-
|
|
172
|
+
usage: "version [--dist] [--check]", description: "Print the installed package version. Top-level aliases: --version and -v.",
|
|
173
|
+
options: {
|
|
174
|
+
"--dist": "Also print the resolved build directory and distribution fingerprint.",
|
|
175
|
+
"--check": "Compare the entire source version when package.json is named tickmarkr; otherwise check installed agent guidance against this binary. Exit non-zero on drift; no installed guidance exits zero.",
|
|
176
|
+
},
|
|
177
|
+
examples: ["version", "version --dist", "version --check"],
|
|
175
178
|
},
|
|
176
179
|
verify: {
|
|
177
180
|
usage: "verify [--base <ref>] [--criteria <file> | --task <id>] [options]",
|
package/dist/compile/gsd.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
3
|
import { parse as parseYaml } from "yaml";
|
|
5
4
|
import { AcceptanceItemSchema, TIERS, validateGraph } from "../graph/schema.js";
|
|
6
5
|
import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
|
|
6
|
+
import { gitSync } from "../run/git.js";
|
|
7
7
|
import { classifyContextPath } from "./native.js";
|
|
8
8
|
// GSD artifact front-end (spec v1.3): one GSD *plan* is one tickmarkr *task* — a plan is
|
|
9
9
|
// worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
|
|
@@ -306,9 +306,10 @@ export function compileGsd(src, root) {
|
|
|
306
306
|
// Match native context reachability against the committed tree. This block only obtains the HEAD
|
|
307
307
|
// snapshot; classifyContextPath remains the single authority for missing, untracked, and glob refs.
|
|
308
308
|
const gitDir = dirname(files[0]);
|
|
309
|
-
|
|
309
|
+
// queue row 2 item b: protected git; a trust refusal throws instead of reading as "no repository"
|
|
310
|
+
const top = gitSync(gitDir, ["rev-parse", "--show-toplevel"]);
|
|
310
311
|
// -z (queue row 71): without it a non-ASCII path is C-quoted and a tracked ref reads as untracked
|
|
311
|
-
const tree =
|
|
312
|
+
const tree = gitSync(gitDir, ["ls-tree", "--full-tree", "-r", "--name-only", "-z", "HEAD"]);
|
|
312
313
|
const repoRoot = typeof top.stdout === "string" ? top.stdout.trim() : "";
|
|
313
314
|
// `root` is the repository whose worktrees will consume these refs. A source nested inside some
|
|
314
315
|
// unrelated repository (as with vendored fixtures) must not be judged against that outer tree.
|
package/dist/compile/native.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
5
4
|
import picomatch from "picomatch";
|
|
6
5
|
import { GATE_NAMES, GRAPH_ROUTING_MODES, ORACLES, renderAcceptanceItem, SHAPES, TIERS, validateGraph, } from "../graph/schema.js";
|
|
6
|
+
import { gitSync } from "../run/git.js";
|
|
7
7
|
import { criterionReadsOnly } from "./collateral.js";
|
|
8
8
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
9
9
|
// OBS-170/OBS-184: `context:` is a promise to the worker, and nothing ever checked it could be kept.
|
|
@@ -18,7 +18,8 @@ import { CompileError, inferShape, sha256 } from "./common.js";
|
|
|
18
18
|
// `ls-tree -r HEAD` enumerates that tree once; per-path `cat-file -e HEAD:<path>` is the same oracle
|
|
19
19
|
// one path at a time, and one spawn beats N.
|
|
20
20
|
function trackedAtHead(dir) {
|
|
21
|
-
|
|
21
|
+
// queue row 2 item b: every compile git runs through the protected path; a trust refusal throws, never fails open
|
|
22
|
+
const git = (...args) => gitSync(dir, args);
|
|
22
23
|
const top = git("rev-parse", "--show-toplevel");
|
|
23
24
|
// --full-tree is load-bearing: without it `ls-tree` scopes to the CWD's subtree and prints paths
|
|
24
25
|
// relative to it, so a spec in specs/ would see only specs/-relative names and misjudge every
|
|
@@ -146,7 +147,7 @@ function symlinkScopeWarnings(file, tasks, base) {
|
|
|
146
147
|
if (!scoped.length || !repositoryRoot(file))
|
|
147
148
|
return [];
|
|
148
149
|
const tree = base ?? "HEAD";
|
|
149
|
-
const git = (...args) =>
|
|
150
|
+
const git = (...args) => gitSync(dirname(file), args);
|
|
150
151
|
const failed = (r) => r.error || r.signal || r.status !== 0 || typeof r.stdout !== "string";
|
|
151
152
|
const why = (r) => r.signal ? `signal ${r.signal}` : r.status === null ? "git did not run" : `git exit ${r.status}`;
|
|
152
153
|
const advisory = (what, r) => `tickmarkr: advisory: files[] symlink inspection could not read ${what} in the selected git tree ${JSON.stringify(tree)} (${why(r)}) — symlink scope is unverified for this compile.`;
|
|
@@ -270,7 +271,7 @@ function testsAtHead(root) {
|
|
|
270
271
|
const cached = headTestCache.get(root);
|
|
271
272
|
if (cached)
|
|
272
273
|
return cached;
|
|
273
|
-
const grep =
|
|
274
|
+
const grep = gitSync(root, ["grep", "-I", "-n", "-e", "", "HEAD", "--", "tests/*.test.ts", "tests/**/*.test.ts"]);
|
|
274
275
|
if (grep.status !== 0 || typeof grep.stdout !== "string") {
|
|
275
276
|
headTestCache.set(root, []);
|
|
276
277
|
return [];
|
|
@@ -389,7 +390,7 @@ function criterionScopeFinding(task, text, criterion, id, tests) {
|
|
|
389
390
|
// 20-task spec compiled in seconds and the committed-spec corpus test timed out. Fail-open like
|
|
390
391
|
// testsAtHead: no git answer means no corpus, and the lint stays quiet rather than wrong.
|
|
391
392
|
function repoFileList(root) {
|
|
392
|
-
const ls =
|
|
393
|
+
const ls = gitSync(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
393
394
|
if (ls.status !== 0 || typeof ls.stdout !== "string")
|
|
394
395
|
return [];
|
|
395
396
|
return ls.stdout.split("\0").filter(Boolean).sort();
|
|
@@ -949,7 +950,8 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
949
950
|
mode: partner-led | risk-based | staff-led — this engagement's routing mode
|
|
950
951
|
(loses only to an explicit \`run --mode\` flag)
|
|
951
952
|
base: commit-or-ref — optional declared source base; the declaration must begin at column zero
|
|
952
|
-
(
|
|
953
|
+
(proven against the run's starting commit at run and resume start; a drifted
|
|
954
|
+
or unresolvable base is refused before anything is written)
|
|
953
955
|
|
|
954
956
|
Fields available per task:
|
|
955
957
|
goal: outcome the task must achieve (defaults to the title if omitted)
|
|
@@ -49,7 +49,7 @@ export class SubprocessDriver {
|
|
|
49
49
|
n = 0;
|
|
50
50
|
async slot(cwd, name) {
|
|
51
51
|
const id = `sp-${++this.n}`;
|
|
52
|
-
this.slots.set(id, { buf: "", exited: false });
|
|
52
|
+
this.slots.set(id, { buf: "", receivedBytes: 0, exited: false });
|
|
53
53
|
return { id, name, cwd };
|
|
54
54
|
}
|
|
55
55
|
state(slot) {
|
|
@@ -72,8 +72,13 @@ export class SubprocessDriver {
|
|
|
72
72
|
detached: true,
|
|
73
73
|
});
|
|
74
74
|
s.proc = p;
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// Queue row 233 (D-2130): count both received streams before retention truncates the tail.
|
|
76
|
+
const receive = (d) => {
|
|
77
|
+
s.receivedBytes += d.length;
|
|
78
|
+
s.buf = (s.buf + d).slice(-MAX_BUF);
|
|
79
|
+
};
|
|
80
|
+
p.stdout.on("data", receive);
|
|
81
|
+
p.stderr.on("data", receive);
|
|
77
82
|
p.on("close", () => (s.exited = true));
|
|
78
83
|
p.on("error", (e) => { s.buf = (s.buf + `\n[tickmarkr subprocess error] ${e}\n`).slice(-MAX_BUF); s.exited = true; });
|
|
79
84
|
// Queue row 108 (D-1622): THIS run's transport state. The slot's `exited` outlives an invocation (a reused slot) and
|
|
@@ -117,7 +122,7 @@ export class SubprocessDriver {
|
|
|
117
122
|
async transportState(slot) {
|
|
118
123
|
const s = this.state(slot);
|
|
119
124
|
const run = s.invocation;
|
|
120
|
-
return { closed: run?.closed ?? false, bytes:
|
|
125
|
+
return { closed: run?.closed ?? false, bytes: s.receivedBytes, exitCode: run?.exitCode ?? null, signal: run?.signal ?? null };
|
|
121
126
|
}
|
|
122
127
|
async read(slot, lines) {
|
|
123
128
|
return this.state(slot).buf.split("\n").slice(-lines).join("\n");
|
package/dist/gates/baseline.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { execFileSync } from "node:child_process";
|
|
3
2
|
import { EVICTION_TOMBSTONE_LIMIT, EVICTION_TOMBSTONES_FILE, parseEvictionTombstones } from "../run/receipt-resolver.js";
|
|
4
3
|
import { existsSync, readFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, rmdirSync } from "node:fs";
|
|
5
4
|
import { availableParallelism, loadavg, tmpdir } from "node:os";
|
|
6
5
|
import { basename, join } from "node:path";
|
|
7
6
|
import { DEFAULT_EVIDENCE_QUOTA_BYTES } from "../config/config.js";
|
|
8
|
-
import { dependencyLinkRefusal, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
7
|
+
import { dependencyLinkRefusal, gitSync, DEFAULT_SHELL_TIMEOUT_MS, describeCapacity, sameCapacity, sh } from "../run/git.js";
|
|
9
8
|
import { executionSignal } from "../run/execution-budget.js";
|
|
10
9
|
import { isVitestTestCommand, manifestChildEnv, manifestFileCount } from "./test-manifest.js";
|
|
11
10
|
export const EVIDENCE_TAIL_BYTES = 16 * 1024;
|
|
@@ -121,7 +120,9 @@ export function beginGateEvidence(cwd, gate, command, opts = {}, nonce) {
|
|
|
121
120
|
let subjectCommit = opts.subjectCommit ?? null;
|
|
122
121
|
if (!subjectCommit) {
|
|
123
122
|
try {
|
|
124
|
-
|
|
123
|
+
const result = gitSync(cwd, ["rev-parse", "HEAD"]);
|
|
124
|
+
if (result.status === 0 && result.stdout.trim())
|
|
125
|
+
subjectCommit = result.stdout.trim();
|
|
125
126
|
}
|
|
126
127
|
catch { /* unknown subject */ }
|
|
127
128
|
}
|
|
@@ -427,11 +428,30 @@ export function classifyFailureOutput(output) {
|
|
|
427
428
|
// and `stderr | <task id>` (digits and underscores) when the reporter no longer knows the task
|
|
428
429
|
// (dist/chunks/index.*.js, `headerText`). The stripper knew only the first form.
|
|
429
430
|
const VITEST_ECHO_BLOCK_RE = /^\s*std(?:out|err)\s+\|\s+(?:unknown test\s*$|\d+_[\d_]+\s*$|\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s*$|\s+>\s+\S))/;
|
|
430
|
-
|
|
431
|
+
// Queue row 211 (D-2118): only Jest's complete, column-zero recovery warning is noise. A missing
|
|
432
|
+
// neighbour, changed hint, indented copy or multi-line Watchman error remains failure evidence.
|
|
433
|
+
const JEST_WATCHMAN_HEADER = "jest-haste-map: Watchman crawl failed. Retrying once with node crawler.";
|
|
434
|
+
const JEST_WATCHMAN_HINT = " Usually this happens when watchman isn't running. Create an empty `.watchmanconfig` file in your project's root folder or initialize a git or hg repository in your project.";
|
|
435
|
+
const JEST_WATCHMAN_ERROR_RE = /^ {2}Error: Watchman error: [^\r\n]+\. Make sure watchman is running for this project\. See https:\/\/facebook\.github\.io\/watchman\/docs\/troubleshooting\.$/;
|
|
436
|
+
/** Queue row 211 (D-2118): display readers retain raw Vitest echo lines while dropping this warning. */
|
|
437
|
+
const withoutJestWatchmanBlocks = (output) => {
|
|
438
|
+
const kept = [];
|
|
439
|
+
const lines = output.split("\n");
|
|
440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
441
|
+
const line = lines[i];
|
|
442
|
+
if (line === JEST_WATCHMAN_HEADER && lines[i + 1] === JEST_WATCHMAN_HINT && JEST_WATCHMAN_ERROR_RE.test(lines[i + 2] ?? "")) {
|
|
443
|
+
i += 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
kept.push(line);
|
|
447
|
+
}
|
|
448
|
+
return kept;
|
|
449
|
+
};
|
|
450
|
+
/** Queue row 211 (D-2118): omit the recovered Jest warning alongside Vitest's test-owned echo blocks. */
|
|
431
451
|
const withoutVitestEchoBlocks = (output) => {
|
|
432
452
|
const outside = [];
|
|
433
453
|
let inVitestEchoBlock = false;
|
|
434
|
-
for (const line of output
|
|
454
|
+
for (const line of withoutJestWatchmanBlocks(output)) {
|
|
435
455
|
const clean = line.replace(ANSI_RE, "");
|
|
436
456
|
const runnerLine = stripTurboPrefix(clean) ?? clean;
|
|
437
457
|
if (inVitestEchoBlock) {
|
|
@@ -624,8 +644,7 @@ function fingerprintOccurrences(output) {
|
|
|
624
644
|
}
|
|
625
645
|
// The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
|
|
626
646
|
// verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
|
|
627
|
-
const unrecognizedEvidence = (raw) => raw
|
|
628
|
-
.split("\n")
|
|
647
|
+
const unrecognizedEvidence = (raw) => withoutJestWatchmanBlocks(raw)
|
|
629
648
|
.map((l) => l.replace(ANSI_RE, "").trimEnd())
|
|
630
649
|
.filter((l) => VOCAB_RE.test(l))
|
|
631
650
|
.slice(0, 10)
|
|
@@ -1027,11 +1046,33 @@ export async function detectVacuousOracles(cwd, tasks) {
|
|
|
1027
1046
|
// drop a FAIL line that fingerprint-collides with baseline noise). No headline anchors → byte-identical
|
|
1028
1047
|
// fallback to today's text (non-vitest runners lose nothing). RED-pinned by tests/gates/baseline.test.ts
|
|
1029
1048
|
// "HYG-08: details headlines the failing test, not the noise".
|
|
1049
|
+
// Queue row 224(a) (D-2118): Jest identities and their first message are display-only; Console and
|
|
1050
|
+
// column-zero validation bullets are not identities. Keep the existing metadata extraction separate.
|
|
1051
|
+
const JEST_IDENTITY_RE = /^ {2}● (?!Console\s*$)\S.*$/;
|
|
1030
1052
|
function headlineDetails(raw, fresh, freshLines) {
|
|
1031
|
-
const
|
|
1053
|
+
const namedHeadlines = raw
|
|
1032
1054
|
.split("\n")
|
|
1033
1055
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
1034
1056
|
.filter((l) => namesFailureEitherForm(l) || SUMMARY_FAIL_RE.test(l));
|
|
1057
|
+
const outputLines = withoutJestWatchmanBlocks(raw).map((l) => l.replace(ANSI_RE, ""));
|
|
1058
|
+
let inVitestEchoBlock = false;
|
|
1059
|
+
const headlines = outputLines.flatMap((line, index) => {
|
|
1060
|
+
const runnerLine = stripTurboPrefix(line) ?? line;
|
|
1061
|
+
const echoed = inVitestEchoBlock || VITEST_ECHO_BLOCK_RE.test(runnerLine);
|
|
1062
|
+
if (inVitestEchoBlock) {
|
|
1063
|
+
if (runnerLine.trim() === "")
|
|
1064
|
+
inVitestEchoBlock = false;
|
|
1065
|
+
}
|
|
1066
|
+
else if (echoed)
|
|
1067
|
+
inVitestEchoBlock = true;
|
|
1068
|
+
// Queue row 224(a) (D-2118): preserve raw FAIL/summary headlines; only Jest identities exclude echoes.
|
|
1069
|
+
if (namesFailureEitherForm(line) || SUMMARY_FAIL_RE.test(line))
|
|
1070
|
+
return [line];
|
|
1071
|
+
if (echoed || !JEST_IDENTITY_RE.test(line))
|
|
1072
|
+
return [];
|
|
1073
|
+
const message = withoutVitestEchoBlocks(outputLines.slice(index + 1).join("\n")).find((l) => l.trim() !== "");
|
|
1074
|
+
return message === undefined ? [line] : [line, message];
|
|
1075
|
+
});
|
|
1035
1076
|
// S22: the ORIGINAL runner lines behind v2 comparison keys are the diagnostic; the keys are secondary.
|
|
1036
1077
|
// A legacy entry's lines ARE its keys, so its details stay byte-identical.
|
|
1037
1078
|
const lines = [...new Set(freshLines)].join("\n");
|
|
@@ -1041,7 +1082,7 @@ function headlineDetails(raw, fresh, freshLines) {
|
|
|
1041
1082
|
return { details: same ? `new failures vs baseline:\n${lines}` : `new failures vs baseline:\n${lines}\n\n${keys}` };
|
|
1042
1083
|
return {
|
|
1043
1084
|
details: `failing tests:\n${headlines.join("\n")}\n\n${same ? "" : `new failure lines vs baseline:\n${lines}\n\n`}${keys}`,
|
|
1044
|
-
meta: { failingTests:
|
|
1085
|
+
...(namedHeadlines.length ? { meta: { failingTests: namedHeadlines.filter(namesFailureEitherForm) } } : {}),
|
|
1045
1086
|
};
|
|
1046
1087
|
}
|
|
1047
1088
|
export async function compareToBaseline(cwd, commands, baseline, enabled, opts = {}) {
|
|
@@ -1224,7 +1265,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
1224
1265
|
if (classification === "infra") {
|
|
1225
1266
|
const evidence = failingLines.length
|
|
1226
1267
|
? failingLines.slice(0, 10).join("\n")
|
|
1227
|
-
: unrecognizedEvidence(raw) || raw.trim().split("\n").slice(0, 10).join("\n");
|
|
1268
|
+
: unrecognizedEvidence(raw) || withoutJestWatchmanBlocks(raw).join("\n").trim().split("\n").slice(0, 10).join("\n");
|
|
1228
1269
|
record({
|
|
1229
1270
|
gate: name,
|
|
1230
1271
|
pass: false,
|
package/dist/gates/cache.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { dirname, join, relative, resolve } from "node:path";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import { checkoutIncarnation, describeCapacity, inventoryDependencyLinks, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
6
|
import { shq } from "../adapters/types.js";
|
|
7
|
+
import { trustedCommonDir } from "../run/git-trust.js";
|
|
8
8
|
import { titleEnvironment } from "../run/title-environment.js";
|
|
9
9
|
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
10
10
|
let testCacheBound;
|
|
@@ -269,9 +269,7 @@ export function resolveStateDir(worktree, artifactDir) {
|
|
|
269
269
|
return join(worktree, ".tickmarkr");
|
|
270
270
|
}
|
|
271
271
|
try {
|
|
272
|
-
const
|
|
273
|
-
const resolved = resolve(worktree, commonDir);
|
|
274
|
-
const commonRoot = realpathSync(dirname(resolved));
|
|
272
|
+
const commonRoot = realpathSync(dirname(trustedCommonDir(worktree)));
|
|
275
273
|
if (existsSync(join(commonRoot, ".tickmarkr"))) {
|
|
276
274
|
return join(commonRoot, ".tickmarkr");
|
|
277
275
|
}
|
package/dist/graph/schema.js
CHANGED
|
@@ -118,7 +118,9 @@ export const RunGraphSchema = z
|
|
|
118
118
|
source: z.enum(SPEC_SOURCES),
|
|
119
119
|
paths: z.array(z.string()),
|
|
120
120
|
hash: z.string(),
|
|
121
|
-
// Q11
|
|
121
|
+
// Q11 / queue row 220: an author-declared ref, proven at run and resume start against the run's
|
|
122
|
+
// starting commit (HEAD for a run, the recorded run-start base for a resume) by ancestry or patch-id;
|
|
123
|
+
// drifted or unresolvable, the run or resume is refused before it writes anything. Never rebased onto.
|
|
122
124
|
base: z.string().min(1).optional(),
|
|
123
125
|
}),
|
|
124
126
|
tasks: z.array(TaskSchema).min(1),
|