@primitive.ai/prim 0.1.0-alpha.59 → 0.1.0-alpha.61

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/SKILL.md CHANGED
@@ -80,13 +80,13 @@ Confirmations are author-targeted and rare by design; answering keeps the graph'
80
80
 
81
81
  Capture is automatic for low-level choices made while coding in an active repo. Use the deliberate CLI path for higher-order decisions that emerge in conversation: goals, priorities, principles, invariants, constraints, defaults, commitments, durable tradeoffs, and exceptions.
82
82
 
83
- A decision worth deliberately recording is a genuine **fork in the road**: the user or agent encountered multiple plausible paths, selected one, and that selection should inform future work. The litmus: does the selection govern future work, or did it merely happen once? A release-scoped choice — what is in or out of one release — reverses at the next release; it is ephemeral, not a decision. Record the chosen behavior, direction, constraint, or tradeoff—not routine implementation needed to finish the task or follow an existing convention. Small design choices made in-flight while implementing—such as edge-case handling, parameter defaults, retry or timeout values, log levels, naming, or the shape of an internal data structure—are implementation details even when you chose between alternatives: never deliberately record them or spend a rationale question on them; passive capture already covers coding activity. A teammate working elsewhere should benefit from knowing it — and that test is a gate, not a nicety. The graph is the team's shared memory: a decision that configures only your own working environment — editor, shell, or terminal tooling, a personal dev box or remote coding machine, which worktree manager, launcher, or TUI you run — is personal, not a team decision, however durable and well-reasoned, even when the setup touches repo-owned helper scripts. Never record a personal-environment decision on your own initiative; record only positions the team's shared work must preserve. An explicit request to record that specific decision still wins — note when honoring it that the record is visible to the whole team.
83
+ A decision worth deliberately recording is a genuine **fork in the road**: the user or agent encountered multiple plausible paths, selected one, and that selection should inform future work. The litmus: does the selection govern future work, or did it merely happen once? A second litmus: was the rejected path one the product could actually have shipped — a defensible alternative a teammate could have argued for — rather than an obviously-wrong option the quality bar already excludes (leaving a surface broken or empty)? “Of course we did this” marks a forced choice, not a fork. A release-scoped choice — what is in or out of one release — reverses at the next release; it is ephemeral, not a decision. Record the chosen behavior, direction, constraint, or tradeoff—not routine implementation needed to finish the task or follow an existing convention. Small design choices made in-flight while implementing—such as edge-case handling, parameter defaults, retry or timeout values, log levels, naming, or the shape of an internal data structure—are implementation details even when you chose between alternatives: never deliberately record them or spend a rationale question on them; passive capture already covers coding activity. A teammate working elsewhere should benefit from knowing it — and that test is a gate, not a nicety. The graph is the team's shared memory: a decision that configures only your own working environment — editor, shell, or terminal tooling, a personal dev box or remote coding machine, which worktree manager, launcher, or TUI you run — is personal, not a team decision, however durable and well-reasoned, even when the setup touches repo-owned helper scripts. Never record a personal-environment decision on your own initiative; record only positions the team's shared work must preserve. An explicit request to record that specific decision still wins — note when honoring it that the record is visible to the whole team.
84
84
 
85
85
  ### Ground the rationale in real sources
86
86
 
87
87
  Before deliberately recording a decision, actively gather the real context behind **why this path was chosen** — aim to populate the decision with as much genuine, source-grounded rationale as you can find, never a plausible-sounding guess. Start with the current conversation — it is itself a primary source: when the why was stated or is visible in this session, capture it in `--rationale` at create time rather than omitting it. Then reach for every tool, connector, and MCP server available to you to pull from the actual sources the decision or task points to: Slack threads, Granola or other meeting notes, Linear issues, Zoom transcripts, email, and repository docs or skills. Don't limit yourself to that list — use whatever integrations you have. Read the source directly instead of inferring from memory, and target the specific thread, meeting, ticket, or document the user referenced rather than a broad, scattershot search.
88
88
 
89
- Record only rationale supported by those sources. Do not mistake the implementation method, the task request, or a restatement of the decision for its rationale. Write it as plain, impersonal engineering explanation — the reasons, evidence, and tradeoffs behind the choice, in a sentence or three. Rationale is explanatory, never normative: a constraint future work must obey belongs in `--decided` even when the rationale explains why, and reasoning belongs in `--rationale`, never inlined into the intent while `--rationale` sits empty. `--rationale` is not optional: never invent one, but never silently drop it either — when the why cannot be deduced from all available context, ask the user for it with the one focused question below and record their answer.
89
+ Record only rationale supported by those sources. Do not mistake the implementation method, the task request, or a restatement of the decision for its rationale. Echoing the spec, PRD, or Linear ticket — the “what” — is not rationale either; the capture-worthy part is the approach reasoning: why this shape and why each recorded alternative loses. When you record an option in `--alternatives`, the rationale should say why it was rejected. Write it as plain, impersonal engineering explanation — the reasons, evidence, and tradeoffs behind the choice, in a sentence or three. Rationale is explanatory, never normative: a constraint future work must obey belongs in `--decided` even when the rationale explains why, and reasoning belongs in `--rationale`, never inlined into the intent while `--rationale` sits empty. `--rationale` is not optional: never invent one, but never silently drop it either — when the why cannot be deduced from all available context, ask the user for it with the one focused question below and record their answer.
90
90
 
91
91
  For proactively identified decisions in an active repo, let confidence in the rationale determine the interaction:
92
92
 
@@ -64,6 +64,13 @@ function stripControlChars(text) {
64
64
  }
65
65
  return out;
66
66
  }
67
+ var MAX_HEALTH_ERROR_LENGTH = 240;
68
+ function boundedHealthError(value) {
69
+ if (!value) return void 0;
70
+ const clean = stripControlChars(value).replace(/\s+/g, " ").trim();
71
+ if (!clean) return void 0;
72
+ return clean.length <= MAX_HEALTH_ERROR_LENGTH ? clean : `${clean.slice(0, MAX_HEALTH_ERROR_LENGTH - 1)}\u2026`;
73
+ }
67
74
  function stripAnsi(text) {
68
75
  return text.replace(/\u001b\[[0-9;]*m/g, "");
69
76
  }
@@ -97,6 +104,7 @@ export {
97
104
  bold,
98
105
  colorForArea,
99
106
  stripControlChars,
107
+ boundedHealthError,
100
108
  stripAnsi,
101
109
  requireDurableIngestAcknowledgement
102
110
  };
@@ -1,7 +1,10 @@
1
1
  import {
2
- gitToplevel
2
+ gitToplevel,
3
+ githubRepositoryFullName,
4
+ setRepoSyncId
3
5
  } from "./chunk-LDQZASIF.js";
4
6
  import {
7
+ getClient,
5
8
  withFileLock
6
9
  } from "./chunk-I3HIPAIR.js";
7
10
 
@@ -53,6 +56,45 @@ import { homedir } from "os";
53
56
  import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
54
57
  import { fileURLToPath } from "url";
55
58
  import { parse as parseYaml } from "yaml";
59
+
60
+ // src/lib/semver.ts
61
+ function parseSemver(value) {
62
+ if (typeof value !== "string" || value.length > 256) return;
63
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
64
+ value
65
+ );
66
+ if (!match) return;
67
+ const prerelease = match[4]?.split(".") ?? [];
68
+ if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
69
+ return;
70
+ }
71
+ return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
72
+ }
73
+ function compareSemver(left, right) {
74
+ const a = parseSemver(left);
75
+ const b = parseSemver(right);
76
+ if (!a || !b) return;
77
+ for (let i = 0; i < a.core.length; i += 1) {
78
+ if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
79
+ }
80
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
81
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
82
+ }
83
+ for (let i = 0; i < Math.max(a.prerelease.length, b.prerelease.length); i += 1) {
84
+ const x = a.prerelease[i];
85
+ const y = b.prerelease[i];
86
+ if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
87
+ if (x === y) continue;
88
+ const xNumeric = /^\d+$/u.test(x);
89
+ const yNumeric = /^\d+$/u.test(y);
90
+ if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
91
+ if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
92
+ return x < y ? -1 : 1;
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ // src/commands/claude-plugin.ts
56
98
  var __dirname = dirname(fileURLToPath(import.meta.url));
57
99
  var PLUGIN_DESCRIPTION = "Primitive decision-graph guidance for the prim CLI \u2014 a model-invoked skill installed by prim skill install.";
58
100
  var MAX_MANIFEST_BYTES = 64 * 1024;
@@ -97,42 +139,6 @@ function pluginPaths(dir) {
97
139
  skillPath: join(dir, "SKILL.md")
98
140
  };
99
141
  }
100
- function parseSemver(value) {
101
- if (typeof value !== "string" || value.length > 256) return;
102
- const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.exec(
103
- value
104
- );
105
- if (!match) return;
106
- const prerelease = match[4]?.split(".") ?? [];
107
- if (prerelease.some((part) => /^\d+$/u.test(part) && part.length > 1 && part.startsWith("0"))) {
108
- return;
109
- }
110
- return { core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])], prerelease };
111
- }
112
- function compareSemver(left, right) {
113
- const a = parseSemver(left);
114
- const b = parseSemver(right);
115
- if (!a || !b) return;
116
- for (let i = 0; i < a.core.length; i += 1) {
117
- if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
118
- }
119
- if (a.prerelease.length === 0 || b.prerelease.length === 0) {
120
- return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length === 0 ? 1 : -1;
121
- }
122
- const length = Math.max(a.prerelease.length, b.prerelease.length);
123
- for (let i = 0; i < length; i += 1) {
124
- const x = a.prerelease[i];
125
- const y = b.prerelease[i];
126
- if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
127
- if (x === y) continue;
128
- const xNumeric = /^\d+$/u.test(x);
129
- const yNumeric = /^\d+$/u.test(y);
130
- if (xNumeric && yNumeric) return BigInt(x) < BigInt(y) ? -1 : 1;
131
- if (xNumeric !== yNumeric) return xNumeric ? -1 : 1;
132
- return x < y ? -1 : 1;
133
- }
134
- return 0;
135
- }
136
142
  function pathIsInside(root, path) {
137
143
  const rel = relative(root, path);
138
144
  return rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
@@ -586,8 +592,28 @@ function registerSkillCommands(program) {
586
592
  });
587
593
  }
588
594
 
595
+ // src/lib/repository-binding.ts
596
+ async function bindRepository(root, options) {
597
+ const repositoryFullName = githubRepositoryFullName(root);
598
+ if (!repositoryFullName) {
599
+ throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
600
+ }
601
+ const response = await getClient().post(
602
+ "/api/cli/repositories/bind",
603
+ { repositoryFullName },
604
+ options
605
+ );
606
+ if (typeof response.repoSyncId !== "string" || response.repoSyncId.length === 0) {
607
+ throw new Error("server returned no repository binding");
608
+ }
609
+ setRepoSyncId(root, response.repoSyncId);
610
+ return { repoSyncId: response.repoSyncId, repositoryFullName };
611
+ }
612
+
589
613
  export {
614
+ bindRepository,
590
615
  printJson,
616
+ compareSemver,
591
617
  refreshClaudePlugins,
592
618
  hasUsableCodexGuidance,
593
619
  registerSkillCommands
@@ -1517,6 +1517,7 @@ function handleStatusSnapshot(params) {
1517
1517
  sessionId: activeSessionId,
1518
1518
  lastHeartbeatAt,
1519
1519
  version: runtimeVersion,
1520
+ launchRevision: process.env.PRIM_LAUNCH_REVISION,
1520
1521
  healthy: daemonHealth.healthy,
1521
1522
  needsReauth: daemonHealth.needsReauth === true,
1522
1523
  heartbeat: { ...daemonHealth.heartbeat },
@@ -3,7 +3,7 @@ import {
3
3
  bold,
4
4
  color,
5
5
  requireDurableIngestAcknowledgement
6
- } from "../chunk-QY75BQMF.js";
6
+ } from "../chunk-JQFN6LBO.js";
7
7
  import {
8
8
  resolvePreflightTargets
9
9
  } from "../chunk-B4SHJYIN.js";
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ bindRepository,
3
4
  hasUsableCodexGuidance,
4
5
  refreshClaudePlugins
5
- } from "../chunk-UBEBEBLM.js";
6
+ } from "../chunk-N4R7EXZT.js";
6
7
  import {
7
8
  buildHookOutput,
8
9
  handoffHookOutput
@@ -22,7 +23,8 @@ import {
22
23
  } from "../chunk-YIMUIPMD.js";
23
24
  import {
24
25
  gitToplevel,
25
- isRepoActiveForCapture
26
+ isRepoActiveForCapture,
27
+ repoSyncId
26
28
  } from "../chunk-LDQZASIF.js";
27
29
  import {
28
30
  getSiteUrl,
@@ -66,6 +68,7 @@ function reauthNoticeFields(agent) {
66
68
 
67
69
  // src/hooks/session-start-core.ts
68
70
  var DAEMON_TIMEOUT_MS = 250;
71
+ var REPOSITORY_BIND_TIMEOUT_MS = 1e3;
69
72
  var PRIM_TRIGGER = "When this task chooses between plausible approaches or establishes or changes a lasting goal, priority, constraint, invariant, default, commitment, tradeoff, exception, or shared instruction";
70
73
  var PRIM_EXCLUSIONS = "Never invoke `prim` for routine implementation that merely follows an existing decision made before this task or for temporary tactics; they never qualify, including for evaluation.";
71
74
  function primReminder(action, rationaleAction) {
@@ -74,7 +77,25 @@ function primReminder(action, rationaleAction) {
74
77
  var PRIM_SKILL_REMINDER = primReminder("invoke the `prim` skill", "invoke the skill");
75
78
  var CODEX_PRIM_ACTION = "follow the installed Prim workflow and use the `prim` CLI";
76
79
  var CODEX_PRIM_REMINDER = primReminder(CODEX_PRIM_ACTION, CODEX_PRIM_ACTION);
77
- async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS)) {
80
+ async function activeProjectRoot(cwd) {
81
+ try {
82
+ const root = gitToplevel(cwd);
83
+ if (!root || !isRepoActiveForCapture(cwd)) return null;
84
+ if (!repoSyncId(root)) {
85
+ try {
86
+ await bindRepository(root, {
87
+ signal: AbortSignal.timeout(REPOSITORY_BIND_TIMEOUT_MS),
88
+ quietRefresh: true
89
+ });
90
+ } catch {
91
+ }
92
+ }
93
+ return root;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ async function processSessionStart(raw, agent, feedbackSignal) {
78
99
  let envelope;
79
100
  try {
80
101
  const parsed = JSON.parse(raw);
@@ -98,12 +119,13 @@ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.time
98
119
  { timeoutMs: DAEMON_TIMEOUT_MS }
99
120
  );
100
121
  const cwd = envelope.cwd ?? process.cwd();
122
+ let projectRoot = null;
101
123
  let active = false;
102
124
  let skillState = { installed: 0, refreshed: 0 };
103
125
  if (agent === "claude_code") {
126
+ projectRoot = await activeProjectRoot(cwd);
127
+ active = projectRoot !== null;
104
128
  try {
105
- const projectRoot = gitToplevel(cwd);
106
- active = projectRoot !== null && isRepoActiveForCapture(cwd);
107
129
  skillState = await refreshClaudePlugins(
108
130
  cwd,
109
131
  active && projectRoot !== null ? { includeProject: true, projectRoot } : { includeProject: false }
@@ -128,10 +150,11 @@ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.time
128
150
  { timeoutMs: DAEMON_TIMEOUT_MS }
129
151
  );
130
152
  const presence = snapshot && !snapshot.presenceStale && typeof snapshot.onlineCount === "number" ? `[prim] team: ${snapshot.onlineCount} online` : void 0;
153
+ projectRoot = await activeProjectRoot(cwd);
154
+ active = projectRoot !== null;
131
155
  let proactive = false;
132
156
  try {
133
- const projectRoot = gitToplevel(cwd);
134
- proactive = projectRoot !== null && isRepoActiveForCapture(cwd) && hasUsableCodexGuidance(projectRoot);
157
+ proactive = projectRoot !== null && active && hasUsableCodexGuidance(projectRoot);
135
158
  } catch {
136
159
  }
137
160
  const additionalContext = [proactive ? CODEX_PRIM_REMINDER : void 0, presence].filter((value) => value !== void 0).join("\n\n");
@@ -143,10 +166,11 @@ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.time
143
166
  if (active) {
144
167
  const identity = getOrCreateWorkspaceId(cwd);
145
168
  if (identity.status === "ready") {
169
+ const signal = feedbackSignal ?? AbortSignal.timeout(FEEDBACK_DEADLINE_MS);
146
170
  const lease = await leaseDecisionFeedback({
147
171
  workspaceId: identity.workspaceId,
148
172
  currentSessionId: envelope.session_id,
149
- signal: feedbackSignal
173
+ signal
150
174
  });
151
175
  const rendered = lease ? renderFeedback(lease) : void 0;
152
176
  return {
@@ -159,7 +183,7 @@ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.time
159
183
  await acknowledgeDecisionFeedback({
160
184
  workspaceId: identity.workspaceId,
161
185
  deliveries: rendered.deliveries,
162
- signal: feedbackSignal
186
+ signal
163
187
  });
164
188
  } : void 0
165
189
  };
@@ -167,6 +191,7 @@ async function processSessionStart(raw, agent, feedbackSignal = AbortSignal.time
167
191
  }
168
192
  return { output: buildHookOutput({ additionalContext, reloadSkills }) };
169
193
  }
194
+ await activeProjectRoot(cwd);
170
195
  return { output: buildHookOutput({}) };
171
196
  }
172
197
 
@@ -195,7 +220,6 @@ function readStdin() {
195
220
  });
196
221
  }
197
222
  async function main() {
198
- const feedbackSignal = AbortSignal.timeout(FEEDBACK_DEADLINE_MS);
199
223
  warmBinCache();
200
224
  const agent = parseAgent(process.argv);
201
225
  let raw;
@@ -205,7 +229,7 @@ async function main() {
205
229
  await emitOutput(buildHookOutput({}));
206
230
  return;
207
231
  }
208
- const result = await processSessionStart(raw, agent, feedbackSignal);
232
+ const result = await processSessionStart(raw, agent);
209
233
  await emitOutput(result.output, result.acknowledge);
210
234
  }
211
235
  void main().catch(async () => {