@primitive.ai/prim 0.1.0-alpha.20 → 0.1.0-alpha.22
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 +13 -2
- package/SKILL.md +11 -1
- package/dist/{chunk-BEEGFDGU.js → chunk-4QJOQIY6.js} +8 -0
- package/dist/daemon/server.js +6 -5
- package/dist/hooks/post-tool-use.js +1 -1
- package/dist/index.js +239 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,9 +58,14 @@ graph, conflicting edits are gated, and presence is reported. Each hook
|
|
|
58
58
|
self-resolves the CLI at run time (PATH, then a local install, then
|
|
59
59
|
`npx --yes @latest`), so it keeps working with no global install.
|
|
60
60
|
|
|
61
|
+
Installs into the current project by default — the repo's `.claude/settings.json`
|
|
62
|
+
/ `.codex/hooks.json`, resolved from the git root (so any subdirectory works);
|
|
63
|
+
pass `--scope user` to install machine-wide.
|
|
64
|
+
|
|
61
65
|
```bash
|
|
62
|
-
prim claude install
|
|
63
|
-
prim
|
|
66
|
+
prim claude install # Install Claude Code hooks (project scope; uninstall / status)
|
|
67
|
+
prim claude install --scope user # Install machine-wide instead
|
|
68
|
+
prim codex install # Install OpenAI Codex hooks (project scope)
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
### Daemon
|
|
@@ -113,6 +118,12 @@ and offers to install into `.husky/`.
|
|
|
113
118
|
prim statusline # Render the team-presence statusline (reads the daemon)
|
|
114
119
|
```
|
|
115
120
|
|
|
121
|
+
### Welcome
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
prim welcome # Brief orientation to the decision graph (shown after setup)
|
|
125
|
+
```
|
|
126
|
+
|
|
116
127
|
### Session & journal
|
|
117
128
|
|
|
118
129
|
Lower-level plumbing for the capture pipeline — org binding and the local move
|
package/SKILL.md
CHANGED
|
@@ -67,6 +67,16 @@ npx --yes @primitive.ai/prim decisions confirm <idOrShortId>
|
|
|
67
67
|
|
|
68
68
|
Confirmations are author-targeted and rare by design; answering keeps the graph's rationale trustworthy. Don't manufacture rationale — if you don't know why a decision was made, say so.
|
|
69
69
|
|
|
70
|
+
## Author a decision deliberately
|
|
71
|
+
|
|
72
|
+
Capture is automatic for the decisions you *make while coding*. When the user instead asks you to **record a decision explicitly** — one that didn't fall out of an edit (a design call, a convention, a choice settled in discussion) — author it directly:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab over Yjs" --area data --rationale "Server-authoritative ordering" --alternatives "Yjs,Automerge"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Only `--intent` is required. Optional: `--kind` (change|exploration|task_execution|unclear, default change), `--rationale`, `--area`, `--decided`, `--alternatives` (comma-separated), `--confidence` (high|medium|low, default high), `--reversibility` (high|low, default high), and `--files` (comma-separated repo-relative paths the decision governs — pass these to make the conflict gate fire on later edits to those files, same path form as `decisions check`). STDOUT is the created identity `{ decisionId, shortId, createdAt }`; STDERR prints `[prim] created dec_<short>.` — pass that `dec_<short>` straight into `decisions show` / `cascade` / `confirm`. Author on the user's behalf only when they ask for a decision to be recorded; don't narrate your own routine edits into the graph (the hooks already do that).
|
|
79
|
+
|
|
70
80
|
## Presence
|
|
71
81
|
|
|
72
82
|
With the daemon running (`npx --yes @primitive.ai/prim daemon start`), `npx --yes @primitive.ai/prim daemon status` includes the live online count in its STDOUT JSON (when presence is fresh); Claude Code surfaces it in the statusline as `team: N online`. Your captured decisions are attributed to your agent automatically -- no flag required.
|
|
@@ -108,7 +118,7 @@ Examples:
|
|
|
108
118
|
- **An "unavailable" / "not verified" gate or check is not an all-clear.** Treat constraints as UNKNOWN and proceed deliberately; never read the silence as approval.
|
|
109
119
|
- **A `deny` means a real prior decision conflicts.** Reconcile only when you genuinely intend to override it; otherwise pick an approach that respects it.
|
|
110
120
|
- **Reconcile bypasses are single-use and short-lived.** One bypass clears your *next* edit to the governed file; it is not a standing override.
|
|
111
|
-
- **Capture is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status`) and the daemon is running — don't try to inject moves by hand.
|
|
121
|
+
- **Capture of your coding activity is automatic, never manual.** If decisions aren't showing up, check that the session hooks are installed (`claude status` / `codex status`) and the daemon is running — don't try to inject moves by hand. (Deliberately *authoring* a decision the user asks you to record is a separate, supported path — `decisions create`, above.)
|
|
112
122
|
- **Don't fabricate rationale on a confirmation.** If you don't know why a decision was made, say so rather than guessing.
|
|
113
123
|
|
|
114
124
|
## After each task
|
|
@@ -11,6 +11,7 @@ var ANSI_CODES = {
|
|
|
11
11
|
gray: "\x1B[90m"
|
|
12
12
|
};
|
|
13
13
|
var ANSI_RESET = "\x1B[0m";
|
|
14
|
+
var ANSI_DIM = "\x1B[2m";
|
|
14
15
|
var ANSI_BOLD = "\x1B[1m";
|
|
15
16
|
function supportsColor() {
|
|
16
17
|
if (process.env.NO_COLOR !== void 0 && process.env.NO_COLOR !== "") {
|
|
@@ -24,6 +25,12 @@ function color(text, c) {
|
|
|
24
25
|
}
|
|
25
26
|
return `${ANSI_CODES[c]}${text}${ANSI_RESET}`;
|
|
26
27
|
}
|
|
28
|
+
function dim(text) {
|
|
29
|
+
if (!supportsColor()) {
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
return `${ANSI_DIM}${text}${ANSI_RESET}`;
|
|
33
|
+
}
|
|
27
34
|
function bold(text) {
|
|
28
35
|
if (!supportsColor()) {
|
|
29
36
|
return text;
|
|
@@ -53,6 +60,7 @@ function stripAnsi(text) {
|
|
|
53
60
|
|
|
54
61
|
export {
|
|
55
62
|
color,
|
|
63
|
+
dim,
|
|
56
64
|
bold,
|
|
57
65
|
colorForArea,
|
|
58
66
|
stripAnsi
|
package/dist/daemon/server.js
CHANGED
|
@@ -27,6 +27,7 @@ var client = getClient();
|
|
|
27
27
|
var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
|
|
28
28
|
var lastHeartbeatAt;
|
|
29
29
|
var lastOnlineCount;
|
|
30
|
+
var lastOnlineNames;
|
|
30
31
|
var lastOkAtLocal;
|
|
31
32
|
var heartbeatTimer;
|
|
32
33
|
var tokenCheckTimer;
|
|
@@ -70,9 +71,8 @@ async function sendHeartbeat() {
|
|
|
70
71
|
if (typeof result.lastHeartbeatAt === "number") {
|
|
71
72
|
lastHeartbeatAt = result.lastHeartbeatAt;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
74
|
+
lastOnlineCount = typeof result.onlineCount === "number" ? result.onlineCount : void 0;
|
|
75
|
+
lastOnlineNames = Array.isArray(result.onlineNames) ? result.onlineNames : void 0;
|
|
76
76
|
}
|
|
77
77
|
} catch (err) {
|
|
78
78
|
process.stderr.write(
|
|
@@ -123,9 +123,10 @@ function handleStatusSnapshot() {
|
|
|
123
123
|
uptimeMs: Date.now() - startedAt,
|
|
124
124
|
sessionId: activeSessionId,
|
|
125
125
|
lastHeartbeatAt,
|
|
126
|
-
// Withhold a frozen count once
|
|
127
|
-
// "presence: stale" rather than a confident, wrong
|
|
126
|
+
// Withhold a frozen count/names once they're no longer fresh; the
|
|
127
|
+
// statusline shows "presence: stale" rather than a confident, wrong list.
|
|
128
128
|
onlineCount: presenceFresh ? lastOnlineCount : void 0,
|
|
129
|
+
onlineNames: presenceFresh ? lastOnlineNames : void 0,
|
|
129
130
|
presenceStale
|
|
130
131
|
};
|
|
131
132
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
bold,
|
|
3
4
|
color,
|
|
4
5
|
colorForArea,
|
|
6
|
+
dim,
|
|
5
7
|
stripAnsi
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-4QJOQIY6.js";
|
|
7
9
|
import {
|
|
8
10
|
checkAffectedDecisions,
|
|
9
11
|
daemonOrDirectGet,
|
|
@@ -279,6 +281,7 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
// src/commands/claude-install.ts
|
|
284
|
+
import { execSync } from "child_process";
|
|
282
285
|
import {
|
|
283
286
|
closeSync,
|
|
284
287
|
existsSync as existsSync3,
|
|
@@ -368,7 +371,17 @@ var PRIM_BINS = [
|
|
|
368
371
|
];
|
|
369
372
|
var JSON_INDENT = 2;
|
|
370
373
|
var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
|
|
371
|
-
|
|
374
|
+
function projectRoot() {
|
|
375
|
+
try {
|
|
376
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
377
|
+
encoding: "utf-8",
|
|
378
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
379
|
+
}).trim();
|
|
380
|
+
} catch {
|
|
381
|
+
return process.cwd();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
var projectScopePath = () => join2(projectRoot(), ".claude", "settings.json");
|
|
372
385
|
var CAPTURE_EVENTS = [
|
|
373
386
|
"SessionStart",
|
|
374
387
|
"UserPromptSubmit",
|
|
@@ -389,7 +402,7 @@ var REGISTRATIONS = [
|
|
|
389
402
|
makeRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
390
403
|
];
|
|
391
404
|
function settingsPathFor(scope) {
|
|
392
|
-
return scope === "user" ? USER_SCOPE_PATH :
|
|
405
|
+
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
393
406
|
}
|
|
394
407
|
function readSettings(path) {
|
|
395
408
|
if (!existsSync3(path)) {
|
|
@@ -546,15 +559,15 @@ function performStatus() {
|
|
|
546
559
|
statusline: statuslineInstalled(settings)
|
|
547
560
|
};
|
|
548
561
|
};
|
|
549
|
-
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(
|
|
562
|
+
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
550
563
|
}
|
|
551
564
|
function resolveScope(input) {
|
|
552
|
-
if (input === void 0 || input === "
|
|
553
|
-
return "user";
|
|
554
|
-
}
|
|
555
|
-
if (input === "project") {
|
|
565
|
+
if (input === void 0 || input === "project") {
|
|
556
566
|
return "project";
|
|
557
567
|
}
|
|
568
|
+
if (input === "user") {
|
|
569
|
+
return "user";
|
|
570
|
+
}
|
|
558
571
|
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
559
572
|
process.exit(1);
|
|
560
573
|
}
|
|
@@ -562,7 +575,7 @@ function registerClaudeCommands(program2) {
|
|
|
562
575
|
const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
|
|
563
576
|
claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
|
|
564
577
|
"--scope <scope>",
|
|
565
|
-
"
|
|
578
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
566
579
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
567
580
|
const scope = resolveScope(opts.scope);
|
|
568
581
|
const result = performInstall(scope, opts.force ?? false);
|
|
@@ -579,7 +592,7 @@ function registerClaudeCommands(program2) {
|
|
|
579
592
|
});
|
|
580
593
|
claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
|
|
581
594
|
"--scope <scope>",
|
|
582
|
-
"
|
|
595
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
583
596
|
).action((opts) => {
|
|
584
597
|
const scope = resolveScope(opts.scope);
|
|
585
598
|
const result = performUninstall(scope);
|
|
@@ -627,9 +640,9 @@ var CODEX_REGISTRATIONS = [
|
|
|
627
640
|
makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
|
|
628
641
|
];
|
|
629
642
|
var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
|
|
630
|
-
var
|
|
643
|
+
var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
|
|
631
644
|
function settingsPathFor2(scope) {
|
|
632
|
-
return scope === "user" ? USER_SCOPE_PATH2 :
|
|
645
|
+
return scope === "user" ? USER_SCOPE_PATH2 : projectScopePath2();
|
|
633
646
|
}
|
|
634
647
|
function applyInstall2(settings, options = {}) {
|
|
635
648
|
const hooks = { ...settings.hooks ?? {} };
|
|
@@ -694,24 +707,24 @@ function performStatus2() {
|
|
|
694
707
|
const settings = readSettings(path);
|
|
695
708
|
return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
|
|
696
709
|
};
|
|
697
|
-
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(
|
|
710
|
+
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(projectScopePath2()) };
|
|
698
711
|
}
|
|
699
712
|
function resolveScope2(input) {
|
|
700
|
-
if (input === void 0 || input === "
|
|
701
|
-
return "user";
|
|
702
|
-
}
|
|
703
|
-
if (input === "project") {
|
|
713
|
+
if (input === void 0 || input === "project") {
|
|
704
714
|
return "project";
|
|
705
715
|
}
|
|
716
|
+
if (input === "user") {
|
|
717
|
+
return "user";
|
|
718
|
+
}
|
|
706
719
|
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
707
720
|
process.exit(1);
|
|
708
721
|
}
|
|
709
722
|
var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
|
|
710
723
|
function registerCodexCommands(program2) {
|
|
711
724
|
const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
|
|
712
|
-
codex.command("install").description("Register the prim hooks in Codex's
|
|
725
|
+
codex.command("install").description("Register the prim hooks in Codex's hooks.json (project scope by default)").option(
|
|
713
726
|
"--scope <scope>",
|
|
714
|
-
"
|
|
727
|
+
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
715
728
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
716
729
|
const scope = resolveScope2(opts.scope);
|
|
717
730
|
const result = performInstall2(scope, opts.force ?? false);
|
|
@@ -723,9 +736,9 @@ function registerCodexCommands(program2) {
|
|
|
723
736
|
console.error(TRUST_NOTICE);
|
|
724
737
|
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
725
738
|
});
|
|
726
|
-
codex.command("uninstall").description("Remove all prim hooks from
|
|
739
|
+
codex.command("uninstall").description("Remove all prim hooks from Codex's hooks.json").option(
|
|
727
740
|
"--scope <scope>",
|
|
728
|
-
"
|
|
741
|
+
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
729
742
|
).action((opts) => {
|
|
730
743
|
const scope = resolveScope2(opts.scope);
|
|
731
744
|
const result = performUninstall2(scope);
|
|
@@ -751,6 +764,22 @@ import { spawn } from "child_process";
|
|
|
751
764
|
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
752
765
|
import { homedir as homedir3 } from "os";
|
|
753
766
|
import { join as join4 } from "path";
|
|
767
|
+
|
|
768
|
+
// src/lib/presence.ts
|
|
769
|
+
function formatTeammates(names, cap) {
|
|
770
|
+
if (names === void 0) {
|
|
771
|
+
return "\u2014";
|
|
772
|
+
}
|
|
773
|
+
if (names.length === 0) {
|
|
774
|
+
return "just you";
|
|
775
|
+
}
|
|
776
|
+
if (names.length <= cap) {
|
|
777
|
+
return names.join(", ");
|
|
778
|
+
}
|
|
779
|
+
return `${names.slice(0, cap).join(", ")} +${String(names.length - cap)}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/commands/daemon.ts
|
|
754
783
|
var DAEMON_BIN = "prim-daemon-server";
|
|
755
784
|
var PID_PATH = join4(homedir3(), ".config", "prim", "daemon.pid");
|
|
756
785
|
var SOCK_PATH = join4(homedir3(), ".config", "prim", "sock");
|
|
@@ -924,8 +953,9 @@ async function daemonStatus() {
|
|
|
924
953
|
} else if (!snapshot) {
|
|
925
954
|
process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
|
|
926
955
|
} else {
|
|
956
|
+
const team = snapshot.onlineNames !== void 0 ? ` \xB7 team: ${formatTeammates(snapshot.onlineNames, Number.POSITIVE_INFINITY)}` : "";
|
|
927
957
|
process.stderr.write(
|
|
928
|
-
`[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
|
|
958
|
+
`[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}${team}
|
|
929
959
|
`
|
|
930
960
|
);
|
|
931
961
|
}
|
|
@@ -1189,8 +1219,8 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1189
1219
|
if (args.since !== void 0) {
|
|
1190
1220
|
params.set("since", args.since);
|
|
1191
1221
|
}
|
|
1192
|
-
const client = deps.getClient();
|
|
1193
1222
|
try {
|
|
1223
|
+
const client = deps.getClient();
|
|
1194
1224
|
const res = await daemonOrDirectGet(
|
|
1195
1225
|
"decisions_recent",
|
|
1196
1226
|
`/api/cli/decisions/recent?${params.toString()}`,
|
|
@@ -1236,9 +1266,18 @@ function authorLabel(row) {
|
|
|
1236
1266
|
}
|
|
1237
1267
|
}
|
|
1238
1268
|
var AUTHOR_WIDTH = 18;
|
|
1269
|
+
var AREA_WIDTH = 12;
|
|
1239
1270
|
function padRight(s, width) {
|
|
1240
1271
|
return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
|
|
1241
1272
|
}
|
|
1273
|
+
function formatRecentRow(row) {
|
|
1274
|
+
const clock = formatClock(row.classifiedAt);
|
|
1275
|
+
const author = padRight(authorLabel(row), AUTHOR_WIDTH);
|
|
1276
|
+
const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
|
|
1277
|
+
const areaPlain = padRight(areaText, AREA_WIDTH);
|
|
1278
|
+
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1279
|
+
return ` ${clock} ${author}${areaCol}${row.intent}`;
|
|
1280
|
+
}
|
|
1242
1281
|
function formatRecentHuman(result) {
|
|
1243
1282
|
if (result.unavailable !== void 0) {
|
|
1244
1283
|
return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
|
|
@@ -1248,12 +1287,7 @@ function formatRecentHuman(result) {
|
|
|
1248
1287
|
}
|
|
1249
1288
|
const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
|
|
1250
1289
|
for (const row of result.decisions) {
|
|
1251
|
-
|
|
1252
|
-
const author = padRight(authorLabel(row), AUTHOR_WIDTH);
|
|
1253
|
-
const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
|
|
1254
|
-
const areaPlain = padRight(areaText, 12);
|
|
1255
|
-
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1256
|
-
lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
|
|
1290
|
+
lines.push(formatRecentRow(row));
|
|
1257
1291
|
}
|
|
1258
1292
|
return lines.join("\n");
|
|
1259
1293
|
}
|
|
@@ -1342,6 +1376,44 @@ function formatConfirmJson(result) {
|
|
|
1342
1376
|
return JSON.stringify(result.outcome, null, 2);
|
|
1343
1377
|
}
|
|
1344
1378
|
|
|
1379
|
+
// src/decisions/create.ts
|
|
1380
|
+
var CREATE_TIMEOUT_MS = 1e4;
|
|
1381
|
+
var defaultDeps4 = { getClient };
|
|
1382
|
+
function toRequestBody(request) {
|
|
1383
|
+
const candidate = {
|
|
1384
|
+
intent: request.intent,
|
|
1385
|
+
kind: request.kind,
|
|
1386
|
+
rationale: request.rationale,
|
|
1387
|
+
area: request.area,
|
|
1388
|
+
decided: request.decided,
|
|
1389
|
+
alternatives: request.alternatives,
|
|
1390
|
+
confidence: request.confidence,
|
|
1391
|
+
reversibility: request.reversibility,
|
|
1392
|
+
files: request.files
|
|
1393
|
+
};
|
|
1394
|
+
const body = {};
|
|
1395
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
1396
|
+
const isEmpty = value === void 0 || Array.isArray(value) && value.length === 0;
|
|
1397
|
+
if (!isEmpty) {
|
|
1398
|
+
body[key] = value;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return body;
|
|
1402
|
+
}
|
|
1403
|
+
async function fetchCreate(request, deps = defaultDeps4) {
|
|
1404
|
+
const client = deps.getClient();
|
|
1405
|
+
return await client.post("/api/cli/decisions/create", toRequestBody(request), {
|
|
1406
|
+
signal: AbortSignal.timeout(CREATE_TIMEOUT_MS)
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function formatCreateHuman(outcome) {
|
|
1410
|
+
const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
|
|
1411
|
+
return `[prim] created ${id}.`;
|
|
1412
|
+
}
|
|
1413
|
+
function formatCreateJson(outcome) {
|
|
1414
|
+
return JSON.stringify(outcome, null, 2);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1345
1417
|
// src/decisions/show.ts
|
|
1346
1418
|
var NOT_FOUND_RE3 = /not found/i;
|
|
1347
1419
|
function colorStatus(status) {
|
|
@@ -1354,14 +1426,14 @@ function colorStatus(status) {
|
|
|
1354
1426
|
return color(status, "gray");
|
|
1355
1427
|
}
|
|
1356
1428
|
var SHOW_TIMEOUT_MS = 1e4;
|
|
1357
|
-
var
|
|
1429
|
+
var defaultDeps5 = { getClient };
|
|
1358
1430
|
var DecisionNotFoundError = class extends Error {
|
|
1359
1431
|
constructor(idOrShortId) {
|
|
1360
1432
|
super(`Decision not found: ${idOrShortId}`);
|
|
1361
1433
|
this.name = "DecisionNotFoundError";
|
|
1362
1434
|
}
|
|
1363
1435
|
};
|
|
1364
|
-
async function fetchShow(idOrShortId, deps =
|
|
1436
|
+
async function fetchShow(idOrShortId, deps = defaultDeps5) {
|
|
1365
1437
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1366
1438
|
const client = deps.getClient();
|
|
1367
1439
|
try {
|
|
@@ -1478,13 +1550,15 @@ function formatShowJson(result) {
|
|
|
1478
1550
|
|
|
1479
1551
|
// src/commands/decisions.ts
|
|
1480
1552
|
var EXIT_NOT_FOUND = 4;
|
|
1553
|
+
var EXIT_USAGE = 2;
|
|
1554
|
+
var splitList = (value) => (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1481
1555
|
function registerDecisionsCommands(program2) {
|
|
1482
1556
|
const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
|
|
1483
1557
|
decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
|
|
1484
1558
|
"--files <files>",
|
|
1485
1559
|
"Comma-separated file paths to check against the Decision Graph"
|
|
1486
1560
|
).action(async (opts) => {
|
|
1487
|
-
const filePaths = opts.files
|
|
1561
|
+
const filePaths = splitList(opts.files);
|
|
1488
1562
|
const result = await checkAffectedDecisions(filePaths);
|
|
1489
1563
|
const warning = formatDecisionsWarning(result);
|
|
1490
1564
|
if (warning) {
|
|
@@ -1546,10 +1620,44 @@ function registerDecisionsCommands(program2) {
|
|
|
1546
1620
|
throw err;
|
|
1547
1621
|
}
|
|
1548
1622
|
});
|
|
1623
|
+
decisions.command("create").description("Author a decision directly \u2014 the deliberate manual path around automatic capture").requiredOption("--intent <text>", "What was decided (required)").option("--kind <kind>", "change | exploration | task_execution | unclear (default change)").option("--rationale <text>", "Why the decision was made").option(
|
|
1624
|
+
"--area <area>",
|
|
1625
|
+
"Functional area (auth, data, infra, ui, api, billing, mobile, docs, testing)"
|
|
1626
|
+
).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(
|
|
1627
|
+
"--files <paths>",
|
|
1628
|
+
"Comma-separated repo-relative paths this decision governs (gates edits to them)"
|
|
1629
|
+
).action(async (opts) => {
|
|
1630
|
+
const request = {
|
|
1631
|
+
intent: opts.intent,
|
|
1632
|
+
kind: opts.kind,
|
|
1633
|
+
rationale: opts.rationale,
|
|
1634
|
+
area: opts.area,
|
|
1635
|
+
decided: splitList(opts.decided),
|
|
1636
|
+
alternatives: splitList(opts.alternatives),
|
|
1637
|
+
confidence: opts.confidence,
|
|
1638
|
+
reversibility: opts.reversibility,
|
|
1639
|
+
files: splitList(opts.files)
|
|
1640
|
+
};
|
|
1641
|
+
try {
|
|
1642
|
+
const outcome = await fetchCreate(request);
|
|
1643
|
+
console.error(formatCreateHuman(outcome));
|
|
1644
|
+
console.log(formatCreateJson(outcome));
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
if (err instanceof HttpError && err.status >= 400 && err.status < 500) {
|
|
1647
|
+
console.error(`[prim] create rejected: ${err.message}`);
|
|
1648
|
+
console.log(
|
|
1649
|
+
JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2)
|
|
1650
|
+
);
|
|
1651
|
+
process.exitCode = EXIT_USAGE;
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
throw err;
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1549
1657
|
}
|
|
1550
1658
|
|
|
1551
1659
|
// src/commands/hooks.ts
|
|
1552
|
-
import { execSync } from "child_process";
|
|
1660
|
+
import { execSync as execSync2 } from "child_process";
|
|
1553
1661
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1554
1662
|
import { resolve } from "path";
|
|
1555
1663
|
import { Option } from "commander";
|
|
@@ -1587,7 +1695,7 @@ ${hookShim(spec.binName)}
|
|
|
1587
1695
|
${end}`;
|
|
1588
1696
|
}
|
|
1589
1697
|
function getGitRoot() {
|
|
1590
|
-
return
|
|
1698
|
+
return execSync2("git rev-parse --show-toplevel", {
|
|
1591
1699
|
encoding: "utf-8"
|
|
1592
1700
|
}).trim();
|
|
1593
1701
|
}
|
|
@@ -1866,7 +1974,7 @@ function registerMovesCommands(program2) {
|
|
|
1866
1974
|
|
|
1867
1975
|
// src/commands/reconcile.ts
|
|
1868
1976
|
var EXIT_OK2 = 0;
|
|
1869
|
-
var
|
|
1977
|
+
var EXIT_USAGE2 = 2;
|
|
1870
1978
|
var EXIT_SERVER = 3;
|
|
1871
1979
|
var HTTP_CLIENT_ERROR_MIN = 400;
|
|
1872
1980
|
var HTTP_SERVER_ERROR_MIN = 500;
|
|
@@ -1910,7 +2018,7 @@ async function performReconcile(idOrShortId, opts = {}) {
|
|
|
1910
2018
|
process.stderr.write(`[prim] reconcile rejected: ${err.message}
|
|
1911
2019
|
`);
|
|
1912
2020
|
console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
|
|
1913
|
-
process.exitCode =
|
|
2021
|
+
process.exitCode = EXIT_USAGE2;
|
|
1914
2022
|
return;
|
|
1915
2023
|
}
|
|
1916
2024
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2172,6 +2280,7 @@ import { readFileSync as readFileSync8 } from "fs";
|
|
|
2172
2280
|
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
2173
2281
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2174
2282
|
var STATUSLINE_TIMEOUT_MS = 200;
|
|
2283
|
+
var STATUSLINE_NAME_CAP = 3;
|
|
2175
2284
|
function readPackageVersion() {
|
|
2176
2285
|
try {
|
|
2177
2286
|
const here = dirname5(fileURLToPath3(import.meta.url));
|
|
@@ -2209,7 +2318,14 @@ async function renderStatusline() {
|
|
|
2209
2318
|
if (snapshot.presenceStale) {
|
|
2210
2319
|
return `primitive ${version} (daemon: live \xB7 presence: stale)`;
|
|
2211
2320
|
}
|
|
2212
|
-
|
|
2321
|
+
let team;
|
|
2322
|
+
if (snapshot.onlineNames !== void 0) {
|
|
2323
|
+
team = `team: ${formatTeammates(snapshot.onlineNames, STATUSLINE_NAME_CAP)}`;
|
|
2324
|
+
} else if (typeof snapshot.onlineCount === "number") {
|
|
2325
|
+
team = `team: ${String(snapshot.onlineCount)} online`;
|
|
2326
|
+
} else {
|
|
2327
|
+
team = "team: \u2014";
|
|
2328
|
+
}
|
|
2213
2329
|
return `primitive ${version} (daemon: live \xB7 ${team})`;
|
|
2214
2330
|
}
|
|
2215
2331
|
function registerStatuslineCommands(program2) {
|
|
@@ -2222,6 +2338,91 @@ function registerStatuslineCommands(program2) {
|
|
|
2222
2338
|
});
|
|
2223
2339
|
}
|
|
2224
2340
|
|
|
2341
|
+
// src/commands/welcome.ts
|
|
2342
|
+
var CMD_GUTTER = 38;
|
|
2343
|
+
var RECENT_LIMIT = 5;
|
|
2344
|
+
var REVERSE_PROMPT_LINES = [
|
|
2345
|
+
"What are the most important goals in your organization that you're",
|
|
2346
|
+
"responsible for, right now? What are you not focusing on, in order",
|
|
2347
|
+
"to focus on those goals?"
|
|
2348
|
+
];
|
|
2349
|
+
var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
|
|
2350
|
+
function welcomeStateFromRecent(result) {
|
|
2351
|
+
if (result.unavailable !== void 0) {
|
|
2352
|
+
return { org: "unknown" };
|
|
2353
|
+
}
|
|
2354
|
+
if (result.decisions.length === 0) {
|
|
2355
|
+
return { org: "empty" };
|
|
2356
|
+
}
|
|
2357
|
+
return { org: "active", recent: result.decisions.slice(0, RECENT_LIMIT) };
|
|
2358
|
+
}
|
|
2359
|
+
function formatWelcome(state) {
|
|
2360
|
+
const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
|
|
2361
|
+
const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
|
|
2362
|
+
const head = [
|
|
2363
|
+
bold(color("Welcome to Primitive", "green")),
|
|
2364
|
+
"",
|
|
2365
|
+
"Primitive captures the decisions your team makes while coding into a",
|
|
2366
|
+
"shared graph \u2014 and flags edits that conflict with earlier ones before",
|
|
2367
|
+
"they land.",
|
|
2368
|
+
"",
|
|
2369
|
+
bold("How it works"),
|
|
2370
|
+
bullet("Capture is automatic \u2014 keep coding; your decisions are recorded for you."),
|
|
2371
|
+
bullet("The conflict gate has your back: when an edit conflicts with a"),
|
|
2372
|
+
" load-bearing decision, prim surfaces it. Run `prim reconcile dec_<id>` to clear",
|
|
2373
|
+
" that decision and retry.",
|
|
2374
|
+
bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
|
|
2375
|
+
" answering keeps the graph trustworthy.",
|
|
2376
|
+
""
|
|
2377
|
+
];
|
|
2378
|
+
let body;
|
|
2379
|
+
if (state.org === "active") {
|
|
2380
|
+
body = [
|
|
2381
|
+
bold("Recent team decisions"),
|
|
2382
|
+
...state.recent.map(formatRecentRow),
|
|
2383
|
+
"",
|
|
2384
|
+
bold("Get started"),
|
|
2385
|
+
cmd("prim decisions check --files <files>", "what governs files you're about to change"),
|
|
2386
|
+
cmd("prim --help", "everything else")
|
|
2387
|
+
];
|
|
2388
|
+
} else if (state.org === "empty") {
|
|
2389
|
+
body = [
|
|
2390
|
+
bold("Let's seed your decision graph"),
|
|
2391
|
+
"Your team has no decisions recorded yet. Tell me, in your own words:",
|
|
2392
|
+
"",
|
|
2393
|
+
...REVERSE_PROMPT_LINES.map((line) => ` ${line}`),
|
|
2394
|
+
"",
|
|
2395
|
+
"Share your answer and I'll record each goal as a decision."
|
|
2396
|
+
];
|
|
2397
|
+
} else {
|
|
2398
|
+
body = [
|
|
2399
|
+
bold("Get started"),
|
|
2400
|
+
cmd("prim decisions recent", "what your team has decided lately"),
|
|
2401
|
+
cmd("prim decisions check --files <files>", "what governs files you're about to change"),
|
|
2402
|
+
cmd("prim --help", "everything else")
|
|
2403
|
+
];
|
|
2404
|
+
}
|
|
2405
|
+
return [...head, ...body, "", dim("App: https://app.getprimitive.ai")].join("\n");
|
|
2406
|
+
}
|
|
2407
|
+
function welcomeJson(state) {
|
|
2408
|
+
if (state.org === "active") {
|
|
2409
|
+
return { welcomed: true, org: "active", recent: state.recent };
|
|
2410
|
+
}
|
|
2411
|
+
if (state.org === "empty") {
|
|
2412
|
+
return { welcomed: true, org: "empty", reversePrompt: REVERSE_PROMPT };
|
|
2413
|
+
}
|
|
2414
|
+
return { welcomed: true, org: "unknown" };
|
|
2415
|
+
}
|
|
2416
|
+
function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
2417
|
+
program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(async () => {
|
|
2418
|
+
const result = await fetchRecent({ limit: RECENT_LIMIT }, deps);
|
|
2419
|
+
const state = welcomeStateFromRecent(result);
|
|
2420
|
+
process.stderr.write(`${formatWelcome(state)}
|
|
2421
|
+
`);
|
|
2422
|
+
printJson(welcomeJson(state));
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2225
2426
|
// src/index.ts
|
|
2226
2427
|
var __dirname2 = dirname6(fileURLToPath4(import.meta.url));
|
|
2227
2428
|
var pkg = JSON.parse(readFileSync9(resolve4(__dirname2, "../package.json"), "utf-8"));
|
|
@@ -2242,6 +2443,7 @@ registerCodexCommands(program);
|
|
|
2242
2443
|
registerDaemonCommands(program);
|
|
2243
2444
|
registerReconcileCommands(program);
|
|
2244
2445
|
registerStatuslineCommands(program);
|
|
2446
|
+
registerWelcomeCommand(program);
|
|
2245
2447
|
process.on("unhandledRejection", (err) => {
|
|
2246
2448
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2247
2449
|
console.error(msg);
|
package/package.json
CHANGED