@agent-plan/core 0.2.23 → 0.2.24

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,86 @@
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
+ * every entity's updatedAt 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
+ sessionInfo?: SessionInfoEntry[];
16
+ };
12
17
  export type ContextReadEligibility = {
13
18
  eligible: boolean;
14
19
  reason: string;
15
20
  };
16
- /** Record a full feature read. */
21
+ export interface SessionContextReadInput {
22
+ sessionId: string;
23
+ taskId: string;
24
+ phaseId: string;
25
+ featureId?: string;
26
+ task?: ReadTrackedEntity;
27
+ phase?: ReadTrackedEntity;
28
+ feature?: ReadTrackedEntity;
29
+ requirements?: Array<ReadTrackedEntity & {
30
+ id: string;
31
+ }>;
32
+ requirementIds?: string[];
33
+ }
34
+ /** Record a full feature read in the default compatibility session. */
17
35
  export declare function markFeatureRead(featureId: string): void;
18
- /** Record a full phase read. The parent feature is intentionally not implied. */
36
+ /** Record a full feature read for an explicit harness session. */
37
+ export declare function markFeatureReadForSessionId(sessionId: string, featureId: string): void;
38
+ /** Record a full phase read in the default compatibility session. */
19
39
  export declare function markPhaseRead(phaseId: string, _featureId?: string): void;
20
- /** Record a full task read. Its parent phase and feature are intentionally not implied. */
40
+ /** Record a full phase read for an explicit harness session. */
41
+ export declare function markPhaseReadForSessionId(sessionId: string, phaseId: string): void;
42
+ /** Record a full task read in the default compatibility session. */
21
43
  export declare function markTaskRead(taskId: string, _phaseId?: string, _featureId?: string): void;
22
- /** Record that a requirement was explicitly read via the requirement list tool. */
44
+ /** Record a full task read for an explicit harness session. */
45
+ export declare function markTaskReadForSessionId(sessionId: string, taskId: string): void;
46
+ /** Record that a requirement was explicitly read in the default session. */
23
47
  export declare function markRequirementRead(requirementId: string): void;
48
+ /** Record that a requirement was explicitly read for an explicit session. */
49
+ export declare function markRequirementReadForSessionId(sessionId: string, requirementId: string): void;
24
50
  /**
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.
51
+ * Evaluate exact in-memory read ordering first, then a persisted attestation
52
+ * for the same session and current entity revisions. A previously persisted
53
+ * but now stale attestation always wins over stale in-memory ordering.
27
54
  */
55
+ export declare function contextReadEligibilityForSession(input: SessionContextReadInput): ContextReadEligibility;
56
+ /** Return true only when the persisted attestation covers the current revisions. */
57
+ export declare function hasValidSessionAttestation(input: SessionContextReadInput): boolean;
58
+ /** Compatibility check for the default session and in-memory reads only. */
28
59
  export declare function contextReadEligibility(taskId: string, phaseId: string, featureId?: string): ContextReadEligibility;
60
+ /** Legacy parent-read check retained for callers that only need independent parent state. */
61
+ export declare function hasReadParents(featureId: string | undefined, phaseId: string): boolean;
29
62
  /**
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.
63
+ * Whether linked requirements are read in memory or have valid persisted
64
+ * attestations for the current session and entity revisions.
33
65
  */
34
- export declare function hasReadParents(featureId: string | undefined, phaseId: string): boolean;
35
- /** Whether every linked requirement has been explicitly read. */
66
+ export declare function hasReadRequirementsForSession(sessionId: string, requirementIds: string[], requirements?: Array<ReadTrackedEntity & {
67
+ id: string;
68
+ }>): boolean;
69
+ /** Legacy requirement check for the default compatibility session. */
36
70
  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;
71
+ /** Clear one session's read state, or all state for legacy callers. */
72
+ export declare function invalidateReads(sessionId?: string): void;
73
+ /** Initialize an explicit session without clearing other harness sessions. */
74
+ export declare function startReadSession(sessionId: string): void;
39
75
  /** Compatibility advisory for non-lifecycle callers. */
40
76
  export declare function parentReadAdvisory(featureId: string | undefined, phaseId: string): string;
41
77
  /** Advisory text for the separate linked-requirements gate. */
42
78
  export declare function requirementReadAdvisory(requirementIds: string[]): string;
43
- /** Snapshot for diagnostics and tests. */
44
- export declare function readTrackingSnapshot(): {
79
+ /** Snapshot for diagnostics. */
80
+ export declare function readTrackingSnapshot(sessionId?: string): {
45
81
  features: string[];
46
82
  phases: string[];
47
83
  requirements: string[];
48
84
  };
85
+ export {};
49
86
  //# 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
+ * every entity's updatedAt 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);
49
+ }
50
+ /** Record a full feature read for an explicit harness session. */
51
+ export function markFeatureReadForSessionId(sessionId, featureId) {
52
+ markFeatureReadForSession(sessionId, featureId);
26
53
  }
27
- /** Record a full phase read. The parent feature is intentionally not implied. */
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);
30
57
  }
31
- /** Record a full task read. Its parent phase and feature are intentionally not implied. */
58
+ /** Record a full phase read for an explicit harness session. */
59
+ export function markPhaseReadForSessionId(sessionId, phaseId) {
60
+ markPhaseReadForSession(sessionId, phaseId);
61
+ }
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);
65
+ }
66
+ /** Record a full task read for an explicit harness session. */
67
+ export function markTaskReadForSessionId(sessionId, taskId) {
68
+ markTaskReadForSession(sessionId, taskId);
34
69
  }
35
- /** Record that a requirement was explicitly read via the requirement list tool. */
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,87 @@ export function contextReadEligibility(taskId, phaseId, featureId) {
57
93
  }
58
94
  return { eligible: true, reason: "" };
59
95
  }
96
+ function validSessionInfo(entity, sessionId) {
97
+ const entry = entity?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
98
+ return Boolean(entity && entry && entity.updatedAt <= entry.createdAt);
99
+ }
100
+ function persistedEligibility(input) {
101
+ if (!validSessionInfo(input.task, input.sessionId) || !validSessionInfo(input.phase, input.sessionId))
102
+ return false;
103
+ if (input.featureId && !validSessionInfo(input.feature, input.sessionId))
104
+ return false;
105
+ const requirementIds = input.requirementIds ?? [];
106
+ return requirementIds.every((id) => validSessionInfo(input.requirements?.find((requirement) => requirement.id === id), input.sessionId));
107
+ }
108
+ function hasStoredSessionAttestation(input) {
109
+ const entities = [input.task, input.phase, input.feature, ...(input.requirements ?? [])];
110
+ return entities.some((entity) => entity?.sessionInfo?.some((entry) => entry.sessionId === input.sessionId));
111
+ }
60
112
  /**
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.
113
+ * Evaluate exact in-memory read ordering first, then a persisted attestation
114
+ * for the same session and current entity revisions. A previously persisted
115
+ * but now stale attestation always wins over stale in-memory ordering.
64
116
  */
117
+ export function contextReadEligibilityForSession(input) {
118
+ const ordered = orderedEligibility(input.sessionId, input.taskId, input.phaseId, input.featureId);
119
+ if (ordered.eligible)
120
+ return ordered;
121
+ if (input.task && input.phase && persistedEligibility(input))
122
+ return { eligible: true, reason: "" };
123
+ if (input.task && input.phase && hasStoredSessionAttestation(input)) {
124
+ return { eligible: false, reason: "Context changed since the last session read; reread the exact task, phase, feature, and linked requirements." };
125
+ }
126
+ return ordered;
127
+ }
128
+ /** Return true only when the persisted attestation covers the current revisions. */
129
+ export function hasValidSessionAttestation(input) {
130
+ return persistedEligibility(input);
131
+ }
132
+ /** Compatibility check for the default session and in-memory reads only. */
133
+ export function contextReadEligibility(taskId, phaseId, featureId) {
134
+ return orderedEligibility(DEFAULT_SESSION_ID, taskId, phaseId, featureId);
135
+ }
136
+ /** Legacy parent-read check retained for callers that only need independent parent state. */
65
137
  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;
138
+ const state = stateFor(DEFAULT_SESSION_ID);
139
+ return state.phases.has(phaseId) && (!featureId || state.features.has(featureId));
69
140
  }
70
- /** Whether every linked requirement has been explicitly read. */
141
+ /**
142
+ * Whether linked requirements are read in memory or have valid persisted
143
+ * attestations for the current session and entity revisions.
144
+ */
145
+ export function hasReadRequirementsForSession(sessionId, requirementIds, requirements = []) {
146
+ const state = stateFor(sessionId);
147
+ return requirementIds.every((id) => state.requirements.has(id) || validSessionInfo(requirements.find((requirement) => requirement.id === id), sessionId));
148
+ }
149
+ /** Legacy requirement check for the default compatibility session. */
71
150
  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
- };
151
+ return requirementIds.every((id) => stateFor(DEFAULT_SESSION_ID).requirements.has(id));
152
+ }
153
+ /** Clear one session's read state, or all state for legacy callers. */
154
+ export function invalidateReads(sessionId) {
155
+ if (sessionId) {
156
+ states.delete(normalizeSessionId(sessionId));
157
+ stateFor(sessionId);
158
+ return;
159
+ }
160
+ states.clear();
161
+ states.set(DEFAULT_SESSION_ID, newState());
162
+ }
163
+ /** Initialize an explicit session without clearing other harness sessions. */
164
+ export function startReadSession(sessionId) {
165
+ stateFor(sessionId);
83
166
  }
84
167
  /** Compatibility advisory for non-lifecycle callers. */
85
168
  export function parentReadAdvisory(featureId, phaseId) {
86
- if (hasReadParents(featureId, phaseId))
169
+ const state = stateFor(DEFAULT_SESSION_ID);
170
+ if (state.phases.has(phaseId) && (!featureId || state.features.has(featureId)))
87
171
  return "";
88
172
  return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
89
173
  }
90
174
  /** Advisory text for the separate linked-requirements gate. */
91
175
  export function requirementReadAdvisory(requirementIds) {
176
+ const state = stateFor(DEFAULT_SESSION_ID);
92
177
  if (requirementIds.length === 0)
93
178
  return "";
94
179
  const unread = requirementIds.filter((id) => !state.requirements.has(id));
@@ -96,8 +181,9 @@ export function requirementReadAdvisory(requirementIds) {
96
181
  return "";
97
182
  return "\n\n⚠️ REQUIREMENTS READ REQUIRED before proceeding: read the requirements linked to this phase and feature.";
98
183
  }
99
- /** Snapshot for diagnostics and tests. */
100
- export function readTrackingSnapshot() {
184
+ /** Snapshot for diagnostics. */
185
+ export function readTrackingSnapshot(sessionId) {
186
+ const state = stateFor(sessionId);
101
187
  return {
102
188
  features: [...state.features.keys()],
103
189
  phases: [...state.phases.keys()],