Skip to content

Commit 6f4dd5a

Browse files
authored
fix captain lavish command (#65)
1 parent d355964 commit 6f4dd5a

4 files changed

Lines changed: 60 additions & 12 deletions

File tree

.agents/skills/lavish-decisions/SKILL.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,20 @@ Use the firstmate-owned `lavish-axi` file protocol documented in `tools/lavish/R
2323
Use nonempty unique lowercase-slug keys.
2424
4. Choose a durable `$FM_HOME`-relative destination below `data/`.
2525
This is where intake commits the validated answer before writing its receipt.
26-
5. Run `lavish-axi create` with a stable decision id, title, Markdown request, question JSON, and destination.
27-
6. Run `lavish show <id>` and `lavish inbox` to verify the exact durable request.
28-
7. Surface only the title and command:
26+
5. Run `lavish-axi create` with a stable decision id, title, Markdown request, question JSON, and destination, and retain its emitted `Run:` line.
27+
6. From firstmate's environment, run `lavish show <id>` and `lavish inbox` to verify the exact durable request.
28+
7. Surface only the title and the exact `Run:` line emitted by `lavish-axi create`:
2929

3030
```text
3131
Decision waiting: <short title>
32-
Run: lavish answer <decision-id>
32+
<exact Run: line emitted by lavish-axi create>
3333
```
3434

35+
The surfaced command is for the captain's shell, not firstmate's environment.
36+
It must retain the emitted `--home` argument and resolved absolute home path even when firstmate has `FM_HOME` exported.
37+
Never shorten the command or reconstruct it from a placeholder.
38+
Always carrying the explicit home is slightly noisier than asking the captain to export `FM_HOME`, but it makes every decision independently runnable and avoids a hidden setup dependency.
39+
3540
Do not edit `request.md` or `manifest.toon` after surfacing the decision.
3641
Their digest and ordered question set are the immutable contract.
3742

tools/lavish/README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ An unanswered request remains answerable until the files are deliberately remove
88

99
## Human commands
1010

11+
The captain-facing command contract is owned by the [`lavish-decisions` skill](../../.agents/skills/lavish-decisions/SKILL.md).
12+
`lavish-axi create` follows that contract by printing an answer command with the resolved absolute home path.
13+
For direct human use, pass the same explicit home to every command, replacing this example path with the fleet home's resolved absolute path:
14+
1115
```sh
12-
lavish inbox
13-
lavish show <decision-id>
14-
lavish answer <decision-id>
16+
lavish inbox --home '/Users/example/firstmate-home'
17+
lavish show <decision-id> --home '/Users/example/firstmate-home'
18+
lavish answer <decision-id> --home '/Users/example/firstmate-home'
1519
```
1620

1721
`lavish answer` renders the complete request, collects one numbered choice for every ordered question, accepts an optional note, shows the whole batch, and requires one explicit confirmation.
@@ -62,8 +66,9 @@ Intake validates every unreceipted answer, writes the declared destination first
6266
An existing matching destination or receipt is an idempotent success.
6367
A conflicting destination fails closed.
6468

65-
All commands use `FM_HOME`.
66-
Tests and recovery tools may pass `--home <path>` explicitly.
69+
All commands require either `FM_HOME` or an explicit `--home <path>` and never guess a fleet home.
70+
Firstmate's internal commands use `FM_HOME`; captain-facing commands carry the resolved absolute `--home` path.
71+
Tests and recovery tools may also pass `--home <path>` explicitly.
6772
The firstmate bootstrap install command also records the checkout's narrow wake adapter with `lavish-axi configure-wake`; this local pointer is not inherited into other homes.
6873

6974
## Protocol

tools/lavish/src/cli.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ function resolveHome(options) {
9898
return resolve(home);
9999
}
100100

101+
function shellQuote(value) {
102+
return `'${value.replaceAll("'", `'"'"'`)}'`;
103+
}
104+
101105
function printRequest(decision) {
102106
process.stdout.write(`${decision.requestText.trimEnd()}\n\n`);
103107
process.stdout.write(`Decision: ${decision.manifest.title} (${decision.id})\n`);
@@ -342,7 +346,7 @@ async function createCommand(options) {
342346
});
343347
process.stdout.write(
344348
`${result.created ? 'Created' : 'Already exists'}: ${result.decision.id}\n`
345-
+ `Run: lavish answer ${result.decision.id}\n`,
349+
+ `Run: lavish answer ${result.decision.id} --home ${shellQuote(home)}\n`,
346350
);
347351
}
348352

tools/lavish/test/lavish.test.mjs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,14 @@ function runCli(args, {
149149
function runExecutable(executable, args, {
150150
env = {},
151151
input = '',
152+
unsetEnv = [],
152153
} = {}) {
153154
return new Promise((resolveRun, rejectRun) => {
155+
const childEnv = { ...process.env, ...env };
156+
for (const key of unsetEnv) delete childEnv[key];
154157
const child = spawn(executable, args, {
155158
detached: true,
156-
env: { ...process.env, ...env },
159+
env: childEnv,
157160
stdio: ['pipe', 'pipe', 'pipe'],
158161
});
159162
let stdout = '';
@@ -182,6 +185,7 @@ async function createRequest(fx, {
182185
id = 'release-choice',
183186
destination = 'data/replies/release-choice.toon',
184187
createdAt = undefined,
188+
returnResult = false,
185189
} = {}) {
186190
const args = [
187191
'create',
@@ -199,7 +203,7 @@ async function createRequest(fx, {
199203
if (createdAt !== undefined) args.push('--created-at', createdAt);
200204
const result = await runCli(args, { home: fx.home });
201205
assert.equal(result.code, 0, result.stderr);
202-
return id;
206+
return returnResult ? { id, result } : id;
203207
}
204208

205209
async function answer(fx, id, {
@@ -235,6 +239,36 @@ test('a seven-day-old request remains answerable with no firstmate process', asy
235239
assert.equal(inbox.stdout, 'No pending Lavish decisions.\n');
236240
});
237241

242+
test('the surfaced captain command works with FM_HOME unset', async () => {
243+
const fx = await fixture('captain shell');
244+
const { id, result: created } = await createRequest(fx, { returnResult: true });
245+
const runLine = created.stdout
246+
.split('\n')
247+
.find((line) => line.startsWith('Run: '));
248+
assert.ok(runLine, `create did not surface a Run line: ${created.stdout}`);
249+
const surfacedCommand = runLine.slice('Run: '.length);
250+
assert.equal(
251+
surfacedCommand,
252+
`lavish answer ${id} --home '${fx.home}'`,
253+
);
254+
255+
const fakeBin = join(fx.root, 'captain-bin');
256+
await mkdir(fakeBin);
257+
await symlink(CLI, join(fakeBin, 'lavish'));
258+
const answered = await runExecutable('/bin/sh', ['-c', surfacedCommand], {
259+
env: { PATH: `${fakeBin}:${process.env.PATH}` },
260+
unsetEnv: ['FM_HOME'],
261+
input: '1\nRun from the captain shell\ny\n',
262+
});
263+
assert.equal(answered.code, 0, answered.stderr);
264+
assert.doesNotMatch(answered.stderr, /FM_HOME is required/);
265+
assert.match(answered.stdout, /Answer saved.*wake queued/);
266+
assert.equal(
267+
await exists(join(fx.home, 'data/decisions', id, 'answer.toon')),
268+
true,
269+
);
270+
});
271+
238272
test('interruption before rename cannot expose a partial answer', async () => {
239273
const fx = await fixture('interrupted');
240274
const id = await createRequest(fx);

0 commit comments

Comments
 (0)