@agent-plan/core 0.2.22 → 0.2.23
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 +3 -5
- package/dist/display-status.d.ts.map +1 -1
- package/dist/display-status.js +5 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/plan-store.d.ts +21 -9
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +234 -71
- 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 +49 -0
- package/dist/read-tracking.d.ts.map +1 -0
- package/dist/read-tracking.js +106 -0
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +30 -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 +38 -1
- package/dist/task-selection.d.ts.map +1 -1
- package/dist/task-selection.js +49 -11
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session, ordered context-read enforcement for agent lifecycle operations.
|
|
3
|
+
*
|
|
4
|
+
* An agent must read the exact task, then its parent phase, then its parent
|
|
5
|
+
* feature with full=true before it can start, resume, or switch to that task.
|
|
6
|
+
* Compact list/identity reads never count. The state is process-local and is
|
|
7
|
+
* cleared on pause, switch, and session startup by the harness adapters.
|
|
8
|
+
*
|
|
9
|
+
* Linked requirements remain a separate explicit-read gate. They are recorded
|
|
10
|
+
* only through the requirement list tool and never by an entity read.
|
|
11
|
+
*/
|
|
12
|
+
let state = {
|
|
13
|
+
tasks: new Map(),
|
|
14
|
+
phases: new Map(),
|
|
15
|
+
features: new Map(),
|
|
16
|
+
requirements: new Set(),
|
|
17
|
+
nextSequence: 0,
|
|
18
|
+
};
|
|
19
|
+
function record(map, id) {
|
|
20
|
+
state.nextSequence += 1;
|
|
21
|
+
map.set(id, state.nextSequence);
|
|
22
|
+
}
|
|
23
|
+
/** Record a full feature read. */
|
|
24
|
+
export function markFeatureRead(featureId) {
|
|
25
|
+
record(state.features, featureId);
|
|
26
|
+
}
|
|
27
|
+
/** Record a full phase read. The parent feature is intentionally not implied. */
|
|
28
|
+
export function markPhaseRead(phaseId, _featureId) {
|
|
29
|
+
record(state.phases, phaseId);
|
|
30
|
+
}
|
|
31
|
+
/** Record a full task read. Its parent phase and feature are intentionally not implied. */
|
|
32
|
+
export function markTaskRead(taskId, _phaseId, _featureId) {
|
|
33
|
+
record(state.tasks, taskId);
|
|
34
|
+
}
|
|
35
|
+
/** Record that a requirement was explicitly read via the requirement list tool. */
|
|
36
|
+
export function markRequirementRead(requirementId) {
|
|
37
|
+
state.requirements.add(requirementId);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Verify the required task(full) → phase(full) → feature(full) order for one
|
|
41
|
+
* exact task lineage. Orphan phases do not require a feature read.
|
|
42
|
+
*/
|
|
43
|
+
export function contextReadEligibility(taskId, phaseId, featureId) {
|
|
44
|
+
const taskSequence = state.tasks.get(taskId);
|
|
45
|
+
if (taskSequence === undefined) {
|
|
46
|
+
return { eligible: false, reason: "Read this exact task with full=true first." };
|
|
47
|
+
}
|
|
48
|
+
const phaseSequence = state.phases.get(phaseId);
|
|
49
|
+
if (phaseSequence === undefined || phaseSequence <= taskSequence) {
|
|
50
|
+
return { eligible: false, reason: "After reading the task, read its parent phase with full=true." };
|
|
51
|
+
}
|
|
52
|
+
if (!featureId)
|
|
53
|
+
return { eligible: true, reason: "" };
|
|
54
|
+
const featureSequence = state.features.get(featureId);
|
|
55
|
+
if (featureSequence === undefined || featureSequence <= phaseSequence) {
|
|
56
|
+
return { eligible: false, reason: "After reading the phase, read its parent feature with full=true." };
|
|
57
|
+
}
|
|
58
|
+
return { eligible: true, reason: "" };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Legacy parent-read check retained for callers that only need to know whether
|
|
62
|
+
* both parent entities have been read. Lifecycle gates must use
|
|
63
|
+
* contextReadEligibility so the task and ordering cannot be bypassed.
|
|
64
|
+
*/
|
|
65
|
+
export function hasReadParents(featureId, phaseId) {
|
|
66
|
+
const phaseOk = state.phases.has(phaseId);
|
|
67
|
+
const featureOk = featureId ? state.features.has(featureId) : true;
|
|
68
|
+
return phaseOk && featureOk;
|
|
69
|
+
}
|
|
70
|
+
/** Whether every linked requirement has been explicitly read. */
|
|
71
|
+
export function hasReadRequirements(requirementIds) {
|
|
72
|
+
return requirementIds.every((id) => state.requirements.has(id));
|
|
73
|
+
}
|
|
74
|
+
/** Clear all read state so later lifecycle work requires fresh context. */
|
|
75
|
+
export function invalidateReads() {
|
|
76
|
+
state = {
|
|
77
|
+
tasks: new Map(),
|
|
78
|
+
phases: new Map(),
|
|
79
|
+
features: new Map(),
|
|
80
|
+
requirements: new Set(),
|
|
81
|
+
nextSequence: 0,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Compatibility advisory for non-lifecycle callers. */
|
|
85
|
+
export function parentReadAdvisory(featureId, phaseId) {
|
|
86
|
+
if (hasReadParents(featureId, phaseId))
|
|
87
|
+
return "";
|
|
88
|
+
return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
|
|
89
|
+
}
|
|
90
|
+
/** Advisory text for the separate linked-requirements gate. */
|
|
91
|
+
export function requirementReadAdvisory(requirementIds) {
|
|
92
|
+
if (requirementIds.length === 0)
|
|
93
|
+
return "";
|
|
94
|
+
const unread = requirementIds.filter((id) => !state.requirements.has(id));
|
|
95
|
+
if (unread.length === 0)
|
|
96
|
+
return "";
|
|
97
|
+
return "\n\n⚠️ REQUIREMENTS READ REQUIRED before proceeding: read the requirements linked to this phase and feature.";
|
|
98
|
+
}
|
|
99
|
+
/** Snapshot for diagnostics and tests. */
|
|
100
|
+
export function readTrackingSnapshot() {
|
|
101
|
+
return {
|
|
102
|
+
features: [...state.features.keys()],
|
|
103
|
+
phases: [...state.phases.keys()],
|
|
104
|
+
requirements: [...state.requirements],
|
|
105
|
+
};
|
|
106
|
+
}
|
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,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
|
|
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: ${taskShowCmd} ${tr} (full=true), ${phaseShowCmd} ${pr} (full=true), ${featureShowCmd} ${fr} (full=true).`
|
|
83
|
+
: `⚠️ This task is in-progress (work started in a previous session). Before continuing, re-read the full context in this order: ${taskShowCmd} ${tr} (full=true), ${phaseShowCmd} ${pr} (full=true), ${featureShowCmd} ${fr} (full=true).`);
|
|
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];
|
|
@@ -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
|
-
? `
|
|
111
|
-
: `
|
|
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 (
|
|
114
|
-
const feature = feats.find((entry) => entry.id ===
|
|
115
|
-
const ref = `${formatPhaseRef(
|
|
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
|
-
? `
|
|
118
|
-
: `
|
|
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 ? "##
|
|
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 (
|
|
143
|
-
lines.push("", italian ? `##
|
|
144
|
-
for (const entry of
|
|
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 ||
|
|
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 &&
|
|
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.`);
|