@agent-plan/core 0.2.21 → 0.2.22-next.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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * T307 (P072/F005) — tracked, deduplicated parent read-enforcement.
3
+ *
4
+ * Analogous to the existing NO ACTIVE TASK advisory: an in-memory, per-session
5
+ * record of which feature/phase refs the agent has actually read (via the
6
+ * feature/phase/task get/show tools). Starting, resuming, or switching a task
7
+ * checks that the target's parent feature + phase are in the read set; a missing
8
+ * read yields an unavoidable (non-blocking) advisory instead of a silent skip.
9
+ *
10
+ * Dedupe: reading a phase (or any task inside it) records both its phase and
11
+ * feature, so 10 sibling tasks need a single feature + phase read. Switching to
12
+ * a different phase/feature naturally fails (that phase is not recorded). A
13
+ * pause invalidates the set, so resuming forces a fresh read.
14
+ *
15
+ * T319 (P072/F005) extends the set with requirements: starting/resuming/switching
16
+ * also requires the requirements linked to the phase (and, when present, the
17
+ * feature) to have been explicitly read. Requirements are NOT auto-recorded by a
18
+ * phase/feature/task read (per project decision: explicit separate read); they
19
+ * are recorded only via the requirement list tool (markRequirementRead). An empty
20
+ * linked-requirement list means nothing is required.
21
+ */
22
+ let state = { features: new Set(), phases: new Set(), requirements: new Set() };
23
+ /** Record that a feature was read. Does NOT imply any phase was read. */
24
+ export function markFeatureRead(featureId) {
25
+ state.features.add(featureId);
26
+ }
27
+ /** Record that a phase (and, when known, its feature) was read. */
28
+ export function markPhaseRead(phaseId, featureId) {
29
+ state.phases.add(phaseId);
30
+ if (featureId)
31
+ state.features.add(featureId);
32
+ }
33
+ /** Record that a task (and, by context, its parent phase + feature) was read. */
34
+ export function markTaskRead(_taskId, phaseId, featureId) {
35
+ state.phases.add(phaseId);
36
+ if (featureId)
37
+ state.features.add(featureId);
38
+ }
39
+ /** Record that a requirement was explicitly read (via the requirement list tool). */
40
+ export function markRequirementRead(requirementId) {
41
+ state.requirements.add(requirementId);
42
+ }
43
+ /**
44
+ * Whether the target task's parent feature + phase have both been read.
45
+ * `featureId` is optional: an orphan phase (no feature) only requires the phase.
46
+ */
47
+ export function hasReadParents(featureId, phaseId) {
48
+ const phaseOk = state.phases.has(phaseId);
49
+ const featureOk = featureId ? state.features.has(featureId) : true;
50
+ return phaseOk && featureOk;
51
+ }
52
+ /** Whether every linked requirement (phase + feature) has been explicitly read. */
53
+ export function hasReadRequirements(requirementIds) {
54
+ return requirementIds.every((id) => state.requirements.has(id));
55
+ }
56
+ /** Clear the read set (e.g. on pause, so resuming forces a fresh read). */
57
+ export function invalidateReads() {
58
+ state = { features: new Set(), phases: new Set(), requirements: new Set() };
59
+ }
60
+ /**
61
+ * Non-blocking advisory (mirrors the NO ACTIVE TASK pattern): empty when the
62
+ * target's parent feature + phase have both been read, otherwise a loud
63
+ * READ REQUIRED notice the agent cannot silently skip.
64
+ */
65
+ export function parentReadAdvisory(featureId, phaseId) {
66
+ if (hasReadParents(featureId, phaseId))
67
+ return "";
68
+ return "\n\n⚠️ READ REQUIRED before proceeding: read the parent feature and phase (full=true) for this task before starting or resuming it. One feature+phase read covers every task in that phase; a pause/resume invalidates the read and requires re-reading.";
69
+ }
70
+ /**
71
+ * Non-blocking advisory (mirrors parentReadAdvisory): empty when every linked
72
+ * requirement (phase + feature) has been explicitly read, otherwise a loud
73
+ * REQUIREMENTS READ REQUIRED notice. An empty list means the phase/feature has
74
+ * no linked requirements, so nothing is required.
75
+ */
76
+ export function requirementReadAdvisory(requirementIds) {
77
+ if (requirementIds.length === 0)
78
+ return "";
79
+ const unread = requirementIds.filter((id) => !state.requirements.has(id));
80
+ if (unread.length === 0)
81
+ return "";
82
+ return "\n\n⚠️ REQUIREMENTS READ REQUIRED before proceeding: read the requirements linked to this phase (and feature) before starting or resuming the task. Use the requirement list tool to read them (one read covers every task in that phase; a pause/resume invalidates the read).";
83
+ }
84
+ /** Snapshot for diagnostics/tests. */
85
+ export function readTrackingSnapshot() {
86
+ return { features: [...state.features], phases: [...state.phases], requirements: [...state.requirements] };
87
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmOhH"}
1
+ {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC;AAExC,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,GAAE,YAAiB,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA4OhH"}
package/dist/recap.js CHANGED
@@ -33,20 +33,20 @@ export async function buildRecap(st, web = {}, opts = {}) {
33
33
  const totalT = allTasks.length;
34
34
  const doneT = allTasks.filter(({ task }) => task.status === "done").length;
35
35
  const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
36
- const pausedTasks = allTasks.filter(({ task }) => task.status === "paused");
37
- const pausedT = pausedTasks.length;
36
+ const checkpointedTasks = allTasks.filter(({ task }) => !["done", "canceled", "rejected"].includes(task.status) && task.pauseSnapshot);
37
+ const checkpointedT = checkpointedTasks.length;
38
38
  const pendingDeviation = [...plan.project.workDeviations]
39
39
  .filter((deviation) => deviation.state === "resume-required" || deviation.state === "resolved")
40
40
  .sort((left, right) => right.createdAt.localeCompare(left.createdAt))
41
41
  .find((deviation) => allTasks.some(({ task }) => task.id === deviation.resumeTaskId
42
- && (task.status === "paused" || task.status === "planned" || task.status === "waiting")));
42
+ && (task.status === "planned" || task.status === "waiting" || task.status === "in-progress")));
43
43
  const pendingResume = pendingDeviation
44
44
  ? allTasks.find(({ task }) => task.id === pendingDeviation.resumeTaskId)
45
45
  : undefined;
46
- const standalonePaused = pausedTasks
46
+ const standaloneCheckpoints = checkpointedTasks
47
47
  .filter(({ task }) => task.id !== pendingResume?.task.id)
48
48
  .sort((left, right) => (right.task.pauseSnapshot?.pausedAt ?? "").localeCompare(left.task.pauseSnapshot?.pausedAt ?? ""));
49
- const latestStandalonePaused = standalonePaused[0];
49
+ const latestStandaloneCheckpoint = standaloneCheckpoints[0];
50
50
  // Plan is fully complete: there is work and all of it is done, nothing active.
51
51
  // (totalT > 0 guards the empty/unstarted case from looking "complete".)
52
52
  const planComplete = totalT > 0 && doneT === totalT && doneP === totalP && doneF === totalF;
@@ -63,28 +63,34 @@ export async function buildRecap(st, web = {}, opts = {}) {
63
63
  const featureAddCmd = cmd("/planner feature add", "planner-feature-add");
64
64
  const phaseAddCmd = cmd("/planner phase add", "planner-phase-add");
65
65
  const handoffShowCmd = cmd("/planner handoff show", "planner-handoff-show");
66
+ const featureShowCmd = cmd("/planner feature show", "planner-feature-show");
67
+ const phaseShowCmd = cmd("/planner phase show", "planner-phase-show");
68
+ const taskShowCmd = cmd("/planner task show", "planner-task-show");
66
69
  const lines = [];
67
70
  lines.push(italian ? "## Ripresa planner" : "## Planner recap");
68
71
  const name = plan.project.name || "(unnamed project)";
69
72
  lines.push(`${italian ? "Progetto" : "Project"}: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
70
73
  lines.push(italian
71
- ? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${pausedT} in pausa)`
72
- : `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${pausedT} paused)`);
74
+ ? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${checkpointedT} con checkpoint)`
75
+ : `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${checkpointedT} with checkpoints)`);
73
76
  if (focusTask && focusPhase) {
74
77
  const fr = focusFeature ? fref(focusFeature.number) : "?";
75
78
  const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
76
79
  const tr = tref(focusTask.task.number);
77
80
  lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${fr} — ${focusFeature?.name ?? "?"} / ${pr} — ${focusPhase.title} / ${tr} — ${focusTask.task.title} (in-progress)`);
81
+ lines.push("", italian
82
+ ? `Questo task è in-progress (lavoro iniziato in una sessione precedente). Prima di continuare, rileggi il contesto completo: ${featureShowCmd} ${fr}, ${phaseShowCmd} ${pr}, ${taskShowCmd} ${tr} (full).`
83
+ : `⚠️ This task is in-progress (work started in a previous session). Before continuing, re-read the full context: ${featureShowCmd} ${fr}, ${phaseShowCmd} ${pr}, ${taskShowCmd} ${tr} (full).`);
78
84
  }
79
85
  else if (pendingResume) {
80
86
  const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
81
87
  const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
82
88
  lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "ripresa obbligatoria" : "resume required"} — ${ref} — ${pendingResume.task.title} (${pendingResume.task.status})`);
83
89
  }
84
- else if (latestStandalonePaused) {
85
- const feature = feats.find((entry) => entry.id === latestStandalonePaused.phase.featureId);
86
- const ref = `${formatPhaseRef(latestStandalonePaused.phase.number, feature?.number)}/${tref(latestStandalonePaused.task.number)}`;
87
- lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "task in pausa da riprendere" : "paused task to resume"} — ${ref} — ${latestStandalonePaused.task.title}`);
90
+ else if (latestStandaloneCheckpoint) {
91
+ const feature = feats.find((entry) => entry.id === latestStandaloneCheckpoint.phase.featureId);
92
+ const ref = `${formatPhaseRef(latestStandaloneCheckpoint.phase.number, feature?.number)}/${tref(latestStandaloneCheckpoint.task.number)}`;
93
+ lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "checkpoint da valutare" : "checkpoint to evaluate"} — ${ref} — ${latestStandaloneCheckpoint.task.title}`);
88
94
  }
89
95
  else if (handoffs.length > 0) {
90
96
  const top = handoffs[0];
@@ -107,15 +113,15 @@ export async function buildRecap(st, web = {}, opts = {}) {
107
113
  const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
108
114
  const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
109
115
  lines.push(italian
110
- ? `Prossimo step: riprendi ${ref} con ${taskStartCmd}. Il checkpoint del task è prioritario rispetto a nuovo lavoro e handoff di fase.`
111
- : `Next step: resume ${ref} with ${taskStartCmd}. Its task checkpoint takes precedence over new work and phase handoffs.`);
116
+ ? `Avviso ripresa: valuta se riprendere ${ref} con ${taskStartCmd}. Il checkpoint del task va letto prima di decidere se tornare su questo lavoro o proseguire altrove.`
117
+ : `Resume advisory: evaluate whether to resume ${ref} with ${taskStartCmd}. Read its checkpoint before deciding whether to return to this work or continue elsewhere.`);
112
118
  }
113
- else if (latestStandalonePaused) {
114
- const feature = feats.find((entry) => entry.id === latestStandalonePaused.phase.featureId);
115
- const ref = `${formatPhaseRef(latestStandalonePaused.phase.number, feature?.number)}/${tref(latestStandalonePaused.task.number)}`;
119
+ else if (latestStandaloneCheckpoint) {
120
+ const feature = feats.find((entry) => entry.id === latestStandaloneCheckpoint.phase.featureId);
121
+ const ref = `${formatPhaseRef(latestStandaloneCheckpoint.phase.number, feature?.number)}/${tref(latestStandaloneCheckpoint.task.number)}`;
116
122
  lines.push(italian
117
- ? `Prossimo step: riprendi il checkpoint più recente, ${ref}, con ${taskStartCmd}, prima di scegliere nuovo lavoro.`
118
- : `Next step: resume the newest checkpoint, ${ref}, with ${taskStartCmd} before selecting new work.`);
123
+ ? `Avviso ripresa: valuta il checkpoint più recente, ${ref}, con ${taskStartCmd}, prima di scegliere se tornare su quel lavoro o avviarne altro.`
124
+ : `Resume advisory: evaluate the newest checkpoint, ${ref}, with ${taskStartCmd} before deciding whether to return to it or start other work.`);
119
125
  }
120
126
  else if (handoffs.length > 0) {
121
127
  lines.push(italian
@@ -137,11 +143,11 @@ export async function buildRecap(st, web = {}, opts = {}) {
137
143
  const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
138
144
  const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
139
145
  const snapshot = pendingResume.task.pauseSnapshot ?? pendingDeviation?.snapshot;
140
- lines.push("", italian ? "## Ripresa task obbligatoria" : "## Task resume required", `${ref} — ${pendingResume.task.title}`, snapshot ? `${italian ? "Perché è stato sospeso" : "Paused because"}: ${snapshot.reason}` : "", snapshot ? `${italian ? "Stato del lavoro" : "Work checkpoint"}: ${snapshot.whatWasBeingDone}` : "", snapshot ? `${italian ? "Riprendi da" : "Resume from"}: ${snapshot.resumeLocation}` : "", snapshot ? `${italian ? "Come riprendere" : "How to resume"}: ${snapshot.howToResume}` : "", `${italian ? "Azione" : "Action"}: ${taskStartCmd} ${ref}`);
146
+ lines.push("", italian ? "## Avviso ripresa task" : "## Task resume advisory", `${ref} — ${pendingResume.task.title}`, snapshot ? `${italian ? "Perché è stato salvato il checkpoint" : "Checkpoint reason"}: ${snapshot.reason}` : "", snapshot ? `${italian ? "Stato del lavoro" : "Work checkpoint"}: ${snapshot.whatWasBeingDone}` : "", snapshot ? `${italian ? "Riprendi da" : "Resume from"}: ${snapshot.resumeLocation}` : "", snapshot ? `${italian ? "Come riprendere" : "How to resume"}: ${snapshot.howToResume}` : "", italian ? `Azione suggerita: valuta ${taskStartCmd} ${ref}` : `Suggested action: evaluate ${taskStartCmd} ${ref}`);
141
147
  }
142
- if (standalonePaused.length > 0) {
143
- lines.push("", italian ? `## Task in pausa (${standalonePaused.length})` : `## Paused tasks (${standalonePaused.length})`);
144
- for (const entry of standalonePaused) {
148
+ if (standaloneCheckpoints.length > 0) {
149
+ lines.push("", italian ? `## Checkpoint salvati (${standaloneCheckpoints.length})` : `## Saved checkpoints (${standaloneCheckpoints.length})`);
150
+ for (const entry of standaloneCheckpoints) {
145
151
  const feature = feats.find((item) => item.id === entry.phase.featureId);
146
152
  const ref = `${formatPhaseRef(entry.phase.number, feature?.number)}/${tref(entry.task.number)}`;
147
153
  const snapshot = entry.task.pauseSnapshot;
@@ -152,7 +158,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
152
158
  const top = handoffs[0];
153
159
  lines.push("", italian ? `## Handoff di fase pendenti (${handoffs.length})` : `## Pending phase handoffs (${handoffs.length})`);
154
160
  handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
155
- if (pendingResume || latestStandalonePaused) {
161
+ if (pendingResume || latestStandaloneCheckpoint) {
156
162
  lines.push("", italian
157
163
  ? "→ Questi handoff restano disponibili come contesto, ma prima risolvi il task da riprendere indicato sopra."
158
164
  : "→ These handoffs remain available as context, but first resolve the task resume shown above.");
@@ -173,7 +179,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
173
179
  ? `Piano completo — aggiungi una nuova feature (${featureAddCmd}) o fase (${phaseAddCmd}) per continuare.`
174
180
  : `Plan complete — add a new feature (${featureAddCmd}) or phase (${phaseAddCmd}) to continue.`);
175
181
  }
176
- else if (activeT === 0 && !pendingResume && standalonePaused.length === 0) {
182
+ else if (activeT === 0 && !pendingResume && standaloneCheckpoints.length === 0) {
177
183
  lines.push("", italian
178
184
  ? `Nessun handoff pendente e nessun task in-progress. Usa ${taskAddCmd} / ${taskStartCmd} per iniziare.`
179
185
  : `No phase handoff pending and no task in-progress. Use ${taskAddCmd} / ${taskStartCmd} to begin work.`);