Sed-pipeline (16 patterns, longest-match-first) sweeper residuelle ultra*-treff i prose, command-narrativ, agent-prompts, hook-kommentarer, doc-prosa. Pipeline-utvidelser fra V4-prompten: - BSD-syntax [[:<:]]ultra[[:>:]] istedenfor \bultra\b (BSD sed mangler \b) - 6 compound-patterns for ultraplan/ultraexecute/ultraresearch/ultrabrief/ ultrareview/ultracontinue uten -local-suffiks - ultra*-stats glob -> trek*-stats glob - Linje-eksklusjon redusert til ultra-cc-architect (Q8); session-state- eksklusjonen var over-protektiv - File-eksklusjon utvidet til settings.json, package.json, plugin.json, hele .claude/-treet (gitignored + V5-territorium) Q8-undantak holdt: architecture-discovery.mjs + project-discovery.mjs urort. Filnavn-konvensjon holdt: .session-state.local.json + *.local.* preservert. Manuell narrative-fix: tests/lib/agent-frontmatter.test.mjs linje 10 mangled "/ultra*-local" til "/voyage*-local" (ingen slik kommando finnes); korrigert til "/trek*". Residualer utenfor scope (V5 handterer): package.json + .claude-plugin/ plugin.json (Step 12-14 versjons-bump). .claude/* er gitignored spec-historikk med tilsiktet BEFORE/AFTER-narrativ. Part of voyage-rebrand session 3 (Wave 4 / Step 10). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
129 lines
4.3 KiB
JavaScript
129 lines
4.3 KiB
JavaScript
// lib/util/autonomy-gate.mjs
|
|
// Autonomy-gate state machine for /trekexecute + /trekplan
|
|
// (plan-v2 Step 4 — drives the --gates flag).
|
|
//
|
|
// States:
|
|
// idle — not yet started
|
|
// gates_on — gates enabled, between phases
|
|
// auto_running — running phases continuously without pausing
|
|
// paused_for_gate — stopped at a phase boundary; awaiting `resume`
|
|
// completed — terminal
|
|
//
|
|
// Events:
|
|
// start — begin a run (gates flag chooses route)
|
|
// phase_boundary — a phase finished
|
|
// resume — operator confirmed; leave the gate
|
|
// finish — pipeline reached its end
|
|
//
|
|
// CLI shim:
|
|
// node lib/util/autonomy-gate.mjs --state X --event Y [--gates true|false]
|
|
// → JSON: { ok: true, next_state: "..." } (success)
|
|
// → JSON: { ok: false, error: "..." } (invalid transition; exit 1)
|
|
//
|
|
// Pure data; no I/O. Re-entry to `completed` is idempotent.
|
|
|
|
export const STATES = Object.freeze({
|
|
IDLE: 'idle',
|
|
GATES_ON: 'gates_on',
|
|
AUTO_RUNNING: 'auto_running',
|
|
PAUSED_FOR_GATE: 'paused_for_gate',
|
|
COMPLETED: 'completed',
|
|
});
|
|
|
|
export const EVENTS = Object.freeze({
|
|
START: 'start',
|
|
PHASE_BOUNDARY: 'phase_boundary',
|
|
RESUME: 'resume',
|
|
FINISH: 'finish',
|
|
});
|
|
|
|
const STATE_SET = new Set(Object.values(STATES));
|
|
const EVENT_SET = new Set(Object.values(EVENTS));
|
|
|
|
/**
|
|
* Compute the next state given the current state, event, and (optional)
|
|
* gates-flag intent (only consulted on `start` from `idle`).
|
|
*
|
|
* @param {string} state
|
|
* @param {string} event
|
|
* @param {{ gates?: boolean }} [opts]
|
|
* @returns {{ ok: true, next_state: string } | { ok: false, error: string }}
|
|
*/
|
|
export function transition(state, event, opts = {}) {
|
|
if (!STATE_SET.has(state)) {
|
|
return { ok: false, error: `unknown state: ${state}` };
|
|
}
|
|
if (!EVENT_SET.has(event)) {
|
|
return { ok: false, error: `unknown event: ${event}` };
|
|
}
|
|
|
|
// completed is terminal & idempotent
|
|
if (state === STATES.COMPLETED) {
|
|
return { ok: true, next_state: STATES.COMPLETED };
|
|
}
|
|
|
|
if (state === STATES.IDLE) {
|
|
if (event === EVENTS.START) {
|
|
const gates = opts.gates === true;
|
|
return { ok: true, next_state: gates ? STATES.GATES_ON : STATES.AUTO_RUNNING };
|
|
}
|
|
return { ok: false, error: `invalid transition: idle + ${event} (only \`start\` allowed from idle)` };
|
|
}
|
|
|
|
if (state === STATES.GATES_ON) {
|
|
if (event === EVENTS.PHASE_BOUNDARY) return { ok: true, next_state: STATES.PAUSED_FOR_GATE };
|
|
if (event === EVENTS.FINISH) return { ok: true, next_state: STATES.COMPLETED };
|
|
return { ok: false, error: `invalid transition: gates_on + ${event}` };
|
|
}
|
|
|
|
if (state === STATES.AUTO_RUNNING) {
|
|
if (event === EVENTS.PHASE_BOUNDARY) return { ok: true, next_state: STATES.AUTO_RUNNING };
|
|
if (event === EVENTS.FINISH) return { ok: true, next_state: STATES.COMPLETED };
|
|
return { ok: false, error: `invalid transition: auto_running + ${event}` };
|
|
}
|
|
|
|
if (state === STATES.PAUSED_FOR_GATE) {
|
|
if (event === EVENTS.RESUME) return { ok: true, next_state: STATES.GATES_ON };
|
|
if (event === EVENTS.FINISH) return { ok: true, next_state: STATES.COMPLETED };
|
|
return { ok: false, error: `invalid transition: paused_for_gate + ${event}` };
|
|
}
|
|
|
|
return { ok: false, error: `unhandled state: ${state}` };
|
|
}
|
|
|
|
/**
|
|
* Convenience: is this state terminal?
|
|
*/
|
|
export function isTerminal(state) {
|
|
return state === STATES.COMPLETED;
|
|
}
|
|
|
|
// ---- CLI shim ----------------------------------------------------------------
|
|
|
|
function parseArgs(argv) {
|
|
const out = {};
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === '--state') out.state = argv[++i];
|
|
else if (a === '--event') out.event = argv[++i];
|
|
else if (a === '--gates') {
|
|
const v = argv[++i];
|
|
out.gates = v === 'true';
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (!args.state || !args.event) {
|
|
process.stdout.write(JSON.stringify({
|
|
ok: false,
|
|
error: 'usage: autonomy-gate.mjs --state <state> --event <event> [--gates true|false]',
|
|
}) + '\n');
|
|
process.exit(1);
|
|
}
|
|
const result = transition(args.state, args.event, { gates: args.gates });
|
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
process.exit(result.ok ? 0 : 1);
|
|
}
|