@ctrl-spc/cs 0.7.0 → 0.7.1
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/dist/codex-home.js +62 -1
- package/dist/panel3/prompt.js +329 -61
- package/dist/panel3/run.js +584 -56
- package/dist/panel3/session.js +128 -0
- package/dist/panel3/show.js +2 -1
- package/dist/panel3/spawn.js +80 -18
- package/dist/panel3/tools.js +353 -48
- package/dist/workflows.js +68 -0
- package/package.json +1 -1
package/dist/codex-home.js
CHANGED
|
@@ -122,6 +122,16 @@ const OWNER_FILE = 'ctrl-spc-owner.json';
|
|
|
122
122
|
export function codexHomesRoot() {
|
|
123
123
|
return join(configDir(), 'codex-homes');
|
|
124
124
|
}
|
|
125
|
+
/** Persistent isolated homes belong only to panel3 conversation owners. They
|
|
126
|
+
* are separate from `codexHomesRoot()` so the per-process PID sweep can keep
|
|
127
|
+
* its existing contract without deleting a resumable owner conversation. */
|
|
128
|
+
export function panel3CodexOwnerHomesRoot() {
|
|
129
|
+
return join(configDir(), 'panel3-codex-owner-homes');
|
|
130
|
+
}
|
|
131
|
+
export function panel3CodexOwnerHomePath(ownerRunId) {
|
|
132
|
+
const key = ownerRunId.replace(/[^a-zA-Z0-9-]/g, '_');
|
|
133
|
+
return join(panel3CodexOwnerHomesRoot(), key);
|
|
134
|
+
}
|
|
125
135
|
/** Where this run's throwaway codex home lives. Inside the product's own config
|
|
126
136
|
* dir, beside `scratch/`, so it is the user's own directory and it is obvious
|
|
127
137
|
* what wrote it. Keyed by request so two concurrent runs never share one. */
|
|
@@ -197,7 +207,7 @@ export function codexRunConfigToml(server, runTodoId, runtimePlatform = process.
|
|
|
197
207
|
* only v3's `startAgent` passes it off. Never inferred from `server`'s shape:
|
|
198
208
|
* v2 and v3 both pass `{ url }` here, so the shape cannot tell them apart.
|
|
199
209
|
*/
|
|
200
|
-
subagentsEnabled = true) {
|
|
210
|
+
subagentsEnabled = true, persistentPanelOwner = false) {
|
|
201
211
|
const lines = [
|
|
202
212
|
'# Written by CTRL+SPC for ONE codex run. Not the user\'s config; never read',
|
|
203
213
|
'# by anything but the worker this was written for.',
|
|
@@ -207,6 +217,13 @@ subagentsEnabled = true) {
|
|
|
207
217
|
only for that bundled runtime; other platforms keep Codex's own model
|
|
208
218
|
selection. */
|
|
209
219
|
...(runtimePlatform === 'win32' ? ['model = "gpt-5.5"', ''] : []),
|
|
220
|
+
...(persistentPanelOwner ? [
|
|
221
|
+
'sandbox_mode = "workspace-write"',
|
|
222
|
+
'',
|
|
223
|
+
'[sandbox_workspace_write]',
|
|
224
|
+
'network_access = true',
|
|
225
|
+
'',
|
|
226
|
+
] : []),
|
|
210
227
|
'[features]',
|
|
211
228
|
/* The account's own connectors, which no config.toml grants and no argv
|
|
212
229
|
removes. See the block comment above. */
|
|
@@ -340,6 +357,50 @@ subagentsEnabled = true, runtimePlatform = process.platform) {
|
|
|
340
357
|
return null;
|
|
341
358
|
}
|
|
342
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Create or refresh the isolated home for one durable panel3 Level 2 owner.
|
|
362
|
+
* Unlike `ensureCodexRunHome`, this never removes the directory: Codex's native
|
|
363
|
+
* session files inside it are the state a later activation resumes. Only the
|
|
364
|
+
* credential copy and current sealed MCP configuration are refreshed.
|
|
365
|
+
*/
|
|
366
|
+
export function ensurePanel3CodexOwnerHome(server, ownerRunId, runtimePlatform = process.platform) {
|
|
367
|
+
const source = join(userCodexHome(), 'auth.json');
|
|
368
|
+
if (!existsSync(source))
|
|
369
|
+
return null;
|
|
370
|
+
const home = panel3CodexOwnerHomePath(ownerRunId);
|
|
371
|
+
try {
|
|
372
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
373
|
+
chmodSync(home, 0o700);
|
|
374
|
+
copyFileSync(source, join(home, 'auth.json'));
|
|
375
|
+
writeFileSync(join(home, 'config.toml'), codexRunConfigToml(server, null, runtimePlatform, false, true), { mode: 0o600 });
|
|
376
|
+
return home;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** Product-owned persistent state only. Windows owners use the installed Codex
|
|
383
|
+
* home, so this path is normally absent there and removal remains harmless. */
|
|
384
|
+
export function removePanel3CodexOwnerHome(ownerRunId) {
|
|
385
|
+
try {
|
|
386
|
+
rmSync(panel3CodexOwnerHomePath(ownerRunId), { recursive: true, force: true });
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Reconciliation retries. Cleanup must never turn a completed turn into a failure.
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/** Valid directory names only; callers reconcile each id against the signed-in
|
|
393
|
+
* owner row before retaining it. */
|
|
394
|
+
export function listPanel3CodexOwnerHomeIds() {
|
|
395
|
+
try {
|
|
396
|
+
return readdirSync(panel3CodexOwnerHomesRoot(), { withFileTypes: true })
|
|
397
|
+
.filter((entry) => entry.isDirectory())
|
|
398
|
+
.map((entry) => entry.name);
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
343
404
|
/**
|
|
344
405
|
* Delete a run's home once the run is over.
|
|
345
406
|
*
|
package/dist/panel3/prompt.js
CHANGED
|
@@ -90,8 +90,7 @@
|
|
|
90
90
|
* can look — is named beside it.
|
|
91
91
|
*
|
|
92
92
|
* ---------------------------------------------------------------------------
|
|
93
|
-
* ═══ A DISPATCHED
|
|
94
|
-
* CONVERSATION. ═══
|
|
93
|
+
* ═══ A DISPATCHED WORK BRIEF HOLDS THE JOB, NOT THE CONVERSATION. ═══
|
|
95
94
|
*
|
|
96
95
|
* ux.md: "a fresh subagent needs its task, the interfaces it touches, and the
|
|
97
96
|
* global constraints. Nothing else", with the measured cost of the alternative —
|
|
@@ -99,9 +98,10 @@
|
|
|
99
98
|
*
|
|
100
99
|
* So `workBrief` carries exactly two things written by judgement, the
|
|
101
100
|
* responsibility and the boundary, and the facts the product attaches. It
|
|
102
|
-
* carries NO messages, no earlier replies and no history
|
|
103
|
-
*
|
|
104
|
-
*
|
|
101
|
+
* carries NO messages, no earlier replies and no history. A level 3 worker gets
|
|
102
|
+
* that brief directly. A level 2 owner gets it inside `ownerActivationPrompt`,
|
|
103
|
+
* beside the visible conversation read from the card rather than a launcher
|
|
104
|
+
* retelling of it.
|
|
105
105
|
*
|
|
106
106
|
* ═══ AND IT IS THE SAME VOICE, WHICH IS WHY THE OUTBOUND RULES ARE SHARED. ═══
|
|
107
107
|
* ux.md: "the brief is the same at every level. Nothing in a reply reveals which
|
|
@@ -142,7 +142,20 @@
|
|
|
142
142
|
const whatYouSendBack = (produced) => [
|
|
143
143
|
'WHAT YOU SEND BACK',
|
|
144
144
|
'Your reply is the whole of what they read, so write it to them, about their own work.',
|
|
145
|
-
'-
|
|
145
|
+
'- By default, be very brief: use the shortest accurate reply that lets them understand the',
|
|
146
|
+
' outcome, decide, or unblock the work.',
|
|
147
|
+
'- A routine update is absent unless it is useful, and then it is one short sentence or phrase.',
|
|
148
|
+
'- An ordinary completion starts with the outcome, includes only the result they need, and is one',
|
|
149
|
+
' or two short sentences; never add a third.',
|
|
150
|
+
'- If the work cannot continue without a decision or input only the person can supply, you MUST',
|
|
151
|
+
' call `ask_question`. This includes asking them to choose between options. That tool is the only',
|
|
152
|
+
' person-question path: writing the question in an ordinary reply or completing the card with it',
|
|
153
|
+
' is wrong. After the call, stop without repeating the question in an ordinary reply. Use only',
|
|
154
|
+
' the minimum context they need to answer safely.',
|
|
155
|
+
'- Do not copy explanations, logs, prompts, internal reasoning, test inventories or internal',
|
|
156
|
+
' orchestration into an ordinary reply.',
|
|
157
|
+
'- If they explicitly ask for detail, give it for that reply, then return to the brief default.',
|
|
158
|
+
' Accuracy and truthful failures are never shortened away.',
|
|
146
159
|
'- Name the real things: the actual items, projects and people, by the names they have.',
|
|
147
160
|
...produced,
|
|
148
161
|
'- Never name the machinery. Words like agent, run, dispatch, level, card, turn, coordinator,',
|
|
@@ -196,12 +209,20 @@ const WHILE_YOU_ARE_WORKING = [
|
|
|
196
209
|
* ═══ WHAT THE PERSON ATTACHED, AS A FACT AND NOTHING MORE. ═══
|
|
197
210
|
*
|
|
198
211
|
* plan-work-items.md constraint 5: "no agent is asked in prose to go and find
|
|
199
|
-
* what a card is about." So
|
|
200
|
-
* it belongs, and nothing more
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
212
|
+
* what a card is about." So the LIST is one sentence saying the attaching
|
|
213
|
+
* happened and to whom it belongs, and nothing more. What it cannot discover on
|
|
214
|
+
* its own is that these specific ids are the ones the person meant, and that is
|
|
215
|
+
* the fact the list states.
|
|
216
|
+
*
|
|
217
|
+
* ═══ EACH KIND THEN GETS ONE SENTENCE SAYING WHAT THAT WORD MEANS. ═══ Three
|
|
218
|
+
* of them now, and the work item's is the one added last (the
|
|
219
|
+
* `work-lands-on-the-item-6` slice, off ux.md's "before an agent begins working
|
|
220
|
+
* on it, it must read the description, and any artifacts"). An earlier draft of
|
|
221
|
+
* this comment said none of them may tell the agent what to DO; that was true
|
|
222
|
+
* when a work item was only a subject. It is not the same rule as constraint 5,
|
|
223
|
+
* which forbids sending an agent LOOKING for which item a card is about. The id
|
|
224
|
+
* is handed over here by the product; reading the item it was handed is not
|
|
225
|
+
* finding it.
|
|
205
226
|
*
|
|
206
227
|
* SHARED between `levelOnePrompt` and `workBrief` for the same reason
|
|
207
228
|
* `whatYouSendBack` is: one wording for "what was attached", never two that
|
|
@@ -215,13 +236,35 @@ const whatWasAttached = (attachments) => {
|
|
|
215
236
|
'WHAT THE PERSON ATTACHED',
|
|
216
237
|
'They attached these when they sent this, so what is asked belongs against them.',
|
|
217
238
|
...personAttachments,
|
|
218
|
-
/* ═══
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
239
|
+
/* ═══ READ BEFORE YOU DECIDE, WHICH IS THE WHOLE OF THIS SLICE. ═══ ux.md:
|
|
240
|
+
"Before an agent begins working on it, it must read the description, and
|
|
241
|
+
any artifacts, and intelligently decide what to do next." An agent that
|
|
242
|
+
decides first and reads afterwards writes a second plan onto an item that
|
|
243
|
+
already carries one, against a description it never opened.
|
|
244
|
+
|
|
245
|
+
═══ AND IT LIVES HERE, NOT UNDER `THE WORK ITEM THIS IS ABOUT`. ═══ That
|
|
246
|
+
header is the obvious home and it is the wrong one: `workItemId` comes
|
|
247
|
+
only from `dispatch`'s OPTIONAL `work_item_id`, which no prompt asks a
|
|
248
|
+
launcher to supply, and which the launcher MUST NOT supply when several
|
|
249
|
+
items are attached (the selection is the person's, below). So at exactly
|
|
250
|
+
the moment reading the items decides the answer, that header is absent.
|
|
251
|
+
This block is written from the person's own attachment rows, travels to
|
|
252
|
+
every dispatch on the card, and is shared by `levelOnePrompt` and
|
|
253
|
+
`workBrief`, so the sentence reaches all three levels. The tools it needs
|
|
254
|
+
— `get_work_item`, `list_artifacts`, `get_artifact` — are `levels: ALL`.
|
|
255
|
+
|
|
256
|
+
NO TOOL NAMES IN IT. `get_work_item` already says it returns "its
|
|
257
|
+
description, status, placement, and the artifacts on it", and
|
|
258
|
+
`list_artifacts` already ends "Use get_artifact to read one". The prompt
|
|
259
|
+
carries the obligation; the tool descriptions carry the mechanics. */
|
|
260
|
+
'A `work_item` is what the work is about. Read its description and the artifacts already on it',
|
|
261
|
+
'before you decide anything.',
|
|
262
|
+
/* ═══ THE KIND THAT IS A METHOD RATHER THAN A SUBJECT. ═══ A skill is not
|
|
263
|
+
one more thing the request belongs against: it is a method a person
|
|
264
|
+
chose, and an agent that read it as a subject would quote it back and
|
|
265
|
+
carry on doing the work its own way. That is the fact it cannot discover
|
|
266
|
+
from the line above, so it is the sentence added, and the tool's own
|
|
267
|
+
description carries the rest.
|
|
225
268
|
|
|
226
269
|
═══ SAID WHENEVER ANYTHING IS ATTACHED, WHICH IS A CHOICE AND NOT AN
|
|
227
270
|
ACCIDENT. ═══ Both callers hold the rows, `kind` and all, so narrowing
|
|
@@ -249,6 +292,21 @@ const whatWasAttached = (attachments) => {
|
|
|
249
292
|
prompt. */
|
|
250
293
|
'A `credential` is a secret the person authorised for this work. Its name is above; the value',
|
|
251
294
|
'is readable only by the agents doing the work, with `get_credential`.',
|
|
295
|
+
/* ═══ THE KIND THAT IS A SEQUENCE RATHER THAN A METHOD OR A SUBJECT. ═══ A
|
|
296
|
+
workflow says what happens in what order, and the fact an agent cannot
|
|
297
|
+
discover from the line above is that ONE of the three levels is the one
|
|
298
|
+
that reads it. So this names the ROLE rather than instructing anybody,
|
|
299
|
+
following the credential sentence's shape of naming a tool the reader may
|
|
300
|
+
not hold: `get_workflow` is level 2's alone, and `levelOnePrompt` already
|
|
301
|
+
calls the level 2 agent "the owner", so the word lands at every level this
|
|
302
|
+
one sentence reaches.
|
|
303
|
+
|
|
304
|
+
AND IT SAYS NOTHING ABOUT WORKING STAGES ONE AT A TIME. That rule
|
|
305
|
+
overrides a default written in `workBrief`'s own IF YOU SPLIT IT block, so
|
|
306
|
+
it lives inside that block and nowhere else. A second copy here would be a
|
|
307
|
+
second place for it to drift. */
|
|
308
|
+
'A `workflow` is the process this work follows: stages, in an order it gives. The owner of this',
|
|
309
|
+
'conversation is the one who reads it, with `get_workflow`.',
|
|
252
310
|
];
|
|
253
311
|
return [
|
|
254
312
|
...(codebases.length === 0 ? [] : [
|
|
@@ -316,8 +374,9 @@ const projectCodebases = (codebases) => {
|
|
|
316
374
|
*/
|
|
317
375
|
export function levelOnePrompt(cardTitle, messages, produced, attachments = [], codebases = null) {
|
|
318
376
|
return [
|
|
319
|
-
'You are the
|
|
320
|
-
'the
|
|
377
|
+
'You are the short-lived launcher for one CTRL+SPC conversation. You do not do the work and',
|
|
378
|
+
'you do not answer the person. Your whole job is to clarify the destination once if you truly',
|
|
379
|
+
'cannot choose it, or launch exactly one owner and then exit.',
|
|
321
380
|
'',
|
|
322
381
|
'WHAT YOU CAN SEE',
|
|
323
382
|
'Your tools read the record. Read as much of it as the question needs.',
|
|
@@ -331,11 +390,12 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
|
|
|
331
390
|
'makes every true one worthless.',
|
|
332
391
|
'',
|
|
333
392
|
'WHAT YOU CAN SEND SOMEBODY TO DO',
|
|
334
|
-
'
|
|
335
|
-
'the
|
|
336
|
-
'
|
|
337
|
-
'
|
|
338
|
-
'
|
|
393
|
+
'Launch one owner for the conversation. Give it the complete responsibility and boundary. If',
|
|
394
|
+
'the request belongs to a registered codebase, name it. If it is record-only work, launch it',
|
|
395
|
+
'without a codebase. The owner talks to the person and may send workers of its own.',
|
|
396
|
+
'Your only question is a destination or codebase you truly cannot choose. NEVER ask the person',
|
|
397
|
+
'to make a work or product decision. When one registered codebase clearly fits, launch its owner',
|
|
398
|
+
'even if the work itself needs the person to choose between options; the owner asks that question.',
|
|
339
399
|
...projectCodebases(codebases),
|
|
340
400
|
'',
|
|
341
401
|
...WHILE_YOU_ARE_WORKING,
|
|
@@ -355,22 +415,17 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
|
|
|
355
415
|
'',
|
|
356
416
|
...messages.map((body, i) => `${i + 1}. ${body}`),
|
|
357
417
|
'',
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
' front of them as something they can open, so name what you made and stop there. Writing its',
|
|
361
|
-
' contents out again leaves them holding two copies of the same words, and the one they can edit',
|
|
362
|
-
' is not this one.',
|
|
363
|
-
]),
|
|
418
|
+
'AFTER YOU LAUNCH THE OWNER, EXIT IMMEDIATELY. Do not write a reply, wait for it, summarize what',
|
|
419
|
+
'it will do, or ask another question. CTRL+SPC publishes no stdout from this launcher.',
|
|
364
420
|
].join('\n');
|
|
365
421
|
}
|
|
366
422
|
/**
|
|
367
423
|
* What an agent sent to do a piece of work is told: its own job, the facts of
|
|
368
424
|
* where it is, and nothing about the conversation it came out of.
|
|
369
425
|
*
|
|
370
|
-
* ═══ THIS IS
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* than a description of it.
|
|
426
|
+
* ═══ THIS IS THE IMMUTABLE BRIEF. ═══ `panel3_runs.brief` stores it exactly.
|
|
427
|
+
* A level 3 process is started with it directly; a level 2 owner's activation
|
|
428
|
+
* prompt carries it verbatim with the live conversation around it.
|
|
374
429
|
*
|
|
375
430
|
* ═══ AND IT CARRIES NO PATH, WHICH IS CONSTRAINT 6. ═══ The working copy is
|
|
376
431
|
* the directory the process is started in, so it never has to be named — and
|
|
@@ -388,28 +443,121 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
|
|
|
388
443
|
* from nothing but a responsibility sentence somebody else wrote down.
|
|
389
444
|
*/
|
|
390
445
|
export function workBrief(level, responsibility, boundary, workItemId, attachments = [], codebase) {
|
|
446
|
+
/* ═══ ONLY WHEN A WORKFLOW IS ACTUALLY ON THE CARD. ═══ An owner is not
|
|
447
|
+
instructed about a process that is not there, which is the rule
|
|
448
|
+
`personAttachmentLines` already keeps by rendering nothing when nothing is
|
|
449
|
+
attached. The same prefix `whatWasAttached` partitions on, since both read
|
|
450
|
+
the one line `attachmentLine` writes. */
|
|
451
|
+
const hasWorkflow = attachments.some((attachment) => attachment.startsWith('workflow '));
|
|
391
452
|
return [
|
|
392
453
|
...(level === 2
|
|
393
454
|
? [
|
|
394
|
-
'You see one thing a person asked for through to done.
|
|
395
|
-
'
|
|
455
|
+
'You see one thing a person asked for through to done. Keep the whole picture and stay',
|
|
456
|
+
'available to the person while the work moves.',
|
|
457
|
+
'',
|
|
458
|
+
/* ═══ THE FIRST THING, EVERY TIME, AND IT IS NOT THE REPLY. ═══ Nothing
|
|
459
|
+
an owner writes reaches the person until its process exits, and a turn
|
|
460
|
+
that exits by asking writes no message at all. So on the walk of
|
|
461
|
+
2026-08-24 "Write a plan for this item" was silent for two and a half
|
|
462
|
+
minutes and then said a question, twice over, and Lane's requirement
|
|
463
|
+
is that every message he sends is acknowledged. `say` is the only
|
|
464
|
+
thing that can do that, because an acknowledgement cannot be an exit.
|
|
465
|
+
WRITTEN AS THE FIRST INSTRUCTION IN THE BRANCH because it is the first
|
|
466
|
+
act, and a rule about what to do first that arrives after four
|
|
467
|
+
paragraphs about the work is read after the work has started. */
|
|
468
|
+
'FIRST, SAY WHAT YOU ARE GOING TO DO',
|
|
469
|
+
'They have seen their own message and nothing else. Before you start, call `say` with one or',
|
|
470
|
+
'two sentences: what you are about to do about what they asked, and what they will get back.',
|
|
471
|
+
'Do this every time you are started with something new from them, including the very first',
|
|
472
|
+
'time, and before any reading that takes a while. It does not end your turn and it is not',
|
|
473
|
+
'your reply; you carry on straight afterwards.',
|
|
396
474
|
'',
|
|
397
|
-
'
|
|
398
|
-
'
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'
|
|
475
|
+
'TRACEABILITY BEFORE A CODEBASE FILE CHANGES',
|
|
476
|
+
'A codebase-file change means ANY file change: source, config, docs, tests, migrations,',
|
|
477
|
+
'generated inputs, and assets all count. "Non-code" means no codebase file changes at all.',
|
|
478
|
+
'When this work has a Work Item and will change a codebase file, you may investigate',
|
|
479
|
+
'read-only first. Any investigation you send must explicitly forbid file edits. Create one',
|
|
480
|
+
'useful `analysis` or `plan` artifact on the exact Work Item, carrying the document the work',
|
|
481
|
+
'is missing: a spec, a PRD, user stories or a repro is its CONTENT, not a second artifact.',
|
|
482
|
+
'For a bug: what reproduces it, the behaviour it has now, the behaviour it should have, the',
|
|
483
|
+
'cause and the correction. For something new: what a person will be able to do, and how you',
|
|
484
|
+
'would know it works.',
|
|
485
|
+
'If several Work Items were supplied, one is selected only when the person identifies it by',
|
|
486
|
+
'name, id, or an unambiguous reference in a message or answer. That selection stays in force',
|
|
487
|
+
'unless the person changes it. An attachment label, its order, similarity, or which seems most',
|
|
488
|
+
'likely is NEVER a selection by itself; the person explicitly referring to that label IS a',
|
|
489
|
+
'selection.',
|
|
490
|
+
'Until one is selected, do not',
|
|
491
|
+
'call `update_work_item`, create or update an artifact, dispatch work, or change any file.',
|
|
492
|
+
'Call `ask_question` once with no `work_item_id`, `answer_mode: single_select`, and the exact',
|
|
493
|
+
'attached Work Item names as options, and then stop.',
|
|
494
|
+
'Present the artifact with `ask_question`: name both `work_item_id` and',
|
|
495
|
+
'`related_artifact_id`, use `answer_mode: single_select` with `approve` and',
|
|
496
|
+
'`request changes`, and stop. Do not edit any file OR dispatch implementation until the',
|
|
497
|
+
'artifact revision that question presented is approved. If changes are requested, use',
|
|
498
|
+
'`update_artifact` on the SAME artifact, present its new revision in a new approval question,',
|
|
499
|
+
'and apply that same no-edit, no-implementation-dispatch fence until it is approved.',
|
|
500
|
+
'The only bypass is the person explicitly asking you to change codebase files WITHOUT a',
|
|
501
|
+
'Work Item artifact. Urgency, smallness, silence, or a generic "go ahead" is not a bypass.',
|
|
502
|
+
'When no codebase file will change, this gate does not apply.',
|
|
405
503
|
'',
|
|
406
|
-
'
|
|
407
|
-
'
|
|
504
|
+
'WHO DOES THE WORK IS YOUR JUDGEMENT',
|
|
505
|
+
'Substantial implementation, investigation, testing, and other substantial work normally',
|
|
506
|
+
'goes to the people you send. Before you start substantial multi-part work, send at least',
|
|
507
|
+
'one meaningful independent implementation, investigation, or test part. Multiple independent',
|
|
508
|
+
'file, module, or test parts belong with the people you send by default; do not keep all of',
|
|
509
|
+
'them just because you could complete every piece yourself.',
|
|
510
|
+
'Skip sending only when the whole turn is very small, or when sending any meaningful',
|
|
511
|
+
'independent part would genuinely make the result slower or less accurate. You keep the full',
|
|
512
|
+
'picture, answer the person, and put the results together.',
|
|
513
|
+
/* ═══ SENDING COMES BEFORE READING, AND THAT IS ABOUT THE PERSON RATHER
|
|
514
|
+
THAN ABOUT THROUGHPUT. ═══ The person reads nothing until this process
|
|
515
|
+
exits, so every minute the owner spends reading the codebase before it
|
|
516
|
+
sends anybody is a minute of pulse and no words. Measured on
|
|
517
|
+
2026-08-24: 101 seconds to the first message, of which about 80 were
|
|
518
|
+
the owner reading first and sending second.
|
|
519
|
+
IT IS STILL NOT A GATE. `ux.md` deleted the manager that never touches
|
|
520
|
+
code because that shape "required the coordinator to predict, at
|
|
521
|
+
dispatch time, whether a request was big. It cannot", and Issue 3 made
|
|
522
|
+
this judgement "never a mechanical gate". So this is which order to do
|
|
523
|
+
two things the owner was already going to do, not a rule about
|
|
524
|
+
whether. The paragraph above still decides whether. */
|
|
525
|
+
'',
|
|
526
|
+
'WHEN YOU ARE GOING TO SEND SOMEBODY, SEND THEM FIRST',
|
|
527
|
+
'Read what you need to write their piece, and no more; the rest of the reading is theirs to',
|
|
528
|
+
'do. Nothing you write reaches the person until you stop, so every minute you spend reading',
|
|
529
|
+
'before you send anybody is a minute they watch a line with no answer coming.',
|
|
408
530
|
'',
|
|
409
531
|
'IF YOU SPLIT IT',
|
|
410
532
|
'Everybody you send works in the SAME copy of the codebase as everybody else, at the same',
|
|
411
533
|
'time. Nothing keeps them out of each other\'s way, so anybody CHANGING files needs a piece',
|
|
412
534
|
'that touches nobody else\'s files, or has to be sent on their own.',
|
|
535
|
+
/* ═══ AND A WORKFLOW OVERRIDES THAT DEFAULT, INSIDE THE BLOCK THAT SETS
|
|
536
|
+
IT. ═══ Six lines above, the owner is told the opposite: multiple
|
|
537
|
+
independent parts belong with the people it sends, all working at the
|
|
538
|
+
same time. A workflow's stages ARE multiple parts, so without a
|
|
539
|
+
precedence rule the owner sends them all at once, the read-back
|
|
540
|
+
barrier restarts it ONCE, and the order the person chose the workflow
|
|
541
|
+
for is gone. Nothing in the record refuses a second dispatch before
|
|
542
|
+
the first has ended — the barrier re-arms an owner when EVERY child
|
|
543
|
+
has finished, which sequences stages only because the owner sends one
|
|
544
|
+
at a time. So the rule is here, in the immutable brief that is
|
|
545
|
+
replayed verbatim on every restart, and therefore in front of the
|
|
546
|
+
owner at every stage boundary rather than only the first.
|
|
547
|
+
|
|
548
|
+
THE SECOND SENTENCE IS NOT DECORATION. A dispatched worker gets a
|
|
549
|
+
working copy and the responsibility and boundary AND NOTHING ELSE, and
|
|
550
|
+
`get_workflow` is this level's alone. Without it, the product takes
|
|
551
|
+
deliberate trouble to hand the owner an untruncated stage document and
|
|
552
|
+
the agent that performs the stage receives a paraphrase of it. */
|
|
553
|
+
...(hasWorkflow
|
|
554
|
+
? [
|
|
555
|
+
'A WORKFLOW IS ATTACHED, AND ITS STAGES GO OUT ONE AT A TIME. Send somebody for the',
|
|
556
|
+
'first stage only, and do not start the next stage until the one before it has come',
|
|
557
|
+
'back. When you send somebody for a stage, put that stage\'s own document into what you',
|
|
558
|
+
'give them, in full: they cannot read the workflow themselves.',
|
|
559
|
+
]
|
|
560
|
+
: []),
|
|
413
561
|
'What you have already sent somebody to do is written down, and you are handed that list',
|
|
414
562
|
'rather than having to remember it. Anything on it has been sent. Nothing on it is sent again.',
|
|
415
563
|
'When every one of them has finished you are started again, with what each of them wrote,',
|
|
@@ -417,13 +565,15 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
|
|
|
417
565
|
'through you, so the reply that covers all of it is yours to write and nobody else\'s.',
|
|
418
566
|
'WHICH MEANS THE REPLY YOU WRITE NOW IS NOT THAT ANSWER. If you have sent anybody, you are',
|
|
419
567
|
'stopping in the middle, and what you write as you stop goes to the person as a message from',
|
|
420
|
-
'you.
|
|
421
|
-
'is the one you write when you come back.
|
|
422
|
-
'
|
|
568
|
+
'you. Write it. One short sentence, naming what has gone out and what you are waiting on:',
|
|
569
|
+
'the real answer is the one you write when you come back. Never stop silently. What you said',
|
|
570
|
+
'with `say` was about to happen; this is what has happened and what is still to come.',
|
|
423
571
|
]
|
|
424
572
|
: [
|
|
425
|
-
'You
|
|
426
|
-
'
|
|
573
|
+
'You own one piece of the work, the one below, and nothing else. Whoever sent you is doing',
|
|
574
|
+
'the rest. If your responsibility is investigation-only, read and report without changing',
|
|
575
|
+
'any file. The conversation owner handles the Work Item artifact and approval; do not',
|
|
576
|
+
'duplicate that record. If you were given approved implementation work, make that change.',
|
|
427
577
|
'',
|
|
428
578
|
'Somebody else may be working in this same copy of the codebase at the same time as you, on',
|
|
429
579
|
'a different piece. Stay inside what you were given and leave the rest of it alone.',
|
|
@@ -443,15 +593,33 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
|
|
|
443
593
|
] : []),
|
|
444
594
|
'',
|
|
445
595
|
'WHERE YOU ARE',
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
596
|
+
...(codebase
|
|
597
|
+
? [
|
|
598
|
+
'The directory you are in is a working copy of the codebase. Read it, run things in it, and',
|
|
599
|
+
'change it where that is what you were asked for. Name a file by its path inside this copy,',
|
|
600
|
+
'never by the full path from the root of this machine: the full one means nothing to the person',
|
|
601
|
+
'reading and is not ours to hand around.',
|
|
602
|
+
]
|
|
603
|
+
: [
|
|
604
|
+
'The directory you are in is an empty private workspace. No codebase checkout was attached to',
|
|
605
|
+
'this conversation. Work from the record and the tools you have; do not claim to have read or',
|
|
606
|
+
'changed project files.',
|
|
607
|
+
]),
|
|
450
608
|
'',
|
|
451
609
|
'WHAT YOU WERE AND WERE NOT GIVEN',
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
610
|
+
...(level === 2
|
|
611
|
+
? [
|
|
612
|
+
'This brief is your immutable responsibility and boundary. The visible conversation is',
|
|
613
|
+
'handed to you separately, in chronological order, whenever you are started. Read it as the',
|
|
614
|
+
'person wrote it; the launcher does not stand in for it. If something you genuinely need is',
|
|
615
|
+
'missing and only the person can supply it, you MUST call `ask_question` and stop instead',
|
|
616
|
+
'of saying it in your reply. Otherwise say what is missing plainly rather than guessing.',
|
|
617
|
+
]
|
|
618
|
+
: [
|
|
619
|
+
'What is above is the whole of it. You have not seen what anybody said, and you are not meant',
|
|
620
|
+
'to: what matters was written into what you own. If something you genuinely need is missing,',
|
|
621
|
+
'say so plainly in your reply rather than guessing at it.',
|
|
622
|
+
]),
|
|
455
623
|
'',
|
|
456
624
|
'WHAT CANNOT BE DECIDED HERE',
|
|
457
625
|
'Some things are not yours to settle, and guessing at one of those quietly is worse than any',
|
|
@@ -478,6 +646,105 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
|
|
|
478
646
|
: whatAWorkerSendsBack()),
|
|
479
647
|
].join('\n');
|
|
480
648
|
}
|
|
649
|
+
export const presentedArtifactAnswerContext = (artifact) => artifact === null
|
|
650
|
+
? []
|
|
651
|
+
: artifact.answer === 'request changes'
|
|
652
|
+
? [
|
|
653
|
+
'',
|
|
654
|
+
'ARTIFACT REVISION THIS ANSWER SENT BACK FOR CHANGES',
|
|
655
|
+
`Artifact ${artifact.id}, revision ${artifact.revision}.`,
|
|
656
|
+
'Do not edit any codebase file or dispatch implementation. Revise this SAME artifact as the',
|
|
657
|
+
'person requested, present its new revision in a new approval question, and stop.',
|
|
658
|
+
]
|
|
659
|
+
: [
|
|
660
|
+
'',
|
|
661
|
+
'ARTIFACT REVISION THIS ANSWER APPROVED',
|
|
662
|
+
`Artifact ${artifact.id}, revision ${artifact.revision}.`,
|
|
663
|
+
'Before editing any file or dispatching implementation, call `get_artifact` and compare its',
|
|
664
|
+
'current revision with this one. If they differ, this answer does not approve the current body:',
|
|
665
|
+
'present the current revision in a new approval question and stop.',
|
|
666
|
+
];
|
|
667
|
+
/**
|
|
668
|
+
* ═══ WHAT A RUN IS TOLD WHEN A PROCESS OF ITS OWN ENDED BEFORE IT FINISHED.
|
|
669
|
+
* ═══
|
|
670
|
+
*
|
|
671
|
+
* Named once here rather than written inline, because `respawnPrompt` says both
|
|
672
|
+
* of these too, in its own longer wording (`WHAT YOU ALREADY SENT SOMEBODY TO
|
|
673
|
+
* DO`, and `CARRY ON FROM WHERE THAT LEAVES YOU`). **The two must move
|
|
674
|
+
* together.** They are not folded into one string because doing that would
|
|
675
|
+
* rewrite prose that is already shipped and proven on the retry and recovery
|
|
676
|
+
* paths, which is not this slice's assignment; naming them is what makes the
|
|
677
|
+
* pairing findable by the next person to edit either.
|
|
678
|
+
*
|
|
679
|
+
* ═══ THEY SAY WHAT TO DO, AND NEVER WHY IT HAPPENED. ═══ Three things end a
|
|
680
|
+
* process early and they are not the same event: a harness that crashed, a
|
|
681
|
+
* machine that went down, and a person who said the work was going the wrong
|
|
682
|
+
* way. `resumePrompt` and `retryPrompt` exist as two functions precisely
|
|
683
|
+
* because telling an agent an untrue reason for its restart is what this file
|
|
684
|
+
* exists to prevent, so a sentence shared by all three may only say the part
|
|
685
|
+
* that is true of all three.
|
|
686
|
+
*/
|
|
687
|
+
const DO_NOT_SEND_SOMEBODY_TWICE = 'Do not send somebody to do a piece of work that is already in this list. Anything of theirs '
|
|
688
|
+
+ 'still going is still going, and asking again for what they are already doing costs the person twice.';
|
|
689
|
+
const LOOK_AT_THE_WORK_ITSELF = 'A process of yours ended before it finished, so what you last wrote down is what you had time to '
|
|
690
|
+
+ 'write, not everything that happened. Where your notes are silent, look at the work itself rather '
|
|
691
|
+
+ 'than starting it over.';
|
|
692
|
+
/**
|
|
693
|
+
* The complete one-shot context for the durable conversational owner.
|
|
694
|
+
*
|
|
695
|
+
* Visible history is supplied as one ordered list. A newly leased message is
|
|
696
|
+
* marked in place rather than appended, and a question plus its canonical
|
|
697
|
+
* answer is one event, so neither can occur twice in the prompt.
|
|
698
|
+
*/
|
|
699
|
+
export function ownerActivationPrompt(brief, report, events, children, childQuestion, currentArtifactAnswer = null,
|
|
700
|
+
/** A process of this run's own ended before it finished, so the working copy
|
|
701
|
+
* may hold work no report mentions and the worker list may hold one it
|
|
702
|
+
* dispatched a moment before. Never says WHICH ending it was: see
|
|
703
|
+
* `LOOK_AT_THE_WORK_ITSELF`. */
|
|
704
|
+
afterAProcessEnded = false) {
|
|
705
|
+
const history = events.map((event) => {
|
|
706
|
+
if (event.kind === 'question') {
|
|
707
|
+
return [
|
|
708
|
+
`CTRL+SPC QUESTION ${event.body}`,
|
|
709
|
+
event.answer ? `PERSON ANSWER ${event.answer}` : 'PERSON ANSWER waiting',
|
|
710
|
+
...(event.relatedArtifactId && event.relatedArtifactRevision
|
|
711
|
+
? [`PRESENTED ARTIFACT ${event.relatedArtifactId}, revision ${event.relatedArtifactRevision}`]
|
|
712
|
+
: []),
|
|
713
|
+
...presentedArtifactAnswerContext(currentArtifactAnswer?.questionId === event.id
|
|
714
|
+
? currentArtifactAnswer
|
|
715
|
+
: null),
|
|
716
|
+
].join('\n');
|
|
717
|
+
}
|
|
718
|
+
const who = event.kind === 'user' ? 'PERSON' : 'CTRL+SPC';
|
|
719
|
+
return `${who}${event.needsReply ? ` — REPLY TO THIS NOW (turn ${event.id})` : ''}\n${event.body}`;
|
|
720
|
+
});
|
|
721
|
+
return [
|
|
722
|
+
'YOU OWN THIS CTRL+SPC CONVERSATION.',
|
|
723
|
+
'You are this conversation\'s one owner, started for one serialized activation. Speak directly to the',
|
|
724
|
+
'person as CTRL+SPC. The launcher is finished and cannot answer or receive an escalation.',
|
|
725
|
+
'',
|
|
726
|
+
'VISIBLE CONVERSATION, OLDEST FIRST',
|
|
727
|
+
...(history.length === 0 ? ['Nothing has been said yet.'] : history),
|
|
728
|
+
...(childQuestion === null ? [] : [
|
|
729
|
+
'',
|
|
730
|
+
'A WORKER NEEDS YOUR DECISION',
|
|
731
|
+
`Question ${childQuestion.id}: ${childQuestion.question}`,
|
|
732
|
+
'Answer it with answer_escalation if you can. If the person must decide, ask them directly.',
|
|
733
|
+
]),
|
|
734
|
+
'',
|
|
735
|
+
'WHAT YOU LAST WROTE DOWN',
|
|
736
|
+
report ?? 'Nothing yet.',
|
|
737
|
+
'',
|
|
738
|
+
'PRIVATE WORKER SNAPSHOT',
|
|
739
|
+
...(children.length === 0 ? ['You have sent nobody.'] : children),
|
|
740
|
+
...(afterAProcessEnded ? ['', LOOK_AT_THE_WORK_ITSELF, DO_NOT_SEND_SOMEBODY_TWICE] : []),
|
|
741
|
+
'',
|
|
742
|
+
// THE BRIEF IS LAST, and these go before it: the brief ends with how to
|
|
743
|
+
// write the reply, and anything after it pushes that ending into the middle.
|
|
744
|
+
'YOUR IMMUTABLE BRIEF',
|
|
745
|
+
brief,
|
|
746
|
+
].join('\n');
|
|
747
|
+
}
|
|
481
748
|
/**
|
|
482
749
|
* What an agent that was stopped is started again with: the same brief, its own
|
|
483
750
|
* notes, and what it already sent others to do.
|
|
@@ -559,7 +826,7 @@ export function retryPrompt(brief, report, children) {
|
|
|
559
826
|
* something the agent is better placed to see." So it is named as a judgement
|
|
560
827
|
* the agent makes, in one sentence, rather than a rule about when to ask.
|
|
561
828
|
*/
|
|
562
|
-
export function answerPrompt(brief, report, children, question, answer) {
|
|
829
|
+
export function answerPrompt(brief, report, children, question, answer, relatedArtifact = null) {
|
|
563
830
|
return respawnPrompt([
|
|
564
831
|
'YOU STOPPED BECAUSE YOU COULD NOT GO ON WITHOUT KNOWING SOMETHING. HERE IT IS.',
|
|
565
832
|
'You are the same agent, started again, and everything you did before you asked is still there.',
|
|
@@ -569,6 +836,7 @@ export function answerPrompt(brief, report, children, question, answer) {
|
|
|
569
836
|
'',
|
|
570
837
|
'WHAT CAME BACK',
|
|
571
838
|
answer,
|
|
839
|
+
...presentedArtifactAnswerContext(relatedArtifact),
|
|
572
840
|
'',
|
|
573
841
|
'If this answers less than you asked, or turns out to change more than the piece you were sent',
|
|
574
842
|
'to do, ask again rather than deciding it yourself. Carrying on from a guess about something this',
|