@timqi/pier 0.0.29 → 0.1.1
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 +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/log.js
CHANGED
|
@@ -1,52 +1,35 @@
|
|
|
1
|
-
// What Pier writes to its own log
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// log configuration. Under systemd that *is* the log: journald stamps the time,
|
|
5
|
-
// keeps the history, rotates it and lets `journalctl -p warning` filter it
|
|
6
|
-
// (docs/deploy.md), and in a terminal it is the terminal. A logger that opened
|
|
7
|
-
// its own file would duplicate all of that and hide half the output from
|
|
8
|
-
// `journalctl` — the one place an operator actually looks.
|
|
9
|
-
//
|
|
10
|
-
// Like paths.ts, this is a leaf everything may import and that imports nothing:
|
|
11
|
-
// a log line is not a seam crossing, so no area owns it.
|
|
1
|
+
// What Pier writes to its own log. stdout/stderr only: under systemd journald
|
|
2
|
+
// is the log (timestamps, history, rotation, `journalctl -p warning`), and a
|
|
3
|
+
// file of our own would hide half the output from it.
|
|
12
4
|
import { homedir } from "node:os";
|
|
13
5
|
const ORDER = ["debug", "info", "warn", "error"];
|
|
14
6
|
/** `silent` exists for test runs, which drive failure paths on purpose. */
|
|
15
7
|
const THRESHOLDS = [...ORDER, "silent"];
|
|
16
8
|
const isThreshold = (value) => THRESHOLDS.includes(value);
|
|
17
9
|
const RANK = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
18
|
-
/** `PIER_LOG=debug` turns on the per-message tracing; default keeps it out. */
|
|
19
10
|
const raw = (process.env.PIER_LOG ?? "").toLowerCase();
|
|
20
11
|
const threshold = isThreshold(raw) ? raw : "info";
|
|
21
|
-
// systemd sets JOURNAL_STREAM when
|
|
22
|
-
//
|
|
23
|
-
// plain stream tells journald its priority (sd-daemon(3)), so the same line
|
|
24
|
-
// that reads well in a terminal stays greppable *and* filterable by level.
|
|
12
|
+
// systemd sets JOURNAL_STREAM when output goes to the journal; a `<N>` prefix
|
|
13
|
+
// is how a plain stream tells journald its priority (sd-daemon(3)).
|
|
25
14
|
const toJournal = process.env.JOURNAL_STREAM !== undefined;
|
|
26
15
|
const PRIORITY = { debug: "<7>", info: "<6>", warn: "<4>", error: "<3>" };
|
|
27
|
-
/** `$HOME`
|
|
28
|
-
* Skipped when `$HOME` is `/` (containers do this), where it would rewrite
|
|
29
|
-
* every slash in every message. */
|
|
16
|
+
/** Skipped when `$HOME` is `/` (containers), where it would rewrite every slash. */
|
|
30
17
|
const home = homedir();
|
|
31
18
|
const shorten = (text) => home.length > 1 ? text.replaceAll(home, "~") : text;
|
|
32
|
-
/** An Error contributes its stack — the post-mortem is why it was logged. */
|
|
33
19
|
const describe = (cause) => cause instanceof Error ? (cause.stack ?? `${cause.name}: ${cause.message}`) : String(cause);
|
|
34
20
|
function write(level, area, message, cause) {
|
|
35
21
|
if (RANK[level] < RANK[threshold])
|
|
36
22
|
return;
|
|
37
23
|
const text = shorten(cause === undefined ? message : `${message}: ${describe(cause)}`);
|
|
38
|
-
// Per line
|
|
39
|
-
//
|
|
40
|
-
// newline in something a browser reported would let it forge a level.
|
|
24
|
+
// Per line: journald reads a prefix off each line, and a newline in
|
|
25
|
+
// something a browser reported would otherwise let it forge a level.
|
|
41
26
|
const line = toJournal
|
|
42
27
|
? text.split("\n").map((part, i) => `${PRIORITY[level]}${i === 0 ? `${area}: ` : ""}${part}`).join("\n")
|
|
43
28
|
: `${new Date().toISOString()} ${level.toUpperCase().padEnd(5)} ${area}: ${text}`;
|
|
44
|
-
// Warnings and errors on stderr: it is what journald and every wrapper
|
|
45
|
-
// already treat as the abnormal stream, with or without the prefix above.
|
|
46
29
|
const stream = level === "warn" || level === "error" ? process.stderr : process.stdout;
|
|
47
30
|
stream.write(`${line}\n`);
|
|
48
31
|
}
|
|
49
|
-
/**
|
|
32
|
+
/** The area is the grep handle, so keep it stable. */
|
|
50
33
|
export const logger = (area) => ({
|
|
51
34
|
debug: (message, cause) => write("debug", area, message, cause),
|
|
52
35
|
info: (message, cause) => write("info", area, message, cause),
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizeAgentSnapshot } from "./agent/config-sync.js";
|
|
|
8
8
|
import { ConfigSync } from "./config-sync.js";
|
|
9
9
|
import { configSyncTask } from "./config-sync-task.js";
|
|
10
10
|
import { CredentialStore } from "./agent/credentials.js";
|
|
11
|
+
import { IndexedListing } from "./agent/listing.js";
|
|
11
12
|
import { PiAgentFactory } from "./agent/pi.js";
|
|
12
13
|
import { defaultBoardsDir, registerBoardRoutes } from "./boards/boards.js";
|
|
13
14
|
import { ChannelStore } from "./channels/config.js";
|
|
@@ -20,11 +21,13 @@ import { SlackDirectory } from "./channels/slack-directory.js";
|
|
|
20
21
|
import { handleSlackTool, slackToolAvailable, slackToolSpec } from "./channels/slack-tool.js";
|
|
21
22
|
import { parseConversation as parseSlackConversation } from "./channels/slack.js";
|
|
22
23
|
import { EventHub } from "./core/hub.js";
|
|
24
|
+
import { splitSpeaker } from "./core/identity.js";
|
|
23
25
|
import { pierDb } from "./db.js";
|
|
24
26
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
25
27
|
import { bundledInfo } from "./extensions/index.js";
|
|
26
28
|
import { surfacePrompt } from "./core/reply.js";
|
|
27
29
|
import { Router } from "./core/router.js";
|
|
30
|
+
import { acquireInstanceLock } from "./lock.js";
|
|
28
31
|
import { logger } from "./log.js";
|
|
29
32
|
import { registerTaskRoutes } from "./tasks/routes.js";
|
|
30
33
|
import { TaskService } from "./tasks/service.js";
|
|
@@ -43,51 +46,41 @@ import { PushStore, registerPushRoutes } from "./web/push.js";
|
|
|
43
46
|
import { SessionStateStore } from "./web/session-state.js";
|
|
44
47
|
import { createServer } from "./web/server.js";
|
|
45
48
|
const log = logger("pier");
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// An operator override wins — but only a human's. Everything Pier spawns (the
|
|
51
|
-
// agent's own shell, task sessions) inherits this variable, so a second
|
|
52
|
-
// Pier started from inside the first with its own PIER_HOME would adopt the
|
|
53
|
-
// first one's agent dir and write its sessions, SYSTEM.md and models.json
|
|
54
|
-
// there: two instances sharing a directory neither was told to share, and
|
|
55
|
-
// PIER_HOME looking like it did nothing. PIER_AGENT_DIR marks the value as
|
|
56
|
-
// ours, and a value that is ours is not an override, it is a leak — derive it
|
|
57
|
-
// again from this instance's own PIER_HOME.
|
|
49
|
+
// Before any SDK call resolves a path, so everything Pi derives from its agent
|
|
50
|
+
// dir lands under PIER_HOME. PIER_AGENT_DIR marks the value as ours: a second
|
|
51
|
+
// Pier spawned from inside the first inherits it, and an inherited value is a
|
|
52
|
+
// leak, not an operator override (paths.ts).
|
|
58
53
|
process.env.PI_CODING_AGENT_DIR = resolveAgentDir(process.env);
|
|
59
54
|
process.env.PIER_AGENT_DIR = process.env.PI_CODING_AGENT_DIR;
|
|
60
|
-
// Ahead of everything Pier spawns — sessions and tasks all inherit this
|
|
61
|
-
// process's env. A tool switched on in the Console is Pier's
|
|
62
|
-
// copy at Pier's version, so it goes first, not last.
|
|
63
55
|
prependPath(process.env);
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
56
|
+
// Before the database and before task recovery, which would mark the running
|
|
57
|
+
// instance's queued and active runs interrupted; the port is discovered far too
|
|
58
|
+
// late, and a second Pier on another port would never notice at all.
|
|
59
|
+
const lock = acquireInstanceLock();
|
|
60
|
+
if ("heldBy" in lock) {
|
|
61
|
+
log.error(`another Pier (pid ${lock.heldBy === null ? "unknown" : String(lock.heldBy)}) owns ${PIER_HOME} — refusing to start`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
// Every exit path at once: the drain and both shutdowns end in `process.exit`,
|
|
65
|
+
// and the crash that skips this is what the stale-claim takeover is for.
|
|
66
|
+
process.on("exit", lock.release);
|
|
67
|
+
// A schema that cannot be migrated must stop the process before a port is open.
|
|
67
68
|
const db = pierDb();
|
|
68
|
-
//
|
|
69
|
-
// pier.db now, and a setting that silently stops being read is a 5b violation:
|
|
70
|
-
// the operator who wrote it deserves to hear that it no longer applies.
|
|
69
|
+
// A setting that silently stops being read is a §5 violation.
|
|
71
70
|
for (const stale of ["settings.json", "pins.json", "unread.json"]) {
|
|
72
71
|
if (existsSync(pierPath(stale))) {
|
|
73
72
|
log.warn(`${pierPath(stale)} is no longer read — its value lives in pier.db now; re-enter it in the Console and delete the file`);
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
|
-
// One store, two readers: the Console writes the public URL, and every session
|
|
77
|
-
// opened after that is told the new one.
|
|
78
75
|
const settings = new SettingsStore(db);
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// nothing that needs a token may run before the key arrives.
|
|
76
|
+
// Unlocked below: vt mode waits on a human approval, and nothing that needs a
|
|
77
|
+
// token may run before the key arrives.
|
|
82
78
|
const secrets = new Secrets();
|
|
83
79
|
let tasks;
|
|
84
80
|
const conversations = new ConversationStore(db);
|
|
85
81
|
let resolveIm;
|
|
86
|
-
// Declared before the store exists because the factory is built first; the tool
|
|
87
|
-
// only ever runs long after wiring is done.
|
|
88
82
|
let channelStore;
|
|
89
|
-
// Shared by the adapter and the tool:
|
|
90
|
-
// process, not once per message and again per transcript.
|
|
83
|
+
// Shared by the adapter and the tool: one display-name lookup per process.
|
|
91
84
|
const slackDirectory = new SlackDirectory((m) => logger("slack").warn(m));
|
|
92
85
|
let readyForConfigReload = false;
|
|
93
86
|
const piConfig = new PiConfigStore();
|
|
@@ -100,15 +93,12 @@ const factory = new PiAgentFactory([
|
|
|
100
93
|
slackToolSpec((params, callerSessionId) => handleSlackTool({
|
|
101
94
|
store: channelStore,
|
|
102
95
|
directory: slackDirectory,
|
|
103
|
-
//
|
|
104
|
-
// and a client captured at boot would keep using the old one.
|
|
96
|
+
// Per call: the Console can change the token underneath us.
|
|
105
97
|
client: () => {
|
|
106
98
|
const config = channelStore.get("slack");
|
|
107
99
|
return config.token ? new SlackApi(config.token, config.appToken) : null;
|
|
108
100
|
},
|
|
109
|
-
//
|
|
110
|
-
// no ids. Looked up per call: the mapping is durable, the session is
|
|
111
|
-
// not.
|
|
101
|
+
// Per call: the mapping is durable, the session is not.
|
|
112
102
|
here: (sessionId) => {
|
|
113
103
|
const key = router.conversationOf(sessionId);
|
|
114
104
|
if (key?.channelId !== "slack")
|
|
@@ -117,50 +107,33 @@ const factory = new PiAgentFactory([
|
|
|
117
107
|
return channel && threadTs ? { channel, threadTs } : null;
|
|
118
108
|
},
|
|
119
109
|
log: (m) => logger("slack.tool").warn(m),
|
|
120
|
-
}, params, callerSessionId),
|
|
121
|
-
// No Slack, no schema: an unconfigured tool would sit in every prompt of
|
|
122
|
-
// every session and be able to answer nothing.
|
|
123
|
-
() => slackToolAvailable(channelStore)),
|
|
110
|
+
}, params, callerSessionId), () => slackToolAvailable(channelStore)),
|
|
124
111
|
],
|
|
125
|
-
//
|
|
126
|
-
//
|
|
112
|
+
// Getters, read per session open: a Console change reaches the next session
|
|
113
|
+
// without a restart.
|
|
127
114
|
() => surfacePrompt({ boardsDir: defaultBoardsDir(), publicUrl: settings.get().publicUrl }),
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// imported on first use and renamed to auth.json.imported.
|
|
133
|
-
new CredentialStore(db, secrets), piConfig,
|
|
134
|
-
// Operator pins ride ahead of the curated catalog in every model picker.
|
|
135
|
-
() => settings.get().modelMenu,
|
|
136
|
-
// Bundled extensions the Console switched on; read per session open, so the
|
|
137
|
-
// toggle reaches the next session the same way an edited agent file does.
|
|
138
|
-
() => settings.get().extensions);
|
|
115
|
+
// Documents Pier's own tools, so it loads only inside a Pier session.
|
|
116
|
+
[fileURLToPath(new URL("../skills", import.meta.url))], new CredentialStore(db, secrets), piConfig, () => settings.get().modelMenu, () => settings.get().extensions, () => settings.get().titleModel,
|
|
117
|
+
// Transcripts carry the speaker header core wrote for the model.
|
|
118
|
+
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text));
|
|
139
119
|
const hub = new EventHub();
|
|
140
120
|
const router = new Router(hub, (key) => {
|
|
141
|
-
// Web conversation ids
|
|
142
|
-
//
|
|
143
|
-
// cwd the chat is configured for) — a restart must not re-route a group.
|
|
121
|
+
// Web conversation ids are session ids; an IM id is a chat, resolved through
|
|
122
|
+
// the durable map so a restart does not re-route a group.
|
|
144
123
|
if (key.channelId === "web" || key.channelId === "task") {
|
|
145
124
|
return factory.resume(key.conversationId);
|
|
146
125
|
}
|
|
147
126
|
return resolveIm(key);
|
|
148
|
-
});
|
|
149
|
-
// An attached session holds a live Pi runtime and its transcript, and nothing
|
|
150
|
-
// else ever lets one go: without this, one per conversation ever answered.
|
|
127
|
+
}, (key) => conversations.get(key));
|
|
151
128
|
const stopEviction = router.startIdleEviction();
|
|
152
129
|
tasks = new TaskService(new TaskStore(db), factory, router, hub, {
|
|
153
130
|
modelMenu: () => settings.get().modelMenu,
|
|
154
131
|
systemActions: { "config-sync": (signal) => configSync.sync(signal) },
|
|
155
132
|
});
|
|
156
133
|
const configurationSync = configSyncTask(tasks, configSync);
|
|
157
|
-
// The managed CLI tools (src/tools.ts), and the daily task that keeps them
|
|
158
|
-
// current (src/tools-task.ts) — an ordinary bash task on an ordinary cron,
|
|
159
|
-
// wired here because tools.ts may not import tasks/.
|
|
160
134
|
const managedTools = new ManagedTools();
|
|
161
135
|
const toolsUpdate = toolsTask(tasks);
|
|
162
|
-
// Before any route exists: two first flips could otherwise both
|
|
163
|
-
// and create one each. A failure here is logged, and the next flip retries.
|
|
136
|
+
// Before any route exists: two first flips could otherwise both create a task.
|
|
164
137
|
const reconciled = await toolsUpdate.reconcile();
|
|
165
138
|
if ("problem" in reconciled)
|
|
166
139
|
log.error(`tools cannot be managed: ${reconciled.problem}`);
|
|
@@ -168,31 +141,25 @@ channelStore = new ChannelStore(db, secrets);
|
|
|
168
141
|
const control = createControl({ router, factory, conversations, store: channelStore });
|
|
169
142
|
const channels = new ChannelRuntime(channelStore, router, control);
|
|
170
143
|
resolveIm = resolveConversation(conversations, factory, control.launchFor, (message) => logger("channels").warn(message));
|
|
171
|
-
// Channels connect
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
// Once they are up, the chats a previous restart cut off are told (drain.ts) —
|
|
175
|
-
// on this path and on a later Console unlock alike, because a note held back
|
|
176
|
-
// by locked secrets must not wait for yet another restart.
|
|
144
|
+
// Channels connect once tokens are readable; a refused unlock must not take
|
|
145
|
+
// down the web surface, which is where the operator repairs it. The chats a
|
|
146
|
+
// previous restart cut off are told as soon as channels are up (drain.ts).
|
|
177
147
|
const restartLedger = new RestartLedger(db);
|
|
178
148
|
const startChannels = async () => {
|
|
179
149
|
await channels.reload();
|
|
180
150
|
await deliverLedger(restartLedger, (entry) => channels.notify(entry.channelId, entry.conversationId, entry.note))
|
|
181
151
|
.catch((err) => log.error("restart-note delivery failed", err));
|
|
182
152
|
};
|
|
183
|
-
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* stored in a transcript. SIGHUP (`pier reload`) and the Console's Reload are
|
|
187
|
-
* both this call; `includeWatched` is the only difference, and only because the
|
|
188
|
-
* Console knows a person asked from the session they are looking at. */
|
|
153
|
+
/** Adapters re-read their configuration and sessions are let go, so the next
|
|
154
|
+
* message re-opens them with current skills, extensions, prompts and
|
|
155
|
+
* credentials. SIGHUP and the Console's Reload are both this call. */
|
|
189
156
|
const reloadInstance = async (includeWatched = false) => {
|
|
190
157
|
await channels.reload();
|
|
191
158
|
return router.evictIdle(0, Date.now(), { includeWatched });
|
|
192
159
|
};
|
|
193
|
-
// Bound the startup check before sessions and scheduled work can start.
|
|
194
160
|
// Remote outages keep the last local configuration available.
|
|
195
161
|
if (configSync.status().enabled) {
|
|
162
|
+
// sync() already logged the cause; this line says what the boot did about it.
|
|
196
163
|
try {
|
|
197
164
|
await configSync.sync();
|
|
198
165
|
}
|
|
@@ -204,28 +171,15 @@ await configurationSync.reconcile();
|
|
|
204
171
|
tasks.start();
|
|
205
172
|
readyForConfigReload = true;
|
|
206
173
|
void secrets.unlock().then(startChannels, (err) => log.error("secrets locked — channels not started; unlock from Console → Settings → Security, or repair master.key", err));
|
|
207
|
-
// Replacing Pier is systemd's job
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
// that unit there is nothing to hand the work to, and the Console says so
|
|
211
|
-
// instead of offering a button that cannot work.
|
|
174
|
+
// Replacing Pier is systemd's job: the oneshot unit snapshots the database,
|
|
175
|
+
// installs, then stops and starts the service. Without that unit the Console
|
|
176
|
+
// says so instead of offering a button that cannot work.
|
|
212
177
|
const updates = new UpdateCheck();
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// of a Pier nobody has a browser open on.
|
|
178
|
+
// At boot, not lazily: it puts the answer in the journal of a Pier nobody has
|
|
179
|
+
// a browser open on.
|
|
216
180
|
void updates.refresh();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* `systemctl stop`, i.e. a SIGTERM, which is the *fast* teardown — so anything
|
|
220
|
-
* that started since the idle check would be killed with no note anywhere. The
|
|
221
|
-
* gate closes first and the drain waits, exactly as `pier restart` does,
|
|
222
|
-
* ledger included; only then is the install handed over. A handover that never
|
|
223
|
-
* starts reopens the gate, because a Pier that silently refuses every message
|
|
224
|
-
* forever is worse than the race it was avoiding.
|
|
225
|
-
*/
|
|
226
|
-
// Shared restart state. The updater's handover, the SIGUSR2 drain (below) and
|
|
227
|
-
// the final teardown must see each other: without this, two paths drain the
|
|
228
|
-
// same Pier at once, and a failure on one reopens the gate the other still
|
|
181
|
+
// The updater's handover, the SIGUSR2 drain and the teardown must see each
|
|
182
|
+
// other, or two paths drain the same Pier and one reopens the gate the other
|
|
229
183
|
// needs shut.
|
|
230
184
|
let handingOver = false;
|
|
231
185
|
let draining = false;
|
|
@@ -233,32 +187,23 @@ let shuttingDown = false;
|
|
|
233
187
|
const takeWorkAgain = (why) => {
|
|
234
188
|
handingOver = false;
|
|
235
189
|
log.error(`${why} — taking work again`);
|
|
236
|
-
// Not ours to reopen: a
|
|
237
|
-
// and reopening it would hand new work to a process that is exiting.
|
|
190
|
+
// Not ours to reopen: a restart or the teardown owns the gate now.
|
|
238
191
|
if (draining || shuttingDown)
|
|
239
192
|
return;
|
|
240
193
|
router.endDrain();
|
|
241
194
|
tasks.unpause();
|
|
242
|
-
//
|
|
243
|
-
// restart that was supposed to follow, that debt would wait for one days
|
|
244
|
-
// away (§5b) — so the chats are told now, by the process that cut them off.
|
|
195
|
+
// Turns the drain deadline-aborted must not wait for a restart days away (§5).
|
|
245
196
|
void deliverLedger(restartLedger, (entry) => channels.notify(entry.channelId, entry.conversationId, entry.note))
|
|
246
197
|
.catch((err) => log.error("restart-note delivery failed", err));
|
|
247
198
|
};
|
|
248
|
-
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* someone else's network — ten seconds on a good day, minutes on a bad one.
|
|
253
|
-
* So this waits far longer than the stop itself needs, because reopening the
|
|
254
|
-
* gate mid-install would take work we are about to be SIGTERM'd out of, and
|
|
255
|
-
* still short enough that a handover which never happens does not refuse
|
|
256
|
-
* messages all afternoon. */
|
|
199
|
+
/** `systemctl start --no-block` returns when the job is queued; the real
|
|
200
|
+
* outcome is a SIGTERM after a registry download on someone else's network.
|
|
201
|
+
* Long enough not to reopen the gate mid-install, short enough that a handover
|
|
202
|
+
* that never happens does not refuse messages all afternoon. */
|
|
257
203
|
const HANDOVER_GRACE_MS = 5 * 60_000;
|
|
258
204
|
const handOverToUpdater = async () => {
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// twice, each believing the gate is its own to reopen on failure.
|
|
205
|
+
// The updater's first act is `systemctl stop`, the fast teardown: the gate
|
|
206
|
+
// closes and the drain waits first, as `pier restart` does.
|
|
262
207
|
if (handingOver || draining || shuttingDown)
|
|
263
208
|
return "busy";
|
|
264
209
|
handingOver = true;
|
|
@@ -268,11 +213,8 @@ const handOverToUpdater = async () => {
|
|
|
268
213
|
takeWorkAgain(`update not started (${started})`);
|
|
269
214
|
return started;
|
|
270
215
|
}
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
// masked, the job sat behind another — would leave Pier alive and refusing
|
|
274
|
-
// every message with no way back. Unref'd: this must not be what keeps the
|
|
275
|
-
// process up while systemd is trying to stop it.
|
|
216
|
+
// A handover that queues and goes nowhere would leave Pier refusing every
|
|
217
|
+
// message with no way back. Unref'd: must not keep the process up under stop.
|
|
276
218
|
setTimeout(() => {
|
|
277
219
|
takeWorkAgain(`still running ${String(HANDOVER_GRACE_MS / 1000)}s after handing over — pier-update.service never stopped Pier` +
|
|
278
220
|
` (check: journalctl --user -u pier-update.service -e)`);
|
|
@@ -282,20 +224,17 @@ const handOverToUpdater = async () => {
|
|
|
282
224
|
const updater = process.platform === "linux" && existsSync(unitPath())
|
|
283
225
|
? { apply: handOverToUpdater, problem: () => updaterProblem() }
|
|
284
226
|
: null;
|
|
285
|
-
// Unattended only when the operator asked for it *and* nothing is running.
|
|
286
227
|
if (updater) {
|
|
287
228
|
const problem = updaterProblem();
|
|
288
|
-
//
|
|
289
|
-
//
|
|
229
|
+
// At boot, when the operator is looking; the alternative is a restart that
|
|
230
|
+
// fails months from now.
|
|
290
231
|
if (problem)
|
|
291
232
|
log.warn(`the updater cannot run: ${problem}`);
|
|
292
233
|
startAutoUpdate(updates, {
|
|
293
234
|
enabled: () => settings.get().autoUpdate,
|
|
294
235
|
idle: () => router.busy().length === 0 && tasks.activeRunCount() === 0,
|
|
295
236
|
apply: async () => {
|
|
296
|
-
//
|
|
297
|
-
// recorded Node months into an uptime, and draining for a handover that
|
|
298
|
-
// cannot happen would take the whole instance down with it.
|
|
237
|
+
// A version manager can remove the recorded Node months into an uptime.
|
|
299
238
|
const now = updaterProblem();
|
|
300
239
|
if (now) {
|
|
301
240
|
log.error(`auto-update skipped: ${now}`);
|
|
@@ -305,18 +244,14 @@ if (updater) {
|
|
|
305
244
|
},
|
|
306
245
|
});
|
|
307
246
|
}
|
|
308
|
-
// Composition happens here so web/ and tasks/ never import each other.
|
|
309
247
|
const app = new Hono();
|
|
310
|
-
//
|
|
311
|
-
// Hono's default handler writes nothing to the log, so the operator sees a
|
|
312
|
-
// failed request in the browser and an empty journal.
|
|
248
|
+
// Hono's default handler writes nothing to the log.
|
|
313
249
|
app.onError((err, c) => {
|
|
314
250
|
log.error(`${c.req.method} ${c.req.path} failed`, err);
|
|
315
251
|
return c.json({ error: String(err) }, 500);
|
|
316
252
|
});
|
|
317
|
-
// Before every route
|
|
318
|
-
//
|
|
319
|
-
// before the listener: a first run generates and prints its password here.
|
|
253
|
+
// Before every route: Hono runs middleware in registration order, so a surface
|
|
254
|
+
// added later is covered without knowing this exists.
|
|
320
255
|
const auth = new AuthStore(db);
|
|
321
256
|
registerConfigShareRoute(app, configSync);
|
|
322
257
|
app.use("*", requireAuth(auth));
|
|
@@ -331,11 +266,8 @@ registerTaskRoutes(app, tasks, { factory, router });
|
|
|
331
266
|
registerChannelRoutes(app, channelStore, channels);
|
|
332
267
|
registerBoardRoutes(app);
|
|
333
268
|
const sessionState = new SessionStateStore(db);
|
|
334
|
-
// The workbench's notifications to a browser that is not open. Composed here,
|
|
335
|
-
// beside the other surfaces: it consumes the same event stream the web server
|
|
336
|
-
// does, and neither one runs the other.
|
|
337
269
|
registerPushRoutes(app, {
|
|
338
|
-
store: new PushStore(db),
|
|
270
|
+
store: new PushStore(db, secrets),
|
|
339
271
|
hub,
|
|
340
272
|
unread: (id) => sessionState.unread(id),
|
|
341
273
|
channelOf: (id) => router.conversationOf(id)?.channelId,
|
|
@@ -350,11 +282,8 @@ app.route("/", createServer({
|
|
|
350
282
|
config: piConfig,
|
|
351
283
|
providers: factory,
|
|
352
284
|
settings,
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
// One list, assembled where both halves are visible: the extensions Pier
|
|
356
|
-
// loads from inside itself and the binaries it installs are the same kind of
|
|
357
|
-
// switch to the person flipping it, and rtk is both.
|
|
285
|
+
// Assembled here so web/ gets names and summaries, not a module that
|
|
286
|
+
// imports the Pi SDK; extensions and binaries are one kind of switch.
|
|
358
287
|
catalog: async () => {
|
|
359
288
|
const { extensions, tools, customTools } = settings.get();
|
|
360
289
|
return {
|
|
@@ -362,14 +291,10 @@ app.route("/", createServer({
|
|
|
362
291
|
toolsTaskId: toolsUpdate.id(),
|
|
363
292
|
};
|
|
364
293
|
},
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// code, never against a catalog whose custom half the request may be
|
|
368
|
-
// rewriting.
|
|
294
|
+
// A switch is validated against what this Pier *can* switch, never against
|
|
295
|
+
// a catalog whose custom half the request may be rewriting.
|
|
369
296
|
names: { extensions: bundledInfo([]).map((entry) => entry.name), tools: MANAGED.map((tool) => tool.name) },
|
|
370
297
|
onToolsChanged: toolsUpdate.changed,
|
|
371
|
-
// The rule lives with the installer; the names the bundled catalog already
|
|
372
|
-
// owns live with the extensions. Only here are both in scope.
|
|
373
298
|
validateCustomTools: (raw) => {
|
|
374
299
|
const validated = normalizeCustomTools(raw, bundledInfo([]).map((entry) => entry.name));
|
|
375
300
|
return validated ? { tools: validated } : { error: CUSTOM_TOOL_RULES };
|
|
@@ -377,7 +302,6 @@ app.route("/", createServer({
|
|
|
377
302
|
secrets,
|
|
378
303
|
updates,
|
|
379
304
|
updater,
|
|
380
|
-
// Unlocked from the Console: start the channels boot held back.
|
|
381
305
|
onUnlocked: () => void startChannels(),
|
|
382
306
|
reload: () => reloadInstance(true),
|
|
383
307
|
backgroundRuns: (id) => tasks.backgroundRuns(id),
|
|
@@ -387,56 +311,45 @@ app.route("/", createServer({
|
|
|
387
311
|
}));
|
|
388
312
|
const port = Number(process.env.PORT ?? 3141);
|
|
389
313
|
const hostname = process.env.HOST ?? "127.0.0.1";
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
// silently changes what half the ecosystem builds; PORT and HOST would aim an
|
|
394
|
-
// agent's own dev server at Pier's socket. Deleted at runtime rather than only
|
|
395
|
-
// dropped from the unit, because an installed unit is rewritten by
|
|
396
|
-
// `pier service install --force` and by nothing else.
|
|
314
|
+
// Every command a turn runs inherits this env: `NODE_ENV=production` makes an
|
|
315
|
+
// agent's `npm install` skip devDependencies, and PORT/HOST would aim its dev
|
|
316
|
+
// server at Pier's socket.
|
|
397
317
|
for (const leak of ["NODE_ENV", "PORT", "HOST"])
|
|
398
318
|
delete process.env[leak];
|
|
399
319
|
const server = serve({ fetch: app.fetch, port, hostname }, () => {
|
|
400
320
|
log.info(`workbench on http://${hostname}:${port}`);
|
|
401
321
|
log.info(`pid ${process.pid}, node ${process.version}, home ${PIER_HOME}`);
|
|
402
|
-
//
|
|
403
|
-
// the one thing about this process's paths that cannot be guessed from it.
|
|
322
|
+
// The one path that cannot be guessed from PIER_HOME.
|
|
404
323
|
if (process.env.PI_CODING_AGENT_DIR !== pierPath("pi")) {
|
|
405
324
|
log.info(`agent dir ${process.env.PI_CODING_AGENT_DIR} (PI_CODING_AGENT_DIR)`);
|
|
406
325
|
}
|
|
407
326
|
});
|
|
408
|
-
// A crash and a clean stop must be distinguishable after the fact, and both
|
|
409
|
-
// left nothing behind before this.
|
|
410
327
|
process.on("uncaughtException", (err) => {
|
|
411
328
|
log.error("uncaught exception, exiting", err);
|
|
412
329
|
process.exit(1); // Node's own default outcome, with the area named
|
|
413
330
|
});
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// every scheduled task down with it — so it is logged loudly and Pier serves on.
|
|
331
|
+
// Node's default is to crash; a stray rejection in one adapter's background
|
|
332
|
+
// work must not take every session and scheduled task down with it.
|
|
417
333
|
process.on("unhandledRejection", (reason) => {
|
|
418
334
|
log.error("unhandled rejection", reason);
|
|
419
335
|
});
|
|
420
336
|
const shutdown = (stopTasks = true) => {
|
|
421
|
-
//
|
|
422
|
-
// racing each other close the same sockets twice.
|
|
337
|
+
// SIGTERM can land while a drain is finishing.
|
|
423
338
|
if (shuttingDown)
|
|
424
339
|
return;
|
|
425
340
|
shuttingDown = true;
|
|
426
|
-
//
|
|
427
|
-
//
|
|
341
|
+
// A socket an adapter cannot close must not turn `systemctl restart` into a
|
|
342
|
+
// 90-second wait for SIGKILL.
|
|
428
343
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
429
344
|
stopEviction();
|
|
430
|
-
// The drain path leaves task runs alone: aborting
|
|
431
|
-
//
|
|
432
|
-
// boot-time interrupted marking is the recovery that was promised.
|
|
345
|
+
// The drain path leaves task runs alone: aborting would record them cancelled,
|
|
346
|
+
// when the boot-time interrupted marking is the recovery that was promised.
|
|
433
347
|
if (stopTasks)
|
|
434
348
|
tasks.stop();
|
|
435
349
|
void channels.stop().finally(() => {
|
|
436
350
|
server.close(() => process.exit(0));
|
|
437
351
|
// Every workbench tab holds an SSE stream open, so `close()` alone would
|
|
438
|
-
//
|
|
439
|
-
// union with HTTP/2, which has no such method — and no such problem.)
|
|
352
|
+
// wait out the timer above. (`in`: the served type is a union with HTTP/2.)
|
|
440
353
|
if ("closeAllConnections" in server)
|
|
441
354
|
server.closeAllConnections();
|
|
442
355
|
});
|
|
@@ -447,10 +360,8 @@ for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
|
447
360
|
shutdown();
|
|
448
361
|
});
|
|
449
362
|
}
|
|
450
|
-
// The slow restart (`pier restart`):
|
|
451
|
-
//
|
|
452
|
-
// next process up. SIGTERM above stays the fast path systemd expects. `on`,
|
|
453
|
-
// not `once`: a second SIGUSR2 with no handler would fall back to Node's
|
|
363
|
+
// The slow restart (`pier restart`): drain, then exit for `Restart=always`.
|
|
364
|
+
// `on`, not `once`: a second SIGUSR2 with no handler would fall back to Node's
|
|
454
365
|
// default and kill the drain it meant to hurry.
|
|
455
366
|
process.on("SIGUSR2", () => {
|
|
456
367
|
if (draining) {
|
|
@@ -463,11 +374,8 @@ process.on("SIGUSR2", () => {
|
|
|
463
374
|
.catch((err) => log.error("drain failed — shutting down anyway", err))
|
|
464
375
|
.then(() => shutdown(false));
|
|
465
376
|
});
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// Only under systemd (the CLI signals through systemctl): a foreground `pier
|
|
469
|
-
// serve` keeps SIGHUP's default, dying with its terminal instead of surviving
|
|
470
|
-
// as an orphan that holds the port.
|
|
377
|
+
// Only under systemd: a foreground `pier serve` keeps SIGHUP's default, dying
|
|
378
|
+
// with its terminal instead of surviving as an orphan that holds the port.
|
|
471
379
|
if (process.env.INVOCATION_ID) {
|
|
472
380
|
process.on("SIGHUP", () => {
|
|
473
381
|
log.info("SIGHUP received, reloading channels and recycling idle sessions");
|
package/dist/paths.js
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
// Where Pier keeps its state, resolved once.
|
|
2
|
-
//
|
|
3
|
-
// This is process configuration, not a per-call decision: the same
|
|
4
|
-
// `PIER_HOME ?? ~/.pier` line had grown six copies, one per module that needed
|
|
5
|
-
// a file, and no area could own the fix — channels/ must not import tasks/,
|
|
6
|
-
// web/ must not import channels/. So it lives in a leaf that everything may
|
|
7
|
-
// depend on and that depends on nothing.
|
|
1
|
+
// Where Pier keeps its state, resolved once, in a leaf every area may import.
|
|
8
2
|
import { homedir } from "node:os";
|
|
9
3
|
import { join } from "node:path";
|
|
10
|
-
/** Empty is unset, not a value: `PIER_HOME=`
|
|
11
|
-
*
|
|
12
|
-
* database, the boards and the master key would land wherever the process
|
|
13
|
-
* happened to start. Pure, because that rule is worth a test. */
|
|
4
|
+
/** Empty is unset, not a value: `PIER_HOME=` would otherwise resolve every
|
|
5
|
+
* path relative to the working directory. */
|
|
14
6
|
export const resolveHome = (value, home = homedir()) => value || join(home, ".pier");
|
|
15
|
-
/** `$PIER_HOME`, or `~/.pier`. Fixed for the life of the process. */
|
|
16
7
|
export const PIER_HOME = resolveHome(process.env.PIER_HOME);
|
|
17
|
-
/** A path inside it — `pierPath("boards")`. */
|
|
18
8
|
export const pierPath = (...parts) => join(PIER_HOME, ...parts);
|
|
19
|
-
/**
|
|
20
|
-
* directory so db.ts can lock that directory down to 0700 without touching
|
|
9
|
+
/** In its own directory so db.ts can lock it down to 0700 without touching
|
|
21
10
|
* the boards PIER_HOME also holds. */
|
|
22
11
|
export const PIER_DB = pierPath("db", "pier.db");
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* own PIER_HOME is. `PIER_AGENT_DIR` carries the value Pier itself set: when
|
|
30
|
-
* the two match, the variable is a leak rather than an instruction, and this
|
|
31
|
-
* instance derives its own. Pure, because the rule is worth a test and the
|
|
32
|
-
* process only gets to apply it once (main.ts).
|
|
33
|
-
*/
|
|
12
|
+
/** At the root of PIER_HOME: the claim is the whole instance directory, not the
|
|
13
|
+
* database (lock.ts). */
|
|
14
|
+
export const PIER_LOCK = pierPath("pier.lock");
|
|
15
|
+
/** `PI_CODING_AGENT_DIR` is an operator override — unless it equals
|
|
16
|
+
* `PIER_AGENT_DIR`, the value Pier itself set: then a Pier spawned from inside
|
|
17
|
+
* another inherited it, and a leak is not an instruction. */
|
|
34
18
|
export function resolveAgentDir(env, derived = pierPath("pi")) {
|
|
35
19
|
const given = env.PI_CODING_AGENT_DIR;
|
|
36
20
|
return !given || given === env.PIER_AGENT_DIR ? derived : given;
|