pi-note 0.7.1 → 0.9.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.
- package/README.md +39 -6
- package/index.ts +57 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,10 +56,10 @@ standalone `settings/pi/extensions/pinote.ts` entry if installed; load only one
|
|
|
56
56
|
|
|
57
57
|
## Use
|
|
58
58
|
|
|
59
|
-
`/pi-note` picks a task, marks it in progress, and adds this prompt to the editor:
|
|
59
|
+
`/pi-note` picks a task, marks it in progress, and adds this default prompt to the editor:
|
|
60
60
|
“Read the current Pinote task. Summarize your understanding, but don’t start work yet.”
|
|
61
61
|
The agent fetches current task text and saved handoff fields when you submit;
|
|
62
|
-
they are not copied into the draft. The prompt asks for a summary, not implementation.
|
|
62
|
+
they are not copied into the draft. The default prompt asks for a summary, not implementation.
|
|
63
63
|
Existing drafts are preserved; nothing is submitted automatically.
|
|
64
64
|
With a selected task it offers **Continue**, **Done**, and **Switch task**.
|
|
65
65
|
Type in the task picker to filter by text, ID, tag, or state (all words must match).
|
|
@@ -74,14 +74,47 @@ Resume restores that session's task; a new session starts unselected and does no
|
|
|
74
74
|
an older per-folder selection. Pi saves the session after the first submitted message;
|
|
75
75
|
quitting before that leaves the next launch unselected, while the task stays in progress.
|
|
76
76
|
Use **Continue**
|
|
77
|
-
to insert the same
|
|
77
|
+
to insert the same configured prompt. Completing, removing, or scheduling a task clears it
|
|
78
78
|
from sessions that refresh it. Switching does not complete or reset the previous task.
|
|
79
79
|
Task fields such as `Worktree` and `PR` stay on the task, so sessions can use separate
|
|
80
80
|
worktrees and pull requests. Task status refreshes at session start, before/after
|
|
81
81
|
agent activity, and after commands/tools. The CLI's per-directory `agent selected`
|
|
82
82
|
command is not this session memory.
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
### Personal configuration
|
|
85
|
+
|
|
86
|
+
Configure `~/.pi/agent/pi-note.json` (or the agent directory set by
|
|
87
|
+
`PI_CODING_AGENT_DIR`):
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"handoffPrompt": "Read the current Pinote task. Summarize your understanding and proposed approach, but don’t start work yet.",
|
|
92
|
+
"taskOfferPolicy": "github-remote"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`taskOfferPolicy` controls the agent's task-offer guidance:
|
|
97
|
+
|
|
98
|
+
- `always` (default): offer a task when given work with no task selected.
|
|
99
|
+
- `github-remote`: first verify with Git that the repository has an HTTPS or SSH
|
|
100
|
+
remote hosted on `github.com`; otherwise do not offer. Local paths and other
|
|
101
|
+
hosts do not qualify. This is agent guidance, not a network or PR-access check.
|
|
102
|
+
- `never`: do not offer tasks; explicit requests to create one remain allowed.
|
|
103
|
+
|
|
104
|
+
All policies preserve the existing creation tools, user-consent requirement,
|
|
105
|
+
and selected tasks. Run `/reload` after changing this setting. Invalid policy
|
|
106
|
+
values or unreadable/malformed configuration suppress offers; interactive startup
|
|
107
|
+
warns until you fix the file and reload. No project-local configuration is read.
|
|
108
|
+
|
|
109
|
+
Task selection and **Continue** reread `handoffPrompt` each time; changing that
|
|
110
|
+
field needs no `/reload`. Its value is literal text, not a prompt template; use
|
|
111
|
+
`\n` in JSON strings for multiple lines. It must be a nonblank string without control
|
|
112
|
+
characters other than tabs and newlines. A missing file or key uses the default.
|
|
113
|
+
Invalid/unreadable configuration reports an error without starting/switching a
|
|
114
|
+
task or changing the editor. **Done** and agent tools are unaffected. This is
|
|
115
|
+
user-level configuration only; project-local files are not read.
|
|
116
|
+
|
|
117
|
+
### Agent tools
|
|
85
118
|
|
|
86
119
|
- `pinote_get_current`: read this session's selected task, including its revision.
|
|
87
120
|
Takes no arguments; returns null when this session has no selected task.
|
|
@@ -98,8 +131,8 @@ Version 0.7.0 replaces `pinote_get`/`pinote_update` with these current-task tool
|
|
|
98
131
|
and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
|
|
99
132
|
or retag existing tasks; use `/pi-note` to select an existing task.
|
|
100
133
|
|
|
101
|
-
When
|
|
102
|
-
|
|
134
|
+
When allowed by `taskOfferPolicy`, the agent proposes one note as `[tag] text`
|
|
135
|
+
and asks before creating it. No continues without a note. Yes calls
|
|
103
136
|
`pinote_add` with `select: true`. An existing selection is not replaced unless the
|
|
104
137
|
user asks to switch. Reuse a saved tag name when it fits.
|
|
105
138
|
|
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
4
5
|
import { stripVTControlCharacters } from "node:util";
|
|
5
6
|
import { Type } from "typebox";
|
|
6
7
|
import { createPRWatcher } from "./pr-watch.ts";
|
|
@@ -8,10 +9,13 @@ import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
|
|
|
8
9
|
import { bundledCLIVersion, cliMenu, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
|
|
9
10
|
|
|
10
11
|
const handoffGuidance = "Keep agent notes to at most three short bullets total: outcome, blocker, next action, only when relevant. Replace stale notes; omit narration, repeated task text, and routine test logs. Keep a GitHub pull request in the PR field. To show another footer field, set its Markdown value and append its label to Bar, one label per line. Links in that text are clickable. Do not list PR in Bar. Remove the field and its Bar line to drop it. Notes stay off the footer unless named in Bar.";
|
|
11
|
-
const
|
|
12
|
-
"When the user gives work and no pinote task is selected, propose one note as `[tag] text` and ask before creating it.
|
|
13
|
-
"
|
|
14
|
-
|
|
12
|
+
const offerGuidance = {
|
|
13
|
+
always: "When the user gives work and no pinote task is selected, propose one note as `[tag] text` and ask before creating it.",
|
|
14
|
+
"github-remote": "When the user gives work and no pinote task is selected, first verify with Git that the current repository has a remote whose URL host is github.com (HTTPS or SSH). Only then propose one note as `[tag] text` and ask before creating it. Local paths, other hosts, and GitHub-looking URL paths do not qualify. If no GitHub remote is verified, do not offer a task; explicit user requests to create one remain allowed.",
|
|
15
|
+
never: "Do not offer to create a pinote task. Create one only when the user explicitly requests it.",
|
|
16
|
+
};
|
|
17
|
+
const consentGuidance = "On no, continue without a note. On yes, call pinote_add with select true so it becomes this session's active task. Never add or select without a yes. If a task is already selected, do not replace it unless the user asks to switch.";
|
|
18
|
+
const tagGuidance = "Reuse a pinote_tags name when it fits. One tag; case-sensitive, trimmed, at most 64 characters. Tags are set when creating tasks with pinote_add; agent tools cannot retag existing tasks. Do not use pinote_update_current for tags or task text.";
|
|
15
19
|
type Task = {
|
|
16
20
|
id: number;
|
|
17
21
|
text: string;
|
|
@@ -34,6 +38,51 @@ const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-
|
|
|
34
38
|
const firstLine = (task: Summary) => clean(task.text.split("\n", 1)[0]).replace(/\s+/gu, " ").trim();
|
|
35
39
|
const title = (task: Summary) => truncateToWidth(`#${task.id} ${firstLine(task)}`, 60);
|
|
36
40
|
|
|
41
|
+
const defaultHandoffPrompt = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
|
|
42
|
+
function readConfig(): Record<string, unknown> {
|
|
43
|
+
const path = join(getAgentDir(), "pi-note.json");
|
|
44
|
+
let raw: string;
|
|
45
|
+
try {
|
|
46
|
+
raw = readFileSync(path, "utf8");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
49
|
+
throw new Error(`Cannot read ${path}. Check the pi-note configuration.`);
|
|
50
|
+
}
|
|
51
|
+
let config: unknown;
|
|
52
|
+
try { config = JSON.parse(raw); } catch { throw new Error(`Invalid JSON in ${path}.`); }
|
|
53
|
+
if (!record(config)) throw new Error(`Expected a JSON object in ${path}.`);
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function creationGuidance(): { guidelines: string[]; error?: string } {
|
|
58
|
+
let policy: keyof typeof offerGuidance = "always";
|
|
59
|
+
let error: string | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const config = readConfig();
|
|
62
|
+
if ("taskOfferPolicy" in config) {
|
|
63
|
+
const value = config.taskOfferPolicy;
|
|
64
|
+
if (value !== "always" && value !== "github-remote" && value !== "never") {
|
|
65
|
+
throw new Error(`taskOfferPolicy in ${join(getAgentDir(), "pi-note.json")} must be always, github-remote, or never.`);
|
|
66
|
+
}
|
|
67
|
+
policy = value;
|
|
68
|
+
}
|
|
69
|
+
} catch (cause) {
|
|
70
|
+
policy = "never";
|
|
71
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
72
|
+
}
|
|
73
|
+
return { guidelines: [`${offerGuidance[policy]} ${consentGuidance}`, tagGuidance], error };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handoffPrompt(): string {
|
|
77
|
+
const config = readConfig();
|
|
78
|
+
const path = join(getAgentDir(), "pi-note.json");
|
|
79
|
+
if (!("handoffPrompt" in config)) return defaultHandoffPrompt;
|
|
80
|
+
if (!text(config.handoffPrompt) || !config.handoffPrompt.trim()) {
|
|
81
|
+
throw new Error(`handoffPrompt in ${path} must be a nonblank string without control characters (except tabs/newlines).`);
|
|
82
|
+
}
|
|
83
|
+
return config.handoffPrompt;
|
|
84
|
+
}
|
|
85
|
+
|
|
37
86
|
function parse(raw: string): unknown {
|
|
38
87
|
try { return JSON.parse(raw); } catch { throw new Error(compatible); }
|
|
39
88
|
}
|
|
@@ -69,6 +118,7 @@ function toolResult(value: Task | null) {
|
|
|
69
118
|
}
|
|
70
119
|
|
|
71
120
|
export default function (pi: ExtensionAPI) {
|
|
121
|
+
const { guidelines: createGuidance, error: offerConfigError } = creationGuidance();
|
|
72
122
|
let alive = true;
|
|
73
123
|
let epoch = 0;
|
|
74
124
|
let refreshSerial = 0;
|
|
@@ -227,6 +277,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
227
277
|
const generation = epoch;
|
|
228
278
|
cliState = undefined;
|
|
229
279
|
if (ctx.hasUI && ctx.mode === "tui") {
|
|
280
|
+
if (offerConfigError) ctx.ui.notify(`Pinote task offers disabled: ${clean(offerConfigError)} Fix the configuration and /reload.`, "warning");
|
|
230
281
|
ctx.ui.addAutocompleteProvider((current) => cliMenu(current, () => setupAbort ? undefined : cliState));
|
|
231
282
|
}
|
|
232
283
|
// Keep the setup lock until its aborted subprocess has actually settled.
|
|
@@ -333,6 +384,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
333
384
|
if (currentSession()) ctx.ui.notify(`Pinote #${current.id} completed.`, "info");
|
|
334
385
|
return;
|
|
335
386
|
}
|
|
387
|
+
const handoff = handoffPrompt();
|
|
336
388
|
let id: number;
|
|
337
389
|
if (action === "Continue" && current) {
|
|
338
390
|
id = current.id;
|
|
@@ -349,7 +401,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
349
401
|
const chosen = await startTask(id, canAct);
|
|
350
402
|
if (!chosen || !canAct()) return;
|
|
351
403
|
const draft = ctx.ui.getEditorText();
|
|
352
|
-
const handoff = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
|
|
353
404
|
ctx.ui.setEditorText(draft ? `${draft}\n\n${handoff}` : handoff);
|
|
354
405
|
ctx.ui.notify(`Pinote #${chosen.id} is in progress. Task added to input.`, "info");
|
|
355
406
|
} catch (error) {
|