@agent-plan/core 0.2.21 → 0.2.22-next.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/display-status.d.ts +2 -4
- package/dist/display-status.d.ts.map +1 -1
- package/dist/display-status.js +4 -13
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/package-version.d.ts +9 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +35 -0
- package/dist/plan-store.d.ts +21 -9
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +232 -70
- package/dist/planner-rules.d.ts +21 -0
- package/dist/planner-rules.d.ts.map +1 -0
- package/dist/planner-rules.js +58 -0
- package/dist/read-tracking.d.ts +58 -0
- package/dist/read-tracking.d.ts.map +1 -0
- package/dist/read-tracking.js +87 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +39 -24
- package/dist/schema.d.ts +227 -227
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -6
- package/dist/task-selection.d.ts +37 -0
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +38 -10
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/recap.d.ts.map
CHANGED
|
@@ -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,
|
|
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,CAyPhH"}
|
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
|
|
37
|
-
const
|
|
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 === "
|
|
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
|
|
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
|
|
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, ${
|
|
72
|
-
: `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${
|
|
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 (
|
|
85
|
-
const feature = feats.find((entry) => entry.id ===
|
|
86
|
-
const ref = `${formatPhaseRef(
|
|
87
|
-
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "
|
|
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];
|
|
@@ -98,6 +104,15 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
98
104
|
else {
|
|
99
105
|
lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo — rivedi il piano e scegli il prossimo task concreto" : "no active task — review the plan and pick the next concrete task"}`);
|
|
100
106
|
}
|
|
107
|
+
// Planner extension rules — the agent-behavior contract for every project
|
|
108
|
+
// using the extension. Static (no timestamps), loaded from .planner/rules.json
|
|
109
|
+
// or the canonical code set. Kept out of project.json so it never diverges
|
|
110
|
+
// across worktrees/branches.
|
|
111
|
+
const extensionRules = await st.extensionRules();
|
|
112
|
+
if (extensionRules.length > 0) {
|
|
113
|
+
lines.push("", italian ? "## Regole del planner (estensione)" : "## Planner rules (extension)");
|
|
114
|
+
extensionRules.forEach((rule, i) => lines.push(`${i + 1}. ${rule}`));
|
|
115
|
+
}
|
|
101
116
|
// Next step: the phase handoff (phase.handoff) is the authoritative,
|
|
102
117
|
// actively-managed resume context. When a handoff is pending, point to it
|
|
103
118
|
// instead of surfacing potentially-stale resume.json nextSteps. When no
|
|
@@ -107,15 +122,15 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
107
122
|
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
108
123
|
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
109
124
|
lines.push(italian
|
|
110
|
-
? `
|
|
111
|
-
: `
|
|
125
|
+
? `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.`
|
|
126
|
+
: `Resume advisory: evaluate whether to resume ${ref} with ${taskStartCmd}. Read its checkpoint before deciding whether to return to this work or continue elsewhere.`);
|
|
112
127
|
}
|
|
113
|
-
else if (
|
|
114
|
-
const feature = feats.find((entry) => entry.id ===
|
|
115
|
-
const ref = `${formatPhaseRef(
|
|
128
|
+
else if (latestStandaloneCheckpoint) {
|
|
129
|
+
const feature = feats.find((entry) => entry.id === latestStandaloneCheckpoint.phase.featureId);
|
|
130
|
+
const ref = `${formatPhaseRef(latestStandaloneCheckpoint.phase.number, feature?.number)}/${tref(latestStandaloneCheckpoint.task.number)}`;
|
|
116
131
|
lines.push(italian
|
|
117
|
-
? `
|
|
118
|
-
: `
|
|
132
|
+
? `Avviso ripresa: valuta il checkpoint più recente, ${ref}, con ${taskStartCmd}, prima di scegliere se tornare su quel lavoro o avviarne altro.`
|
|
133
|
+
: `Resume advisory: evaluate the newest checkpoint, ${ref}, with ${taskStartCmd} before deciding whether to return to it or start other work.`);
|
|
119
134
|
}
|
|
120
135
|
else if (handoffs.length > 0) {
|
|
121
136
|
lines.push(italian
|
|
@@ -137,11 +152,11 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
137
152
|
const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
|
|
138
153
|
const ref = `${formatPhaseRef(pendingResume.phase.number, feature?.number)}/${tref(pendingResume.task.number)}`;
|
|
139
154
|
const snapshot = pendingResume.task.pauseSnapshot ?? pendingDeviation?.snapshot;
|
|
140
|
-
lines.push("", italian ? "##
|
|
155
|
+
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
156
|
}
|
|
142
|
-
if (
|
|
143
|
-
lines.push("", italian ? `##
|
|
144
|
-
for (const entry of
|
|
157
|
+
if (standaloneCheckpoints.length > 0) {
|
|
158
|
+
lines.push("", italian ? `## Checkpoint salvati (${standaloneCheckpoints.length})` : `## Saved checkpoints (${standaloneCheckpoints.length})`);
|
|
159
|
+
for (const entry of standaloneCheckpoints) {
|
|
145
160
|
const feature = feats.find((item) => item.id === entry.phase.featureId);
|
|
146
161
|
const ref = `${formatPhaseRef(entry.phase.number, feature?.number)}/${tref(entry.task.number)}`;
|
|
147
162
|
const snapshot = entry.task.pauseSnapshot;
|
|
@@ -152,7 +167,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
152
167
|
const top = handoffs[0];
|
|
153
168
|
lines.push("", italian ? `## Handoff di fase pendenti (${handoffs.length})` : `## Pending phase handoffs (${handoffs.length})`);
|
|
154
169
|
handoffs.forEach((h, i) => lines.push(`[${i + 1}] ${h.compositeRef} — ${h.updatedAt} — "${h.firstLine}"`));
|
|
155
|
-
if (pendingResume ||
|
|
170
|
+
if (pendingResume || latestStandaloneCheckpoint) {
|
|
156
171
|
lines.push("", italian
|
|
157
172
|
? "→ Questi handoff restano disponibili come contesto, ma prima risolvi il task da riprendere indicato sopra."
|
|
158
173
|
: "→ These handoffs remain available as context, but first resolve the task resume shown above.");
|
|
@@ -173,7 +188,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
|
|
|
173
188
|
? `Piano completo — aggiungi una nuova feature (${featureAddCmd}) o fase (${phaseAddCmd}) per continuare.`
|
|
174
189
|
: `Plan complete — add a new feature (${featureAddCmd}) or phase (${phaseAddCmd}) to continue.`);
|
|
175
190
|
}
|
|
176
|
-
else if (activeT === 0 && !pendingResume &&
|
|
191
|
+
else if (activeT === 0 && !pendingResume && standaloneCheckpoints.length === 0) {
|
|
177
192
|
lines.push("", italian
|
|
178
193
|
? `Nessun handoff pendente e nessun task in-progress. Usa ${taskAddCmd} / ${taskStartCmd} per iniziare.`
|
|
179
194
|
: `No phase handoff pending and no task in-progress. Use ${taskAddCmd} / ${taskStartCmd} to begin work.`);
|