Publish reviewed development agents and WUI draft with recovery evidence (#1497)
This commit is contained in:
@@ -5,7 +5,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$REPO"
|
||||
|
||||
usage() {
|
||||
echo 'Usage: scripts/agent.sh --host-dev AGENT [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE]'
|
||||
echo 'Usage: scripts/agent.sh --host-dev AGENT [--fresh] [--check] [--soul FILE] [--constitution FILE] [--user FILE] [--provider NAME] [--model ID] [--thinking LEVEL]'
|
||||
echo 'Default: resume the latest agent development conversation; first launch starts one.'
|
||||
}
|
||||
fail() { echo "${AGENT:-tui}: $*" >&2; exit 1; }
|
||||
@@ -20,12 +20,23 @@ shift
|
||||
SOUL="$REPO/agents/$AGENT/SOUL.md"
|
||||
CONSTITUTION="$REPO/contracts/CONSTITUTION.md"
|
||||
USER_FILE=""
|
||||
PROVIDER=""
|
||||
MODEL=""
|
||||
THINKING=""
|
||||
FRESH=false
|
||||
CHECK=false
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--fresh) FRESH=true; shift ;;
|
||||
--check) CHECK=true; shift ;;
|
||||
--provider|--model|--thinking)
|
||||
[ "$#" -ge 2 ] && [ -n "$2" ] && [[ "$2" != --* ]] || fail "$1 needs a value"
|
||||
case "$1" in
|
||||
--provider) PROVIDER="$2" ;;
|
||||
--model) MODEL="$2" ;;
|
||||
--thinking) THINKING="$2" ;;
|
||||
esac
|
||||
shift 2 ;;
|
||||
--soul|--constitution|--user)
|
||||
[ "$#" -ge 2 ] && [ -n "$2" ] || fail "$1 needs a file"
|
||||
case "$1" in
|
||||
@@ -43,6 +54,15 @@ done
|
||||
source "$REPO/scripts/common.sh"
|
||||
load_config
|
||||
[ "$MOSAIC_ADAPTER" = pi ] || fail 'native launcher requires the pi adapter'
|
||||
PROVIDER="${PROVIDER:-$MOSAIC_PROVIDER}"
|
||||
MODEL="${MODEL:-$MOSAIC_MODEL}"
|
||||
THINKING_ARGS=()
|
||||
if [ -n "$THINKING" ]; then
|
||||
case "$THINKING" in
|
||||
off|minimal|low|medium|high|xhigh|max) THINKING_ARGS=(--thinking "$THINKING") ;;
|
||||
*) fail "invalid thinking level: $THINKING" ;;
|
||||
esac
|
||||
fi
|
||||
USER_FILE="${USER_FILE:-$MOSAIC_DATA_ROOT/user/USER.md}"
|
||||
PI="$REPO/node_modules/.bin/pi"
|
||||
[ -x "$PI" ] || fail 'run npm ci --ignore-scripts --no-audit --no-fund in the repository first'
|
||||
@@ -62,7 +82,7 @@ for skill in "${SKILLS[@]}"; do
|
||||
done
|
||||
[ -s "$REPO/extensions/goal/index.ts" ] || fail 'missing canonical goal extension'
|
||||
STATE="$REPO/.pi/state/$AGENT"
|
||||
echo "$AGENT: native host TUI | Pi $PIN | $MOSAIC_PROVIDER/$MOSAIC_MODEL"
|
||||
echo "$AGENT: native host TUI | Pi $PIN | $PROVIDER/$MODEL${THINKING:+:$THINKING}"
|
||||
echo "$AGENT: workspace $REPO"
|
||||
echo "$AGENT: SOUL=$SOUL | CONSTITUTION=$CONSTITUTION | USER=$USER_FILE"
|
||||
echo "$AGENT: sessions $STATE/sessions | skills: ${SKILLS[*]}"
|
||||
@@ -116,5 +136,5 @@ exec "$PI" --approve --offline --no-context-files --no-extensions --no-skills \
|
||||
--extension "$REPO/.pi/extensions/goal/index.ts" \
|
||||
"${SKILL_ARGS[@]}" \
|
||||
--tools read,bash,edit,write,grep,find,ls,goal_report \
|
||||
--provider "$MOSAIC_PROVIDER" --model "$MOSAIC_MODEL" \
|
||||
--provider "$PROVIDER" --model "$MODEL" "${THINKING_ARGS[@]}" \
|
||||
--append-system-prompt "$PROMPT" "${SESSION_ARGS[@]}"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
# [--session <name>] [--tools <comma,list>] [--auth <account>]
|
||||
# scripts/agent.sh --host-dev <name> [--fresh] [--check]
|
||||
# [--soul <file>] [--constitution <file>] [--user <file>]
|
||||
# [--provider <name>] [--model <id>] [--thinking <level>]
|
||||
#
|
||||
# The agent receives the four immutable contracts (constitution, standards,
|
||||
# SOUL, USER) plus its own identity and optional mission directives as its
|
||||
|
||||
@@ -7,13 +7,20 @@ import { join, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const source = resolve(import.meta.dirname, '..');
|
||||
test('Darkwing launch context, resources, session recovery, and refusals', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'darkwing launch '));
|
||||
for (const agent of ['darkwing', 'dewey', 'filbert']) test(`${agent} launch context, resources, session recovery, and refusals`, () => {
|
||||
const root = mkdtempSync(join(tmpdir(), `${agent} launch `));
|
||||
try {
|
||||
for (const dir of ['agents', 'scripts', 'contracts', 'user', 'node_modules/.bin', 'extensions/goal']) {
|
||||
mkdirSync(join(root, dir), { recursive: true });
|
||||
}
|
||||
cpSync(join(source, 'agents/darkwing'), join(root, 'agents/darkwing'), { recursive: true });
|
||||
// Copy only runtime inputs. Agent work/scratch may be large or actively
|
||||
// authored and is unrelated to isolated launcher verification.
|
||||
mkdirSync(join(root, `agents/${agent}`), { recursive: true });
|
||||
for (const file of ['launch.sh', 'SOUL.md', 'CONTEXT.md', 'validate-sessions.mjs']) {
|
||||
cpSync(join(source, `agents/${agent}`, file), join(root, `agents/${agent}`, file));
|
||||
}
|
||||
assert.equal(existsSync(join(root, `agents/${agent}/work`)), false);
|
||||
assert.equal(existsSync(join(root, `agents/${agent}/scratch`)), false);
|
||||
cpSync(join(source, 'scripts/agent-host-dev.sh'), join(root, 'scripts/agent-host-dev.sh'));
|
||||
cpSync(join(source, 'scripts/agent.sh'), join(root, 'scripts/agent.sh'));
|
||||
cpSync(join(source, 'skills'), join(root, 'skills'), { recursive: true });
|
||||
@@ -27,15 +34,15 @@ test('Darkwing launch context, resources, session recovery, and refusals', () =>
|
||||
writeFileSync(join(root, 'node_modules/.bin/pi'), `#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
if (process.argv[2] === '--version') { console.log('0.84.4'); process.exit(0); }
|
||||
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), incarnation: process.env.MOSAIC_LAUNCH_INCARNATION}));
|
||||
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION}));
|
||||
`, { mode: 0o755 });
|
||||
const launch = (args = '', tty = true) => spawnSync(tty ? 'script' : 'bash', tty
|
||||
? ['-q', '-e', '-c', `bash agents/darkwing/launch.sh ${args}`, '/dev/null']
|
||||
: ['agents/darkwing/launch.sh', ...args.split(' ').filter(Boolean)],
|
||||
? ['-q', '-e', '-c', `bash agents/${agent}/launch.sh ${args}`, '/dev/null']
|
||||
: [`agents/${agent}/launch.sh`, ...args.split(' ').filter(Boolean)],
|
||||
{ cwd: root, env: { ...process.env, MOSAIC_LAUNCH_INCARNATION: 'must-not-inherit' }, encoding: 'utf8', timeout: 10000 });
|
||||
const ok = (result) => assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
ok(launch('--check', false));
|
||||
const direct = spawnSync('bash', [join(root, 'scripts/agent.sh'), '--host-dev', 'darkwing', '--check'],
|
||||
const direct = spawnSync('bash', [join(root, 'scripts/agent.sh'), '--host-dev', agent, '--check'],
|
||||
{ cwd: tmpdir(), encoding: 'utf8', timeout: 10000 });
|
||||
ok(direct);
|
||||
assert.match(direct.stdout, /configuration checks passed/);
|
||||
@@ -44,6 +51,11 @@ fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), in
|
||||
ok(launch());
|
||||
let captured = JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||
assert.equal(captured.incarnation, undefined);
|
||||
assert.equal(captured.agent, agent);
|
||||
assert.equal(captured.args[captured.args.indexOf('--provider') + 1], agent === 'filbert' ? 'openai-codex' : 'test');
|
||||
assert.equal(captured.args[captured.args.indexOf('--model') + 1], agent === 'filbert' ? 'gpt-6-astra' : 'test-model');
|
||||
if (agent === 'filbert') assert.equal(captured.args[captured.args.indexOf('--thinking') + 1], 'low');
|
||||
assert.equal(captured.args[captured.args.indexOf('--session-dir') + 1], join(root, `.pi/state/${agent}/sessions`));
|
||||
assert.equal(captured.args.includes('--continue'), false);
|
||||
assert.ok(captured.args.includes('--no-context-files'));
|
||||
assert.ok(captured.args.includes('--no-skills'));
|
||||
@@ -54,8 +66,8 @@ fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), in
|
||||
assert.match(context, /fixture contracts\/CONSTITUTION.md/);
|
||||
assert.match(context, /fixture user\/USER.md/);
|
||||
assert.match(context, /fixture AGENTS.md/);
|
||||
assert.match(context, /You are Darkwing/);
|
||||
const sessions = join(root, '.pi/state/darkwing/sessions');
|
||||
assert.match(context, new RegExp(`You are ${agent[0].toUpperCase() + agent.slice(1)}`));
|
||||
const sessions = join(root, `.pi/state/${agent}/sessions`);
|
||||
const session = join(sessions, 'history.jsonl');
|
||||
const history = JSON.stringify({ type: 'session', version: 3, id: 'test', cwd: root, timestamp: new Date().toISOString() }) + '\n';
|
||||
writeFileSync(session, history);
|
||||
@@ -73,10 +85,10 @@ fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), in
|
||||
assert.notEqual(launch('--unexpected', false).status, 0);
|
||||
assert.notEqual(launch('', false).status, 0);
|
||||
const locked = spawnSync('script', ['-q', '-e', '-c',
|
||||
'flock .pi/state/darkwing/launch.lock bash agents/darkwing/launch.sh --fresh', '/dev/null'],
|
||||
`flock .pi/state/${agent}/launch.lock bash agents/${agent}/launch.sh --fresh`, '/dev/null'],
|
||||
{ cwd: root, encoding: 'utf8', timeout: 10000 });
|
||||
assert.notEqual(locked.status, 0);
|
||||
assert.match(locked.stdout, /another darkwing TUI/);
|
||||
assert.match(locked.stdout, new RegExp(`another ${agent} TUI`));
|
||||
writeFileSync(join(root, 'alternate.md'), 'alternate user context\n');
|
||||
ok(launch('--fresh --user alternate.md'));
|
||||
captured = JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Offline Claude launcher checks; no authentication or model calls.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, cpSync, existsSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const source = resolve(import.meta.dirname, '..');
|
||||
test('Rocko uses Sonnet, isolated resume IDs, current context and launch refusals', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'rocko launch '));
|
||||
try {
|
||||
for (const dir of ['agents', 'scripts', 'contracts', 'user', 'bin']) mkdirSync(join(root, dir), { recursive: true });
|
||||
cpSync(join(source, 'agents/rocko'), join(root, 'agents/rocko'), { recursive: true });
|
||||
for (const file of ['contracts/CONSTITUTION.md', 'contracts/STANDARDS.md', 'user/USER.md', 'AGENTS.md']) {
|
||||
writeFileSync(join(root, file), `fixture ${file}\n`);
|
||||
}
|
||||
writeFileSync(join(root, 'scripts/common.sh'), 'load_config() { MOSAIC_DATA_ROOT="$PWD"; }\n');
|
||||
writeFileSync(join(root, 'bin/claude'), `#!/usr/bin/env node
|
||||
const fs = require('node:fs');
|
||||
if (process.argv[2] === '--version') { console.log('fixture Claude'); process.exit(0); }
|
||||
fs.writeFileSync('capture.json', JSON.stringify({args: process.argv.slice(2), cwd: process.cwd(), agent: process.env.MOSAIC_AGENT_NAME, incarnation: process.env.MOSAIC_LAUNCH_INCARNATION}));
|
||||
if (process.env.FAKE_CLAUDE_FAIL) process.exit(7);
|
||||
`, { mode: 0o755 });
|
||||
const env = { ...process.env, PATH: `${join(root, 'bin')}:${process.env.PATH}`, MOSAIC_LAUNCH_INCARNATION: 'do-not-inherit' };
|
||||
const run = (args = '', tty = true, extraEnv = {}) => spawnSync(tty ? 'script' : 'bash', tty
|
||||
? ['-q', '-e', '-c', `bash agents/rocko/launch.sh ${args}`, '/dev/null']
|
||||
: [join(root, 'agents/rocko/launch.sh'), ...args.split(' ').filter(Boolean)],
|
||||
{ cwd: tty ? root : tmpdir(), env: { ...env, ...extraEnv }, encoding: 'utf8', timeout: 10000 });
|
||||
const ok = result => assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
const capture = () => JSON.parse(readFileSync(join(root, 'capture.json')));
|
||||
const state = join(root, '.pi/state/rocko');
|
||||
ok(run('--check', false));
|
||||
assert.equal(existsSync(state), false);
|
||||
assert.equal(existsSync(join(root, 'capture.json')), false);
|
||||
ok(run());
|
||||
let captured = capture();
|
||||
assert.equal(captured.cwd, root);
|
||||
assert.equal(captured.agent, 'rocko');
|
||||
assert.equal(captured.incarnation, undefined);
|
||||
assert.equal(captured.args[captured.args.indexOf('--model') + 1], 'sonnet');
|
||||
assert.equal(captured.args[captured.args.indexOf('--name') + 1], 'Rocko');
|
||||
assert.equal(captured.args.includes('--continue'), false);
|
||||
assert.equal(captured.args.includes('--dangerously-skip-permissions'), false);
|
||||
assert.equal(captured.args.includes('--agent'), false);
|
||||
const firstId = readFileSync(join(state, 'session-id'), 'utf8').trim();
|
||||
assert.equal(captured.args[captured.args.indexOf('--session-id') + 1], firstId);
|
||||
const prompt = captured.args[captured.args.indexOf('--append-system-prompt') + 1];
|
||||
for (const value of ['You are Rocko', 'Darkwing', 'fixture AGENTS.md', 'fixture user/USER.md', 'ROCKO NATIVE DEVELOPMENT CONTEXT']) assert.ok(prompt.includes(value));
|
||||
writeFileSync(join(root, 'agents/rocko/SOUL.md'), 'You are Rocko. Updated context.\n');
|
||||
ok(run());
|
||||
captured = capture();
|
||||
assert.equal(captured.args[captured.args.indexOf('--resume') + 1], firstId);
|
||||
assert.equal(captured.args.includes('--session-id'), false);
|
||||
assert.match(captured.args[captured.args.indexOf('--append-system-prompt') + 1], /Updated context/);
|
||||
const failed = run('', true, { FAKE_CLAUDE_FAIL: '1' });
|
||||
assert.equal(failed.status, 7);
|
||||
assert.equal(readFileSync(join(state, 'session-id'), 'utf8').trim(), firstId);
|
||||
ok(run('--fresh'));
|
||||
const secondId = readFileSync(join(state, 'session-id'), 'utf8').trim();
|
||||
assert.notEqual(secondId, firstId);
|
||||
assert.equal(capture().args.includes('--resume'), false);
|
||||
writeFileSync(join(state, 'session-id'), 'corrupt\n');
|
||||
assert.notEqual(run().status, 0);
|
||||
ok(run('--fresh'));
|
||||
assert.notEqual(run('--unexpected', false).status, 0);
|
||||
assert.notEqual(run('--user missing.md --check', false).status, 0);
|
||||
assert.notEqual(run('', false).status, 0);
|
||||
const locked = spawnSync('script', ['-q', '-e', '-c',
|
||||
'flock .pi/state/rocko/launch.lock bash agents/rocko/launch.sh --fresh', '/dev/null'],
|
||||
{ cwd: root, env, encoding: 'utf8', timeout: 10000 });
|
||||
assert.notEqual(locked.status, 0);
|
||||
assert.match(locked.stdout, /another rocko TUI/);
|
||||
writeFileSync(join(root, 'alternate.md'), 'alternate user context\n');
|
||||
ok(run('--fresh --user alternate.md'));
|
||||
captured = capture();
|
||||
assert.match(captured.args[captured.args.indexOf('--append-system-prompt') + 1], /alternate user context/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user