@ctrl-spc/cs 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/codex-home.js +10 -6
- package/dist/config.js +17 -0
- package/dist/index.js +93 -12
- package/dist/login.js +9 -1
- package/dist/mcp.js +186 -28
- package/dist/panel3/answer.js +12 -12
- package/dist/panel3/checkout.js +522 -2
- package/dist/panel3/cli.js +49 -62
- package/dist/panel3/client.js +53 -16
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/prompt.js +146 -3
- package/dist/panel3/run.js +638 -71
- package/dist/panel3/say.js +10 -10
- package/dist/panel3/show.js +195 -22
- package/dist/panel3/spawn.js +5 -2
- package/dist/panel3/tools.js +326 -5
- package/dist/presence.js +8 -0
- package/dist/skills.js +174 -0
- package/package.json +2 -3
package/dist/panel3/cli.js
CHANGED
|
@@ -1,83 +1,70 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* ═══ AGENT PANEL v3: the
|
|
3
|
+
* ═══ AGENT PANEL v3: the panel's commands, and the harness bin that runs them. ═══
|
|
4
4
|
*
|
|
5
5
|
* THIS FILE BELONGS TO AGENT PANEL v3. Nothing outside `src/panel3/` may
|
|
6
|
-
* import it.
|
|
6
|
+
* import it, but for the one mount `cli-v2/src/index.ts` makes on
|
|
7
|
+
* `panelCommand`, named in `test/panel3-isolation.contract.test.mjs`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* ═══ THE PERSON TYPES `cs`, AND ONLY `cs`. ═══ Lane's ruling (2026-08-24):
|
|
10
|
+
* nothing the product names may carry a version the person never knew about,
|
|
11
|
+
* and there is one CLI to launch and sign into. So `say`, `answer` and `show`
|
|
12
|
+
* are `cs` subcommands, reached through `panelCommand` below, and the `cs3` bin
|
|
13
|
+
* that used to publish them is gone. The router stays HERE rather than moving
|
|
14
|
+
* into `index.ts` so that v3 is still one directory to delete: `index.ts` holds
|
|
15
|
+
* three cases and a single import, not the panel's argument handling.
|
|
12
16
|
*
|
|
13
|
-
* ═══ AND
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* one `run()` in `run.ts` with two callers
|
|
21
|
-
*
|
|
22
|
-
* `test/panel3-acceptance.mjs` and the three walk scripts stand a daemon up
|
|
23
|
-
* against their own isolated account and their own working copy without touching
|
|
24
|
-
* the installed CLI's `session.json` or its presence. Deleting it would mean
|
|
25
|
-
* rebuilding that isolation inside `cs start`, which is a launch path the tests
|
|
26
|
-
* would then own. DO NOT PUT IT BACK IN THE HELP TEXT.
|
|
17
|
+
* ═══ AND `run` IS STILL NOT SOMETHING A PERSON TYPES. ═══ `cs start` runs the
|
|
18
|
+
* panel's poll loop through `startPanel`, so that is the only launch the product
|
|
19
|
+
* names. `run` is reachable only by running this file directly, which is what
|
|
20
|
+
* `test/panel3-acceptance.mjs` and the walk scripts do. It signs in from
|
|
21
|
+
* `CTRL_SPC_V3_EMAIL` / `CTRL_SPC_V3_PASSWORD`, which is what lets them stand a
|
|
22
|
+
* daemon up against their own isolated account and their own working copy
|
|
23
|
+
* without touching the installed CLI's `session.json` or its presence. There is
|
|
24
|
+
* one `run()` in `run.ts` with two callers, not two copies of the loop. DO NOT
|
|
25
|
+
* PUT IT IN `cs`.
|
|
27
26
|
*
|
|
28
27
|
* ═══ A FAILURE EXITS NON-ZERO AND SAYS WHY, ALWAYS. ═══ Every read in `show`
|
|
29
|
-
* and every write in `say` either returns its rows or throws, and
|
|
30
|
-
* only
|
|
31
|
-
* command whose whole job is observation is worthless
|
|
32
|
-
* mean either "nothing is there" or "the read
|
|
33
|
-
* zero without writing would be an
|
|
28
|
+
* and every write in `say` either returns its rows or throws, and the two
|
|
29
|
+
* handlers below are the only ones. Nothing here converts a failure into an
|
|
30
|
+
* empty result, because a command whose whole job is observation is worthless
|
|
31
|
+
* the first time silence can mean either "nothing is there" or "the read
|
|
32
|
+
* failed", and a `say` that exited zero without writing would be an
|
|
33
|
+
* acknowledgement of nothing.
|
|
34
34
|
*/
|
|
35
|
+
import { realpathSync } from 'node:fs';
|
|
36
|
+
import { fileURLToPath } from 'node:url';
|
|
35
37
|
import { answer } from './answer.js';
|
|
36
38
|
import { run } from './run.js';
|
|
37
39
|
import { say } from './say.js';
|
|
38
40
|
import { show } from './show.js';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
cs3 show <id> One card in full, or one run with its brief and report
|
|
47
|
-
cs3 help Show this help
|
|
48
|
-
|
|
49
|
-
This machine answers cards when \`cs start\` is running. These commands are the
|
|
50
|
-
terminal's view of the same record.
|
|
51
|
-
|
|
52
|
-
Acts as CTRL_SPC_V3_EMAIL / CTRL_SPC_V3_PASSWORD, against the hosted project
|
|
53
|
-
by default. Point it at a local stack with:
|
|
54
|
-
CTRL_SPC_SUPABASE_URL=http://127.0.0.1:54321
|
|
55
|
-
CTRL_SPC_SUPABASE_KEY=<the local publishable key from \`supabase status\`>
|
|
56
|
-
`;
|
|
57
|
-
async function main() {
|
|
58
|
-
const command = process.argv[2];
|
|
41
|
+
/**
|
|
42
|
+
* One panel command, given the whole tail of `process.argv` from the command
|
|
43
|
+
* word on. `cs` routes `say`, `answer` and `show` here; running this file
|
|
44
|
+
* directly reaches `run` as well.
|
|
45
|
+
*/
|
|
46
|
+
export async function panelCommand(argv) {
|
|
47
|
+
const command = argv[0];
|
|
59
48
|
switch (command) {
|
|
60
49
|
case 'say':
|
|
61
|
-
return say(
|
|
50
|
+
return say(argv.slice(1));
|
|
62
51
|
case 'answer':
|
|
63
|
-
return answer(
|
|
52
|
+
return answer(argv.slice(1));
|
|
64
53
|
case 'run':
|
|
65
|
-
return run(
|
|
54
|
+
return run(argv.slice(1));
|
|
66
55
|
case 'show':
|
|
67
|
-
return show(
|
|
68
|
-
case undefined:
|
|
69
|
-
case 'help':
|
|
70
|
-
case '--help':
|
|
71
|
-
case '-h':
|
|
72
|
-
console.log(HELP);
|
|
73
|
-
return;
|
|
56
|
+
return show(argv[1]);
|
|
74
57
|
default:
|
|
75
|
-
|
|
76
|
-
console.log(HELP);
|
|
77
|
-
process.exitCode = 1;
|
|
58
|
+
throw new Error(`unknown command: ${command ?? '(none)'}`);
|
|
78
59
|
}
|
|
79
60
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
61
|
+
/* THE HARNESS ENTRY, AND IT MUST NOT FIRE WHEN `cs` IMPORTS `panelCommand`.
|
|
62
|
+
Both sides go through `realpathSync` because the installed CLI is reached
|
|
63
|
+
through a symlinked bin, and comparing the raw argument to the module path
|
|
64
|
+
would make this file two different files depending on how it was reached. */
|
|
65
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
66
|
+
panelCommand(process.argv.slice(2)).catch((err) => {
|
|
67
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
70
|
+
}
|
package/dist/panel3/client.js
CHANGED
|
@@ -16,14 +16,23 @@
|
|
|
16
16
|
* copying it a third time.
|
|
17
17
|
*
|
|
18
18
|
* ---------------------------------------------------------------------------
|
|
19
|
-
* WHY
|
|
19
|
+
* WHY A SESSION OF ITS OWN, AND WHY THE STORED ONE IS READ THE WAY IT IS.
|
|
20
20
|
*
|
|
21
21
|
* The installed CLI keeps one `session.json` on this machine and its long-lived
|
|
22
22
|
* daemon holds the access token that file names. A second process that reads it
|
|
23
|
-
* and lets the client
|
|
24
|
-
* and the daemon's token stops working. So
|
|
25
|
-
* in memory, and never touches the file:
|
|
26
|
-
* client has any storage to write a rotated
|
|
23
|
+
* and lets the client REFRESH writes a ROTATED token back over the same file,
|
|
24
|
+
* and the daemon's token stops working. So when credentials are given, v3 takes
|
|
25
|
+
* a fresh session of its own, in memory, and never touches the file:
|
|
26
|
+
* `persistSession: false`, and no v3 client has any storage to write a rotated
|
|
27
|
+
* token into.
|
|
28
|
+
*
|
|
29
|
+
* ═══ AND WITH NO CREDENTIALS IT READS THAT FILE, BECAUSE THE PERSON TYPES
|
|
30
|
+
* `cs`. ═══ `say`, `answer` and `show` are `cs` subcommands, and a person
|
|
31
|
+
* who ran `cs login` has no v3 environment set and must not be asked to sign in
|
|
32
|
+
* twice. `storedSessionClient` below is that read, in the one shape that cannot
|
|
33
|
+
* rotate anything: the access token on a header, no auth session, no refresh
|
|
34
|
+
* token, nothing written back. The hazard above is about REFRESHING against
|
|
35
|
+
* that file, not about reading it.
|
|
27
36
|
*
|
|
28
37
|
* ═══ THAT IS NOW TRUE OF THE COMMANDS AND NOT OF THE PANEL, AND THE DIFFERENCE
|
|
29
38
|
* IS THE WORD "PROCESS". ═══ recovery-1 Slice 4: `cs start` runs the poll
|
|
@@ -35,9 +44,9 @@
|
|
|
35
44
|
* token. `signedInClient` must never be called from that path, because taking a
|
|
36
45
|
* session of its own there is what would recreate the race indoors.
|
|
37
46
|
*
|
|
38
|
-
* So this function now serves `show`, `say`, `answer`, and
|
|
39
|
-
*
|
|
40
|
-
*
|
|
47
|
+
* So this function now serves `show`, `say`, `answer`, and the acceptance
|
|
48
|
+
* harness's isolated `run` entry — every one of them a process that is NOT the
|
|
49
|
+
* installed daemon, which is exactly the set the paragraph above describes.
|
|
41
50
|
*
|
|
42
51
|
* ---------------------------------------------------------------------------
|
|
43
52
|
* ═══ AND THE DAEMON'S CLIENT REFRESHES, WHILE A ONE-SHOT COMMAND'S DOES NOT.
|
|
@@ -45,8 +54,8 @@
|
|
|
45
54
|
*
|
|
46
55
|
* `autoRefreshToken` was off for every v3 client, which is right for `show`,
|
|
47
56
|
* `say` and `answer` — each signs in, does one thing and exits, long inside the
|
|
48
|
-
* access token's lifetime — and WRONG for `
|
|
49
|
-
* as long as the daemon lives and hands that same client to every tool call an
|
|
57
|
+
* access token's lifetime — and WRONG for the harness's `run`, which holds one
|
|
58
|
+
* client for as long as the daemon lives and hands that same client to every tool call an
|
|
50
59
|
* agent makes. Past the token's TTL every poll and every in-flight tool call
|
|
51
60
|
* would start failing, and the card would sit working with a daemon that could
|
|
52
61
|
* no longer read or write anything.
|
|
@@ -75,8 +84,9 @@
|
|
|
75
84
|
* published (`"files": ["dist"]`, `"access": "public"`), and `dist/` is
|
|
76
85
|
* committed, so a literal password here is one `npm publish` away from the
|
|
77
86
|
* public registry — a harm about DISTRIBUTION that the loopback guard below,
|
|
78
|
-
* which is about where a request is SENT, does nothing to prevent. So
|
|
79
|
-
*
|
|
87
|
+
* which is about where a request is SENT, does nothing to prevent. So no
|
|
88
|
+
* password is ever defaulted: unset falls through to the stored `cs login`
|
|
89
|
+
* session, and with neither the command refuses and says what to do.
|
|
80
90
|
*
|
|
81
91
|
* ---------------------------------------------------------------------------
|
|
82
92
|
* ═══ THE LOOPBACK-ONLY GUARD IS GONE, AND THIS IS THE EDIT IT ASKED FOR. ═══
|
|
@@ -107,6 +117,7 @@
|
|
|
107
117
|
*/
|
|
108
118
|
import { createClient } from '@supabase/supabase-js';
|
|
109
119
|
import { SUPABASE_URL, SUPABASE_KEY } from '../env.js';
|
|
120
|
+
import { readSession } from '../config.js';
|
|
110
121
|
/**
|
|
111
122
|
* How a v3 client holds its session, for the two lifetimes v3 has.
|
|
112
123
|
*
|
|
@@ -122,6 +133,34 @@ export const sessionOptions = (living) => ({
|
|
|
122
133
|
the token's lifetime, and the daemon does not. */
|
|
123
134
|
autoRefreshToken: living,
|
|
124
135
|
});
|
|
136
|
+
/**
|
|
137
|
+
* ═══ THE PERSON SIGNED IN ONCE, WITH `cs login`, AND `cs say` MUST NOT ASK
|
|
138
|
+
* AGAIN. ═══
|
|
139
|
+
*
|
|
140
|
+
* `say`, `answer` and `show` are `cs` subcommands, so the person who ran
|
|
141
|
+
* `cs login` types them with no environment set at all. This is that session,
|
|
142
|
+
* and it is read in the one shape that CANNOT rotate the token the daemon is
|
|
143
|
+
* holding: the stored access token goes on the Authorization header, there is no
|
|
144
|
+
* auth session on the client, no refresh token is handed over, and nothing is
|
|
145
|
+
* ever written back to `session.json`. A rotation here is exactly the failure
|
|
146
|
+
* the header above describes, and the header cannot happen if no client in this
|
|
147
|
+
* process ever holds a refresh token.
|
|
148
|
+
*
|
|
149
|
+
* An expired access token surfaces as the request's own 401 through
|
|
150
|
+
* `returned()`, which is the truthful failure: `cs start` refreshes the file
|
|
151
|
+
* this reads, so a person whose machine is online has a live token.
|
|
152
|
+
*/
|
|
153
|
+
function storedSessionClient() {
|
|
154
|
+
const stored = readSession();
|
|
155
|
+
if (!stored?.access_token) {
|
|
156
|
+
throw new Error('not signed in. Run `cs login`, or set CTRL_SPC_V3_EMAIL and CTRL_SPC_V3_PASSWORD to the '
|
|
157
|
+
+ `account this command should act as at ${SUPABASE_URL}`);
|
|
158
|
+
}
|
|
159
|
+
return createClient(SUPABASE_URL, SUPABASE_KEY, {
|
|
160
|
+
auth: sessionOptions(false),
|
|
161
|
+
global: { headers: { Authorization: `Bearer ${stored.access_token}` } },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
125
164
|
/**
|
|
126
165
|
* @param living whether this client outlives its access token. The daemon's
|
|
127
166
|
* does; every one-shot command's does not.
|
|
@@ -129,10 +168,8 @@ export const sessionOptions = (living) => ({
|
|
|
129
168
|
export async function signedInClient(living = false) {
|
|
130
169
|
const email = process.env.CTRL_SPC_V3_EMAIL;
|
|
131
170
|
const password = process.env.CTRL_SPC_V3_PASSWORD;
|
|
132
|
-
if (!email || !password)
|
|
133
|
-
|
|
134
|
-
+ `should act as at ${SUPABASE_URL}`);
|
|
135
|
-
}
|
|
171
|
+
if (!email || !password)
|
|
172
|
+
return storedSessionClient();
|
|
136
173
|
const client = createClient(SUPABASE_URL, SUPABASE_KEY, { auth: sessionOptions(living) });
|
|
137
174
|
const { data, error } = await client.auth.signInWithPassword({ email, password });
|
|
138
175
|
// Truthfully, and naming both halves of what was attempted: a wrong password
|
package/dist/panel3/presence.js
CHANGED
|
@@ -9,7 +9,7 @@ import { returned } from './client.js';
|
|
|
9
9
|
export const LISTENING_WINDOW_MS = 15_000;
|
|
10
10
|
/** Is this machine still listening?
|
|
11
11
|
*
|
|
12
|
-
* THE ONE PREDICATE, shared by `
|
|
12
|
+
* THE ONE PREDICATE, shared by `cs show` and the panel, so the two screens
|
|
13
13
|
* cannot disagree about whether anything can pick a card up.
|
|
14
14
|
*
|
|
15
15
|
* ═══ IT TAKES THE ROW, NOT A TIMESTAMP, BECAUSE FRESHNESS IS NO LONGER THE
|
package/dist/panel3/prompt.js
CHANGED
|
@@ -228,13 +228,32 @@ const WHILE_YOU_ARE_WORKING = [
|
|
|
228
228
|
* `whatYouSendBack` is: one wording for "what was attached", never two that
|
|
229
229
|
* drift.
|
|
230
230
|
*/
|
|
231
|
-
|
|
231
|
+
// Exported since attaching-after-the-fact-10: the owner's activation joins this
|
|
232
|
+
// block live, in front of a prompt whose brief already ends with a frozen copy
|
|
233
|
+
// of it (see the supersession sentence below and `withStandingRules` in
|
|
234
|
+
// `run.ts`). No wrapper around it, because it already emits its own header and
|
|
235
|
+
// already returns `[]` when nothing is attached.
|
|
236
|
+
export const whatWasAttached = (attachments) => {
|
|
232
237
|
const codebases = attachments.filter((attachment) => attachment.startsWith('codebase '));
|
|
233
238
|
const personAttachments = attachments.filter((attachment) => !attachment.startsWith('codebase '));
|
|
234
239
|
const personAttachmentLines = personAttachments.length === 0 ? [] : [
|
|
235
240
|
'',
|
|
236
241
|
'WHAT THE PERSON ATTACHED',
|
|
237
242
|
'They attached these when they sent this, so what is asked belongs against them.',
|
|
243
|
+
/* ═══ THE SUPERSESSION SENTENCE, IN THE SHAPE `standingRules` ALREADY USES.
|
|
244
|
+
═══ attaching-after-the-fact-10: a person may attach to a card that is
|
|
245
|
+
already running, and the owner's prompt replays an immutable brief that
|
|
246
|
+
ends with THIS block as it stood at dispatch. The live block is joined in
|
|
247
|
+
front of that prompt, so without this sentence the owner would read two
|
|
248
|
+
lists of attachments with the stale one last. Written once, here, rather
|
|
249
|
+
than at the join, so one wording exists.
|
|
250
|
+
|
|
251
|
+
AND IT IS TRUE AND INERT IN THE OTHER TWO CALLERS. `levelOnePrompt` and
|
|
252
|
+
`workBrief` each render this block exactly once in a prompt that holds no
|
|
253
|
+
other copy, so "it replaces any earlier list" is simply true there. */
|
|
254
|
+
'This is the complete and current set of what the person attached to this conversation. It',
|
|
255
|
+
'REPLACES any list of attachments you were given earlier, including one inside a brief below:',
|
|
256
|
+
'everything below is in force, and nothing that is not below is.',
|
|
238
257
|
...personAttachments,
|
|
239
258
|
/* ═══ READ BEFORE YOU DECIDE, WHICH IS THE WHOLE OF THIS SLICE. ═══ ux.md:
|
|
240
259
|
"Before an agent begins working on it, it must read the description, and
|
|
@@ -319,6 +338,69 @@ const whatWasAttached = (attachments) => {
|
|
|
319
338
|
...personAttachmentLines,
|
|
320
339
|
];
|
|
321
340
|
};
|
|
341
|
+
export const standingRules = (documents) => {
|
|
342
|
+
if (documents.length === 0)
|
|
343
|
+
return [];
|
|
344
|
+
return [
|
|
345
|
+
'THE STANDING RULES ON THIS PROJECT',
|
|
346
|
+
'Written by the people you are working for, in the product, and handed to you in full every',
|
|
347
|
+
'time you are started. They apply to everything on this conversation unless the person said',
|
|
348
|
+
'otherwise, and they are what to follow when nobody said anything. This is the complete and',
|
|
349
|
+
'current set: it REPLACES any standing rules you were given earlier in this conversation, so a',
|
|
350
|
+
'rule that is not below is not in force, whatever you were told before.',
|
|
351
|
+
'When something else disagrees with them, the order is: what the person said on this',
|
|
352
|
+
'conversation, then what they attached to it, then these rules, then the project context you',
|
|
353
|
+
'can go and read, then your own judgement. A rule marked for one codebase beats a rule for the',
|
|
354
|
+
'whole project when you are working in that codebase.',
|
|
355
|
+
'If what you were asked for is the opposite of a rule below, do not settle it quietly either',
|
|
356
|
+
'way: the person is the one who decides which they meant, and asking is how you find out.',
|
|
357
|
+
...documents.flatMap((document) => [
|
|
358
|
+
'',
|
|
359
|
+
`--- ${document.title}${document.codebase_id === null ? '' : ' (this codebase only)'} ---`,
|
|
360
|
+
document.content,
|
|
361
|
+
]),
|
|
362
|
+
];
|
|
363
|
+
};
|
|
364
|
+
/** What each ending means for the agent, and all three say the same thing about
|
|
365
|
+
* the agent's own part in it: landing is the product's, never yours. */
|
|
366
|
+
const LANDING_LINES = {
|
|
367
|
+
pr: [
|
|
368
|
+
'Finished work in this codebase lands on a branch, in a pull request that a person reviews.',
|
|
369
|
+
'Landing is the product\'s job and never yours: do not merge and do not push.',
|
|
370
|
+
],
|
|
371
|
+
main: [
|
|
372
|
+
'Finished work in this codebase lands on the main branch, and the person is asked first.',
|
|
373
|
+
'When the card\'s work is done, call `offer_ending` with one sentence saying what was done.',
|
|
374
|
+
'The product writes the question and both answers, and it performs the merge itself if they',
|
|
375
|
+
'choose to put the work back. Landing is the product\'s job and never yours: never merge and',
|
|
376
|
+
'never push.',
|
|
377
|
+
],
|
|
378
|
+
branch: [
|
|
379
|
+
'Finished work in this codebase stays on its branch. Nothing is pushed and nothing is merged.',
|
|
380
|
+
'Say where the work is in your answer.',
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
export const workingRules = (rules) => [
|
|
384
|
+
`WORKING IN ${rules.codebase}`,
|
|
385
|
+
...(rules.branch === null
|
|
386
|
+
? [
|
|
387
|
+
'Every card sent here works in a copy of its own, on a branch of its own, which the product',
|
|
388
|
+
`makes before any agent starts. Branches are named ${rules.branchPattern}.`,
|
|
389
|
+
]
|
|
390
|
+
: [
|
|
391
|
+
`This is your card's own copy of this codebase, on branch ${rules.branch}. The product made both.`,
|
|
392
|
+
'Everybody working this card shares this copy and this branch; nobody outside it does.',
|
|
393
|
+
'It is a fresh checkout of tracked files only, so install and build before assuming anything',
|
|
394
|
+
'is there. Never reach into another card\'s copy or branch.',
|
|
395
|
+
]),
|
|
396
|
+
...(rules.base === null ? [] : [
|
|
397
|
+
`Work starts from ${rules.base}, which was brought up to date when your copy was made.`,
|
|
398
|
+
`Never commit to ${rules.base}. Git itself refuses it from here, because ${rules.base} is`,
|
|
399
|
+
'checked out somewhere else. If you are refused, say so with `say` and carry on: it is a rule',
|
|
400
|
+
'holding, not a failure.',
|
|
401
|
+
]),
|
|
402
|
+
...LANDING_LINES[rules.landing],
|
|
403
|
+
];
|
|
322
404
|
const projectCodebases = (codebases) => {
|
|
323
405
|
if (codebases === null)
|
|
324
406
|
return [];
|
|
@@ -396,6 +478,16 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
|
|
|
396
478
|
'Your only question is a destination or codebase you truly cannot choose. NEVER ask the person',
|
|
397
479
|
'to make a work or product decision. When one registered codebase clearly fits, launch its owner',
|
|
398
480
|
'even if the work itself needs the person to choose between options; the owner asks that question.',
|
|
481
|
+
/* ═══ THE TWO WAYS OUT, NAMED WHATEVER THE CARD IS FILED UNDER. ═══
|
|
482
|
+
Both endings are a TOOL CALL, and prose is neither of them. This used to
|
|
483
|
+
be said only in the codebase list, so a card with no project was told to
|
|
484
|
+
ask and never told what asking is: measured 2 of 5 level 1 codex runs
|
|
485
|
+
wrote the question as an ordinary reply, which exits 0 with a message,
|
|
486
|
+
reads as a real answer, and lands as the run row `the launcher ended
|
|
487
|
+
without appointing an owner or asking a routing question`. */
|
|
488
|
+
'BOTH OF THOSE ARE A TOOL CALL AND NOTHING ELSE: `dispatch` to launch the owner, `ask_question`',
|
|
489
|
+
'to ask. Writing either one as your reply does neither. Nobody reads this launcher\'s prose, so a',
|
|
490
|
+
'question written as text is never delivered and the conversation is left with no agent on it.',
|
|
399
491
|
...projectCodebases(codebases),
|
|
400
492
|
'',
|
|
401
493
|
...WHILE_YOU_ARE_WORKING,
|
|
@@ -664,6 +756,52 @@ export const presentedArtifactAnswerContext = (artifact) => artifact === null
|
|
|
664
756
|
'current revision with this one. If they differ, this answer does not approve the current body:',
|
|
665
757
|
'present the current revision in a new approval question and stop.',
|
|
666
758
|
];
|
|
759
|
+
export const landingOutcomeContext = (landing) => {
|
|
760
|
+
if (landing === null)
|
|
761
|
+
return [];
|
|
762
|
+
const head = ['', 'WHAT THE PRODUCT DID WITH THAT ANSWER'];
|
|
763
|
+
if (landing.outcome === 'landed') {
|
|
764
|
+
return [
|
|
765
|
+
...head,
|
|
766
|
+
`They chose to put this work back, and the product has merged ${landing.branch} onto `
|
|
767
|
+
+ `${landing.base} on this machine. It is done and it is not yours to do.`,
|
|
768
|
+
'Say so plainly in your reply, name the branch it went onto, and finish the card.',
|
|
769
|
+
];
|
|
770
|
+
}
|
|
771
|
+
if (landing.outcome === 'leave') {
|
|
772
|
+
return [
|
|
773
|
+
...head,
|
|
774
|
+
'They chose to leave this work on its branch. Nothing was merged and nothing was pushed.',
|
|
775
|
+
`Say so, name the branch${landing.branch === null ? '' : ` (${landing.branch})`} so they can `
|
|
776
|
+
+ 'find it, and finish the card.',
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
if (landing.outcome === 'unclear') {
|
|
780
|
+
return [
|
|
781
|
+
...head,
|
|
782
|
+
'They chose neither of the two answers, so nothing was merged and the work is still on '
|
|
783
|
+
+ `${landing.branch ?? 'its branch'}.`,
|
|
784
|
+
'Read what they actually wrote and answer it. If they still want the work put back, offer the',
|
|
785
|
+
'ending again with `offer_ending` rather than replying: a reply finishes the card, and an',
|
|
786
|
+
'offer leaves it with them.',
|
|
787
|
+
];
|
|
788
|
+
}
|
|
789
|
+
/* ═══ IT DOES NOT SAY "DO NOT FINISH THE CARD", BECAUSE THE AGENT CANNOT
|
|
790
|
+
OBEY THAT. ═══ The card reads done the moment the owner replies, by
|
|
791
|
+
`panel3_answer`, and nothing an agent writes changes it. Telling it
|
|
792
|
+
otherwise would be a rule that can only be broken. What it can do is say
|
|
793
|
+
plainly that the work did not go back and where it still is, which is what
|
|
794
|
+
the person needs; C3 replaces this relay with a question, and a question is
|
|
795
|
+
what keeps the card open. */
|
|
796
|
+
return [
|
|
797
|
+
...head,
|
|
798
|
+
'They chose to put this work back and the product could not do it:',
|
|
799
|
+
landing.because,
|
|
800
|
+
'Tell them that in your reply, in their words: the work did NOT go back, nothing was merged,',
|
|
801
|
+
'and it is still on its branch. Say what would have to happen for it to go back. Never merge',
|
|
802
|
+
'anything yourself, and never write as though it landed.',
|
|
803
|
+
];
|
|
804
|
+
};
|
|
667
805
|
/**
|
|
668
806
|
* ═══ WHAT A RUN IS TOLD WHEN A PROCESS OF ITS OWN ENDED BEFORE IT FINISHED.
|
|
669
807
|
* ═══
|
|
@@ -701,7 +839,10 @@ export function ownerActivationPrompt(brief, report, events, children, childQues
|
|
|
701
839
|
* may hold work no report mentions and the worker list may hold one it
|
|
702
840
|
* dispatched a moment before. Never says WHICH ending it was: see
|
|
703
841
|
* `LOOK_AT_THE_WORK_ITSELF`. */
|
|
704
|
-
afterAProcessEnded = false
|
|
842
|
+
afterAProcessEnded = false,
|
|
843
|
+
/** What the product did with the answer this activation is delivering, when
|
|
844
|
+
* that answer was the product's own ending offer. See `landingOutcomeContext`. */
|
|
845
|
+
landing = null) {
|
|
705
846
|
const history = events.map((event) => {
|
|
706
847
|
if (event.kind === 'question') {
|
|
707
848
|
return [
|
|
@@ -738,6 +879,7 @@ afterAProcessEnded = false) {
|
|
|
738
879
|
'PRIVATE WORKER SNAPSHOT',
|
|
739
880
|
...(children.length === 0 ? ['You have sent nobody.'] : children),
|
|
740
881
|
...(afterAProcessEnded ? ['', LOOK_AT_THE_WORK_ITSELF, DO_NOT_SEND_SOMEBODY_TWICE] : []),
|
|
882
|
+
...landingOutcomeContext(landing),
|
|
741
883
|
'',
|
|
742
884
|
// THE BRIEF IS LAST, and these go before it: the brief ends with how to
|
|
743
885
|
// write the reply, and anything after it pushes that ending into the middle.
|
|
@@ -826,7 +968,7 @@ export function retryPrompt(brief, report, children) {
|
|
|
826
968
|
* something the agent is better placed to see." So it is named as a judgement
|
|
827
969
|
* the agent makes, in one sentence, rather than a rule about when to ask.
|
|
828
970
|
*/
|
|
829
|
-
export function answerPrompt(brief, report, children, question, answer, relatedArtifact = null) {
|
|
971
|
+
export function answerPrompt(brief, report, children, question, answer, relatedArtifact = null, landing = null) {
|
|
830
972
|
return respawnPrompt([
|
|
831
973
|
'YOU STOPPED BECAUSE YOU COULD NOT GO ON WITHOUT KNOWING SOMETHING. HERE IT IS.',
|
|
832
974
|
'You are the same agent, started again, and everything you did before you asked is still there.',
|
|
@@ -837,6 +979,7 @@ export function answerPrompt(brief, report, children, question, answer, relatedA
|
|
|
837
979
|
'WHAT CAME BACK',
|
|
838
980
|
answer,
|
|
839
981
|
...presentedArtifactAnswerContext(relatedArtifact),
|
|
982
|
+
...landingOutcomeContext(landing),
|
|
840
983
|
'',
|
|
841
984
|
'If this answers less than you asked, or turns out to change more than the piece you were sent',
|
|
842
985
|
'to do, ask again rather than deciding it yourself. Carrying on from a guess about something this',
|