@agent-plan/core 0.2.28 → 0.3.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 (79) hide show
  1. package/dist/accepted-decision-guard.d.ts +94 -0
  2. package/dist/accepted-decision-guard.d.ts.map +1 -0
  3. package/dist/accepted-decision-guard.js +110 -0
  4. package/dist/checklist.d.ts +28 -0
  5. package/dist/checklist.d.ts.map +1 -1
  6. package/dist/checklist.js +59 -0
  7. package/dist/feature-delete.d.ts +62 -0
  8. package/dist/feature-delete.d.ts.map +1 -0
  9. package/dist/feature-delete.js +49 -0
  10. package/dist/guard-decision.d.ts +118 -0
  11. package/dist/guard-decision.d.ts.map +1 -0
  12. package/dist/guard-decision.js +203 -0
  13. package/dist/handoff-context.d.ts +130 -1
  14. package/dist/handoff-context.d.ts.map +1 -1
  15. package/dist/handoff-context.js +167 -9
  16. package/dist/handoff-reply.d.ts +108 -0
  17. package/dist/handoff-reply.d.ts.map +1 -0
  18. package/dist/handoff-reply.js +279 -0
  19. package/dist/index.d.ts +15 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +15 -1
  22. package/dist/mutation-reply.d.ts +105 -0
  23. package/dist/mutation-reply.d.ts.map +1 -0
  24. package/dist/mutation-reply.js +203 -0
  25. package/dist/naming.d.ts +1 -1
  26. package/dist/naming.d.ts.map +1 -1
  27. package/dist/naming.js +1 -1
  28. package/dist/pagination.d.ts +19 -0
  29. package/dist/pagination.d.ts.map +1 -0
  30. package/dist/pagination.js +15 -0
  31. package/dist/payload-fallback.js +1 -1
  32. package/dist/plan-store.d.ts +141 -8
  33. package/dist/plan-store.d.ts.map +1 -1
  34. package/dist/plan-store.js +459 -87
  35. package/dist/project-context-migration.d.ts +5 -0
  36. package/dist/project-context-migration.d.ts.map +1 -1
  37. package/dist/project-context-migration.js +13 -0
  38. package/dist/project-context.d.ts +395 -0
  39. package/dist/project-context.d.ts.map +1 -0
  40. package/dist/project-context.js +295 -0
  41. package/dist/recap.d.ts.map +1 -1
  42. package/dist/recap.js +27 -0
  43. package/dist/recommend-reply.d.ts +51 -0
  44. package/dist/recommend-reply.d.ts.map +1 -0
  45. package/dist/recommend-reply.js +84 -0
  46. package/dist/refs.d.ts +16 -0
  47. package/dist/refs.d.ts.map +1 -1
  48. package/dist/refs.js +38 -0
  49. package/dist/renderer.d.ts.map +1 -1
  50. package/dist/schema.d.ts +7 -2
  51. package/dist/schema.d.ts.map +1 -1
  52. package/dist/schema.js +8 -3
  53. package/dist/stranded-phase.d.ts +73 -0
  54. package/dist/stranded-phase.d.ts.map +1 -0
  55. package/dist/stranded-phase.js +93 -0
  56. package/dist/task-completion-gate.d.ts +84 -0
  57. package/dist/task-completion-gate.d.ts.map +1 -0
  58. package/dist/task-completion-gate.js +72 -0
  59. package/dist/task-context.d.ts +1 -0
  60. package/dist/task-context.d.ts.map +1 -1
  61. package/dist/task-context.js +31 -13
  62. package/dist/task-lifecycle.d.ts +12 -0
  63. package/dist/task-lifecycle.d.ts.map +1 -0
  64. package/dist/task-lifecycle.js +23 -0
  65. package/dist/task-order-context.d.ts +72 -0
  66. package/dist/task-order-context.d.ts.map +1 -0
  67. package/dist/task-order-context.js +82 -0
  68. package/dist/task-selection.d.ts.map +1 -1
  69. package/dist/task-selection.js +14 -12
  70. package/dist/task-start-outcome.d.ts +26 -0
  71. package/dist/task-start-outcome.d.ts.map +1 -1
  72. package/dist/task-start-outcome.js +23 -0
  73. package/dist/text-bounds.d.ts +32 -0
  74. package/dist/text-bounds.d.ts.map +1 -0
  75. package/dist/text-bounds.js +43 -0
  76. package/dist/write-coordination.d.ts +2 -2
  77. package/dist/write-coordination.js +2 -2
  78. package/package.json +1 -1
  79. package/planner-skill.md +30 -12
@@ -0,0 +1,82 @@
1
+ import { featureNumberOfPhase, formatPhaseRef } from "./naming.js";
2
+ const TERMINAL_STATUSES = ["done", "canceled", "rejected"];
3
+ function taskPriority(task) {
4
+ return task.priority ?? task.number;
5
+ }
6
+ function compareByPriority(left, right) {
7
+ return taskPriority(left) - taskPriority(right) || left.number - right.number;
8
+ }
9
+ function isTaskReady(task, statusById) {
10
+ if (task.status !== "planned")
11
+ return false;
12
+ return (task.dependsOn ?? []).every((id) => statusById.get(id) === "done");
13
+ }
14
+ /**
15
+ * Build the ordering context for one task. `allPhases` and `features` are
16
+ * needed (not just the task's own phase) because a dependency can name a
17
+ * task in a different phase (see PlanStore.addTaskDependency, which allows
18
+ * any project-wide target) and its composite ref/status must resolve
19
+ * project-wide, not just among siblings.
20
+ */
21
+ export function buildTaskOrderContext(task, phase, allPhases, features) {
22
+ const statusById = new Map();
23
+ const refById = new Map();
24
+ for (const candidatePhase of allPhases) {
25
+ const phaseRef = formatPhaseRef(candidatePhase.number, featureNumberOfPhase(candidatePhase, features));
26
+ for (const candidateTask of candidatePhase.tasks ?? []) {
27
+ statusById.set(candidateTask.id, candidateTask.status);
28
+ refById.set(candidateTask.id, `${phaseRef}/T${String(candidateTask.number).padStart(3, "0")}`);
29
+ }
30
+ }
31
+ const siblings = phase.tasks ?? [];
32
+ const readySiblings = siblings.filter((sibling) => isTaskReady(sibling, statusById)).sort(compareByPriority);
33
+ const ordersApply = !TERMINAL_STATUSES.includes(task.status);
34
+ const readyAheadCount = ordersApply
35
+ ? readySiblings.filter((sibling) => sibling.id !== task.id && compareByPriority(sibling, task) < 0).length
36
+ : 0;
37
+ const topReady = readySiblings[0];
38
+ const nextByPriorityRef = ordersApply && topReady && topReady.id !== task.id
39
+ ? (refById.get(topReady.id) ?? null)
40
+ : null;
41
+ const dependsOn = (task.dependsOn ?? []).map((id) => ({
42
+ ref: refById.get(id) ?? id,
43
+ taskId: id,
44
+ status: statusById.get(id) ?? "planned",
45
+ }));
46
+ return {
47
+ priority: taskPriority(task),
48
+ ordersApply,
49
+ readyCount: readySiblings.length,
50
+ readyAheadCount,
51
+ nextByPriorityRef,
52
+ dependsOn,
53
+ };
54
+ }
55
+ /** One line for the human-readable channel of the full task view: the exact
56
+ * shape both adapters render, so the fact is never assembled twice. Bounded
57
+ * by construction — a count, a priority number and a short dependency list,
58
+ * never the entities themselves. */
59
+ export function taskOrderContextLine(context, status) {
60
+ const depsText = context.dependsOn.length > 0
61
+ ? context.dependsOn.map((dependency) => `${dependency.ref} (${dependency.status})`).join(", ")
62
+ : "None";
63
+ if (!context.ordersApply) {
64
+ return `Priority ${context.priority} (${status}; ordering no longer applies). Depends on: ${depsText}.`;
65
+ }
66
+ const parts = [
67
+ `Priority ${context.priority}`,
68
+ `${context.readyCount} ready in phase`,
69
+ `${context.readyAheadCount} ready ahead of this one`,
70
+ ];
71
+ if (context.nextByPriorityRef)
72
+ parts.push(`next by priority: ${context.nextByPriorityRef}`);
73
+ return `${parts.join(" · ")}. Depends on: ${depsText}.`;
74
+ }
75
+ /** The fact a task-creation confirmation must state: where in the phase's
76
+ * priority order the planner placed the new task. Centralized so the two
77
+ * otherwise differently-worded adapter confirmations never diverge on how
78
+ * this fragment reads (see P104(F005)/T421 — eight tasks were created in
79
+ * this phase without a priority ever being shown). */
80
+ export function taskCreatedPriorityFragment(priority) {
81
+ return `priority ${priority}`;
82
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"task-selection.d.ts","sourceRoot":"","sources":["../src/task-selection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"task-selection.d.ts","sourceRoot":"","sources":["../src/task-selection.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- const unavailable = new Set(["blocked", "waiting", "deferred", "canceled", "rejected"]);
2
1
  const hardUnavailable = new Set(["blocked", "deferred", "canceled", "rejected"]);
3
2
  const terminal = new Set(["done", "canceled", "rejected"]);
4
3
  const priority = (entity) => entity.priority != null ? entity.priority : Number.MAX_SAFE_INTEGER;
@@ -116,11 +115,6 @@ export function checkExplicitTaskStart(features, phases, taskId, deviations = []
116
115
  return { eligible: false, reason: "Requested task no longer exists." };
117
116
  const isTemporaryOverride = deviations.some((deviation) => (deviation.state === "approved" || deviation.state === "active")
118
117
  && deviation.temporaryTaskId === taskId);
119
- const isPreservedResumeTarget = deviations.some((deviation) => (deviation.state === "approved"
120
- || deviation.state === "active"
121
- || deviation.state === "resume-required"
122
- || deviation.state === "resolved")
123
- && deviation.resumeTaskId === taskId);
124
118
  const startableStatus = candidate.task.status === "planned"
125
119
  || (candidate.task.status === "waiting" && isTemporaryOverride);
126
120
  if (!startableStatus)
@@ -160,18 +154,26 @@ export function recommendNextTask(features, phases, deviations = [], currentPhas
160
154
  const newestFirst = (left, right) => right.createdAt.localeCompare(left.createdAt);
161
155
  const resumable = (candidate) => candidate
162
156
  && (candidate.task.status === "planned" || candidate.task.status === "waiting");
163
- const open = deviations
157
+ // "resolved" is a legacy synonym for "resume-required": both mean the
158
+ // temporary work has ended and the preserved task must be returned to.
159
+ // No live code path writes "resolved" anymore (setWorkDeviationState only
160
+ // ever sets "resume-required" / "resumed" / "canceled"), but every reader
161
+ // of persisted deviations still treats the two states identically (see
162
+ // recap.ts, plan-mcp/src/index.ts, plan-server/src/serve.ts, and
163
+ // pi-adapter/src/index.ts). Naming the pair here keeps that equivalence
164
+ // explicit instead of two bare string literals that look like an
165
+ // accidental inclusion of a "closed" state in an "open" list.
166
+ const returnRequiredStates = new Set(["resume-required", "resolved"]);
167
+ const liveDeviations = deviations
164
168
  .filter((deviation) => deviation.state === "approved"
165
169
  || deviation.state === "active"
166
- || deviation.state === "resume-required"
167
- || deviation.state === "resolved")
170
+ || returnRequiredStates.has(deviation.state))
168
171
  .sort(newestFirst);
169
- const top = open[0];
172
+ const top = liveDeviations[0];
170
173
  if (top) {
171
174
  const temporary = byTaskId.get(top.temporaryTaskId);
172
175
  const resume = byTaskId.get(top.resumeTaskId);
173
- const returnIsRequired = top.state === "resume-required"
174
- || top.state === "resolved"
176
+ const returnIsRequired = returnRequiredStates.has(top.state)
175
177
  || (temporary && terminal.has(temporary.task.status));
176
178
  if (returnIsRequired && resume && resumable(resume)) {
177
179
  return { kind: "resume", candidate: resume, deviation: top, reason: "Resume required: return to the task preserved by the most recent deviation." };
@@ -1,3 +1,4 @@
1
+ import type { ContextReadEligibility, ProjectGuidelinesReadState, RequiredContextRead } from "./read-tracking.js";
1
2
  export declare const TASK_START_ERROR_CODES: readonly ["PLAN_NOT_FOUND", "TASK_NOT_FOUND", "TASK_DONE", "PROJECT_GUIDELINES_READ_REQUIRED", "CONTEXT_READ_REQUIRED", "REQUIREMENTS_READ_REQUIRED", "START_NOT_ALLOWED", "ACTIVE_TASK_CONFLICT", "PERSISTENCE_VERIFICATION_FAILED"];
2
3
  export type TaskStartErrorCode = (typeof TASK_START_ERROR_CODES)[number];
3
4
  export interface TaskStartDeniedOutcome {
@@ -20,4 +21,29 @@ export declare function taskStartDenied(errorCode: TaskStartErrorCode, message:
20
21
  requirementIds?: string[];
21
22
  }): TaskStartDeniedOutcome;
22
23
  export declare function taskStartSucceeded(taskId: string, alreadyStarted?: boolean): TaskStartSucceededOutcome;
24
+ /**
25
+ * Advisory-only readiness summary for `task_start` / `planner-task-start`,
26
+ * built from the exact same three checks the lifecycle gate itself runs
27
+ * (Project Guidelines, task/phase/feature context reads, linked
28
+ * requirements). `planner-task-show` / `task_get` with `full=true` attaches
29
+ * this so a caller sees, in the same call that reads the task, whether
30
+ * anything else is still outstanding before starting it — the read gate's
31
+ * requirements made discoverable up front instead of learned from a denial.
32
+ *
33
+ * This is read-only reporting, never enforcement: `task_start` recomputes
34
+ * eligibility itself at call time and stays the sole authority on whether
35
+ * work may begin. A `ready: true` report is not a status change and does
36
+ * not shortcut the gate; it can still go stale between this read and the
37
+ * next `task_start` call, which is exactly the edge case the gate itself
38
+ * (not this summary) is responsible for catching.
39
+ */
40
+ export interface TaskStartGateState {
41
+ ready: boolean;
42
+ projectGuidelinesReadState: ProjectGuidelinesReadState;
43
+ missingReads: RequiredContextRead[];
44
+ missingRequirementIds: string[];
45
+ }
46
+ export declare function taskStartGateState(contextEligibility: ContextReadEligibility, requirementEligibility: ContextReadEligibility, projectGuidelinesReadState: ProjectGuidelinesReadState): TaskStartGateState;
47
+ /** One line for the human-readable channel: what, if anything, `task_start` still needs. */
48
+ export declare function taskStartGateStateLine(gate: TaskStartGateState): string;
23
49
  //# sourceMappingURL=task-start-outcome.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-start-outcome.d.ts","sourceRoot":"","sources":["../src/task-start-outcome.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,uOAUzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,GAAG,yBAAyB,CAAC;AAElF,wBAAgB,eAAe,CAC7B,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC3D,sBAAsB,CASxB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,GAAG,yBAAyB,CAEpG"}
1
+ {"version":3,"file":"task-start-outcome.d.ts","sourceRoot":"","sources":["../src/task-start-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAElH,eAAO,MAAM,sBAAsB,uOAUzB,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,KAAK,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,GAAG,yBAAyB,CAAC;AAElF,wBAAgB,eAAe,CAC7B,SAAS,EAAE,kBAAkB,EAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC3D,sBAAsB,CASxB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,GAAG,yBAAyB,CAEpG;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,0BAA0B,EAAE,0BAA0B,CAAC;IACvD,YAAY,EAAE,mBAAmB,EAAE,CAAC;IACpC,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,wBAAgB,kBAAkB,CAChC,kBAAkB,EAAE,sBAAsB,EAC1C,sBAAsB,EAAE,sBAAsB,EAC9C,0BAA0B,EAAE,0BAA0B,GACrD,kBAAkB,CASpB;AAED,4FAA4F;AAC5F,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CAWvE"}
@@ -22,3 +22,26 @@ export function taskStartDenied(errorCode, message, nextActions, details = {}) {
22
22
  export function taskStartSucceeded(taskId, alreadyStarted = false) {
23
23
  return { started: true, taskId, status: "in-progress", alreadyStarted };
24
24
  }
25
+ export function taskStartGateState(contextEligibility, requirementEligibility, projectGuidelinesReadState) {
26
+ const projectGuidelinesReady = projectGuidelinesReadState === "valid" || projectGuidelinesReadState === "not-required";
27
+ return {
28
+ ready: projectGuidelinesReady && contextEligibility.eligible && requirementEligibility.eligible,
29
+ projectGuidelinesReadState,
30
+ missingReads: contextEligibility.requiredReads ?? [],
31
+ missingRequirementIds: (requirementEligibility.requiredReads ?? []).map((read) => read.id),
32
+ };
33
+ }
34
+ /** One line for the human-readable channel: what, if anything, `task_start` still needs. */
35
+ export function taskStartGateStateLine(gate) {
36
+ if (gate.ready)
37
+ return "task_start readiness: ready.";
38
+ const outstanding = [];
39
+ if (gate.projectGuidelinesReadState !== "valid" && gate.projectGuidelinesReadState !== "not-required") {
40
+ outstanding.push(`Project Guidelines (${gate.projectGuidelinesReadState})`);
41
+ }
42
+ outstanding.push(...gate.missingReads.map((read) => `${read.kind} ${read.id} (${read.state})`));
43
+ if (gate.missingRequirementIds.length > 0) {
44
+ outstanding.push(`${gate.missingRequirementIds.length} linked requirement(s)`);
45
+ }
46
+ return `task_start readiness: not ready — still needed: ${outstanding.join(", ")}.`;
47
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared text-truncation primitive. Lives in its own module, with no
3
+ * dependency on handoff-context.ts or task-context.ts, because both of
4
+ * those files need it and importing it from either one into the other
5
+ * would create a cycle (handoff-context.ts already imports buildPhaseWorkMap
6
+ * from task-context.ts).
7
+ *
8
+ * This is the one place that decides where an oversized string breaks. Three
9
+ * call sites each carried their own raw `slice(0, maxChars)` before this
10
+ * existed — handoff prose, mutation-reply echoes, and the Accepted Decision
11
+ * context — and a raw slice cuts mid-word ("Ownershi|p") or mid-list-item.
12
+ * A fourth independent copy is not an option; every caller that needs to
13
+ * bound arbitrary prose for transport imports this.
14
+ */
15
+ /**
16
+ * Truncate `value` to at most `maxChars`, cutting at the nearest paragraph
17
+ * break, then line break, then word boundary before the limit — never
18
+ * mid-word and never mid-list-item. Falls back to a hard cut at `maxChars`
19
+ * only when no boundary exists before it (e.g. one word longer than the
20
+ * limit), so the result is never empty when `maxChars > 0`.
21
+ */
22
+ export declare function truncateAtSafeBoundary(value: string, maxChars: number): string;
23
+ /** Truncate `content` to at most `maxChars` at a safe boundary (see
24
+ * truncateAtSafeBoundary) and report whether a cut happened. Callers append
25
+ * their own contextual continuation message — this stays message-agnostic
26
+ * so every caller shares one truncation rule without sharing wording that
27
+ * fits only one of them. */
28
+ export declare function boundedContentForTransport(content: string, maxChars: number): {
29
+ content: string;
30
+ truncated: boolean;
31
+ };
32
+ //# sourceMappingURL=text-bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-bounds.d.ts","sourceRoot":"","sources":["../src/text-bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAS9E;AAED;;;;4BAI4B;AAC5B,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAGrH"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared text-truncation primitive. Lives in its own module, with no
3
+ * dependency on handoff-context.ts or task-context.ts, because both of
4
+ * those files need it and importing it from either one into the other
5
+ * would create a cycle (handoff-context.ts already imports buildPhaseWorkMap
6
+ * from task-context.ts).
7
+ *
8
+ * This is the one place that decides where an oversized string breaks. Three
9
+ * call sites each carried their own raw `slice(0, maxChars)` before this
10
+ * existed — handoff prose, mutation-reply echoes, and the Accepted Decision
11
+ * context — and a raw slice cuts mid-word ("Ownershi|p") or mid-list-item.
12
+ * A fourth independent copy is not an option; every caller that needs to
13
+ * bound arbitrary prose for transport imports this.
14
+ */
15
+ /**
16
+ * Truncate `value` to at most `maxChars`, cutting at the nearest paragraph
17
+ * break, then line break, then word boundary before the limit — never
18
+ * mid-word and never mid-list-item. Falls back to a hard cut at `maxChars`
19
+ * only when no boundary exists before it (e.g. one word longer than the
20
+ * limit), so the result is never empty when `maxChars > 0`.
21
+ */
22
+ export function truncateAtSafeBoundary(value, maxChars) {
23
+ if (maxChars <= 0)
24
+ return "";
25
+ if (value.length <= maxChars)
26
+ return value;
27
+ const slice = value.slice(0, maxChars);
28
+ const paragraphBreak = slice.lastIndexOf("\n\n");
29
+ const lineBreak = slice.lastIndexOf("\n");
30
+ const wordBreak = slice.lastIndexOf(" ");
31
+ const cut = paragraphBreak >= 0 ? paragraphBreak : lineBreak >= 0 ? lineBreak : wordBreak >= 0 ? wordBreak : maxChars;
32
+ return slice.slice(0, cut).trimEnd();
33
+ }
34
+ /** Truncate `content` to at most `maxChars` at a safe boundary (see
35
+ * truncateAtSafeBoundary) and report whether a cut happened. Callers append
36
+ * their own contextual continuation message — this stays message-agnostic
37
+ * so every caller shares one truncation rule without sharing wording that
38
+ * fits only one of them. */
39
+ export function boundedContentForTransport(content, maxChars) {
40
+ if (content.length <= maxChars)
41
+ return { content, truncated: false };
42
+ return { content: truncateAtSafeBoundary(content, maxChars), truncated: true };
43
+ }
@@ -19,8 +19,8 @@ export declare class PlanWriterBusyError extends Error {
19
19
  }
20
20
  /**
21
21
  * Serialize one logical planner mutation across processes for a planner root.
22
- * Nested writes in the same async transaction are re-entrant. Reads never take
23
- * this lock, so secondary processes remain available for inspection.
22
+ * Nested writes in the same async transaction are re-entrant. Reads never
23
+ * take this lock, so secondary processes remain available for inspection.
24
24
  */
25
25
  export declare function withPlanRootWriteLock<T>(root: string, fn: () => Promise<T>): Promise<T>;
26
26
  export {};
@@ -203,8 +203,8 @@ async function acquirePlanRootLock(planRoot) {
203
203
  }
204
204
  /**
205
205
  * Serialize one logical planner mutation across processes for a planner root.
206
- * Nested writes in the same async transaction are re-entrant. Reads never take
207
- * this lock, so secondary processes remain available for inspection.
206
+ * Nested writes in the same async transaction are re-entrant. Reads never
207
+ * take this lock, so secondary processes remain available for inspection.
208
208
  */
209
209
  export async function withPlanRootWriteLock(root, fn) {
210
210
  const planRoot = resolve(root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/core",
3
- "version": "0.2.28",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "Harness-agnostic core for Agent Plan: schemas, persistence, ordering, status rollups, and markdown rendering.",
6
6
  "license": "MIT",
package/planner-skill.md CHANGED
@@ -13,6 +13,7 @@ This is the canonical, project-local operating guide for Agent Plan. A managed c
13
13
  - Starting only the dashboard does not enable planner context. Pi uses `planner-web`; MCP uses `planner-web`.
14
14
  - Stop Pi planner context and its dashboard with `/planner stop`, `planner-stop`, or `/planner disable`. For MCP, `planner-disable` explains how to disable the server.
15
15
  - `.planner/` is the operational source of truth. Keep feature, phase, task, requirement, decision, checklist, handoff, and Project Guidelines state synchronized with completed work.
16
+ - Explicit planner load (`/planner load`, `planner-load`) delivers the complete project-level context in one attested read: description, goal, scope, out-of-scope, technologies, tools, language preferences, Project Guidelines, every Requirement, and every project Accepted Decision. Check the result's `contextComplete`/`loaded` field; only `true` proves the project-level context was actually delivered — a bounded response, an omitted field, or a truncated first chunk never proves a full read. If `false`, follow the returned `nextActions` (retry with a larger `maxChars`) before relying on it. `task_start` / `planner-task-start` stays scoped to feature/phase/task context and does not redeliver project-level context; if it changed since your last planner load, task start returns a non-blocking project-context advisory telling you to reload.
16
17
  - Planner metadata operations are not code edits. Code, configuration, repository, dependency, or environment changes still require the project’s own approval rules.
17
18
  - The Web UI is a human-supervisor surface. It may bypass agent-only governance or motivation gates. Agents must not imitate its source header or treat its exemptions as an agent bypass.
18
19
 
@@ -31,24 +32,26 @@ Discover before mutating:
31
32
  1. List features, phases, or tasks using compact list tools.
32
33
  2. Follow the lowest visible ready priority unless the automatic recommendation or an approved deviation says otherwise.
33
34
  3. Use `task_recommend` / `planner-task-recommend` when choosing the next task. Treat its `claims` as bounded evidence: priority is a policy signal, a handoff is actionable only when persisted `resumeReady=true`, and Markdown prose or terminal archives never create an action claim.
34
- 4. Read the exact entity with its full-detail show/get surface when full context is needed.
35
- 5. Never infer an ambiguous bare reference. Ask for the exact composite reference.
36
- 6. Never claim that no task is active from counts, feature summaries, or omitted task detail. Use an explicit `activeTaskState`/`activeTasks` result from `plan_get`, `planner-show`, or the lifecycle recommendation. Only `activeTaskState: none` (verified from all persisted task statuses) proves absence; `conflict` means multiple active tasks must be reconciled.
35
+ 4. A task that comes up in conversation is not the next task. Discussing a subject, describing a capability, or naming a task is not an instruction to work on it: the recommendation decides what is next, and a task already in progress stays in progress. When the conversation suggests other work is more valuable, propose it as an explicit deviation with its reason and wait for a decision, or record the change through `task_switch` / `task_deviation`. Being told directly to work on something is an instruction — follow it, and it needs no ceremony.
36
+ 5. Read the exact entity with its full-detail show/get surface when full context is needed.
37
+ 6. Never infer an ambiguous bare reference. Ask for the exact composite reference.
38
+ 7. Never claim that no task is active from counts, feature summaries, or omitted task detail. Use an explicit `activeTaskState`/`activeTasks` result from `plan_get`, `planner-show`, or the lifecycle recommendation. Only `activeTaskState: none` (verified from all persisted task statuses) proves absence; `conflict` means multiple active tasks must be reconciled.
37
39
 
38
40
  Feature and phase statuses are derived from their children. Do not write their status directly; update the relevant child tasks. A `DERIVED_STATUS_READ_ONLY` result is a non-success result.
39
41
 
40
42
  ## Lifecycle-first context protocol
41
43
 
42
- Before touching code, call `task_start` / `planner-task-start`, or use `task_switch` / `planner-task-switch` when another task is already active. The lifecycle tool may deny the transition and return typed diagnostics.
44
+ Read before you call the lifecycle tool; do not use a denial to discover what to read. Before touching code:
43
45
 
44
- When denied:
46
+ 1. Read the exact task with `task_get` / `planner-task-show` (`full=true`). Its response reports `taskStartGateState` and, when anything is still outstanding, `taskStartNextActions` — the same checks `task_start` itself runs, seen up front instead of from a rejection.
47
+ 2. Perform the reads that state still lists as outstanding: the parent phase with `full=true`; the parent feature with `full=true`, when there is one; `project_guidelines_show` / `planner-project-guidelines-show`, when Project Guidelines are listed; and `requirement_list` / `planner-requirement-list` with the exact `phaseRef` given, when linked requirements are listed — a broad unscoped inventory does not attest that every requirement was read. Reads may be completed in any order, and fresh unchanged feature, phase, and linked-requirement reads may be reused across sibling tasks in the same session, so this is usually few or none of these, not a fixed list per task. A full feature/phase/task read is complete only when it delivers all canonical Accepted Decision fields (`id`, `title`, `decision`, `rationale`, `implementationNotes`, `acceptedAt`); title-only summaries never satisfy the read gate.
48
+ 3. Use the phase's task list or the task's own sibling refs to review sibling task goals, dependencies, statuses, and remaining capability ownership. Before proposing or creating work, reread the canonical phase and the relevant sibling task full view; never duplicate a capability already owned by another task.
49
+ 4. Call `task_start` / `planner-task-start`, or `task_switch` / `planner-task-switch` when another task is already active.
50
+
51
+ `task_start` still checks every read itself at call time and denies the transition with typed diagnostics if anything is missing or has gone stale since — this is the enforcement, and it is a safety net for a stale or skipped read, not the step that is supposed to teach you what to read. If it denies:
45
52
 
46
53
  1. Confirm `started` is `false` and read `errorCode` plus `nextActions`.
47
- 2. Perform only the missing or stale reads listed in `nextActions`. Reads may be completed in any order within the current session.
48
- 3. If Project Guidelines are listed, call `project_guidelines_show` or `planner-project-guidelines-show` and retain the content while working.
49
- 4. Read each task on every start or resume. Fresh unchanged feature, phase, and linked-requirement reads may be reused across sibling tasks in the same session. When linked requirements are requested, call `requirement_list` or `planner-requirement-list` with the exact `phaseRef` from `nextActions`; a broad unscoped inventory does not attest that every requirement was read. A full feature/phase/task read is complete only when it delivers all canonical Accepted Decision fields (`id`, `title`, `decision`, `rationale`, `implementationNotes`, `acceptedAt`); title-only summaries never satisfy the read gate.
50
- 5. Use the returned priority-ordered phase work map to review sibling task refs, goals, dependencies, statuses, and remaining capability ownership. Before proposing or creating work, reread the canonical phase and the relevant sibling task full view; never duplicate a capability already owned by another task.
51
- 6. Retry the lifecycle operation. Only `started: true` proves work is active.
54
+ 2. Perform only the missing or stale reads listed in `nextActions`, then retry. Only `started: true` proves work is active.
52
55
 
53
56
  Do not convert a denial into a planner status change merely to bypass the gate. Common typed denials include `PROJECT_GUIDELINES_READ_REQUIRED`, `CONTEXT_READ_REQUIRED`, `REQUIREMENTS_READ_REQUIRED`, `START_NOT_ALLOWED`, `ACTIVE_TASK_CONFLICT`, `TASK_DONE`, and persistence verification failures.
54
57
 
@@ -63,6 +66,16 @@ Do not convert a denial into a planner status change merely to bypass the gate.
63
66
  - Explicit planner load automatically and atomically deduplicates legacy `globalRules`, textual `workflowRules`, and project `decisions` into canonical Project Guidelines and Accepted Decisions before recap/context delivery. Ordinary entity reads remain non-mutating. `project_context_migrate` and `planner-project-context-migrate` remain manual preview/recovery diagnostics; repeated applications are idempotent.
64
67
  - The Web UI may display the section for the human supervisor, but guideline-read enforcement applies to agents.
65
68
 
69
+ ## Accepted Decision ownership
70
+
71
+ A decision has exactly one owner: the project when it is project-wide, a feature when it is feature-wide, a phase when it is phase-specific, or a task when the decision is genuinely local to that task's work and will not outlive it. Never record the same decision on more than one owner — there is no default that duplicates a decision across scopes.
72
+
73
+ - Record every new decision or user-agreed modification with `accepted_decision_create` / `planner-accepted-decision-create` on that single owner, choosing `targetType` (`project`, `feature`, `phase`, or `task`) by where the decision actually belongs, not by where the discussion happened to take place. A project-wide decision discovered while a task is active still goes to the project, not the task.
74
+ - `accepted_decision_update` / `planner-accepted-decision-update` and `accepted_decision_delete` / `planner-accepted-decision-delete` (confirmation-required) mutate one decision on its existing owner in place; they never move a decision to a different owner or replace the full array.
75
+ - `decision_record` (Pi) is a deprecated, write-disabled compatibility tool. It performs no write and no longer duplicates a decision across a feature and a phase; it resolves the given feature/phase and returns guidance to call `accepted_decision_create` with the correct single owner instead.
76
+ - The legacy free-form `decisions` array (project, phase, task) is read-only history. Supplying it to an update tool is rejected (`LEGACY_DECISIONS_ARRAY_READ_ONLY`); it is not decision authority and is never treated as satisfying a decision-record requirement. The same applies to a description, notes, a status log, or a handoff completion summary — any of these may reference a canonical decision's id, but none of them is the record itself.
77
+ - A completed or archived owner still shows its Accepted Decisions on read; completion never hides them. A decision recorded against a task that is later deleted is deleted with it — record project/feature/phase-durable decisions on an owner that will outlive the task.
78
+
66
79
  ## Task execution
67
80
 
68
81
  - Create rich feature, phase, and task descriptions with current state, concrete goals, relevant systems, file/symbol references, behaviors to preserve, and edge cases.
@@ -101,8 +114,12 @@ The resume-critical content is: exact focus and resume point; current/partial st
101
114
 
102
115
  Write the canonical handoff as a compact resume capsule targeting at most 8,000 inline characters (24,000 remains only as an absolute compatibility ceiling). Keep inline only the exact focus, current/partial state, decisions or constraints, relevant verification, blockers, and ordered resume steps. Put extended detail in `.planner/docs/` only when the next agent genuinely needs it; externalization is automatic and does not require a manual inventory.
103
116
 
117
+ The `supportingDocuments` manifest is optional and usually unnecessary: content above the target length is externalized automatically, and the planner appends that entry itself. Only pass `supportingDocuments` to link a `.planner/docs/` file you created yourself. If you already know that manifest before drafting, pass it to `handoff_prepare` / `planner-handoff-prepare` too — every defect that does not depend on the drafted body (bad path, missing file, symlink, duplicate, empty file) then fails before a token is issued and before any content is transmitted, instead of costing a full resend at write time. The write-time check remains the authority (it also catches a document deleted after prepare), and any `HANDOFF_SUPPORTING_DOCUMENT_INVALID` failure names dropping the field as a valid retry.
118
+
104
119
  Then call `handoff_write` / `planner-handoff-write` once with the preparation token, structured `reason`, compact capsule, optional supporting-document manifest, and reconciled task/phase/feature context. Missing preflight/reason, unresolved placeholders, invalid documents, or failed persistence read-back are typed failures and must never be reported as success.
105
120
 
121
+ If a write fails for any reason, the planner retains the exact capsule you submitted against that phase and preparation token. **Do not redraft or resend `content`/`markdown_content` on the retry** — fix only what the error named (a missing field, a stale token needs a fresh `handoff_prepare`, a corrected supporting-document manifest, etc.) and call `handoff_write` again with the same `expectedHandoffUpdatedAt`, omitting `content`; the retained capsule is reused automatically. Only pass `content` again if you are deliberately changing the body — the new one replaces the retained one. Omitting it when nothing was ever retained for that exact phase + token (a first attempt, an aged-out draft, or a phase that has since gone terminal) is itself a typed failure (`HANDOFF_RETAINED_CONTENT_NOT_FOUND`) naming the fix. A stale token is still refused even when a capsule is retained for it — retention makes the retry cheap, it never lets outdated content overwrite newer state.
122
+
106
123
  A successful write persists only a **handoff candidate** and returns `resumeReady: false`. Immediately call `handoff_show` / `planner-handoff-show` with the exact phase reference and read the persisted capsule. If the capsule omits resume-critical context, rewrite it; otherwise call `handoff_verify` / `planner-handoff-verify` with the content hash. Source reviews and omission lists are optional legacy evidence; the planner derives them when omitted. Only a successful verification result with `resumeReady: true` authorizes telling the user that the handoff is resume-ready.
107
124
 
108
125
  `handoff_list` is a compact paginated index of active handoffs and exposes whether each is resume-ready. Use `handoff_show` for one bounded active body and its metadata; after phase completion/rejection/cancellation, the same exact phase-scoped show call returns the latest terminal archive so its closeout and `.planner/docs/` references remain discoverable. Clear/archive only after explicit intent or when phase completion makes the handoff non-operational.
@@ -167,6 +184,7 @@ Supported interactive command paths:
167
184
  - `/planner task delete <P00x(F00x)/T00x>`
168
185
  - `/planner task start <P00x(F00x)/T00x>`
169
186
  - `/planner task complete <P00x(F00x)/T00x>`
187
+ - `/planner task recommend`
170
188
  - `/planner task checklist-add <task> <title>`
171
189
  - `/planner task checklist-remove <task> <C{n}|id|title>`
172
190
  - `/planner task checklist-toggle <task> <C{n}|id|title> [on|off]`
@@ -178,7 +196,7 @@ Supported interactive command paths:
178
196
 
179
197
  `handoff_verify` is an agent tool rather than an interactive command; call it only after `handoff_show` completes the separate persisted read-back.
180
198
 
181
- Pause, switch, deviation, recommendation, requirement, and decision operations are available through the registered Pi tools below rather than every interactive `/planner` path.
199
+ Pause, switch, deviation, requirement, and decision operations are available through the registered Pi tools below rather than every interactive `/planner` path. Recommendation has both: `/planner task recommend` for a person at the command line, `task_recommend` for an agent.
182
200
 
183
201
  ### Dashboard, export, and guard
184
202
 
@@ -210,7 +228,7 @@ The Pi adapter registers these tools:
210
228
  - Project and requirements: `project_set_language_preferences`, `project_update`, `project_guidelines_show`, `project_guidelines_update`, `project_context_migrate`, `accepted_decision_create`, `accepted_decision_update`, `accepted_decision_delete`, `requirement_list`, `requirement_create`, `requirement_update`, `requirement_delete`.
211
229
  - Plan: `plan_init`, `plan_get`, `description_freshness`, `plan_render`, `plan_repair`, `plan_cleanup_orphan_phases`, `plan_authorize_bypass`, `plan_clear_bypass`.
212
230
  - Features: `feature_list`, `feature_get`, `feature_create`, `feature_discuss`, `feature_update`, `feature_delete`.
213
- - Phases and decisions: `phase_list`, `phase_get`, `phase_create`, `phase_discuss`, `phase_update`, `phase_delete`, `decision_record`.
231
+ - Phases: `phase_list`, `phase_get`, `phase_create`, `phase_discuss`, `phase_update`, `phase_delete`, `decision_record` (deprecated, write-disabled compatibility redirect — see Accepted Decision ownership).
214
232
  - Tasks: `task_list`, `task_get`, `task_create`, `task_update`, `task_dependency_add`, `task_dependency_delete`, `task_delete`, `task_recommend`, `task_deviation`, `task_pause`, `task_switch`, `task_start`, `task_reopen`, `task_complete`, `task_checklist_toggle`, `task_checklist_add`, `task_checklist_remove`.
215
233
  - Handoffs: `handoff_list`, `handoff_show`, `handoff_prepare`, `handoff_write`, `handoff_verify`, `handoff_clear`.
216
234
  - Dashboard and lifecycle: `planner-web`, `planner-load`, `planner-stop`.