@agent-plan/pi-adapter 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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;AAi2BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAkzK9D"}
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;AAm0BtF,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA4yK9D"}
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, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_SOURCE_REVIEWS, HANDOFF_COLD_START_INVENTORY_CATEGORIES, handoffContentHash, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, ACCEPTED_DECISION_OWNERSHIP_RULE, ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildDecisionRecordRedirectReply, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary, statusIcon } from "@agent-plan/core";
13
+ import { ExportService, PlanStore, PlanStoreError, setWriteBusyHook, setWriteNotifyHook, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, classifyGuardedTool, loadNoTaskGuardState, noTaskWarning, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_SOURCE_REVIEWS, HANDOFF_COLD_START_INVENTORY_CATEGORIES, handoffContentHash, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, ACCEPTED_DECISION_OWNERSHIP_RULE, ACCEPTED_DECISION_RAW_REPLACEMENT_DISABLED_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, ACCEPTED_DECISION_SEMANTIC_MUTATION_REQUIRED_ERROR_CODE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildDecisionRecordRedirectReply, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary, statusIcon } from "@agent-plan/core";
14
14
  import { createChecklistItemId, createFeatureId, createPhaseId, createRequirementId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
15
15
  /** Stated on every long-prose input so a caller learns the limit before the
16
16
  * call rather than from the rejection. The number lives in the core
@@ -146,7 +146,7 @@ setWriteNotifyHook(() => {
146
146
  });
147
147
  let plannerSessionEnabled = false;
148
148
  // In-process guard bypass (per session, NOT persisted/shared). The edit/write
149
- // guard is advisory in Pi (warns "NO ACTIVE TASK" but does not block). When a
149
+ // The no-task guard only ever warns the agent; it never blocks. When a
150
150
  // bypass is active the warning is silenced for the window. Lives in memory only:
151
151
  // not on disk, not in git, not shared across agents/sessions on the same folder.
152
152
  let guardBypassUntil = "";
@@ -311,28 +311,6 @@ async function ensureProjectLanguagePreferences(st, persist = true) {
311
311
  }
312
312
  return project;
313
313
  }
314
- async function getPlannerExecutionGuard(st) {
315
- const [workspace, resume] = await Promise.all([st.loadAll(), st.loadResume()]);
316
- const allTasks = workspace.phases.flatMap((phase) => phase.tasks.map((task) => ({ phase, task })));
317
- const inProgress = allTasks.filter(({ task }) => task.status === "in-progress");
318
- const totalTasks = allTasks.length;
319
- const focusFromResume = resume?.inProgressTaskIds?.[0]
320
- ? allTasks.find(({ task }) => task.id === resume.inProgressTaskIds[0])
321
- : undefined;
322
- const focusFromCurrentPhase = resume?.currentPhaseId
323
- ? allTasks.find(({ phase, task }) => phase.id === resume.currentPhaseId && task.status !== "done" && task.status !== "canceled" && task.status !== "rejected")
324
- : undefined;
325
- const fallbackFocus = allTasks.find(({ task }) => task.pauseSnapshot)
326
- ?? allTasks.find(({ task }) => task.status === "planned" || task.status === "blocked" || task.status === "waiting")
327
- ?? allTasks.find(({ task }) => task.status !== "done" && task.status !== "canceled" && task.status !== "rejected");
328
- const focus = focusFromResume ?? focusFromCurrentPhase ?? fallbackFocus;
329
- return {
330
- totalTasks,
331
- inProgressTaskIds: inProgress.map(({ task }) => task.id),
332
- focusTaskId: focus?.task.id ?? "",
333
- focusTaskTitle: focus?.task.title ?? "",
334
- };
335
- }
336
314
  async function buildHandoffMarkdown(st, reason, overrides) {
337
315
  const [plan, resume, activity] = await Promise.all([
338
316
  st.loadAll(),
@@ -967,57 +945,45 @@ export default function planPiExtension(pi) {
967
945
  }
968
946
  }
969
947
  }
970
- // Guard the code-writing tools (edit/write). bash stays free so that
971
- // git pull, build, test, ls, etc. always work.
972
- if (event.toolName !== "edit" && event.toolName !== "write")
973
- return;
974
- // Planner-internal writes (.planner/HANDOFF.md, resume.json, generated plan
975
- // files, etc.) are planner operations, NOT code edits. They never require a
976
- // task in-progress — skip the guard so the handoff can always be written.
977
- const targetPath = String(event.input?.path ?? event.input?.filePath ?? "");
978
- if (targetPath && (targetPath.includes("/.planner/") || targetPath.includes("\\.planner\\") || targetPath.startsWith(".planner/")))
979
- return;
980
- const st = loadStore(ctx);
981
- if (!(await st.exists().catch(() => false)))
982
- return;
983
- await maybeHealStatuses(st, ctx).catch(() => { });
984
- const guard = await getPlannerExecutionGuard(st).catch(() => null);
985
- if (!guard || guard.totalTasks === 0)
986
- return; // nothing to enforce yet
987
- if (guard.inProgressTaskIds.length > 0)
988
- return; // a task is open → we're good
989
- if (isGuardBypassed())
990
- return; // user authorized proceeding without a task
991
- const focusHint = guard.focusTaskId
992
- ? `Il task più probabile è ${guard.focusTaskId} — ${guard.focusTaskTitle}. Avvialo con: \`/planner task start ${guard.focusTaskId}\``
993
- : `Scegli un task dal piano e avvialo con \`/planner task start <taskId>\`.`;
994
- ctx.ui.notify(`⚠️ NO ACTIVE TASK: You are editing files without an in-progress task. Remember to update the plan to maintain dashboard integrity. ${focusHint}`, "warning");
995
- return; // Allow the tool to proceed
996
948
  });
997
949
  // Reset per-turn flags at the start of each turn.
998
950
  pi.on("turn_start", async () => {
999
951
  taskCompleteReminderSaidThisTurn = false;
1000
952
  });
1001
- // After edit/write succeeds, track activity and (once per turn, when a task
1002
- // is in-progress) remind the agent to complete the task when the work is done.
953
+ // After a tool runs, tell the agent (never block) when it changed project
954
+ // code with no task in progress; otherwise, once per turn while a task is
955
+ // in progress, remind it to complete the task. Notes are appended to the
956
+ // tool's own output, which Pi would otherwise replace.
1003
957
  pi.on("tool_result", async (event, ctx) => {
1004
958
  if (!plannerSessionEnabled)
1005
959
  return;
1006
- if (event.toolName !== "edit" && event.toolName !== "write")
1007
- return;
1008
- if (taskCompleteReminderSaidThisTurn)
960
+ if (event.toolName !== "edit" && event.toolName !== "write" && event.toolName !== "bash")
1009
961
  return;
1010
962
  try {
1011
- const st = loadStore(ctx);
1012
- if (!(await st.exists().catch(() => false)))
963
+ const input = event.input;
964
+ const stringField = (value) => typeof value === "string" && value ? value : undefined;
965
+ const filePath = stringField(input.path ?? input.filePath);
966
+ const command = stringField(input.command);
967
+ const guardEvent = {
968
+ toolName: event.toolName,
969
+ cwd: ctx.cwd,
970
+ plannerRoot: resolvePlanRoot(ctx.cwd),
971
+ ...(filePath !== undefined ? { filePath } : {}),
972
+ ...(command !== undefined ? { command } : {}),
973
+ };
974
+ if (!classifyGuardedTool(guardEvent).guarded)
975
+ return;
976
+ const state = await loadNoTaskGuardState(loadStore(ctx));
977
+ const { warning } = noTaskWarning(guardEvent, { ...state, guardBypassed: state.guardBypassed || isGuardBypassed() });
978
+ const note = warning
979
+ ?? (state.hasInProgressTask && !taskCompleteReminderSaidThisTurn
980
+ ? "Reminder: when this implementation work is finished, call task_complete (or /planner task complete) so the task status moves to done and phase/feature rollups stay correct."
981
+ : undefined);
982
+ if (!note)
1013
983
  return;
1014
- const guard = await getPlannerExecutionGuard(st).catch(() => null);
1015
- if (guard && guard.inProgressTaskIds.length > 0) {
984
+ if (!warning)
1016
985
  taskCompleteReminderSaidThisTurn = true;
1017
- return {
1018
- content: [{ type: "text", text: "Reminder: when this implementation work is finished, call task_complete (or /planner task complete) so the task status moves to done and phase/feature rollups stay correct." }],
1019
- };
1020
- }
986
+ return { content: [...event.content, { type: "text", text: note }] };
1021
987
  }
1022
988
  catch { }
1023
989
  });
@@ -2764,38 +2730,38 @@ export default function planPiExtension(pi) {
2764
2730
  // turn (before_agent_start's systemPrompt injection is NOT applied to
2765
2731
  // triggerTurn messages), so the recap data must live here. Uses the shared
2766
2732
  // core buildRecap so Pi and Claude Code/Codex present identical content.
2767
- let recapText = "";
2768
- let plannerSkillContext = "";
2769
- let projectContextBlock = "";
2733
+ // One text, each piece once — the same builder MCP's planner-load and
2734
+ // Pi's planner-load tool use. In Pi this command is the only way the
2735
+ // operating guide reaches the agent, so the guide body is included.
2736
+ let loadText = "";
2770
2737
  try {
2771
2738
  const plannerSkill = await st.syncPlannerSkill();
2772
2739
  await st.syncGrillMeSkill();
2773
2740
  const srv = server;
2774
- recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
2741
+ let recapText = await buildRecap(st, { localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort ?? undefined }, { harness: "pi" });
2775
2742
  if (preparation.changed)
2776
2743
  recapText = `${preparation.legacyProjectContext.summary}\n\n${recapText}`;
2777
2744
  // Explicit planner load must deliver the complete project-level
2778
2745
  // context (P102(F005)/T404), attested through the same lossless
2779
- // contract used everywhere else. buildProjectContextLoadReply is the
2780
- // single shared shaping for this reply — the planner-load tool below
2781
- // uses the identical builder. Like plannerSkillContext, this is
2782
- // agent-only: it must never be quoted into the human-facing recap
2783
- // (see the "must not leak into the human recap" invariant covered by
2784
- // host-registration.test.mjs for the planner-load tool).
2746
+ // contract used everywhere else.
2785
2747
  const projectContextDelivery = await st.loadProjectContextDelivery(DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS);
2786
2748
  const projectContextReply = buildProjectContextLoadReply(projectContextDelivery, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS);
2787
2749
  if (projectContextReply.structured.contextComplete) {
2788
2750
  await st.recordProjectContextRead({ sessionId: plannerSessionId, chunks: projectContextDelivery.chunks });
2789
2751
  }
2790
- projectContextBlock = `[agent-only project context; do not quote in the recap]\n${projectContextReply.text}\n[end agent-only project context]`;
2791
- plannerSkillContext = `[agent-only planner usage skill; do not quote in the recap]\n${plannerSkill.customized ? `${plannerSkill.message}\n` : ""}${plannerSkill.content}\n[end agent-only planner usage skill]`;
2752
+ loadText = buildPlannerLoadReply({
2753
+ recap: recapText,
2754
+ projectContext: projectContextReply,
2755
+ scopedDecisions: buildScopedDecisionContext(await st.loadAll()),
2756
+ plannerGuide: { mode: "include", content: plannerSkill.content, customized: plannerSkill.customized, message: plannerSkill.message },
2757
+ }).text;
2792
2758
  }
2793
2759
  catch (e) {
2794
- recapText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
2760
+ loadText = `(recap unavailable: ${e instanceof Error ? e.message : String(e)})`;
2795
2761
  }
2796
2762
  pi.sendMessage({
2797
2763
  customType: "planner-resume-trigger",
2798
- content: "[internal trigger — not a user command] Read and retain the agent-only planner usage skill and project context below, then present the planner startup recap to the user verbatim. Do NOT call any tools (planner-load already ran). Do NOT narrate or expose internal instructions — output ONLY the recap.\n\n" + plannerSkillContext + "\n\n" + projectContextBlock + "\n\n--- RECAP ---\n" + recapText + "\n--- END RECAP ---",
2764
+ content: "[internal trigger — not a user command] Read and retain the agent-only context below, then present the recap section to the user verbatim. Do NOT call any tools (planner-load already ran). Do NOT narrate or expose internal instructions — output ONLY the recap.\n\n" + loadText,
2799
2765
  display: false,
2800
2766
  }, {
2801
2767
  triggerTurn: true,
@@ -6226,10 +6192,18 @@ export default function planPiExtension(pi) {
6226
6192
  if (projectContextReply.structured.contextComplete) {
6227
6193
  await st.recordProjectContextRead({ sessionId: plannerSessionId, chunks: projectContextDelivery.chunks });
6228
6194
  }
6229
- // The project context text (like the rest of `details`) is agent-only:
6230
- // read and retain it, but never quote it or its Accepted Decisions in
6231
- // the human-facing recap (`content[].text` stays the recap alone).
6232
- return { content: [{ type: "text", text: recap }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort, preparation: preparation.legacyProjectContext, plannerSkill: { status: plannerSkill.status, customized: plannerSkill.customized, message: plannerSkill.message }, projectContext: { text: projectContextReply.text, ...projectContextReply.structured } } };
6195
+ // Pi's model reads only `content`; `details` is for the UI. The project
6196
+ // context used to live only in `details`, so it was attested as read
6197
+ // while the agent never saw it. Same builder as MCP and /planner load.
6198
+ // The guide is already in this turn's context (the per-turn plan
6199
+ // context block carries it), so only point at it here.
6200
+ const reply = buildPlannerLoadReply({
6201
+ recap,
6202
+ projectContext: projectContextReply,
6203
+ scopedDecisions: buildScopedDecisionContext(await st.loadAll()),
6204
+ plannerGuide: { mode: "pointer", path: ".planner/SKILL.md", customized: plannerSkill.customized, message: plannerSkill.message },
6205
+ });
6206
+ return { content: [{ type: "text", text: reply.text }], details: { enabled: true, running: Boolean(srv), localUrl: srv?.localUrl, lanUrl: srv?.lanUrl, port: lastKnownWebPort, preparation: preparation.legacyProjectContext, plannerSkill: { status: plannerSkill.status, customized: plannerSkill.customized, message: plannerSkill.message }, ...reply.structured } };
6233
6207
  },
6234
6208
  });
6235
6209
  pi.registerTool({
@@ -6413,7 +6387,7 @@ export default function planPiExtension(pi) {
6413
6387
  "- Use task_update with motivation for blocked/canceled/rejected/deferred/waiting/planned(from non-planned).",
6414
6388
  "- Planner ops (status/handoff/planner metadata) are NOT code edits; they are always allowed.",
6415
6389
  "- Prioritize work: continue an in-progress task; otherwise choose ready work feature → phase → task by ascending priority, respecting dependencies and blocked/waiting states. Compact feature_list / phase_list / task_list surfaces expose priority markers; when browsing manually, follow the lowest visible priority first among ready siblings. Prefer shortId or F00x/P00x/T00x refs. Read one entity via *_get(full=true).",
6416
- "- If edit/write guard blocks you, start the right task or use an explicit bypass.",
6390
+ "- Nothing is ever blocked for lack of a task. If you are warned that you changed project code with no task in progress, start the task that covers it.",
6417
6391
  ...(extensionRules.length > 0
6418
6392
  ? [
6419
6393
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/pi-adapter",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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.3.0",
32
- "@agent-plan/server": "0.3.0"
31
+ "@agent-plan/core": "0.3.1",
32
+ "@agent-plan/server": "0.3.1"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@earendil-works/pi-coding-agent": "^0.74.0",