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

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
 
@@ -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
 
@@ -586,7 +589,26 @@ function registerSkillCommands(program) {
586
589
  });
587
590
  }
588
591
 
592
+ // src/lib/repository-binding.ts
593
+ async function bindRepository(root, options) {
594
+ const repositoryFullName = githubRepositoryFullName(root);
595
+ if (!repositoryFullName) {
596
+ throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
597
+ }
598
+ const response = await getClient().post(
599
+ "/api/cli/repositories/bind",
600
+ { repositoryFullName },
601
+ options
602
+ );
603
+ if (typeof response.repoSyncId !== "string" || response.repoSyncId.length === 0) {
604
+ throw new Error("server returned no repository binding");
605
+ }
606
+ setRepoSyncId(root, response.repoSyncId);
607
+ return { repoSyncId: response.repoSyncId, repositoryFullName };
608
+ }
609
+
589
610
  export {
611
+ bindRepository,
590
612
  printJson,
591
613
  refreshClaudePlugins,
592
614
  hasUsableCodexGuidance,
@@ -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-MZQO7K2P.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 () => {
package/dist/index.js CHANGED
@@ -23,9 +23,10 @@ import {
23
23
  readMovesFromPath
24
24
  } from "./chunk-S4LFRVGD.js";
25
25
  import {
26
+ bindRepository,
26
27
  printJson,
27
28
  registerSkillCommands
28
- } from "./chunk-UBEBEBLM.js";
29
+ } from "./chunk-MZQO7K2P.js";
29
30
  import {
30
31
  fetchFeedbackCapability
31
32
  } from "./chunk-R4CUX6FY.js";
@@ -45,11 +46,9 @@ import {
45
46
  canonicalRepositoryPath,
46
47
  decisionIngestionStatus,
47
48
  gitToplevel,
48
- githubRepositoryFullName,
49
49
  isRepoActiveForCapture,
50
50
  repoSyncId,
51
- setRepoActive,
52
- setRepoSyncId
51
+ setRepoActive
53
52
  } from "./chunk-LDQZASIF.js";
54
53
  import {
55
54
  HttpError,
@@ -91,18 +90,7 @@ async function applyActivation(active) {
91
90
  try {
92
91
  let bound;
93
92
  if (active) {
94
- const repositoryFullName = githubRepositoryFullName(root);
95
- if (!repositoryFullName) {
96
- throw new Error("origin must be a GitHub HTTPS/SSH remote in owner/name form");
97
- }
98
- const response = await getClient().post("/api/cli/repositories/bind", {
99
- repositoryFullName
100
- });
101
- if (typeof response.repoSyncId !== "string" || response.repoSyncId.length === 0) {
102
- throw new Error("server returned no repository binding");
103
- }
104
- setRepoSyncId(root, response.repoSyncId);
105
- bound = { repoSyncId: response.repoSyncId, repositoryFullName };
93
+ bound = await bindRepository(root);
106
94
  }
107
95
  setRepoActive(root, active);
108
96
  process.stderr.write(`[prim] prim ${active ? "enabled" : "disabled"} in ${root}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.59",
3
+ "version": "0.1.0-alpha.60",
4
4
  "description": "CLI for Primitive's decision graph — passive decision capture, conflict gate, and team presence",
5
5
  "type": "module",
6
6
  "license": "MIT",