Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions models.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
"gate": "none"
},
"frontier": {
"match": ["*opus*", "*fable*", "*gpt-5*", "*gemini-*-pro*"],
"match": ["*opus*", "*sonnet*", "*fable*", "*gpt-5*", "*gemini-*-pro*"],
"gate": "escalation-token"
}
},
"models": {
"lmstudio/qwen3.6-35b-a3b": { "max_output_real": 10000 }
},
"roles": {
"coder": "github-copilot/claude-haiku-4.5",
"reviewer": "github-copilot/claude-sonnet-5",
"coder": "github-copilot/claude-sonnet-5",
"reviewer": "openai/gpt-5.6-terra",
"challenger": "github-copilot/gpt-5.4",
"advisor": "github-copilot/claude-sonnet-5"
}
Expand Down
5 changes: 3 additions & 2 deletions scripts/conductor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,9 @@ immediately before that spawn (same pattern as `scripts/opencode-local` and
Four changes, each from a receipt in a real end-to-end run (two tickets,
OpenAI terra/luna, full code → dual review → runtime → merge chain):

- **Single-conductor lock.** `.git/conductor.lock` (runtime dir when the
board is file-backed). A second conductor on the same root exits 4 —
- **Single-conductor lock.** `<git-common-dir>/conductor.lock` (runtime dir
outside Git repositories), shared by the repository's linked worktrees. A
second conductor on the same repository exits 4 —
observed live: a supervised run and an orphaned detached run interleaved,
one released a ticket while the other's rounds went green, and the green
was never landed. Stale locks from dead pids clear themselves.
Expand Down
27 changes: 18 additions & 9 deletions scripts/conductor/conductor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -259,13 +259,20 @@ const STOPFILE = resolve(ROOT, 'STOP');
// a supervised run and an orphaned detached run interleaved on one board;
// one released a ticket mid-flight while the other's rounds went green, and
// the green was never landed. Two conductors on one ROOT is never legal.
// The lock lives in .git/ (or the runtime dir when file-backed boards have
// no .git) — a lock in the worktree dirties the target and trips the
// conductor's own clean-tree gate, which is how the first draft of this
// lock was caught by the test suite.
const LOCKFILE = existsSync(resolve(ROOT, '.git'))
? resolve(ROOT, '.git', 'conductor.lock')
: resolve(RUNTIME_DIR, 'conductor.lock');
// The lock lives in Git's common directory so every linked worktree for the
// repository shares one lock. In a linked worktree `.git` is a file, not a
// directory, so appending `conductor.lock` to ROOT/.git fails with ENOTDIR.
const LOCKFILE = (() => {
try {
const commonDir = execFileSync('git', ['rev-parse', '--git-common-dir'], {
cwd: ROOT,
encoding: 'utf8',
}).trim();
return resolve(ROOT, commonDir, 'conductor.lock');
} catch {
return resolve(RUNTIME_DIR, 'conductor.lock');
}
})();
function acquireRunLock() {
if (existsSync(LOCKFILE)) {
const pid = Number(readFileSync(LOCKFILE, 'utf8').trim() || '0');
Expand Down Expand Up @@ -1483,7 +1490,8 @@ async function main() {
// (after a human looks at the gap history) is free to retry.
const skippedThisRun = new Set();
const landedThisRun = new Set();
while (landed < MAX_TICKETS) {
let processed = landed;
while (processed < MAX_TICKETS) {
if (existsSync(STOPFILE)) { log('conductor.stop', { msg: 'STOP file present' }); break; }

let plan = loadFreshPlan();
Expand All @@ -1509,6 +1517,7 @@ async function main() {
persistPlan(plan, `chore(${next.id}): conductor claims ticket`);

log('ticket.start', { ticket: next.id, msg: next.title });
processed++;
const res = await executeTicket(plan, next);
if (res.ok) {
const landedOk = land(plan, next, res.branch, res.wt);
Expand All @@ -1533,7 +1542,7 @@ async function main() {

const finalPlan = loadFreshPlan();
const counts = tallyStatuses(finalPlan);
log('conductor.end', { msg: `landed=${landed} board=${JSON.stringify(counts)}` });
log('conductor.end', { msg: `processed=${processed} landed=${landed} board=${JSON.stringify(counts)}` });
}

main().catch((e) => { log('conductor.fatal', { msg: e.message }); process.exit(1); });
42 changes: 39 additions & 3 deletions scripts/conductor/conductor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,26 @@ test('conductor.mjs: 3-ticket fixture lands 2, releases the gate-failing one, ne
}
});

test('conductor.mjs: shares its lock through the common Git directory when root is a linked worktree', { timeout: 180_000 }, () => {
const { base, target, stub } = setupFixture();
const linkedRoot = resolve(base, 'linked-target');
try {
sh('git', ['checkout', '--detach', '-q'], { cwd: target });
sh('git', ['worktree', 'add', '-q', linkedRoot, 'main'], { cwd: target });

sh('node', [CONDUCTOR, '--root', linkedRoot, '--rounds', '1', '--actor', 'conductor', '--reviewer-actor', 'conductor-review', '--max-attempts', '1', '--max-tickets', '1', '--no-push'], {
cwd: linkedRoot,
env: { ...process.env, OPENCODE_BIN: stub },
});

const commonDir = sh('git', ['rev-parse', '--git-common-dir'], { cwd: linkedRoot }).trim();
assert.equal(existsSync(resolve(linkedRoot, commonDir, 'conductor.lock')), false,
'the process exit handler should remove the shared lock from the common Git directory');
} finally {
rmSync(base, { recursive: true, force: true });
}
});

test('attempt outcome reports the terminal cause before historical failures', () => {
const attempts = [
['formatting failed in changed source'],
Expand Down Expand Up @@ -283,6 +303,20 @@ test('conductor.mjs: red configured baseline refuses before claim and consumes z
test('conductor.mjs: provider failure blocks without exhausting the feature retry budget', { timeout: 60_000 }, () => {
const { base, target, stub } = setupRoleRoutingFixture();
try {
const planPath = resolve(target, 'plan.json');
const plan = JSON.parse(readFileSync(planPath, 'utf8'));
plan.modules.push({
...plan.modules[0],
id: 'TICK-SECOND',
title: 'Must remain unclaimed',
write_scope: ['b/**'],
verify: plan.modules[0].verify.replaceAll('TICK-ROLE', 'TICK-SECOND').replace('--scope a', '--scope b'),
manifest: 'docs/reviews/MANIFEST_TICK-SECOND.md',
});
writeFileSync(planPath, JSON.stringify(plan, null, 2) + '\n');
sh('git', ['add', 'plan.json'], { cwd: target });
sh('git', ['commit', '-q', '-m', 'add second ready ticket'], { cwd: target });

writeFileSync(stub, `#!/usr/bin/env bash
if [[ "\${1:-}" == "models" ]]; then
printf '%s\\n' fixture/coder-model fixture/reviewer-model
Expand All @@ -293,19 +327,21 @@ exit 9
`);
chmodSync(stub, 0o755);

sh('node', [CONDUCTOR, '--root', target, '--rounds', '1', '--max-attempts', '2', '--no-push'], {
sh('node', [CONDUCTOR, '--root', target, '--rounds', '1', '--max-attempts', '2', '--max-tickets', '1', '--no-push'], {
cwd: target,
env: { ...process.env, OPENCODE_BIN: stub },
});

const plan = JSON.parse(readFileSync(resolve(target, 'plan.json'), 'utf8'));
assert.equal(plan.modules[0].status, 'ready', 'provider failure releases the ticket');
const finalPlan = JSON.parse(readFileSync(planPath, 'utf8'));
assert.equal(finalPlan.modules[0].status, 'ready', 'provider failure releases the ticket');
assert.equal(finalPlan.modules[1].status, 'ready', 'the bounded run must not claim a second ticket');
const rows = readFileSync(resolve(target, 'docs/work/conductor-log.jsonl'), 'utf8')
.trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
assert.equal(rows.filter((r) => r.kind === 'ticket.attempt').length, 1,
'a provider failure must not start a second feature coding attempt');
assert.ok(rows.some((r) => r.kind === 'ticket.blocked' && r.category === 'coder-session'));
assert.equal(rows.some((r) => r.kind === 'ticket.exhausted'), false);
assert.deepEqual(rows.filter((r) => r.kind === 'ticket.start').map((r) => r.ticket), ['TICK-ROLE']);
} finally {
rmSync(base, { recursive: true, force: true });
}
Expand Down
5 changes: 3 additions & 2 deletions scripts/lib/model-tiers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
// (unknown tier), not an error -- an unrecognized model id is exactly the
// "any raw pin outside models.json" case the G3 lint warns on, not a crash.

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
import { join, extname, relative } from 'node:path';
import { pathToFileURL } from 'node:url';

export function globToRegExp(glob) {
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
Expand Down Expand Up @@ -168,6 +169,6 @@ function main() {
process.exitCode = 2;
}

if (import.meta.url === `file://${process.argv[1]}`) {
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
main();
}
8 changes: 7 additions & 1 deletion scripts/test-autopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* DRIVE / HEAL / EXIT), the story-denominated assessment ("a drained
* board proves nothing"), done-is-a-predicate, the heal ladder in order
* (narrowed retry → split → tier escalation → park with evidence), the
* byte-identical no-progress HALT, the mandatory iteration cap using
* JIRA-authoritative board assessment, byte-identical no-progress HALT,
* the mandatory iteration cap using
* existing tier ceilings (6 metered / 12 local), and that it DRIVES the
* existing machinery by path instead of inventing new loops.
* 2. It is REGISTERED everywhere its siblings (/goal, /wave) are:
Expand Down Expand Up @@ -77,6 +78,11 @@ const REQUIRED: Array<[string, RegExp]> = [
// exit shape
["assembly-gate-style exit predicate", /assembly-gate-style predicate/i],
["halt leaves stuck evidence", /AUTOPILOT_HALT\.md/],
// live JIRA projects do not mirror lifecycle state into plan.json
["JIRA board mode is authoritative", /CONDUCTOR_BOARD=jira[\s\S]{0,200}JIRA is authoritative/i],
["JIRA assessment uses live target commands", /jira\.sh stats[\s\S]{0,200}jira\.sh ready/i],
["empty plan is allowed in JIRA mode", /empty `docs\/work\/plan\.json` is not a blocker in JIRA mode/i],
["JIRA drive preserves board authority", /never substitute or update[\s\S]{0,80}`plan\.json` lifecycle state/i],
];

export async function testAutopilot(
Expand Down
23 changes: 23 additions & 0 deletions scripts/test-model-tier-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function testModelTierLint(
],
["cheap — *haiku*", "anthropic/claude-haiku-4-5", "cheap"],
["frontier — *opus*", "anthropic/claude-opus-4-8", "frontier"],
["frontier — *sonnet*", "github-copilot/claude-sonnet-5", "frontier"],
["frontier — *fable*", "claude-fable-5", "frontier"],
["frontier — *gpt-5*", "openai/gpt-5", "frontier"],
["frontier — *gemini-*-pro*", "google/gemini-2.5-pro", "frontier"],
Expand Down Expand Up @@ -81,6 +82,28 @@ export async function testModelTierLint(
ok("model-tiers — max_output_real lookup, unknown model -> null");
else fail("model-tiers — max_output_real unknown model", "expected null");

// The CLI entry guard must survive symlinked paths such as macOS /tmp ->
// /private/tmp; otherwise verification exits 0 without running main().
const cliFixture = fs.mkdtempSync(path.join(fs.realpathSync(root), ".tmp-model-tier-cli-"));
try {
const cliAlias = path.join(cliFixture, "model-tiers.mjs");
fs.symlinkSync(path.join(root, "scripts/lib/model-tiers.mjs"), cliAlias);
const result = spawnSync(
process.execPath,
[cliAlias, "resolve", "github-copilot/claude-sonnet-5", path.join(root, "models.json")],
{ encoding: "utf8" },
);
if (result.status === 0 && result.stdout.trim() === "frontier")
ok("model-tiers — CLI runs through a symlinked script path");
else
fail(
"model-tiers — CLI symlink path",
`exit=${result.status}; stdout=${JSON.stringify(result.stdout)}; stderr=${JSON.stringify(result.stderr)}`,
);
} finally {
fs.rmSync(cliFixture, { recursive: true, force: true });
}

// -- 2. G3 config-pin lint: planted fixtures via the real CLI ------------
const scriptPath = path.join(
root,
Expand Down
5 changes: 3 additions & 2 deletions scripts/test-session-model-receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function testSessionModelReceipt(
["local — lmstudio/*", "lmstudio/qwen3.6-35b-a3b", "local"],
["cheap — *haiku*", "anthropic/claude-haiku-4-5", "cheap"],
["frontier — *opus*", "anthropic/claude-opus-4-8", "frontier"],
["frontier — *sonnet*", "github-copilot/claude-sonnet-5", "frontier"],
["no match — unknown model", "unknown/foo-bar", null],
];
for (const [label, modelId, expected] of cases) {
Expand Down Expand Up @@ -112,8 +113,8 @@ export async function testSessionModelReceipt(
logSessionReceipt(dir, {
sessionID: "sess-unclassified-1",
mode: "build",
providerID: "anthropic",
modelID: "claude-sonnet-5",
providerID: "unknown",
modelID: "foo-bar",
time: { created: 1752400000000 },
});
const rows = readReceipts(dir);
Expand Down
37 changes: 23 additions & 14 deletions skills/autopilot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@ Collect each of these from its existing source; every line of the report
cites the command output behind it. A source that does not exist in the
target repo is recorded as ABSENT, never guessed at:

1. **Board state** — `docs/work/plan.json` tickets by status
(ready / claimed / in_progress / in_review / blocked / parked / done),
via `scripts/validators/validate-tickets.sh` +
`scripts/lib/tickets.mjs`. Parked and blocked are listed by name — a
park is not a landing.
1. **Board state** — select the same board driver DRIVE will use:
- `CONDUCTOR_BOARD=jira`: JIRA is authoritative. Run the target repo's
`./scripts/jira.sh stats`, `./scripts/jira.sh mine`, and
`./scripts/jira.sh ready`, then `./scripts/jira.sh blockers <key>` for
each candidate under consideration. Read `docs/work/ticket-scope-map.json`
for module contracts only; it does not own lifecycle state. An absent or
empty `docs/work/plan.json` is not a blocker in JIRA mode.
- Otherwise, read `docs/work/plan.json` tickets by status (ready / claimed /
in_progress / in_review / blocked / parked / done), via
`scripts/validators/validate-tickets.sh` + `scripts/lib/tickets.mjs`.
Parked and blocked work is listed by name — a park is not a landing.
2. **Phase gates** — `scripts/validators/validate-phase-gate.sh <phase>`
(read-only check) for the current phase per `docs/work/STATE.md`;
receipts at `docs/work/gates/`.
Expand Down Expand Up @@ -80,15 +86,18 @@ do not traverse symlinks and produced a false "loops absent" halt in the
field. Loops truly absent in all three → that IS the deterministic blocker;
say so and halt. Never reimplement them.

- **Ticket board** (`docs/work/plan.json`): `scripts/conductor/conductor.mjs`
under `scripts/conductor/supervise.sh` — claim → isolated worktree →
outside gates (`scripts/validators/run-handoff-gates.sh`) → distinct
reviewer → merge. `STOP` file semantics and `--max-attempts` apply as
documented in `scripts/conductor/README.md`. The conductor holds a
`.conductor.lock` — a second conductor on the same root refuses (exit 4);
never delete a live lock, and WAIT for a spawned supervise.sh to exit
rather than ending your session over it (a killed parent orphans the
claim, and the next run's reconcile makes a human clean it up).
- **Ticket board**: `scripts/conductor/conductor.mjs` under
`scripts/conductor/supervise.sh` — claim → isolated worktree → outside gates
(`scripts/validators/run-handoff-gates.sh`) → distinct reviewer → merge.
With `CONDUCTOR_BOARD=jira`, the conductor uses the target repo's JIRA board
driver and `docs/work/ticket-scope-map.json`; never substitute or update
`plan.json` lifecycle state. Otherwise it uses `docs/work/plan.json`.
`STOP` file semantics and `--max-attempts` apply as documented in
`scripts/conductor/README.md`. The conductor holds a `.conductor.lock` — a
second conductor on the same root refuses (exit 4); never delete a live
lock, and WAIT for a spawned supervise.sh to exit rather than ending your
session over it (a killed parent orphans the claim, and the next run's
reconcile makes a human clean it up).
- **SDLC phase work**: `scripts/run-until-done.sh` (resume from STATE.md,
watchdog + stall detection, `<promise>COMPLETE</promise>` verified by
validators, never trusted).
Expand Down
Loading