tickmarkr 1.37.0 → 1.40.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/README.md +25 -23
- package/dist/adapters/claude-code.js +4 -4
- package/dist/adapters/codex.js +1 -1
- package/dist/adapters/fake.d.ts +2 -2
- package/dist/adapters/fake.js +29 -11
- package/dist/adapters/grok.js +5 -5
- package/dist/adapters/model-lints.d.ts +4 -4
- package/dist/adapters/model-lints.js +1 -1
- package/dist/adapters/prompt.js +8 -13
- package/dist/adapters/registry.d.ts +8 -8
- package/dist/adapters/registry.js +17 -17
- package/dist/adapters/types.d.ts +4 -4
- package/dist/adapters/types.js +3 -2
- package/dist/brand.d.ts +2 -0
- package/dist/brand.js +17 -0
- package/dist/cli/commands/approve.js +3 -3
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/init.js +57 -19
- package/dist/cli/commands/plan.js +12 -1
- package/dist/cli/commands/profile.js +6 -6
- package/dist/cli/commands/resume.d.ts +4 -1
- package/dist/cli/commands/resume.js +4 -1
- package/dist/cli/commands/run.d.ts +4 -1
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/commands/status.js +2 -1
- package/dist/cli/commands/unlock.js +1 -1
- package/dist/cli/commands/version.d.ts +1 -0
- package/dist/cli/commands/version.js +8 -0
- package/dist/cli/index.d.ts +6 -3
- package/dist/cli/index.js +13 -18
- package/dist/compile/gsd.js +1 -1
- package/dist/compile/index.js +9 -3
- package/dist/compile/native.d.ts +2 -0
- package/dist/compile/native.js +12 -4
- package/dist/config/config.d.ts +9 -6
- package/dist/config/config.js +15 -13
- package/dist/drivers/herdr.js +5 -5
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/subprocess.js +12 -5
- package/dist/drivers/types.js +4 -6
- package/dist/gates/acceptance.d.ts +1 -0
- package/dist/gates/acceptance.js +25 -5
- package/dist/gates/baseline.d.ts +2 -2
- package/dist/gates/llm.js +12 -9
- package/dist/gates/review.d.ts +2 -2
- package/dist/gates/review.js +27 -8
- package/dist/gates/run-gates.d.ts +2 -2
- package/dist/gates/run-gates.js +2 -2
- package/dist/graph/graph.d.ts +2 -2
- package/dist/graph/graph.js +4 -16
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +16 -1
- package/dist/plan/prompt.js +1 -1
- package/dist/plan/scope.d.ts +2 -2
- package/dist/plan/scope.js +5 -5
- package/dist/route/preference.d.ts +4 -4
- package/dist/route/router.d.ts +3 -3
- package/dist/run/consult.d.ts +5 -2
- package/dist/run/consult.js +31 -7
- package/dist/run/daemon.js +31 -18
- package/dist/run/git.d.ts +1 -1
- package/dist/run/git.js +5 -9
- package/dist/run/journal.d.ts +2 -2
- package/dist/run/journal.js +4 -4
- package/dist/run/lock.js +11 -11
- package/dist/run/merge.d.ts +2 -2
- package/dist/run/merge.js +3 -3
- package/dist/run/reconcile.js +1 -1
- package/package.json +2 -2
- package/schema/rungraph.schema.json +4 -0
- package/skills/tickmarkr-auto/SKILL.md +23 -2
- package/skills/tickmarkr-loop/SKILL.md +23 -2
package/README.md
CHANGED
|
@@ -6,25 +6,25 @@
|
|
|
6
6
|
|
|
7
7
|
tickmarkr is a spec-driven orchestration harness for AI coding agent CLIs. You write a spec with
|
|
8
8
|
acceptance criteria; the engine routes tasks to the best installed agent CLI (claude-code, codex,
|
|
9
|
-
cursor-agent, opencode, grok, pi) by cost and capability, dispatches work in
|
|
10
|
-
— as interactive TUIs when running under [herdr](https://herdr.dev), headless
|
|
11
|
-
— and independently verifies
|
|
9
|
+
cursor-agent, opencode, grok, pi) by cost and capability, dispatches work in git worktrees for
|
|
10
|
+
change isolation — as interactive TUIs when running under [herdr](https://herdr.dev), headless
|
|
11
|
+
subprocesses otherwise — and independently verifies each committed result by checking for no new
|
|
12
|
+
baseline failures per task, then strictly verifying the integration tip. Green tasks consolidate onto a
|
|
12
13
|
`tickmarkr/<runId>` branch; merging to your mainline is always your call, never automated. Engage
|
|
13
14
|
with full visibility into routing decisions, worker progress, and gate verdicts — or run headless
|
|
14
|
-
and review the
|
|
15
|
+
and review the local execution log afterward.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<!-- tickmarkr:compat-legacy END -->
|
|
17
|
+
Here, **isolated git worktrees** means change isolation: each task gets its own worktree and branch
|
|
18
|
+
so its diff is separated from sibling tasks. It is not a process sandbox or host-containment boundary.
|
|
19
19
|
|
|
20
20
|
## Invariants
|
|
21
21
|
|
|
22
22
|
These are law; the codebase fails closed around them:
|
|
23
23
|
|
|
24
24
|
- `acceptance[]` required on every task; compile fails without it
|
|
25
|
-
- new engagements consolidate to a `tickmarkr/<runId>` branch — never main
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- new engagements consolidate to a `tickmarkr/<runId>` branch — never main
|
|
26
|
+
- gates never trust worker claims; each task is checked for no new baseline failures and the merged
|
|
27
|
+
integration tip is strictly re-verified
|
|
28
28
|
- state is files + git only; no DB, no services
|
|
29
29
|
- worker/judge/review/consult prompts end with machine-parseable trailers
|
|
30
30
|
(structured JSON verdicts); parse defensively, fail closed
|
|
@@ -73,7 +73,8 @@ tickmarkr report <runId> --md # engagement record in Markdown
|
|
|
73
73
|
|
|
74
74
|
That's the flow: `init` scaffolds config, you write tasks with `acceptance[]` criteria, `compile`
|
|
75
75
|
validates and builds the graph, `plan` shows routing decisions and cost, `run` dispatches work to
|
|
76
|
-
installed CLIs and
|
|
76
|
+
installed CLIs and checks each task for no new baseline failures before strictly verifying the
|
|
77
|
+
integration tip, and `report` documents the outcome. All
|
|
77
78
|
green tasks land on `tickmarkr/<runId>` — merge to your mainline is your decision.
|
|
78
79
|
|
|
79
80
|
## Agent-ready repos: `tickmarkr init --agent`
|
|
@@ -103,7 +104,7 @@ Consent rules — every write is additive, never destructive:
|
|
|
103
104
|
|
|
104
105
|
```bash
|
|
105
106
|
tickmarkr status # engagement state (--watch to follow live)
|
|
106
|
-
tickmarkr resume <runId> # continue an engagement from the
|
|
107
|
+
tickmarkr resume <runId> # continue an engagement from the local execution log
|
|
107
108
|
tickmarkr approve <runId> <taskId> # approve a parked task (--reason to document)
|
|
108
109
|
tickmarkr report <runId> # cost/quality report
|
|
109
110
|
tickmarkr profile # show the learned routing profile
|
|
@@ -165,11 +166,11 @@ routing:
|
|
|
165
166
|
```
|
|
166
167
|
`tickmarkr plan` lints denied models identically to unauthed ones. Re-enable by deleting one line.
|
|
167
168
|
|
|
168
|
-
## Run output and
|
|
169
|
+
## Run output and local execution log
|
|
169
170
|
|
|
170
|
-
When you execute `tickmarkr run`, the daemon records every event in an append-only
|
|
171
|
+
When you execute `tickmarkr run`, the daemon records every event in an append-only local journal:
|
|
171
172
|
task dispatch, gate verdicts, worker status, and merges. Narration streams to stdout; you can also
|
|
172
|
-
`--watch` or tail the
|
|
173
|
+
`--watch` or tail the journal directly. If interrupted, `tickmarkr resume <runId>` replays the journal
|
|
173
174
|
and continues from the last stable state — deterministic, not ephemeral.
|
|
174
175
|
|
|
175
176
|
## Usage and cost
|
|
@@ -198,7 +199,7 @@ and first-attempt success rate. Cost reporting follows strict honesty rules and
|
|
|
198
199
|
- **Basis always shown**: every cost figure prints the token count, rate used, and date so estimates can be audited
|
|
199
200
|
- **No network calls**: pricing config is operator-maintained locally, seeded with dated
|
|
200
201
|
comments and LiteLLM's JSON file named as the copy-from source; tickmarkr never calls home to fetch rates
|
|
201
|
-
- **Attribution from journal**: token counts come from tickmarkr's own telemetry spans in the
|
|
202
|
+
- **Attribution from journal**: token counts come from tickmarkr's own telemetry spans in the local journal, never from provider invoices or dashboards
|
|
202
203
|
|
|
203
204
|
## Visibility: optional supervised workspace
|
|
204
205
|
|
|
@@ -253,7 +254,7 @@ Every pane and tab tickmarkr creates receives a **parseable ownership name** enc
|
|
|
253
254
|
|
|
254
255
|
tickmarkr creates all owned panes only within the run's workspace; any tickmarkr-owned panes discovered outside the run's workspace (from prior runs or placement bugs) are reconciled and closed. Any pane not matching the ownership contract is **foreign** — created by you or another tool — and is never closed automatically.
|
|
255
256
|
|
|
256
|
-
**Desired-state reconciliation**: A pure function computes the exact set of panes that should exist from the
|
|
257
|
+
**Desired-state reconciliation**: A pure function computes the exact set of panes that should exist from the local journal at any moment:
|
|
257
258
|
- Worker panes for all in-flight task attempts
|
|
258
259
|
- Gate panes for unread judge/review/consult verdicts
|
|
259
260
|
- The watch pane (if running)
|
|
@@ -269,11 +270,11 @@ Reconciliation failures (herdr unavailable, a pane vanished mid-sweep) never fai
|
|
|
269
270
|
|
|
270
271
|
### Workspace trust
|
|
271
272
|
|
|
272
|
-
tickmarkr
|
|
273
|
-
|
|
273
|
+
tickmarkr creates fresh git worktrees for change isolation. That is separate from each CLI's own
|
|
274
|
+
workspace-trust behavior:
|
|
274
275
|
|
|
275
|
-
- `tickmarkr doctor`
|
|
276
|
-
- Some CLIs show a "Workspace Trust" dialog; tickmarkr
|
|
276
|
+
- `tickmarkr doctor` invokes each installed adapter's trust hook where supported and seeds trust where possible
|
|
277
|
+
- Some CLIs show a "Workspace Trust" dialog; tickmarkr auto-answers only a recognized fingerprint once per slot
|
|
277
278
|
- Other blocked dialogs page you for manual approval
|
|
278
279
|
|
|
279
280
|
## Spec formats
|
|
@@ -298,8 +299,9 @@ Every task requires explicit `acceptance[]` criteria; compile fails without them
|
|
|
298
299
|
- **`test <name>`** — run a named test from your suite (must exist)
|
|
299
300
|
- **`judge <rubric>`** — LLM verdict against your rubric; fail-closed, never overrides failed command/test
|
|
300
301
|
|
|
301
|
-
At runtime, the **scope gate**
|
|
302
|
-
|
|
302
|
+
At runtime, the **scope gate** derives `git diff --name-only` from the task base and compares it with
|
|
303
|
+
the spec-declared files. Out-of-scope edits fail unless the operator config explicitly allowlists
|
|
304
|
+
them; worker-declared deviations are recorded as notes, not authority.
|
|
303
305
|
|
|
304
306
|
## Claude Code integration
|
|
305
307
|
|
|
@@ -6,7 +6,7 @@ import { parseWorkerResult } from "./prompt.js";
|
|
|
6
6
|
import { channelsFromConfig, shq, TokenUsageSchema } from "./types.js";
|
|
7
7
|
// SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
|
|
8
8
|
// realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
|
|
9
|
-
// The old `/`-only formula missed the "." in `.
|
|
9
|
+
// The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
|
|
10
10
|
// Each assistant message carries a per-record ISO `timestamp` and message.usage. We read it POST-HOC
|
|
11
11
|
// (never the pane, never the trailer) and sum
|
|
12
12
|
// tokens for records whose top-level cwd matches this task's worktree AND whose timestamp is at/after
|
|
@@ -43,9 +43,9 @@ export const claudeCode = {
|
|
|
43
43
|
channels: (cfg) => channelsFromConfig("claude-code", cfg),
|
|
44
44
|
// --strict-mcp-config --mcp-config '{"mcpServers":{}}': pin the MCP surface to empty so fresh-worktree
|
|
45
45
|
// workers/gates don't load project .mcp.json servers (herdr scrapes dialogs as idle — v1.4 incident,
|
|
46
|
-
// memory
|
|
46
|
+
// memory tickmarkr-worker-mcp-dialog-stall). Live-verified 2026-07-10 on claude 2.1.205 (operator check):
|
|
47
47
|
// headless (-p) fully suppressed — exit 0, no dialog. Interactive TUI STILL shows the project
|
|
48
|
-
// MCP-enable dialog (project trust/enablement, not config loading); Esc dismisses it, and
|
|
48
|
+
// MCP-enable dialog (project trust/enablement, not config loading); Esc dismisses it, and tickmarkr's
|
|
49
49
|
// blocked/idle paging surfaces the pane to the operator (same path as cursor's trust dialog).
|
|
50
50
|
// Gotchas (both bit the 2026-07-10 live check): bare '{}' is REJECTED ("mcpServers: expected record"),
|
|
51
51
|
// and --mcp-config is VARIADIC — a positional after it is eaten as a config-file path, so another
|
|
@@ -53,7 +53,7 @@ export const claudeCode = {
|
|
|
53
53
|
headlessCommand: (promptFile, model) => `claude -p "$(cat ${shq(promptFile)})" --model ${shq(model)} --permission-mode bypassPermissions --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format text`,
|
|
54
54
|
// HYG-03: the residual first-entry dialog on an interactive TUI is the workspace TRUST dialog (not MCP
|
|
55
55
|
// config loading) — CLI-imposed, no flag to pre-accept, only store is claude's global last-writer-wins
|
|
56
|
-
// ~/.claude.json keyed on the exact path. Closed WON'T-FIX (decision B, 2026-07-10):
|
|
56
|
+
// ~/.claude.json keyed on the exact path. Closed WON'T-FIX (decision B, 2026-07-10): tickmarkr writes nothing
|
|
57
57
|
// to that file (a seed races claude's own writes, nondeterministically). Amortizes to one operator dismissal
|
|
58
58
|
// per stable worktree path; blocked-pane paging surfaces it. Do NOT change this command to "fix" the dialog —
|
|
59
59
|
// see .planning/REQUIREMENTS.md HYG-03 and 21-02-LIVE-CHECK.md. Revisit if upstream ships a --trust flag.
|
package/dist/adapters/codex.js
CHANGED
|
@@ -75,7 +75,7 @@ export function readCodexModelsCache(path) {
|
|
|
75
75
|
return { models: [] };
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
//
|
|
78
|
+
// tickmarkr worktrees keep their gitdir under the MAIN repo's .git/worktrees/<name> — outside the
|
|
79
79
|
// workspace-write sandbox root — so git commit dies on index.lock (incident run-20260709-104447
|
|
80
80
|
// P87-08: "Unable to create …/.git/worktrees/…/index.lock"). Expanded by the pane shell at the
|
|
81
81
|
// worktree cwd; in a plain repo it resolves to ./.git (already writable, harmless).
|
package/dist/adapters/fake.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import type { Task } from "../graph/schema.js";
|
|
3
3
|
import { type Assignment, type AuthHealth, type BillingChannel, type ContextUsage, type Invocation, type SessionRef, type TokenUsage, type WorkerAdapter, type WorkerResult } from "./types.js";
|
|
4
4
|
export interface FakeScript {
|
|
@@ -29,7 +29,7 @@ export declare class FakeAdapter implements WorkerAdapter {
|
|
|
29
29
|
private attempts;
|
|
30
30
|
constructor(scriptPath: string);
|
|
31
31
|
probe(): Promise<AuthHealth>;
|
|
32
|
-
channels(_cfg:
|
|
32
|
+
channels(_cfg: TickmarkrConfig): BillingChannel[];
|
|
33
33
|
private judgeIdx;
|
|
34
34
|
headlessCommand(promptFile: string, _model: string): string;
|
|
35
35
|
resumeCommand(_sessionId: string, promptFile: string, model: string): string;
|
package/dist/adapters/fake.js
CHANGED
|
@@ -14,7 +14,13 @@ export class FakeAdapter {
|
|
|
14
14
|
this.script = JSON.parse(readFileSync(scriptPath, "utf8"));
|
|
15
15
|
}
|
|
16
16
|
async probe() {
|
|
17
|
-
return {
|
|
17
|
+
return {
|
|
18
|
+
installed: true, authed: true, version: "fake", models: ["fake-1", "fake-2"],
|
|
19
|
+
modelAuth: {
|
|
20
|
+
"fake-1": { authed: true, probedAt: "1970-01-01T00:00:00.000Z" },
|
|
21
|
+
"fake-2": { authed: true, probedAt: "1970-01-01T00:00:00.000Z" },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
18
24
|
}
|
|
19
25
|
channels(_cfg) {
|
|
20
26
|
return [
|
|
@@ -22,14 +28,16 @@ export class FakeAdapter {
|
|
|
22
28
|
{ adapter: "fake", vendor: "fake-b", model: "fake-2", channel: "api", tier: "frontier" },
|
|
23
29
|
];
|
|
24
30
|
}
|
|
25
|
-
judgeIdx = 0; // Phase 47: per-instance judge verdict cursor — advances only on
|
|
31
|
+
judgeIdx = 0; // Phase 47: per-instance judge verdict cursor — advances only on TICKMARKR-JUDGE prompts
|
|
26
32
|
headlessCommand(promptFile, _model) {
|
|
27
33
|
// Phase 47 (GATE-09): detect role at COMMAND-BUILD time. runHeadless/runViaDriver write the prompt
|
|
28
34
|
// file BEFORE calling headlessCommand, so it exists here. The counter advances ONLY when the prompt
|
|
29
|
-
// contains
|
|
35
|
+
// contains TICKMARKR-JUDGE — a review/consult prompt never touches it (research Pitfall 3).
|
|
36
|
+
let prompt = "";
|
|
30
37
|
let isJudge = false;
|
|
31
38
|
try {
|
|
32
|
-
|
|
39
|
+
prompt = readFileSync(promptFile, "utf8");
|
|
40
|
+
isJudge = /TICKMARKR-JUDGE/.test(prompt);
|
|
33
41
|
}
|
|
34
42
|
catch {
|
|
35
43
|
// unreadable promptFile: can't detect role; serve static values (legacy headless-call behavior)
|
|
@@ -41,11 +49,19 @@ export class FakeAdapter {
|
|
|
41
49
|
val = val[Math.min(this.judgeIdx, val.length - 1)];
|
|
42
50
|
this.judgeIdx++;
|
|
43
51
|
}
|
|
52
|
+
if (key === "judge" && isJudge && val && typeof val === "object" && !Array.isArray(val)) {
|
|
53
|
+
const verdict = val;
|
|
54
|
+
if (verdict.pass === true && Array.isArray(verdict.criteria) && verdict.criteria.length === 0) {
|
|
55
|
+
const items = prompt.match(/## Acceptance criteria \(judge\)\n([\s\S]*?)\n\n## Diff/)?.[1]
|
|
56
|
+
.split("\n").filter((line) => line.startsWith("- ")).map((line) => line.slice(2)) ?? [];
|
|
57
|
+
val = { ...verdict, criteria: items.map((criterion) => ({ criterion, met: true, reason: "scripted fake pass" })) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
44
60
|
const p = join(dirname(this.scriptPath), `${key}.json`);
|
|
45
61
|
writeFileSync(p, JSON.stringify(val ?? {}, null, 1));
|
|
46
62
|
return p;
|
|
47
63
|
};
|
|
48
|
-
return `bash -c 'grep -q
|
|
64
|
+
return `bash -c 'grep -q TICKMARKR-JUDGE ${shq(promptFile)} && cat ${shq(serve("judge"))}; grep -q TICKMARKR-REVIEW ${shq(promptFile)} && cat ${shq(serve("review"))}; grep -q TICKMARKR-CONSULT ${shq(promptFile)} && cat ${shq(serve("consult"))}; true'`;
|
|
49
65
|
}
|
|
50
66
|
resumeCommand(_sessionId, promptFile, model) {
|
|
51
67
|
return this.interactiveCommand(promptFile, model) ?? this.headlessCommand(promptFile, model);
|
|
@@ -59,21 +75,23 @@ export class FakeAdapter {
|
|
|
59
75
|
// SPEND-01: synthetic usage as a TIMESTAMPED disk record the fake's own reader picks up post-hoc —
|
|
60
76
|
// never a trailer, never pane text. node stamps a real ms-precision ISO time (bash 3.2 on darwin has
|
|
61
77
|
// no ms clock) so the record lands at/after this attempt's dispatch and clears the sinceMs cursor.
|
|
62
|
-
// Written
|
|
78
|
+
// Written BEFORE the trailer echo (same order real CLIs use: session-store rows land during work,
|
|
79
|
+
// completion marker last) so an interactive harvest on the trailer never races the usage writer.
|
|
80
|
+
// Still after the step's `git add`/commit ⇒ stays untracked ⇒ invisible to scope/evidence gates.
|
|
63
81
|
const usageWrite = step.usage
|
|
64
|
-
? `; node -e ${shq(`require("fs").writeFileSync(".
|
|
82
|
+
? `; node -e ${shq(`require("fs").writeFileSync(".tickmarkr-usage.json", JSON.stringify({timestamp:new Date().toISOString(), usage:${JSON.stringify(step.usage)}}))`)}`
|
|
65
83
|
: "";
|
|
66
84
|
const base = !step.result
|
|
67
85
|
? `bash -c ${shq(step.shell)}` // no trailer: scripted stall/quota
|
|
68
|
-
: `bash -c ${shq(step.shell)}; echo ${shq(`
|
|
69
|
-
return base
|
|
86
|
+
: `bash -c ${shq(step.shell)}${usageWrite}; echo ${shq(`TICKMARKR_RESULT_${nonce} ` + JSON.stringify({ deviations: [], ...step.result }))}`;
|
|
87
|
+
return base;
|
|
70
88
|
}
|
|
71
89
|
// SPEND-01: read the cwd-keyed usage record from DISK (never a pane), honoring the attempt cursor.
|
|
72
90
|
// A record stamped before sinceMs, an unparseable stamp, or a bad usage shape all fail OPEN to
|
|
73
91
|
// undefined ⇒ unmetered — never 0, never a thrown error that could fail a healthy task.
|
|
74
92
|
collectUsage(cwd, sinceMs) {
|
|
75
93
|
try {
|
|
76
|
-
const rec = JSON.parse(readFileSync(join(cwd, ".
|
|
94
|
+
const rec = JSON.parse(readFileSync(join(cwd, ".tickmarkr-usage.json"), "utf8"));
|
|
77
95
|
const ts = Date.parse(rec?.timestamp);
|
|
78
96
|
if (!Number.isFinite(ts) || ts < sinceMs)
|
|
79
97
|
return undefined; // cursor honored, fail-open
|
|
@@ -93,7 +111,7 @@ export class FakeAdapter {
|
|
|
93
111
|
// parseWorkerResult(output, nonce) succeeds exactly as before the nonce existed
|
|
94
112
|
nonceFor(promptFile) {
|
|
95
113
|
try {
|
|
96
|
-
return /
|
|
114
|
+
return /TICKMARKR_RESULT_([0-9a-z]+)/.exec(readFileSync(promptFile, "utf8"))?.[1] ?? "";
|
|
97
115
|
}
|
|
98
116
|
catch {
|
|
99
117
|
return "";
|
package/dist/adapters/grok.js
CHANGED
|
@@ -19,11 +19,11 @@ import { channelsFromConfig, MODEL_ID_RE, shq } from "./types.js";
|
|
|
19
19
|
// (4) `grok trace <id> --local` export and ~/.grok/logs/unified.jsonl (1,447 lines) — zero counters.
|
|
20
20
|
// (5) --debug-file traces DO carry real per-turn input_tokens=… output_tokens=… cache_read_tokens=…
|
|
21
21
|
// — a REAL source, REJECTED for stated reasons:
|
|
22
|
-
// (a) it exists ONLY because
|
|
23
|
-
//
|
|
22
|
+
// (a) it exists ONLY because tickmarkr would inject --debug-file into every dispatch — a side-channel
|
|
23
|
+
// tickmarkr creates, not a record the CLI keeps (same class as cursor's rejected --print --trust
|
|
24
24
|
// stdout tee, cursor-agent.ts:25-28);
|
|
25
25
|
// (b) DECISIVE: at DEBUG level the log verbatim-dumps the full OIDC bearer JWT and every MCP server
|
|
26
|
-
// env secret — an actual plaintext SUPABASE_ACCESS_TOKEN was observed —
|
|
26
|
+
// env secret — an actual plaintext SUPABASE_ACCESS_TOKEN was observed — tickmarkr forcing a
|
|
27
27
|
// per-attempt secret-spilling file onto disk to meter a sub channel fails its own security
|
|
28
28
|
// posture;
|
|
29
29
|
// (c) it is tracing diagnostics text with no format contract (xAI can reshape it in any patch).
|
|
@@ -102,11 +102,11 @@ export const grok = {
|
|
|
102
102
|
// 2026-07-11 (40-RESEARCH F-3): trailer intact + unwrapped, exit 0. "$(cat file)" matches every
|
|
103
103
|
// other adapter and is already quoting-proven. --permission-mode bypassPermissions is the
|
|
104
104
|
// autonomous-worker analog of claude's --dangerously-skip-permissions. NEVER pass grok's
|
|
105
|
-
// -w/--worktree flag — grok manages its OWN worktrees; a nested worktree inside
|
|
105
|
+
// -w/--worktree flag — grok manages its OWN worktrees; a nested worktree inside tickmarkr's worktree
|
|
106
106
|
// is scope-gate chaos (src/run/git.ts owns worktrees).
|
|
107
107
|
headlessCommand: (promptFile, model) => `grok -p "$(cat ${shq(promptFile)})" --model ${shq(model)} --permission-mode bypassPermissions --output-format plain`,
|
|
108
108
|
// GROK-02 interactive. Positional prompt seeds the TUI (40-CONTEXT D-01). Same permission flag;
|
|
109
|
-
// NEVER the -w/--worktree flag (see headless comment above —
|
|
109
|
+
// NEVER the -w/--worktree flag (see headless comment above — tickmarkr owns worktrees).
|
|
110
110
|
interactiveCommand: (promptFile, model) => `grok --model ${shq(model)} --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
|
|
111
111
|
invoke(task, _cwd, a, ctx) {
|
|
112
112
|
return { command: this.headlessCommand(ctx.promptFile, a.model) };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import { type AuthHealth, type WorkerAdapter } from "./types.js";
|
|
3
3
|
export declare const SEED_STAMPED = "2026-07-09";
|
|
4
4
|
export declare const MODEL_STALE_DAYS = 30;
|
|
5
5
|
export declare const ttyVisual: () => boolean;
|
|
6
|
-
export declare function seedPreferLints(cfg:
|
|
7
|
-
export declare function modelLints(cfg:
|
|
6
|
+
export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
|
|
7
|
+
export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
|
|
8
8
|
tty?: boolean;
|
|
9
9
|
stateDir?: string;
|
|
10
10
|
overlayPreferShapes?: ReadonlySet<string>;
|
|
@@ -14,4 +14,4 @@ export declare function formatModelAuthLine(excluded: {
|
|
|
14
14
|
reason: string;
|
|
15
15
|
probedAt: string;
|
|
16
16
|
}[], tty?: boolean, stateDir?: string): string;
|
|
17
|
-
export declare function suggestOverlay(cfg:
|
|
17
|
+
export declare function suggestOverlay(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string): string;
|
|
@@ -11,7 +11,7 @@ const DAY_MS = 86400000;
|
|
|
11
11
|
const LINT_VARIANT_RE = /^auto$|-(fast|minimal|low|medium|high|xhigh)$/;
|
|
12
12
|
const LINT_CAP = 5;
|
|
13
13
|
const TTY_LINT_CAP = 3;
|
|
14
|
-
const DEFAULT_STATE_DIR = ".
|
|
14
|
+
const DEFAULT_STATE_DIR = ".tickmarkr";
|
|
15
15
|
const doctorJsonRef = (stateDir) => ` — see ${stateDir}/doctor.json`;
|
|
16
16
|
export const ttyVisual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
17
17
|
const adapterHasAuthedChannel = (adapterId, shape, cfg, health, adapters) => {
|
package/dist/adapters/prompt.js
CHANGED
|
@@ -18,7 +18,7 @@ ${task.files.length ? `\n## File scope — touch ONLY paths matching:\n${list(ta
|
|
|
18
18
|
- Do not ask questions; you are unattended. Make the smallest correct change.
|
|
19
19
|
${feedback ? `\n## Previous attempt failed gates — fix these specifically\n${feedback}\n` : ""}
|
|
20
20
|
When finished, end your final message with exactly one line (no code fence):
|
|
21
|
-
|
|
21
|
+
TICKMARKR_RESULT_${nonce} {"ok":true|false,"summary":"<one sentence>","deviations":["<path or reason>"]}
|
|
22
22
|
`;
|
|
23
23
|
}
|
|
24
24
|
export function writePrompt(dir, task, attempt, feedback = "", nonce = "") {
|
|
@@ -31,26 +31,21 @@ export function writePrompt(dir, task, attempt, feedback = "", nonce = "") {
|
|
|
31
31
|
// across lines by a TUI renderer ([\s\S] bridges newlines between tokens) — but never the template
|
|
32
32
|
// line above, whose literal `true|false` is not a bool followed by , or }. v1.4: the run-tagged nonce
|
|
33
33
|
// anchors it so displayed source/diffs (which can't know the nonce) can never premature-harvest.
|
|
34
|
-
// v1.16 (REN-02): dual-parse — accepts both the new marker spelling and the legacy alternation, so a
|
|
35
|
-
// worker/fixture still emitting the old marker keeps parsing through the rename.
|
|
36
34
|
export function trailerPattern(nonce) {
|
|
37
|
-
return `
|
|
35
|
+
return `TICKMARKR_RESULT_${nonce} \\{[\\s\\S]{0,120}?"ok":\\s*(true|false)\\s*[,}]`;
|
|
38
36
|
}
|
|
39
|
-
// all start positions of either marker spelling, ascending — merged so "last occurrence" and
|
|
40
|
-
// "scan backward for the first parseable one" work identically across both spellings.
|
|
41
37
|
function trailerTokenPositions(raw, nonce) {
|
|
38
|
+
const token = `TICKMARKR_RESULT_${nonce}`;
|
|
42
39
|
const positions = [];
|
|
43
|
-
for (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
return positions.sort((a, b) => a - b);
|
|
40
|
+
for (let i = raw.indexOf(token); i !== -1; i = raw.indexOf(token, i + 1))
|
|
41
|
+
positions.push(i);
|
|
42
|
+
return positions;
|
|
48
43
|
}
|
|
49
44
|
export function parseWorkerResult(raw, nonce) {
|
|
50
45
|
const fail = (summary) => ({ ok: false, summary, deviations: [], raw });
|
|
51
46
|
const positions = trailerTokenPositions(raw, nonce);
|
|
52
47
|
if (positions.length === 0)
|
|
53
|
-
return fail("worker produced no
|
|
48
|
+
return fail("worker produced no TICKMARKR_RESULT trailer");
|
|
54
49
|
// TUIs echo the prompt template, redraw lines, and HARD-wrap the JSON with per-line margins
|
|
55
50
|
// (cursor does; recent-unwrapped can't rejoin hard newlines). Scan occurrences backward — last
|
|
56
51
|
// parseable wins — joining wrapped lines, stripping margin/box chrome, and growing the candidate
|
|
@@ -80,5 +75,5 @@ export function parseWorkerResult(raw, nonce) {
|
|
|
80
75
|
}
|
|
81
76
|
}
|
|
82
77
|
}
|
|
83
|
-
return fail("unparseable
|
|
78
|
+
return fail("unparseable TICKMARKR_RESULT trailer");
|
|
84
79
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
2
|
import { type RoutingProfile } from "../route/profile.js";
|
|
3
3
|
import { type AuthHealth, type BillingChannel, type WorkerAdapter } from "./types.js";
|
|
4
4
|
export declare function allAdapters(opts?: {
|
|
@@ -14,20 +14,20 @@ export interface AutoPreferDoc {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const pendingAutoPreferKey: unique symbol;
|
|
16
16
|
export declare const DOCTOR_ROUTING_STALE_MS: number;
|
|
17
|
-
export declare function deriveAutoPrefer(cfg:
|
|
17
|
+
export declare function deriveAutoPrefer(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, profile?: RoutingProfile): AutoPreferDoc;
|
|
18
18
|
export declare function readAutoPrefer(repoRoot: string): AutoPreferDoc | null;
|
|
19
|
-
export declare function routingPreferContext(repoRoot: string, cfg:
|
|
19
|
+
export declare function routingPreferContext(repoRoot: string, cfg: TickmarkrConfig, opts?: {
|
|
20
20
|
globalDir?: string;
|
|
21
21
|
}): {
|
|
22
22
|
autoPrefer?: AutoPreferDoc;
|
|
23
23
|
doctorFresh: boolean;
|
|
24
24
|
overlayPreferShapes: ReadonlySet<string>;
|
|
25
25
|
};
|
|
26
|
-
export declare function probeModels(cfg:
|
|
26
|
+
export declare function probeModels(cfg: TickmarkrConfig, repoRoot: string, adapters: WorkerAdapter[], health: Record<string, AuthHealth>, onProgress?: ProbeModelProgress): Promise<void>;
|
|
27
27
|
export declare function writeDoctor(repoRoot: string, health: Record<string, AuthHealth>): void;
|
|
28
28
|
export declare function readDoctor(repoRoot: string): Record<string, AuthHealth> | null;
|
|
29
|
-
export declare function discoverChannels(cfg:
|
|
30
|
-
export declare function servableExclusions(cfg:
|
|
29
|
+
export declare function discoverChannels(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): BillingChannel[];
|
|
30
|
+
export declare function servableExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
|
|
31
31
|
key: string;
|
|
32
32
|
adapter: string;
|
|
33
33
|
}[];
|
|
@@ -35,7 +35,7 @@ export declare function servabilityLine(excluded: {
|
|
|
35
35
|
key: string;
|
|
36
36
|
adapter: string;
|
|
37
37
|
}[]): string;
|
|
38
|
-
export declare function modelAuthExclusions(cfg:
|
|
38
|
+
export declare function modelAuthExclusions(cfg: TickmarkrConfig, adapters: WorkerAdapter[], health: Record<string, AuthHealth>): {
|
|
39
39
|
key: string;
|
|
40
40
|
adapter: string;
|
|
41
41
|
reason: string;
|
|
@@ -54,6 +54,6 @@ export declare function initDoctorReuse(repoRoot: string, fresh: boolean): {
|
|
|
54
54
|
ageMs: number | null;
|
|
55
55
|
health: Record<string, AuthHealth> | null;
|
|
56
56
|
};
|
|
57
|
-
export declare function formatDoctorReport(cwd: string, cfg:
|
|
57
|
+
export declare function formatDoctorReport(cwd: string, cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
|
|
58
58
|
wrote?: boolean;
|
|
59
59
|
}): string;
|
|
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { overlayPreferShapes, TIER_RANK } from "../config/config.js";
|
|
5
5
|
import { modelLints, suggestOverlay } from "./model-lints.js";
|
|
6
6
|
import { HerdrDriver } from "../drivers/herdr.js";
|
|
7
|
-
import {
|
|
7
|
+
import { tickmarkrDir, stateDirName } from "../graph/graph.js";
|
|
8
8
|
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../route/preference.js";
|
|
9
9
|
import { learnedScore } from "../route/profile.js";
|
|
10
10
|
import { marginalCostRank } from "../route/router.js";
|
|
@@ -22,7 +22,7 @@ export function allAdapters(opts = {}) {
|
|
|
22
22
|
// keeps the Phase 6 shape→channel matrix byte-identical; inserting anywhere else silently
|
|
23
23
|
// reassigns shapes on same-tier ties. grok is appended AFTER pi for the same reason.
|
|
24
24
|
const real = [claudeCode, codex, cursorAgent, opencode, pi, grok];
|
|
25
|
-
const fakePath = opts.fakeScriptPath ?? process.env.
|
|
25
|
+
const fakePath = opts.fakeScriptPath ?? process.env.TICKMARKR_FAKE_SCRIPT;
|
|
26
26
|
return fakePath ? [new FakeAdapter(fakePath), ...real] : real;
|
|
27
27
|
}
|
|
28
28
|
export function getAdapter(id, adapters) {
|
|
@@ -131,7 +131,7 @@ export async function probeModels(cfg, repoRoot, adapters, health, onProgress) {
|
|
|
131
131
|
verdict = { authed: false, reason: "headless probe unavailable", probedAt, durationMs: Date.now() - t0 };
|
|
132
132
|
}
|
|
133
133
|
else {
|
|
134
|
-
const promptFile = join(mkdtempSync(join(tmpdir(), "
|
|
134
|
+
const promptFile = join(mkdtempSync(join(tmpdir(), "tickmarkr-auth-")), "probe.md");
|
|
135
135
|
writeFileSync(promptFile, MODEL_PROBE_PROMPT);
|
|
136
136
|
const r = await sh(a.headlessCommand(promptFile, model), probeRoot, MODEL_PROBE_TIMEOUT_MS);
|
|
137
137
|
if (r.timedOut && priorTimedOut) {
|
|
@@ -181,7 +181,7 @@ function readDoctorFile(repoRoot) {
|
|
|
181
181
|
return existsSync(doctorPath(repoRoot)) ? JSON.parse(readFileSync(doctorPath(repoRoot), "utf8")) : null;
|
|
182
182
|
}
|
|
183
183
|
export function writeDoctor(repoRoot, health) {
|
|
184
|
-
|
|
184
|
+
tickmarkrDir(repoRoot);
|
|
185
185
|
const pending = health[pendingAutoPreferKey];
|
|
186
186
|
const payload = { ...health };
|
|
187
187
|
delete payload[pendingAutoPreferKey];
|
|
@@ -202,10 +202,10 @@ export function discoverChannels(cfg, adapters, health) {
|
|
|
202
202
|
.flatMap((a) => {
|
|
203
203
|
const h = health[a.id];
|
|
204
204
|
const s = h?.servable;
|
|
205
|
-
// T2 (2026-07-13): a model doctor marked
|
|
206
|
-
// channel — routing
|
|
207
|
-
//
|
|
208
|
-
return a.channels(cfg).filter((c) => modelAuthed(h, c.model) && (!s || s.includes(c.model)));
|
|
205
|
+
// T2 (2026-07-13): only a model doctor marked authed (modelAuth[model].authed===true) advertises a
|
|
206
|
+
// channel — routing an unknown or 403 into dispatch is fail-closed. Operators with pre-v1.21 doctor.json
|
|
207
|
+
// can opt into the prior unknown-is-routable behavior with routing.allowUnverifiedModels.
|
|
208
|
+
return a.channels(cfg).filter((c) => modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels) && (!s || s.includes(c.model)));
|
|
209
209
|
});
|
|
210
210
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
211
211
|
return base;
|
|
@@ -233,23 +233,23 @@ export function servabilityLine(excluded) {
|
|
|
233
233
|
const parts = excluded.map(({ key, adapter }) => `${key} (not in ${adapter}'s served model list)`);
|
|
234
234
|
return `servability: ${excluded.length} channel(s) unservable — ${parts.join(", ")}`;
|
|
235
235
|
}
|
|
236
|
-
// T2 (2026-07-13): channels discoverChannels dropped because doctor marked their model unauthed
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
// a missing modelAuth record ⇒ unknown ⇒ NOT excluded (compat with pre-v1.5 doctor.json + probeAll fallback).
|
|
236
|
+
// T2 (2026-07-13): channels discoverChannels dropped because doctor marked their model unauthed or left it
|
|
237
|
+
// unverified. Computed from the SAME inputs as the drop (a.channels(cfg)) so attribution can never drift from
|
|
238
|
+
// behavior. installed+authed mirrors the discoverChannels adapter gate.
|
|
240
239
|
export function modelAuthExclusions(cfg, adapters, health) {
|
|
241
240
|
const out = [];
|
|
242
241
|
for (const a of adapters) {
|
|
243
242
|
const h = health[a.id];
|
|
244
243
|
if (!h?.installed || !h?.authed)
|
|
245
244
|
continue;
|
|
246
|
-
const verdicts = h.modelAuth;
|
|
247
|
-
if (!verdicts)
|
|
248
|
-
continue;
|
|
249
245
|
for (const c of a.channels(cfg)) {
|
|
250
|
-
const v =
|
|
246
|
+
const v = h.modelAuth?.[c.model];
|
|
247
|
+
if (modelAuthed(h, c.model, cfg.routing.allowUnverifiedModels))
|
|
248
|
+
continue;
|
|
251
249
|
if (v?.authed === false)
|
|
252
250
|
out.push({ key: channelKey(c), adapter: a.id, reason: v.reason ?? "probe failed", probedAt: v.probedAt });
|
|
251
|
+
else
|
|
252
|
+
out.push({ key: channelKey(c), adapter: a.id, reason: "no model auth verdict — run tickmarkr doctor", probedAt: "not recorded" });
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
return out;
|
|
@@ -327,7 +327,7 @@ export function formatDoctorReport(cwd, cfg, health, adapters, opts = {}) {
|
|
|
327
327
|
let drift = "";
|
|
328
328
|
if (frag) {
|
|
329
329
|
if (visual) {
|
|
330
|
-
const overlayPath = join(
|
|
330
|
+
const overlayPath = join(tickmarkrDir(cwd), "doctor-overlay.yaml");
|
|
331
331
|
writeFileSync(overlayPath, frag);
|
|
332
332
|
drift = `\nmodel drift: unclassified models detected — paste-ready overlay written to ${overlayPath} (advisory; tickmarkr never applies it)`;
|
|
333
333
|
}
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TickmarkrConfig, Tier } from "../config/config.js";
|
|
3
3
|
import type { Task } from "../graph/schema.js";
|
|
4
4
|
export declare const TokenUsageSchema: z.ZodObject<{
|
|
5
5
|
input: z.ZodNumber;
|
|
@@ -38,7 +38,7 @@ export interface AuthHealth {
|
|
|
38
38
|
servable?: string[];
|
|
39
39
|
modelAuth?: Record<string, ModelAuth>;
|
|
40
40
|
}
|
|
41
|
-
export declare function modelAuthed(health: AuthHealth | undefined, model: string): boolean;
|
|
41
|
+
export declare function modelAuthed(health: AuthHealth | undefined, model: string, allowUnverifiedModels?: boolean): boolean;
|
|
42
42
|
export interface Invocation {
|
|
43
43
|
command: string;
|
|
44
44
|
}
|
|
@@ -74,7 +74,7 @@ export interface WorkerAdapter {
|
|
|
74
74
|
vendor: string;
|
|
75
75
|
probeCwd?: "repo" | "neutral";
|
|
76
76
|
probe(): Promise<AuthHealth>;
|
|
77
|
-
channels(cfg:
|
|
77
|
+
channels(cfg: TickmarkrConfig): BillingChannel[];
|
|
78
78
|
headlessCommand(promptFile: string, model: string): string;
|
|
79
79
|
interactiveCommand(promptFile: string, model: string): string | null;
|
|
80
80
|
resumeCommand?(sessionId: string, promptFile: string, model: string): string;
|
|
@@ -89,7 +89,7 @@ export interface WorkerAdapter {
|
|
|
89
89
|
trust?(repoRoot: string): TrustVerdict;
|
|
90
90
|
trustDialog?: TrustDialog;
|
|
91
91
|
}
|
|
92
|
-
export declare function channelsFromConfig(adapterId: string, cfg:
|
|
92
|
+
export declare function channelsFromConfig(adapterId: string, cfg: TickmarkrConfig): BillingChannel[];
|
|
93
93
|
export declare function channelKey(c: {
|
|
94
94
|
adapter: string;
|
|
95
95
|
model: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -23,8 +23,9 @@ export function addUsage(a, b) {
|
|
|
23
23
|
reasoning: add(a.reasoning, b.reasoning),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
export function modelAuthed(health, model) {
|
|
27
|
-
|
|
26
|
+
export function modelAuthed(health, model, allowUnverifiedModels = false) {
|
|
27
|
+
const authed = health?.modelAuth?.[model]?.authed;
|
|
28
|
+
return authed === true || (authed === undefined && allowUnverifiedModels);
|
|
28
29
|
}
|
|
29
30
|
export function matchesTrustDialog(paneText, dialog) {
|
|
30
31
|
return paneText.includes(dialog.fingerprint);
|
package/dist/brand.d.ts
ADDED
package/dist/brand.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// TTY-only pixel-tick logo (assets/mark.svg is the image twin — same block geometry).
|
|
2
|
+
// Never printed to pipes — the non-TTY stdout surface is byte-pinned by tests and consumed by machines.
|
|
3
|
+
const B = "\x1b[1m", R = "\x1b[0m";
|
|
4
|
+
const g = (n, s) => `\x1b[38;5;${n}m${s}${R}`; // 256-color green ramp, bright → deep
|
|
5
|
+
export const BANNER = [
|
|
6
|
+
` ${g(84, "▄▄████")}`,
|
|
7
|
+
` ${g(78, "▄▄████▀▀")}`,
|
|
8
|
+
`${g(41, "████▄▄▄▄████▀▀")} ${B}tickmarkr${R}`,
|
|
9
|
+
` ${g(35, "▀▀████▀▀")} spec in, verified work out.`,
|
|
10
|
+
"",
|
|
11
|
+
].join("\n");
|
|
12
|
+
// Shell one-liner that prints the banner inside a pane before a gate command runs. ESC bytes are
|
|
13
|
+
// carried as printf %b escapes (never raw control bytes in a command string crossing the herdr socket).
|
|
14
|
+
export function bannerShell() {
|
|
15
|
+
const printable = BANNER.replaceAll("\x1b", "\\033").replaceAll("\n", "\\n");
|
|
16
|
+
return `printf '%b\\n' '${printable}'`;
|
|
17
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { userInfo } from "node:os";
|
|
2
2
|
import { ATTEMPT_CAP_RELEASE, Journal } from "../../run/journal.js";
|
|
3
|
-
// GATE-08 (v1.12): approve a parked human gate so the next `
|
|
3
|
+
// GATE-08 (v1.12): approve a parked human gate so the next `tickmarkr resume <runId>` dispatches it.
|
|
4
4
|
//
|
|
5
5
|
// The approval is a JOURNAL EVENT (task-approved) carrying who and when — it touches ONLY the
|
|
6
|
-
// append-only journal. Writing it into
|
|
6
|
+
// append-only journal. Writing it into tickmarkr's compiled graph artifact would be silently erased by
|
|
7
7
|
// the next recompile (which re-emits humanGate:true from the plan frontmatter) — Phase 42 D-02.
|
|
8
8
|
//
|
|
9
9
|
// v1.24 OBS-18: when the park was attempt-cap (not a humanGate pre-dispatch park), the event also
|
|
@@ -13,7 +13,7 @@ import { ATTEMPT_CAP_RELEASE, Journal } from "../../run/journal.js";
|
|
|
13
13
|
//
|
|
14
14
|
// Fail-closed (D-05): unknown runId, unknown taskId, a not-parked task, and a double-approve are all
|
|
15
15
|
// LOUD refusals that name the reason and append NO event — never a silent no-op. A handler throw
|
|
16
|
-
// becomes `
|
|
16
|
+
// becomes `tickmarkr approve: <message>` at exit 1 (src/cli/index.ts dispatch).
|
|
17
17
|
//
|
|
18
18
|
// Who/when is truthful, not dressed-up auth (D-03): default actor os.userInfo().username; --by overrides
|
|
19
19
|
// for delegated approval; optional --reason; the event's ts (stamped by Journal.append) is the when.
|