tickmarkr 1.30.0 → 1.33.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 +94 -13
- package/dist/cli/commands/doctor.js +2 -2
- package/dist/cli/commands/init.js +79 -8
- package/dist/cli/commands/status.d.ts +0 -4
- package/dist/cli/commands/status.js +67 -56
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +4 -4
- package/dist/compile/native.js +2 -2
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +107 -16
- package/dist/drivers/types.js +4 -3
- package/dist/plan/prompt.js +1 -1
- package/dist/plan/scope.js +4 -4
- package/dist/run/daemon.js +4 -12
- package/dist/run/reconcile.js +1 -0
- package/package.json +4 -4
- package/skills/tickmarkr-auto/SKILL.md +41 -0
- package/skills/tickmarkr-loop/SKILL.md +39 -0
package/README.md
CHANGED
|
@@ -13,8 +13,7 @@ Green tasks consolidate onto a `tickmarkr/<runId>` **consolidation** branch; **s
|
|
|
13
13
|
your mainline) is always the Partner's call, never the engine's. A fully green engagement is an
|
|
14
14
|
**unqualified opinion** — every assertion independently ticked.
|
|
15
15
|
|
|
16
|
-
> The package ships `tickmarkr`
|
|
17
|
-
> backward compatibility — all three invoke the same entry. New installs should call `tickmarkr`.
|
|
16
|
+
> The package ships `tickmarkr` and `tkr` only; `tkr` is the short alias.
|
|
18
17
|
|
|
19
18
|
## Vocabulary
|
|
20
19
|
|
|
@@ -53,24 +52,27 @@ These are law; the codebase fails closed around them:
|
|
|
53
52
|
- worker/judge/review/consult prompts end with machine-parseable trailers
|
|
54
53
|
(`DROVR_RESULT` / JSON verdicts); parse defensively, fail closed
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
Any agent (or human) operating this repo must respect the same five rules above — they are not
|
|
56
|
+
merely internal implementation details, they are the contract the gates enforce.
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
`opencode`, `grok`, or `pi`) authenticated through its own login.
|
|
58
|
+
## Install
|
|
60
59
|
|
|
61
60
|
```bash
|
|
62
|
-
npm
|
|
63
|
-
npm run build
|
|
64
|
-
npm link # exposes the `tickmarkr` bin (and the `drovr` / `drover` compat alias)
|
|
61
|
+
npm i -g tickmarkr
|
|
65
62
|
```
|
|
66
63
|
|
|
64
|
+
This installs two identical bins: `tickmarkr` and its short alias `tkr` — use whichever you
|
|
65
|
+
prefer, every example below works with either. Requirements: Node ≥ 20, git, and at least one
|
|
66
|
+
agent CLI on PATH (`claude`, `codex`, `cursor-agent`, `opencode`, `grok`, or `pi`) authenticated
|
|
67
|
+
through its own login.
|
|
68
|
+
|
|
67
69
|
Then verify your fleet:
|
|
68
70
|
|
|
69
71
|
```bash
|
|
70
72
|
tickmarkr doctor # probes installed adapters, herdr, auth; prints the capability matrix
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
## Quickstart
|
|
75
|
+
## Quickstart (5 minutes)
|
|
74
76
|
|
|
75
77
|
```bash
|
|
76
78
|
tickmarkr init # guided setup + doctor; scaffolds .drovr/config.yaml and drovr.spec.md
|
|
@@ -81,6 +83,34 @@ tickmarkr run # execute the graph (--concurrency N --driver her
|
|
|
81
83
|
tickmarkr report <runId> --md # engagement record in Markdown
|
|
82
84
|
```
|
|
83
85
|
|
|
86
|
+
That's the whole loop: `init` scaffolds, you write the spec's tasks and `acceptance[]`, `compile`
|
|
87
|
+
turns it into a task graph, `plan` shows you the routing/cost dry run, `run` dispatches field teams
|
|
88
|
+
and gates every result, and `report` writes the engagement record. Nothing merges to your mainline
|
|
89
|
+
without your sign-off.
|
|
90
|
+
|
|
91
|
+
## Agent-ready repos: `tickmarkr init --agent`
|
|
92
|
+
|
|
93
|
+
`tickmarkr init --agent` composes with the base `init` above and additionally installs the
|
|
94
|
+
consumer-facing skills (`tickmarkr-loop`, `tickmarkr-auto`) that ship in the npm tarball, so a
|
|
95
|
+
coding agent working in your repo knows how to drive tickmarkr without reading its source:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
tickmarkr init --agent # installs .claude/skills/tickmarkr-{loop,auto}, offers CLAUDE.md/AGENTS.md notes
|
|
99
|
+
tickmarkr init --agent --force # also overwrite skill files that already exist
|
|
100
|
+
tickmarkr init --agent --docs # also append the agent-docs section without an interactive prompt
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Consent rules — every write is additive, never destructive:
|
|
104
|
+
|
|
105
|
+
- an existing `.claude/skills/tickmarkr-{loop,auto}/SKILL.md` is left untouched unless you answer
|
|
106
|
+
yes to a per-file prompt, or pass `--force`
|
|
107
|
+
- a short "tickmarkr" section (the loop commands + the invariants above) is appended to `CLAUDE.md`
|
|
108
|
+
— or `AGENTS.md` if that exists and `CLAUDE.md` doesn't — only after you say yes, or pass `--docs`;
|
|
109
|
+
it's wrapped in `<!-- tickmarkr:agent-docs begin/end -->` markers and never inserted twice
|
|
110
|
+
- declining a prompt still lets the rest of `init` complete
|
|
111
|
+
- non-interactive shells (no TTY) never prompt: missing skills are installed, everything else is
|
|
112
|
+
skipped, and the summary names exactly what was skipped and which flag would enable it
|
|
113
|
+
|
|
84
114
|
Supervise and finish:
|
|
85
115
|
|
|
86
116
|
```bash
|
|
@@ -332,14 +362,65 @@ Cloning this repo gives Claude Code project skills under `.claude/skills/`:
|
|
|
332
362
|
|
|
333
363
|
Repo-scoped: available automatically in Claude Code sessions inside this repo; not installed by npm.
|
|
334
364
|
|
|
335
|
-
##
|
|
365
|
+
## Contributing
|
|
366
|
+
|
|
367
|
+
### Dev setup
|
|
336
368
|
|
|
337
369
|
```bash
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
npm
|
|
370
|
+
git clone <your fork>
|
|
371
|
+
cd tickmarkr
|
|
372
|
+
npm install
|
|
373
|
+
npm run build # tsc → dist/; the only enforced static check
|
|
374
|
+
npm link # exposes the tickmarkr/tkr bins from your checkout
|
|
341
375
|
```
|
|
342
376
|
|
|
377
|
+
### Tests are zero-token by design
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm test # vitest unit+integration — fake adapter only, spends no tokens, ever
|
|
381
|
+
npm run test:coverage # same suite, coverage floors enforced (see below)
|
|
382
|
+
npm run e2e # opt-in real-CLI end-to-end — DOES spend tokens, needs ≥1 agent CLI installed
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
`npm test` and `npm run test:coverage` must never call a real agent CLI or spend tokens — that's
|
|
386
|
+
what `DROVR_FAKE_SCRIPT` and the `FakeAdapter` (`src/adapters/fake.ts`) exist for. If a change
|
|
387
|
+
needs a real CLI to verify, that test belongs under `npm run e2e` (`tests/e2e/`), gated behind
|
|
388
|
+
`DROVR_E2E=1`, never in the default suite.
|
|
389
|
+
|
|
390
|
+
Coverage floor: `src/{graph,route,gates,run}/**` must stay at lines 80% / functions 80% /
|
|
391
|
+
branches 70% — this is the CLAUDE.md invariant, enforced verbatim in `vitest.config.ts`.
|
|
392
|
+
`src/compile/**`, `src/adapters/**`, and `src/drivers/**` carry their own (higher) measured
|
|
393
|
+
thresholds in the same file.
|
|
394
|
+
|
|
395
|
+
### The spec-driven workflow — the gates are the review
|
|
396
|
+
|
|
397
|
+
Changes to tickmarkr itself land the same way tickmarkr expects consumer changes to land: write a
|
|
398
|
+
native spec (`specs/<version>-<name>.spec.md`, marked `<!-- tickmarkr:spec -->`), give every task
|
|
399
|
+
real `acceptance[]` criteria, then compile/plan/run it and let the gates (baseline, evidence, scope,
|
|
400
|
+
acceptance-judge, cross-vendor review) independently verify the diff before it merges. PRs are
|
|
401
|
+
welcome, but a PR that only "looks right" isn't the bar — a green, gated engagement is. If you're
|
|
402
|
+
proposing a non-trivial change, sketch it as a spec first; small fixes and docs can go straight to
|
|
403
|
+
a PR.
|
|
404
|
+
|
|
405
|
+
### Adding a worker adapter
|
|
406
|
+
|
|
407
|
+
Support for a new agent CLI is one new file implementing `WorkerAdapter`
|
|
408
|
+
(`src/adapters/types.ts`) — `id`, `vendor`, `probe()`, `channels()`, `headlessCommand()`,
|
|
409
|
+
`interactiveCommand()`, `invoke()`, `parse()`, plus the optional `listModels`/`resumeCommand`
|
|
410
|
+
capabilities other adapters use when the CLI supports them. Use an existing adapter as the
|
|
411
|
+
template (`src/adapters/codex.ts` is a good starting shape), then register it with one line in
|
|
412
|
+
`src/adapters/registry.ts`'s `allAdapters()` — append it, don't insert it in the middle, since
|
|
413
|
+
same-tier routing ties resolve by discovery order. No other file needs to know a new adapter
|
|
414
|
+
exists.
|
|
415
|
+
|
|
416
|
+
### Filing defects
|
|
417
|
+
|
|
418
|
+
Abnormalities found during a real engagement (not routine bugs — surprising or systemic
|
|
419
|
+
behavior) are logged as a numbered entry (`OBS-NN`) in `.planning/OBSERVATIONS.md`, with a status
|
|
420
|
+
(`OPEN` / `CLOSED-FIXED` / `CLOSED-MITIGATED`) and, once fixed, the spec/commit that closed it.
|
|
421
|
+
That file is the standing abnormality ledger for this repo — read it before assuming a rough edge
|
|
422
|
+
is new.
|
|
423
|
+
|
|
343
424
|
Design spec: `docs/superpowers/specs/2026-07-07-drover-design.md`.
|
|
344
425
|
|
|
345
426
|
## License
|
|
@@ -68,7 +68,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
68
68
|
rows.push(` ! ${servabilityLine(servable)}`);
|
|
69
69
|
// MODEL-05/06: print-only drift fragment; advisory, whole-line-commented additions, drovr NEVER applies it.
|
|
70
70
|
const frag = suggestOverlay(cfg, health, adapters);
|
|
71
|
-
const drift = frag ? `\nmodel drift — paste-ready overlay (advisory;
|
|
71
|
+
const drift = frag ? `\nmodel drift — paste-ready overlay (advisory; tickmarkr never applies):\n${frag}` : "";
|
|
72
72
|
// T4: model-status table — one row per CLASSIFIED model (tiers config) with tier, auth verdict
|
|
73
73
|
// (reason + date when unauthed), operator-deny flag, and prefer rank across the routing map.
|
|
74
74
|
// Unclassified listed models (detected via listModels but never tiered) compress to one count line
|
|
@@ -99,5 +99,5 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
99
99
|
return rows;
|
|
100
100
|
});
|
|
101
101
|
const modelSummary = modelStatus.length ? `\nmodel status:\n${modelStatus.join("\n")}` : "";
|
|
102
|
-
return `
|
|
102
|
+
return `tickmarkr doctor — capability matrix:\n${rows.join("\n")}${modelSummary}${drift}\nwrote .drovr/doctor.json`;
|
|
103
103
|
}
|
|
@@ -1,12 +1,78 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { configTemplate, globalConfigDir } from "../../config/config.js";
|
|
5
6
|
import { specTemplate } from "../../compile/native.js";
|
|
6
7
|
import { drovrDir } from "../../graph/graph.js";
|
|
7
8
|
import { doctor } from "./doctor.js";
|
|
9
|
+
const AGENT_SKILLS = ["tickmarkr-loop", "tickmarkr-auto"];
|
|
10
|
+
const DOCS_BEGIN = "<!-- tickmarkr:agent-docs begin -->";
|
|
11
|
+
const DOCS_END = "<!-- tickmarkr:agent-docs end -->";
|
|
12
|
+
const AGENT_DOCS = `${DOCS_BEGIN}
|
|
13
|
+
## tickmarkr
|
|
14
|
+
|
|
15
|
+
tickmarkr turns repository specs into isolated, independently verified agent work. Use \`/tickmarkr-loop\` for one spec or \`/tickmarkr-auto\` for several.
|
|
16
|
+
|
|
17
|
+
Loop: \`tickmarkr compile <spec>\` → \`tickmarkr plan\` → \`tickmarkr run\` → \`tickmarkr report <runId> --md\`.
|
|
18
|
+
|
|
19
|
+
- Never run two tickmarkr runs in the same repository concurrently.
|
|
20
|
+
- Never merge tickmarkr work directly to main; new runs consolidate on \`tickmarkr/<runId>\`.
|
|
21
|
+
- Fix source specs instead of editing generated graphs.
|
|
22
|
+
- Never trust a worker's completion claim; gates independently verify the work.
|
|
23
|
+
- Treat missing or unparseable results as failures.
|
|
24
|
+
${DOCS_END}
|
|
25
|
+
`;
|
|
26
|
+
async function installAgentFiles(cwd, force, docs, notes) {
|
|
27
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
28
|
+
let prompt;
|
|
29
|
+
const confirm = async (question) => {
|
|
30
|
+
if (!interactive)
|
|
31
|
+
return false;
|
|
32
|
+
prompt ??= createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
return /^(?:y|yes)$/i.test((await prompt.question(`${question} [y/N] `)).trim());
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
for (const skill of AGENT_SKILLS) {
|
|
37
|
+
const dest = join(cwd, ".claude", "skills", skill, "SKILL.md");
|
|
38
|
+
const exists = existsSync(dest);
|
|
39
|
+
if (exists && !force && !(await confirm(`Overwrite ${dest}?`))) {
|
|
40
|
+
notes.push(`skipped existing ${dest}; pass --force to overwrite it`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
mkdirSync(join(cwd, ".claude", "skills", skill), { recursive: true });
|
|
44
|
+
writeFileSync(dest, readFileSync(new URL(`../../../skills/${skill}/SKILL.md`, import.meta.url)), exists ? undefined : { flag: "wx" });
|
|
45
|
+
notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
|
|
46
|
+
}
|
|
47
|
+
const claude = join(cwd, "CLAUDE.md");
|
|
48
|
+
const agents = join(cwd, "AGENTS.md");
|
|
49
|
+
const docPath = existsSync(claude) || !existsSync(agents) ? claude : agents;
|
|
50
|
+
const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
|
|
51
|
+
if (current.includes(DOCS_BEGIN) || current.includes(DOCS_END)) {
|
|
52
|
+
notes.push(`kept existing tickmarkr agent docs in ${docPath}`);
|
|
53
|
+
}
|
|
54
|
+
else if (docs || await confirm(`Append tickmarkr agent docs to ${docPath}?`)) {
|
|
55
|
+
appendFileSync(docPath, `${current ? current.endsWith("\n") ? "\n" : "\n\n" : ""}${AGENT_DOCS}`);
|
|
56
|
+
notes.push(`appended tickmarkr agent docs to ${docPath}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
notes.push(`skipped agent docs for ${docPath}; pass --docs to append them`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
prompt?.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
8
66
|
export async function init(argv, cwd = process.cwd()) {
|
|
9
|
-
const { values } = parseArgs({
|
|
67
|
+
const { values } = parseArgs({
|
|
68
|
+
args: argv,
|
|
69
|
+
options: {
|
|
70
|
+
"global-dir": { type: "string" },
|
|
71
|
+
agent: { type: "boolean" },
|
|
72
|
+
force: { type: "boolean" },
|
|
73
|
+
docs: { type: "boolean" },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
10
76
|
const gdir = values["global-dir"] ?? globalConfigDir();
|
|
11
77
|
mkdirSync(gdir, { recursive: true });
|
|
12
78
|
const notes = [];
|
|
@@ -19,14 +85,19 @@ export async function init(argv, cwd = process.cwd()) {
|
|
|
19
85
|
notes.push(`kept existing ${p}`);
|
|
20
86
|
}
|
|
21
87
|
}
|
|
22
|
-
const specPath = join(cwd, "
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
88
|
+
const specPath = join(cwd, "tickmarkr.spec.md");
|
|
89
|
+
if (existsSync(specPath)) {
|
|
90
|
+
notes.push(`kept existing ${specPath}`);
|
|
91
|
+
}
|
|
92
|
+
else if (existsSync(join(cwd, "drovr.spec.md"))) {
|
|
93
|
+
notes.push(`kept existing ${join(cwd, "drovr.spec.md")}`);
|
|
26
94
|
}
|
|
27
95
|
else {
|
|
28
|
-
|
|
96
|
+
writeFileSync(specPath, specTemplate());
|
|
97
|
+
notes.push(`wrote ${specPath}`);
|
|
29
98
|
}
|
|
99
|
+
if (values.agent)
|
|
100
|
+
await installAgentFiles(cwd, values.force ?? false, values.docs ?? false, notes);
|
|
30
101
|
const doc = await doctor([], cwd);
|
|
31
|
-
return `${notes.join("\n")}\n${doc}\nnext: edit
|
|
102
|
+
return `${notes.join("\n")}\n${doc}\nnext: edit tickmarkr.spec.md, then tickmarkr compile tickmarkr.spec.md && tickmarkr plan && tickmarkr run`;
|
|
32
103
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { type Task } from "../../graph/schema.js";
|
|
2
|
-
import { type JournalEvent } from "../../run/journal.js";
|
|
3
1
|
export type StatusOpts = {
|
|
4
2
|
iterations?: number;
|
|
5
3
|
sleep?: (ms: number) => Promise<void>;
|
|
6
4
|
};
|
|
7
|
-
/** Derive the live phase column for one task. Counts only that task's events since its last dispatch/escalation. */
|
|
8
|
-
export declare const derivePhase: (task: Task, events: JournalEvent[], channel?: string) => string;
|
|
9
5
|
export declare function status(argv: string[], cwd?: string, opts?: StatusOpts): Promise<string>;
|
|
@@ -6,6 +6,7 @@ const REFRESH_MS = 2000;
|
|
|
6
6
|
// The timer must keep the process ALIVE: an unref'd timer here let the event loop drain after the
|
|
7
7
|
// first frame, so a live `--watch` printed once and exited 0 (OBS-11). Never unref this.
|
|
8
8
|
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
const GATE_KEYS = { build: "B", test: "T", lint: "L", evidence: "E", scope: "S", acceptance: "A", review: "R" };
|
|
9
10
|
const attemptStartIdx = (events, taskId) => {
|
|
10
11
|
let idx = -1;
|
|
11
12
|
for (let i = 0; i < events.length; i++) {
|
|
@@ -15,39 +16,51 @@ const attemptStartIdx = (events, taskId) => {
|
|
|
15
16
|
}
|
|
16
17
|
return idx;
|
|
17
18
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return "
|
|
29
|
-
if (!attempt.some((e) => e.event === "worker-result"))
|
|
30
|
-
return channel ? `worker · ${channel}` : "worker";
|
|
31
|
-
const enabled = GATE_NAMES.filter((g) => task.gates.includes(g));
|
|
32
|
-
const n = enabled.length;
|
|
33
|
-
const gateResults = attempt.filter((e) => e.event === "gate-result");
|
|
34
|
-
if (gateResults.length > 0) {
|
|
35
|
-
const last = gateResults[gateResults.length - 1];
|
|
36
|
-
if (!last.data.pass)
|
|
37
|
-
return `gate ✗ ${last.data.gate}`;
|
|
19
|
+
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
20
|
+
const color = (text, code, enabled) => enabled ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
21
|
+
const taskBox = (status, unicode) => {
|
|
22
|
+
if (unicode) {
|
|
23
|
+
if (status === "done")
|
|
24
|
+
return "✓";
|
|
25
|
+
if (status === "failed")
|
|
26
|
+
return "✗";
|
|
27
|
+
if (status === "human")
|
|
28
|
+
return "⏸";
|
|
29
|
+
return "☐";
|
|
38
30
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
31
|
+
if (status === "done")
|
|
32
|
+
return "[x]";
|
|
33
|
+
if (status === "failed" || status === "human")
|
|
34
|
+
return "[!]";
|
|
35
|
+
return "[ ]";
|
|
36
|
+
};
|
|
37
|
+
const gateBox = (state, unicode) => {
|
|
38
|
+
if (unicode)
|
|
39
|
+
return state === "pass" ? "✓" : state === "fail" ? "✗" : state === "skip" ? "·" : "☐";
|
|
40
|
+
return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
|
|
41
|
+
};
|
|
42
|
+
const gateChain = (task, events, unicode) => {
|
|
43
|
+
const outcomes = new Map();
|
|
44
|
+
const start = attemptStartIdx(events, task.id);
|
|
45
|
+
if (start >= 0) {
|
|
46
|
+
for (const e of events.slice(start)) {
|
|
47
|
+
if (e.taskId !== task.id || e.event !== "gate-result" || typeof e.data.gate !== "string")
|
|
48
|
+
continue;
|
|
49
|
+
if (e.data.pass === true)
|
|
50
|
+
outcomes.set(e.data.gate, "pass");
|
|
51
|
+
else if (e.data.pass === false)
|
|
52
|
+
outcomes.set(e.data.gate, "fail");
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
|
-
return
|
|
55
|
+
return GATE_NAMES.map((gate) => `${GATE_KEYS[gate]}${gateBox(task.gates.includes(gate) ? outcomes.get(gate) ?? "open" : "skip", unicode)}`).join(" ");
|
|
56
|
+
};
|
|
57
|
+
const shortGoal = (goal, max) => {
|
|
58
|
+
const clause = goal.split(/[,;.?!]/, 1)[0].trim();
|
|
59
|
+
if (clause.length <= max)
|
|
60
|
+
return clause;
|
|
61
|
+
if (max <= 3)
|
|
62
|
+
return clause.slice(0, Math.max(0, max));
|
|
63
|
+
return `${clause.slice(0, max - 3).trimEnd()}...`;
|
|
51
64
|
};
|
|
52
65
|
// VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
|
|
53
66
|
// daemon pid is still alive. Honest about unknowns: a pre-v1.13 journal with no pid renders "unknown",
|
|
@@ -72,14 +85,14 @@ const daemonPid = (events) => {
|
|
|
72
85
|
}
|
|
73
86
|
return undefined;
|
|
74
87
|
};
|
|
75
|
-
const
|
|
88
|
+
const liveness = (events) => {
|
|
76
89
|
const last = events.at(-1);
|
|
77
90
|
if (!last)
|
|
78
|
-
return
|
|
91
|
+
return "last event unknown · daemon pid unknown";
|
|
79
92
|
const age = fmtAge(Date.now() - Date.parse(last.ts));
|
|
80
93
|
const pid = daemonPid(events);
|
|
81
94
|
if (pid === undefined)
|
|
82
|
-
return `
|
|
95
|
+
return `last event ${age} ago · daemon pid unknown`;
|
|
83
96
|
let state;
|
|
84
97
|
try {
|
|
85
98
|
process.kill(pid, 0);
|
|
@@ -88,17 +101,14 @@ const livenessLine = (events) => {
|
|
|
88
101
|
catch (k) {
|
|
89
102
|
state = k.code === "ESRCH" ? "dead" : "alive";
|
|
90
103
|
} // EPERM ⇒ alive
|
|
91
|
-
return `
|
|
104
|
+
return `last event ${age} ago · daemon pid ${pid} ${state}`;
|
|
92
105
|
};
|
|
93
106
|
const renderFrame = (cwd) => {
|
|
94
107
|
const g = loadGraph(cwd);
|
|
95
108
|
const runId = Journal.latestRunId(cwd);
|
|
96
109
|
const assignments = new Map();
|
|
97
|
-
const gates = new Map();
|
|
98
110
|
let replayed = null;
|
|
99
111
|
let events = [];
|
|
100
|
-
// v1.23 T2: latest known context tokens per task (from context-sample journal events). Informational
|
|
101
|
-
// only — never feeds status/phase/gate classification (derivePhase and replayStatuses ignore them).
|
|
102
112
|
const contexts = new Map();
|
|
103
113
|
if (runId) {
|
|
104
114
|
const j = Journal.open(cwd, runId);
|
|
@@ -107,11 +117,8 @@ const renderFrame = (cwd) => {
|
|
|
107
117
|
for (const e of events) {
|
|
108
118
|
if (e.event === "task-dispatch" && e.taskId) {
|
|
109
119
|
const a = e.data.assignment;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (e.event === "gate-result" && e.taskId) {
|
|
113
|
-
const prev = gates.get(e.taskId) ?? "";
|
|
114
|
-
gates.set(e.taskId, `${prev}${e.data.pass ? "✓" : "✗"}${e.data.gate} `);
|
|
120
|
+
if (typeof a.adapter === "string" && typeof a.model === "string")
|
|
121
|
+
assignments.set(e.taskId, `${a.adapter}:${a.model}`);
|
|
115
122
|
}
|
|
116
123
|
if (e.event === "context-sample" && e.taskId && typeof e.data.tokens === "number" && Number.isFinite(e.data.tokens)) {
|
|
117
124
|
contexts.set(e.taskId, e.data.tokens); // last write wins
|
|
@@ -121,22 +128,26 @@ const renderFrame = (cwd) => {
|
|
|
121
128
|
const effective = { ...g, tasks: g.tasks.map((t) => ({ ...t, status: replayed?.get(t.id) ?? t.status })) };
|
|
122
129
|
const starved = new Set(blockedTasks(effective).map((t) => t.id));
|
|
123
130
|
const waiting = new Set(pendingTasks(effective).map((t) => t.id));
|
|
131
|
+
const unicode = visual();
|
|
132
|
+
const divider = unicode ? " · " : " / ";
|
|
124
133
|
const rows = g.tasks.map((t) => {
|
|
125
134
|
const st = replayed?.get(t.id) ?? t.status;
|
|
126
135
|
const label = starved.has(t.id) ? " starved" : waiting.has(t.id) ? " dep-waiting" : "";
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
136
|
+
const channel = assignments.get(t.id) ?? "-";
|
|
137
|
+
const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
|
|
138
|
+
const chain = gateChain(t, events, unicode);
|
|
139
|
+
const statusText = color(String(st), st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36, unicode);
|
|
140
|
+
const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
|
|
141
|
+
const width = process.stdout.columns ?? 120;
|
|
142
|
+
const prefix = ` ${taskBox(st, unicode)} ${t.id} `;
|
|
143
|
+
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))} ${chain} ${statusText}${label} ${assignCol}`;
|
|
133
144
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
].join("\n");
|
|
145
|
+
const done = effective.tasks.filter((t) => t.status === "done").length;
|
|
146
|
+
const header = runId
|
|
147
|
+
? `tickmarkr status${divider}run ${runId}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
148
|
+
: `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
|
|
149
|
+
const legend = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
150
|
+
return [header, legend, ...rows].join("\n");
|
|
140
151
|
};
|
|
141
152
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
142
153
|
if (!argv.includes("--watch"))
|
|
@@ -146,7 +157,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
146
157
|
const bounded = Number.isFinite(iterations);
|
|
147
158
|
const frames = [];
|
|
148
159
|
const sep = "\n---\n";
|
|
149
|
-
const tty =
|
|
160
|
+
const tty = visual();
|
|
150
161
|
for (let i = 0; i < iterations; i++) {
|
|
151
162
|
const frame = renderFrame(cwd);
|
|
152
163
|
if (tty)
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
export type CommandMap = Record<string, (argv: string[]) => Promise<string>>;
|
|
3
3
|
export declare const COMMANDS: CommandMap;
|
|
4
|
-
export declare const USAGE = "
|
|
4
|
+
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n compile <src> spec \u2192 .drovr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
|
|
5
5
|
export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
|
|
6
6
|
out: string;
|
|
7
7
|
code: number;
|
package/dist/cli/index.js
CHANGED
|
@@ -16,9 +16,9 @@ import { unlock } from "./commands/unlock.js";
|
|
|
16
16
|
export const COMMANDS = {
|
|
17
17
|
init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve,
|
|
18
18
|
};
|
|
19
|
-
export const USAGE = `
|
|
20
|
-
usage:
|
|
21
|
-
init guided setup + doctor
|
|
19
|
+
export const USAGE = `tickmarkr — spec-driven orchestration harness for AI coding agents
|
|
20
|
+
usage: tickmarkr <command>
|
|
21
|
+
init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
|
|
22
22
|
doctor re-probe adapters, herdr, auth; print capability matrix
|
|
23
23
|
compile <src> spec → .drovr/graph.json (fails without acceptance criteria)
|
|
24
24
|
scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
|
|
@@ -41,7 +41,7 @@ export async function dispatch(cmd, argv, commands = COMMANDS) {
|
|
|
41
41
|
return { out: await fn(argv), code: 0 };
|
|
42
42
|
}
|
|
43
43
|
catch (err) {
|
|
44
|
-
return { out: `
|
|
44
|
+
return { out: `tickmarkr ${cmd}: ${err.message}`, code: 1 };
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/* v8 ignore start -- binary entry: printing + process.exit side effects, not unit-testable (ROADMAP crit 2) */
|
package/dist/compile/native.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { GATE_NAMES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
|
|
3
3
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
4
|
-
export const NATIVE_MARKER = /^<!--\s*drovr:spec(?:\s+v1)?\s*-->\s*$/m;
|
|
4
|
+
export const NATIVE_MARKER = /^<!--\s*(?:tickmarkr|drovr):spec(?:\s+v1)?\s*-->\s*$/m;
|
|
5
5
|
const HEAD_RE = /^## (T\d+):\s*(.+)$/;
|
|
6
6
|
const FIELD_RE = /^- (\w+):\s*(.*)$/;
|
|
7
7
|
const NESTED_RE = /^\s+- (.+)$/;
|
|
@@ -143,7 +143,7 @@ export function compileNative(file) {
|
|
|
143
143
|
// parser ignores) so the template itself round-trips through compileSource() unchanged. Every field is
|
|
144
144
|
// documented; the two example tasks illustrate plain vs deps+routing-hint, each with acceptance[].
|
|
145
145
|
export function specTemplate() {
|
|
146
|
-
return `<!--
|
|
146
|
+
return `<!-- tickmarkr:spec -->
|
|
147
147
|
# drovr native spec
|
|
148
148
|
|
|
149
149
|
Your starting point for a drovr native spec. Edit this file, then run:
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -11,10 +11,13 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
11
11
|
private groups;
|
|
12
12
|
private groupSerial;
|
|
13
13
|
private ws;
|
|
14
|
+
private callerPane;
|
|
15
|
+
private watches;
|
|
14
16
|
constructor(bin?: string, workersPerTab?: number);
|
|
15
17
|
private serial;
|
|
16
18
|
static available(): boolean;
|
|
17
19
|
private herdr;
|
|
20
|
+
private namedPaneId;
|
|
18
21
|
private paneId;
|
|
19
22
|
private paneWidth;
|
|
20
23
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
@@ -36,6 +39,8 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
36
39
|
notify(msg: string, opts?: NotifyOpts): Promise<void>;
|
|
37
40
|
close(slot: Slot): Promise<void>;
|
|
38
41
|
private closeGrouped;
|
|
42
|
+
private priorWatch;
|
|
43
|
+
private watchSlot;
|
|
39
44
|
narrator(cwd: string, command: string, runId?: string): Promise<Slot>;
|
|
40
45
|
reconcile(desired: Set<string>, runId: string, opts?: {
|
|
41
46
|
spareLiveLlm?: boolean;
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { shq } from "../adapters/types.js";
|
|
2
2
|
import { createWorktree, sh } from "../run/git.js";
|
|
3
3
|
import { herdrSealShellPrefix } from "./subprocess.js";
|
|
4
|
-
import { canonicalizeLegacyName, formatOwnedName, panesToClose } from "./types.js";
|
|
4
|
+
import { canonicalizeLegacyName, formatOwnedName, panesToClose, parseOwnedName } from "./types.js";
|
|
5
5
|
// VIS-09 P43-03: adopted safety floor from 43-MEASUREMENT.md (narrowest safe 53 → floor 108).
|
|
6
6
|
export const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
7
7
|
export const TRAILER_WIDTH_MARGIN = 2; // cols below (floor + margin) refuse a rightward first split
|
|
@@ -24,6 +24,8 @@ export class HerdrDriver {
|
|
|
24
24
|
// operator's env before the driver is built). Required at slot() time, never in the constructor —
|
|
25
25
|
// pickDriver and its unit test construct HerdrDriver without env, so slot() is the trust gate.
|
|
26
26
|
ws = process.env.HERDR_WORKSPACE_ID;
|
|
27
|
+
callerPane = process.env.HERDR_PANE_ID;
|
|
28
|
+
watches = new Map();
|
|
27
29
|
constructor(bin = "herdr", workersPerTab = 3) {
|
|
28
30
|
this.bin = bin;
|
|
29
31
|
this.workersPerTab = workersPerTab;
|
|
@@ -39,18 +41,21 @@ export class HerdrDriver {
|
|
|
39
41
|
herdr(args, cwd = process.cwd(), timeoutMs) {
|
|
40
42
|
return sh(`${shq(this.bin)} ${args}`, cwd, timeoutMs);
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
async namedPaneId(name) {
|
|
45
|
+
const r = await this.herdr(`agent get ${shq(name)}`);
|
|
46
|
+
if (r.code !== 0)
|
|
47
|
+
return null;
|
|
45
48
|
try {
|
|
46
49
|
const id = JSON.parse(r.stdout).result?.agent?.pane_id;
|
|
47
|
-
|
|
48
|
-
return id;
|
|
50
|
+
return typeof id === "string" && id ? id : null;
|
|
49
51
|
}
|
|
50
52
|
catch {
|
|
51
|
-
|
|
53
|
+
return null;
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
}
|
|
56
|
+
// pane ids compact when panes close — resolve fresh via the durable agent name (spec §5)
|
|
57
|
+
async paneId(slot) {
|
|
58
|
+
return await this.namedPaneId(slot.name) ?? slot.id;
|
|
54
59
|
}
|
|
55
60
|
// VIS-09 P43-03: runtime width for the layout gate (43-MEASUREMENT.md licensing condition 2).
|
|
56
61
|
async paneWidth(paneId) {
|
|
@@ -310,6 +315,13 @@ export class HerdrDriver {
|
|
|
310
315
|
await this.herdr(`notification show ${shq(msg)} --sound ${opts?.tier === "attention" ? "request" : opts?.sound ?? "request"}`);
|
|
311
316
|
}
|
|
312
317
|
async close(slot) {
|
|
318
|
+
if (this.watches.get(slot.name)?.id === slot.id) {
|
|
319
|
+
this.watches.delete(slot.name);
|
|
320
|
+
const pane = await this.namedPaneId(slot.name);
|
|
321
|
+
if (pane)
|
|
322
|
+
await this.herdr(`pane close ${shq(pane)}`);
|
|
323
|
+
return; // run-end reconcile may already have reaped it; never close a compacted stale id
|
|
324
|
+
}
|
|
313
325
|
if (slot.group && this.groups.has(slot.group)) {
|
|
314
326
|
return this.serial(() => this.closeGrouped(slot));
|
|
315
327
|
}
|
|
@@ -342,16 +354,95 @@ export class HerdrDriver {
|
|
|
342
354
|
this.groups.delete(slot.group); // group dies when all generations gone
|
|
343
355
|
}
|
|
344
356
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
357
|
+
async priorWatch(runId) {
|
|
358
|
+
if (!this.ws)
|
|
359
|
+
throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
|
|
360
|
+
const list = await this.herdr("agent list");
|
|
361
|
+
if (list.code !== 0)
|
|
362
|
+
throw new Error(`herdr agent list failed: ${list.stderr || list.stdout}`);
|
|
363
|
+
let agents;
|
|
364
|
+
try {
|
|
365
|
+
agents = JSON.parse(list.stdout).result?.agents;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
throw new Error(`herdr agent list returned unparseable JSON: ${list.stdout}`);
|
|
369
|
+
}
|
|
370
|
+
if (!Array.isArray(agents))
|
|
371
|
+
throw new Error(`herdr agent list returned no agents: ${list.stdout}`);
|
|
372
|
+
const prior = agents.find((a) => {
|
|
373
|
+
const owned = typeof a.name === "string" ? parseOwnedName(a.name) : null;
|
|
374
|
+
return a.workspace_id === this.ws && typeof a.pane_id === "string" && owned?.role === "watch" && owned.taskId === "run" && owned.runId !== runId;
|
|
375
|
+
});
|
|
376
|
+
return prior?.pane_id ?? null;
|
|
377
|
+
}
|
|
378
|
+
// T2: the watch is a rightward sibling of the daemon's own pane, never a separate tab. Its durable
|
|
379
|
+
// owned name lets a resumed daemon find an already-running watch instead of stacking another one.
|
|
380
|
+
async watchSlot(cwd, name) {
|
|
381
|
+
if (!this.ws)
|
|
382
|
+
throw new Error("herdr watch placement requires HERDR_WORKSPACE_ID — refusing unseeded pane");
|
|
383
|
+
if (!this.callerPane)
|
|
384
|
+
throw new Error("herdr watch placement requires HERDR_PANE_ID — refusing untargeted split");
|
|
385
|
+
const sp = await this.herdr(`pane split ${shq(this.callerPane)} --direction right --no-focus`);
|
|
386
|
+
if (sp.code !== 0)
|
|
387
|
+
throw new Error(`herdr watch split failed: ${sp.stderr || sp.stdout}`);
|
|
388
|
+
let pane;
|
|
389
|
+
try {
|
|
390
|
+
pane = JSON.parse(sp.stdout).result?.pane?.pane_id;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* fail closed below */
|
|
394
|
+
}
|
|
395
|
+
if (typeof pane !== "string" || !pane)
|
|
396
|
+
throw new Error(`herdr watch split returned no pane id: ${sp.stdout}`);
|
|
397
|
+
const renamed = await this.herdr(`agent rename ${shq(pane)} ${shq(name)}`);
|
|
398
|
+
if (renamed.code !== 0 || await this.namedPaneId(name) !== pane) {
|
|
399
|
+
await this.herdr(`pane close ${shq(pane)}`);
|
|
400
|
+
throw new Error(`herdr watch rename failed: ${renamed.stderr || renamed.stdout}`);
|
|
401
|
+
}
|
|
402
|
+
const seed = await this.herdr(`pane run ${shq(pane)} ${shq(`cd ${shq(cwd)}; export HERDR_WORKSPACE_ID=${shq(this.ws)}; ${herdrSealShellPrefix()}`)}`, cwd);
|
|
403
|
+
if (seed.code !== 0) {
|
|
404
|
+
await this.herdr(`pane close ${shq(pane)}`);
|
|
405
|
+
throw new Error(`herdr watch seed failed: ${seed.stderr || seed.stdout}`);
|
|
406
|
+
}
|
|
407
|
+
return { id: pane, name, cwd };
|
|
408
|
+
}
|
|
409
|
+
// T6 narrator: the run's single live status surface. Reuse a local or already-running owned watch;
|
|
410
|
+
// a new run reowns its prior watch, and only a newly split pane receives the watch command. The
|
|
411
|
+
// status command reads the latest run every frame, so the renamed pane follows the new run without
|
|
412
|
+
// interrupting the operator's watch loop. Failures propagate — the daemon swallows.
|
|
350
413
|
async narrator(cwd, command, runId) {
|
|
351
414
|
const name = runId ? formatOwnedName({ role: "watch", taskId: "run", attempt: 0, runId }) : `narrator-watch-${process.pid}`;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
415
|
+
return this.serial(async () => {
|
|
416
|
+
const cached = this.watches.get(name);
|
|
417
|
+
if (cached)
|
|
418
|
+
return cached;
|
|
419
|
+
const existing = await this.namedPaneId(name);
|
|
420
|
+
if (existing) {
|
|
421
|
+
const s = { id: existing, name, cwd };
|
|
422
|
+
this.watches.set(name, s);
|
|
423
|
+
return s;
|
|
424
|
+
}
|
|
425
|
+
const prior = runId ? await this.priorWatch(runId) : null;
|
|
426
|
+
if (prior) {
|
|
427
|
+
const renamed = await this.herdr(`agent rename ${shq(prior)} ${shq(name)}`);
|
|
428
|
+
if (renamed.code !== 0 || await this.namedPaneId(name) !== prior) {
|
|
429
|
+
throw new Error(`herdr watch reclaim failed: ${renamed.stderr || renamed.stdout}`);
|
|
430
|
+
}
|
|
431
|
+
const s = { id: prior, name, cwd };
|
|
432
|
+
this.watches.set(name, s);
|
|
433
|
+
return s;
|
|
434
|
+
}
|
|
435
|
+
const s = await this.watchSlot(cwd, name);
|
|
436
|
+
this.watches.set(name, s);
|
|
437
|
+
try {
|
|
438
|
+
await this.run(s, command);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
this.watches.delete(name);
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
return s;
|
|
445
|
+
});
|
|
355
446
|
}
|
|
356
447
|
// OBS-17 T2 / v1.22b T1: close every drovr-owned pane that should not exist (superseded attempts,
|
|
357
448
|
// killed-daemon orphans, leftovers from OLDER runs) — in this run's workspace OR misplaced in any
|
package/dist/drivers/types.js
CHANGED
|
@@ -20,19 +20,20 @@ export function parseOwnedName(name) {
|
|
|
20
20
|
export function isForeignName(name) {
|
|
21
21
|
return parseOwnedName(name) === null;
|
|
22
22
|
}
|
|
23
|
-
// v1.22b T1: workspace-aware fold over a fleet snapshot — decides which owned panes are garbage
|
|
23
|
+
// v1.22b T1: workspace-aware fold over a fleet snapshot — decides which owned task panes are garbage
|
|
24
24
|
// right now. In-workspace: the existing desired-set/spareLiveLlm sweep (OBS-17 T2). Out-of-workspace:
|
|
25
25
|
// an owned pane from a DIFFERENT run is a misplaced leftover (bug, foreign actor, pre-VIS-10 relic)
|
|
26
26
|
// and closes regardless of `desired`; an owned pane from THIS run elsewhere is left alone — a live
|
|
27
27
|
// run can legitimately hold panes across workspaces, so only run age marks a misplaced pane garbage.
|
|
28
|
-
//
|
|
28
|
+
// Watch panes are operator-owned after run end and are reclaimed by the next run; foreign names
|
|
29
|
+
// (parseOwnedName fails) are never candidates, in any workspace.
|
|
29
30
|
export function panesToClose(agents, desired, ws, runId, opts) {
|
|
30
31
|
const out = [];
|
|
31
32
|
for (const a of agents) {
|
|
32
33
|
if (typeof a.name !== "string" || typeof a.paneId !== "string")
|
|
33
34
|
continue;
|
|
34
35
|
const owned = parseOwnedName(a.name);
|
|
35
|
-
if (!owned)
|
|
36
|
+
if (!owned || owned.role === "watch")
|
|
36
37
|
continue;
|
|
37
38
|
if (a.workspaceId === ws) {
|
|
38
39
|
if (desired.has(a.name))
|
package/dist/plan/prompt.js
CHANGED
|
@@ -3,7 +3,7 @@ export function scopePrompt(intent, repair) {
|
|
|
3
3
|
You are drafting a drovr native spec from an answered intent. Return only the Markdown spec, with no commentary or code fence.
|
|
4
4
|
|
|
5
5
|
The draft must:
|
|
6
|
-
- start with <!--
|
|
6
|
+
- start with <!-- tickmarkr:spec -->
|
|
7
7
|
- include a Requirements section whose ids are sequential REQ-01, REQ-02, ...
|
|
8
8
|
- include an Assumptions section that makes every residual uncertainty explicit
|
|
9
9
|
- include a Traceability section mapping every REQ-nn to at least one Tn task
|
package/dist/plan/scope.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "no
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { basename, dirname, extname, join } from "node:path";
|
|
4
4
|
import { discoverChannels, getAdapter, probeAll } from "../adapters/registry.js";
|
|
5
|
-
import { compileNative } from "../compile/native.js";
|
|
5
|
+
import { compileNative, NATIVE_MARKER } from "../compile/native.js";
|
|
6
6
|
import { pickDriver } from "../drivers/index.js";
|
|
7
7
|
import { extractJson, runLlm } from "../gates/llm.js";
|
|
8
8
|
import { TaskSchema } from "../graph/schema.js";
|
|
@@ -64,12 +64,12 @@ function extractDraft(raw) {
|
|
|
64
64
|
const fenced = [...clean.matchAll(/```(?:markdown|md)?\s*\n([\s\S]*?)```/gi)].at(-1)?.[1];
|
|
65
65
|
if (fenced)
|
|
66
66
|
return fenced;
|
|
67
|
-
const marker = clean.
|
|
67
|
+
const marker = clean.search(/<!--\s*(?:tickmarkr|drovr):spec/);
|
|
68
68
|
return (marker === -1 ? clean : clean.slice(marker)).trimEnd() + "\n";
|
|
69
69
|
}
|
|
70
70
|
function validateDraft(draft) {
|
|
71
|
-
if (
|
|
72
|
-
throw new Error("draft is missing the
|
|
71
|
+
if (!NATIVE_MARKER.test(draft))
|
|
72
|
+
throw new Error("draft is missing the tickmarkr native marker");
|
|
73
73
|
if (!section(draft, "Assumptions").trim())
|
|
74
74
|
throw new Error("draft is missing explicit assumptions");
|
|
75
75
|
const requirements = [...new Set(section(draft, "Requirements").match(/\bREQ-\d{2}\b/g) ?? [])];
|
package/dist/run/daemon.js
CHANGED
|
@@ -38,9 +38,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
38
38
|
// BEFORE loadGraph; release in the finally below (every exit path, incl. throws).
|
|
39
39
|
const runId = opts.runId ?? newRunId();
|
|
40
40
|
// T6 narrator: one live status surface per run (herdr only — driver.narrator is undefined on
|
|
41
|
-
// subprocess, so the optional-chain open below is a no-op there).
|
|
42
|
-
//
|
|
43
|
-
let narrator = null;
|
|
41
|
+
// subprocess, so the optional-chain open below is a no-op there). Cosmetic-only: any failure is
|
|
42
|
+
// swallowed (never affects the run); the operator closes a surviving watch pane.
|
|
44
43
|
const lock = acquireRunLock(repoRoot, runId);
|
|
45
44
|
try {
|
|
46
45
|
let graph = loadGraph(repoRoot);
|
|
@@ -91,10 +90,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
91
90
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
|
92
91
|
// a failed-to-open or later-dead watch pane never affects the run.
|
|
93
92
|
try {
|
|
94
|
-
|
|
93
|
+
await driver.narrator?.(repoRoot, "drovr status --watch", runId);
|
|
95
94
|
}
|
|
96
95
|
catch {
|
|
97
|
-
|
|
96
|
+
/* cosmetic-only — the run proceeds without a live surface */
|
|
98
97
|
}
|
|
99
98
|
const intWt = await ensureIntegration(repoRoot, branch, baseRef);
|
|
100
99
|
const concurrency = opts.concurrency ?? cfg.concurrency;
|
|
@@ -716,13 +715,6 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
716
715
|
return summary;
|
|
717
716
|
}
|
|
718
717
|
finally {
|
|
719
|
-
// T6: close the narrator at run end — cosmetic, swallowed (never blocks lock release)
|
|
720
|
-
if (narrator) {
|
|
721
|
-
try {
|
|
722
|
-
await driver.close(narrator);
|
|
723
|
-
}
|
|
724
|
-
catch { /* cosmetic */ }
|
|
725
|
-
}
|
|
726
718
|
releaseRunLock(repoRoot);
|
|
727
719
|
}
|
|
728
720
|
}
|
package/dist/run/reconcile.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"tickmarkr": "dist/cli/index.js",
|
|
12
|
-
"
|
|
13
|
-
"drover": "dist/cli/index.js"
|
|
12
|
+
"tkr": "dist/cli/index.js"
|
|
14
13
|
},
|
|
15
14
|
"files": [
|
|
16
15
|
"dist",
|
|
17
|
-
"schema"
|
|
16
|
+
"schema",
|
|
17
|
+
"skills"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tickmarkr-auto
|
|
3
|
+
description: 'Run a repository’s requested specs autonomously with tickmarkr. Triggers: "/tickmarkr-auto", "run these specs with tickmarkr", "autonomous tickmarkr run".'
|
|
4
|
+
argument-hint: "[spec-or-directory ...]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# tickmarkr-auto — run repository specs autonomously
|
|
8
|
+
|
|
9
|
+
Use this to execute a requested sequence of repository specs. It is SDD-agnostic: each target can be any file or directory accepted by `tickmarkr compile`.
|
|
10
|
+
|
|
11
|
+
## Two-tier by default — role check before the loop
|
|
12
|
+
|
|
13
|
+
When working in a multi-agent terminal environment, decide your role before starting:
|
|
14
|
+
|
|
15
|
+
- **Orchestrator:** your session was started to execute the mission. Run the loop below.
|
|
16
|
+
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified send, then supervise it.
|
|
17
|
+
- **Primary session without an orchestrator:** create one child orchestration session, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself.
|
|
18
|
+
|
|
19
|
+
Outside a multi-agent terminal environment, run the loop directly.
|
|
20
|
+
|
|
21
|
+
## Invariants
|
|
22
|
+
|
|
23
|
+
- Never run two tickmarkr runs in the same repository concurrently.
|
|
24
|
+
- Never let tickmarkr merge work to the main branch. New work consolidates on `tickmarkr/<runId>`.
|
|
25
|
+
- Do not edit a generated graph to force an outcome; correct the source spec and compile again.
|
|
26
|
+
- Gates independently verify evidence, scope, acceptance criteria, and review. A worker's completion claim is not evidence.
|
|
27
|
+
- Treat missing or unparseable machine results and verdicts as failures. Never bypass a failed gate or merge partial work without a human decision.
|
|
28
|
+
|
|
29
|
+
## Act by default
|
|
30
|
+
|
|
31
|
+
Run every requested target in order without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, a designed human gate, or a failed run that needs a human decision. Diagnose from the journal and evidence first; self-release and resume only after fixing and verifying a harness defect.
|
|
32
|
+
|
|
33
|
+
## Per-spec loop
|
|
34
|
+
|
|
35
|
+
1. **Prepare** — confirm the target list, check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
36
|
+
2. **Compile** — run `tickmarkr compile <spec-or-directory>`. Fix source-spec defects instead of editing the generated graph.
|
|
37
|
+
3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
|
|
38
|
+
4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than polling agents. Resolve blocked interactions in the relevant agent session.
|
|
39
|
+
5. **Verify and consolidate** — continue only after a green run. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
|
|
40
|
+
6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
|
|
41
|
+
7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tickmarkr-loop
|
|
3
|
+
description: 'Run one repository spec autonomously with tickmarkr. Triggers: "/tickmarkr-loop", "run this spec with tickmarkr", "tickmarkr the spec".'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# tickmarkr-loop — run one spec autonomously
|
|
7
|
+
|
|
8
|
+
Use this for any spec that `tickmarkr compile` accepts. It is SDD-agnostic: use the repository's requested spec format and keep the execution record beside that source spec.
|
|
9
|
+
|
|
10
|
+
## Two-tier by default — role check before the loop
|
|
11
|
+
|
|
12
|
+
When working in a multi-agent terminal environment, decide your role before starting:
|
|
13
|
+
|
|
14
|
+
- **Orchestrator:** your session was started to execute the mission. Run the loop below.
|
|
15
|
+
- **Supervisor with a live orchestrator:** do not start a second run. Relay the mission to the existing orchestrator with a verified send, then supervise it.
|
|
16
|
+
- **Primary session without an orchestrator:** create one child orchestration session, give it the mission and these rules verbatim, then supervise it. Do not drive a duplicate single-tier run yourself.
|
|
17
|
+
|
|
18
|
+
Outside a multi-agent terminal environment, run the loop directly.
|
|
19
|
+
|
|
20
|
+
## Invariants
|
|
21
|
+
|
|
22
|
+
- Never run two tickmarkr runs in the same repository concurrently.
|
|
23
|
+
- Never let tickmarkr merge work to the main branch. New work consolidates on `tickmarkr/<runId>`.
|
|
24
|
+
- Do not edit the compiled graph to force an outcome; fix the source spec and compile again.
|
|
25
|
+
- Gates verify commits, diffs, acceptance criteria, and reviews independently. Never trust a worker's claim that work is complete.
|
|
26
|
+
- Treat missing or unparseable machine results and verdicts as failures. Do not release, resume, or merge around failed gates.
|
|
27
|
+
|
|
28
|
+
## Act by default
|
|
29
|
+
|
|
30
|
+
Proceed through the loop without seeking routine confirmation. Stop only for a blocked agent interaction, a genuinely unresolved stalled task, or a designed human gate. Diagnose from the journal and available evidence before escalating; if a harness defect is fixed and verified, resume the run.
|
|
31
|
+
|
|
32
|
+
## The loop
|
|
33
|
+
|
|
34
|
+
1. **Prepare** — start from the requested spec. Check `git status`, confirm no tickmarkr run is active, and work from a non-main branch.
|
|
35
|
+
2. **Compile** — run `tickmarkr compile <spec>`. Correct compilation errors in the spec, never in the generated graph.
|
|
36
|
+
3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
|
|
37
|
+
4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal event rather than repeatedly polling agents. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
|
|
38
|
+
5. **Verify and consolidate** — accept only a green run. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
|
|
39
|
+
6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
|