@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 +2 -2
- package/dist/{chunk-UBEBEBLM.js → chunk-MZQO7K2P.js} +23 -1
- package/dist/hooks/session-start.js +35 -11
- package/dist/index.js +4 -16
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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