@ctrl-spc/cs 0.7.5 → 0.7.7
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/panel3/prompt.js +20 -1
- package/dist/panel3/run.js +48 -11
- package/dist/panel3/tools.js +14 -4
- package/package.json +2 -2
package/dist/panel3/prompt.js
CHANGED
|
@@ -550,6 +550,23 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
|
|
|
550
550
|
* Every child it starts, and every grandchild THEY start, would be working
|
|
551
551
|
* from nothing but a responsibility sentence somebody else wrote down.
|
|
552
552
|
*/
|
|
553
|
+
export const OWNER_COMPLETION_RULES = [
|
|
554
|
+
'ASSIGNMENT STATUS — THIS OVERRIDES OLDER ENDING INSTRUCTIONS IN YOUR BRIEF.',
|
|
555
|
+
'A reply or process exit does not finish the assignment. Keep working through all remaining',
|
|
556
|
+
'required steps and checks. Status questions never cancel work already in progress.',
|
|
557
|
+
'When you yield to workers, write_report with what remains and exit; you resume with results.',
|
|
558
|
+
'When no worker is active and work remains, continue it yourself or dispatch it. If you exit',
|
|
559
|
+
'without declaring completion, the product starts you again to carry on.',
|
|
560
|
+
'Only when the latest user assignment is fulfilled, call write_report with work_complete=true',
|
|
561
|
+
'and a report explaining what fulfilled it. Check all required steps and verification first.',
|
|
562
|
+
'Then give the final answer and exit. Do not declare completion for an interim status answer.',
|
|
563
|
+
'A failed or skipped required step is unfinished work, even if all workers have returned.',
|
|
564
|
+
'Resolve it or ask for the real required intervention; never convert it into a completion caveat.',
|
|
565
|
+
'If a required user action prevents progress, use ask_question and stop. It must name the actual',
|
|
566
|
+
'approval, credential or manual action needed. Never ask just because no agent is running.',
|
|
567
|
+
'A handover explicitly requested by the person may finish that request without finishing a',
|
|
568
|
+
'feature. Say exactly what was handed over and do not claim the unfinished feature is done.',
|
|
569
|
+
].join('\n');
|
|
553
570
|
export function workBrief(level, responsibility, boundary, workItemId, attachments = [], codebase) {
|
|
554
571
|
/* ═══ ONLY WHEN A WORKFLOW IS ACTUALLY ON THE CARD. ═══ An owner is not
|
|
555
572
|
instructed about a process that is not there, which is the rule
|
|
@@ -562,6 +579,7 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
|
|
|
562
579
|
? [
|
|
563
580
|
'You see one thing a person asked for through to done. Keep the whole picture and stay',
|
|
564
581
|
'available to the person while the work moves.',
|
|
582
|
+
OWNER_COMPLETION_RULES,
|
|
565
583
|
'',
|
|
566
584
|
/* ═══ THE FIRST THING, EVERY TIME, AND IT IS NOT THE REPLY. ═══ Nothing
|
|
567
585
|
an owner writes reaches the person until its process exits, and a turn
|
|
@@ -836,7 +854,7 @@ export const landingOutcomeContext = (landing) => {
|
|
|
836
854
|
'They chose neither of the two answers, so nothing was merged and the work is still on '
|
|
837
855
|
+ `${landing.branch ?? 'its branch'}.`,
|
|
838
856
|
'Read what they actually wrote and answer it. If they still want the work put back, offer the',
|
|
839
|
-
'ending again with `offer_ending` rather than
|
|
857
|
+
'ending again with `offer_ending` rather than declaring completion: an',
|
|
840
858
|
'offer leaves it with them.',
|
|
841
859
|
];
|
|
842
860
|
}
|
|
@@ -915,6 +933,7 @@ landing = null) {
|
|
|
915
933
|
});
|
|
916
934
|
return [
|
|
917
935
|
'YOU OWN THIS CTRL+SPC CONVERSATION.',
|
|
936
|
+
OWNER_COMPLETION_RULES,
|
|
918
937
|
'You are this conversation\'s one owner, started for one serialized activation. Speak directly to the',
|
|
919
938
|
'person as CTRL+SPC. The launcher is finished and cannot answer or receive an escalation.',
|
|
920
939
|
'',
|
package/dist/panel3/run.js
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
// `import type` loses its v3 header in the published `dist/`.
|
|
134
134
|
import { out, returned, signedInClient } from './client.js';
|
|
135
135
|
import { readableWriteError } from '../firewall.js';
|
|
136
|
-
import { answerPrompt, escalationPrompt, levelOnePrompt, ownerActivationPrompt, readBackPrompt, landingOutcomeContext, presentedArtifactAnswerContext, resumePrompt, retryPrompt, standingRules, whatWasAttached, workingRules, } from './prompt.js';
|
|
136
|
+
import { answerPrompt, escalationPrompt, levelOnePrompt, ownerActivationPrompt, readBackPrompt, landingOutcomeContext, presentedArtifactAnswerContext, resumePrompt, retryPrompt, standingRules, whatWasAttached, workingRules, OWNER_COMPLETION_RULES, } from './prompt.js';
|
|
137
137
|
import { ASK_CONTENT_COLUMNS, attachmentLine, gitRulesFor, loadAttachments, loadOutputNames, outputOf, recordBaseProtection, standingRulesFor, withAskContent, } from './show.js';
|
|
138
138
|
import { forgetSecrets, redactSecrets } from './secrets.js';
|
|
139
139
|
import { sayListening, stopListening } from './presence.js';
|
|
@@ -148,6 +148,7 @@ import { listCodebases } from '../codebases.js';
|
|
|
148
148
|
import { killTree, processIsAlive } from '../win-shell.js';
|
|
149
149
|
import { hostname, uptime } from 'node:os';
|
|
150
150
|
import { execFileSync } from 'node:child_process';
|
|
151
|
+
import { existsSync } from 'node:fs';
|
|
151
152
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
152
153
|
import { join } from 'node:path';
|
|
153
154
|
const USAGE = 'usage: run [--once]';
|
|
@@ -1030,6 +1031,14 @@ export async function picturesOnDisk(client, cardId, where, level) {
|
|
|
1030
1031
|
.eq('card_id', cardId).order('created_at'), 'read', `the pictures on card ${cardId}`);
|
|
1031
1032
|
if (images.length === 0)
|
|
1032
1033
|
return [];
|
|
1034
|
+
/* ═══ A CWD THAT IS GONE IS A FAILURE, NOT A FOLDER TO MAKE. ═══ `recursive`
|
|
1035
|
+
below would rebuild a removed worktree as a plain directory holding one
|
|
1036
|
+
`ctrl-spc-images/`, which is exactly the folder `worktreeForCard` then
|
|
1037
|
+
refuses on every start (.bugs/.resolved/20260905-card-stuck-not-a-working-copy). */
|
|
1038
|
+
if (!existsSync(where.cwd)) {
|
|
1039
|
+
throw new Error('This card\'s working copy went away while its agent was starting. Send your message '
|
|
1040
|
+
+ 'again and it is made afresh.');
|
|
1041
|
+
}
|
|
1033
1042
|
const folder = join(gitDirOf(where.cwd) ?? where.cwd, 'ctrl-spc-images');
|
|
1034
1043
|
await mkdir(folder, { recursive: true });
|
|
1035
1044
|
const lines = [];
|
|
@@ -1941,8 +1950,17 @@ export function ownerContinuationPrompt(brief, report, events, children, deliver
|
|
|
1941
1950
|
const messages = events.filter((event) => event.kind === 'user' && event.needsReply);
|
|
1942
1951
|
return [
|
|
1943
1952
|
'CONTINUE THE CTRL+SPC CONVERSATION YOU ALREADY OWN.',
|
|
1953
|
+
OWNER_COMPLETION_RULES,
|
|
1944
1954
|
'Use only the new facts below as the new turn. Keep coordinating the whole card and speak as CTRL+SPC.',
|
|
1945
1955
|
'',
|
|
1956
|
+
...(messages.length === 0 && delivered === null ? [
|
|
1957
|
+
'WHY YOU WERE STARTED AGAIN',
|
|
1958
|
+
'The assignment is still open. Your previous final reply did not record completion.',
|
|
1959
|
+
'Do not repeat a completion reply by itself. If the assignment is fulfilled, call',
|
|
1960
|
+
'write_report with work_complete=true NOW, then give the final answer. Otherwise do the',
|
|
1961
|
+
'remaining work or resolve the new worker results below. No new user message is needed.',
|
|
1962
|
+
'',
|
|
1963
|
+
] : []),
|
|
1946
1964
|
'NEW USER MESSAGES',
|
|
1947
1965
|
...(messages.length === 0
|
|
1948
1966
|
? ['None.']
|
|
@@ -2416,15 +2434,17 @@ export async function reconcileOwnerSessions(client, machineId, _machineHarness,
|
|
|
2416
2434
|
async function killStopped(client, machineId) {
|
|
2417
2435
|
const stopped = await returned(client
|
|
2418
2436
|
.from('panel3_runs')
|
|
2419
|
-
.select('id, card_id, pid, started_at, resumed_at')
|
|
2437
|
+
.select('id, card_id, pid, state, started_at, resumed_at')
|
|
2420
2438
|
.eq('machine_id', machineId)
|
|
2421
|
-
.in('state', ENDED_BY_THE_PERSON)
|
|
2439
|
+
.in('state', [...ENDED_BY_THE_PERSON, 'finished', 'failed'])
|
|
2422
2440
|
.not('pid', 'is', null), 'read', 'the runs on this machine that are not coming back');
|
|
2423
2441
|
if (stopped.length === 0)
|
|
2424
2442
|
return;
|
|
2425
2443
|
const booted = bootedAt();
|
|
2426
2444
|
for (const run of stopped) {
|
|
2427
2445
|
if (runProcessIsAlive(run, booted)) {
|
|
2446
|
+
if (!ENDED_BY_THE_PERSON.includes(run.state))
|
|
2447
|
+
continue;
|
|
2428
2448
|
try {
|
|
2429
2449
|
process.kill(run.pid);
|
|
2430
2450
|
out(`stopped run ${run.id} card ${run.card_id} killed pid ${run.pid}`);
|
|
@@ -2446,11 +2466,11 @@ async function killStopped(client, machineId) {
|
|
|
2446
2466
|
await returned(client
|
|
2447
2467
|
.from('panel3_runs')
|
|
2448
2468
|
.update({ pid: null })
|
|
2449
|
-
//
|
|
2450
|
-
//
|
|
2451
|
-
// keeps that true if something one day does.
|
|
2469
|
+
// A finished owner may activate again between this read and write.
|
|
2470
|
+
// Only acknowledge the exact process whose exit was observed.
|
|
2452
2471
|
.eq('id', run.id)
|
|
2453
|
-
.
|
|
2472
|
+
.eq('pid', run.pid)
|
|
2473
|
+
.in('state', [...ENDED_BY_THE_PERSON, 'finished', 'failed'])
|
|
2454
2474
|
.select('id'), 'clear the process id of', `run ${run.id}`);
|
|
2455
2475
|
}
|
|
2456
2476
|
catch (error) {
|
|
@@ -2553,7 +2573,9 @@ async function recoverStranded(client, tools, machineId, mine, hold) {
|
|
|
2553
2573
|
await writeAnswer(client, run.id, run.card_id, null, run.process_token ?? undefined);
|
|
2554
2574
|
}
|
|
2555
2575
|
else {
|
|
2556
|
-
|
|
2576
|
+
// This process belonged to a previous daemon. Use the fenced recovery
|
|
2577
|
+
// claim, not a failed-attempt retry whose budget may already be spent.
|
|
2578
|
+
const resumed = await activateOwner(client, tools, machineId, run.id, run.process_token, null);
|
|
2557
2579
|
if (resumed)
|
|
2558
2580
|
hold(run.id, resumed.settled);
|
|
2559
2581
|
}
|
|
@@ -2847,6 +2869,8 @@ export async function takeHandedBack(client, tools, machineId, mine, hold) {
|
|
|
2847
2869
|
* normalised into a constant getter below, so there is one shape inside `run`
|
|
2848
2870
|
* and no branch anywhere near the poll.
|
|
2849
2871
|
*/
|
|
2872
|
+
/** The card states in which nobody will start again on the card's copy. */
|
|
2873
|
+
const CARD_IS_OVER = ['done', 'failed', 'stopped'];
|
|
2850
2874
|
/**
|
|
2851
2875
|
* ═══ THE CARD IS OVER, SO ITS COPY GOES — AND THE BRANCH STAYS. ═══
|
|
2852
2876
|
*
|
|
@@ -2865,7 +2889,7 @@ export async function takeHandedBack(client, tools, machineId, mine, hold) {
|
|
|
2865
2889
|
* A folder no row explains is LEFT ALONE. Nothing here deletes something it
|
|
2866
2890
|
* cannot account for.
|
|
2867
2891
|
*/
|
|
2868
|
-
async function sweepFinishedWorktrees(client) {
|
|
2892
|
+
export async function sweepFinishedWorktrees(client) {
|
|
2869
2893
|
const held = worktreesOnThisMachine();
|
|
2870
2894
|
if (held.length === 0)
|
|
2871
2895
|
return;
|
|
@@ -2889,14 +2913,27 @@ async function sweepFinishedWorktrees(client) {
|
|
|
2889
2913
|
const cards = [...new Set([...cardOfFolder.values()].flat())];
|
|
2890
2914
|
if (cards.length === 0)
|
|
2891
2915
|
return;
|
|
2916
|
+
/* ═══ THE CARD IS ASKED, NOT ONLY ITS RUNS. ═══ A conversational owner is
|
|
2917
|
+
parked between wakes with `ended_at` set, so the second its last worker
|
|
2918
|
+
finishes, no run on the card is open, while the card itself still reads
|
|
2919
|
+
`working`. Reading the runs alone called that card finished and took the
|
|
2920
|
+
copy out from under the owner as it was starting
|
|
2921
|
+
(.bugs/.resolved/20260905-card-stuck-not-a-working-copy). The card's own
|
|
2922
|
+
state is the record's word for whether it is over; the runs still guard a
|
|
2923
|
+
card that is over on paper while a process of it is going. */
|
|
2924
|
+
const over = new Set((await returned(client.from('panel3_cards')
|
|
2925
|
+
.select('id, state')
|
|
2926
|
+
.in('id', cards), 'read', 'which of those cards are over')).filter((row) => CARD_IS_OVER.includes(row.state)).map((row) => row.id));
|
|
2892
2927
|
const live = new Set((await returned(client.from('panel3_runs')
|
|
2893
2928
|
.select('card_id')
|
|
2894
2929
|
.in('card_id', cards)
|
|
2895
|
-
.
|
|
2930
|
+
.or('ended_at.is.null,pid.not.is.null'), 'read', 'which of those cards still have an agent going')).map((row) => row.card_id));
|
|
2896
2931
|
const touched = new Set();
|
|
2897
2932
|
for (const one of held) {
|
|
2898
2933
|
const cardIds = cardOfFolder.get(one.folder) ?? [];
|
|
2899
|
-
if (cardIds.length === 0
|
|
2934
|
+
if (cardIds.length === 0)
|
|
2935
|
+
continue;
|
|
2936
|
+
if (!cardIds.every((cardId) => over.has(cardId) && !live.has(cardId)))
|
|
2900
2937
|
continue;
|
|
2901
2938
|
try {
|
|
2902
2939
|
settleCardWorktree(one.folder);
|
package/dist/panel3/tools.js
CHANGED
|
@@ -2481,11 +2481,21 @@ const TOOLS = [
|
|
|
2481
2481
|
description: 'Write down what is true NOW: what you have done, what you decided and why, and what is still '
|
|
2482
2482
|
+ 'open. It REPLACES your last report rather than adding to it, and it is what you are handed if '
|
|
2483
2483
|
+ 'you are respawned, so write it as the thing you would want to read to carry on. Write it as '
|
|
2484
|
-
+ 'you go, not only at the end. '
|
|
2484
|
+
+ 'you go, not only at the end. The conversation owner MUST set work_complete=true only when '
|
|
2485
|
+
+ 'the latest user assignment is fulfilled, including its required steps and checks. A status '
|
|
2486
|
+
+ 'reply is not completion. This records intent; Done waits for all agents to exit. '
|
|
2485
2487
|
+ FIREWALL_WRITING_RULE,
|
|
2486
|
-
input: { report: z.string().min(1) },
|
|
2487
|
-
handler: async ({ client, runId, processToken }, args) => {
|
|
2488
|
-
const { report } = args;
|
|
2488
|
+
input: { report: z.string().min(1), work_complete: z.boolean().optional() },
|
|
2489
|
+
handler: async ({ client, runId, processToken, isOwner }, args) => {
|
|
2490
|
+
const { report, work_complete } = args;
|
|
2491
|
+
if (work_complete) {
|
|
2492
|
+
if (!isOwner || processToken === undefined)
|
|
2493
|
+
throw new Error('Only the conversation owner can declare the assignment complete.');
|
|
2494
|
+
await whileRunning(client.rpc('panel3_request_completion', {
|
|
2495
|
+
p_run_id: runId, p_process_token: processToken, p_summary: report,
|
|
2496
|
+
}), runId, 'declare completion of');
|
|
2497
|
+
return 'Completion recorded. Give your final answer and exit. Done waits until all processes have exited.';
|
|
2498
|
+
}
|
|
2489
2499
|
await whileRunning(processToken === undefined
|
|
2490
2500
|
? client.from('panel3_runs').update({ report }).eq('id', runId)
|
|
2491
2501
|
.in('state', STILL_WRITING).is('ended_at', null).select('id')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctrl-spc/cs",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "CTRL+SPC
|
|
3
|
+
"version": "0.7.7",
|
|
4
|
+
"description": "CTRL+SPC \u2014 minimal, reliable per-machine agent presence. Sign-in, auto-start, agent detection, heartbeat presence, and ping acknowledgement.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
7
7
|
},
|