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

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/README.md CHANGED
@@ -9,10 +9,9 @@ presence — from the command line and via session + git hooks.
9
9
 
10
10
  > [!NOTE]
11
11
  > **Conflict Gates** check each edit against the decision graph and surface any
12
- > load-bearing decision it conflicts with; their **Enforcement** tier blocks or pauses
13
- > a conflicting edit until you reconcile and retry. Conflict Gates are **not currently
14
- > enabled** — automatic decision capture in active repos runs regardless. To enable them
15
- > for your team, contact **support@getprimitive.ai**.
12
+ > load-bearing decision it conflicts with. When **Enforcement** is enabled for your
13
+ > organization, gates can warn, pause, or block a conflicting edit until you reconcile
14
+ > the decision and retry. Automatic decision capture in active repositories is separate.
16
15
 
17
16
  ## Installation
18
17
 
@@ -197,7 +196,7 @@ installation.
197
196
  ### Daemon
198
197
 
199
198
  A supervised long-lived companion process that continuously drains captured
200
- Moves, accelerates in-session decision checks, and powers the "team: N online"
199
+ Moves, accelerates decision-graph reads, and powers the "team: N online"
201
200
  presence count. `prim setup` requires it to become healthy unless the explicit
202
201
  `--no-daemon` opt-out is supplied; hooks still fail soft if it later degrades.
203
202
 
@@ -249,8 +248,8 @@ would create a cycle (exit 2); an unresolved id exits 4.
249
248
  prim reconcile <id> # Mint a single-use bypass for a decision Conflict Gates flagged
250
249
  ```
251
250
 
252
- Part of Conflict Gates **Enforcement**, which is not currently enabled (see the note
253
- at the top). Contact support@getprimitive.ai to enable it.
251
+ When Conflict Gates **Enforcement** is enabled for your organization, this command
252
+ authorizes one retry for the named decision.
254
253
 
255
254
  ### Hooks
256
255
 
package/SKILL.md CHANGED
@@ -9,7 +9,7 @@ description: Use the prim CLI for Primitive’s decision graph. MUST INVOKE befo
9
9
 
10
10
  ## Mental model
11
11
 
12
- In an active repo, prim passively captures the **decisions** your team makes -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. **Conflict Gates** can check a later change against that graph and surface any load-bearing decision it conflicts with; with **Enforcement**, prim blocks the edit until you reconcile the decision and retry. Conflict Gates are **not currently enabled** — capture in active repos runs regardless. To enable them for your team, contact support@getprimitive.ai.
12
+ In an active repo, prim passively captures the **decisions** your team makes -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions (auto-linked from shared files, or related by hand — see *Relate decisions*) and reference the files it touched. **Conflict Gates** can check a later change against that graph and surface any load-bearing decision it conflicts with. When **Enforcement** is enabled for an organization, prim can warn, pause, or block the edit until you reconcile the decision and retry. Capture in active repos runs independently.
13
13
 
14
14
  Low-level capture runs automatically in active repos through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code), `npx --yes @primitive.ai/prim codex install` (Codex), or `npx --yes @primitive.ai/prim hermes install` (Hermes). An inactive repo is not passively captured. Deliberately record higher-order forks in the road through `prim decisions create` as described below. Your job is also to **read** the graph before load-bearing edits and **answer** the occasional rationale confirmation. (Responding to Conflict Gates applies only once Enforcement is enabled — see below.)
15
15
 
@@ -32,17 +32,17 @@ The CLI auto-refreshes a still-valid session from the stored refresh token (proa
32
32
  3. `<idOrShortId>` arguments accept either a full decision ID or the short ID shown in feeds (and gate reasons, when Conflict Gates are enabled).
33
33
  4. In `npx --yes @primitive.ai/prim ...`, `npx --yes` is npm's flag and only skips npm's package-install confirmation. It is not user consent for a Primitive action. Prim's global flag appears after the package name: `npx --yes @primitive.ai/prim --yes ...`.
34
34
 
35
- ## Conflict Gates & Enforcement (not currently enabled)
35
+ ## Conflict Gates & Enforcement
36
36
 
37
- **Conflict Gates** check each edit against the decision graph before it lands and surface any load-bearing decision it conflicts with. Their **Enforcement** tier goes further -- a conflicting edit is blocked (or paused for confirmation) until you reconcile the decision and retry. **Conflict Gates are not currently enabled** — automatic decision capture in active repos (above) runs regardless. To enable Conflict Gates and Enforcement for your team, contact support@getprimitive.ai.
37
+ **Conflict Gates** check each edit against the decision graph before it lands and surface any load-bearing decision it conflicts with. When **Enforcement** is enabled for an organization, a conflicting edit can be warned, blocked, or paused for confirmation until you reconcile the decision and retry. Automatic decision capture in active repos (above) runs independently.
38
38
 
39
39
  When Enforcement is enabled, before an edit (Claude Code: Edit/Write/MultiEdit; Codex: apply_patch; Hermes: write_file/patch) a PreToolUse hook scores the target file against the graph:
40
40
 
41
41
  - **deny** -- the edit is blocked: it conflicts with a load-bearing prior decision. Don't fight it. Read the reason line; it names the decision id. If you genuinely intend to override that decision, run `npx --yes @primitive.ai/prim reconcile dec_<shortId>`, then retry the edit once. Otherwise choose an approach that respects the decision.
42
- - **warn / additional context** -- the edit proceeds, but a relevant prior decision is surfaced. Read it. On Codex a would-be `ask` is delivered as allow-plus-context (Codex can't pause mid-tool), so that context is your only signal -- read it before continuing. Hermes has no soft-confirm tier, so a would-be `ask` arrives as a **deny** carrying the same reconcile directive: reconcile and retry, or set `PRIM_HOOK_MODE=warn` to downgrade it to context-only.
42
+ - **warn / additional context** -- the edit proceeds, but a relevant prior decision is surfaced. Read it. Codex cannot pause mid-tool, so a would-be `ask` arrives as a **deny** carrying the reconcile directive. Hermes also has no soft-confirm tier, so a would-be `ask` arrives as a **deny** carrying the same directive: reconcile and retry, or set `PRIM_HOOK_MODE=warn` to downgrade it to context-only.
43
43
  - **"decision check skipped / not verified" or "... partial / truncated"** -- the check could not fully run. Treat constraints as UNKNOWN, not clear; never read silence as approval.
44
44
 
45
- When enabled, the gate fail-opens on its *own* infrastructure errors (no daemon, network blip, org-unbound token) -- a setup problem would not block your edit. That is why an "unavailable" note would matter: it is the honest signal that the check, not your edit, is what failed.
45
+ When enabled, the gate sends its preflight check directly to Primitive and fail-opens on infrastructure errors (for example, a network failure or org-unbound token) -- a service problem would not block your edit. That is why an "unavailable" note matters: it is the honest signal that the check, not your edit, is what failed.
46
46
 
47
47
  ## Read the graph before large or load-bearing edits
48
48
 
@@ -56,7 +56,7 @@ Then tailor the delivery to this requester, not a generic reader. Take your cues
56
56
 
57
57
  ## Reconcile and the verdict footer
58
58
 
59
- Reconcile and the verdict footer are part of Conflict Gates **Enforcement**, which is **not currently enabled** (contact support@getprimitive.ai to turn it on); the `reconcile` command stays available regardless. When Enforcement is on:
59
+ Reconcile and the verdict footer are part of Conflict Gates **Enforcement**; the `reconcile` command stays available regardless. When Enforcement is enabled for your organization:
60
60
 
61
61
  `npx --yes @primitive.ai/prim reconcile <idOrShortId>` mints a single-use bypass for the named decision -- it prints `[prim] reconcile bypass issued for dec_<short> (expires in ...)` to STDERR, with the bypass JSON on STDOUT. Your *next* edit to the governed file then goes through, and on that edit prim prints a verdict footer to STDERR -- confirmation the override was recorded, not silently dropped:
62
62
 
@@ -107,7 +107,7 @@ an explicit local `prim.active=false` is always inactive. Every
107
107
 
108
108
  - An explicit request to record or create that Decision is approval for that one
109
109
  invocation; do not ask redundantly.
110
- - For a proactively identified Decision, present the proposed intent and supported
110
+ - For a proactively identified Decision, present the proposed decision and supported
111
111
  rationale — folding the single rationale question into the same prompt when none
112
112
  was found — then wait for approval before creating it. A previous approval never
113
113
  carries forward to another Decision.
@@ -144,25 +144,15 @@ Treat changes to shared agent instructions and standards—such as `AGENTS.md`,
144
144
 
145
145
  Apply the same attention during non-code deliberation: planning or “grill me” workflows, spec refinement, behavior design, PR planning or review, and conversations imported from Granola or other connected sources. These workflows can settle important forks before any code changes. Accumulate the confirmed decisions and record them at a natural phase or task boundary; do not interrupt or call Primitive after every answer.
146
146
 
147
- ### Onboarding: propose decisions from shared repository memory
147
+ ### Onboarding: seed the graph from memory, one proposal at a time
148
148
 
149
- When onboarding or `prim welcome` reports `"org": "seed"`, complete this procedure before falling back to the open goals question. Do not treat memory already loaded into the conversation as a substitute for the repository scan. This is a separate decision-proposal pass: do not present repository rules as the user's stated goals or insert them into `$FOUND_GOALS`.
149
+ When onboarding or `prim welcome` reports `"org": "seed"`, seed the viewer's graph by proposing decisions mined from memory. There is no open goals question: the proposals are the whole seeding pass. Propose **one decision per message**, wait for the user's verdict, and only then present the next — never present the candidates as a batch.
150
150
 
151
- 1. From the repository root, discover every Git-tracked shared memory or agent-instruction file with:
152
-
153
- ```
154
- git ls-files -- \
155
- 'MEMORY.md' ':(glob)**/MEMORY.md' \
156
- 'AGENTS.md' ':(glob)**/AGENTS.md' \
157
- 'CLAUDE.md' ':(glob)**/CLAUDE.md'
158
- ```
159
-
160
- 2. Read every path returned before selecting candidates. Never automatically read an untracked or gitignored memory file; use one only when the user explicitly asks to promote it to the team's Primitive graph.
161
- 3. Extract at most three explicit, durable positions that appear to remain in force. A candidate must state a genuine fork in the road—such as a goal, priority, principle, invariant, constraint, default, commitment, tradeoff, or exception. Do not infer decisions from code, repository structure, or unstated implications. Do not bulk-import the documents.
162
- 4. Run `npx --yes @primitive.ai/prim decisions recent --limit 20` and omit candidates equivalent to existing decisions. If the result is unavailable or too incomplete to rule out a duplicate confidently, do not propose the uncertain candidate; explain the limitation.
163
- 5. Present the remaining candidates as proposed decisions. For each one, include the proposed intent, the source path, and only rationale or alternatives explicitly supported by that source; when a source yields no rationale, ask for the why in the same approval prompt. Ask the user to approve or revise the proposals before creating them; do not silently attribute a historical repository decision to the onboarding user.
164
-
165
- If the scan returns no matching files or no eligible candidates, say so briefly and continue with the normal open onboarding question. Never invent a proposal to fill the gap.
151
+ 1. Gather candidates from the repo's Git-tracked shared memory and agent-instruction files (`git ls-files -- 'MEMORY.md' ':(glob)**/MEMORY.md' 'AGENTS.md' ':(glob)**/AGENTS.md' 'CLAUDE.md' ':(glob)**/CLAUDE.md'` from the repository root; read every path returned) and from your own memory and conversation context — positions the user has already *stated*; the setup guide names each agent's memory surface. Never automatically read an untracked, gitignored, or un-shared memory source; use one only when the user explicitly asks to promote it. Only their words count: never infer a position from the repo's code or history, and never invent one they didn't state.
152
+ 2. A candidate must state an explicit, durable position that appears to remain in force — a genuine fork in the road such as a goal, priority, principle, invariant, constraint, default, commitment, tradeoff, or exception. Do not infer decisions from code, repository structure, or unstated implications, and do not bulk-import the documents. The personal-environment exclusion above applies: never propose a position that binds no teammate.
153
+ 3. Run `npx --yes @primitive.ai/prim decisions recent --limit 20` and drop candidates equivalent to existing decisions. If the result is unavailable or too incomplete to rule out a duplicate confidently, drop the uncertain candidate; explain the limitation.
154
+ 4. Propose the strongest candidates one at a time — **at most six in total**, fewer when the material runs out (never invent a proposal to fill the quota), stopping early if the user declines to continue. Each proposal carries the proposed decision, its source (file path or the stated context), and only rationale or alternatives explicitly supported by that source; when the source yields no rationale, fold the single rationale question into the same prompt. In this and any user-facing presentation, label the position **Decision**, never "Intent" — intent is `--intent` flag vocabulary, not user copy — and label the rejected options **Alternatives rejected**, never "alternatives considered". Make clear, at least on the first proposal, that approved decisions join the team's shared decision graph, visible to any teammates — not a private list. Ask the user to approve, revise, or reject it — do not silently attribute a historical repository position to the onboarding user — and create each approved decision (per the wording and flag rules below) before presenting the next.
155
+ 5. After the final proposal's verdict — or immediately, when there are no viable candidates (say so briefly) — close with the standing guidance from welcome's STDOUT `seedGuidance` field: the user can tell their agent to add any decision to the Primitive decision graph at any time; otherwise, once the agent's prim hooks are active, Primitive passively captures decisions in the background while they work; and other repositories are captured only after `prim enable` is run in each. On an older CLI without that field, state those three points yourself.
166
156
 
167
157
  Outside onboarding, before creating a proactively identified decision, read recent decisions to avoid duplicating an existing position:
168
158
 
@@ -173,22 +163,22 @@ npx --yes @primitive.ai/prim decisions recent --limit 20
173
163
  If an equivalent decision is already present, do not create or suggest it again. After this duplicate check—or after the user confirms an onboarding proposal—author the decision directly. If the repo is inactive, that confirmation authorizes Prim's `--yes` only for the approved create:
174
164
 
175
165
  ```
176
- npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --attribution user --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge" --decided "Collaborative editing state syncs through prosemirror-collab","The server is the single authority for step ordering"
166
+ npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --attribution user --area data --rationale "Server-authoritative ordering" --alternatives "Yjs" --alternatives "Automerge" --decided "Collaborative editing state syncs through prosemirror-collab" --decided "The server is the single authority for step ordering"
177
167
  ```
178
168
 
179
169
  Inactive-repo form after approval:
180
170
 
181
171
  ```
182
- npx --yes @primitive.ai/prim --yes decisions create --intent "Adopt prosemirror-collab over Yjs" --attribution user --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge" --decided "Collaborative editing state syncs through prosemirror-collab","The server is the single authority for step ordering"
172
+ npx --yes @primitive.ai/prim --yes decisions create --intent "Adopt prosemirror-collab over Yjs" --attribution user --area data --rationale "Server-authoritative ordering" --alternatives "Yjs" --alternatives "Automerge" --decided "Collaborative editing state syncs through prosemirror-collab" --decided "The server is the single authority for step ordering"
183
173
  ```
184
174
 
185
175
  Both `--intent` and `--attribution` are required. Set `--attribution user` only when the person directly stated, selected, or confirmed the exact recorded choice. Set `--attribution agent` when you introduced that exact choice while pursuing a broader request. A broad task prompt, implementation permission, or assignment of responsibility does not turn your implementation choice into a user Decision. If the origin is ambiguous, ask the person to confirm the exact choice before creating it; after confirmation, use `user`. Never guess attribution.
186
176
 
187
177
  Word `--intent` as a short sentence-case normative headline — ideally 4–12 words, no terminal period — stating the standing constraint future work must follow, not a report of the action taken: "Consume AADT and safety data from street_export, not gps_probes_osm", never "Migrate AADT and safety data managers off gps_probes_osm". A one-time-action verb (Migrate, Backfill, Publish) headlines a task execution: restate the ongoing constraint the action implies, and when none exists, do not record a decision at all. Make the intent self-contained for a reader who wasn't in the session — name the release, artifact, or system explicitly, and state what a referenced change does rather than citing a PR or ticket number, which is itself session context; "limit this release" and "keep the GitHub surface (PR 1102)" are failures. Carry one governing decision per intent — a reader of the intent alone must see which option is now in force. Expand every binding specific the choice adopts — scope boundaries, defaults, invariants, schema or API shapes, exceptions, required mechanisms — into `--decided`, and move non-binding reasoning into `--rationale`; a paragraph-length intent that inlines its constraints and reasons is a failure.
188
178
 
189
- Other flags: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale` (required by the recording policy above — the CLI accepts its absence, but only for the declined/unanswered fallback), `--area` (a single short lowercase noun — `auth`, `billing`, `infra`), `--decided`, `--alternatives`, `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — these are the files Conflict Gates would check on later edits, same path form as `decisions check`; Conflict Gates are not currently enabled). Omit `--files` for broad directions that should not immediately participate in file-based Conflict Gates. STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`.
179
+ Other flags: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale` (required by the recording policy above — the CLI accepts its absence, but only for the declined/unanswered fallback), `--area` (a single short lowercase noun — `auth`, `billing`, `infra`), `--decided`, `--alternatives`, `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — these are the files Conflict Gates check on later edits when Enforcement is enabled, in the same path form as `decisions check`). Omit `--files` for broad directions that should not immediately participate in file-based Conflict Gates. STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`.
190
180
 
191
- `--decided` carries the enforceable detail as standalone bullets — one per independently adopted constraint, the core choice first, with exact names and values. Each bullet states what must remain true and makes sense by itself: never narrate the session ("Chose to document the behavior" is a failure) and never pack independent rules into one semicolon-delimited entry. `--alternatives` takes one concise entry per option actually considered and rejected; omitting it when none were named is normal. Both flags split their value on commas with no escape, so keep each entry comma-free — rephrase around colons, dashes, or "and".
181
+ `--decided` carries the enforceable detail as standalone bullets — one per independently adopted constraint, the core choice first, with exact names and values. Each bullet states what must remain true and makes sense by itself: never narrate the session ("Chose to document the behavior" is a failure) and never pack independent rules into one semicolon-delimited entry. `--alternatives` takes one concise entry per option actually considered and rejected; omitting it when none were named is normal. The decision's own negation is never an alternative — restating the prohibited or displaced behavior adds nothing; list only options a source or the conversation actually names as considered. Both flags are repeatable: pass one entry per flag occurrence, verbatim — repeat `--decided` for each bullet and `--alternatives` for each option. Values are never split on commas, so write entries as natural prose; a repeated flag, not a delimiter, is what separates entries.
192
182
 
193
183
  ### Inferred decisions: finish first, then optionally ask once
194
184
 
@@ -6,6 +6,9 @@ import {
6
6
  canonicalGitRoot,
7
7
  canonicalRepositoryPath
8
8
  } from "./chunk-LDQZASIF.js";
9
+ import {
10
+ getClient
11
+ } from "./chunk-I3HIPAIR.js";
9
12
 
10
13
  // src/hooks/pre-tool-use-scoring.ts
11
14
  import { isAbsolute, relative, sep } from "path";
@@ -25,50 +28,32 @@ function unverifiedNote(results) {
25
28
  }
26
29
  return causes.map((c) => `[primitive] ${c}`).join("\n");
27
30
  }
28
- function buildHookOutput(aggregate, results, agent = "claude_code") {
29
- if (aggregate === "deny") {
30
- const reason = results.filter((r) => r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] conflict detected (no detail available)";
31
+ function blockingReason(results) {
32
+ const parts = results.filter((result) => result.verdict === "ask" || result.verdict === "deny").flatMap((result) => [result.reason, result.additionalContext]).filter((value) => value.length > 0);
33
+ return [...new Set(parts)].join("\n\n") || "[primitive] conflict detected (no detail available)";
34
+ }
35
+ function advisoryNote(aggregate, results) {
36
+ const details = results.flatMap((result) => [
37
+ ...aggregate === "warn" ? [result.reason] : [],
38
+ result.additionalContext
39
+ ]);
40
+ if (anyUnverified(results)) details.push(unverifiedNote(results));
41
+ return [...new Set(details.filter((value) => value.length > 0))].join("\n\n");
42
+ }
43
+ function buildHookOutput(aggregate, results) {
44
+ if (aggregate === "deny" || aggregate === "ask") {
31
45
  return {
32
46
  hookSpecificOutput: {
33
47
  hookEventName: "PreToolUse",
34
- permissionDecision: "deny",
35
- permissionDecisionReason: reason
48
+ permissionDecision: aggregate,
49
+ permissionDecisionReason: blockingReason(results)
36
50
  }
37
51
  };
38
52
  }
39
- if (agent === "codex" && aggregate === "ask") {
40
- const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n");
41
- const context = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
42
- const merged = [reason, context].filter((s) => s.length > 0).join("\n\n");
43
- const out = {
44
- hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }
45
- };
46
- if (merged.length > 0) {
47
- out.hookSpecificOutput.additionalContext = merged;
48
- }
49
- return out;
50
- }
51
- if (aggregate === "ask") {
52
- const reason = results.filter((r) => r.verdict === "ask" || r.verdict === "deny").map((r) => r.reason).filter((s) => s.length > 0).join("\n\n") || "[primitive] please confirm this edit";
53
- const additionalContext = results.map((r) => r.additionalContext).filter((s) => s.length > 0 && s !== reason).join("\n");
54
- const out = {
55
- hookSpecificOutput: {
56
- hookEventName: "PreToolUse",
57
- permissionDecision: "ask",
58
- permissionDecisionReason: reason
59
- }
60
- };
61
- if (additionalContext.length > 0) {
62
- out.hookSpecificOutput.additionalContext = additionalContext;
63
- }
64
- return out;
65
- }
66
- const notes = [
67
- ...results.map((r) => r.additionalContext).filter((s) => s.length > 0),
68
- anyUnverified(results) ? unverifiedNote(results) : ""
69
- ].filter((s) => s.length > 0).join("\n");
53
+ const notes = advisoryNote(aggregate, results);
70
54
  if (notes.length > 0) {
71
55
  return {
56
+ systemMessage: notes,
72
57
  hookSpecificOutput: {
73
58
  hookEventName: "PreToolUse",
74
59
  permissionDecision: "allow",
@@ -83,6 +68,22 @@ function buildHookOutput(aggregate, results, agent = "claude_code") {
83
68
  }
84
69
  };
85
70
  }
71
+ function buildCodexOutput(aggregate, results) {
72
+ if (aggregate === "deny" || aggregate === "ask") {
73
+ return {
74
+ hookSpecificOutput: {
75
+ hookEventName: "PreToolUse",
76
+ permissionDecision: "deny",
77
+ permissionDecisionReason: blockingReason(results)
78
+ }
79
+ };
80
+ }
81
+ const notes = advisoryNote(aggregate, results);
82
+ return notes ? {
83
+ systemMessage: notes,
84
+ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: notes }
85
+ } : {};
86
+ }
86
87
  function failOpenOutput() {
87
88
  return {
88
89
  hookSpecificOutput: {
@@ -91,6 +92,9 @@ function failOpenOutput() {
91
92
  }
92
93
  };
93
94
  }
95
+ function failOpenCodex() {
96
+ return {};
97
+ }
94
98
  function buildHermesOutput(aggregate, results) {
95
99
  if (aggregate !== "deny" && aggregate !== "ask") {
96
100
  return {};
@@ -328,6 +332,7 @@ function analyzeShellTargets(source) {
328
332
 
329
333
  // src/hooks/preflight-v3.ts
330
334
  var PREFLIGHT_PROTOCOL_VERSION = 3;
335
+ var PREFLIGHT_TIMEOUT_MS = 6500;
331
336
  var MAX_PREFLIGHT_PATHS = 32;
332
337
  var MAX_PROPOSAL_BYTES = 6144;
333
338
  function resolvePreflightTargets(args) {
@@ -378,6 +383,13 @@ function parsePreflightResponse(value) {
378
383
  }
379
384
  return response;
380
385
  }
386
+ async function requestPreflight(request, signal = AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS)) {
387
+ const response = await getClient().post("/api/cli/decisions/conflict-check", request, {
388
+ signal,
389
+ quietRefresh: true
390
+ });
391
+ return parsePreflightResponse(response);
392
+ }
381
393
  function resultForPreflight(response) {
382
394
  const verdict = response.verdict === "block" ? "deny" : response.verdict;
383
395
  const safe = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
@@ -410,7 +422,9 @@ function unverifiedResult(message) {
410
422
 
411
423
  export {
412
424
  buildHookOutput,
425
+ buildCodexOutput,
413
426
  failOpenOutput,
427
+ failOpenCodex,
414
428
  buildHermesOutput,
415
429
  failOpenHermes,
416
430
  readHookMode,
@@ -418,7 +432,7 @@ export {
418
432
  PREFLIGHT_PROTOCOL_VERSION,
419
433
  resolvePreflightTargets,
420
434
  proposalFor,
421
- parsePreflightResponse,
435
+ requestPreflight,
422
436
  resultForPreflight,
423
437
  unverifiedResult
424
438
  };
@@ -1,17 +1,3 @@
1
- // src/client.ts
2
- import { createHash, randomBytes as randomBytes2 } from "crypto";
3
- import {
4
- chmodSync,
5
- existsSync as existsSync2,
6
- mkdirSync as mkdirSync2,
7
- readFileSync as readFileSync2,
8
- renameSync,
9
- rmSync as rmSync2,
10
- writeFileSync as writeFileSync2
11
- } from "fs";
12
- import { homedir } from "os";
13
- import { dirname as dirname2, join as join2, resolve } from "path";
14
-
15
1
  // src/lib/file-lock.ts
16
2
  import { randomBytes } from "crypto";
17
3
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
@@ -150,6 +136,18 @@ async function withFileLock(lockDir, operation, options = {}) {
150
136
  }
151
137
 
152
138
  // src/client.ts
139
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
140
+ import {
141
+ chmodSync,
142
+ existsSync as existsSync2,
143
+ mkdirSync as mkdirSync2,
144
+ readFileSync as readFileSync2,
145
+ renameSync,
146
+ rmSync as rmSync2,
147
+ writeFileSync as writeFileSync2
148
+ } from "fs";
149
+ import { homedir } from "os";
150
+ import { dirname as dirname2, join as join2, resolve } from "path";
153
151
  var CONFIG_DIR_MODE = 448;
154
152
  var CREDENTIAL_FILE_MODE = 384;
155
153
  var REFRESH_THRESHOLD_MS = 6e4;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getClient
3
- } from "./chunk-DWDEQRWN.js";
3
+ } from "./chunk-I3HIPAIR.js";
4
4
 
5
5
  // src/decisions/feedback.ts
6
6
  var FEEDBACK_PROTOCOL_VERSION = 1;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getSiteUrl
3
- } from "./chunk-DWDEQRWN.js";
3
+ } from "./chunk-I3HIPAIR.js";
4
4
 
5
5
  // src/journal.ts
6
6
  import {
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-LDQZASIF.js";
4
4
  import {
5
5
  withFileLock
6
- } from "./chunk-DWDEQRWN.js";
6
+ } from "./chunk-I3HIPAIR.js";
7
7
 
8
8
  // src/commands/skill.ts
9
9
  import { randomUUID } from "crypto";
@@ -1,5 +1,5 @@
1
1
  // src/hooks/prim-hook-core.ts
2
- import { randomUUID } from "crypto";
2
+ import { createHash, randomUUID } from "crypto";
3
3
  import { platform } from "os";
4
4
 
5
5
  // src/protocol/move.ts
@@ -8,12 +8,20 @@ var AGENT_ENVELOPE_VERSION = 3;
8
8
  var ENVELOPE_VERSION = LEGACY_ENVELOPE_VERSION;
9
9
 
10
10
  // src/hooks/prim-hook-core.ts
11
+ function postToolInvocationId(parsed, agent) {
12
+ if (parsed.hook_event_name !== "PostToolUse") return void 0;
13
+ const raw = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
14
+ return typeof raw === "string" && raw.length > 0 ? raw : void 0;
15
+ }
11
16
  function toMove(parsed, cliVersion, agent = "claude_code", workspaceId, invocationId) {
17
+ const sessionId = parsed.session_id ?? "";
18
+ const eventType = parsed.hook_event_name ?? "unknown";
19
+ const moveId = eventType === "PostToolUse" && sessionId && invocationId ? `posttool:v1:${createHash("sha256").update(JSON.stringify([agent, sessionId, eventType, invocationId])).digest("hex")}` : randomUUID();
12
20
  return {
13
- moveId: randomUUID(),
21
+ moveId,
14
22
  capturedAt: Date.now(),
15
- sessionId: parsed.session_id ?? "",
16
- eventType: parsed.hook_event_name ?? "unknown",
23
+ sessionId,
24
+ eventType,
17
25
  payload: parsed,
18
26
  env: {
19
27
  cwd: parsed.cwd ?? process.cwd(),
@@ -52,6 +60,7 @@ function shouldFlushAfter(eventType) {
52
60
  }
53
61
 
54
62
  export {
63
+ postToolInvocationId,
55
64
  toMove,
56
65
  toCommitMove,
57
66
  shouldFlushAfter
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  getClient,
3
3
  getSiteUrl
4
- } from "./chunk-DWDEQRWN.js";
4
+ } from "./chunk-I3HIPAIR.js";
5
5
  import {
6
6
  daemonRequest
7
7
  } from "./chunk-UTKQTZHL.js";
@@ -1486,20 +1486,6 @@ async function runIngestionLoop() {
1486
1486
  scheduleIngestion(delay);
1487
1487
  }
1488
1488
  }
1489
- async function handleConflictCheck(params) {
1490
- assertCallerEnvMatches(params.callerEnv, getSiteUrl());
1491
- if (params.protocolVersion === 3) {
1492
- const { callerEnv: _callerEnv, ...body } = params;
1493
- return await client.post("/api/cli/decisions/conflict-check", body, {
1494
- signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS),
1495
- quietRefresh: true
1496
- });
1497
- }
1498
- if (typeof params.file !== "string") {
1499
- throw new Error("conflict_check requires `file: string`");
1500
- }
1501
- return await client.post("/api/cli/decisions/conflict-check", { file: params.file });
1502
- }
1503
1489
  function pathParam(params) {
1504
1490
  if (typeof params.path !== "string" || !params.path.startsWith("/api/cli/")) {
1505
1491
  throw new Error("proxy request requires `path: string` under /api/cli/");
@@ -1562,10 +1548,6 @@ async function dispatchRequest(req) {
1562
1548
  const id = req.id;
1563
1549
  try {
1564
1550
  switch (req.method) {
1565
- case "conflict_check": {
1566
- const result = await handleConflictCheck(req.params ?? {});
1567
- return { id, ok: true, result };
1568
- }
1569
1551
  case "decisions_recent": {
1570
1552
  const result = await proxyGet(req.params ?? {}, "/api/cli/decisions/recent");
1571
1553
  return { id, ok: true, result };
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  toCommitMove
4
- } from "../chunk-TNNHR3EZ.js";
4
+ } from "../chunk-YDBZTFW5.js";
5
5
  import {
6
6
  appendMove,
7
7
  resolveOrg
8
- } from "../chunk-BOTKPLTI.js";
8
+ } from "../chunk-S4LFRVGD.js";
9
9
  import {
10
10
  isRepoActiveForCapture
11
11
  } from "../chunk-LDQZASIF.js";
12
- import "../chunk-DWDEQRWN.js";
12
+ import "../chunk-I3HIPAIR.js";
13
13
 
14
14
  // src/hooks/post-commit.ts
15
15
  import { execSync, spawn } from "child_process";
@@ -6,14 +6,15 @@ import {
6
6
  } from "../chunk-QY75BQMF.js";
7
7
  import {
8
8
  resolvePreflightTargets
9
- } from "../chunk-SWWBK4MP.js";
9
+ } from "../chunk-B4SHJYIN.js";
10
10
  import {
11
+ postToolInvocationId,
11
12
  toMove
12
- } from "../chunk-TNNHR3EZ.js";
13
+ } from "../chunk-YDBZTFW5.js";
13
14
  import {
14
15
  appendMove,
15
16
  resolveOrg
16
- } from "../chunk-BOTKPLTI.js";
17
+ } from "../chunk-S4LFRVGD.js";
17
18
  import {
18
19
  scrubFromCwd
19
20
  } from "../chunk-GUXBUBQ7.js";
@@ -29,7 +30,7 @@ import {
29
30
  } from "../chunk-LDQZASIF.js";
30
31
  import {
31
32
  getClient
32
- } from "../chunk-DWDEQRWN.js";
33
+ } from "../chunk-I3HIPAIR.js";
33
34
  import {
34
35
  normalizeEnvelope,
35
36
  parseAgent
@@ -168,7 +169,7 @@ async function main() {
168
169
  }
169
170
  const identity = getOrCreateWorkspaceId(cwd);
170
171
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
171
- const invocationId = agent === "claude_code" && typeof envelope.tool_use_id === "string" ? envelope.tool_use_id : void 0;
172
+ const invocationId = postToolInvocationId(parsed, agent);
172
173
  const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
173
174
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
174
175
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  checkAffectedDecisions,
4
4
  formatDecisionsWarning
5
- } from "../chunk-VDLAAUWV.js";
6
- import "../chunk-DWDEQRWN.js";
5
+ } from "../chunk-YITJ2H3B.js";
6
+ import "../chunk-I3HIPAIR.js";
7
7
  import "../chunk-UTKQTZHL.js";
8
8
 
9
9
  // src/hooks/pre-commit.ts
@@ -1,18 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  PREFLIGHT_PROTOCOL_VERSION,
4
+ buildCodexOutput,
4
5
  buildHermesOutput,
5
6
  buildHookOutput,
6
7
  demoteForMode,
8
+ failOpenCodex,
7
9
  failOpenHermes,
8
10
  failOpenOutput,
9
- parsePreflightResponse,
10
11
  proposalFor,
11
12
  readHookMode,
13
+ requestPreflight,
12
14
  resolvePreflightTargets,
13
15
  resultForPreflight,
14
16
  unverifiedResult
15
- } from "../chunk-SWWBK4MP.js";
17
+ } from "../chunk-B4SHJYIN.js";
16
18
  import "../chunk-GUXBUBQ7.js";
17
19
  import {
18
20
  warmBinCache
@@ -21,22 +23,14 @@ import {
21
23
  isRepoActive,
22
24
  repoSyncId
23
25
  } from "../chunk-LDQZASIF.js";
24
- import {
25
- getClient,
26
- getSiteUrl
27
- } from "../chunk-DWDEQRWN.js";
28
- import {
29
- daemonRequest
30
- } from "../chunk-UTKQTZHL.js";
26
+ import "../chunk-I3HIPAIR.js";
31
27
  import {
32
28
  normalizeEnvelope,
33
29
  parseAgent
34
30
  } from "../chunk-TCDUH7AN.js";
35
31
 
36
32
  // src/hooks/pre-tool-use.ts
37
- var HOOK_TIMEOUT_MS = 4500;
38
33
  var STDIN_TIMEOUT_MS = 1e3;
39
- var DAEMON_TIMEOUT_MS = 250;
40
34
  async function readStdin() {
41
35
  return new Promise((resolve, reject) => {
42
36
  const chunks = [];
@@ -60,7 +54,8 @@ function emit(output) {
60
54
  `);
61
55
  }
62
56
  function failOpen() {
63
- return agent === "hermes" ? failOpenHermes() : failOpenOutput();
57
+ if (agent === "hermes") return failOpenHermes();
58
+ return agent === "codex" ? failOpenCodex() : failOpenOutput();
64
59
  }
65
60
  function invocationId(envelope) {
66
61
  const value = agent === "hermes" ? envelope.extra?.tool_call_id : envelope.tool_use_id;
@@ -73,7 +68,9 @@ function emitUnverified(message) {
73
68
  `);
74
69
  emit(failOpenHermes());
75
70
  } else {
76
- emit(buildHookOutput("allow", [result], agent));
71
+ emit(
72
+ agent === "codex" ? buildCodexOutput("allow", [result]) : buildHookOutput("allow", [result])
73
+ );
77
74
  }
78
75
  }
79
76
  async function main() {
@@ -144,19 +141,7 @@ async function main() {
144
141
  };
145
142
  let result;
146
143
  try {
147
- const fromDaemon = await daemonRequest(
148
- "conflict_check",
149
- { ...request, callerEnv: getSiteUrl() },
150
- { timeoutMs: DAEMON_TIMEOUT_MS }
151
- );
152
- let response = parsePreflightResponse(fromDaemon);
153
- if (!response) {
154
- const direct = await getClient().post("/api/cli/decisions/conflict-check", request, {
155
- signal: AbortSignal.timeout(HOOK_TIMEOUT_MS),
156
- quietRefresh: true
157
- });
158
- response = parsePreflightResponse(direct);
159
- }
144
+ const response = await requestPreflight(request);
160
145
  if (!response) {
161
146
  emitUnverified("enforcement service returned an incompatible response");
162
147
  return;
@@ -175,7 +160,7 @@ async function main() {
175
160
  `);
176
161
  }
177
162
  emit(
178
- agent === "hermes" ? buildHermesOutput(aggregate, [result]) : buildHookOutput(aggregate, [result], agent)
163
+ agent === "hermes" ? buildHermesOutput(aggregate, [result]) : agent === "codex" ? buildCodexOutput(aggregate, [result]) : buildHookOutput(aggregate, [result])
179
164
  );
180
165
  }
181
166
  main().catch(() => {
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ postToolInvocationId,
3
4
  shouldFlushAfter,
4
5
  toMove
5
- } from "../chunk-TNNHR3EZ.js";
6
+ } from "../chunk-YDBZTFW5.js";
6
7
  import {
7
8
  appendMove,
8
9
  resolveOrg
9
- } from "../chunk-BOTKPLTI.js";
10
+ } from "../chunk-S4LFRVGD.js";
10
11
  import {
11
12
  scrubFromCwd
12
13
  } from "../chunk-GUXBUBQ7.js";
@@ -19,7 +20,7 @@ import {
19
20
  acknowledgeDecisionFeedback,
20
21
  leaseDecisionFeedback,
21
22
  renderFeedback
22
- } from "../chunk-WELBNODJ.js";
23
+ } from "../chunk-R4CUX6FY.js";
23
24
  import {
24
25
  getOrCreateWorkspaceId
25
26
  } from "../chunk-IMAIBPUC.js";
@@ -29,7 +30,7 @@ import {
29
30
  import {
30
31
  isRepoActiveForCapture
31
32
  } from "../chunk-LDQZASIF.js";
32
- import "../chunk-DWDEQRWN.js";
33
+ import "../chunk-I3HIPAIR.js";
33
34
  import {
34
35
  normalizeEnvelope,
35
36
  parseAgent
@@ -90,9 +91,7 @@ async function main() {
90
91
  const identity = getOrCreateWorkspaceId(cwd);
91
92
  const workspaceId = identity.status === "ready" ? identity.workspaceId : void 0;
92
93
  try {
93
- const successfulPost = parsed.hook_event_name === "PostToolUse";
94
- const rawInvocationId = agent === "hermes" ? parsed.extra?.tool_call_id : parsed.tool_use_id;
95
- const invocationId = successfulPost && typeof rawInvocationId === "string" ? rawInvocationId : void 0;
94
+ const invocationId = postToolInvocationId(parsed, agent);
96
95
  const base = toMove(parsed, resolveCliVersion(), agent, workspaceId, invocationId);
97
96
  const move = { ...base, payload: scrubFromCwd(parsed, cwd) };
98
97
  const { orgId } = resolveOrg({ sessionId: move.sessionId, cwd: move.env.cwd });
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  hasUsableCodexGuidance,
4
4
  refreshClaudePlugins
5
- } from "../chunk-GQURYENF.js";
5
+ } from "../chunk-UBEBEBLM.js";
6
6
  import {
7
7
  buildHookOutput,
8
8
  handoffHookOutput
@@ -12,7 +12,7 @@ import {
12
12
  acknowledgeDecisionFeedback,
13
13
  leaseDecisionFeedback,
14
14
  renderFeedback
15
- } from "../chunk-WELBNODJ.js";
15
+ } from "../chunk-R4CUX6FY.js";
16
16
  import {
17
17
  getOrCreateWorkspaceId
18
18
  } from "../chunk-IMAIBPUC.js";
@@ -27,7 +27,7 @@ import {
27
27
  import {
28
28
  getSiteUrl,
29
29
  isSessionEnded
30
- } from "../chunk-DWDEQRWN.js";
30
+ } from "../chunk-I3HIPAIR.js";
31
31
  import {
32
32
  daemonRequest
33
33
  } from "../chunk-UTKQTZHL.js";
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  checkAffectedDecisions,
13
13
  daemonOrDirectGet,
14
14
  formatDecisionsWarning
15
- } from "./chunk-VDLAAUWV.js";
15
+ } from "./chunk-YITJ2H3B.js";
16
16
  import {
17
17
  JOURNAL_DIR,
18
18
  SESSIONS_DIR,
@@ -21,14 +21,14 @@ import {
21
21
  listFlushing,
22
22
  pendingJournalStats,
23
23
  readMovesFromPath
24
- } from "./chunk-BOTKPLTI.js";
24
+ } from "./chunk-S4LFRVGD.js";
25
25
  import {
26
26
  printJson,
27
27
  registerSkillCommands
28
- } from "./chunk-GQURYENF.js";
28
+ } from "./chunk-UBEBEBLM.js";
29
29
  import {
30
30
  fetchFeedbackCapability
31
- } from "./chunk-WELBNODJ.js";
31
+ } from "./chunk-R4CUX6FY.js";
32
32
  import {
33
33
  inspectWorkspaceId
34
34
  } from "./chunk-IMAIBPUC.js";
@@ -65,7 +65,7 @@ import {
65
65
  resolveAuthCredential,
66
66
  setStoredToken,
67
67
  withFileLock
68
- } from "./chunk-DWDEQRWN.js";
68
+ } from "./chunk-I3HIPAIR.js";
69
69
  import {
70
70
  daemonIsLive,
71
71
  daemonRequest
@@ -3054,14 +3054,16 @@ var CREATE_INACTIVE_PROMPT = "[prim] Decision ingestion is disabled here. Create
3054
3054
  var CREATE_INACTIVE_APPROVED = "[prim] one-time Decision creation approved; passive ingestion remains disabled here";
3055
3055
  var CREATE_INACTIVE_REJECTED = "[prim] decision not created: Decision ingestion is disabled here; rerun with prim's --yes to approve this one Decision, or run `prim enable` in a Git project";
3056
3056
  var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
3057
+ var collectItem = (value, previous) => previous.concat(value);
3058
+ var collectPaths = (value, previous = []) => previous.concat(splitList(value));
3057
3059
  function registerDecisionsCommands(program2) {
3058
3060
  const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
3059
3061
  decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
3060
3062
  "--files <files>",
3061
- "Comma-separated file paths to check against the Decision Graph"
3063
+ "Comma-separated file paths to check against the Decision Graph (repeatable)",
3064
+ collectPaths
3062
3065
  ).action(async (opts) => {
3063
- const filePaths = splitList(opts.files);
3064
- const result = await checkAffectedDecisions(filePaths);
3066
+ const result = await checkAffectedDecisions(opts.files);
3065
3067
  const warning = formatDecisionsWarning(result);
3066
3068
  if (warning) {
3067
3069
  console.error(warning);
@@ -3134,8 +3136,22 @@ function registerDecisionsCommands(program2) {
3134
3136
  ).option("--kind <kind>", "change | exploration | task_execution | unclear (default change)").option("--rationale <text>", "Why the decision was made").option(
3135
3137
  "--area <area>",
3136
3138
  "Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
3137
- ).option("--decided <items>", "Comma-separated option(s) chosen").option("--alternatives <items>", "Comma-separated options considered but rejected").option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option("--files <paths>", "Comma-separated exact repo-relative paths this decision governs").action(async (opts, command) => {
3138
- const requestedFiles = splitList(opts.files);
3139
+ ).option(
3140
+ "--decided <item>",
3141
+ "One adopted constraint, verbatim; repeat the flag for each bullet",
3142
+ collectItem,
3143
+ []
3144
+ ).option(
3145
+ "--alternatives <item>",
3146
+ "One rejected option, verbatim; repeat the flag for each",
3147
+ collectItem,
3148
+ []
3149
+ ).option("--confidence <level>", "high | medium | low (default high)").option("--reversibility <level>", "high | low (default high)").option(
3150
+ "--files <paths>",
3151
+ "Comma-separated exact repo-relative paths this decision governs (repeatable)",
3152
+ collectPaths
3153
+ ).action(async (opts, command) => {
3154
+ const requestedFiles = opts.files ?? [];
3139
3155
  let explicitScope = {};
3140
3156
  if (requestedFiles.length > 0) {
3141
3157
  const binding = repoSyncId(process.cwd());
@@ -3175,8 +3191,8 @@ function registerDecisionsCommands(program2) {
3175
3191
  kind: opts.kind,
3176
3192
  rationale: opts.rationale,
3177
3193
  area: opts.area,
3178
- decided: splitList(opts.decided),
3179
- alternatives: splitList(opts.alternatives),
3194
+ decided: opts.decided,
3195
+ alternatives: opts.alternatives,
3180
3196
  confidence: opts.confidence,
3181
3197
  reversibility: opts.reversibility,
3182
3198
  ...explicitScope
@@ -4621,9 +4637,7 @@ async function performReconcile(idOrShortId, opts = {}) {
4621
4637
  process.exitCode = EXIT_SERVER;
4622
4638
  }
4623
4639
  function registerReconcileCommands(program2) {
4624
- program2.command("reconcile <idOrShortId>").description(
4625
- "Issue a single-use bypass for a decision flagged by Conflict Gates Enforcement (not currently enabled)"
4626
- ).option(
4640
+ program2.command("reconcile <idOrShortId>").description("Issue a single-use bypass for a decision flagged by Conflict Gates Enforcement").option(
4627
4641
  "--flag <conflictFlagId>",
4628
4642
  "Specific flag id to bind the bypass to (default: the decision's latest unack'd flag)"
4629
4643
  ).option("--json", "(reserved; STDOUT is always JSON)").action(async (idOrShortId, opts) => {
@@ -4906,7 +4920,7 @@ function registerSetupCommand(program2, dependencies = {}) {
4906
4920
  }
4907
4921
  }
4908
4922
  note("welcome");
4909
- results.welcome = run(["welcome"]).code === 0 ? "ok" : "failed";
4923
+ results.welcome = run(["welcome", "--agent", agent]).code === 0 ? "ok" : "failed";
4910
4924
  const failed = Object.entries(results).filter(([, v]) => v === "failed").map(([k]) => k);
4911
4925
  const trail = Object.entries(results).map(([k, v]) => `${k}:${v}`).join(" \xB7 ");
4912
4926
  note(
@@ -5012,28 +5026,58 @@ function registerStatuslineCommands(program2) {
5012
5026
  // src/commands/welcome.ts
5013
5027
  var CMD_GUTTER = 38;
5014
5028
  var RECENT_LIMIT = 5;
5015
- var REVERSE_PROMPT_LINES = [
5016
- "What are the most important goals in your organization that you're",
5017
- "responsible for, right now? What are you not focusing on, in order",
5018
- "to focus on those goals?"
5019
- ];
5020
- var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
5021
- var REVERSE_PROMPT_TEMPLATE = "We're interested in learning your current goals, as well as what you're not focusing on to achieve those goals. We found the following:\n\n$FOUND_GOALS\n\nHow do these look to you? Would you like to change these goals?";
5022
- var CALLOUT_TITLE = "Your turn";
5023
- var CALLOUT_INDENT = " ";
5024
- function ruledQuestion(lines) {
5025
- const prefix = `\u250C\u2500 ${CALLOUT_TITLE} `;
5026
- const width = Math.max(
5027
- `${prefix}\u2510`.length,
5028
- ...lines.map((line) => CALLOUT_INDENT.length + line.length + 1)
5029
- );
5030
- const top = `${prefix}${"\u2500".repeat(width - prefix.length - 1)}\u2510`;
5031
- const bottom = `\u2514${"\u2500".repeat(top.length - 2)}\u2518`;
5029
+ var WELCOME_AGENTS = ["claude", "codex", "hermes"];
5030
+ function resolveWelcomeAgent(agentFlag, env) {
5031
+ if (agentFlag !== void 0 && WELCOME_AGENTS.includes(agentFlag)) {
5032
+ return agentFlag;
5033
+ }
5034
+ if (agentFlag === void 0 && env.HERMES_INTERACTIVE) {
5035
+ return "hermes";
5036
+ }
5037
+ return void 0;
5038
+ }
5039
+ var CAPTURE_LINES = {
5040
+ claude: ["Capture is automatic \u2014 keep coding; your decisions are recorded for you."],
5041
+ codex: [
5042
+ "Capture starts once you trust the prim hooks \u2014 run /hooks in Codex;",
5043
+ "until then nothing is recorded."
5044
+ ],
5045
+ hermes: [
5046
+ "Capture starts once you approve the prim hooks when Hermes prompts;",
5047
+ "until then nothing is recorded."
5048
+ ],
5049
+ generic: [
5050
+ "Capture is automatic once your agent's prim hooks are active \u2014 keep",
5051
+ "coding; your decisions are recorded for you."
5052
+ ]
5053
+ };
5054
+ var PASSIVE_LINES = {
5055
+ claude: ["Otherwise Primitive captures decisions passively while you work."],
5056
+ codex: [
5057
+ "Once you trust the prim hooks (/hooks in Codex), Primitive captures",
5058
+ "decisions passively while you work."
5059
+ ],
5060
+ hermes: [
5061
+ "Once you approve the prim hooks, Primitive captures decisions",
5062
+ "passively while you work."
5063
+ ],
5064
+ generic: [
5065
+ "Once your agent's prim hooks are active, Primitive captures decisions",
5066
+ "passively while you work."
5067
+ ]
5068
+ };
5069
+ var GUIDANCE_PASSIVE = {
5070
+ claude: "Otherwise, Primitive passively captures decisions in the background while you work.",
5071
+ codex: "Otherwise, once you trust the prim hooks (run /hooks in Codex), Primitive passively captures decisions in the background while you work.",
5072
+ hermes: "Otherwise, once you approve the prim hooks when Hermes prompts, Primitive passively captures decisions in the background while you work.",
5073
+ generic: "Otherwise, once your agent's prim hooks are active, Primitive passively captures decisions in the background while you work."
5074
+ };
5075
+ function seedGuidanceFor(agent) {
5032
5076
  return [
5033
- color(top, "green"),
5034
- ...lines.map((line) => `${CALLOUT_INDENT}${line}`),
5035
- color(bottom, "green")
5036
- ];
5077
+ "You can tell your agent to add any decision to your Primitive decision graph at any time.",
5078
+ GUIDANCE_PASSIVE[agent ?? "generic"],
5079
+ "To capture decisions in other repositories, enable Primitive there too: run `prim enable` in each one."
5080
+ ].join(" ");
5037
5081
  }
5038
5082
  function welcomeStateFromRecent(result) {
5039
5083
  if (result.unavailable !== void 0) {
@@ -5046,9 +5090,14 @@ function welcomeStateFromRecent(result) {
5046
5090
  }
5047
5091
  return { org: "active", recent };
5048
5092
  }
5049
- function formatWelcome(state) {
5093
+ function formatWelcome(state, agent) {
5050
5094
  const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
5051
5095
  const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
5096
+ const asBullet = ([first, ...rest]) => [
5097
+ bullet(first),
5098
+ ...rest.map((line) => ` ${line}`)
5099
+ ];
5100
+ const copyKey = agent ?? "generic";
5052
5101
  const head = [
5053
5102
  bold(color("Welcome to Primitive", "green")),
5054
5103
  "",
@@ -5056,10 +5105,9 @@ function formatWelcome(state) {
5056
5105
  "shared graph \u2014 automatically, as you work.",
5057
5106
  "",
5058
5107
  bold("How it works"),
5059
- bullet("Capture is automatic \u2014 keep coding; your decisions are recorded for you."),
5060
- bullet("Conflict Gates (with Enforcement) flag or block edits that conflict"),
5061
- " with a load-bearing decision \u2014 not currently enabled. Contact",
5062
- " support@getprimitive.ai to turn them on for your team.",
5108
+ ...asBullet(CAPTURE_LINES[copyKey]),
5109
+ bullet("Conflict Gates can warn, ask, or block edits that conflict"),
5110
+ " with a load-bearing decision when Enforcement is enabled.",
5063
5111
  bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
5064
5112
  " answering keeps the graph trustworthy.",
5065
5113
  ""
@@ -5079,10 +5127,16 @@ function formatWelcome(state) {
5079
5127
  body = [
5080
5128
  ...teamContext,
5081
5129
  bold("Let's seed your decision graph"),
5082
- "You haven't recorded a decision yet \u2014 answer this and I'll record",
5083
- "each goal as a decision:",
5130
+ "You haven't recorded a decision yet. Your agent will look through",
5131
+ "what you've told it and this repo's shared memory files, and propose",
5132
+ "any decisions it finds \u2014 approve, revise, or reject each one, and",
5133
+ 'share the "why" when it asks. Approved decisions become part of',
5134
+ "your team's shared graph, visible to any teammates.",
5084
5135
  "",
5085
- ...ruledQuestion(REVERSE_PROMPT_LINES)
5136
+ bold("From here on"),
5137
+ bullet("Tell your agent to add any decision to Primitive, any time."),
5138
+ ...asBullet(PASSIVE_LINES[copyKey]),
5139
+ bullet("Run `prim enable` in each additional repo you want captured.")
5086
5140
  ];
5087
5141
  } else {
5088
5142
  body = [
@@ -5092,10 +5146,9 @@ function formatWelcome(state) {
5092
5146
  cmd("prim --help", "everything else")
5093
5147
  ];
5094
5148
  }
5095
- const footer = state.org === "seed" ? [] : ["", dim("App: https://app.getprimitive.ai")];
5096
- return [...head, ...body, ...footer].join("\n");
5149
+ return [...head, ...body, "", dim("App: https://app.getprimitive.ai")].join("\n");
5097
5150
  }
5098
- function welcomeJson(state) {
5151
+ function welcomeJson(state, agent) {
5099
5152
  if (state.org === "active") {
5100
5153
  return { welcomed: true, org: "active", recent: state.recent };
5101
5154
  }
@@ -5103,20 +5156,23 @@ function welcomeJson(state) {
5103
5156
  return {
5104
5157
  welcomed: true,
5105
5158
  org: "seed",
5106
- reversePrompt: REVERSE_PROMPT,
5107
- reversePromptTemplate: REVERSE_PROMPT_TEMPLATE,
5159
+ seedGuidance: seedGuidanceFor(agent),
5108
5160
  recent: state.recent
5109
5161
  };
5110
5162
  }
5111
5163
  return { welcomed: true, org: "unknown" };
5112
5164
  }
5113
5165
  function registerWelcomeCommand(program2, deps = { getClient }) {
5114
- program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(async () => {
5166
+ program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").option(
5167
+ "--agent <agent>",
5168
+ "claude, codex, or hermes \u2014 tailors the capture guidance (Hermes auto-detected)"
5169
+ ).action(async (options) => {
5170
+ const agent = resolveWelcomeAgent(options.agent, process.env);
5115
5171
  const result = await fetchRecent({ limit: RECENT_LIMIT }, deps);
5116
5172
  const state = welcomeStateFromRecent(result);
5117
- process.stderr.write(`${formatWelcome(state)}
5173
+ process.stderr.write(`${formatWelcome(state, agent)}
5118
5174
  `);
5119
- printJson(welcomeJson(state));
5175
+ printJson(welcomeJson(state, agent));
5120
5176
  });
5121
5177
  }
5122
5178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primitive.ai/prim",
3
- "version": "0.1.0-alpha.57",
3
+ "version": "0.1.0-alpha.59",
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",