refactor(gates): the autonomy gate is the --gates flag parser
lib/util/autonomy-gate.mjs keeps only gatesValue + parseGatesArgs and the
CLI ({"ok":true,"gates":true|false}; exact usage error on a bad value;
the old --state/--event arguments are a usage error). The four commands
run 'autonomy-gate.mjs --gates [value]' and read gates. Prose (README,
operations, architecture, sdlc-playbook-gap) and the S24, gates-flag
and [#1] tests follow (R9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
651be3468b
commit
308b149e70
12 changed files with 165 additions and 355 deletions
|
|
@ -1,160 +1,71 @@
|
|||
// lib/util/autonomy-gate.mjs
|
||||
// Autonomy-gate state machine for /trekexecute + /trekplan
|
||||
// (plan-v2 Step 4 — drives the --gates flag).
|
||||
// The --gates flag parser for /trekbrief, /trekresearch, /trekplan and
|
||||
// /trekexecute: maps the flag's value to gates on or off.
|
||||
//
|
||||
// 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
|
||||
// CLI:
|
||||
// node lib/util/autonomy-gate.mjs [--gates [true|false|closed|open]]
|
||||
// (bare --gates = on; true|closed = on, false|open = off; no flag = off.
|
||||
// Any other value is a usage error, exit 1; case-sensitive. One token
|
||||
// --gates=<value> is read as --gates <value>; an empty value is the same
|
||||
// usage error. Any other argument is a usage error.)
|
||||
// → JSON: { ok: true, gates: true|false } (exit 0)
|
||||
// → JSON: { ok: false, error: "usage: ..." } (exit 1)
|
||||
//
|
||||
// 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|closed|open]]
|
||||
// (bare --gates = on; true|closed = on, false|open = off; any other value
|
||||
// is a usage error, exit 1; case-sensitive. One token --gates=<value> is
|
||||
// read as --gates <value>; an empty value is the same usage error)
|
||||
// → 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.
|
||||
// Pure data; no I/O beyond the CLI's stdout.
|
||||
|
||||
import { isMain } from './is-main.mjs';
|
||||
|
||||
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`).
|
||||
* A --gates value: true|closed = on, false|open = off, else undefined.
|
||||
*
|
||||
* @param {string} state
|
||||
* @param {string} event
|
||||
* @param {{ gates?: boolean }} [opts]
|
||||
* @returns {{ ok: true, next_state: string } | { ok: false, error: string }}
|
||||
* @param {string|undefined} v
|
||||
* @returns {boolean|undefined}
|
||||
*/
|
||||
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 ----------------------------------------------------------------
|
||||
|
||||
/** A --gates value: true|closed = on, false|open = off, else undefined. */
|
||||
function gatesValue(v) {
|
||||
export function gatesValue(v) {
|
||||
if (v === 'true' || v === 'closed') return true;
|
||||
if (v === 'false' || v === 'open') return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const GATES_USAGE = 'usage: --gates takes true, false, closed or open';
|
||||
const USAGE = 'usage: autonomy-gate.mjs [--gates [true|false|closed|open]]';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {};
|
||||
const setGates = (v) => {
|
||||
const g = gatesValue(v);
|
||||
if (g === undefined) out.error = GATES_USAGE;
|
||||
else out.gates = g;
|
||||
};
|
||||
/**
|
||||
* Parse the CLI arguments.
|
||||
*
|
||||
* @param {string[]} argv
|
||||
* @returns {{ ok: true, gates: boolean } | { ok: false, error: string }}
|
||||
*/
|
||||
export function parseGatesArgs(argv) {
|
||||
let gates = false;
|
||||
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.startsWith('--gates=')) {
|
||||
let v;
|
||||
if (a.startsWith('--gates=')) {
|
||||
// The one-token form, read as `--gates <value>`; an empty value is no value.
|
||||
setGates(a.slice('--gates='.length));
|
||||
v = a.slice('--gates='.length);
|
||||
} else if (a === '--gates') {
|
||||
// Bare `--gates` (last, or followed by another flag) means on and
|
||||
// consumes nothing. A value is case-sensitive: true|closed = on,
|
||||
// false|open = off; anything else is a usage error.
|
||||
const v = argv[i + 1];
|
||||
if (v === undefined || v.startsWith('--')) {
|
||||
out.gates = true;
|
||||
} else {
|
||||
setGates(v);
|
||||
i++;
|
||||
// consumes nothing.
|
||||
const next = argv[i + 1];
|
||||
if (next === undefined || next.startsWith('--')) {
|
||||
gates = true;
|
||||
continue;
|
||||
}
|
||||
v = next;
|
||||
i++;
|
||||
} else {
|
||||
return { ok: false, error: USAGE };
|
||||
}
|
||||
const g = gatesValue(v);
|
||||
if (g === undefined) return { ok: false, error: GATES_USAGE };
|
||||
gates = g;
|
||||
}
|
||||
return out;
|
||||
return { ok: true, gates };
|
||||
}
|
||||
|
||||
if (isMain(import.meta.url)) {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.error || !args.state || !args.event) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
ok: false,
|
||||
error: args.error
|
||||
|| 'usage: autonomy-gate.mjs --state <state> --event <event> [--gates [true|false|closed|open]]',
|
||||
}) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
const result = transition(args.state, args.event, { gates: args.gates });
|
||||
const result = parseGatesArgs(process.argv.slice(2));
|
||||
process.stdout.write(JSON.stringify(result) + '\n');
|
||||
process.exit(result.ok ? 0 : 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue