@agent-plan/pi-adapter 0.2.22-next.2 → 0.2.22-next.4

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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AA40BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAw5H9D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AA40BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAs6H9D"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { Type } from "typebox";
12
12
  import { paginatedSelect, paginatedNotify } from "./ui/paginate.js";
13
- import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureRead, markPhaseRead, markTaskRead, hasReadParents, parentReadAdvisory, hasReadRequirements, requirementReadAdvisory, markRequirementRead, invalidateReads } from "@agent-plan/core";
13
+ import { ExportService, PlanStore, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, buildPhaseContextBlock, checkExplicitTaskStart, recommendNextTask, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, markFeatureRead, markPhaseRead, markTaskRead, contextReadEligibility, hasReadRequirements, markRequirementRead, invalidateReads } from "@agent-plan/core";
14
14
  import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
15
15
  import { join, dirname } from "node:path";
16
16
  import { homedir } from "node:os";
@@ -786,6 +786,7 @@ export default function planPiExtension(pi) {
786
786
  capturedPi = pi;
787
787
  // ── Restore on session start/reload ─────────────────────────────────
788
788
  pi.on("session_start", async (_event, ctx) => {
789
+ invalidateReads();
789
790
  try {
790
791
  ctx.ui.addAutocompleteProvider((current) => ({
791
792
  triggerCharacters: ["/", " "],
@@ -1464,9 +1465,9 @@ export default function planPiExtension(pi) {
1464
1465
  if (b === "language") {
1465
1466
  const project = await st.loadProject();
1466
1467
  const contentLanguage = await ctx.ui.input(`Plan content language [${project.contentLanguage || "English"}]`);
1467
- const chatLanguage = await ctx.ui.input(`Chat language [${project.chatLanguage || contentLanguage?.trim() || project.contentLanguage || "Italian"}]`);
1468
+ const chatLanguage = await ctx.ui.input(`Chat language [${project.chatLanguage || contentLanguage?.trim() || project.contentLanguage || "English"}]`);
1468
1469
  project.contentLanguage = contentLanguage?.trim() || project.contentLanguage || "English";
1469
- project.chatLanguage = chatLanguage?.trim() || project.chatLanguage || project.contentLanguage || "Italian";
1470
+ project.chatLanguage = chatLanguage?.trim() || project.chatLanguage || project.contentLanguage || "English";
1470
1471
  await st.saveProject(project);
1471
1472
  await st.writeGenerated();
1472
1473
  ctx.ui.notify(`Saved language preferences: content=${project.contentLanguage}, chat=${project.chatLanguage}`, "info");
@@ -2159,8 +2160,9 @@ export default function planPiExtension(pi) {
2159
2160
  ctx.ui.notify(`Invalid status. Use: ${valid.join(", ")}`, "error");
2160
2161
  return;
2161
2162
  }
2162
- if (normalizedStatus === "in-progress" && (await st.listHandoffs()).length > 0) {
2163
- ctx.ui.notify("ℹ️ One or more phases have a pending handoff — if relevant to this task, read it with /planner handoff show <ref>, then clear with /planner handoff clear. Proceeding with the status change.", "info");
2163
+ if (normalizedStatus === "in-progress" && normalizedStatus !== task.status) {
2164
+ ctx.ui.notify("Task start/resume transitions require /planner task start so ordered full context reads and lifecycle state remain consistent.", "warning");
2165
+ return;
2164
2166
  }
2165
2167
  applyTaskLifecycleDates(task, normalizedStatus, now);
2166
2168
  }
@@ -2198,16 +2200,19 @@ export default function planPiExtension(pi) {
2198
2200
  return;
2199
2201
  }
2200
2202
  }
2201
- if (phase && !hasReadParents(phase.featureId, phase.id)) {
2202
- ctx.ui.notify("⚠️ READ REQUIRED before proceeding: read the parent feature and phase (full=true) for this task before starting it.", "warning");
2203
- }
2204
2203
  if (phase) {
2204
+ const contextEligibility = contextReadEligibility(task.id, phase.id, phase.featureId);
2205
+ if (!contextEligibility.eligible) {
2206
+ ctx.ui.notify(`Task start denied: context reads required. ${contextEligibility.reason}`, "warning");
2207
+ return;
2208
+ }
2205
2209
  const linkedRequirementIds = [
2206
2210
  ...(await st.linkedRequirementsForPhase(phase.id)).map((r) => r.id),
2207
2211
  ...(phase.featureId ? (await st.linkedRequirementsForFeature(phase.featureId)).map((r) => r.id) : []),
2208
2212
  ];
2209
- if (linkedRequirementIds.length > 0 && !hasReadRequirements(linkedRequirementIds)) {
2210
- ctx.ui.notify("⚠️ REQUIREMENTS READ REQUIRED: read the requirements linked to this phase (and feature) before starting the task.", "warning");
2213
+ if (!hasReadRequirements(linkedRequirementIds)) {
2214
+ ctx.ui.notify("Task start denied: read the requirements linked to this phase and feature first.", "warning");
2215
+ return;
2211
2216
  }
2212
2217
  }
2213
2218
  if (task.status === "in-progress") {
@@ -3271,7 +3276,8 @@ export default function planPiExtension(pi) {
3271
3276
  if (!resolvedFeature.ok)
3272
3277
  return { content: [{ type: "text", text: resolvedFeature.error }], details: {} };
3273
3278
  const feature = resolvedFeature.feature;
3274
- markFeatureRead(feature.id);
3279
+ if (params.full)
3280
+ markFeatureRead(feature.id);
3275
3281
  const phases = (await st.loadAllPhases()).filter((p) => p.featureId === feature.id);
3276
3282
  const linkedRequirements = await st.linkedRequirementsForFeature(feature.id);
3277
3283
  const summary = `${feature.name} — ${formatFeatureRef(feature.number)}${feature.shortId ? ` · ${feature.shortId}` : ""} (${feature.status}; ${phases.length} phases${linkedRequirements.length ? `; ${linkedRequirements.length} linked requirement${linkedRequirements.length === 1 ? "" : "s"}` : ""})`;
@@ -3570,7 +3576,8 @@ export default function planPiExtension(pi) {
3570
3576
  const phase = findPhaseByRef(await st.loadAllPhases(), features, params.phaseId.trim());
3571
3577
  if (!phase)
3572
3578
  return { content: [{ type: "text", text: `Phase not found: ${params.phaseId}` }], details: {} };
3573
- markPhaseRead(phase.id, phase.featureId);
3579
+ if (params.full)
3580
+ markPhaseRead(phase.id, phase.featureId);
3574
3581
  const linkedRequirements = await st.linkedRequirementsForPhase(phase.id);
3575
3582
  const reqCount = linkedRequirements.length;
3576
3583
  const summary = `${phase.title} — ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, features))}${phase.shortId ? ` · ${phase.shortId}` : ""} (${phase.status}; ${phase.tasks.length} tasks${reqCount ? `; ${reqCount} linked requirement${reqCount === 1 ? "" : "s"}` : ""})`;
@@ -3921,7 +3928,8 @@ export default function planPiExtension(pi) {
3921
3928
  const found = findTaskByRef(await st.loadAllPhases(), features, params.taskId.trim());
3922
3929
  if (!found)
3923
3930
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
3924
- markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
3931
+ if (params.full)
3932
+ markTaskRead(found.task.id, found.phase.id, found.phase.featureId);
3925
3933
  const summary = `${found.task.title} — ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${pad(found.task.number)}${found.task.shortId ? ` · ${found.task.shortId}` : ""} (${found.task.status})`;
3926
3934
  if (!params.full)
3927
3935
  return { content: [{ type: "text", text: summary }], details: {} };
@@ -3999,15 +4007,15 @@ export default function planPiExtension(pi) {
3999
4007
  const existingPhase = await st.loadPhase(phaseId);
4000
4008
  if (!existingPhase)
4001
4009
  return { content: [{ type: "text", text: `Resolved phase ${phaseId} no longer exists. Refusing to create task.` }], details: {} };
4010
+ const initialStatus = params.status ?? "planned";
4011
+ if (initialStatus !== "planned") {
4012
+ return { content: [{ type: "text", text: "Tasks must be created planned. Use task_start for in-progress work and task_update for other justified status changes." }], details: {} };
4013
+ }
4002
4014
  const shortName = clampSlug(params.shortName ?? title, 30, `task-${Date.now().toString(36)}`); // clamp+strip trailing dash; never empty
4003
4015
  const taskId = createTaskId();
4004
4016
  const identity = await st.allocateEntityIdentity("task", taskId);
4005
4017
  const priority = await st.nextPriority("task", phaseId);
4006
4018
  const now = nowISO();
4007
- const initialStatus = params.status ?? "planned";
4008
- if (initialStatus === "paused") {
4009
- return { content: [{ type: "text", text: "A task cannot be created paused without an in-progress checkpoint. Create it planned, then use task_pause/task_switch." }], details: {} };
4010
- }
4011
4019
  const task = {
4012
4020
  id: taskId, phaseId, number: identity.number, shortId: identity.shortId, priority, shortName,
4013
4021
  title,
@@ -4025,8 +4033,8 @@ export default function planPiExtension(pi) {
4025
4033
  dependsOn: [],
4026
4034
  pauseSnapshot: null,
4027
4035
  pauseHistory: [],
4028
- startedAt: initialStatus === "in-progress" || initialStatus === "done" ? now : "",
4029
- completedAt: initialStatus === "done" ? now : "",
4036
+ startedAt: "",
4037
+ completedAt: "",
4030
4038
  createdAt: now, updatedAt: now,
4031
4039
  };
4032
4040
  // Atomic read-modify-write on the phase file: serializes concurrent
@@ -4080,9 +4088,9 @@ export default function planPiExtension(pi) {
4080
4088
  // Validate the motivation requirement for restrictive status transitions,
4081
4089
  // mirroring the MCP adapter and the /planner task update command
4082
4090
  // (AGENTS.md rule 8 — rejected writes must leave the files unchanged).
4083
- if (found.task.pauseSnapshot && params.status === "in-progress" && params.status !== found.task.status) {
4091
+ if (params.status === "in-progress" && params.status !== found.task.status) {
4084
4092
  return {
4085
- content: [{ type: "text", text: "Resume checkpoint lifecycle transitions require task_start so the checkpoint and return stack remain consistent." }],
4093
+ content: [{ type: "text", text: "Task start/resume transitions require task_start so lifecycle state, checkpoints, and return stacks remain consistent." }],
4086
4094
  details: {},
4087
4095
  };
4088
4096
  }
@@ -4180,14 +4188,14 @@ export default function planPiExtension(pi) {
4180
4188
  pi.registerTool({
4181
4189
  name: "task_recommend",
4182
4190
  label: "Task Recommend",
4183
- description: "Return the recommended next task without starting work: continue one active task, otherwise choose ready work by feature → phase → task priority. Reports approved deviation/resume context and never blocks an explicit user choice.",
4191
+ description: "Return the recommended next task without starting work: continue one active task or the current phase, otherwise choose ready work by feature → phase → task priority. Reports approved deviation/resume context and never blocks an explicit user choice.",
4184
4192
  parameters: Type.Object({}),
4185
4193
  async execute(_id, _params, _signal, _onUpdate, ctx) {
4186
4194
  const st = await requirePlan(ctx);
4187
4195
  if (!st)
4188
4196
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4189
- const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
4190
- const result = recommendNextTask(features.features, phases, project.workDeviations);
4197
+ const [features, phases, project, resume] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
4198
+ const result = recommendNextTask(features.features, phases, project.workDeviations, resume?.currentPhaseId);
4191
4199
  if (!result.candidate)
4192
4200
  return { content: [{ type: "text", text: `No task recommendation: ${result.reason}` }], details: result };
4193
4201
  const { candidate } = result;
@@ -4211,11 +4219,11 @@ export default function planPiExtension(pi) {
4211
4219
  const st = await requirePlan(ctx);
4212
4220
  if (!st)
4213
4221
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4214
- const [features, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
4222
+ const [features, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
4215
4223
  const temporary = findTaskByRef(phases, features.features, params.temporary_task.trim());
4216
4224
  if (!temporary)
4217
4225
  return { content: [{ type: "text", text: `Task not found: ${params.temporary_task}` }], details: {} };
4218
- const selected = recommendNextTask(features.features, phases, project.workDeviations);
4226
+ const selected = recommendNextTask(features.features, phases, project.workDeviations, focus?.currentPhaseId);
4219
4227
  const resume = params.resume_task ? findTaskByRef(phases, features.features, params.resume_task.trim()) : selected.candidate;
4220
4228
  if (!resume)
4221
4229
  return { content: [{ type: "text", text: `No resume task is available. Provide resume_task explicitly. ${selected.reason}` }], details: selected };
@@ -4311,7 +4319,7 @@ export default function planPiExtension(pi) {
4311
4319
  const st = await requirePlan(ctx);
4312
4320
  if (!st)
4313
4321
  return { content: [{ type: "text", text: "No .planner/ found." }], details: {} };
4314
- const [featuresDoc, phases, project] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject()]);
4322
+ const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
4315
4323
  const features = featuresDoc.features;
4316
4324
  const source = findTaskByRef(phases, features, params.from_task.trim());
4317
4325
  const target = findTaskByRef(phases, features, params.to_task.trim());
@@ -4324,6 +4332,17 @@ export default function planPiExtension(pi) {
4324
4332
  if (source.task.status !== "in-progress" && !source.task.pauseSnapshot) {
4325
4333
  return { content: [{ type: "text", text: `Task switch denied: source is ${source.task.status}; only in-progress or checkpointed work can be switched.` }], details: {} };
4326
4334
  }
4335
+ const contextEligibility = contextReadEligibility(target.task.id, target.phase.id, target.phase.featureId);
4336
+ if (!contextEligibility.eligible) {
4337
+ return { content: [{ type: "text", text: `Task switch denied: context reads required. ${contextEligibility.reason}` }], details: contextEligibility };
4338
+ }
4339
+ const linkedRequirementIds = [
4340
+ ...(await st.linkedRequirementsForPhase(target.phase.id)).map((requirement) => requirement.id),
4341
+ ...(target.phase.featureId ? (await st.linkedRequirementsForFeature(target.phase.featureId)).map((requirement) => requirement.id) : []),
4342
+ ];
4343
+ if (!hasReadRequirements(linkedRequirementIds)) {
4344
+ return { content: [{ type: "text", text: "Task switch denied: read the requirements linked to the target phase and feature before switching." }], details: { requirementIds: linkedRequirementIds } };
4345
+ }
4327
4346
  const eligibility = checkExplicitTaskStart(features, phases, target.task.id, project.workDeviations);
4328
4347
  if (!eligibility.eligible)
4329
4348
  return { content: [{ type: "text", text: `Task switch denied: ${eligibility.reason}` }], details: eligibility };
@@ -4333,7 +4352,7 @@ export default function planPiExtension(pi) {
4333
4352
  resumeLocation: params.resume_location.trim(), howToResume: params.how_to_resume.trim(),
4334
4353
  relatedTaskId: target.task.id, pausedAt: timestamp, pausedBy: params.switched_by?.trim() ?? "",
4335
4354
  };
4336
- const selection = recommendNextTask(features, phases, project.workDeviations);
4355
+ const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
4337
4356
  const record = {
4338
4357
  id: crypto.randomUUID(), recommendedTaskId: selection.candidate?.task.id ?? source.task.id,
4339
4358
  temporaryTaskId: target.task.id, resumeTaskId: source.task.id, reason: snapshot.reason, snapshot,
@@ -4413,30 +4432,24 @@ export default function planPiExtension(pi) {
4413
4432
  if (target.phase.id !== source.phase.id)
4414
4433
  await st.syncTaskStatusRollup(target.phase.id);
4415
4434
  await st.writeGenerated();
4435
+ invalidateReads();
4416
4436
  const sourceRef = `${formatPhaseRef(source.phase.number, featureNumberOfPhase(source.phase, features))}/T${String(source.task.number).padStart(3, "0")}`;
4417
4437
  const targetRef = `${formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features))}/T${String(target.task.number).padStart(3, "0")}`;
4418
- const targetReadAdvisory = parentReadAdvisory(target.phase.featureId, target.phase.id);
4419
- const targetRequirementAdvisory = requirementReadAdvisory([
4420
- ...(await st.linkedRequirementsForPhase(target.phase.id)).map((r) => r.id),
4421
- ...(target.phase.featureId ? (await st.linkedRequirementsForFeature(target.phase.featureId)).map((r) => r.id) : []),
4422
- ]);
4423
4438
  return {
4424
4439
  content: [{ type: "text", text: [
4425
4440
  `🔀 Task switched: ${sourceRef} → ${targetRef}`,
4426
4441
  `Resume checkpoint: ${snapshot.whatWasBeingDone}`,
4427
4442
  `Return target: ${sourceRef} from ${snapshot.resumeLocation}`,
4428
4443
  "Normal priority was deliberately overridden; completing the temporary task will emit RESUME REQUIRED.",
4429
- ...(targetReadAdvisory ? [targetReadAdvisory.trimStart()] : []),
4430
- ...(targetRequirementAdvisory ? [targetRequirementAdvisory.trimStart()] : []),
4431
4444
  ].join("\n") }],
4432
- details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id, ...(targetReadAdvisory || targetRequirementAdvisory ? { readRequired: true } : {}) },
4445
+ details: { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id },
4433
4446
  };
4434
4447
  },
4435
4448
  });
4436
4449
  pi.registerTool({
4437
4450
  name: "task_start",
4438
4451
  label: "Task Start",
4439
- description: "Set a task to in-progress or resume checkpointed work. BEFORE calling, read feature_get(full=true), phase_get(full=true), then task_get(full=true). A different active task must first be suspended through task_switch. Otherwise follow active/pending-resume/feature → phase → task priority guidance.",
4452
+ description: "Set a task to in-progress or resume checkpointed work. BEFORE calling, read task_get(full=true), then its parent phase_get(full=true), then its parent feature_get(full=true), in that exact order. Read linked requirements with requirement_list when present. A different active task must first be suspended through task_switch.",
4440
4453
  parameters: Type.Object({
4441
4454
  taskId: Type.String({ description: "Task ref: F00x/P00x/T00x, bare T00x (global), 5-char shortId, UUID, or title to start" }),
4442
4455
  }),
@@ -4453,12 +4466,22 @@ export default function planPiExtension(pi) {
4453
4466
  return { content: [{ type: "text", text: `Task ${task.id} is already in-progress.` }], details: task };
4454
4467
  if (task.status === "done")
4455
4468
  return { content: [{ type: "text", text: `Task ${task.id} is done. Use task_update to reopen.` }], details: task };
4456
- const project = await st.loadProject();
4457
- const phases = await st.loadAllPhases();
4469
+ const contextEligibility = contextReadEligibility(task.id, found.phase.id, found.phase.featureId);
4470
+ if (!contextEligibility.eligible) {
4471
+ return { content: [{ type: "text", text: `Task start denied: context reads required. ${contextEligibility.reason}` }], details: contextEligibility };
4472
+ }
4473
+ const linkedRequirementIds = [
4474
+ ...(await st.linkedRequirementsForPhase(found.phase.id)).map((requirement) => requirement.id),
4475
+ ...(found.phase.featureId ? (await st.linkedRequirementsForFeature(found.phase.featureId)).map((requirement) => requirement.id) : []),
4476
+ ];
4477
+ if (!hasReadRequirements(linkedRequirementIds)) {
4478
+ return { content: [{ type: "text", text: "Task start denied: read the requirements linked to the task's phase and feature first." }], details: { requirementIds: linkedRequirementIds } };
4479
+ }
4480
+ const [project, phases, focus] = await Promise.all([st.loadProject(), st.loadAllPhases(), st.loadResume()]);
4458
4481
  const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
4459
4482
  if (!eligibility.eligible)
4460
4483
  return { content: [{ type: "text", text: `Task start denied: ${eligibility.reason}` }], details: eligibility };
4461
- const selection = recommendNextTask(features, phases, project.workDeviations);
4484
+ const selection = recommendNextTask(features, phases, project.workDeviations, focus?.currentPhaseId);
4462
4485
  if (selection.kind === "conflict") {
4463
4486
  return { content: [{ type: "text", text: `${selection.reason} Pause/reconcile active tasks before starting another task.` }], details: selection };
4464
4487
  }
@@ -4537,16 +4560,10 @@ export default function planPiExtension(pi) {
4537
4560
  await st.writeGenerated();
4538
4561
  if (!startedTask)
4539
4562
  return { content: [{ type: "text", text: `Task not found: ${params.taskId}` }], details: {} };
4540
- const readAdvisory = parentReadAdvisory(parentFeature?.id, found.phase.id);
4541
- const requirementAdvisory = requirementReadAdvisory([
4542
- ...(phaseWithRequirements.linkedRequirements ?? []).map((r) => r.id),
4543
- ...featureRequirements.map((r) => r.id),
4544
- ]);
4545
4563
  return {
4546
- content: [{ type: "text", text: `✅ Task started: ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(startedTask.number).padStart(3, "0")} — ${startedTask.title} (in-progress)${startedTask.shortId ? ` · ${startedTask.shortId}` : ""}${phaseContext}${advisory}${readAdvisory}${requirementAdvisory}` }],
4564
+ content: [{ type: "text", text: `✅ Task started: ${formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features))}/T${String(startedTask.number).padStart(3, "0")} — ${startedTask.title} (in-progress)${startedTask.shortId ? ` · ${startedTask.shortId}` : ""}${phaseContext}${advisory}` }],
4547
4565
  details: startedTask,
4548
4566
  ...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
4549
- ...(readAdvisory || requirementAdvisory ? { readRequired: true } : {}),
4550
4567
  };
4551
4568
  },
4552
4569
  });
@@ -4994,7 +5011,7 @@ export default function planPiExtension(pi) {
4994
5011
  "",
4995
5012
  "Operational rules:",
4996
5013
  "- Handoffs are context, not locks. Read the relevant handoff with handoff show <ref> before resuming.",
4997
- "- BEFORE work: read feature_get(full=true) for the parent feature and its linked requirements, then phase_get(full=true) for the parent phase and its linked requirements, then task_get(full=true); only then call task_start before any code edit/write. AFTER finishing a task: task_complete.",
5014
+ "- BEFORE work: read task_get(full=true), then its parent phase_get(full=true), then its parent feature_get(full=true), in that exact order. Read linked requirements with requirement_list when present. task_start and task_switch reject work until these reads are complete; only then touch code. AFTER finishing a task: task_complete.",
4998
5015
  "- Record every new decision or user-agreed modification in both the relevant feature and phase before treating the discussion as complete.",
4999
5016
  "- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
5000
5017
  "- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.2.22-next.2",
3
+ "version": "0.2.22-next.4",
4
4
  "private": false,
5
5
  "description": "Pi agent extension for Agent Plan — structured project planning in .planner/",
6
6
  "keywords": [
@@ -28,8 +28,8 @@
28
28
  "dependencies": {
29
29
  "typebox": "^1.1.24",
30
30
  "zod": "^3.24.0",
31
- "@agent-plan/core": "0.2.22-next.2",
32
- "@agent-plan/server": "0.2.22-next.2"
31
+ "@agent-plan/core": "0.2.22-next.4",
32
+ "@agent-plan/server": "0.2.22-next.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",