@agent-plan/core 0.3.0 → 0.3.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.
@@ -1,24 +1,27 @@
1
1
  /**
2
- * Harness-agnostic decision logic for the "no task in progress" edit guard.
3
- * Any adapter (Claude Code PreToolUse hook, Pi's advisory guard, a future
4
- * harness) can feed it a parsed tool-use event plus already-fetched planner
5
- * state and get back the same decision. This never talks to the plan store
2
+ * Harness-agnostic logic for the "no task in progress" warning. The guard
3
+ * never blocks and never asks: every tool call proceeds, and a call that
4
+ * changes project code while no task is in progress only earns the agent a
5
+ * warning. Planner operations never need a task. Any adapter (Claude Code
6
+ * PreToolUse hook, Pi's tool_call handler, a future harness) feeds it a
7
+ * parsed tool-use event plus already-fetched planner state and gets back the
8
+ * same warning, or none. This never talks to the plan store
6
9
  * or stdin/stdout — callers own that I/O. It does read the filesystem in one
7
10
  * narrow way (see {@link canonicalize}): resolving symlinks so a path
8
11
  * comparison isn't fooled by one, which is cheap (a handful of stat calls,
9
12
  * not a plan load) and safe to call on paths that don't exist yet.
10
13
  */
11
- export type GuardPermissionDecision = "allow" | "ask" | "deny";
12
14
  /** The tool-use fields the decision needs, already pulled out of whatever
13
15
  * shape the harness's raw event uses (Claude Code's `tool_input.file_path`,
14
16
  * Pi's `event.input.path`, etc.). */
15
17
  export interface GuardEventInput {
18
+ /** Tool name in any case: Claude Code's `Edit`/`Bash` and Pi's `edit`/`bash` are the same tool. */
16
19
  toolName: string;
17
20
  /** Absolute (or resolvable) working directory used to resolve relative paths. */
18
21
  cwd: string;
19
22
  /** Absolute planner root, e.g. `join(cwd, ".planner")`. */
20
23
  plannerRoot: string;
21
- /** Edit / Write target path. */
24
+ /** Edit / Write target path (Pi: `path`). */
22
25
  filePath?: string;
23
26
  /** NotebookEdit target path. */
24
27
  notebookPath?: string;
@@ -31,36 +34,39 @@ export interface GuardStateInput {
31
34
  totalTasks: number;
32
35
  hasInProgressTask: boolean;
33
36
  guardBypassed: boolean;
34
- /** Human-readable "start this task" hint, or the generic fallback; empty allowed. */
35
- startHint: string;
37
+ /** The task the warning suggests starting, when there is an obvious one. */
38
+ focusTask?: {
39
+ id: string;
40
+ title: string;
41
+ };
36
42
  }
37
43
  export interface GuardClassification {
38
- /** Whether this tool call is one the guard evaluates at all. */
44
+ /** Whether this tool call may write project code, so the guard must look at task state. */
39
45
  guarded: boolean;
40
- /** Best-effort candidate paths the call would write to (may be empty when unknown). */
46
+ /** The project paths outside `.planner/` the call would write to; empty when the target is unknown. */
41
47
  paths: string[];
42
48
  }
43
- export interface GuardDecision {
44
- decision: GuardPermissionDecision;
45
- reason?: string;
49
+ export interface NoTaskWarning {
50
+ /** Text for the agent, present only when it should be warned. */
51
+ warning?: string;
46
52
  }
47
53
  /**
48
54
  * Whether a Bash command line contains a write-shaped operation: output
49
55
  * redirection, `tee`, in-place `sed`/`perl`, a file-creating `cp`/`mv`,
50
56
  * `rm`, or a `git` operation that rewrites tracked files. Deliberately
51
57
  * over-inclusive: `git`, build/test runners, `ls`, `cat`, `grep`, `find`
52
- * and friends must stay unmatched, but when in doubt this returns true — the
53
- * cost of a false positive here is one extra prompt, not a blocked
54
- * operation, so under-matching (missing a real edit) is the worse failure.
58
+ * and friends must stay unmatched, but when in doubt this returns true — a
59
+ * false positive costs one warning to the agent, never a blocked call or a
60
+ * prompt, so under-matching (missing a real edit) is the worse failure.
55
61
  */
56
62
  export declare function isWriteShapedBashCommand(command: string): boolean;
57
63
  /**
58
64
  * Best-effort extraction of the file path(s) a write-shaped Bash segment
59
- * targets, so a command that only touches `.planner/` can still be
60
- * recognized and allowed. This cannot see through a heredoc whose target is
61
- * computed (a variable, command substitution, etc.) rather than written
62
- * literally in the command line — that case is expected to fall through to
63
- * the normal in-progress/bypass checks below, not to be silently allowed.
65
+ * targets, so a command that only touches `.planner/` or files outside the
66
+ * project earns no warning. This cannot see through a heredoc whose target
67
+ * is computed (a variable, command substitution, etc.) rather than written
68
+ * literally in the command line — that case still reaches the in-progress /
69
+ * bypass checks and can warn, rather than being silently treated as safe.
64
70
  *
65
71
  * A wider gap of the same kind: a command that writes through an
66
72
  * interpreter (`python3 - <<PY` then `open(path, "w")`, `node -e`, a script
@@ -87,32 +93,36 @@ export declare function isWriteShapedBashCommand(command: string): boolean;
87
93
  export declare function extractBashWriteTargets(command: string): string[];
88
94
  /** Whether `candidate` resolves to a path inside `plannerRoot` (or is it exactly). */
89
95
  export declare function isPathInsidePlannerRoot(candidate: string, plannerRoot: string, cwd: string): boolean;
90
- /** True only when every candidate path is inside the planner root, and there
91
- * is at least one candidate — an empty/unknown path list must never count as
92
- * "inside", or an unresolvable target would be silently allowed. Exported so
93
- * a hook harness can use the exact same check for its cheap early-allow path
94
- * without duplicating the rule. */
95
- export declare function isEntirelyInsidePlannerRoot(paths: string[], plannerRoot: string, cwd: string): boolean;
96
+ /** Whether writing `candidate` changes the project's code: it is inside the
97
+ * project (the directory that owns `.planner/`) and not inside `.planner/`.
98
+ * `/dev/null`, `~/.claude/...`, temp dirs and other repositories are not. */
99
+ export declare function isProjectCodePath(candidate: string, plannerRoot: string, cwd: string): boolean;
96
100
  /**
97
- * Which tool calls the guard covers, and what path(s) they would write to.
98
- * Everything else (Read, Grep, WebFetch, MCP tools, read-only Bash, ...) is
99
- * unguarded and returns `{ guarded: false }` without touching the planner
100
- * store.
101
+ * Whether a tool call may change the project's code, and so needs the guard
102
+ * to look at task state. This is the one place that rule lives; the hook's
103
+ * cheap early exit and {@link noTaskWarning} both read `guarded`.
104
+ *
105
+ * - Tools that never write (Read, Grep, MCP tools, read-only Bash, ...) are
106
+ * not guarded.
107
+ * - A write whose targets are all known and none is project code (inside
108
+ * `.planner/`, `/dev/null`, `~/.claude/...`, a temp dir, another repo) is
109
+ * not guarded: updating the planner or the user's own config never needs a
110
+ * task.
111
+ * - A write with at least one project-code target is guarded, with `paths`
112
+ * narrowed to those targets.
113
+ * - A write whose target is unknown (a computed heredoc, an Edit with no
114
+ * path) is guarded with empty `paths` — never silently allowed.
101
115
  */
102
116
  export declare function classifyGuardedTool(event: GuardEventInput): GuardClassification;
103
117
  /**
104
- * The full "no task in progress" guard decision, as a pure function: given a
105
- * parsed tool-use event and planner state the caller already fetched, decide
106
- * allow or ask (deny is part of the type for a future genuinely-deny case,
107
- * but nothing here returns it today). No stdin/stdout and no plan-store
108
- * calls — the hook harness (or a Pi equivalent) does the reading and
109
- * printing around this; the only filesystem access is the cheap symlink
110
- * canonicalization in {@link isPathInsidePlannerRoot}.
118
+ * The warning for a tool call that changes project code while no task is in
119
+ * progress, or none. Never a block or a prompt: the adapter lets the call
120
+ * proceed either way and only passes the text to the agent. Pure apart from
121
+ * the cheap symlink canonicalization in {@link isPathInsidePlannerRoot}.
111
122
  *
112
- * Order matters and mirrors the cost of finding out: anything the guard
113
- * doesn't cover, and anything that resolves entirely inside the planner
114
- * root, is decided from the event alone (no planner state needed). Only a
115
- * real candidate for blocking reaches the state-based checks.
123
+ * Order mirrors the cost of finding out: anything that cannot change project
124
+ * code (see {@link classifyGuardedTool}) is settled from the event alone, so
125
+ * adapters can skip loading the plan for it.
116
126
  */
117
- export declare function decideGuardPreToolUse(event: GuardEventInput, state: GuardStateInput): GuardDecision;
127
+ export declare function noTaskWarning(event: GuardEventInput, state: GuardStateInput): NoTaskWarning;
118
128
  //# sourceMappingURL=guard-decision.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guard-decision.d.ts","sourceRoot":"","sources":["../src/guard-decision.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AAEH,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE/D;;qCAEqC;AACrC,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAC;IACjB,uFAAuF;IACvF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,uBAAuB,CAAC;IAClC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAsBD,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAKpG;AAED;;;;mCAImC;AACnC,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAa/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,aAAa,CAkBnG"}
1
+ {"version":3,"file":"guard-decision.d.ts","sourceRoot":"","sources":["../src/guard-decision.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AAEH;;qCAEqC;AACrC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,aAAa,EAAE,OAAO,CAAC;IACvB,4EAA4E;IAC5E,SAAS,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,OAAO,EAAE,OAAO,CAAC;IACjB,uGAAuG;IACvG,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiFD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA2BjE;AAuCD,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpG;AAED;;6EAE6E;AAC7E,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAE9F;AAoBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAM/E;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,GAAG,aAAa,CAa3F"}
@@ -1,6 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
- const GUARDED_FILE_TOOLS = new Set(["Edit", "Write"]);
4
+ const FILE_WRITE_TOOLS = new Set(["edit", "write"]);
4
5
  /** Strip fd-duplication idioms (`2>&1`, `>&2`, `1>&2`) before segmenting or
5
6
  * matching — they duplicate a stream, not a file, are extremely common in
6
7
  * otherwise read-only commands, and their bare `&` would otherwise be
@@ -8,11 +9,53 @@ const GUARDED_FILE_TOOLS = new Set(["Edit", "Write"]);
8
9
  function stripFdDuplication(command) {
9
10
  return command.replace(/\d*>&\d+/g, " ");
10
11
  }
12
+ /** Blank out shell operators (`>`, `<`, `|`, `;`, `&`, newlines) that sit
13
+ * inside single or double quotes. They are literal text there — a commit
14
+ * message's `<name@example.com>`, a `grep "a|b"` pattern — not redirects or
15
+ * separators. Quoted paths keep their characters, so `> "/tmp/x"` still
16
+ * reads as a redirect to `/tmp/x`. */
17
+ function maskQuotedOperators(command) {
18
+ let out = "";
19
+ let quote = null;
20
+ for (let i = 0; i < command.length; i += 1) {
21
+ const ch = command[i];
22
+ if (quote === null) {
23
+ if (ch === "\\") {
24
+ out += ch + (command[i + 1] ?? "");
25
+ i += 1;
26
+ continue;
27
+ }
28
+ if (ch === "'" || ch === '"')
29
+ quote = ch;
30
+ out += ch;
31
+ continue;
32
+ }
33
+ if (quote === '"' && ch === "\\") {
34
+ out += "__";
35
+ i += 1;
36
+ continue;
37
+ }
38
+ if (ch === quote) {
39
+ quote = null;
40
+ out += ch;
41
+ continue;
42
+ }
43
+ out += /[<>|;&\n]/.test(ch) ? "_" : ch;
44
+ }
45
+ return out;
46
+ }
47
+ /** The command line as write-detection reads it: quoted operators masked,
48
+ * then fd-duplication removed. Both detection and target extraction use
49
+ * this, so they always see the same text. */
50
+ function normalizeBashCommand(command) {
51
+ return stripFdDuplication(maskQuotedOperators(command));
52
+ }
11
53
  /** Split a Bash command line into pipe/list segments so write-detection on
12
54
  * one segment (e.g. `sed -i` on the left of a pipe) doesn't get confused by
13
55
  * an unrelated flag on the other side (e.g. `grep -i` on the right). Best
14
- * effort only — quoting and nested subshells are not fully parsed. Callers
15
- * must strip fd-duplication first, or a bare `2>&1` splits on its `&`. */
56
+ * effort only — nested subshells are not parsed. Callers must pass the
57
+ * command through {@link normalizeBashCommand} first, or a bare `2>&1`
58
+ * splits on its `&` and a quoted `|` splits a string in two. */
16
59
  function bashSegments(command) {
17
60
  return command
18
61
  .split(/(?:&&|\|\||[;&|\n])+/)
@@ -42,22 +85,22 @@ function isWriteShapedBashSegment(segment) {
42
85
  * redirection, `tee`, in-place `sed`/`perl`, a file-creating `cp`/`mv`,
43
86
  * `rm`, or a `git` operation that rewrites tracked files. Deliberately
44
87
  * over-inclusive: `git`, build/test runners, `ls`, `cat`, `grep`, `find`
45
- * and friends must stay unmatched, but when in doubt this returns true — the
46
- * cost of a false positive here is one extra prompt, not a blocked
47
- * operation, so under-matching (missing a real edit) is the worse failure.
88
+ * and friends must stay unmatched, but when in doubt this returns true — a
89
+ * false positive costs one warning to the agent, never a blocked call or a
90
+ * prompt, so under-matching (missing a real edit) is the worse failure.
48
91
  */
49
92
  export function isWriteShapedBashCommand(command) {
50
93
  if (!command || !command.trim())
51
94
  return false;
52
- return bashSegments(stripFdDuplication(command)).some(isWriteShapedBashSegment);
95
+ return bashSegments(normalizeBashCommand(command)).some(isWriteShapedBashSegment);
53
96
  }
54
97
  /**
55
98
  * Best-effort extraction of the file path(s) a write-shaped Bash segment
56
- * targets, so a command that only touches `.planner/` can still be
57
- * recognized and allowed. This cannot see through a heredoc whose target is
58
- * computed (a variable, command substitution, etc.) rather than written
59
- * literally in the command line — that case is expected to fall through to
60
- * the normal in-progress/bypass checks below, not to be silently allowed.
99
+ * targets, so a command that only touches `.planner/` or files outside the
100
+ * project earns no warning. This cannot see through a heredoc whose target
101
+ * is computed (a variable, command substitution, etc.) rather than written
102
+ * literally in the command line — that case still reaches the in-progress /
103
+ * bypass checks and can warn, rather than being silently treated as safe.
61
104
  *
62
105
  * A wider gap of the same kind: a command that writes through an
63
106
  * interpreter (`python3 - <<PY` then `open(path, "w")`, `node -e`, a script
@@ -83,7 +126,7 @@ export function isWriteShapedBashCommand(command) {
83
126
  */
84
127
  export function extractBashWriteTargets(command) {
85
128
  const targets = [];
86
- for (const segment of bashSegments(stripFdDuplication(command))) {
129
+ for (const segment of bashSegments(normalizeBashCommand(command))) {
87
130
  const redirect = segment.match(/>{1,2}\s*([^\s;&|>]+)/);
88
131
  if (redirect?.[1])
89
132
  targets.push(redirect[1]);
@@ -130,74 +173,97 @@ function canonicalize(path) {
130
173
  return join(canonicalize(parent), basename(path));
131
174
  }
132
175
  }
133
- /** Whether `candidate` resolves to a path inside `plannerRoot` (or is it exactly). */
134
- export function isPathInsidePlannerRoot(candidate, plannerRoot, cwd) {
135
- const resolvedRoot = canonicalize(resolve(plannerRoot));
136
- const resolvedCandidate = canonicalize(resolve(cwd, candidate));
176
+ /** Turn a path as written in a command line into one `resolve` understands:
177
+ * drop surrounding quotes and expand a leading `~` or `$HOME`. Anything else
178
+ * the shell would expand (other variables, command substitution) is left
179
+ * as-is, so it resolves inside the project and the agent is still warned. */
180
+ function shellPathToFsPath(candidate) {
181
+ const unquoted = candidate.replace(/^['"]|['"]$/g, "");
182
+ return unquoted.replace(/^(?:~|\$HOME|\$\{HOME\})(?=\/|$)/, homedir());
183
+ }
184
+ /** Whether `candidate` resolves to `root` itself or a path inside it. */
185
+ function isPathInside(candidate, root, cwd) {
186
+ const resolvedRoot = canonicalize(resolve(root));
187
+ const resolvedCandidate = canonicalize(resolve(cwd, shellPathToFsPath(candidate)));
137
188
  const rel = relative(resolvedRoot, resolvedCandidate);
138
189
  return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
139
190
  }
140
- /** True only when every candidate path is inside the planner root, and there
141
- * is at least one candidate — an empty/unknown path list must never count as
142
- * "inside", or an unresolvable target would be silently allowed. Exported so
143
- * a hook harness can use the exact same check for its cheap early-allow path
144
- * without duplicating the rule. */
145
- export function isEntirelyInsidePlannerRoot(paths, plannerRoot, cwd) {
146
- return paths.length > 0 && paths.every((path) => isPathInsidePlannerRoot(path, plannerRoot, cwd));
191
+ /** Whether `candidate` resolves to a path inside `plannerRoot` (or is it exactly). */
192
+ export function isPathInsidePlannerRoot(candidate, plannerRoot, cwd) {
193
+ return isPathInside(candidate, plannerRoot, cwd);
147
194
  }
148
- /**
149
- * Which tool calls the guard covers, and what path(s) they would write to.
150
- * Everything else (Read, Grep, WebFetch, MCP tools, read-only Bash, ...) is
151
- * unguarded and returns `{ guarded: false }` without touching the planner
152
- * store.
153
- */
154
- export function classifyGuardedTool(event) {
155
- if (GUARDED_FILE_TOOLS.has(event.toolName)) {
156
- return { guarded: true, paths: event.filePath ? [event.filePath] : [] };
195
+ /** Whether writing `candidate` changes the project's code: it is inside the
196
+ * project (the directory that owns `.planner/`) and not inside `.planner/`.
197
+ * `/dev/null`, `~/.claude/...`, temp dirs and other repositories are not. */
198
+ export function isProjectCodePath(candidate, plannerRoot, cwd) {
199
+ return isPathInside(candidate, dirname(plannerRoot), cwd) && !isPathInside(candidate, plannerRoot, cwd);
200
+ }
201
+ /** Which paths a covered tool call would write to, before any filtering;
202
+ * empty when the target cannot be read from the event. */
203
+ function rawWriteTargets(event) {
204
+ const tool = event.toolName.toLowerCase();
205
+ if (FILE_WRITE_TOOLS.has(tool)) {
206
+ return { covered: true, paths: event.filePath ? [event.filePath] : [] };
157
207
  }
158
- if (event.toolName === "NotebookEdit") {
159
- return { guarded: true, paths: event.notebookPath ? [event.notebookPath] : [] };
208
+ if (tool === "notebookedit") {
209
+ return { covered: true, paths: event.notebookPath ? [event.notebookPath] : [] };
160
210
  }
161
- if (event.toolName === "Bash") {
211
+ if (tool === "bash") {
162
212
  const command = event.command ?? "";
163
213
  if (!isWriteShapedBashCommand(command))
164
- return { guarded: false, paths: [] };
165
- return { guarded: true, paths: extractBashWriteTargets(command) };
214
+ return { covered: false, paths: [] };
215
+ return { covered: true, paths: extractBashWriteTargets(command) };
166
216
  }
167
- return { guarded: false, paths: [] };
217
+ return { covered: false, paths: [] };
218
+ }
219
+ /**
220
+ * Whether a tool call may change the project's code, and so needs the guard
221
+ * to look at task state. This is the one place that rule lives; the hook's
222
+ * cheap early exit and {@link noTaskWarning} both read `guarded`.
223
+ *
224
+ * - Tools that never write (Read, Grep, MCP tools, read-only Bash, ...) are
225
+ * not guarded.
226
+ * - A write whose targets are all known and none is project code (inside
227
+ * `.planner/`, `/dev/null`, `~/.claude/...`, a temp dir, another repo) is
228
+ * not guarded: updating the planner or the user's own config never needs a
229
+ * task.
230
+ * - A write with at least one project-code target is guarded, with `paths`
231
+ * narrowed to those targets.
232
+ * - A write whose target is unknown (a computed heredoc, an Edit with no
233
+ * path) is guarded with empty `paths` — never silently allowed.
234
+ */
235
+ export function classifyGuardedTool(event) {
236
+ const { covered, paths } = rawWriteTargets(event);
237
+ if (!covered)
238
+ return { guarded: false, paths: [] };
239
+ if (paths.length === 0)
240
+ return { guarded: true, paths: [] };
241
+ const projectPaths = paths.filter((path) => isProjectCodePath(path, event.plannerRoot, event.cwd));
242
+ return { guarded: projectPaths.length > 0, paths: projectPaths };
168
243
  }
169
244
  /**
170
- * The full "no task in progress" guard decision, as a pure function: given a
171
- * parsed tool-use event and planner state the caller already fetched, decide
172
- * allow or ask (deny is part of the type for a future genuinely-deny case,
173
- * but nothing here returns it today). No stdin/stdout and no plan-store
174
- * calls — the hook harness (or a Pi equivalent) does the reading and
175
- * printing around this; the only filesystem access is the cheap symlink
176
- * canonicalization in {@link isPathInsidePlannerRoot}.
245
+ * The warning for a tool call that changes project code while no task is in
246
+ * progress, or none. Never a block or a prompt: the adapter lets the call
247
+ * proceed either way and only passes the text to the agent. Pure apart from
248
+ * the cheap symlink canonicalization in {@link isPathInsidePlannerRoot}.
177
249
  *
178
- * Order matters and mirrors the cost of finding out: anything the guard
179
- * doesn't cover, and anything that resolves entirely inside the planner
180
- * root, is decided from the event alone (no planner state needed). Only a
181
- * real candidate for blocking reaches the state-based checks.
250
+ * Order mirrors the cost of finding out: anything that cannot change project
251
+ * code (see {@link classifyGuardedTool}) is settled from the event alone, so
252
+ * adapters can skip loading the plan for it.
182
253
  */
183
- export function decideGuardPreToolUse(event, state) {
254
+ export function noTaskWarning(event, state) {
184
255
  const classification = classifyGuardedTool(event);
185
256
  if (!classification.guarded)
186
- return { decision: "allow" };
187
- // Writing a handoff or recording task state is how an agent reports what
188
- // it did, and it is most needed exactly when no task is in progress. Never
189
- // block or ask for anything inside .planner/, regardless of task state.
190
- if (isEntirelyInsidePlannerRoot(classification.paths, event.plannerRoot, event.cwd)) {
191
- return { decision: "allow" };
192
- }
193
- if (!state.hasPlannerDir)
194
- return { decision: "allow" };
195
- if (state.totalTasks === 0)
196
- return { decision: "allow" };
197
- if (state.hasInProgressTask)
198
- return { decision: "allow" };
199
- if (state.guardBypassed)
200
- return { decision: "allow" };
201
- const reason = `Agent Plan guard: no task is in-progress, and this ${event.toolName} touches a file outside .planner/.${state.startHint} Or authorize a temporary bypass so this and further edits this session don't ask again (run /planner bypass, or call planner-authorize-bypass) — same bypass window an in-progress task already skips.`;
202
- return { decision: "ask", reason };
257
+ return {};
258
+ if (!state.hasPlannerDir || state.totalTasks === 0)
259
+ return {};
260
+ if (state.hasInProgressTask || state.guardBypassed)
261
+ return {};
262
+ const target = classification.paths.length > 0 ? classification.paths.join(", ") : "a project file";
263
+ const start = state.focusTask
264
+ ? `start the task that covers it (/planner task start ${state.focusTask.id} — ${state.focusTask.title}, or another one)`
265
+ : "start the task that covers it (/planner task start <task>)";
266
+ return {
267
+ warning: `Agent Plan: no task is in progress, and this call changes project code (${target}). It was not blocked. If this is planned work, ${start} so the plan stays accurate. Planner changes never need a task.`,
268
+ };
203
269
  }
@@ -0,0 +1,14 @@
1
+ import type { PlanStore } from "./plan-store.js";
2
+ import type { GuardStateInput } from "./guard-decision.js";
3
+ export interface NoTaskGuardState extends GuardStateInput {
4
+ inProgressTaskIds: string[];
5
+ }
6
+ /**
7
+ * Read the planner state the no-task warning needs, once, for any adapter.
8
+ * The suggested task is the one the plan is already pointing at: the
9
+ * resume file's in-progress task, then an open task in the current phase,
10
+ * then a paused task, then the first open task. Never throws for a missing
11
+ * planner; it reports `hasPlannerDir: false` instead.
12
+ */
13
+ export declare function loadNoTaskGuardState(st: PlanStore): Promise<NoTaskGuardState>;
14
+ //# sourceMappingURL=guard-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard-state.d.ts","sourceRoot":"","sources":["../src/guard-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAID;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4BnF"}
@@ -0,0 +1,35 @@
1
+ const CLOSED_STATUSES = new Set(["done", "canceled", "rejected"]);
2
+ /**
3
+ * Read the planner state the no-task warning needs, once, for any adapter.
4
+ * The suggested task is the one the plan is already pointing at: the
5
+ * resume file's in-progress task, then an open task in the current phase,
6
+ * then a paused task, then the first open task. Never throws for a missing
7
+ * planner; it reports `hasPlannerDir: false` instead.
8
+ */
9
+ export async function loadNoTaskGuardState(st) {
10
+ const hasPlannerDir = await st.exists().catch(() => false);
11
+ if (!hasPlannerDir) {
12
+ return { hasPlannerDir, totalTasks: 0, hasInProgressTask: false, inProgressTaskIds: [], guardBypassed: false };
13
+ }
14
+ const [workspace, resume, guardBypassed] = await Promise.all([
15
+ st.loadAll(),
16
+ st.loadResume().catch(() => null),
17
+ st.isGuardBypassed().catch(() => false),
18
+ ]);
19
+ const allTasks = workspace.phases.flatMap((phase) => phase.tasks.map((task) => ({ phase, task })));
20
+ const inProgressTaskIds = allTasks.filter(({ task }) => task.status === "in-progress").map(({ task }) => task.id);
21
+ const isOpen = (status) => !CLOSED_STATUSES.has(status);
22
+ const focus = (resume?.inProgressTaskIds?.[0] ? allTasks.find(({ task }) => task.id === resume.inProgressTaskIds[0]) : undefined)
23
+ ?? (resume?.currentPhaseId ? allTasks.find(({ phase, task }) => phase.id === resume.currentPhaseId && isOpen(task.status)) : undefined)
24
+ ?? allTasks.find(({ task }) => task.pauseSnapshot)
25
+ ?? allTasks.find(({ task }) => task.status === "planned" || task.status === "blocked" || task.status === "waiting")
26
+ ?? allTasks.find(({ task }) => isOpen(task.status));
27
+ return {
28
+ hasPlannerDir,
29
+ totalTasks: allTasks.length,
30
+ hasInProgressTask: inProgressTaskIds.length > 0,
31
+ inProgressTaskIds,
32
+ guardBypassed,
33
+ ...(focus ? { focusTask: { id: focus.task.id, title: focus.task.title } } : {}),
34
+ };
35
+ }
@@ -11,6 +11,11 @@
11
11
  * per-field comments) rather than by a single blanket rule — see AGENTS.md
12
12
  * rule 4: this placement decision belongs in the core exactly once, not
13
13
  * once per adapter.
14
+ *
15
+ * Transport note: Claude Code shows the agent only structuredContent when a
16
+ * result has one, so the MCP server folds `text` into the structured part
17
+ * for every tool (foldTextIntoStructured in plan-mcp). The rule above still
18
+ * holds for what these builders return; the fold is the MCP transport's job.
14
19
  */
15
20
  import type { HandoffCompletenessAudit, Phase } from "./schema.js";
16
21
  import type { PhaseWorkMap } from "./task-context.js";
@@ -1 +1 @@
1
- {"version":3,"file":"handoff-reply.d.ts","sourceRoot":"","sources":["../src/handoff-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAA6B,KAAK,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,oBAAoB,EAAE,MAAM,GAAE,SAAS,GAAG,aAAyB,GAAG,aAAa,CAMrI;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;oBAMoB;AACpB,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAKjF;AA8CD;6CAC6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAmCD,MAAM,MAAM,qBAAqB,GAC7B;IACE,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,YAAY,CAsEhF;AAED;wDACwD;AACxD,eAAO,MAAM,iCAAiC,0IAC2F,CAAC;AAE1I,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,wBAAwB,GAAG,YAAY,CAkFtF"}
1
+ {"version":3,"file":"handoff-reply.d.ts","sourceRoot":"","sources":["../src/handoff-reply.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAA6B,KAAK,EAAE,MAAM,aAAa,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,oBAAoB,EAAE,MAAM,GAAE,SAAS,GAAG,aAAyB,GAAG,aAAa,CAMrI;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;oBAMoB;AACpB,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAKjF;AA8CD;6CAC6C;AAC7C,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAmCD,MAAM,MAAM,qBAAqB,GAC7B;IACE,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,wBAAwB,GAAG,IAAI,CAAC;CAC/C,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;CAC5B,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,YAAY,CAsEhF;AAED;wDACwD;AACxD,eAAO,MAAM,iCAAiC,0IAC2F,CAAC;AAE1I,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,wBAAwB,GAAG,YAAY,CAkFtF"}
package/dist/index.d.ts CHANGED
@@ -27,11 +27,14 @@ export * from "./feature-delete.js";
27
27
  export * from "./accepted-decision-guard.js";
28
28
  export * from "./project-context-migration.js";
29
29
  export * from "./project-context.js";
30
+ export * from "./load-reply.js";
31
+ export * from "./on-hold.js";
30
32
  export * from "./requirement-macro-tasks.js";
31
33
  export * from "./package-version.js";
32
34
  export * from "./runtime-diagnostics.js";
33
35
  export * from "./write-coordination.js";
34
36
  export * from "./guard-decision.js";
37
+ export * from "./guard-state.js";
35
38
  export { PlanStore, PlanStoreError, PlanStaleWriteError, PlanUnsupportedAllocationKindError, assertPlannerRevision, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, nowISO, type IdeaCreateInput, type IdeaUpdateInput, type IdeaPromotionTargetInput, type AcceptedDecisionOwner, type AcceptedDecisionCreateInput, type AcceptedDecisionUpdateInput, type RecordProjectContextReadInput, type RecordProjectContextReadResult, type PhaseHandoffSummary, type OrphanPhaseSummary } from "./plan-store.js";
36
39
  export { PlanRenderer } from "./renderer.js";
37
40
  export { ExportService } from "./export-service.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,mBAAmB,EAAE,kCAAkC,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,KAAK,2BAA2B,EAAE,KAAK,2BAA2B,EAAE,KAAK,6BAA6B,EAAE,KAAK,8BAA8B,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC3hB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,YAAY,CAAC;AAC3B,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,sBAAsB,CAAC;AACrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC;AACpC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,8BAA8B,CAAC;AAC7C,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,mBAAmB,EAAE,kCAAkC,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,EAAE,KAAK,eAAe,EAAE,KAAK,eAAe,EAAE,KAAK,wBAAwB,EAAE,KAAK,qBAAqB,EAAE,KAAK,2BAA2B,EAAE,KAAK,2BAA2B,EAAE,KAAK,6BAA6B,EAAE,KAAK,8BAA8B,EAAE,KAAK,mBAAmB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC3hB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -27,11 +27,14 @@ export * from "./feature-delete.js";
27
27
  export * from "./accepted-decision-guard.js";
28
28
  export * from "./project-context-migration.js";
29
29
  export * from "./project-context.js";
30
+ export * from "./load-reply.js";
31
+ export * from "./on-hold.js";
30
32
  export * from "./requirement-macro-tasks.js";
31
33
  export * from "./package-version.js";
32
34
  export * from "./runtime-diagnostics.js";
33
35
  export * from "./write-coordination.js";
34
36
  export * from "./guard-decision.js";
37
+ export * from "./guard-state.js";
35
38
  export { PlanStore, PlanStoreError, PlanStaleWriteError, PlanUnsupportedAllocationKindError, assertPlannerRevision, setWriteBusyHook, setWriteNotifyHook, migrateToUuids, migrateToGlobalSequence, withFeatureLock, nowISO } from "./plan-store.js";
36
39
  export { PlanRenderer } from "./renderer.js";
37
40
  export { ExportService } from "./export-service.js";
@@ -0,0 +1,63 @@
1
+ import type { ProjectContextReply } from "./project-context.js";
2
+ import type { PlanWorkspace } from "./schema.js";
3
+ /**
4
+ * Reply shaping for an explicit planner load, shared by every adapter
5
+ * (MCP planner-load, Pi's planner-load tool and /planner load command).
6
+ *
7
+ * Every piece is sent once, in one text the agent reads top to bottom: the
8
+ * recap to show the user, then the agent-only context. `structured` carries
9
+ * only status fields; hosts that show the agent only structured content
10
+ * still get the text because the MCP server folds it in.
11
+ */
12
+ /** How the planner operating guide reaches the agent on this load. */
13
+ export type PlannerGuideDelivery =
14
+ /** The guide body is included (a host with no other way to deliver it). */
15
+ {
16
+ mode: "include";
17
+ content: string;
18
+ customized: boolean;
19
+ message: string;
20
+ }
21
+ /** The host already delivers the guide (e.g. Claude Code's /planner
22
+ * command); send only where it lives, never the body again. */
23
+ | {
24
+ mode: "pointer";
25
+ path: string;
26
+ customized: boolean;
27
+ message: string;
28
+ };
29
+ export interface PlannerLoadReplyInput {
30
+ recap: string;
31
+ projectContext: ProjectContextReply;
32
+ /** Accepted Decisions below project scope (feature/phase/task). Project
33
+ * decisions are already inside the project context. */
34
+ scopedDecisions?: {
35
+ content: string;
36
+ total: number;
37
+ truncated: boolean;
38
+ };
39
+ plannerGuide: PlannerGuideDelivery;
40
+ }
41
+ export interface PlannerLoadReply {
42
+ text: string;
43
+ structured: Record<string, unknown>;
44
+ }
45
+ export declare const PLANNER_LOAD_RECAP_HEADING = "## Recap \u2014 show this to the user verbatim";
46
+ export declare const PLANNER_LOAD_AGENT_HEADING = "## Agent-only context \u2014 read and keep it; never quote it in the recap";
47
+ /** Split a load reply's text back into the recap the user sees and the
48
+ * agent-only context. Any client (or test) that must show only the recap
49
+ * uses this instead of guessing at the layout. */
50
+ export declare function splitPlannerLoadText(text: string): {
51
+ recap: string;
52
+ agentContext: string;
53
+ };
54
+ export declare function buildPlannerLoadReply(input: PlannerLoadReplyInput): PlannerLoadReply;
55
+ /** Accepted Decisions owned by features, phases and tasks, bounded for a
56
+ * load reply. Project decisions are left out: the project context already
57
+ * carries them. */
58
+ export declare function buildScopedDecisionContext(plan: PlanWorkspace): {
59
+ content: string;
60
+ total: number;
61
+ truncated: boolean;
62
+ };
63
+ //# sourceMappingURL=load-reply.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-reply.d.ts","sourceRoot":"","sources":["../src/load-reply.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAIjD;;;;;;;;GAQG;AAEH,sEAAsE;AACtE,MAAM,MAAM,oBAAoB;AAC9B,2EAA2E;AACzE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC5E;+DAC+D;GAC7D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5E,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,mBAAmB,CAAC;IACpC;2DACuD;IACvD,eAAe,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IACzE,YAAY,EAAE,oBAAoB,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,eAAO,MAAM,0BAA0B,mDAA8C,CAAC;AACtF,eAAO,MAAM,0BAA0B,+EAA0E,CAAC;AAElH;;kDAEkD;AAClD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAK1F;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,qBAAqB,GAAG,gBAAgB,CAmCpF;AAED;;mBAEmB;AACnB,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAYtH"}