@ctrl-spc/cs 0.7.0 → 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.
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;
@@ -0,0 +1,68 @@
1
+ async function read(query, subject) {
2
+ const { data, error } = await query;
3
+ if (error)
4
+ throw new Error(`could not read ${subject}: ${error.message}`);
5
+ return data ?? [];
6
+ }
7
+ /**
8
+ * A workflow, its stages in order, and the exits off them.
9
+ *
10
+ * SORTED HERE RATHER THAN TRUSTED, which is the web reader's own rule: order is
11
+ * the whole point of a workflow, positions are deliberately not unique (a
12
+ * reorder rewrites the set), and a tie has to break the same way twice.
13
+ *
14
+ * FOUR READS RATHER THAN ONE EMBED. `cliv2_workflow_stages_in_workflow` carries
15
+ * two foreign keys into `cliv2_workflow_stages` (`stage_id` and
16
+ * `loop_to_stage_id`), so a nested embed of the stage document would need
17
+ * disambiguating by constraint name — a coupling to a constraint's spelling for
18
+ * the sake of saving a round trip on a tool call an agent makes once.
19
+ */
20
+ export async function readWorkflow(client, workflowId) {
21
+ const found = await read(client.from('cliv2_workflows').select('id, name, description, ending').eq('id', workflowId), `workflow ${workflowId}`);
22
+ if (found.length === 0) {
23
+ throw new Error(`could not read workflow ${workflowId}: there is no such row, or it is not yours`);
24
+ }
25
+ const row = found[0];
26
+ const [links, exits] = await Promise.all([
27
+ read(client.from('cliv2_workflow_stages_in_workflow').select('stage_id, position')
28
+ .eq('workflow_id', workflowId), `the stages of workflow ${workflowId}`),
29
+ read(client.from('cliv2_workflow_stage_exits').select('stage_id, to_stage_id, condition, position')
30
+ .eq('workflow_id', workflowId), `the exits of workflow ${workflowId}`),
31
+ ]);
32
+ const ordered = [...links].sort((a, b) => a.position - b.position || a.stage_id.localeCompare(b.stage_id));
33
+ const stageRows = ordered.length === 0 ? [] : await read(client.from('cliv2_workflow_stages').select('id, name, description, body')
34
+ .in('id', ordered.map((link) => link.stage_id)), `the stage documents of workflow ${workflowId}`);
35
+ const byId = new Map(stageRows.map((stage) => [stage.id, stage]));
36
+ return {
37
+ id: row.id,
38
+ name: row.name,
39
+ description: row.description ?? '',
40
+ /* Defaulted here as well as in the column, for the web reader's reason: a
41
+ workflow written before the ending existed reads back as 'end'. */
42
+ ending: row.ending ?? 'end',
43
+ stages: ordered.map((link) => {
44
+ const stage = byId.get(link.stage_id);
45
+ /* A LISTED STAGE THAT DID NOT COME BACK IS AN ERROR, NEVER A GAP. The
46
+ join cascades on delete and archiving is not deletion, so this cannot
47
+ happen through the product — and a stage silently missing from the
48
+ middle of a recipe would be a different process presented as this one. */
49
+ if (!stage) {
50
+ throw new Error(`could not read the stage documents of workflow ${workflowId}: stage ${link.stage_id} `
51
+ + 'is in the workflow but its document did not come back');
52
+ }
53
+ return {
54
+ id: stage.id,
55
+ name: stage.name,
56
+ description: stage.description ?? '',
57
+ body: stage.body ?? '',
58
+ };
59
+ }),
60
+ exits: [...exits]
61
+ .sort((a, b) => a.position - b.position || a.to_stage_id.localeCompare(b.to_stage_id))
62
+ .map((exit) => ({
63
+ stageId: exit.stage_id,
64
+ toStageId: exit.to_stage_id,
65
+ condition: exit.condition,
66
+ })),
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@ctrl-spc/cs",
3
- "version": "0.7.0",
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"