@primitive.ai/prim 0.1.0-alpha.21 → 0.1.0-alpha.23
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 +19 -7
- package/SKILL.md +20 -1
- package/dist/index.js +250 -47
- 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
|
|
@@ -78,16 +83,23 @@ prim daemon start # start (stop / restart / status)
|
|
|
78
83
|
Read and respond to the decision graph.
|
|
79
84
|
|
|
80
85
|
```bash
|
|
81
|
-
prim decisions recent
|
|
82
|
-
prim decisions show <id>
|
|
83
|
-
prim decisions cascade <id>
|
|
84
|
-
prim decisions check --files <…>
|
|
85
|
-
prim decisions confirm <id>
|
|
86
|
+
prim decisions recent # Recent decisions feed
|
|
87
|
+
prim decisions show <id> # Drill into one decision
|
|
88
|
+
prim decisions cascade <id> # Blast radius of a decision
|
|
89
|
+
prim decisions check --files <…> # Active decisions referencing files (warn-only)
|
|
90
|
+
prim decisions confirm <id> # Answer a rationale-confirmation prompt
|
|
91
|
+
prim decisions create --intent <…> # Author a decision directly (flags-only)
|
|
92
|
+
prim decisions link <child> --on <parent> # Relate: <child> depends on <parent>
|
|
93
|
+
prim decisions unlink <child> --on <parent> # Remove that dependency
|
|
86
94
|
```
|
|
87
95
|
|
|
88
96
|
`<id>` accepts a full decision ID or its short ID. STDOUT is machine-readable
|
|
89
97
|
JSON; human-readable status goes to STDERR.
|
|
90
98
|
|
|
99
|
+
`link` / `unlink` curate the dependency edges the automatic linker would otherwise
|
|
100
|
+
own — `<child>` depends on `<parent>`. Both are idempotent and refuse any link that
|
|
101
|
+
would create a cycle (exit 2); an unresolved id exits 4.
|
|
102
|
+
|
|
91
103
|
### Reconcile
|
|
92
104
|
|
|
93
105
|
```bash
|
package/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ description: Use the prim CLI for Primitive's decision graph — passive decisio
|
|
|
9
9
|
|
|
10
10
|
## Mental model
|
|
11
11
|
|
|
12
|
-
As your team codes, prim passively captures the **decisions** you make -- which library, which pattern, which config value -- into a queryable graph, and links them: a decision can depend on earlier decisions and reference the files it touched. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
|
|
12
|
+
As your team codes, prim passively captures the **decisions** you make -- 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. When a later change conflicts with a load-bearing prior decision, prim **gates** the edit and surfaces the decision for review.
|
|
13
13
|
|
|
14
14
|
You never invoke capture. It runs automatically through the session hooks installed by `npx --yes @primitive.ai/prim claude install` (Claude Code) or `npx --yes @primitive.ai/prim codex install` (Codex). Your job is to **respond** to the gate, **read** the graph before load-bearing edits, and **answer** the occasional rationale confirmation.
|
|
15
15
|
|
|
@@ -77,6 +77,25 @@ npx --yes @primitive.ai/prim decisions create --intent "Adopt prosemirror-collab
|
|
|
77
77
|
|
|
78
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
79
|
|
|
80
|
+
## Relate decisions (link / unlink)
|
|
81
|
+
|
|
82
|
+
prim links decisions automatically when their files overlap, but that heuristic misses real connections and occasionally invents wrong ones. When the user asks you to **relate two existing decisions** — "B depends on A", "these are connected", wiring up two orphans — or to **cut a wrong link**, do it by hand:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
npx --yes @primitive.ai/prim decisions link <child> --on <parent> # record that <child> depends on <parent>
|
|
86
|
+
npx --yes @primitive.ai/prim decisions unlink <child> --on <parent> # remove that dependency
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Direction is **`<child>` depends on `<parent>`** — the parent is the prerequisite. Read the echoed verdict to confirm you got the arrow right: `[prim] <child> now depends on <parent>.` After linking, `decisions show <child>` lists `<parent>` upstream and `decisions cascade <parent>` shows `<child>` in its downstream blast radius; after unlinking they drop. Both ids accept `dec_<short>` or a full id and may be any two decisions in your org, regardless of status.
|
|
90
|
+
|
|
91
|
+
Safe to run repeatedly:
|
|
92
|
+
|
|
93
|
+
- **Idempotent** — re-linking an existing edge (or unlinking a missing one) is a no-op that still exits 0 (`already_linked` / `not_linked`).
|
|
94
|
+
- **Acyclic** — a self-loop, or any link that would close a dependency cycle, is refused with exit 2 (with the offending chain when it's short enough to render); the graph stays a DAG.
|
|
95
|
+
- **Exit codes** (treat non-zero as actionable): `0` success or no-op; `2` a refused link (self-loop, cycle, or an ambiguous short id — retry with the full id); `4` an id that doesn't resolve. After a non-zero exit, branch on the exit code and the `[prim]` STDERR verdict, **not** on STDOUT keys: only the exit-0 outcomes carry the full `{ outcome, childId, childShortId, parentId, parentShortId }`; a refused link prints a smaller `{ outcome, … }`, and an unresolved id (exit 4) prints nothing to STDOUT.
|
|
96
|
+
|
|
97
|
+
Like authoring, relate only what the user asks for — don't invent relationships they didn't state.
|
|
98
|
+
|
|
80
99
|
## Presence
|
|
81
100
|
|
|
82
101
|
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.
|
package/dist/index.js
CHANGED
|
@@ -281,6 +281,7 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
// src/commands/claude-install.ts
|
|
284
|
+
import { execSync } from "child_process";
|
|
284
285
|
import {
|
|
285
286
|
closeSync,
|
|
286
287
|
existsSync as existsSync3,
|
|
@@ -370,7 +371,17 @@ var PRIM_BINS = [
|
|
|
370
371
|
];
|
|
371
372
|
var JSON_INDENT = 2;
|
|
372
373
|
var USER_SCOPE_PATH = join2(homedir(), ".claude", "settings.json");
|
|
373
|
-
|
|
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");
|
|
374
385
|
var CAPTURE_EVENTS = [
|
|
375
386
|
"SessionStart",
|
|
376
387
|
"UserPromptSubmit",
|
|
@@ -391,7 +402,7 @@ var REGISTRATIONS = [
|
|
|
391
402
|
makeRegistration("SessionEnd", "*", SESSION_END_BIN)
|
|
392
403
|
];
|
|
393
404
|
function settingsPathFor(scope) {
|
|
394
|
-
return scope === "user" ? USER_SCOPE_PATH :
|
|
405
|
+
return scope === "user" ? USER_SCOPE_PATH : projectScopePath();
|
|
395
406
|
}
|
|
396
407
|
function readSettings(path) {
|
|
397
408
|
if (!existsSync3(path)) {
|
|
@@ -548,15 +559,15 @@ function performStatus() {
|
|
|
548
559
|
statusline: statuslineInstalled(settings)
|
|
549
560
|
};
|
|
550
561
|
};
|
|
551
|
-
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(
|
|
562
|
+
return { user: statusFor(USER_SCOPE_PATH), project: statusFor(projectScopePath()) };
|
|
552
563
|
}
|
|
553
564
|
function resolveScope(input) {
|
|
554
|
-
if (input === void 0 || input === "
|
|
555
|
-
return "user";
|
|
556
|
-
}
|
|
557
|
-
if (input === "project") {
|
|
565
|
+
if (input === void 0 || input === "project") {
|
|
558
566
|
return "project";
|
|
559
567
|
}
|
|
568
|
+
if (input === "user") {
|
|
569
|
+
return "user";
|
|
570
|
+
}
|
|
560
571
|
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
561
572
|
process.exit(1);
|
|
562
573
|
}
|
|
@@ -564,7 +575,7 @@ function registerClaudeCommands(program2) {
|
|
|
564
575
|
const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
|
|
565
576
|
claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
|
|
566
577
|
"--scope <scope>",
|
|
567
|
-
"
|
|
578
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
568
579
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
569
580
|
const scope = resolveScope(opts.scope);
|
|
570
581
|
const result = performInstall(scope, opts.force ?? false);
|
|
@@ -581,7 +592,7 @@ function registerClaudeCommands(program2) {
|
|
|
581
592
|
});
|
|
582
593
|
claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
|
|
583
594
|
"--scope <scope>",
|
|
584
|
-
"
|
|
595
|
+
"project (default, the repo's .claude/settings.json) or user (~/.claude/settings.json)"
|
|
585
596
|
).action((opts) => {
|
|
586
597
|
const scope = resolveScope(opts.scope);
|
|
587
598
|
const result = performUninstall(scope);
|
|
@@ -629,9 +640,9 @@ var CODEX_REGISTRATIONS = [
|
|
|
629
640
|
makeRegistration("SessionStart", "*", SESSION_START_BIN2, CODEX_ARGS)
|
|
630
641
|
];
|
|
631
642
|
var USER_SCOPE_PATH2 = join3(homedir2(), ".codex", "hooks.json");
|
|
632
|
-
var
|
|
643
|
+
var projectScopePath2 = () => join3(projectRoot(), ".codex", "hooks.json");
|
|
633
644
|
function settingsPathFor2(scope) {
|
|
634
|
-
return scope === "user" ? USER_SCOPE_PATH2 :
|
|
645
|
+
return scope === "user" ? USER_SCOPE_PATH2 : projectScopePath2();
|
|
635
646
|
}
|
|
636
647
|
function applyInstall2(settings, options = {}) {
|
|
637
648
|
const hooks = { ...settings.hooks ?? {} };
|
|
@@ -696,24 +707,24 @@ function performStatus2() {
|
|
|
696
707
|
const settings = readSettings(path);
|
|
697
708
|
return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
|
|
698
709
|
};
|
|
699
|
-
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(
|
|
710
|
+
return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(projectScopePath2()) };
|
|
700
711
|
}
|
|
701
712
|
function resolveScope2(input) {
|
|
702
|
-
if (input === void 0 || input === "
|
|
703
|
-
return "user";
|
|
704
|
-
}
|
|
705
|
-
if (input === "project") {
|
|
713
|
+
if (input === void 0 || input === "project") {
|
|
706
714
|
return "project";
|
|
707
715
|
}
|
|
716
|
+
if (input === "user") {
|
|
717
|
+
return "user";
|
|
718
|
+
}
|
|
708
719
|
console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
|
|
709
720
|
process.exit(1);
|
|
710
721
|
}
|
|
711
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.";
|
|
712
723
|
function registerCodexCommands(program2) {
|
|
713
724
|
const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
|
|
714
|
-
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(
|
|
715
726
|
"--scope <scope>",
|
|
716
|
-
"
|
|
727
|
+
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
717
728
|
).option("--force", "Replace any drifted prim hook entries").action((opts) => {
|
|
718
729
|
const scope = resolveScope2(opts.scope);
|
|
719
730
|
const result = performInstall2(scope, opts.force ?? false);
|
|
@@ -725,9 +736,9 @@ function registerCodexCommands(program2) {
|
|
|
725
736
|
console.error(TRUST_NOTICE);
|
|
726
737
|
console.log(JSON.stringify(result, null, JSON_INDENT2));
|
|
727
738
|
});
|
|
728
|
-
codex.command("uninstall").description("Remove all prim hooks from
|
|
739
|
+
codex.command("uninstall").description("Remove all prim hooks from Codex's hooks.json").option(
|
|
729
740
|
"--scope <scope>",
|
|
730
|
-
"
|
|
741
|
+
"project (default, the repo's .codex/hooks.json) or user (~/.codex/hooks.json)"
|
|
731
742
|
).action((opts) => {
|
|
732
743
|
const scope = resolveScope2(opts.scope);
|
|
733
744
|
const result = performUninstall2(scope);
|
|
@@ -1208,8 +1219,8 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1208
1219
|
if (args.since !== void 0) {
|
|
1209
1220
|
params.set("since", args.since);
|
|
1210
1221
|
}
|
|
1211
|
-
const client = deps.getClient();
|
|
1212
1222
|
try {
|
|
1223
|
+
const client = deps.getClient();
|
|
1213
1224
|
const res = await daemonOrDirectGet(
|
|
1214
1225
|
"decisions_recent",
|
|
1215
1226
|
`/api/cli/decisions/recent?${params.toString()}`,
|
|
@@ -1217,6 +1228,9 @@ async function fetchRecent(args, deps = defaultDeps2) {
|
|
|
1217
1228
|
RECENT_TIMEOUT_MS
|
|
1218
1229
|
);
|
|
1219
1230
|
const result = { decisions: res.decisions };
|
|
1231
|
+
if (res.viewerHasDecisions !== void 0) {
|
|
1232
|
+
result.viewerHasDecisions = res.viewerHasDecisions;
|
|
1233
|
+
}
|
|
1220
1234
|
if (res.unavailable !== void 0) {
|
|
1221
1235
|
result.unavailable = res.unavailable;
|
|
1222
1236
|
}
|
|
@@ -1255,9 +1269,18 @@ function authorLabel(row) {
|
|
|
1255
1269
|
}
|
|
1256
1270
|
}
|
|
1257
1271
|
var AUTHOR_WIDTH = 18;
|
|
1272
|
+
var AREA_WIDTH = 12;
|
|
1258
1273
|
function padRight(s, width) {
|
|
1259
1274
|
return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
|
|
1260
1275
|
}
|
|
1276
|
+
function formatRecentRow(row) {
|
|
1277
|
+
const clock = formatClock(row.classifiedAt);
|
|
1278
|
+
const author = padRight(authorLabel(row), AUTHOR_WIDTH);
|
|
1279
|
+
const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
|
|
1280
|
+
const areaPlain = padRight(areaText, AREA_WIDTH);
|
|
1281
|
+
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1282
|
+
return ` ${clock} ${author}${areaCol}${row.intent}`;
|
|
1283
|
+
}
|
|
1261
1284
|
function formatRecentHuman(result) {
|
|
1262
1285
|
if (result.unavailable !== void 0) {
|
|
1263
1286
|
return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
|
|
@@ -1267,12 +1290,7 @@ function formatRecentHuman(result) {
|
|
|
1267
1290
|
}
|
|
1268
1291
|
const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
|
|
1269
1292
|
for (const row of result.decisions) {
|
|
1270
|
-
|
|
1271
|
-
const author = padRight(authorLabel(row), AUTHOR_WIDTH);
|
|
1272
|
-
const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
|
|
1273
|
-
const areaPlain = padRight(areaText, 12);
|
|
1274
|
-
const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
|
|
1275
|
-
lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
|
|
1293
|
+
lines.push(formatRecentRow(row));
|
|
1276
1294
|
}
|
|
1277
1295
|
return lines.join("\n");
|
|
1278
1296
|
}
|
|
@@ -1399,8 +1417,85 @@ function formatCreateJson(outcome) {
|
|
|
1399
1417
|
return JSON.stringify(outcome, null, 2);
|
|
1400
1418
|
}
|
|
1401
1419
|
|
|
1402
|
-
// src/decisions/
|
|
1420
|
+
// src/decisions/link.ts
|
|
1421
|
+
var RELATE_TIMEOUT_MS = 1e4;
|
|
1422
|
+
var defaultDeps5 = { getClient };
|
|
1403
1423
|
var NOT_FOUND_RE3 = /not found/i;
|
|
1424
|
+
var AMBIGUOUS_RE2 = /ambiguous/i;
|
|
1425
|
+
var CYCLE_RE = /cycle/i;
|
|
1426
|
+
var LinkNotFoundError = class extends Error {
|
|
1427
|
+
constructor(which) {
|
|
1428
|
+
super(`Decision not found: ${which}`);
|
|
1429
|
+
this.name = "LinkNotFoundError";
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
function isRelateRejection(outcome) {
|
|
1433
|
+
return outcome.outcome === "self_loop" || outcome.outcome === "would_cycle" || outcome.outcome === "ambiguous";
|
|
1434
|
+
}
|
|
1435
|
+
function foldRelateError(err) {
|
|
1436
|
+
if (err instanceof Error) {
|
|
1437
|
+
if (NOT_FOUND_RE3.test(err.message)) {
|
|
1438
|
+
throw new LinkNotFoundError(err.message.includes("parent") ? "parent" : "child");
|
|
1439
|
+
}
|
|
1440
|
+
if (AMBIGUOUS_RE2.test(err.message)) {
|
|
1441
|
+
return { outcome: "ambiguous", which: err.message.includes("(parent)") ? "parent" : "child" };
|
|
1442
|
+
}
|
|
1443
|
+
if (CYCLE_RE.test(err.message)) {
|
|
1444
|
+
return { outcome: "would_cycle", detail: err.message };
|
|
1445
|
+
}
|
|
1446
|
+
if (/itself/i.test(err.message)) {
|
|
1447
|
+
return { outcome: "self_loop" };
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
throw err;
|
|
1451
|
+
}
|
|
1452
|
+
async function relate(path, request, deps) {
|
|
1453
|
+
const client = deps.getClient();
|
|
1454
|
+
try {
|
|
1455
|
+
const outcome = await client.post(path, request, {
|
|
1456
|
+
signal: AbortSignal.timeout(RELATE_TIMEOUT_MS)
|
|
1457
|
+
});
|
|
1458
|
+
return { request, outcome };
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
return { request, outcome: foldRelateError(err) };
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
function fetchLink(child, parent, deps = defaultDeps5) {
|
|
1464
|
+
return relate("/api/cli/decisions/link", { child, parent }, deps);
|
|
1465
|
+
}
|
|
1466
|
+
function fetchUnlink(child, parent, deps = defaultDeps5) {
|
|
1467
|
+
return relate("/api/cli/decisions/unlink", { child, parent }, deps);
|
|
1468
|
+
}
|
|
1469
|
+
function endpointRef(outcome, side) {
|
|
1470
|
+
return side === "child" ? renderIdentifier({ shortId: outcome.childShortId, id: outcome.childId }) : renderIdentifier({ shortId: outcome.parentShortId, id: outcome.parentId });
|
|
1471
|
+
}
|
|
1472
|
+
function formatRelateHuman(result) {
|
|
1473
|
+
const { request, outcome } = result;
|
|
1474
|
+
switch (outcome.outcome) {
|
|
1475
|
+
case "linked":
|
|
1476
|
+
return `[prim] ${endpointRef(outcome, "child")} now depends on ${endpointRef(outcome, "parent")}.`;
|
|
1477
|
+
case "already_linked":
|
|
1478
|
+
return `[prim] ${endpointRef(outcome, "child")} already depends on ${endpointRef(outcome, "parent")}; nothing to change.`;
|
|
1479
|
+
case "unlinked":
|
|
1480
|
+
return `[prim] ${endpointRef(outcome, "child")} no longer depends on ${endpointRef(outcome, "parent")}.`;
|
|
1481
|
+
case "not_linked":
|
|
1482
|
+
return `[prim] ${endpointRef(outcome, "child")} did not depend on ${endpointRef(outcome, "parent")}; nothing to change.`;
|
|
1483
|
+
case "self_loop":
|
|
1484
|
+
return "[prim] a decision cannot depend on itself.";
|
|
1485
|
+
case "would_cycle":
|
|
1486
|
+
return `[prim] refusing to link \u2014 ${outcome.detail}.`;
|
|
1487
|
+
default: {
|
|
1488
|
+
const typed = outcome.which === "parent" ? request.parent : request.child;
|
|
1489
|
+
return `[prim] the ${outcome.which} id "${typed}" is ambiguous in this organization \u2014 retry with the full decision id.`;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function formatRelateJson(result) {
|
|
1494
|
+
return JSON.stringify(result.outcome, null, 2);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/decisions/show.ts
|
|
1498
|
+
var NOT_FOUND_RE4 = /not found/i;
|
|
1404
1499
|
function colorStatus(status) {
|
|
1405
1500
|
if (status === "under_review") {
|
|
1406
1501
|
return color(status, "orange");
|
|
@@ -1411,14 +1506,14 @@ function colorStatus(status) {
|
|
|
1411
1506
|
return color(status, "gray");
|
|
1412
1507
|
}
|
|
1413
1508
|
var SHOW_TIMEOUT_MS = 1e4;
|
|
1414
|
-
var
|
|
1509
|
+
var defaultDeps6 = { getClient };
|
|
1415
1510
|
var DecisionNotFoundError = class extends Error {
|
|
1416
1511
|
constructor(idOrShortId) {
|
|
1417
1512
|
super(`Decision not found: ${idOrShortId}`);
|
|
1418
1513
|
this.name = "DecisionNotFoundError";
|
|
1419
1514
|
}
|
|
1420
1515
|
};
|
|
1421
|
-
async function fetchShow(idOrShortId, deps =
|
|
1516
|
+
async function fetchShow(idOrShortId, deps = defaultDeps6) {
|
|
1422
1517
|
const params = new URLSearchParams({ id: idOrShortId });
|
|
1423
1518
|
const client = deps.getClient();
|
|
1424
1519
|
try {
|
|
@@ -1429,7 +1524,7 @@ async function fetchShow(idOrShortId, deps = defaultDeps5) {
|
|
|
1429
1524
|
SHOW_TIMEOUT_MS
|
|
1430
1525
|
);
|
|
1431
1526
|
} catch (err) {
|
|
1432
|
-
if (err instanceof Error &&
|
|
1527
|
+
if (err instanceof Error && NOT_FOUND_RE4.test(err.message)) {
|
|
1433
1528
|
throw new DecisionNotFoundError(idOrShortId);
|
|
1434
1529
|
}
|
|
1435
1530
|
throw err;
|
|
@@ -1639,10 +1734,44 @@ function registerDecisionsCommands(program2) {
|
|
|
1639
1734
|
throw err;
|
|
1640
1735
|
}
|
|
1641
1736
|
});
|
|
1737
|
+
decisions.command("link <child>").description("Record that <child> depends on <parent> (adds a dependency edge)").requiredOption("--on <parent>", "The decision <child> depends on").action(async (child, opts) => {
|
|
1738
|
+
try {
|
|
1739
|
+
const result = await fetchLink(child, opts.on);
|
|
1740
|
+
console.error(formatRelateHuman(result));
|
|
1741
|
+
console.log(formatRelateJson(result));
|
|
1742
|
+
if (isRelateRejection(result.outcome)) {
|
|
1743
|
+
process.exitCode = EXIT_USAGE;
|
|
1744
|
+
}
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
if (err instanceof LinkNotFoundError) {
|
|
1747
|
+
console.error(`[prim] ${err.message}`);
|
|
1748
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
throw err;
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
decisions.command("unlink <child>").description("Remove <child>'s recorded dependency on <parent>").requiredOption("--on <parent>", "The decision <child> no longer depends on").action(async (child, opts) => {
|
|
1755
|
+
try {
|
|
1756
|
+
const result = await fetchUnlink(child, opts.on);
|
|
1757
|
+
console.error(formatRelateHuman(result));
|
|
1758
|
+
console.log(formatRelateJson(result));
|
|
1759
|
+
if (isRelateRejection(result.outcome)) {
|
|
1760
|
+
process.exitCode = EXIT_USAGE;
|
|
1761
|
+
}
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
if (err instanceof LinkNotFoundError) {
|
|
1764
|
+
console.error(`[prim] ${err.message}`);
|
|
1765
|
+
process.exitCode = EXIT_NOT_FOUND;
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
throw err;
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1642
1771
|
}
|
|
1643
1772
|
|
|
1644
1773
|
// src/commands/hooks.ts
|
|
1645
|
-
import { execSync } from "child_process";
|
|
1774
|
+
import { execSync as execSync2 } from "child_process";
|
|
1646
1775
|
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1647
1776
|
import { resolve } from "path";
|
|
1648
1777
|
import { Option } from "commander";
|
|
@@ -1680,7 +1809,7 @@ ${hookShim(spec.binName)}
|
|
|
1680
1809
|
${end}`;
|
|
1681
1810
|
}
|
|
1682
1811
|
function getGitRoot() {
|
|
1683
|
-
return
|
|
1812
|
+
return execSync2("git rev-parse --show-toplevel", {
|
|
1684
1813
|
encoding: "utf-8"
|
|
1685
1814
|
}).trim();
|
|
1686
1815
|
}
|
|
@@ -2325,10 +2454,44 @@ function registerStatuslineCommands(program2) {
|
|
|
2325
2454
|
|
|
2326
2455
|
// src/commands/welcome.ts
|
|
2327
2456
|
var CMD_GUTTER = 38;
|
|
2328
|
-
|
|
2457
|
+
var RECENT_LIMIT = 5;
|
|
2458
|
+
var REVERSE_PROMPT_LINES = [
|
|
2459
|
+
"What are the most important goals in your organization that you're",
|
|
2460
|
+
"responsible for, right now? What are you not focusing on, in order",
|
|
2461
|
+
"to focus on those goals?"
|
|
2462
|
+
];
|
|
2463
|
+
var REVERSE_PROMPT = REVERSE_PROMPT_LINES.join(" ");
|
|
2464
|
+
var CALLOUT_TITLE = "Your turn";
|
|
2465
|
+
var CALLOUT_INDENT = " ";
|
|
2466
|
+
function ruledQuestion(lines) {
|
|
2467
|
+
const prefix = `\u250C\u2500 ${CALLOUT_TITLE} `;
|
|
2468
|
+
const width = Math.max(
|
|
2469
|
+
`${prefix}\u2510`.length,
|
|
2470
|
+
...lines.map((line) => CALLOUT_INDENT.length + line.length + 1)
|
|
2471
|
+
);
|
|
2472
|
+
const top = `${prefix}${"\u2500".repeat(width - prefix.length - 1)}\u2510`;
|
|
2473
|
+
const bottom = `\u2514${"\u2500".repeat(top.length - 2)}\u2518`;
|
|
2474
|
+
return [
|
|
2475
|
+
color(top, "green"),
|
|
2476
|
+
...lines.map((line) => `${CALLOUT_INDENT}${line}`),
|
|
2477
|
+
color(bottom, "green")
|
|
2478
|
+
];
|
|
2479
|
+
}
|
|
2480
|
+
function welcomeStateFromRecent(result) {
|
|
2481
|
+
if (result.unavailable !== void 0) {
|
|
2482
|
+
return { org: "unknown" };
|
|
2483
|
+
}
|
|
2484
|
+
const recent = result.decisions.slice(0, RECENT_LIMIT);
|
|
2485
|
+
const viewerHasDecisions = result.viewerHasDecisions ?? result.decisions.length > 0;
|
|
2486
|
+
if (!viewerHasDecisions) {
|
|
2487
|
+
return { org: "seed", recent };
|
|
2488
|
+
}
|
|
2489
|
+
return { org: "active", recent };
|
|
2490
|
+
}
|
|
2491
|
+
function formatWelcome(state) {
|
|
2329
2492
|
const cmd = (command, desc) => ` ${dim(command.padEnd(CMD_GUTTER))}${desc}`;
|
|
2330
2493
|
const bullet = (text) => ` ${color("\u2022", "green")} ${text}`;
|
|
2331
|
-
|
|
2494
|
+
const head = [
|
|
2332
2495
|
bold(color("Welcome to Primitive", "green")),
|
|
2333
2496
|
"",
|
|
2334
2497
|
"Primitive captures the decisions your team makes while coding into a",
|
|
@@ -2342,20 +2505,60 @@ function formatWelcome() {
|
|
|
2342
2505
|
" that decision and retry.",
|
|
2343
2506
|
bullet('Occasional yes/no prompts confirm the "why" behind a decision \u2014'),
|
|
2344
2507
|
" answering keeps the graph trustworthy.",
|
|
2345
|
-
""
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2508
|
+
""
|
|
2509
|
+
];
|
|
2510
|
+
let body;
|
|
2511
|
+
if (state.org === "active") {
|
|
2512
|
+
body = [
|
|
2513
|
+
bold("Recent team decisions"),
|
|
2514
|
+
...state.recent.map(formatRecentRow),
|
|
2515
|
+
"",
|
|
2516
|
+
bold("Get started"),
|
|
2517
|
+
cmd("prim decisions check --files <files>", "what governs files you're about to change"),
|
|
2518
|
+
cmd("prim --help", "everything else")
|
|
2519
|
+
];
|
|
2520
|
+
} else if (state.org === "seed") {
|
|
2521
|
+
const teamContext = state.recent.length > 0 ? [bold("Recent team decisions"), ...state.recent.map(formatRecentRow), ""] : [];
|
|
2522
|
+
body = [
|
|
2523
|
+
...teamContext,
|
|
2524
|
+
bold("Let's seed your decision graph"),
|
|
2525
|
+
"You haven't recorded a decision yet \u2014 answer this and I'll record",
|
|
2526
|
+
"each goal as a decision:",
|
|
2527
|
+
"",
|
|
2528
|
+
...ruledQuestion(REVERSE_PROMPT_LINES)
|
|
2529
|
+
];
|
|
2530
|
+
} else {
|
|
2531
|
+
body = [
|
|
2532
|
+
bold("Get started"),
|
|
2533
|
+
cmd("prim decisions recent", "what your team has decided lately"),
|
|
2534
|
+
cmd("prim decisions check --files <files>", "what governs files you're about to change"),
|
|
2535
|
+
cmd("prim --help", "everything else")
|
|
2536
|
+
];
|
|
2537
|
+
}
|
|
2538
|
+
const footer = state.org === "seed" ? [] : ["", dim("App: https://app.getprimitive.ai")];
|
|
2539
|
+
return [...head, ...body, ...footer].join("\n");
|
|
2540
|
+
}
|
|
2541
|
+
function welcomeJson(state) {
|
|
2542
|
+
if (state.org === "active") {
|
|
2543
|
+
return { welcomed: true, org: "active", recent: state.recent };
|
|
2544
|
+
}
|
|
2545
|
+
if (state.org === "seed") {
|
|
2546
|
+
return {
|
|
2547
|
+
welcomed: true,
|
|
2548
|
+
org: "seed",
|
|
2549
|
+
reversePrompt: REVERSE_PROMPT,
|
|
2550
|
+
recent: state.recent
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
return { welcomed: true, org: "unknown" };
|
|
2353
2554
|
}
|
|
2354
|
-
function registerWelcomeCommand(program2) {
|
|
2355
|
-
program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(() => {
|
|
2356
|
-
|
|
2555
|
+
function registerWelcomeCommand(program2, deps = { getClient }) {
|
|
2556
|
+
program2.command("welcome").description("Print a brief orientation to Primitive's decision graph").action(async () => {
|
|
2557
|
+
const result = await fetchRecent({ limit: RECENT_LIMIT }, deps);
|
|
2558
|
+
const state = welcomeStateFromRecent(result);
|
|
2559
|
+
process.stderr.write(`${formatWelcome(state)}
|
|
2357
2560
|
`);
|
|
2358
|
-
printJson(
|
|
2561
|
+
printJson(welcomeJson(state));
|
|
2359
2562
|
});
|
|
2360
2563
|
}
|
|
2361
2564
|
|
package/package.json
CHANGED