tickmarkr 1.62.0 → 1.63.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/status.js +15 -2
- package/dist/compile/native.js +4 -0
- package/dist/gates/baseline.d.ts +7 -0
- package/dist/gates/baseline.js +34 -1
- package/dist/run/daemon.js +27 -2
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -1
- package/skills/tickmarkr-loop/SKILL.md +2 -1
- package/skills/tickmarkr-overseer/SKILL.md +1 -1
|
@@ -103,14 +103,27 @@ const daemonPid = (events) => {
|
|
|
103
103
|
}
|
|
104
104
|
return undefined;
|
|
105
105
|
};
|
|
106
|
+
const terminalFailureCause = (events) => {
|
|
107
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
108
|
+
const e = events[i];
|
|
109
|
+
if (e.event !== "run-end" || typeof e.data.error !== "string")
|
|
110
|
+
continue;
|
|
111
|
+
const phase = typeof e.data.phase === "string" && e.data.phase.trim()
|
|
112
|
+
? `${e.data.phase.trim()} failed`
|
|
113
|
+
: "run failed";
|
|
114
|
+
return `${phase}: ${e.data.error.replace(/\s+/g, " ").trim()}`;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
};
|
|
106
118
|
const liveness = (events) => {
|
|
107
119
|
const last = events.at(-1);
|
|
108
120
|
if (!last)
|
|
109
121
|
return "last event unknown · daemon pid unknown";
|
|
110
122
|
const age = fmtAge(Date.now() - Date.parse(last.ts));
|
|
111
123
|
const pid = daemonPid(events);
|
|
124
|
+
const cause = terminalFailureCause(events);
|
|
112
125
|
if (pid === undefined)
|
|
113
|
-
return `last event ${age} ago · daemon pid unknown`;
|
|
126
|
+
return `last event ${age} ago · daemon pid unknown${cause ? ` · ${cause}` : ""}`;
|
|
114
127
|
// a dead pid after run-end is a clean exit, not a crash — "dead" is only alarming (red) while
|
|
115
128
|
// the run is still incomplete (operator's crash indicator)
|
|
116
129
|
const ended = events.some((e) => e.event === "run-end");
|
|
@@ -122,7 +135,7 @@ const liveness = (events) => {
|
|
|
122
135
|
catch (k) {
|
|
123
136
|
state = k.code === "ESRCH" ? (ended ? "finished" : "dead") : "alive";
|
|
124
137
|
} // EPERM ⇒ alive
|
|
125
|
-
return `last event ${age} ago · daemon pid ${pid} ${state}`;
|
|
138
|
+
return `last event ${age} ago · daemon pid ${pid} ${state}${cause ? ` · ${cause}` : ""}`;
|
|
126
139
|
};
|
|
127
140
|
const renderFrame = (cwd) => {
|
|
128
141
|
const g = loadGraph(cwd);
|
package/dist/compile/native.js
CHANGED
|
@@ -21,6 +21,10 @@ export function compileNative(file) {
|
|
|
21
21
|
if (!existsSync(file))
|
|
22
22
|
throw new CompileError(`no such native spec file: ${file}`);
|
|
23
23
|
const content = readFileSync(file, "utf8");
|
|
24
|
+
// Exact shared-template check: init writes specTemplate(), and any edit changes the body.
|
|
25
|
+
if (content === specTemplate()) {
|
|
26
|
+
throw new CompileError(`${file} is the unedited tickmarkr init scaffold. Edit ${file} before compiling.`);
|
|
27
|
+
}
|
|
24
28
|
const drafts = [];
|
|
25
29
|
let plainCount = 0; // v1.19: plain-string acceptance items compiled as judge oracles (compat) — warn once
|
|
26
30
|
let specMode;
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -4,7 +4,14 @@ export interface Baseline {
|
|
|
4
4
|
commands: Record<string, {
|
|
5
5
|
exitCode: number;
|
|
6
6
|
fingerprints: string[];
|
|
7
|
+
missingCommand?: boolean;
|
|
7
8
|
}>;
|
|
9
|
+
warnings?: BaselineWarning[];
|
|
10
|
+
}
|
|
11
|
+
export interface BaselineWarning {
|
|
12
|
+
kind: "wrong-environment";
|
|
13
|
+
commands: string[];
|
|
14
|
+
reason: string;
|
|
8
15
|
}
|
|
9
16
|
export declare function fingerprint(output: string): string[];
|
|
10
17
|
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
package/dist/gates/baseline.js
CHANGED
|
@@ -41,12 +41,45 @@ export function detectGateCommands(repoRoot, cfg) {
|
|
|
41
41
|
}
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
|
+
const shellToken = (cmd) => {
|
|
45
|
+
for (const raw of cmd.trim().split(/\s+/)) {
|
|
46
|
+
if (!raw || /^[A-Za-z_][A-Za-z0-9_]*=/.test(raw))
|
|
47
|
+
continue;
|
|
48
|
+
if (raw === "env")
|
|
49
|
+
continue;
|
|
50
|
+
return raw.replace(/^['"]|['"]$/g, "");
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
const reEscape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
function missingConfiguredCommand(cmd, result) {
|
|
56
|
+
if (result.code !== 127)
|
|
57
|
+
return false;
|
|
58
|
+
const token = shellToken(cmd);
|
|
59
|
+
if (!token)
|
|
60
|
+
return false;
|
|
61
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
62
|
+
return new RegExp(`(?:^|[:\\s])${reEscape(token)}:\\s+(?:command not found|No such file or directory)`, "i").test(output);
|
|
63
|
+
}
|
|
44
64
|
export async function captureBaseline(cwd, commands) {
|
|
45
65
|
const base = { commands: {} };
|
|
46
66
|
for (const [name, cmd] of Object.entries(commands)) {
|
|
47
67
|
const r = await sh(cmd, cwd);
|
|
48
68
|
// ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
|
|
49
|
-
base.commands[name] = {
|
|
69
|
+
base.commands[name] = {
|
|
70
|
+
exitCode: r.code,
|
|
71
|
+
fingerprints: fingerprint((r.stdout + "\n" + r.stderr).split(cwd).join("")),
|
|
72
|
+
missingCommand: missingConfiguredCommand(cmd, r),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const names = Object.keys(commands);
|
|
76
|
+
const missing = names.filter((name) => base.commands[name]?.missingCommand === true);
|
|
77
|
+
if (names.length > 0 && missing.length === names.length) {
|
|
78
|
+
base.warnings = [{
|
|
79
|
+
kind: "wrong-environment",
|
|
80
|
+
commands: missing,
|
|
81
|
+
reason: `wrong environment: every configured baseline command was missing (${missing.join(", ")})`,
|
|
82
|
+
}];
|
|
50
83
|
}
|
|
51
84
|
return base;
|
|
52
85
|
}
|
package/dist/run/daemon.js
CHANGED
|
@@ -109,6 +109,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
109
109
|
// v1.54 T2: declared before the try so the finally can always deregister (a throw before
|
|
110
110
|
// registration leaves it undefined — the guard below covers that path).
|
|
111
111
|
let onTermination;
|
|
112
|
+
let journal;
|
|
113
|
+
let runStarted = false;
|
|
114
|
+
let taskLoopStarted = false;
|
|
115
|
+
let branch = "";
|
|
112
116
|
try {
|
|
113
117
|
let graph = loadGraph(repoRoot);
|
|
114
118
|
// v1.51 T2: the routing mode resolves BEFORE any routing input is built — run flag > spec front-matter
|
|
@@ -184,12 +188,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
184
188
|
};
|
|
185
189
|
process.on("SIGINT", onTermination);
|
|
186
190
|
process.on("SIGTERM", onTermination);
|
|
187
|
-
|
|
191
|
+
journal = opts.resume ? Journal.open(repoRoot, runId, opts.narrate) : Journal.create(repoRoot, runId, opts.narrate);
|
|
188
192
|
const branchEvent = opts.resume
|
|
189
193
|
? [...journal.read()].reverse().find((e) => (e.event === "run-start" || e.event === "run-end" || e.event === "merge") && typeof e.data.branch === "string")
|
|
190
194
|
: undefined;
|
|
191
195
|
const recordedBranch = typeof branchEvent?.data.branch === "string" ? branchEvent.data.branch : undefined;
|
|
192
|
-
|
|
196
|
+
branch = recordedBranch
|
|
193
197
|
? branchEvent.event === "merge" ? recordedBranch.slice(0, recordedBranch.lastIndexOf("--")) : recordedBranch
|
|
194
198
|
: integrationBranch(cfg, runId);
|
|
195
199
|
if (lock.reclaimed)
|
|
@@ -249,9 +253,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
249
253
|
baseline = await captureBaseline(repoRoot, commands);
|
|
250
254
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(baseline, null, 2));
|
|
251
255
|
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch, graphDefinitionHash: graphDefinitionHash(graph), mode: rm.mode.mode, modeSource: rm.source, ...(prior ? { supersedes: prior.runId } : {}) }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2; supersedes: v1.53 T5
|
|
256
|
+
runStarted = true;
|
|
252
257
|
// v1.53 T5: mark the prior run AFTER this run's run-start exists, so the prior journal never
|
|
253
258
|
// names a successor that has no journal. Append-only — the prior journal is never rewritten.
|
|
254
259
|
prior?.append("superseded", undefined, { by: runId });
|
|
260
|
+
for (const warning of baseline.warnings ?? [])
|
|
261
|
+
journal.append("baseline-warning", undefined, { ...warning });
|
|
255
262
|
}
|
|
256
263
|
// T6: open the narrator AFTER run-start/run-resume is journaled so the watch surface has a run to
|
|
257
264
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
|
@@ -979,6 +986,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
979
986
|
return;
|
|
980
987
|
}
|
|
981
988
|
};
|
|
989
|
+
taskLoopStarted = true;
|
|
982
990
|
const inflight = new Map();
|
|
983
991
|
while (true) {
|
|
984
992
|
// v1.54 T2: a signal that landed while nothing was racing `aborted` (empty inflight window)
|
|
@@ -1069,6 +1077,23 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1069
1077
|
await driver.notify(`tickmarkr ${runId}: ${summary.done.length} done, ${summary.failed.length} failed, ${summary.human.length} awaiting human, ${summary.blocked.length} blocked, ${summary.pending.length} pending${attribution ? ` (${attribution})` : ""}${tipFail} — integration branch ${branch} (merge to main is yours)`, { tier: summary.tipVerify === "failed" ? "attention" : "routine" });
|
|
1070
1078
|
return summary;
|
|
1071
1079
|
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
if (runStarted && !taskLoopStarted && !journal.read().some((e) => e.event === "run-end")) {
|
|
1082
|
+
journal.append("run-end", undefined, {
|
|
1083
|
+
runId,
|
|
1084
|
+
branch,
|
|
1085
|
+
done: [],
|
|
1086
|
+
failed: [],
|
|
1087
|
+
human: [],
|
|
1088
|
+
blocked: [],
|
|
1089
|
+
pending: [],
|
|
1090
|
+
phase: "setup",
|
|
1091
|
+
fatal: true,
|
|
1092
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
throw err;
|
|
1096
|
+
}
|
|
1072
1097
|
finally {
|
|
1073
1098
|
// v1.54 T2: deregister on EVERY exit (normal run end, throw, termination unwind) — the daemon
|
|
1074
1099
|
// test suite runs runDaemon dozens of times in one process; a leaked handler would close a
|
package/package.json
CHANGED
|
@@ -16,7 +16,8 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
16
16
|
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
17
17
|
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has stood down (monitors stopped, input box empty — dim ghost-text suggestions are UI, not queued input; ANSI-verify before alarming) and close its tab — the journal, records, and ledger hold the story; scrollback is disposable.
|
|
18
18
|
- **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
|
|
19
|
-
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
19
|
+
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
20
|
+
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
20
21
|
|
|
21
22
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
22
23
|
|
|
@@ -15,7 +15,8 @@ When working in a multi-agent terminal environment, decide your role before star
|
|
|
15
15
|
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a [verified handoff](#verified-handoffs-agent-to-agent-messaging), then supervise it as OVERSEER.
|
|
16
16
|
- **Primary session without an orchestrator:** rename your own tab `OVERSEER · <version>` and your agent `overseer`, spawn one child orchestration session with your host's launch form, label its tab `ORCH · <version>` and name its agent, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself. Before spawning, confirm any PREVIOUS orchestrator has [stood down](#stand-down-mission-end-and-retirement) and close its tab.
|
|
17
17
|
- **Claude Code:** `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions`
|
|
18
|
-
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
18
|
+
- **Codex:** `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` — the unsandboxed flag is REQUIRED, not optional: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation (`git worktree add` cannot lock the ref). Do not downgrade this flag; the herdr pane and repo scope are the containment.
|
|
19
|
+
- **Auxiliary agents you spawn (consultants, reviewers, scouts) follow the same forms.** Never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions` (tickmarkr's own adapter uses exactly this for workers, judges, and consults). A read-only codex consultant may use `--sandbox read-only`; any codex session that must touch git needs the unsandboxed flag above.
|
|
19
20
|
|
|
20
21
|
Outside a multi-agent terminal environment, run the loop directly.
|
|
21
22
|
|
|
@@ -29,7 +29,7 @@ Requires `HERDR_ENV=1`; if unset, say so and stop.
|
|
|
29
29
|
main name plus at most ONE hot-state token. Vocabulary: ORCH carries the milestone and progress
|
|
30
30
|
fraction (`ORCH · v1.19 4/5`, updated on every task-done); WORKERS carries the task token (tickmarkr
|
|
31
31
|
updates it). Never long context strings or ✓-chains.
|
|
32
|
-
2. **Orchestrator**: Launch the orchestrator with your agent host. For Claude Code, use `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions` (pin a strong model with `--model <m>` if the operator has a policy). For Codex, use `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --
|
|
32
|
+
2. **Orchestrator**: Launch the orchestrator with your agent host. For Claude Code, use `herdr agent start orchestrator --cwd <repo> --no-focus -- claude --permission-mode bypassPermissions` (pin a strong model with `--model <m>` if the operator has a policy). For Codex, use `herdr agent start orchestrator --cwd <repo> --no-focus -- codex --dangerously-bypass-approvals-and-sandbox` (add `--model <m>` to specify the model). The unsandboxed flag is REQUIRED: codex's `workspace-write` sandbox keeps `.git` refs read-only, so a sandboxed orchestrator's `tickmarkr run` dies at integration-branch creation — do not downgrade it. Workers you never spawn — tickmarkr spawns its own visible worker panes. Auxiliary agents you do spawn (consultants, reviewers, scouts) follow the same forms: never launch a claude session in plan mode or default permission mode for autonomous work — both stall on per-command approval prompts nobody is watching; claude is always `--permission-mode bypassPermissions`, and a read-only codex consultant may use `--sandbox read-only`.
|
|
33
33
|
3. **Standing instructions travel as a brief FILE, never as pane text** — PTY input truncates at ~1024B and a
|
|
34
34
|
truncated brief silently drops policy. Write the full brief to `<repo>/.tickmarkr/overseer/ORCH-BRIEF.md`
|
|
35
35
|
(inside the tickmarkr state dir — already self-gitignored, no exclude step needed), then send one line:
|