@ctrl-spc/cs 0.7.6 → 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.
@@ -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 replying: a reply finishes the card, and an',
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
  '',
@@ -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';
@@ -1950,8 +1950,17 @@ export function ownerContinuationPrompt(brief, report, events, children, deliver
1950
1950
  const messages = events.filter((event) => event.kind === 'user' && event.needsReply);
1951
1951
  return [
1952
1952
  'CONTINUE THE CTRL+SPC CONVERSATION YOU ALREADY OWN.',
1953
+ OWNER_COMPLETION_RULES,
1953
1954
  'Use only the new facts below as the new turn. Keep coordinating the whole card and speak as CTRL+SPC.',
1954
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
+ ] : []),
1955
1964
  'NEW USER MESSAGES',
1956
1965
  ...(messages.length === 0
1957
1966
  ? ['None.']
@@ -2425,15 +2434,17 @@ export async function reconcileOwnerSessions(client, machineId, _machineHarness,
2425
2434
  async function killStopped(client, machineId) {
2426
2435
  const stopped = await returned(client
2427
2436
  .from('panel3_runs')
2428
- .select('id, card_id, pid, started_at, resumed_at')
2437
+ .select('id, card_id, pid, state, started_at, resumed_at')
2429
2438
  .eq('machine_id', machineId)
2430
- .in('state', ENDED_BY_THE_PERSON)
2439
+ .in('state', [...ENDED_BY_THE_PERSON, 'finished', 'failed'])
2431
2440
  .not('pid', 'is', null), 'read', 'the runs on this machine that are not coming back');
2432
2441
  if (stopped.length === 0)
2433
2442
  return;
2434
2443
  const booted = bootedAt();
2435
2444
  for (const run of stopped) {
2436
2445
  if (runProcessIsAlive(run, booted)) {
2446
+ if (!ENDED_BY_THE_PERSON.includes(run.state))
2447
+ continue;
2437
2448
  try {
2438
2449
  process.kill(run.pid);
2439
2450
  out(`stopped run ${run.id} card ${run.card_id} killed pid ${run.pid}`);
@@ -2455,11 +2466,11 @@ async function killStopped(client, machineId) {
2455
2466
  await returned(client
2456
2467
  .from('panel3_runs')
2457
2468
  .update({ pid: null })
2458
- // STILL NOT COMING BACK. Nothing starts either of these states, so
2459
- // this cannot lose a race, and saying so in the statement is what
2460
- // 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.
2461
2471
  .eq('id', run.id)
2462
- .in('state', ENDED_BY_THE_PERSON)
2472
+ .eq('pid', run.pid)
2473
+ .in('state', [...ENDED_BY_THE_PERSON, 'finished', 'failed'])
2463
2474
  .select('id'), 'clear the process id of', `run ${run.id}`);
2464
2475
  }
2465
2476
  catch (error) {
@@ -2562,7 +2573,9 @@ async function recoverStranded(client, tools, machineId, mine, hold) {
2562
2573
  await writeAnswer(client, run.id, run.card_id, null, run.process_token ?? undefined);
2563
2574
  }
2564
2575
  else {
2565
- const resumed = await activateOwner(client, tools, machineId, run.id, run.process_token, run.pid);
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);
2566
2579
  if (resumed)
2567
2580
  hold(run.id, resumed.settled);
2568
2581
  }
@@ -2914,7 +2927,7 @@ export async function sweepFinishedWorktrees(client) {
2914
2927
  const live = new Set((await returned(client.from('panel3_runs')
2915
2928
  .select('card_id')
2916
2929
  .in('card_id', cards)
2917
- .is('ended_at', null), 'read', 'which of those cards still have an agent going')).map((row) => row.card_id));
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));
2918
2931
  const touched = new Set();
2919
2932
  for (const one of held) {
2920
2933
  const cardIds = cardOfFolder.get(one.folder) ?? [];
@@ -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.6",
4
- "description": "CTRL+SPC minimal, reliable per-machine agent presence. Sign-in, auto-start, agent detection, heartbeat presence, and ping acknowledgement.",
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
  },