@ctrl-spc/cli 1.1.15 → 1.2.1

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.
Files changed (42) hide show
  1. package/dist/agents.js +54 -0
  2. package/dist/collision.js +44 -0
  3. package/dist/commands/fetch.js +213 -0
  4. package/dist/commands/init.js +457 -0
  5. package/dist/commands/link.js +152 -0
  6. package/dist/commands/login.js +332 -0
  7. package/dist/commands/logout.js +14 -0
  8. package/dist/commands/run.js +622 -0
  9. package/dist/config.js +68 -0
  10. package/dist/env.js +5 -0
  11. package/dist/git.js +70 -0
  12. package/dist/index.js +56 -72
  13. package/dist/mcp.js +1732 -0
  14. package/dist/presence.js +62 -0
  15. package/dist/prompt.js +47 -0
  16. package/dist/protocol.js +136 -0
  17. package/dist/skills.js +152 -0
  18. package/dist/supabase.js +40 -10
  19. package/dist/topology.js +602 -0
  20. package/package.json +25 -23
  21. package/bin/ctrl-spc.js +0 -6
  22. package/dist/auth.js +0 -172
  23. package/dist/controlRequests.js +0 -164
  24. package/dist/daemon.js +0 -152
  25. package/dist/devices.js +0 -64
  26. package/dist/flagAssembler.js +0 -29
  27. package/dist/init.js +0 -79
  28. package/dist/launchd.js +0 -154
  29. package/dist/lifecycle.js +0 -37
  30. package/dist/mcpConfig.js +0 -12
  31. package/dist/repo.js +0 -52
  32. package/dist/session.js +0 -58
  33. package/dist/setup.js +0 -60
  34. package/dist/spawner.js +0 -140
  35. package/dist/windows-service.js +0 -142
  36. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.d.ts +0 -24
  37. package/node_modules/@ctrl-spc/mcp-server/dist/claimResolver.js +0 -32
  38. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.d.ts +0 -2
  39. package/node_modules/@ctrl-spc/mcp-server/dist/fingerprint.js +0 -21
  40. package/node_modules/@ctrl-spc/mcp-server/dist/index.d.ts +0 -1
  41. package/node_modules/@ctrl-spc/mcp-server/dist/index.js +0 -22
  42. package/node_modules/@ctrl-spc/mcp-server/package.json +0 -38
@@ -0,0 +1,62 @@
1
+ import { REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js';
2
+ function channelName(projectId) {
3
+ return `presence:project:${projectId}`;
4
+ }
5
+ /**
6
+ * Opens the Realtime presence channel for `payload.projectId`, tracked
7
+ * under `payload.machineId`. A stable per-machine key keeps two machines for
8
+ * the same user visible at once without leaking local checkout paths.
9
+ * Tracking only starts once the
10
+ * channel reports `SUBSCRIBED`; `setWorking`/`setAvailable` re-track the
11
+ * updated payload immediately if already subscribed, or lazily once it is.
12
+ */
13
+ export function startPresence(client, payload) {
14
+ const channel = client.channel(channelName(payload.projectId), {
15
+ config: { presence: { key: payload.machineId } },
16
+ });
17
+ let current = { ...payload };
18
+ let subscribed = false;
19
+ // Serializes track() calls on this channel so a burst of
20
+ // setWorking/setAvailable flips can never have two in flight at once —
21
+ // each one lands before the next is sent.
22
+ let trackChain = Promise.resolve();
23
+ function enqueueTrack(next) {
24
+ trackChain = trackChain.then(() => channel.track(next));
25
+ }
26
+ channel.subscribe((status, err) => {
27
+ if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {
28
+ subscribed = true;
29
+ enqueueTrack(current);
30
+ }
31
+ else if (status === REALTIME_SUBSCRIBE_STATES.CHANNEL_ERROR ||
32
+ status === REALTIME_SUBSCRIBE_STATES.TIMED_OUT) {
33
+ // Reconnection is supabase-js's default behavior (Global Constraints);
34
+ // this is visibility only, so a broken presence connection doesn't
35
+ // fail silently behind the "Presence: available" line already printed
36
+ // at startup.
37
+ console.warn(`Presence channel ${status.toLowerCase()}${err ? `: ${err.message}` : ''} — will retry.`);
38
+ }
39
+ });
40
+ function retrack(next) {
41
+ current = next;
42
+ if (subscribed)
43
+ enqueueTrack(current);
44
+ }
45
+ return {
46
+ setWorking(taskId, agent) {
47
+ retrack({ ...current, status: 'working', workingOnTaskId: taskId, workingAgent: agent });
48
+ console.log(`Presence: working on ${taskId}`);
49
+ },
50
+ setAvailable() {
51
+ retrack({ ...current, status: 'available', workingOnTaskId: undefined, workingAgent: undefined });
52
+ console.log('Presence: available');
53
+ },
54
+ async stop() {
55
+ // Let any in-flight track() finish first so it can't land after (and
56
+ // undo) the untrack below.
57
+ await trackChain.catch(() => { });
58
+ await channel.untrack();
59
+ await client.removeChannel(channel);
60
+ },
61
+ };
62
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,47 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ /**
3
+ * Prompts for free-text input with a default. When stdin isn't a TTY
4
+ * (scripted/non-interactive use — including our own manual verification
5
+ * runs) the default is returned without prompting.
6
+ */
7
+ export async function ask(question, defaultValue) {
8
+ if (!process.stdin.isTTY)
9
+ return defaultValue;
10
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
11
+ try {
12
+ const answer = (await rl.question(`${question} [${defaultValue}]: `)).trim();
13
+ return answer || defaultValue;
14
+ }
15
+ finally {
16
+ rl.close();
17
+ }
18
+ }
19
+ /**
20
+ * Prompts for a numbered selection from `choices`. When stdin isn't a TTY,
21
+ * or there's only one choice, `choices[defaultIndex]` is returned without
22
+ * prompting.
23
+ */
24
+ export async function select(question, choices, defaultIndex = 0) {
25
+ if (choices.length === 0)
26
+ throw new Error('select() requires at least one choice');
27
+ const clampedDefault = Math.min(Math.max(defaultIndex, 0), choices.length - 1);
28
+ if (!process.stdin.isTTY || choices.length === 1)
29
+ return choices[clampedDefault].value;
30
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
31
+ try {
32
+ console.log(question);
33
+ choices.forEach((choice, i) => {
34
+ console.log(` ${i + 1}) ${choice.label}${i === clampedDefault ? ' (default)' : ''}`);
35
+ });
36
+ const answer = (await rl.question(`Choice [${clampedDefault + 1}]: `)).trim();
37
+ if (!answer)
38
+ return choices[clampedDefault].value;
39
+ const idx = Number.parseInt(answer, 10) - 1;
40
+ if (Number.isInteger(idx) && idx >= 0 && idx < choices.length)
41
+ return choices[idx].value;
42
+ return choices[clampedDefault].value;
43
+ }
44
+ finally {
45
+ rl.close();
46
+ }
47
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * §5a protocol text, kept in sync with the installed Claude/Codex skill. Two
3
+ * forms, matching the spec's "delivered redundantly" design:
4
+ *
5
+ * - `PROTOCOL_FULL`: the complete section, served by the `ctrl-spc-protocol`
6
+ * MCP prompt (spec §5: "MCP prompt: ctrl-spc-protocol — the full §5a
7
+ * protocol text, referenced by the installed skill").
8
+ * - `PROTOCOL_SHORT`: a compact restatement embedded in every `get_task`
9
+ * response (spec §5: "a compact restatement of the working protocol
10
+ * (§5a), so even a session that skipped the skill gets the rules").
11
+ */
12
+ export const PROTOCOL_FULL = `## 5a. Agent working protocol
13
+
14
+ Delivered redundantly (installed skill + MCP), phrased imperatively for the agent:
15
+
16
+ **HARD STOP — unclear novel feature.** After \`record_context_exploration\` succeeds,
17
+ if the task does not explicitly request a build or name a deliverable, the immediate
18
+ next tool call is \`ask_question\` with category \`intent\` and the exact question:
19
+ “What should I produce for this feature? Select one or more: build, plan, spec,
20
+ diagram, mock, wireframe.” Then call \`end_work\` with reason
21
+ \`pending_user_answer\` and outcome \`blocked\`. Do not call \`get_task\` again,
22
+ \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or any intervening tool.
23
+ The context artifact is the only artifact allowed before the answer. A read-only
24
+ execution sandbox is not a missing checkout and never changes this intent question.
25
+ Multiple selections are allowed.
26
+
27
+ Before the context artifact exists, never state or imply a requested deliverable and
28
+ never mention repository counts, contents, tests, findings, or likely impact. If
29
+ progress commentary is required, use only: “The required CTRL+SPC context review is
30
+ in progress; no decision or repository change has been made.” After persistence and
31
+ before the intent question, either say nothing or only: “The required context review
32
+ is saved to the work item. I am applying the task's explicit intent gate now.”
33
+
34
+ 1. **Resolve without side effects.** \`/ctrl-spc work <id>\` → call \`get_task(id)\` before
35
+ anything else. \`get_task\` is read-only: it does not change task status, presence,
36
+ collaboration, claims, or reservations. The ID is the only context in the paste;
37
+ everything current lives in ctrl-spc.
38
+ 2. **Begin explicitly.** Read every active repository scope, checkout, topology revision,
39
+ unmet dependency, and collaborator, then call \`begin_work\`. The first live run
40
+ becomes coordinator and \`begin_work\` moves a Backlog item to In Progress. Later runs
41
+ join as collaborators without repeating that transition. Only the current coordinator
42
+ may make later task-status changes, and no agent marks an item Done autonomously.
43
+ 3. **Repair missing context.** If \`begin_work\` reports unavailable checkouts, warn the
44
+ user and offer to fetch or link each repository. Never fetch without approval. If the
45
+ user explicitly continues without one, call \`acknowledge_unavailable_checkout\` with a
46
+ reason. After every missing scope is fetched or acknowledged, call \`begin_work\` again.
47
+ Acknowledgement records a limitation; it never pretends the repository was reviewed.
48
+ 4. **Delegate exploration; never substitute direct exploration.** Every pasted work item
49
+ MUST use at least one read-only subagent. After \`begin_work\` joins, create a coverage
50
+ queue and partition every available repository scope exactly once across bounded
51
+ subagent invocations. Every child prompt explicitly lists its assigned
52
+ \`repository_scope_ids\`; across the batch each available ID appears exactly once. If
53
+ using Codex \`spawn_agent\`, MUST pass \`fork_turns: "none"\`; never pass \`all\` or a
54
+ recent-turn count. Each assignment is self-contained and includes only the exact scope
55
+ IDs, checkout paths, inspection focus, read-only restrictions, and required report
56
+ shape. Never include or inherit the parent \`/ctrl-spc work\` command. A context child
57
+ never calls ctrl-spc tools—including \`get_task\` or \`begin_work\`—and never picks up
58
+ the work item; it only inspects its assigned paths and returns its report.
59
+ If
60
+ zero scopes are available after explicit acknowledgements, one child
61
+ inspects the task/artifact context and reports the unavailable scopes without pretending
62
+ they were reviewed. Children inspect only: no file mutation, path reservation, mutating
63
+ ctrl-spc call, \`tee\`, shell redirection, temp/capture file, install, formatter,
64
+ cache-generating command, or any command that can write. Inspection-first is sufficient;
65
+ do not decide intent before the reports are persisted. Each report uses
66
+ \`{ agent, focus, repository_scope_ids, inspected_paths, findings, likely_impact,
67
+ unresolved_questions }\`; artifact coverage remains separate.
68
+ If the client cannot start a subagent, do not inspect directly, plan, ask task questions,
69
+ reserve, or edit. Call \`record_context_exploration\` with
70
+ \`blocked_reason = subagents_unavailable\`, tell the user, and stop.
71
+ 5. **Persist exploration before any decision.** The parent consolidates every child report
72
+ and MUST call \`record_context_exploration\` before deciding the action, asking a task
73
+ question, planning, reserving, or editing. This creates an analysis artifact on the work
74
+ item and covers every active scope exactly once; unavailable scopes carry the current
75
+ run acknowledgement ID. Before the tool succeeds, commentary may state process only:
76
+ never disclose findings, conclusions, likely impact, or decisions. After persistence,
77
+ terminal summaries may reference the persisted artifact/question but must add no
78
+ unpersisted facts.
79
+ 6. **Infer explicit work; otherwise ask exactly what is needed.** After exploration is
80
+ recorded, classify intent from the work item's explicit wording and accepted answers,
81
+ never from the exploration findings. Findings do not turn an unclear request into an
82
+ authorized deliverable. Perform the action only when the item explicitly names it; do
83
+ not ask for confirmation. Otherwise use \`ask_question\`. For a bug, ask for reproduction steps,
84
+ expected behavior, or actual behavior only when each is missing and not discoverable.
85
+ For a novel feature with no explicit build action or deliverable, apply the HARD STOP
86
+ above; the immediate next call is the exact build/plan/spec/diagram/mock/wireframe
87
+ \`ask_question\`.
88
+ Generic goals such as “improve,” “coordinate,” “prepare,” or “support” do not authorize
89
+ analysis, a release decision, implementation, or another artifact. In this state do not
90
+ call \`update_task\`, \`create_artifact\`, \`reserve_work_paths\`, or \`create_task\`; do not
91
+ decide, edit, or mark the run completed. The context-exploration analysis artifact is the
92
+ only output allowed before the user's answer.
93
+ Also ask before overriding an unmet dependency, splitting work, fetching, or accepting a
94
+ collision handoff. After \`ask_question\` succeeds, do no more work in that run: call
95
+ \`end_work\` with reason \`pending_user_answer\`, a receipt outcome of \`blocked\`, the
96
+ final task revision, and every live artifact ID; continue only in a later pasted run.
97
+ 7. **Persist every accepted answer and durable finding.** Immediately call \`update_task\`
98
+ to add accepted decisions, constraints, acceptance criteria, and durable findings to the
99
+ description. Use the latest \`get_task\` revision; refetch and reconcile on conflict.
100
+ Never leave an accepted answer or finding only in terminal, chat, or a subagent report.
101
+ 8. **Reserve the exact write surface.** Before editing, call \`reserve_work_paths\` with
102
+ the topology revision and, for every intended edit, the exact
103
+ \`repository_scope_id\`, selected \`repository_checkout_id\`, repo-relative path, and
104
+ write mode. An empty path reserves the entire scope. Do not edit until the full atomic
105
+ request succeeds, and edit only the checkout + paths returned as reserved.
106
+ 9. **Never edit through a collision.** A collision reserves nothing. Narrow to disjoint
107
+ paths, wait, ask for an explicit handoff, or use an isolated Git worktree. Never edit a
108
+ conflicting path, overwrite unexpected local changes, or assume another agent stopped.
109
+ 10. **Persist every substantive non-question output.** Use \`create_artifact\` for every
110
+ plan, specification, research/finding report, schema, diagram, mock, and wireframe before
111
+ presenting it as complete. Use type \`analysis\` for research/findings, \`plan\` for plans,
112
+ \`spec\` for specifications or proposed schemas, and the matching visual type. Never
113
+ write planning documents into a repository. Every artifact has exactly one coverage disposition and non-empty
114
+ reason for every active repository scope at the current topology revision.
115
+ 11. **Keep the work item self-sufficient.** Questions are durable records created through
116
+ \`ask_question\`; accepted answers and durable facts live in the description; substantive
117
+ outputs live in artifacts. No progress comments. Never leave useful
118
+ context only in terminal, chat history, a subagent report, or a repository planning file.
119
+ If work is too large, ask through \`ask_question\`; after approval use \`create_task\` to
120
+ create dependency-linked items with a shared feature tag and matching board order.
121
+ 12. **Release and end with proof.** Release finished reservations with
122
+ \`release_work_paths\`, then call \`end_work\` with
123
+ \`persistence_receipt = { final_task_revision, artifact_ids, outcome }\`.
124
+ \`artifact_ids\` exactly matches every live artifact produced by this run and includes
125
+ the context-exploration artifact. Use \`completed\` only for finished work, \`blocked\`
126
+ for a persisted question or unavailable subagents, and \`aborted\` only when intentionally
127
+ abandoning the run. Missing, stale, foreign, duplicate, or incomplete receipts are
128
+ rejected. Ending releases only this session's work.`;
129
+ export const PROTOCOL_SHORT = 'ctrl-spc protocol (compact — full text: MCP prompt `ctrl-spc-protocol`): ' +
130
+ 'HARD STOP unclear novel feature: immediately after record_context_exploration, ask_question category intent exactly “What should I produce for this feature? Select one or more: build, plan, spec, diagram, mock, wireframe.” Then end_work blocked pending_user_answer. No intervening get_task/update_task/create_artifact/reservation/tool; read-only sandbox is not a missing checkout. ' +
131
+ '1) get_task is read-only; begin_work (run coordinator); resolve missing checkouts via fetch/link or acknowledge_unavailable_checkout. ' +
132
+ '2) Every item needs read-only subagents covering scope IDs once. Codex spawn_agent MUST use fork_turns:none; its self-contained prompt has exact IDs/paths/focus, no copied command. Child calls no ctrl-spc tools and writes nothing. If unavailable, record blocked; stop. ' +
133
+ '3) Parent must record_context_exploration before deciding, asking, planning, reserving, or editing. Before success narrate process only—no findings/conclusions/impact/decisions. Later summaries add no unpersisted facts. ' +
134
+ '4) Intent only from task/accepted answers, never findings; improve/coordinate/prepare/support are not deliverables. Unclear novel feature: next mutation after context MUST be ask_question for build/plan/spec/diagram/mock/wireframe. No update_task/create_artifact/reservation/decision/edit before answer; end blocked pending_user_answer. Bugs ask only missing repro/expected/actual. ' +
135
+ '5) Put every accepted answer and durable finding in the description via revision-safe update_task; use create_artifact for every substantive non-question output with coverage for every active scope. No progress comments or terminal-only findings. ' +
136
+ '6) reserve_work_paths exact checkout+paths; never edit collisions. release_work_paths, then end_work with persistence_receipt={final_task_revision,artifact_ids,outcome}.';
package/dist/skills.js ADDED
@@ -0,0 +1,152 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ // Verbatim from the task brief — the web/skill contract the copied
6
+ // `/ctrl-spc work <id>` invocation depends on.
7
+ 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>.';
8
+ const CLAUDE_MCP_PERMISSION = 'mcp__ctrl-spc__*';
9
+ /**
10
+ * Home directory the skill files are written under: `CTRL_SPC_HOME` env
11
+ * override (used for tests and manual verification runs so they don't touch
12
+ * this machine's real `~/.claude` / `~/.codex`), else `os.homedir()`.
13
+ */
14
+ export function resolveHome() {
15
+ return process.env.CTRL_SPC_HOME || homedir();
16
+ }
17
+ export function claudeSkillPath() {
18
+ return join(resolveHome(), '.claude', 'skills', 'ctrl-spc', 'SKILL.md');
19
+ }
20
+ export function claudeSettingsPath() {
21
+ return join(resolveHome(), '.claude', 'settings.json');
22
+ }
23
+ export function codexPromptPath() {
24
+ return join(resolveHome(), '.codex', 'prompts', 'ctrl-spc.md');
25
+ }
26
+ export function codexSkillPath() {
27
+ return join(resolveHome(), '.agents', 'skills', 'ctrl-spc', 'SKILL.md');
28
+ }
29
+ export function codexLegacySkillPath() {
30
+ return join(resolveHome(), '.codex', 'skills', 'ctrl-spc', 'SKILL.md');
31
+ }
32
+ /**
33
+ * The §5a protocol, summarized imperatively for an agent that just resolved
34
+ * `/ctrl-spc work <id>` or `/ctrl-spc artifact <id>`. The full text also
35
+ * lives behind the `ctrl-spc-protocol` MCP prompt; parity tests keep the
36
+ * native skill's deterministic gates aligned with both protocol forms.
37
+ */
38
+ function protocolBody(mcpUrl) {
39
+ return `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:
40
+
41
+ HARD STOP — unclear novel feature: 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\` and this exact question: “What should I produce for this feature? Select one or more: build, plan, spec, diagram, mock, 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.
42
+
43
+ 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.”
44
+
45
+ 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.
46
+ 2. Begin explicitly — call \`begin_work\` after reading the topology. 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.
47
+ 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.
48
+ 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.
49
+ 5. Persist context before deciding — the parent consolidates every child report and MUST call \`record_context_exploration\` to create the work item's analysis artifact before deciding what to do, asking a task question, planning, reserving, or editing. Pass the structured reports plus a separate coverage array; include every active 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.
50
+ 6. Decide or ask deterministically — after exploration is recorded, 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\`. 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 \`ask_question\`; it must say “select one or more: build, plan, spec, diagram, mock, wireframe.” 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.
51
+ 7. Persist answers and findings — after every accepted answer, call \`update_task\` to add the decision, constraint, acceptance criterion, and durable findings to the description. Pass the latest revision and refetch/reconcile on conflict. Do not use progress comments; do not leave findings only in the terminal.
52
+ 8. Reserve before writing — call \`reserve_work_paths\` with the topology revision, exact checkout ID, and every repository-relative path you intend to edit. Do not edit until the full atomic request succeeds. On collision, narrow to disjoint paths, wait, ask for an explicit handoff, or use an isolated Git worktree. Never edit a conflicting path.
53
+ 9. Persist every output — every substantive non-question output must be a ctrl-spc artifact before it is presented as complete. Use \`create_artifact\` type \`analysis\` for research/findings, \`plan\` for plans, \`spec\` for specifications or proposed schemas, and the matching type for diagrams, mocks, and wireframes. Never write planning documents into a repository. Every artifact includes exactly one coverage disposition and non-empty reason for every active repository scope.
54
+ 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.
55
+ 11. Keep the item self-sufficient — questions are persisted through \`ask_question\`; accepted answers and durable facts live in the description; substantive outputs live in artifacts. \`add_comment\` is not for routine progress. A fresh agent must need no terminal, chat, or subagent history.
56
+ 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.
57
+
58
+ Full protocol: MCP prompt \`ctrl-spc-protocol\`. Tools live at ${mcpUrl}.
59
+ `;
60
+ }
61
+ function claudeSkillMarkdown(mcpUrl) {
62
+ return `---
63
+ name: ctrl-spc
64
+ description: ${SKILL_DESCRIPTION}
65
+ ---
66
+
67
+ ${protocolBody(mcpUrl)}`;
68
+ }
69
+ function codexSkillMarkdown(mcpUrl) {
70
+ return `---
71
+ name: ctrl-spc
72
+ description: ${SKILL_DESCRIPTION}
73
+ ---
74
+
75
+ ${protocolBody(mcpUrl)}`;
76
+ }
77
+ function ensureClaudeMcpPermission() {
78
+ const path = claudeSettingsPath();
79
+ let settings = {};
80
+ if (existsSync(path)) {
81
+ try {
82
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
83
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
84
+ throw new Error('root must be an object');
85
+ settings = parsed;
86
+ }
87
+ catch (err) {
88
+ throw new Error(`Could not update ${path}: invalid JSON (${err.message})`);
89
+ }
90
+ }
91
+ const currentPermissions = settings.permissions;
92
+ if (currentPermissions !== undefined && (!currentPermissions || typeof currentPermissions !== 'object' || Array.isArray(currentPermissions))) {
93
+ throw new Error(`Could not update ${path}: permissions must be an object.`);
94
+ }
95
+ const permissions = (currentPermissions ?? {});
96
+ const currentAllow = permissions.allow;
97
+ if (currentAllow !== undefined && !Array.isArray(currentAllow)) {
98
+ throw new Error(`Could not update ${path}: permissions.allow must be an array.`);
99
+ }
100
+ const allow = [...(currentAllow ?? [])];
101
+ // Avoid rewriting a user-owned global settings file on every ctrl-spc
102
+ // startup. Besides needless churn, a no-op write can trigger live config
103
+ // reloads in an already-running Claude session.
104
+ if (allow.includes(CLAUDE_MCP_PERMISSION))
105
+ return;
106
+ allow.push(CLAUDE_MCP_PERMISSION);
107
+ settings.permissions = { ...permissions, allow };
108
+ const dir = dirname(path);
109
+ mkdirSync(dir, { recursive: true });
110
+ // Write beside the destination and rename into place so a crash, disk-full
111
+ // error, or interrupted process cannot leave settings.json half-written.
112
+ // Keep an existing file's permission bits; use a private mode for a new
113
+ // user settings file.
114
+ const mode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
115
+ const tempPath = join(dir, `.settings.json.${process.pid}.${randomUUID()}.tmp`);
116
+ try {
117
+ writeFileSync(tempPath, `${JSON.stringify(settings, null, 2)}\n`, { encoding: 'utf8', mode });
118
+ chmodSync(tempPath, mode);
119
+ renameSync(tempPath, path);
120
+ }
121
+ catch (err) {
122
+ rmSync(tempPath, { force: true });
123
+ throw err;
124
+ }
125
+ }
126
+ /**
127
+ * Writes/refreshes the thin native skill for each detected agent (spec §4:
128
+ * "installs/refreshes the `/ctrl-spc` skill" on every start, so it never
129
+ * drifts from the MCP protocol). Only agents present in `agents` get a
130
+ * file — nothing is written for an agent that isn't installed.
131
+ */
132
+ export function installSkills(agents, mcpUrl) {
133
+ if (agents.includes('claude')) {
134
+ ensureClaudeMcpPermission();
135
+ const path = claudeSkillPath();
136
+ mkdirSync(dirname(path), { recursive: true });
137
+ writeFileSync(path, claudeSkillMarkdown(mcpUrl), 'utf8');
138
+ }
139
+ if (agents.includes('codex')) {
140
+ const skillPath = codexSkillPath();
141
+ mkdirSync(dirname(skillPath), { recursive: true });
142
+ writeFileSync(skillPath, codexSkillMarkdown(mcpUrl), 'utf8');
143
+ const legacySkillPath = codexLegacySkillPath();
144
+ mkdirSync(dirname(legacySkillPath), { recursive: true });
145
+ writeFileSync(legacySkillPath, codexSkillMarkdown(mcpUrl), 'utf8');
146
+ // Keep the deprecated custom-prompt file as a compatibility fallback for
147
+ // users who still invoke it explicitly as `/prompts:ctrl-spc ...`.
148
+ const promptPath = codexPromptPath();
149
+ mkdirSync(dirname(promptPath), { recursive: true });
150
+ writeFileSync(promptPath, protocolBody(mcpUrl), 'utf8');
151
+ }
152
+ }
package/dist/supabase.js CHANGED
@@ -1,13 +1,43 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
- export const SUPABASE_URL = 'https://dxlxlphkypjjyqfgpood.supabase.co';
3
- export const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImR4bHhscGhreXBqanlxZmdwb29kIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODAwNzcxMjEsImV4cCI6MjA5NTY1MzEyMX0.s0RkrQUqGRN45Q0lcBb_LkVEqYdYc0FKuVupzKKJrtQ';
4
- export const SUPABASE_FUNCTIONS_URL = `${SUPABASE_URL}/functions/v1`;
5
- export function createSupabaseClient() {
6
- return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
7
- auth: {
8
- autoRefreshToken: true,
9
- detectSessionInUrl: false,
10
- persistSession: false,
11
- },
2
+ import { SUPABASE_URL, SUPABASE_KEY } from './env.js';
3
+ import { readSession, writeSession } from './config.js';
4
+ /** Thrown by {@link getClient} when there is no usable stored session. */
5
+ export class NotLoggedIn extends Error {
6
+ constructor(message = 'Not logged in. Run `ctrl-spc login` first.') {
7
+ super(message);
8
+ this.name = 'NotLoggedIn';
9
+ }
10
+ }
11
+ /**
12
+ * Builds a Supabase client authenticated from the stored session.
13
+ *
14
+ * The client never manages its own session persistence — we own that via
15
+ * config.ts — so any token rotation supabase-js performs while validating
16
+ * the session (`setSession` refreshing an expired access token, for
17
+ * instance) is written straight back to disk via `onAuthStateChange`.
18
+ */
19
+ export async function getClient() {
20
+ const stored = readSession();
21
+ if (!stored || !stored.access_token || !stored.refresh_token) {
22
+ throw new NotLoggedIn();
23
+ }
24
+ const client = createClient(SUPABASE_URL, SUPABASE_KEY, {
25
+ auth: { persistSession: false, autoRefreshToken: false },
26
+ });
27
+ client.auth.onAuthStateChange((_event, session) => {
28
+ if (session) {
29
+ writeSession({
30
+ access_token: session.access_token,
31
+ refresh_token: session.refresh_token,
32
+ });
33
+ }
34
+ });
35
+ const { data, error } = await client.auth.setSession({
36
+ access_token: stored.access_token,
37
+ refresh_token: stored.refresh_token,
12
38
  });
39
+ if (error || !data.session) {
40
+ throw new NotLoggedIn(`Stored session is invalid or expired (${error?.message ?? 'no session returned'}). Run \`ctrl-spc login\` again.`);
41
+ }
42
+ return client;
13
43
  }