@ctrl-spc/cs 0.7.11 → 0.7.12

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/codebases.js CHANGED
@@ -101,6 +101,32 @@ export async function removeCodebase(client, codebaseId) {
101
101
  if (error)
102
102
  throw new Error(error.message);
103
103
  }
104
+ /** Saved work-item scope is shared by the reader and the dispatch guard. */
105
+ export async function listWorkItemCodebaseTargets(client, workItemId) {
106
+ const { data, error } = await client.from('cliv2_task_codebase_targets')
107
+ .select('git_remote_url').eq('task_id', workItemId).order('git_remote_url');
108
+ if (error)
109
+ throw new Error(`Could not read the saved work-item codebase targets: ${error.message}`);
110
+ return (data ?? []).map(row => row.git_remote_url);
111
+ }
112
+ /** The existing database transaction owns membership and final-target guards. */
113
+ export async function editWorkItemCodebaseTarget(client, workItemId, projectId, codebaseId, add) {
114
+ const codebases = await client.from('cliv2_codebases').select('git_remote_url')
115
+ .eq('id', codebaseId).eq('project_id', projectId);
116
+ if (codebases.error)
117
+ throw new Error(`Could not read the project codebase: ${codebases.error.message}`);
118
+ if (codebases.data?.length !== 1)
119
+ throw new Error("Could not read a codebase belonging to this work item's project.");
120
+ const { data, error } = await client.rpc('cliv2_edit_task_codebase_target', {
121
+ p_task_id: workItemId, p_git_remote_url: codebases.data[0].git_remote_url, p_add: add,
122
+ });
123
+ if (error)
124
+ throw new Error(`Could not edit the saved work-item codebase targets: ${error.message}`);
125
+ if (!Array.isArray(data) || !data.every(target => typeof target === 'string')) {
126
+ throw new Error('The saved work-item codebase targets could not be read back after editing.');
127
+ }
128
+ return data;
129
+ }
104
130
  /**
105
131
  * Record that THIS machine has a codebase's folder checked out — Phase 2
106
132
  * per-machine availability. Upserts one path-free row into the owner-scoped
@@ -407,35 +407,10 @@ settings = {}) {
407
407
  + `${branch}. Check that ${codebase.name} is a git repository on ${machineName}.`);
408
408
  }
409
409
  }
410
- /**
411
- * ═══ THE CARD IS OVER: COMMIT WHAT IS THERE, THEN TAKE THE COPY AWAY. ═══
412
- *
413
- * The commit comes first and is unconditional on there being something to
414
- * commit, so removing the copy cannot lose work. The BRANCH is never touched:
415
- * work that never landed survives until a person deals with it.
416
- *
417
- * It takes the folder alone, and that is deliberate: a worktree knows its own
418
- * repository, so the sweep needs neither the located checkout nor the codebase
419
- * row to clean one up.
420
- */
421
- /**
422
- * ═══ EVERYTHING IN A CARD'S COPY, ON ITS BRANCH. ═══
423
- *
424
- * `settleCardWorktree`'s own first three lines, extracted rather than rewritten,
425
- * because the landing needs the commit BEFORE the merge and the sweep needs it
426
- * before the removal. Committing in two places with two messages is how one card
427
- * comes to have two ideas of what its work is.
428
- *
429
- * ═══ ITS FAILURE IS COMPOSED, WHICH THE SWEEP NEVER NEEDED. ═══ The sweep
430
- * writes to stderr, so git's own text was harmless there. On the landing path a
431
- * failure reaches a person through an agent, and `execFileSync`'s message begins
432
- * `Command failed: git -C <absolute path>`. So this wraps in `worktreeForCard`'s
433
- * shape: what was composed here passes through, anything else is replaced.
434
- */
410
+ /** Commit only for an explicitly selected landing action. Cleanup never commits.
411
+ * Failures are path-free because the landing result reaches the hosted panel. */
435
412
  export function commitCardWork(folder,
436
- /* WHAT THE COPY IS, IN WORDS AND NEVER AS A PATH. The sweep has only the
437
- folder and says so; the landing has the codebase and the branch and names
438
- both, which is what a person reading the failure needs. */
413
+ // Name the codebase and branch in failures, without exposing a local path.
439
414
  describe) {
440
415
  try {
441
416
  git(folder, ['add', '-A']);
@@ -491,11 +466,13 @@ export function mergeIntoBase(source, branch, base, codebaseName) {
491
466
  }
492
467
  }
493
468
  export function settleCardWorktree(folder) {
494
- commitCardWork(folder, 'this card\'s working copy');
495
- /* FORCE, AFTER THE COMMIT. Everything tracked is now committed on the branch,
496
- so what `--force` overrides is only ignored files: an installed
497
- `node_modules`, a build output, which the next copy makes again. */
469
+ // Completion is not permission to commit. Preserve unfinished Git work,
470
+ // including staged and untracked files, for the person to review or resume.
471
+ if (git(folder, ['status', '--porcelain']).length > 0)
472
+ return false;
473
+ // Only ignored build/dependency files can remain in a clean copy.
498
474
  removeWorktree(folder);
475
+ return true;
499
476
  }
500
477
  /**
501
478
  * Every card copy this machine currently holds, as `<codebase id>/<folder>`
@@ -0,0 +1,96 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { z } from 'zod';
3
+ import { agentPath } from '../agents.js';
4
+ import { killTree, windowsSafeSpawn } from '../win-shell.js';
5
+ const modelPage = z.object({
6
+ data: z.array(z.object({
7
+ model: z.string().min(1),
8
+ displayName: z.string(),
9
+ description: z.string(),
10
+ defaultReasoningEffort: z.string(),
11
+ supportedReasoningEfforts: z.array(z.object({ reasoningEffort: z.string(), description: z.string() })),
12
+ isDefault: z.boolean(),
13
+ })),
14
+ nextCursor: z.string().nullable(),
15
+ });
16
+ /** Read the installed harness's catalog under its own account/config. No turn,
17
+ * session, product catalog, or generation is created. Keep provider IDs intact. */
18
+ export async function listCodexModels() {
19
+ const bin = agentPath('codex');
20
+ if (!bin)
21
+ throw new Error('Codex is not installed on this machine.');
22
+ const { args, shell } = windowsSafeSpawn(bin, ['app-server']);
23
+ return new Promise((resolve, reject) => {
24
+ const child = spawn(bin, args, { shell, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] });
25
+ const models = [];
26
+ const cursors = new Set();
27
+ let buffer = '';
28
+ let received = 0;
29
+ let settled = false;
30
+ const finish = (error) => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ clearTimeout(timer);
35
+ killTree(child);
36
+ if (error)
37
+ reject(error);
38
+ else
39
+ resolve(models);
40
+ };
41
+ const timer = setTimeout(() => finish(new Error('Codex model discovery timed out.')), 15_000);
42
+ const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
43
+ child.on('error', (error) => finish(error));
44
+ child.stdin.on('error', (error) => finish(error));
45
+ child.on('close', () => finish(new Error('Codex exited before returning its available models.')));
46
+ // Diagnostics can contain local configuration; never return them as catalog data.
47
+ child.stderr.resume();
48
+ child.stdout.setEncoding('utf8').on('data', (chunk) => {
49
+ if (settled)
50
+ return;
51
+ received += chunk.length;
52
+ if (received > 1_000_000)
53
+ return finish(new Error('Codex model discovery response was too large.'));
54
+ buffer += chunk;
55
+ let newline;
56
+ while (!settled && (newline = buffer.indexOf('\n')) >= 0) {
57
+ const line = buffer.slice(0, newline);
58
+ buffer = buffer.slice(newline + 1);
59
+ if (!line.trim())
60
+ continue;
61
+ try {
62
+ const response = JSON.parse(line);
63
+ if (response.id !== 1 && response.id !== 2)
64
+ continue;
65
+ if (response.error)
66
+ throw new Error('Codex rejected model discovery. Use the installed default or an explicit user choice.');
67
+ if (response.id === 1) {
68
+ send({ method: 'initialized' });
69
+ send({ id: 2, method: 'model/list', params: { limit: 100, includeHidden: false } });
70
+ }
71
+ else {
72
+ const page = modelPage.parse(response.result);
73
+ models.push(...page.data);
74
+ if (page.nextCursor !== null) {
75
+ if (cursors.has(page.nextCursor))
76
+ throw new Error('Codex model discovery repeated a page.');
77
+ cursors.add(page.nextCursor);
78
+ send({ id: 2, method: 'model/list', params: { limit: 100, includeHidden: false, cursor: page.nextCursor } });
79
+ }
80
+ else if (models.length === 0) {
81
+ finish(new Error('Codex returned no available models.'));
82
+ }
83
+ else
84
+ finish();
85
+ }
86
+ }
87
+ catch (error) {
88
+ finish(error instanceof SyntaxError || error instanceof z.ZodError
89
+ ? new Error('Codex returned an invalid available-model list. Use the installed default or an explicit user choice.')
90
+ : error instanceof Error ? error : new Error('Codex model discovery failed.'));
91
+ }
92
+ }
93
+ });
94
+ send({ id: 1, method: 'initialize', params: { clientInfo: { name: 'ctrl_spc_models', version: '1' } } });
95
+ });
96
+ }
@@ -1,4 +1,6 @@
1
1
  import { returned } from './client.js';
2
+ import { agentPath } from '../agents.js';
3
+ import { harness } from './spawn.js';
2
4
  /**
3
5
  * Read the designation. `cliv2_orchestrator_preference` carries `unique
4
6
  * (user_id)`, so RLS alone (`auth.uid() = user_id`) can never return more than
@@ -14,5 +16,18 @@ import { returned } from './client.js';
14
16
  export async function designatedCoordinator(client) {
15
17
  const rows = await returned(client.from('cliv2_orchestrator_preference').select('machine_id, agent'), 'read', 'which machine and harness are designated to coordinate');
16
18
  const row = rows[0];
19
+ if (row && (typeof row.machine_id !== 'string' || !['claude', 'codex'].includes(row.agent))) {
20
+ throw new Error('The saved primary agent is invalid; select Claude Code or Codex again.');
21
+ }
17
22
  return row ? { machineId: row.machine_id, agent: row.agent } : null;
18
23
  }
24
+ /** New work follows the shared choice. Existing runs keep their recorded harness. */
25
+ export async function selectedHarness(client, machineId) {
26
+ const designation = await designatedCoordinator(client);
27
+ const selected = designation?.machineId === machineId
28
+ ? designation.agent
29
+ : harness();
30
+ if (!agentPath(selected))
31
+ throw new Error(`${selected} is selected but is not installed on this machine`);
32
+ return selected;
33
+ }
@@ -333,8 +333,9 @@ export const whatWasAttached = (attachments) => {
333
333
  '',
334
334
  'THE CODEBASES THIS CARD TOUCHES',
335
335
  ...codebases,
336
- 'For a dispatched piece, its one codebase is named separately below. The other selected',
337
- 'codebases are separate work, not missing folders, so do not look for or report on them.',
336
+ 'Each worker stays in its separately assigned codebase. The conversation owner remains',
337
+ 'responsible for the whole request across every required codebase, including work delegated',
338
+ 'to other workers. A card attachment or launch brief is not a saved work-item target read.',
338
339
  ]),
339
340
  ...personAttachmentLines,
340
341
  ];
@@ -371,7 +372,9 @@ const LANDING_LINES = {
371
372
  ],
372
373
  main: [
373
374
  'Finished work in this codebase lands on the main branch, and the person is asked first.',
374
- 'When the card\'s work is done, call `offer_ending` with one sentence saying what was done.',
375
+ 'When the whole assignment is done, first write_report with work_complete=true and its verified',
376
+ 'results, then call `offer_ending` with one sentence saying what was done. Never offer an',
377
+ 'ending while you are planning, waiting for workers, or still owe work in another codebase.',
375
378
  'The product writes the question and both answers, and it performs the merge itself if they',
376
379
  'choose to put the work back. Landing is the product\'s job and never yours: never merge and',
377
380
  'never push.',
@@ -416,9 +419,10 @@ const projectCodebases = (codebases) => {
416
419
  : [
417
420
  ...codebases.map((codebase) => (`${codebase.name} — id ${codebase.id} — ${codebase.identity} — `
418
421
  + (codebase.located ? 'located on this machine' : 'not located on this machine'))),
419
- 'Choose the codebase that contains the work and include its id when you send somebody.',
420
- 'If several codebases are needed, send one person per codebase. If the request does not make',
421
- 'the choice clear, call `ask_question` once with `answer_mode` set to `multi_select`, put the',
422
+ 'For an attached work item, read its saved targets; this project list is not its selection.',
423
+ 'Launch one owner for the whole assignment. For several targets, that owner sends a scoped',
424
+ 'worker to each codebase. If the saved record and request do not make the choice clear,',
425
+ 'call `ask_question` once with `answer_mode` set to `multi_select`, put the',
422
426
  'plausible codebase names in `options`, and stop. Do not write a question as your reply.',
423
427
  ]),
424
428
  ];
@@ -479,6 +483,10 @@ export function levelOnePrompt(cardTitle, messages, produced, attachments = [],
479
483
  'Launch one owner for the conversation. Give it the complete responsibility and boundary. If',
480
484
  'the request belongs to a registered codebase, name it. If it is record-only work, launch it',
481
485
  'without a codebase. The owner talks to the person and may send workers of its own.',
486
+ 'For work on a saved Work Item, read get_work_item for its current saved codebase targets.',
487
+ 'Use those targets, not the project-wide choices or a remembered selection. Do not ask the',
488
+ 'person to repeat a saved selection. If several targets are required, give the ONE owner the',
489
+ 'whole assignment and every required target; it can dispatch a worker in each codebase.',
482
490
  'Your only question is a destination or codebase you truly cannot choose. NEVER ask the person',
483
491
  'to make a work or product decision. When one registered codebase clearly fits, launch its owner',
484
492
  'even if the work itself needs the person to choose between options; the owner asks that question.',
@@ -559,6 +567,9 @@ export const OWNER_COMPLETION_RULES = [
559
567
  'without declaring completion, the product starts you again to carry on.',
560
568
  'Only when the latest user assignment is fulfilled, call write_report with work_complete=true',
561
569
  'and a report explaining what fulfilled it. Check all required steps and verification first.',
570
+ 'Only offer_ending after every requested codebase has its required work verified. Owning',
571
+ 'one working copy does not narrow a multi-codebase request to that copy. Dispatch the remaining',
572
+ 'codebase work and read its results before offering completion.',
562
573
  'Then give the final answer and exit. Do not declare completion for an interim status answer.',
563
574
  'A failed or skipped required step is unfinished work, even if all workers have returned.',
564
575
  'Resolve it or ask for the real required intervention; never convert it into a completion caveat.',
@@ -602,6 +613,9 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
602
613
  'When asked to create a workflow, use create_workflow to save it; do not execute the workflow.',
603
614
  'Ask clarifying questions with ask_question only when the missing answer changes its behavior.',
604
615
  'Write the requested tool mentions and per-use permissions in the stage bodies using the tool schema instructions.',
616
+ 'Plain-prose requests must stay plain: do not add unrequested tool permissions or approval templates.',
617
+ 'For edits, read get_workflow first, reword retained stages with reword_stage, and keep their numbered references in edit_workflow. Do not replace retained stages or bypass a protected-body refusal.',
618
+ 'Only you can call get_workflow. If a child is assigned a workflow review, read it yourself first and put the complete relevant workflow content in its brief; do not assign it an unavailable tool call. Authoring and review do not authorize executing the workflow or its stages.',
605
619
  'The saved workflow appears as a reviewable card in this conversation. Do not substitute a plan artifact or a prose-only reply.',
606
620
  '',
607
621
  'TRACEABILITY BEFORE A CODEBASE FILE CHANGES',
@@ -661,7 +675,10 @@ export function workBrief(level, responsibility, boundary, workItemId, attachmen
661
675
  'before you send anybody is a minute they watch a line with no answer coming.',
662
676
  '',
663
677
  'IF YOU SPLIT IT',
664
- 'Everybody you send works in the SAME copy of the codebase as everybody else, at the same',
678
+ 'Everybody you send to the SAME codebase shares its copy; different codebases have separate',
679
+ 'copies. Read the Work Item\'s current saved targets before file work or dispatch, and use',
680
+ 'only those targets. Preserve the person\'s no-commit, no-merge and no-push instructions in',
681
+ 'every worker boundary and at completion. People in the same copy work at the same',
665
682
  'time. Nothing keeps them out of each other\'s way, so anybody CHANGING files needs a piece',
666
683
  'that touches nobody else\'s files, or has to be sent on their own.',
667
684
  /* ═══ AND A WORKFLOW OVERRIDES THAT DEFAULT, INSIDE THE BLOCK THAT SETS
@@ -1170,9 +1187,12 @@ export function modelChoiceRules(harness) {
1170
1187
  return [
1171
1188
  'MODEL AND EFFORT FOR EACH DISPATCH',
1172
1189
  `You run under ${harness}. Your dispatched children use the same harness.`,
1190
+ 'Claude and Codex name harnesses, not model identifiers. A request to use Codex gpt-5.5 means model gpt-5.5 under Codex; do not concatenate a harness label into the model argument. A harness-only request does not specify a model. If you do not know a supported model for the task, omit model to use the installed default rather than inventing an identifier.',
1173
1191
  'The dispatch tool accepts optional model and effort strings. Honor the user\'s exact stated model and effort for the work they apply to, including downstream workers. Copy each requested value byte-for-byte into the dispatch argument: do not expand aliases, normalize spelling or case, or replace it with a canonical provider identifier. Carry those wishes and their scope verbatim in every applicable responsibility and boundary.',
1174
1192
  'When the user requests separate workers or assigns choices to separate parts, call dispatch for those parts. Doing their work yourself does not satisfy that request. If product tools are deferred, discover dispatch before starting; use the product ask_question and write_report tools for questions and completion.',
1175
- 'When the user has not specified a value, choose for this child\'s task using your knowledge of this harness. Pass your choice in the tool arguments; a sentence alone does not select it. Different workers may use different choices.',
1193
+ harness === 'codex'
1194
+ ? 'When the user has not specified a model, call list_codex_models before choosing for this child. Choose a model ID from that live result and an effort it supports, according to the task. If discovery fails, omit autonomous model and effort choices and use installed defaults; do not guess from memory. The list does not override explicit user IDs or aliases. Different workers may use different choices. Pass choices in dispatch arguments; prose alone does not select them.'
1195
+ : 'When the user has not specified a value, choose for this child\'s task using supported Claude aliases such as sonnet or opus. Do not invent version-suffixed aliases such as sonnet-5 or haiku-4.5. If uncertain, omit model to use the installed default. Pass your choice in the tool arguments; a sentence alone does not select it. Different workers may use different choices.',
1176
1196
  'Omitting a value chooses the harness default (or the machine-local model default), never the parent\'s model or effort. No product model catalog exists. Pass provider values unchanged; do not silently substitute after a rejection.',
1177
1197
  ].join('\n');
1178
1198
  }