djournal 0.3.0 → 0.4.0

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.
@@ -42,7 +42,8 @@ native payloads to these names.
42
42
 
43
43
  - Hooks are read-only.
44
44
  - `stop_hook_active: true` always permits stopping.
45
- - `closed` markers must resolve inside `.journal/work/` to an existing Markdown
46
- spine entry.
45
+ - `closed` markers must resolve to an existing Markdown spine entry under the
46
+ resolved journal root; `.journal/...` paths use `.djournal.json` when a
47
+ global project store is configured.
47
48
  - Adapters do not infer task meaning from file changes or unstable transcripts.
48
49
  - Adding a harness must not change `AUTOMATION.md` semantics.
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+
3
+ const { handle } = require("../shared/journal-hook.js");
4
+
5
+ function contextFrom(output) {
6
+ return output?.hookSpecificOutput?.additionalContext || "";
7
+ }
8
+
9
+ function assistantText(message) {
10
+ if (message?.role !== "assistant" || !Array.isArray(message.content)) return null;
11
+ const parts = message.content
12
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
13
+ .map((part) => part.text);
14
+ return parts.length ? parts.join("\n") : null;
15
+ }
16
+
17
+ function registerPiAdapter(pi, options = {}) {
18
+ const checker = options.handle || handle;
19
+ const state = {
20
+ sessionContext: "",
21
+ closureRetryActive: false,
22
+ };
23
+
24
+ function warn(ctx, error) {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ ctx?.ui?.notify?.(`djournal hook skipped: ${message}`, "warning");
27
+ }
28
+
29
+ function check(payload, ctx) {
30
+ try {
31
+ return checker(payload) || {};
32
+ } catch (error) {
33
+ warn(ctx, error);
34
+ return {};
35
+ }
36
+ }
37
+
38
+ pi.on("session_start", (_event, ctx) => {
39
+ state.sessionContext = contextFrom(check({
40
+ cwd: ctx.cwd,
41
+ hook_event_name: "SessionStart",
42
+ }, ctx));
43
+ state.closureRetryActive = false;
44
+ });
45
+
46
+ pi.on("before_agent_start", (event, ctx) => {
47
+ const promptContext = contextFrom(check({
48
+ cwd: ctx.cwd,
49
+ hook_event_name: "UserPromptSubmit",
50
+ prompt: event.prompt,
51
+ }, ctx));
52
+ const additionalContext = [state.sessionContext, promptContext].filter(Boolean).join("\n\n");
53
+ if (!additionalContext) return undefined;
54
+ return { systemPrompt: `${event.systemPrompt}\n\n${additionalContext}` };
55
+ });
56
+
57
+ pi.on("turn_end", (event, ctx) => {
58
+ if (event.message?.role !== "assistant" || event.message.stopReason !== "stop") return;
59
+ const lastAssistantMessage = assistantText(event.message);
60
+ if (lastAssistantMessage === null) return;
61
+ const retryWasActive = state.closureRetryActive;
62
+ const output = check({
63
+ cwd: ctx.cwd,
64
+ hook_event_name: "Stop",
65
+ last_assistant_message: lastAssistantMessage,
66
+ stop_hook_active: retryWasActive,
67
+ }, ctx);
68
+
69
+ if (retryWasActive) {
70
+ state.closureRetryActive = false;
71
+ return;
72
+ }
73
+ if (output.decision !== "block" || typeof output.reason !== "string" || !output.reason) return;
74
+
75
+ state.closureRetryActive = true;
76
+ try {
77
+ pi.sendUserMessage(output.reason, { deliverAs: "followUp" });
78
+ } catch (error) {
79
+ state.closureRetryActive = false;
80
+ warn(ctx, error);
81
+ }
82
+ });
83
+
84
+ return state;
85
+ }
86
+
87
+ module.exports = registerPiAdapter;
88
+ module.exports.assistantText = assistantText;
89
+ module.exports.registerPiAdapter = registerPiAdapter;
@@ -169,7 +169,7 @@ function handle(payload, options = {}) {
169
169
  if (match[1].toLowerCase() === "closed" && !validateClosedPath(root, match[2])) {
170
170
  return {
171
171
  decision: "block",
172
- reason: "The journal-status closed marker must reference an existing repository-relative Markdown spine entry under .journal/work/<work>/journal/.",
172
+ reason: "The journal-status closed marker must reference an existing Markdown spine entry under the resolved journal root. In global-store projects, .journal/... paths resolve through .djournal.json.",
173
173
  };
174
174
  }
175
175
  if (match[1].toLowerCase() === "closed") {
@@ -0,0 +1,6 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import registerPiAdapter from "../../.agents/adapters/pi/journal-hook.js";
3
+
4
+ export default function djournalExtension(pi: ExtensionAPI) {
5
+ registerPiAdapter(pi);
6
+ }
package/README.md CHANGED
@@ -112,7 +112,7 @@ flowchart LR
112
112
  - `AGENTS.md` supplies portable workflow instructions.
113
113
  - Skills handle planning, research, decisions, documentation, recall, audit,
114
114
  reconciliation, and session closure.
115
- - Codex and Claude Code hooks provide reminders and closure validation.
115
+ - Codex, Claude Code, and Pi integrations provide reminders and closure validation.
116
116
  - Hooks never create or modify semantic journal entries.
117
117
  - Read-only and trivial requests do not generate unnecessary ceremony.
118
118
 
@@ -166,12 +166,13 @@ djournal install
166
166
  ```
167
167
 
168
168
  The global install gives you the `djournal` and `journal` commands. The
169
- installer targets the current directory and detects Codex or Claude Code. Select
169
+ installer targets the current directory and detects Codex, Claude Code, or Pi. Select
170
170
  explicitly when needed:
171
171
 
172
172
  ```bash
173
173
  djournal install --harness codex
174
174
  djournal install --harness claude-code
175
+ djournal install --harness pi
175
176
  djournal install --all
176
177
  ```
177
178
 
@@ -202,6 +203,12 @@ commands used by the workflow. Codex uses runtime sandbox configuration for
202
203
  filesystem access, so sandboxed Codex sessions must include the generated store
203
204
  path when they need canonical journal memory.
204
205
 
206
+ Pi loads the managed `.pi/extensions/djournal.ts` integration only after the
207
+ project is trusted. Approve the project interactively with `/trust` and restart
208
+ Pi, or use `pi --approve` for a non-interactive run. Pi has no built-in
209
+ filesystem sandbox; when an external sandbox or container is used, expose the
210
+ global store referenced by `.djournal.json`.
211
+
205
212
  ## Lifecycle
206
213
 
207
214
  ```bash
@@ -230,7 +237,7 @@ dedicated journal repository.
230
237
 
231
238
  ## Status
232
239
 
233
- Codex and Claude Code are supported. OpenCode, Pi, and Zed adapters are planned.
240
+ Codex, Claude Code, and Pi are supported. OpenCode and Zed adapters are planned.
234
241
 
235
242
  Git-backed sharing and automatic sync are opt-in so local-only work remains
236
243
  private by default.
package/bin/journal.js CHANGED
@@ -33,7 +33,7 @@ Options:
33
33
  --yes Disable interactive harness selection
34
34
  --json Emit JSON
35
35
  --work SLUG Select a journal work item instead of active state
36
- --harness LIST Comma-separated codex,claude-code selection
36
+ --harness LIST Comma-separated codex,claude-code,pi selection
37
37
  --all Select all work items for share, or every harness
38
38
  --instructions-only Install core instructions without harness hooks
39
39
  `;
@@ -168,6 +168,12 @@ For Claude Code, install also injects a narrow permission grant into
168
168
  runtime sandbox, so Codex sessions must be launched with the global store path
169
169
  available when sandboxing would otherwise hide `~/.djournal`.
170
170
 
171
+ Pi uses a trusted project extension at `.pi/extensions/djournal.ts`. A thin Pi
172
+ adapter maps `session_start`, `before_agent_start`, and final `turn_end` events
173
+ into the shared checker. Because Pi has no blocking stop hook,
174
+ an invalid final marker causes at most one follow-up turn. Pi project trust is
175
+ managed by Pi, and external sandboxes must expose the global store.
176
+
171
177
  ## Related docs
172
178
 
173
179
  - [Djournal in practice](djournal-in-practice.md)
@@ -42,6 +42,7 @@ This installs:
42
42
 
43
43
  - shared journal rules and skills under `.agents/`
44
44
  - harness-specific hook config when selected
45
+ - a managed `.pi/extensions/djournal.ts` extension when Pi is selected
45
46
  - managed instruction blocks in `AGENTS.md` and/or `CLAUDE.md`
46
47
  - `.djournal.json`, which points to the global project store
47
48
  - for Claude Code installs, a narrow permission grant that lets the agent read
@@ -64,6 +65,12 @@ rather than `.codex/hooks.json`. The installed Codex hook resolves the global
64
65
  store through `.djournal.json`; if the Codex session is sandboxed, launch it
65
66
  with the generated journal store path available as a readable/writable root.
66
67
 
68
+ Pi loads project `.agents/skills` and `.pi/extensions/` only after project
69
+ trust. In interactive Pi, use `/trust` and restart the session. For print, JSON,
70
+ or RPC runs without a stored trust decision, pass `--approve`. djournal does not
71
+ edit Pi's trust file. Pi itself is not a filesystem sandbox; external containers
72
+ or sandboxes must expose the global store referenced by `.djournal.json`.
73
+
67
74
  To share selected work through the product repository, enable colocated
68
75
  projection:
69
76
 
@@ -116,12 +123,13 @@ Install for one harness:
116
123
  ```bash
117
124
  djournal install --harness codex
118
125
  djournal install --harness claude-code
126
+ djournal install --harness pi
119
127
  ```
120
128
 
121
129
  Install for multiple harnesses:
122
130
 
123
131
  ```bash
124
- djournal install --harness codex,claude-code
132
+ djournal install --harness codex,claude-code,pi
125
133
  ```
126
134
 
127
135
  Install every supported harness:
@@ -144,7 +152,9 @@ djournal doctor
144
152
  ```
145
153
 
146
154
  `status` reports installed files and cleanliness. `doctor` checks the local
147
- environment and harness configuration.
155
+ environment and harness configuration. For Pi it reports extension presence
156
+ and reminds you that project trust is required; it does not inspect or change
157
+ Pi's private trust state.
148
158
 
149
159
  ## Related docs
150
160
 
@@ -24,12 +24,13 @@ so.
24
24
  ```bash
25
25
  djournal uninstall --harness codex
26
26
  djournal uninstall --harness claude-code
27
+ djournal uninstall --harness pi
27
28
  ```
28
29
 
29
30
  ## Uninstall multiple harnesses
30
31
 
31
32
  ```bash
32
- djournal uninstall --harness codex,claude-code
33
+ djournal uninstall --harness codex,claude-code,pi
33
34
  ```
34
35
 
35
36
  Partial uninstall removes only the selected harness integration and keeps the
@@ -58,6 +59,7 @@ or select a harness:
58
59
 
59
60
  ```bash
60
61
  djournal install --harness codex
62
+ djournal install --harness pi
61
63
  ```
62
64
 
63
65
  Reinstalling restores local harness integration around the existing journal
@@ -16,8 +16,12 @@ const {
16
16
  } = require("./merge.js");
17
17
 
18
18
  const SCHEMA_VERSION = 1;
19
- const SUPPORTED_HARNESSES = ["codex", "claude-code"];
20
- const FUTURE_HARNESSES = ["opencode", "pi", "zed"];
19
+ const SUPPORTED_HARNESSES = ["codex", "claude-code", "pi"];
20
+ const FUTURE_HARNESSES = ["opencode", "zed"];
21
+ const HARNESS_CONFIG_PATHS = {
22
+ codex: ".codex/hooks.json",
23
+ "claude-code": ".claude/settings.json",
24
+ };
21
25
  const MANIFEST_PATH = ".journal/.install/manifest.json";
22
26
  const TRANSACTION_PATH = ".journal/.install/transaction.json";
23
27
  const PROJECT_MARKER_PATH = ".djournal.json";
@@ -452,7 +456,7 @@ function jsonRecord(target, relative, fragment, oldRecord, harness) {
452
456
  };
453
457
  }
454
458
 
455
- function copyRecord(sourceRoot, target, relative, oldRecord) {
459
+ function copyRecord(sourceRoot, target, relative, oldRecord, harness) {
456
460
  const source = fs.readFileSync(path.join(sourceRoot, relative));
457
461
  const absolute = resolveWithin(target, relative);
458
462
  const current = readOptional(absolute);
@@ -468,6 +472,7 @@ function copyRecord(sourceRoot, target, relative, oldRecord) {
468
472
  record: {
469
473
  path: relative,
470
474
  mode: "copy",
475
+ ...(harness ? { harness } : {}),
471
476
  created: oldRecord?.created ?? current === null,
472
477
  installedHash: sha256(source),
473
478
  },
@@ -475,7 +480,8 @@ function copyRecord(sourceRoot, target, relative, oldRecord) {
475
480
  }
476
481
 
477
482
  function harnessFragment(sourceRoot, harness) {
478
- const relative = harness === "codex" ? ".codex/hooks.json" : ".claude/settings.json";
483
+ const relative = HARNESS_CONFIG_PATHS[harness];
484
+ if (!relative) throw new InstallerError(`harness has no JSON fragment: ${harness}`, "UNSUPPORTED_HARNESS");
479
485
  return parseJson(fs.readFileSync(path.join(sourceRoot, relative)), relative);
480
486
  }
481
487
 
@@ -507,6 +513,16 @@ function planInstall(sourceRoot, target, harnesses, oldManifest, store) {
507
513
  );
508
514
  records.push(hooks.record); operations.push(hooks.op);
509
515
  }
516
+ if (harnesses.includes("pi")) {
517
+ const extension = copyRecord(
518
+ sourceRoot,
519
+ target,
520
+ ".pi/extensions/djournal.ts",
521
+ old.get(".pi/extensions/djournal.ts"),
522
+ "pi",
523
+ );
524
+ records.push(extension.record); operations.push(extension.op);
525
+ }
510
526
 
511
527
  const desired = new Set(records.map((record) => record.path));
512
528
  for (const record of oldManifest?.files || []) {
@@ -828,10 +844,20 @@ function doctor(options) {
828
844
  }
829
845
  }
830
846
  const manifest = readOptional(resolveWithin(target, MANIFEST_PATH));
847
+ let loadedManifest = null;
831
848
  if (manifest) {
832
- try { loadManifest(target); checks.push({ name: "manifest", ok: true, detail: "supported" }); }
849
+ try { loadedManifest = loadManifest(target); checks.push({ name: "manifest", ok: true, detail: "supported" }); }
833
850
  catch (error) { checks.push({ name: "manifest", ok: false, detail: error.message }); }
834
851
  }
852
+ if (loadedManifest?.harnesses?.includes("pi")) {
853
+ const record = loadedManifest.files.find((item) => item.path === ".pi/extensions/djournal.ts" && item.harness === "pi");
854
+ const assetStatus = record ? recordStatus(target, record) : "missing";
855
+ checks.push({
856
+ name: ".pi/extensions/djournal.ts",
857
+ ok: assetStatus === "clean" || assetStatus === "modified",
858
+ detail: `${assetStatus}; Pi project trust required`,
859
+ });
860
+ }
835
861
  return { ok: checks.every((check) => check.ok), target, detected, checks };
836
862
  }
837
863
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "djournal",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Durable project memory for coding agents, stored as linked Markdown",
5
5
  "type": "commonjs",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  ".agents/",
12
12
  ".claude/",
13
13
  ".codex/",
14
+ ".pi/",
14
15
  "AGENTS.md",
15
16
  "CLAUDE.md",
16
17
  "LICENSE",
@@ -25,7 +26,7 @@
25
26
  "check:pr-title": "node scripts/check-pr-title.js",
26
27
  "link:global": "npm link",
27
28
  "release": "semantic-release",
28
- "test": "node tests/journal-hook.test.js && node tests/installer/installer.test.js && node tests/release-policy.test.js && npm run check:package",
29
+ "test": "node tests/journal-hook.test.js && node tests/pi-adapter.test.js && node tests/installer/installer.test.js && node tests/release-policy.test.js && npm run check:package",
29
30
  "prepublishOnly": "npm test"
30
31
  },
31
32
  "repository": {