@agent-plan/core 0.2.29 → 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.
- package/dist/accepted-decision-guard.d.ts +94 -0
- package/dist/accepted-decision-guard.d.ts.map +1 -0
- package/dist/accepted-decision-guard.js +110 -0
- package/dist/checklist.d.ts +28 -0
- package/dist/checklist.d.ts.map +1 -1
- package/dist/checklist.js +59 -0
- package/dist/feature-delete.d.ts +62 -0
- package/dist/feature-delete.d.ts.map +1 -0
- package/dist/feature-delete.js +49 -0
- package/dist/guard-decision.d.ts +118 -0
- package/dist/guard-decision.d.ts.map +1 -0
- package/dist/guard-decision.js +203 -0
- package/dist/handoff-context.d.ts +130 -1
- package/dist/handoff-context.d.ts.map +1 -1
- package/dist/handoff-context.js +167 -9
- package/dist/handoff-reply.d.ts +108 -0
- package/dist/handoff-reply.d.ts.map +1 -0
- package/dist/handoff-reply.js +279 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/mutation-reply.d.ts +105 -0
- package/dist/mutation-reply.d.ts.map +1 -0
- package/dist/mutation-reply.js +203 -0
- package/dist/naming.d.ts +1 -1
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +1 -1
- package/dist/pagination.d.ts +19 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +15 -0
- package/dist/payload-fallback.js +1 -1
- package/dist/plan-store.d.ts +141 -8
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +459 -87
- package/dist/project-context-migration.d.ts +5 -0
- package/dist/project-context-migration.d.ts.map +1 -1
- package/dist/project-context-migration.js +13 -0
- package/dist/project-context.d.ts +395 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +295 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +27 -0
- package/dist/recommend-reply.d.ts +51 -0
- package/dist/recommend-reply.d.ts.map +1 -0
- package/dist/recommend-reply.js +84 -0
- package/dist/refs.d.ts +16 -0
- package/dist/refs.d.ts.map +1 -1
- package/dist/refs.js +38 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/schema.d.ts +7 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +8 -3
- package/dist/stranded-phase.d.ts +73 -0
- package/dist/stranded-phase.d.ts.map +1 -0
- package/dist/stranded-phase.js +93 -0
- package/dist/task-completion-gate.d.ts +84 -0
- package/dist/task-completion-gate.d.ts.map +1 -0
- package/dist/task-completion-gate.js +72 -0
- package/dist/task-context.d.ts +1 -0
- package/dist/task-context.d.ts.map +1 -1
- package/dist/task-context.js +31 -13
- package/dist/task-lifecycle.d.ts +12 -0
- package/dist/task-lifecycle.d.ts.map +1 -0
- package/dist/task-lifecycle.js +23 -0
- package/dist/task-order-context.d.ts +72 -0
- package/dist/task-order-context.d.ts.map +1 -0
- package/dist/task-order-context.js +82 -0
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +14 -12
- package/dist/task-start-outcome.d.ts +26 -0
- package/dist/task-start-outcome.d.ts.map +1 -1
- package/dist/task-start-outcome.js +23 -0
- package/dist/text-bounds.d.ts +32 -0
- package/dist/text-bounds.d.ts.map +1 -0
- package/dist/text-bounds.js +43 -0
- package/dist/write-coordination.d.ts +2 -2
- package/dist/write-coordination.js +2 -2
- package/package.json +1 -1
- 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":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEvE,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,cAAc,CAAC;AAClG,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,aAAa,GAAG,kBAAkB,GAAG,kBAAkB,GAAG,SAAS,GAAG,kBAAkB,CAAC;AAE9I,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,MAAM,EAAE,yBAAyB,CAAC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,6BAA6B;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,qCAAqC;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,6BAA6B,EAAE,CAAC;IAC3C,gBAAgB,CAAC,EAAE,qCAAqC,EAAE,CAAC;CAC5D;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,MAAM,CAAC;AAE5F,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,UAAU,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,WAAW,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,mBAAmB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,iEAAiE;IACjE,gBAAgB,CAAC,EAAE,aAAa,EAAE,CAAC;IACnC,8EAA8E;IAC9E,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,mFAAmF;AACnF,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"task-selection.d.ts","sourceRoot":"","sources":["../src/task-selection.ts"],"names":[],"mappings":"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"}
|
package/dist/task-selection.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
167
|
-
|| deviation.state === "resolved")
|
|
170
|
+
|| returnRequiredStates.has(deviation.state))
|
|
168
171
|
.sort(newestFirst);
|
|
169
|
-
const top =
|
|
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
|
|
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
|
|
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
|
|
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
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.
|
|
35
|
-
5.
|
|
36
|
-
6. Never
|
|
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
|
-
|
|
44
|
+
Read before you call the lifecycle tool; do not use a denial to discover what to read. Before touching code:
|
|
43
45
|
|
|
44
|
-
|
|
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
|
|
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,
|
|
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
|
|
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`.
|