@agent-plan/core 0.2.23 → 0.2.25

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,49 +1,95 @@
1
1
  /**
2
- * Per-session, ordered context-read enforcement for agent lifecycle operations.
2
+ * Session-scoped, ordered context-read enforcement for agent lifecycle operations.
3
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.
4
+ * The first complete read for a task must be task(full) phase(full)
5
+ * feature(full), with linked requirements read independently. A persisted
6
+ * sessionInfo attestation may satisfy later checks in the same session while
7
+ * the entity's context revision remains at or before its attestation timestamp.
11
8
  */
9
+ type SessionInfoEntry = {
10
+ sessionId: string;
11
+ createdAt: string;
12
+ };
13
+ type ReadTrackedEntity = {
14
+ updatedAt: string;
15
+ descriptionUpdatedAt?: string;
16
+ sessionInfo?: SessionInfoEntry[];
17
+ };
18
+ export type ContextReadEntityKind = "task" | "phase" | "feature";
19
+ export type RequiredContextRead = {
20
+ kind: ContextReadEntityKind;
21
+ id: string;
22
+ state: "missing" | "stale" | "out-of-order";
23
+ };
12
24
  export type ContextReadEligibility = {
13
25
  eligible: boolean;
14
26
  reason: string;
27
+ requiredReads?: RequiredContextRead[];
15
28
  };
16
- /** Record a full feature read. */
29
+ export interface SessionContextReadInput {
30
+ sessionId: string;
31
+ taskId: string;
32
+ phaseId: string;
33
+ featureId?: string;
34
+ task?: ReadTrackedEntity;
35
+ phase?: ReadTrackedEntity;
36
+ feature?: ReadTrackedEntity;
37
+ requirements?: Array<ReadTrackedEntity & {
38
+ id: string;
39
+ }>;
40
+ requirementIds?: string[];
41
+ }
42
+ /** Record a full feature read in the default compatibility session. */
17
43
  export declare function markFeatureRead(featureId: string): void;
18
- /** Record a full phase read. The parent feature is intentionally not implied. */
44
+ /** Record a full feature read for an explicit harness session. */
45
+ export declare function markFeatureReadForSessionId(sessionId: string, featureId: string): void;
46
+ /** Record a full phase read in the default compatibility session. */
19
47
  export declare function markPhaseRead(phaseId: string, _featureId?: string): void;
20
- /** Record a full task read. Its parent phase and feature are intentionally not implied. */
48
+ /** Record a full phase read for an explicit harness session. */
49
+ export declare function markPhaseReadForSessionId(sessionId: string, phaseId: string): void;
50
+ /** Record a full task read in the default compatibility session. */
21
51
  export declare function markTaskRead(taskId: string, _phaseId?: string, _featureId?: string): void;
22
- /** Record that a requirement was explicitly read via the requirement list tool. */
52
+ /** Record a full task read for an explicit harness session. */
53
+ export declare function markTaskReadForSessionId(sessionId: string, taskId: string): void;
54
+ /** Record that a requirement was explicitly read in the default session. */
23
55
  export declare function markRequirementRead(requirementId: string): void;
56
+ /** Record that a requirement was explicitly read for an explicit session. */
57
+ export declare function markRequirementReadForSessionId(sessionId: string, requirementId: string): void;
24
58
  /**
25
- * Verify the required task(full) phase(full) feature(full) order for one
26
- * exact task lineage. Orphan phases do not require a feature read.
59
+ * Combine valid persisted attestations with fresh in-memory reads. The initial
60
+ * uncached lineage still requires task phase feature ordering, while a
61
+ * stale or missing individual entity can be reread without discarding valid
62
+ * parent attestations from the same harness session.
27
63
  */
64
+ export declare function contextReadEligibilityForSession(input: SessionContextReadInput): ContextReadEligibility;
65
+ /** Return true only when the persisted attestation covers the current revisions. */
66
+ export declare function hasValidSessionAttestation(input: SessionContextReadInput): boolean;
67
+ /** Compatibility check for the default session and in-memory reads only. */
28
68
  export declare function contextReadEligibility(taskId: string, phaseId: string, featureId?: string): ContextReadEligibility;
69
+ /** Legacy parent-read check retained for callers that only need independent parent state. */
70
+ export declare function hasReadParents(featureId: string | undefined, phaseId: string): boolean;
29
71
  /**
30
- * Legacy parent-read check retained for callers that only need to know whether
31
- * both parent entities have been read. Lifecycle gates must use
32
- * contextReadEligibility so the task and ordering cannot be bypassed.
72
+ * Whether linked requirements are read in memory or have valid persisted
73
+ * attestations for the current session and entity revisions.
33
74
  */
34
- export declare function hasReadParents(featureId: string | undefined, phaseId: string): boolean;
35
- /** Whether every linked requirement has been explicitly read. */
75
+ export declare function hasReadRequirementsForSession(sessionId: string, requirementIds: string[], requirements?: Array<ReadTrackedEntity & {
76
+ id: string;
77
+ }>): boolean;
78
+ /** Legacy requirement check for the default compatibility session. */
36
79
  export declare function hasReadRequirements(requirementIds: string[]): boolean;
37
- /** Clear all read state so later lifecycle work requires fresh context. */
38
- export declare function invalidateReads(): void;
80
+ /** Clear one session's read state, or all state for legacy callers. */
81
+ export declare function invalidateReads(sessionId?: string): void;
82
+ /** Initialize an explicit session without clearing other harness sessions. */
83
+ export declare function startReadSession(sessionId: string): void;
39
84
  /** Compatibility advisory for non-lifecycle callers. */
40
85
  export declare function parentReadAdvisory(featureId: string | undefined, phaseId: string): string;
41
86
  /** Advisory text for the separate linked-requirements gate. */
42
87
  export declare function requirementReadAdvisory(requirementIds: string[]): string;
43
- /** Snapshot for diagnostics and tests. */
44
- export declare function readTrackingSnapshot(): {
88
+ /** Snapshot for diagnostics. */
89
+ export declare function readTrackingSnapshot(sessionId?: string): {
45
90
  features: string[];
46
91
  phases: string[];
47
92
  requirements: string[];
48
93
  };
94
+ export {};
49
95
  //# sourceMappingURL=read-tracking.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"read-tracking.d.ts","sourceRoot":"","sources":["../src/read-tracking.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAeF,kCAAkC;AAClC,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAExE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAEzF;AAED,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAE/D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,sBAAsB,CAmBlH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAItF;AAED,iEAAiE;AACjE,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAErE;AAED,2EAA2E;AAC3E,wBAAgB,eAAe,IAAI,IAAI,CAQtC;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGzF;AAED,+DAA+D;AAC/D,wBAAgB,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,MAAM,CAKxE;AAED,0CAA0C;AAC1C,wBAAgB,oBAAoB,IAAI;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAMvG"}
1
+ {"version":3,"file":"read-tracking.d.ts","sourceRoot":"","sources":["../src/read-tracking.ts"],"names":[],"mappings":"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"}
@@ -1,46 +1,82 @@
1
1
  /**
2
- * Per-session, ordered context-read enforcement for agent lifecycle operations.
2
+ * Session-scoped, ordered context-read enforcement for agent lifecycle operations.
3
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.
4
+ * The first complete read for a task must be task(full) phase(full)
5
+ * feature(full), with linked requirements read independently. A persisted
6
+ * sessionInfo attestation may satisfy later checks in the same session while
7
+ * the entity's context revision remains at or before its attestation timestamp.
11
8
  */
12
- let state = {
9
+ const DEFAULT_SESSION_ID = "__default__";
10
+ const newState = () => ({
13
11
  tasks: new Map(),
14
12
  phases: new Map(),
15
13
  features: new Map(),
16
14
  requirements: new Set(),
17
15
  nextSequence: 0,
18
- };
19
- function record(map, id) {
16
+ });
17
+ const states = new Map([[DEFAULT_SESSION_ID, newState()]]);
18
+ function normalizeSessionId(sessionId) {
19
+ return sessionId?.trim() || DEFAULT_SESSION_ID;
20
+ }
21
+ function stateFor(sessionId) {
22
+ const key = normalizeSessionId(sessionId);
23
+ let state = states.get(key);
24
+ if (!state) {
25
+ state = newState();
26
+ states.set(key, state);
27
+ }
28
+ return state;
29
+ }
30
+ function record(map, id, state) {
20
31
  state.nextSequence += 1;
21
32
  map.set(id, state.nextSequence);
22
33
  }
23
- /** Record a full feature read. */
34
+ function markTaskReadForSession(sessionId, taskId) {
35
+ const state = stateFor(sessionId);
36
+ record(state.tasks, taskId, state);
37
+ }
38
+ function markPhaseReadForSession(sessionId, phaseId) {
39
+ const state = stateFor(sessionId);
40
+ record(state.phases, phaseId, state);
41
+ }
42
+ function markFeatureReadForSession(sessionId, featureId) {
43
+ const state = stateFor(sessionId);
44
+ record(state.features, featureId, state);
45
+ }
46
+ /** Record a full feature read in the default compatibility session. */
24
47
  export function markFeatureRead(featureId) {
25
- record(state.features, featureId);
48
+ markFeatureReadForSession(DEFAULT_SESSION_ID, featureId);
26
49
  }
27
- /** Record a full phase read. The parent feature is intentionally not implied. */
50
+ /** Record a full feature read for an explicit harness session. */
51
+ export function markFeatureReadForSessionId(sessionId, featureId) {
52
+ markFeatureReadForSession(sessionId, featureId);
53
+ }
54
+ /** Record a full phase read in the default compatibility session. */
28
55
  export function markPhaseRead(phaseId, _featureId) {
29
- record(state.phases, phaseId);
56
+ markPhaseReadForSession(DEFAULT_SESSION_ID, phaseId);
57
+ }
58
+ /** Record a full phase read for an explicit harness session. */
59
+ export function markPhaseReadForSessionId(sessionId, phaseId) {
60
+ markPhaseReadForSession(sessionId, phaseId);
30
61
  }
31
- /** Record a full task read. Its parent phase and feature are intentionally not implied. */
62
+ /** Record a full task read in the default compatibility session. */
32
63
  export function markTaskRead(taskId, _phaseId, _featureId) {
33
- record(state.tasks, taskId);
64
+ markTaskReadForSession(DEFAULT_SESSION_ID, taskId);
34
65
  }
35
- /** Record that a requirement was explicitly read via the requirement list tool. */
66
+ /** Record a full task read for an explicit harness session. */
67
+ export function markTaskReadForSessionId(sessionId, taskId) {
68
+ markTaskReadForSession(sessionId, taskId);
69
+ }
70
+ /** Record that a requirement was explicitly read in the default session. */
36
71
  export function markRequirementRead(requirementId) {
37
- state.requirements.add(requirementId);
72
+ stateFor(DEFAULT_SESSION_ID).requirements.add(requirementId);
38
73
  }
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) {
74
+ /** Record that a requirement was explicitly read for an explicit session. */
75
+ export function markRequirementReadForSessionId(sessionId, requirementId) {
76
+ stateFor(sessionId).requirements.add(requirementId);
77
+ }
78
+ function orderedEligibility(sessionId, taskId, phaseId, featureId) {
79
+ const state = stateFor(sessionId);
44
80
  const taskSequence = state.tasks.get(taskId);
45
81
  if (taskSequence === undefined) {
46
82
  return { eligible: false, reason: "Read this exact task with full=true first." };
@@ -57,38 +93,126 @@ export function contextReadEligibility(taskId, phaseId, featureId) {
57
93
  }
58
94
  return { eligible: true, reason: "" };
59
95
  }
96
+ function entityRevision(entity, kind) {
97
+ if ((kind === "phase" || kind === "feature") && entity.descriptionUpdatedAt?.trim()) {
98
+ return entity.descriptionUpdatedAt;
99
+ }
100
+ return entity.updatedAt;
101
+ }
102
+ function storedReadState(entity, sessionId, kind) {
103
+ const entry = entity?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
104
+ if (!entity || !entry)
105
+ return "missing";
106
+ return entityRevision(entity, kind) <= entry.createdAt ? "valid" : "stale";
107
+ }
108
+ function validSessionInfo(entity, sessionId, kind = "requirement") {
109
+ return storedReadState(entity, sessionId, kind) === "valid";
110
+ }
111
+ function persistedEligibility(input) {
112
+ if (!validSessionInfo(input.task, input.sessionId, "task") || !validSessionInfo(input.phase, input.sessionId, "phase"))
113
+ return false;
114
+ if (input.featureId && !validSessionInfo(input.feature, input.sessionId, "feature"))
115
+ return false;
116
+ const requirementIds = input.requirementIds ?? [];
117
+ return requirementIds.every((id) => validSessionInfo(input.requirements?.find((requirement) => requirement.id === id), input.sessionId));
118
+ }
119
+ function requiredReadReason(requiredReads) {
120
+ const labels = requiredReads.map((read) => `${read.kind} ${read.id} (${read.state})`);
121
+ return `Read required context only for: ${labels.join(", ")}. Follow these reads in the listed order, then retry.`;
122
+ }
60
123
  /**
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.
124
+ * Combine valid persisted attestations with fresh in-memory reads. The initial
125
+ * uncached lineage still requires task phase feature ordering, while a
126
+ * stale or missing individual entity can be reread without discarding valid
127
+ * parent attestations from the same harness session.
64
128
  */
129
+ export function contextReadEligibilityForSession(input) {
130
+ const state = stateFor(input.sessionId);
131
+ const requiredReads = [];
132
+ const taskStored = storedReadState(input.task, input.sessionId, "task");
133
+ const taskSequence = state.tasks.get(input.taskId);
134
+ const taskReady = taskStored === "valid" || taskSequence !== undefined;
135
+ if (!taskReady) {
136
+ requiredReads.push({ kind: "task", id: input.taskId, state: taskStored === "stale" ? "stale" : "missing" });
137
+ }
138
+ const phaseStored = storedReadState(input.phase, input.sessionId, "phase");
139
+ const phaseSequence = state.phases.get(input.phaseId);
140
+ const phaseReadInOrder = phaseSequence !== undefined
141
+ && (taskStored === "valid" || (taskSequence !== undefined && phaseSequence > taskSequence));
142
+ const phaseReady = phaseStored === "valid" || phaseReadInOrder;
143
+ if (!phaseReady) {
144
+ requiredReads.push({
145
+ kind: "phase",
146
+ id: input.phaseId,
147
+ state: phaseStored === "stale" ? "stale" : phaseSequence !== undefined ? "out-of-order" : "missing",
148
+ });
149
+ }
150
+ if (input.featureId) {
151
+ const featureStored = storedReadState(input.feature, input.sessionId, "feature");
152
+ const featureSequence = state.features.get(input.featureId);
153
+ const featureReadInOrder = featureSequence !== undefined
154
+ && (phaseStored === "valid" || (phaseReadInOrder && phaseSequence !== undefined && featureSequence > phaseSequence));
155
+ if (featureStored !== "valid" && !featureReadInOrder) {
156
+ requiredReads.push({
157
+ kind: "feature",
158
+ id: input.featureId,
159
+ state: featureStored === "stale" ? "stale" : featureSequence !== undefined ? "out-of-order" : "missing",
160
+ });
161
+ }
162
+ }
163
+ if (requiredReads.length === 0)
164
+ return { eligible: true, reason: "" };
165
+ return { eligible: false, reason: requiredReadReason(requiredReads), requiredReads };
166
+ }
167
+ /** Return true only when the persisted attestation covers the current revisions. */
168
+ export function hasValidSessionAttestation(input) {
169
+ return persistedEligibility(input);
170
+ }
171
+ /** Compatibility check for the default session and in-memory reads only. */
172
+ export function contextReadEligibility(taskId, phaseId, featureId) {
173
+ return orderedEligibility(DEFAULT_SESSION_ID, taskId, phaseId, featureId);
174
+ }
175
+ /** Legacy parent-read check retained for callers that only need independent parent state. */
65
176
  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;
177
+ const state = stateFor(DEFAULT_SESSION_ID);
178
+ return state.phases.has(phaseId) && (!featureId || state.features.has(featureId));
179
+ }
180
+ /**
181
+ * Whether linked requirements are read in memory or have valid persisted
182
+ * attestations for the current session and entity revisions.
183
+ */
184
+ export function hasReadRequirementsForSession(sessionId, requirementIds, requirements = []) {
185
+ const state = stateFor(sessionId);
186
+ return requirementIds.every((id) => state.requirements.has(id) || validSessionInfo(requirements.find((requirement) => requirement.id === id), sessionId));
69
187
  }
70
- /** Whether every linked requirement has been explicitly read. */
188
+ /** Legacy requirement check for the default compatibility session. */
71
189
  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
- };
190
+ return requirementIds.every((id) => stateFor(DEFAULT_SESSION_ID).requirements.has(id));
191
+ }
192
+ /** Clear one session's read state, or all state for legacy callers. */
193
+ export function invalidateReads(sessionId) {
194
+ if (sessionId) {
195
+ states.delete(normalizeSessionId(sessionId));
196
+ stateFor(sessionId);
197
+ return;
198
+ }
199
+ states.clear();
200
+ states.set(DEFAULT_SESSION_ID, newState());
201
+ }
202
+ /** Initialize an explicit session without clearing other harness sessions. */
203
+ export function startReadSession(sessionId) {
204
+ stateFor(sessionId);
83
205
  }
84
206
  /** Compatibility advisory for non-lifecycle callers. */
85
207
  export function parentReadAdvisory(featureId, phaseId) {
86
- if (hasReadParents(featureId, phaseId))
208
+ const state = stateFor(DEFAULT_SESSION_ID);
209
+ if (state.phases.has(phaseId) && (!featureId || state.features.has(featureId)))
87
210
  return "";
88
211
  return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
89
212
  }
90
213
  /** Advisory text for the separate linked-requirements gate. */
91
214
  export function requirementReadAdvisory(requirementIds) {
215
+ const state = stateFor(DEFAULT_SESSION_ID);
92
216
  if (requirementIds.length === 0)
93
217
  return "";
94
218
  const unread = requirementIds.filter((id) => !state.requirements.has(id));
@@ -96,8 +220,9 @@ export function requirementReadAdvisory(requirementIds) {
96
220
  return "";
97
221
  return "\n\n⚠️ REQUIREMENTS READ REQUIRED before proceeding: read the requirements linked to this phase and feature.";
98
222
  }
99
- /** Snapshot for diagnostics and tests. */
100
- export function readTrackingSnapshot() {
223
+ /** Snapshot for diagnostics. */
224
+ export function readTrackingSnapshot(sessionId) {
225
+ const state = stateFor(sessionId);
101
226
  return {
102
227
  features: [...state.features.keys()],
103
228
  phases: [...state.phases.keys()],
@@ -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,CA4OhH"}
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,CAuOhH"}
package/dist/recap.js CHANGED
@@ -63,9 +63,6 @@ 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");
69
66
  const lines = [];
70
67
  lines.push(italian ? "## Ripresa planner" : "## Planner recap");
71
68
  const name = plan.project.name || "(unnamed project)";
@@ -78,9 +75,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
78
75
  const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
79
76
  const tr = tref(focusTask.task.number);
80
77
  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
+ lines.push("", `Continue with ${taskStartCmd} ${tr}. If context reads are required, follow only the missing or stale reads in its nextActions, then retry.`);
84
79
  }
85
80
  else if (pendingResume) {
86
81
  const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);