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

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,CAu6H9D"}
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
  }
@@ -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 };
@@ -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,6 +4466,17 @@ 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 };
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
+ }
4456
4480
  const project = await st.loadProject();
4457
4481
  const phases = await st.loadAllPhases();
4458
4482
  const eligibility = checkExplicitTaskStart(features, phases, task.id, project.workDeviations);
@@ -4537,16 +4561,10 @@ export default function planPiExtension(pi) {
4537
4561
  await st.writeGenerated();
4538
4562
  if (!startedTask)
4539
4563
  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
4564
  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}` }],
4565
+ 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
4566
  details: startedTask,
4548
4567
  ...(resumeProposal ? { resumeRequired: resumeProposal.structured } : {}),
4549
- ...(readAdvisory || requirementAdvisory ? { readRequired: true } : {}),
4550
4568
  };
4551
4569
  },
4552
4570
  });
@@ -4994,7 +5012,7 @@ export default function planPiExtension(pi) {
4994
5012
  "",
4995
5013
  "Operational rules:",
4996
5014
  "- 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.",
5015
+ "- 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
5016
  "- Record every new decision or user-agreed modification in both the relevant feature and phase before treating the discussion as complete.",
4999
5017
  "- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
5000
5018
  "- 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.3",
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.3",
32
+ "@agent-plan/server": "0.2.22-next.3"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",