pi-note 0.8.0 → 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.
Files changed (3) hide show
  1. package/README.md +23 -9
  2. package/index.ts +36 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -81,20 +81,34 @@ worktrees and pull requests. Task status refreshes at session start, before/afte
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
- ### Handoff prompt configuration
84
+ ### Personal configuration
85
85
 
86
- Set `handoffPrompt` in `~/.pi/agent/pi-note.json` (or in the agent directory set
87
- by `PI_CODING_AGENT_DIR`):
86
+ Configure `~/.pi/agent/pi-note.json` (or the agent directory set by
87
+ `PI_CODING_AGENT_DIR`):
88
88
 
89
89
  ```json
90
90
  {
91
- "handoffPrompt": "Read the current Pinote task. Summarize your understanding and proposed approach, but don’t start work yet."
91
+ "handoffPrompt": "Read the current Pinote task. Summarize your understanding and proposed approach, but don’t start work yet.",
92
+ "taskOfferPolicy": "github-remote"
92
93
  }
93
94
  ```
94
95
 
95
- Both task selection and **Continue** read this file each time; no `/reload` is
96
- needed after edits. The value is literal text, not a prompt template; use `\n`
97
- in JSON strings for multiple lines. It must be a nonblank string without control
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
98
112
  characters other than tabs and newlines. A missing file or key uses the default.
99
113
  Invalid/unreadable configuration reports an error without starting/switching a
100
114
  task or changing the editor. **Done** and agent tools are unaffected. This is
@@ -117,8 +131,8 @@ Version 0.7.0 replaces `pinote_get`/`pinote_update` with these current-task tool
117
131
  and removes `pinote_tag`, without aliases. Agents cannot read/update tasks by ID
118
132
  or retag existing tasks; use `/pi-note` to select an existing task.
119
133
 
120
- When the user gives work and no task is selected, the agent proposes one note as
121
- `[tag] text` and asks before creating it. No continues without a note. Yes calls
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
122
136
  `pinote_add` with `select: true`. An existing selection is not replaced unless the
123
137
  user asks to switch. Reuse a saved tag name when it fits.
124
138
 
package/index.ts CHANGED
@@ -9,10 +9,13 @@ import { TaskPicker, taskState, taskTag } from "./task-picker.ts";
9
9
  import { bundledCLIVersion, cliMenu, detectCLI, setupCLI, setupHint, versionAtLeast, type CLIAction } from "./setup.ts";
10
10
 
11
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.";
12
- const createGuidance = [
13
- "When the user gives work and no pinote task is selected, propose one note as `[tag] text` and ask before creating it. 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.",
14
- "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
- ];
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.";
16
19
  type Task = {
17
20
  id: number;
18
21
  text: string;
@@ -36,18 +39,43 @@ const firstLine = (task: Summary) => clean(task.text.split("\n", 1)[0]).replace(
36
39
  const title = (task: Summary) => truncateToWidth(`#${task.id} ${firstLine(task)}`, 60);
37
40
 
38
41
  const defaultHandoffPrompt = "Read the current Pinote task. Summarize your understanding, but don’t start work yet.";
39
- function handoffPrompt(): string {
42
+ function readConfig(): Record<string, unknown> {
40
43
  const path = join(getAgentDir(), "pi-note.json");
41
44
  let raw: string;
42
45
  try {
43
46
  raw = readFileSync(path, "utf8");
44
47
  } catch (error) {
45
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return defaultHandoffPrompt;
48
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
46
49
  throw new Error(`Cannot read ${path}. Check the pi-note configuration.`);
47
50
  }
48
51
  let config: unknown;
49
52
  try { config = JSON.parse(raw); } catch { throw new Error(`Invalid JSON in ${path}.`); }
50
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");
51
79
  if (!("handoffPrompt" in config)) return defaultHandoffPrompt;
52
80
  if (!text(config.handoffPrompt) || !config.handoffPrompt.trim()) {
53
81
  throw new Error(`handoffPrompt in ${path} must be a nonblank string without control characters (except tabs/newlines).`);
@@ -90,6 +118,7 @@ function toolResult(value: Task | null) {
90
118
  }
91
119
 
92
120
  export default function (pi: ExtensionAPI) {
121
+ const { guidelines: createGuidance, error: offerConfigError } = creationGuidance();
93
122
  let alive = true;
94
123
  let epoch = 0;
95
124
  let refreshSerial = 0;
@@ -248,6 +277,7 @@ export default function (pi: ExtensionAPI) {
248
277
  const generation = epoch;
249
278
  cliState = undefined;
250
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");
251
281
  ctx.ui.addAutocompleteProvider((current) => cliMenu(current, () => setupAbort ? undefined : cliState));
252
282
  }
253
283
  // Keep the setup lock until its aborted subprocess has actually settled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-note",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Persistent pinote tasks, Markdown handoffs, and task status for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",