@ctrl-spc/cs 0.7.1 → 0.7.2

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.
@@ -105,7 +105,7 @@
105
105
  * agent naming something out of its reach stops nothing and is told so.
106
106
  *
107
107
  * ═══ AND THE KILL IS STILL THE DAEMON'S. ═══ The record is the intent, exactly
108
- * as it is for the person's own Stop: `cs3 run` kills the processes of stopped
108
+ * as it is for the person's own Stop: `panel3/cli.js run` kills the processes of stopped
109
109
  * runs on its own machine, on the poll it already has. Nothing here waits for
110
110
  * that, and nothing here claims it has happened.
111
111
  */
@@ -122,7 +122,7 @@ import { rememberSecret, redactArgs } from './secrets.js';
122
122
  import { workBrief } from './prompt.js';
123
123
  import { listCodebases } from '../codebases.js';
124
124
  import { readWorkflow } from '../workflows.js';
125
- import { ASK_CONTENT_COLUMNS, attachmentLine, loadAttachments, withAskContent, } from './show.js';
125
+ import { ASK_CONTENT_COLUMNS, attachmentLine, gitRulesFor, loadAttachments, setMainBranch, withAskContent, } from './show.js';
126
126
  // ---------------------------------------------------------------------------
127
127
  // READING AND WRITING. Every query goes through the same guard every other v3
128
128
  // command uses, so a failed read can never reach an agent as an empty list.
@@ -342,6 +342,48 @@ const DECIDING = {
342
342
  + 'for free_text. A SUGGESTION, NOT A BALLOT — the person may always answer in their own words '
343
343
  + 'instead, so offer what you would act on rather than every possibility.'),
344
344
  };
345
+ // ---------------------------------------------------------------------------
346
+ // PROJECT CONTEXT (19/project-context-7b). `public.project_documents` — the
347
+ // reference a team writes in the web app: architecture, design notes, how a
348
+ // subsystem came to be the way it is. Read when the work needs it, at every
349
+ // level, and ADDED TO by the levels that do the work.
350
+ //
351
+ // ═══ IT IS NOT THE SAME TABLE AS THE STANDING RULES, AND THAT IS THE POINT.
352
+ // ═══ `public.agent_instructions` is an OBLIGATION and arrives in the prompt,
353
+ // whole, before an agent acts; no tool here reads or writes it, at any level. A
354
+ // reference an agent chooses to read and a mandate it is handed are two
355
+ // different things, and the schema keeps them apart so a tool cannot confuse
356
+ // them (see 20260919090000_agent_instructions.sql).
357
+ /**
358
+ * ═══ THE READ IS BOUNDED, AND IT SAYS WHEN IT BOUNDED SOMETHING. ═══
359
+ *
360
+ * `project_documents.content` is unbounded `text` and a project may hold any
361
+ * number of documents, so an unbounded read would put a team's whole corpus into
362
+ * the same window the mandatory standing-rules block needs — and would do it
363
+ * silently. There is no existing row or byte limit anywhere in this file to
364
+ * inherit: `mcp.ts` has one of the right shape, and conventions.md rule 2 forbids
365
+ * `panel3/` to import it.
366
+ *
367
+ * TWO BOUNDS RATHER THAN ONE, because they catch different shapes: many small
368
+ * documents, and one enormous one. Whichever bites, the response ends by saying
369
+ * how many documents it did not show and how to ask for fewer — a truncation
370
+ * nobody is told about reads as a project with nothing else written down, which
371
+ * is worse than the read failing.
372
+ */
373
+ const CONTEXT_DOCUMENT_ROWS = 50;
374
+ const CONTEXT_DOCUMENT_CHARACTERS = 60000;
375
+ /**
376
+ * Which project the caller's card is filed under, or null when it has none.
377
+ *
378
+ * `panel3_cards.project_id` IS NULLABLE and a record-only card genuinely has no
379
+ * project, so null is a fact rather than a failure and each caller words its own
380
+ * refusal. A failed READ throws through `rows`, which is a different thing again
381
+ * and must never be flattened into "no project".
382
+ */
383
+ async function projectOfCard(caller) {
384
+ const cards = await rows(caller.client.from('panel3_cards').select('project_id').eq('id', caller.cardId), 'read', `the project for card ${caller.cardId}`);
385
+ return cards[0]?.project_id ?? null;
386
+ }
345
387
  const ALL = [1, 2, 3];
346
388
  const line = (...parts) => parts.filter(Boolean).join(' ');
347
389
  const listed = (items, empty) => (items.length === 0 ? empty : items.join('\n'));
@@ -906,6 +948,85 @@ const TOOLS = [
906
948
  ].join('\n');
907
949
  },
908
950
  },
951
+ {
952
+ name: 'get_project_context',
953
+ /* ALL THREE, straight off ux.md's "Who gets what" table, where
954
+ `get_project_context` has always sat in the "Read the record" row. Reading
955
+ what a team has written down is not a privilege any level is denied. */
956
+ levels: ALL,
957
+ description: 'The documentation this project\'s team has written: architecture, design notes, conventions, '
958
+ + 'how a subsystem came to be the way it is. Read it when the work needs it — before you '
959
+ + 'design something, before you follow a convention you are guessing at, and before you tell '
960
+ + 'the person how this project does something. Pass a codebase id to get that codebase\'s own '
961
+ + 'documents as well as the project\'s.',
962
+ input: {
963
+ codebase_id: z.string().uuid().optional().describe('A registered project codebase, to get its OWN documents alongside the project-wide ones. '
964
+ + 'Leave it out for the project-wide documents alone.'),
965
+ },
966
+ handler: async (caller, args) => {
967
+ const { codebase_id } = args;
968
+ const projectId = await projectOfCard(caller);
969
+ if (!projectId) {
970
+ return 'This conversation is not filed under a project, so there is no project context to read.';
971
+ }
972
+ const scoped = caller.client
973
+ .from('project_documents')
974
+ .select('id, title, type, codebase_id, content')
975
+ .eq('project_id', projectId);
976
+ const documents = await rows((codebase_id === undefined
977
+ ? scoped.is('codebase_id', null)
978
+ : scoped.or(`codebase_id.is.null,codebase_id.eq.${codebase_id}`))
979
+ /* MOST SPECIFIC FIRST, the order ux.md gives for the two scopes: a
980
+ codebase's documents beat the project's for work in that codebase,
981
+ so they are read first and are the ones that survive the bound. */
982
+ .order('codebase_id', { nullsFirst: false })
983
+ .order('created_at')
984
+ /* ONE MORE THAN THE CAP, so "there are more" is something this read
985
+ OBSERVED rather than something it assumed from a full page. The
986
+ extra row is never rendered; it only makes the closing line honest,
987
+ and it is why the count below is "at least". */
988
+ .limit(CONTEXT_DOCUMENT_ROWS + 1), 'read', `the project context on project ${projectId}`);
989
+ const withinRows = documents.slice(0, CONTEXT_DOCUMENT_ROWS);
990
+ const pastRowCap = documents.length > CONTEXT_DOCUMENT_ROWS;
991
+ /* THE CHARACTER WALK. The first document is always included whatever its
992
+ size: a project whose one document is larger than the whole budget must
993
+ still get an answer, and truncating a document's BODY would hand an
994
+ agent half a convention with no sign of where it stopped. */
995
+ const shown = [];
996
+ let used = 0;
997
+ for (const document of withinRows) {
998
+ if (used + document.content.length > CONTEXT_DOCUMENT_CHARACTERS && shown.length > 0)
999
+ break;
1000
+ shown.push(document);
1001
+ used += document.content.length;
1002
+ }
1003
+ if (shown.length === 0) {
1004
+ return codebase_id === undefined
1005
+ ? 'This project has no context documents yet.'
1006
+ : 'This project and that codebase have no context documents yet.';
1007
+ }
1008
+ const cut = withinRows.length - shown.length;
1009
+ return [
1010
+ ...shown.flatMap((document) => [
1011
+ `--- ${document.title} (${document.type}${document.codebase_id === null ? ', project' : ', this codebase'}) id ${document.id} ---`,
1012
+ document.content,
1013
+ '',
1014
+ ]),
1015
+ /* ═══ SAID WHENEVER SOMETHING WAS LEFT OUT, AND NEVER OTHERWISE. ═══ A
1016
+ complete answer that ends by discussing its own limits teaches an
1017
+ agent to distrust a read that was fine. */
1018
+ ...(cut > 0 || pastRowCap
1019
+ ? [
1020
+ `NOT EVERYTHING IS ABOVE: at least ${cut + (pastRowCap ? 1 : 0)} more `
1021
+ + `${cut + (pastRowCap ? 1 : 0) === 1 ? 'document was' : 'documents were'} not shown. `
1022
+ + `This read is capped at ${CONTEXT_DOCUMENT_ROWS} documents and `
1023
+ + `${CONTEXT_DOCUMENT_CHARACTERS} characters, and the most specific documents come `
1024
+ + 'first. Ask again without a codebase, or for one codebase at a time, to narrow it.',
1025
+ ]
1026
+ : []),
1027
+ ].join('\n');
1028
+ },
1029
+ },
909
1030
  {
910
1031
  name: 'list_cards',
911
1032
  /* ═══ LEVEL 1 ONLY, AND THIS OVERRIDES plan.md's TABLE. ═══ The plan grants
@@ -1032,6 +1153,136 @@ const TOOLS = [
1032
1153
  // the placement tools. Editing an item as the work moves, and producing an
1033
1154
  // object, are two other rows of that same table and they are NOT level 1's:
1034
1155
  // see the next section, which is where they now live.
1156
+ /**
1157
+ * ═══ THE ANSWER TO THE ONE GIT QUESTION A PERSON IS EVER ASKED. ═══
1158
+ *
1159
+ * worktrees-8, story 3: a codebase whose base branch is genuinely ambiguous is
1160
+ * settled by asking the person ONCE, and the answer is stored on the CODEBASE
1161
+ * rather than on the card that prompted it, so nobody is asked twice.
1162
+ *
1163
+ * A TOOL RATHER THAN A SIDE EFFECT OF THE ANSWER, for two reasons. The answer
1164
+ * has to land on the codebase and an ask lands on a card. And the only other
1165
+ * route is teaching the questions surface to write a settings table, which
1166
+ * would couple two things that have no reason to know each other.
1167
+ *
1168
+ * LEVEL 1 ONLY. It is the coordinator that sees across cards, that holds
1169
+ * `ask_question`, and that needs the fact before it dispatches. A level 2 or 3
1170
+ * agent asking would be asking about a card, mid-work, having already started
1171
+ * in the wrong place.
1172
+ */
1173
+ {
1174
+ name: 'set_main_branch',
1175
+ levels: [1],
1176
+ description: 'Record which branch finished work in a codebase should go back to. Use this ONLY after asking '
1177
+ + 'the person and getting an answer: it is the organization\'s setting, it applies to every '
1178
+ + 'member and every future card in that codebase, and nobody is asked again. Never guess it, '
1179
+ + 'and never call this to change a branch somebody already set.',
1180
+ input: {
1181
+ codebase_id: z.string().describe('The codebase id, as the codebase list gave it to you.'),
1182
+ branch: z.string().min(1).describe('The branch name exactly as the person chose it.'),
1183
+ },
1184
+ handler: async (caller, args) => {
1185
+ const { codebase_id, branch } = args;
1186
+ /* THROUGH `show.ts`, WHICH IS THE ONE FILE UNDER panel3/ THAT MAY NAME
1187
+ THIS TABLE. conventions.md records why, and
1188
+ panel3-isolation.contract.test.mjs pins it: reads and writes alike go
1189
+ through one file, so a grep for the table finds every caller. */
1190
+ await setMainBranch(caller.client, codebase_id, branch);
1191
+ return `Work in that codebase now starts from ${branch} and goes back to it. `
1192
+ + 'Nobody will be asked again.';
1193
+ },
1194
+ },
1195
+ /**
1196
+ * ═══ THE AGENT ASKS FOR THE OFFER; THE PRODUCT WRITES IT. ═══
1197
+ *
1198
+ * worktrees-8 C1. `ask_question`'s options are typed by a model out of prose,
1199
+ * so any product rule that reads them back is a rule about transcription. This
1200
+ * tool carries ONE agent-typed string, `what_was_done`, and nothing is ever
1201
+ * compared against it: the question, both answers and the merge itself are the
1202
+ * product's, exactly as an artifact approval already is.
1203
+ *
1204
+ * ═══ IT READS STAMPS, IT DOES NOT RESOLVE ANYTHING. ═══ `branch` and `base`
1205
+ * were written by `cardWorktree` precisely so a second reader would not
1206
+ * re-implement the derivation. No git runs here.
1207
+ */
1208
+ {
1209
+ name: 'offer_ending',
1210
+ /* ═══ LEVEL 2 ONLY, AND THE DATABASE RECHECKS THAT IT IS THE OWNER. ═══
1211
+ Level 1 never holds a card's branch, and level 3 has no ending and does
1212
+ not speak to the person. */
1213
+ levels: [2],
1214
+ description: 'Offer the person the ending for this card: put the finished work onto the main branch, or '
1215
+ + 'leave it on its branch. Call this when the card\'s work is DONE and the codebase lands on '
1216
+ + 'the main branch. You do not write the question or the answers and you never merge anything: '
1217
+ + 'the product composes both, and it performs the merge itself if they choose to put the work '
1218
+ + 'back. Say in one sentence what was done, in their words. After this call, stop immediately: '
1219
+ + 'you are started again with what the product did with their answer.',
1220
+ input: {
1221
+ what_was_done: z.string().min(1).describe('One sentence, in the person\'s words, saying what this card\'s work actually changed. It is '
1222
+ + 'the only part of the offer you write.'),
1223
+ },
1224
+ handler: async (caller, args) => {
1225
+ const { what_was_done } = args;
1226
+ const { client, runId, processToken } = caller;
1227
+ if (!caller.isOwner || processToken === undefined) {
1228
+ throw new Error('NOTHING WAS WRITTEN. Only this card\'s current conversation owner can offer the ending.');
1229
+ }
1230
+ if (what_was_done.length > PERSON_READS_LIMIT) {
1231
+ throw new Error(`NOTHING WAS WRITTEN and nobody was asked: that is ${what_was_done.length} characters and `
1232
+ + `they read it on a card three inches wide. The limit is ${PERSON_READS_LIMIT}. One `
1233
+ + 'sentence saying what changed.');
1234
+ }
1235
+ const run = await only(client.from('panel3_runs')
1236
+ .select('codebase_id, branch, base, card:panel3_cards!panel3_runs_card_id_fkey(project_id)')
1237
+ .eq('id', runId), 'read', 'which branch this card\'s work is on');
1238
+ if (!run.codebase_id || !run.branch || !run.base || !run.card?.project_id) {
1239
+ throw new Error('NOTHING WAS WRITTEN. This card has no codebase, branch and base recorded, so there is no '
1240
+ + 'ending to offer. Say what you did and finish the card.');
1241
+ }
1242
+ const rules = await gitRulesFor(client, run.codebase_id, run.card.project_id);
1243
+ if (rules.landing !== 'main') {
1244
+ throw new Error('NOTHING WAS WRITTEN. Finished work in this codebase does not go onto the main branch, so '
1245
+ + 'there is no ending to offer. Say where the work is and finish the card.');
1246
+ }
1247
+ const [ask] = await rows(client.rpc('panel3_ask', {
1248
+ p_run_id: runId,
1249
+ /* THE QUESTION AND THE CONTEXT ARE COMPOSED HERE, from the two stamps,
1250
+ which is why story 4's scenario can promise the branch and the
1251
+ separation are in it. The agent's sentence is appended to the
1252
+ context and decides nothing. */
1253
+ p_question: `Put this work onto ${run.base}?`,
1254
+ p_category: 'Finished work',
1255
+ p_context: `The work on this card is finished and is on branch ${run.branch}, in a copy of its own, `
1256
+ + 'apart from anything else working in this codebase on this computer. '
1257
+ + what_was_done,
1258
+ /* ═══ PASSED AND THEN IGNORED, AND BOTH HALVES ARE DELIBERATE. ═══ The
1259
+ statement overwrites the mode and the options with the product's own
1260
+ when `p_offers_landing` is true, which is where they are enforced.
1261
+ They are named here only because the twelve-argument core carries no
1262
+ defaults, so PostgREST resolves the overload by the exact argument
1263
+ set it is given. */
1264
+ p_answer_mode: 'single_select',
1265
+ p_options: [],
1266
+ p_question_id: null,
1267
+ p_work_item_id: null,
1268
+ p_ask_person: true,
1269
+ p_process_token: processToken,
1270
+ p_related_artifact_id: null,
1271
+ p_offers_landing: true,
1272
+ }), 'put', 'the ending where it can be answered');
1273
+ if (!ask || ask.ask_id === null) {
1274
+ throw new Error(`NOTHING WAS WRITTEN and nobody was asked: run ${runId} has already ended, or a question `
1275
+ + `is already waiting on you${ask?.held_question ? ` (${ask.held_question})` : ''}. Deal `
1276
+ + 'with that first.');
1277
+ }
1278
+ return [
1279
+ `The ending is with the person now, id ${ask.ask_id}. They were asked whether this work `
1280
+ + `should go onto ${run.base}, with the product's own two answers.`,
1281
+ 'YOU HAVE STOPPED. Do not merge anything and do not finish the card: when they answer, you',
1282
+ 'are started again and told what the product did.',
1283
+ ].join('\n');
1284
+ },
1285
+ },
1035
1286
  {
1036
1287
  name: 'create_epic',
1037
1288
  levels: [1],
@@ -1198,6 +1449,77 @@ const TOOLS = [
1198
1449
  return `Updated artifact ${artifact.title ?? artifact.id}.`;
1199
1450
  },
1200
1451
  },
1452
+ {
1453
+ name: 'add_project_context',
1454
+ /* ═══ THE LEVELS THAT DO THE WORK, AND ux.md IS AMENDED RATHER THAN ARGUED
1455
+ PAST. ═══ A context document is a PRODUCED OBJECT, not board structure, so
1456
+ it does not fall under "product structure is level 1's alone"; and the
1457
+ levels that do the work are the levels that learn something worth keeping,
1458
+ while level 1 launches and exits. ux.md's "Who gets what" table carries
1459
+ this ruling, and `panel3-tools.contract.test.mjs` pins all three lists to
1460
+ it exhaustively. */
1461
+ levels: [2, 3],
1462
+ /* ═══ ADD, AND NEVER CHANGE OR REMOVE. ═══ `project_documents` has no
1463
+ history, no version UI and no DELETE policy — hard delete is unreachable
1464
+ rather than merely un-surfaced. That was safe while no agent could write
1465
+ these rows at all. An agent UPDATE would permanently overwrite a person's
1466
+ own text with nothing to restore from, policed only by a sentence in a
1467
+ description. So the tool adds and never changes, and the name says so
1468
+ rather than the description having to. */
1469
+ description: 'Write one new document into this project\'s context — the reference the team keeps for work '
1470
+ + 'like this. Use it when the conversation SETTLES something the project should keep: a '
1471
+ + 'convention agreed, how a subsystem actually fits together, why an approach was rejected. '
1472
+ + 'It adds a document and never changes or removes one, so do not use it to correct something '
1473
+ + 'already written; say what is wrong in your report instead. Not for the work you were sent '
1474
+ + 'to do — that goes in an artifact on the work item.',
1475
+ input: {
1476
+ title: z.string().min(1).max(100).describe('What this document is, in a few words a teammate would search for. 100 characters at most.'),
1477
+ type: z.enum(['instructions', 'architecture', 'design', 'conventions', 'other']).describe('Which kind of reference this is, matching the five the web app offers.'),
1478
+ content: z.string().min(1).describe('The document itself, in markdown, complete. It is read by people and by agents on later '
1479
+ + 'work, neither of whom has seen this conversation.'),
1480
+ scope: z.enum(['project', 'codebase']).optional().describe('`codebase` files it under the codebase you are working in, for something true of that repo '
1481
+ + 'and no other. Defaults to `project`, which every codebase inherits.'),
1482
+ },
1483
+ handler: async (caller, args) => {
1484
+ const a = args;
1485
+ /* ═══ THE PROJECT COMES OFF THE RUN'S OWN CARD, NEVER OFF AN ARGUMENT.
1486
+ ═══ There is no `project_id` input, deliberately: RLS is org-scoped, so
1487
+ a card in one project could otherwise write a document into a sibling
1488
+ project in the same organization, and nothing about that would look like
1489
+ a mistake afterwards. */
1490
+ const projectId = await projectOfCard(caller);
1491
+ if (!projectId) {
1492
+ throw new Error('NOTHING WAS WRITTEN: this conversation is not filed under a project, so there is no '
1493
+ + 'project context to add to. Put what you learned in your report instead.');
1494
+ }
1495
+ let codebaseId = null;
1496
+ if (a.scope === 'codebase') {
1497
+ /* THE RUN'S OWN CODEBASE, AND NO OTHER, for the same reason the project
1498
+ is not an argument. A level 2 owner on a record-only card has none,
1499
+ and that is a refusal rather than a silent fall back to project
1500
+ scope: filing a codebase-specific note project-wide puts it in front
1501
+ of work it does not apply to. */
1502
+ const runs = await rows(caller.client.from('panel3_runs').select('codebase_id').eq('id', caller.runId), 'read', `the codebase run ${caller.runId} belongs to`);
1503
+ codebaseId = runs[0]?.codebase_id ?? null;
1504
+ if (!codebaseId) {
1505
+ throw new Error('NOTHING WAS WRITTEN: this work is not attached to a codebase, so it has no codebase '
1506
+ + 'to file a document under. Call this again with scope `project` if it holds for the '
1507
+ + 'whole project.');
1508
+ }
1509
+ }
1510
+ const document = await only(caller.client.from('project_documents').insert({
1511
+ project_id: projectId,
1512
+ codebase_id: codebaseId,
1513
+ title: a.title.trim(),
1514
+ type: a.type,
1515
+ content: a.content,
1516
+ }).select('id, title'), 'write', 'a project context document');
1517
+ return (`Written. "${document.title}" is in this project's context now, id ${document.id}`
1518
+ + `${codebaseId === null ? '' : ', filed under this codebase'}. `
1519
+ + 'The person will find it under Project settings. You cannot change or remove it, so if it '
1520
+ + 'needs correcting, say so rather than writing a second one.');
1521
+ },
1522
+ },
1201
1523
  // ── Say something now ────────────────────────────────────────────────────
1202
1524
  //
1203
1525
  // ═══ LEVEL 2 ONLY, AND THE DATABASE IS WHAT ENFORCES IT. ═══ `panel3_say`
@@ -1500,8 +1822,7 @@ const TOOLS = [
1500
1822
  }
1501
1823
  let codebase = null;
1502
1824
  if (codebase_id) {
1503
- const cards = await rows(caller.client.from('panel3_cards').select('project_id').eq('id', caller.cardId), 'read', `the project for card ${caller.cardId}`);
1504
- const projectId = cards[0]?.project_id;
1825
+ const projectId = await projectOfCard(caller);
1505
1826
  if (!projectId) {
1506
1827
  throw new Error('This conversation is not filed under a project, so it has no codebase to use.');
1507
1828
  }
@@ -1632,7 +1953,7 @@ async function receipt({ client, runId, cardId }, kind, refId, label) {
1632
1953
  await only(client.from('panel3_outputs').insert({ card_id: cardId, run_id: runId, kind, ref_id: refId, label }).select('id'), 'record', `the ${kind} on the card`);
1633
1954
  }
1634
1955
  /** The names one level is served, in the order they are registered. Exported for
1635
- * the same reason `cs3 show` exists: a rule nobody can print is a rule nobody
1956
+ * the same reason `cs show` exists: a rule nobody can print is a rule nobody
1636
1957
  * can check. */
1637
1958
  export function toolNamesForLevel(level, isOwner = false) {
1638
1959
  return TOOLS.filter((t) => t.levels.includes(level) && !(isOwner && t.name === 'escalate'))
package/dist/presence.js CHANGED
@@ -2,6 +2,7 @@ import { platform } from 'node:os';
2
2
  import { getClient } from './supabase.js';
3
3
  import { getMachineIdentity, mcpToken, supersededMachineIds, clearSupersededMachineIds } from './config.js';
4
4
  import { detectAgents } from './agents.js';
5
+ import { installSkills } from './skills.js';
5
6
  import { startToolsServer, stopToolsServer, toolsServerStatus, registerWithClaude, registerWithCodex, unregisterFromClaude, unregisterFromCodex, agentRegStatus, heartbeatOpenSessions, setToolsClient, } from './mcp.js';
6
7
  import { HEARTBEAT_INTERVAL_MS, COMMAND_POLL_INTERVAL_MS, ORCHESTRATOR_POLL_INTERVAL_MS } from './env.js';
7
8
  import { buildPresenceHeartbeatPayload } from './presence-heartbeat.js';
@@ -317,6 +318,13 @@ export async function startPresence() {
317
318
  // presence heartbeat above, so it's caught and warned like everything here.
318
319
  try {
319
320
  await startToolsServer({ client, userId, machineId: identity.id });
321
+ /* The instructions belong to the release, so they are rewritten beside
322
+ registration on every start: the pair is "make this machine's agents
323
+ ready", and a machine registered against current tools while reading a
324
+ stale document is the exact failure this rewrite exists to kill. */
325
+ const unwritten = installSkills(agents);
326
+ if (unwritten.length)
327
+ console.warn(`Could not write the ctrl-spc skill: ${unwritten.join(', ')}`);
320
328
  if (agents.includes('claude'))
321
329
  registerWithClaude(toolsServerStatus().port);
322
330
  if (agents.includes('codex'))
package/dist/skills.js ADDED
@@ -0,0 +1,165 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ /**
5
+ * THE INSTRUCTIONS THE AGENT FOLLOWS ARE THE RELEASE'S, NOT THE MACHINE'S.
6
+ *
7
+ * These files used to be written only by the deprecated v1 CLI, so on a machine
8
+ * that stopped running v1 they froze: every installed copy still ends in
9
+ * `Tools live at http://localhost:4590/mcp`, a port this product has NEVER used
10
+ * (the real one is 4579, and an agent reaches the tools BY NAME, with no address
11
+ * at all). An agent whose tools were missing therefore probed a dead port,
12
+ * concluded the server was down, and told the user to repair a machine that was
13
+ * working. Writing them from `cs` on every start is what makes an install unable
14
+ * to disagree with itself.
15
+ */
16
+ // Verbatim from the web/skill contract the copied `/ctrl-spc work <id>`
17
+ // invocation depends on.
18
+ const SKILL_DESCRIPTION = 'Work a CTRL+SPC work item or artifact by ID — delegates read-only exploration and writes analysis/plans/specs/diagrams/mocks/wireframes back via the ctrl-spc MCP tools. Use when the user pastes /ctrl-spc work <id> or /ctrl-spc artifact <id>.';
19
+ /** Home the skill files are written under. `CTRL_SPC_HOME` is the existing
20
+ * documented name for exactly this (docs/cli-surface-catalog.md), reused
21
+ * rather than renamed so one machine has one sandbox-home concept. */
22
+ function skillHome() {
23
+ return process.env.CTRL_SPC_HOME || homedir();
24
+ }
25
+ function claudeSkillPath() {
26
+ return join(skillHome(), '.claude', 'skills', 'ctrl-spc', 'SKILL.md');
27
+ }
28
+ /**
29
+ * Codex's three. `.agents` is NOT optional and not a legacy leftover: it is a
30
+ * second skill root that no isolation reaches (not a per-run `CODEX_HOME`, not
31
+ * `--ignore-user-config`, not `skill_search = false`, all three measured in
32
+ * codex-home.ts), and a resolved bug recorded that in 9 of 9 isolated runs
33
+ * Codex's FIRST action was to read that exact file. The leak cannot be closed
34
+ * here; what can be fixed is that the document it leaks is current.
35
+ */
36
+ function codexTargets() {
37
+ return [
38
+ { path: join(skillHome(), '.agents', 'skills', 'ctrl-spc', 'SKILL.md'), text: SKILL_TEXT },
39
+ { path: join(skillHome(), '.codex', 'skills', 'ctrl-spc', 'SKILL.md'), text: SKILL_TEXT },
40
+ // The deprecated custom-prompt file, kept as a fallback for users who still
41
+ // invoke it explicitly as `/prompts:ctrl-spc ...`. No frontmatter.
42
+ { path: join(skillHome(), '.codex', 'prompts', 'ctrl-spc.md'), text: PROMPT_TEXT },
43
+ ];
44
+ }
45
+ /**
46
+ * Write the ctrl-spc skill for each detected agent. ALWAYS OVERWRITES (Lane,
47
+ * 2026-08-26): no content comparison, no prompt, no backup, so a hand-edited or
48
+ * stale file cannot outlive the release that contradicts it.
49
+ *
50
+ * Returns the paths it could not write rather than only warning, and never
51
+ * throws: a SILENT write failure leaves the agent reading the stale file while
52
+ * the daemon reports itself healthy, which is precisely the failure this exists
53
+ * to kill. Registration must not break because a skill file could not be
54
+ * written, so the caller warns and carries on.
55
+ */
56
+ export function installSkills(agents) {
57
+ const targets = [
58
+ ...(agents.includes('claude') ? [{ path: claudeSkillPath(), text: SKILL_TEXT }] : []),
59
+ ...(agents.includes('codex') ? codexTargets() : []),
60
+ ];
61
+ const failed = [];
62
+ for (const { path, text } of targets) {
63
+ try {
64
+ mkdirSync(dirname(path), { recursive: true });
65
+ writeFileSync(path, text, 'utf8');
66
+ }
67
+ catch {
68
+ failed.push(path);
69
+ }
70
+ }
71
+ return failed;
72
+ }
73
+ /**
74
+ * The working protocol, then the section that matters here: what an agent does
75
+ * when the tools are NOT in its list. It names no address, because there is
76
+ * none to name — `cs status` is the only diagnosis.
77
+ */
78
+ const PROTOCOL_BODY = `Resolve a CTRL+SPC work item or artifact pasted as \`/ctrl-spc work <id>\` or \`/ctrl-spc artifact <id>\`, then follow the ctrl-spc working protocol:
79
+
80
+ WORKFLOW AUTHORITY — when \`get_task.workflow.enabled\` is true, follow only its current stage, stored instructions, capabilities, requirements, and gate. Never infer or skip a stage. Persist each required artifact, then call \`hand_off_stage\` with the work item id and this request id, and stop: the next stage is worked by a fresh agent reading the record. It refuses a stage with unfinished steps and the last stage of the process, where you answer and stop instead. Review gates advance only after explicit user approval in the web app or conversation; requested changes stay in the same stage. Only final approval sets Done.
81
+
82
+ HARD STOP — unclear novel feature (No workflow only): when \`workflow.enabled\` is false, after \`record_context_exploration\` succeeds, if the task does not explicitly request a build or name a deliverable, the next tool call MUST be \`ask_question\` with category \`intent\`, the exact question “What should I produce for this feature?”, \`answer_mode = multi_select\`, and options \`build\`, \`plan\`, \`spec\`, \`diagram\`, \`mock\`, and \`wireframe\`. Then call \`end_work\` with reason \`pending_user_answer\` and outcome \`blocked\`. In that run, never call \`get_task\` again, \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or any other tool between the context artifact and \`ask_question\`. The context artifact is the only allowed artifact. Do not copy findings into the task description before the user answers. A read-only execution sandbox is not a missing checkout and must not change this intent question. The user may select one or more: build, plan, spec, diagram, mock, wireframe.
83
+
84
+ Before the context artifact exists, never state or imply a requested deliverable and never mention repository counts, contents, tests, findings, or likely impact. If progress commentary is required, use only: “The required CTRL+SPC context review is in progress; no decision or repository change has been made.” After the context artifact and before the intent question, either say nothing or use only: “The required context review is saved to the work item. I am applying the task's explicit intent gate now.”
85
+
86
+ 1. Resolve without side effects — call \`get_task\` before anything else. It is read-only: it does not change status, presence, claims, or reservations. The ID is the only context in the paste; everything current lives in ctrl-spc.
87
+ 2. Begin explicitly — call \`begin_work\` after reading the topology. If the user supplied an exact instruction alongside the copied command, pass it as \`prompt_instruction\`; never invent one. The first live run is coordinator and \`begin_work\` moves Backlog to In Progress; later runs join as collaborators. Only the coordinator changes status. Never mark an item \`done\` autonomously.
88
+ 3. Repair missing context — if \`begin_work\` reports an unavailable checkout, warn the user and offer to fetch or link it. Never fetch without approval. If the user explicitly continues without it, call \`acknowledge_unavailable_checkout\`, then call \`begin_work\` again after every missing scope is fetched or acknowledged.
89
+ 4. Delegate exploration — every pasted work item MUST use at least one read-only subagent. After \`begin_work\` joins, partition every available repository scope exactly once across bounded subagent invocations. Every child prompt must explicitly list its assigned \`repository_scope_ids\`; across the batch each available ID appears exactly once. When using Codex \`spawn_agent\`, MUST pass \`fork_turns: "none"\`; never pass \`all\` or a recent-turn count. Give each child a self-contained assignment containing only its exact scope IDs, checkout paths, inspection focus, read-only restrictions, and required report shape. Never include or inherit the parent \`/ctrl-spc work\` command. The child must not call any ctrl-spc tool—including \`get_task\` or \`begin_work\`—or pick up the work item; it only inspects assigned paths and returns its report. If zero scopes are available after explicit acknowledgements, one child inspects the task/artifact context and reports those unavailable scopes without pretending they were reviewed. Children inspect only: no file mutation, path reservation, ctrl-spc call, \`tee\`, shell redirection, temp/capture file, install, formatter, cache-generating command, or any command that can write. Inspection-first is sufficient; do not decide intent before reports are persisted. Each report uses \`{ agent, focus, repository_scope_ids, inspected_paths, findings, likely_impact, unresolved_questions }\`. If no subagent is available, do not explore directly: call \`record_context_exploration\` with \`blocked_reason = subagents_unavailable\`, tell the user, and stop.
90
+ 5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create or update the work item's stable-purpose context artifact before deciding what to do, asking a task question, planning, reserving, or editing. Every agent and workflow stage revises the same task-level context artifact instead of creating another card. Pass the structured reports plus a separate coverage array; include every active repository scope exactly once, and give unavailable scopes their acknowledgement. Before the tool succeeds, commentary may state process only: never disclose findings, conclusions, likely impact, or decisions. After persistence, terminal summaries may reference the persisted artifact/question but must add no unpersisted facts.
91
+ 6. Decide or ask deterministically — assigned workflow stage data decides the work. Without a workflow, classify intent only from the work item's explicit wording and accepted answers, never from findings. Findings do not authorize a deliverable. Infer and perform the action only when the item explicitly names it. Otherwise persist only the necessary question with \`ask_question\`. Choose \`free_text\` for an open response, \`single_select\` for exactly one choice, or \`multi_select\` for one or more choices; choice modes require 2–12 concise, unique options. For a bug, ask for reproduction steps, expected behavior, or actual behavior only when each is missing and not discoverable. For a novel feature with no explicit build action or deliverable, apply the HARD STOP above: the mandatory next mutating call after \`record_context_exploration\` is the specified multi-select \`ask_question\`. Generic goals such as “improve,” “coordinate,” “prepare,” or “support” are not deliverables. In this state do not call \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or \`create_task\`; do not decide, edit, or complete. The context analysis is the only allowed artifact before the answer. Ask before overriding an unmet dependency, splitting work, fetching, or accepting a collision handoff. After \`ask_question\` succeeds, do no more work in that run: call \`end_work\` with reason \`pending_user_answer\` and outcome \`blocked\`, then wait for a later pasted run.
92
+ 7. Persist every accepted answer and finding — \`ask_question\` creates first-class decisions and returns decision IDs for the web app to display and answer. When the user answers in the agent conversation instead, use \`record_user_input\` with one structured response per Decision: \`decision_id\`, \`selected_options\`, and optional \`answer_note\`. The legacy decision IDs plus exact-answer shape is free-text only. Decisions create no answer artifact and are not copied into the task description. Do not use progress comments; do not leave findings only in the terminal.
93
+ 8. Reserve before writing — read-only stages never reserve writes or edit files. When the current stage allows writes, call \`reserve_work_paths\` for the exact checkout and paths; never edit through a collision. Never edit a conflicting path; narrow the reservation or use an isolated Git worktree.
94
+ 9. Persist every substantive non-question output — give every artifact a clear title and stable \`purpose_key\`. Call \`create_artifact\` for a new purpose and \`update_artifact\` with the latest revision when the same purpose already exists; a different purpose creates a different artifact. Every artifact must have exactly one coverage disposition for every active repository scope. For workflow tasks, persist every required artifact first and then call \`hand_off_stage\`; it refuses while any step of the stage is unfinished. Plan approval is displayed on the submitted plan and is not a decision artifact. Never write planning documents into a repository.
95
+ 10. Split only with approval — when work is too large, use \`ask_question\`; on approval create dependency-linked items with \`create_task\`, a shared feature tag, and matching board order.
96
+ 11. Keep the item self-sufficient — questions and accepted answers are persisted as first-class decisions through \`ask_question\` and \`record_user_input\`; substantive outputs live in artifacts. Do not use progress comments; \`add_comment\` is not for routine progress. A fresh agent must need no terminal, chat, or subagent history.
97
+ 12. End with proof — release finished reservations with \`release_work_paths\`, then call \`end_work\` with \`persistence_receipt = { final_task_revision, artifact_ids, outcome }\`. \`artifact_ids\` must exactly match every live artifact produced by this run and include the context-exploration artifact. Use outcome \`completed\` only for finished work, \`blocked\` for a persisted question or unavailable subagents, and \`aborted\` only when intentionally abandoning the run. A missing, stale, foreign, duplicate, or incomplete receipt is rejected; ending releases only this session's work.Full protocol: MCP prompt \`ctrl-spc-protocol\`.
98
+
99
+ ## If the ctrl-spc tools are not in your tool list
100
+
101
+ First check your own tool list. **If the ctrl-spc tools are there, this section
102
+ does not apply**: do the work, and do not run \`cs status\` at all. Read on only if
103
+ they are actually missing.
104
+
105
+ The tools are registered into your agent by the CTRL+SPC daemon and are called by
106
+ name. There is no address to connect to.
107
+
108
+ **Diagnose it yourself. Do not ask the user to.** Run \`cs status\` and follow the
109
+ single next step it prints. Never probe a network address, never name a port,
110
+ never report the CTRL+SPC server down, and never hand the user a command whose
111
+ purpose is to find out what is wrong. The only things you ask the user for are
112
+ their password, permission to run something, and restarting their agent.
113
+
114
+ **Never suggest project configuration.** This is not per-project setup. Do not
115
+ create or suggest \`.mcp.json\`, a \`.claude/\` directory, or any file in the
116
+ project. If the tools are missing, the machine needs setup, not the codebase.
117
+
118
+ **Do one step per turn, in order.** Act only on the step \`cs status\` just
119
+ printed, never on one you expect to come next. The order is forced: sign in, then
120
+ start, then restart, and an earlier step must succeed before a later one can
121
+ work. Never ask for two things at once.
122
+
123
+ Act on what it says:
124
+
125
+ - **\`cs\` is not found at all.** CTRL+SPC is not installed on this machine. Tell
126
+ the user, and offer to run \`npm i -g @ctrl-spc/cs\`. Ask before running it.
127
+ - **It says to sign in.** Tell the user to run \`cs login\` themselves, and that a
128
+ browser will open where they sign in with the same email and password they use
129
+ on ctrl-spc.com. Do not run \`cs login\` yourself: it waits up to five minutes
130
+ for that browser. Then stop your turn and hand control back, saying you will
131
+ continue when they tell you sign-in is done. When they say it is done, run
132
+ \`cs status\` again yourself, tell them which account it shows, and carry on with
133
+ the step it now prints.
134
+ - **It says to run \`cs start\`.** Ask permission first, and tell the user it stays
135
+ running in the background to keep their computer online for CTRL+SPC.
136
+ \`cs start\` never exits on its own, so never run it as a command you wait on:
137
+ run exactly \`nohup cs start > /dev/null 2>&1 &\` (on Windows,
138
+ \`start /b cs start\`). Wait a few seconds, then run \`cs status\` again. If your
139
+ tools cannot leave a process running after a command returns, say so and ask
140
+ the user to run \`cs start\` in their own terminal window instead.
141
+ - **It says the tools are not registered into an agent.** Ask permission to
142
+ restart the background daemon, then start it again the same way, and run
143
+ \`cs status\` again. Do not ask the user to press Ctrl-C.
144
+ - **It says to restart your agent.** Ask the user to restart it, pass on the
145
+ reason \`cs status\` gave, and tell them the pasted work item will run once it
146
+ comes back.
147
+
148
+ If a command you ran fails, say which command failed and what it said. \`cs status\`
149
+ exiting non-zero is **not** one of those failures: it exits non-zero whenever
150
+ setup is not yet finished, which only means there is another step. Judge it by
151
+ what it prints, never by its exit code, and never treat a non-zero \`cs status\`
152
+ after a repair as proof the repair failed. Never report setup as complete on the
153
+ strength of a command you did not confirm with a fresh \`cs status\`.
154
+ `;
155
+ /** The three SKILL.md files. Byte-identical for Claude and Codex: the protocol
156
+ * is the same document, and one constant means they cannot drift apart the way
157
+ * v1's two copies did (Codex's still named a tool that no longer exists). */
158
+ const SKILL_TEXT = `---
159
+ name: ctrl-spc
160
+ description: ${SKILL_DESCRIPTION}
161
+ ---
162
+
163
+ ${PROTOCOL_BODY}`;
164
+ /** The Codex prompt file is the same body with no frontmatter. */
165
+ const PROMPT_TEXT = PROTOCOL_BODY;
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cs",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "CTRL+SPC — 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
  },
8
8
  "type": "module",
9
9
  "bin": {
10
- "cs": "dist/index.js",
11
- "cs3": "dist/panel3/cli.js"
10
+ "cs": "dist/index.js"
12
11
  },
13
12
  "files": [
14
13
  "dist"