@agent-plan/core 0.2.24 → 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,13 +1,3 @@
1
- /**
2
- * Canonical planner extension rules — the agent-behavior contract that applies
3
- * to EVERY project using the Agent Plan extension (Pi, MCP / Claude Code / Codex,
4
- * future harnesses). These are STATIC: no timestamps, no dynamic content, so the
5
- * same text seeds every .planner/ and never diverges across worktrees or
6
- * branches (no conflict from date/timestamp changes).
7
- *
8
- * AGENTS.md governs ONLY the development of the agent-plan extension and must
9
- * not duplicate these rules.
10
- */
11
1
  export declare const PLANNER_EXTENSION_RULES: string[];
12
2
  export interface ExtensionRulesFile {
13
3
  extensionRules: string[];
@@ -15,7 +5,9 @@ export interface ExtensionRulesFile {
15
5
  /**
16
6
  * Load the effective extension rules for a planner root. Returns the project's
17
7
  * own .planner/rules.json (static, user-overridable) when present and non-empty,
18
- * otherwise the canonical code set. Never returns timestamps or dynamic data.
8
+ * otherwise the canonical code set. Exact legacy canonical read-protocol rules
9
+ * are upgraded in memory without rewriting the project file; unrelated project
10
+ * overrides remain untouched. Never returns timestamps or dynamic data.
19
11
  */
20
12
  export declare function loadExtensionRules(plannerRoot: string): Promise<string[]>;
21
13
  //# sourceMappingURL=planner-rules.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"planner-rules.d.ts","sourceRoot":"","sources":["../src/planner-rules.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAM,EA2B3C,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/E"}
1
+ {"version":3,"file":"planner-rules.d.ts","sourceRoot":"","sources":["../src/planner-rules.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,uBAAuB,EAAE,MAAM,EA2B3C,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAUD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/E"}
@@ -10,6 +10,10 @@ import { join } from "node:path";
10
10
  * AGENTS.md governs ONLY the development of the agent-plan extension and must
11
11
  * not duplicate these rules.
12
12
  */
13
+ const DETAIL_WRITING_RULE = "Write relevant points (decisions, constraints, current state, file:line refs, edge cases) into the task/phase/feature description or notes as soon as they emerge. For starting, resuming, or switching, call the lifecycle tool first so valid session attestations can be reused. If it denies the operation, perform only the missing or stale full reads listed in nextActions, in that exact order, then retry; read linked requirements explicitly only when requested. Cite entities with composite IDs, not bare UUIDs.";
14
+ const EXPECTED_OPERATIONAL_RULE = "When you begin work, task_start and task_switch enforce session-scoped context reads and return precise missing/stale read actions when needed. Read any relevant phase handoff as additional context, then update the planner before and after significant changes. If you change an architectural decision, document it explicitly.";
15
+ const LEGACY_DETAIL_WRITING_RULE = "Write relevant points (decisions, constraints, current state, file:line refs, edge cases) into the task/phase/feature description or notes as soon as they emerge. Before starting, resuming, or switching to a task, read task_get(full=true), then its parent phase_get(full=true), then its parent feature_get(full=true), in that exact order; read linked requirements explicitly when present. Cite entities with composite IDs, not bare UUIDs.";
16
+ const LEGACY_EXPECTED_OPERATIONAL_RULE = "When you begin work, task_start and task_switch enforce the required ordered full reads. Read any relevant phase handoff as additional context, then update the planner before and after significant changes. If you change an architectural decision, document it explicitly.";
13
17
  export const PLANNER_EXTENSION_RULES = [
14
18
  // §1 — source of truth
15
19
  "Keep the planner as the single operational source of truth while working: read the relevant planner state before starting; update it when an activity starts, changes state, blocks, or concludes; and record next steps, blockers, and decisions in the relevant planner entities. Never leave work only in the conversation.",
@@ -31,24 +35,35 @@ export const PLANNER_EXTENSION_RULES = [
31
35
  "Handoff is per-phase (phase.handoff), not a file. Write it only on explicit user request and only after the exact feature+phase target is confirmed. Run handoff_prepare for that phase, reconcile all still-relevant existing content into one active handoff, and synchronize durable task/phase/feature context in the same handoff_write operation. A pending handoff never blocks task_start and is archived only when the phase completes or the user explicitly clears it; refreshing it must not create superseded copies.",
32
36
  // §12 — operational hygiene
33
37
  "Operational hygiene: start the task (task_start) before thinking about implementation; complete it (task_complete) as part of delivering the deliverable, not after; motivate every block so a third party can understand the impediment.",
34
- // Avvio del planner
38
+ // Planner startup
35
39
  "The planner and Web UI never start automatically. Do not start the Web UI or show its URL unless the user runs load/recap/web-status. The Web UI URL appears only in the recap after load, or on explicit web status.",
36
- // Regola dettagli
37
- "Write relevant points (decisions, constraints, current state, file:line refs, edge cases) into the task/phase/feature description or notes as soon as they emerge. Before starting, resuming, or switching to a task, read task_get(full=true), then its parent phase_get(full=true), then its parent feature_get(full=true), in that exact order; read linked requirements explicitly when present. Cite entities with composite IDs, not bare UUIDs.",
40
+ // Detail-writing rule
41
+ DETAIL_WRITING_RULE,
38
42
  // Expected operational behavior
39
- "When you begin work, task_start and task_switch enforce the required ordered full reads. Read any relevant phase handoff as additional context, then update the planner before and after significant changes. If you change an architectural decision, document it explicitly.",
43
+ EXPECTED_OPERATIONAL_RULE,
40
44
  ];
45
+ function normalizeLegacyRules(rules) {
46
+ return rules.map((rule) => {
47
+ if (rule === LEGACY_DETAIL_WRITING_RULE)
48
+ return DETAIL_WRITING_RULE;
49
+ if (rule === LEGACY_EXPECTED_OPERATIONAL_RULE)
50
+ return EXPECTED_OPERATIONAL_RULE;
51
+ return rule;
52
+ });
53
+ }
41
54
  /**
42
55
  * Load the effective extension rules for a planner root. Returns the project's
43
56
  * own .planner/rules.json (static, user-overridable) when present and non-empty,
44
- * otherwise the canonical code set. Never returns timestamps or dynamic data.
57
+ * otherwise the canonical code set. Exact legacy canonical read-protocol rules
58
+ * are upgraded in memory without rewriting the project file; unrelated project
59
+ * overrides remain untouched. Never returns timestamps or dynamic data.
45
60
  */
46
61
  export async function loadExtensionRules(plannerRoot) {
47
62
  try {
48
63
  const raw = await readFile(join(plannerRoot, "rules.json"), "utf8");
49
64
  const parsed = JSON.parse(raw);
50
65
  if (Array.isArray(parsed.extensionRules) && parsed.extensionRules.length > 0) {
51
- return parsed.extensionRules.filter((r) => typeof r === "string" && r.length > 0);
66
+ return normalizeLegacyRules(parsed.extensionRules.filter((r) => typeof r === "string" && r.length > 0));
52
67
  }
53
68
  }
54
69
  catch {
@@ -4,7 +4,7 @@
4
4
  * The first complete read for a task must be task(full) → phase(full) →
5
5
  * feature(full), with linked requirements read independently. A persisted
6
6
  * sessionInfo attestation may satisfy later checks in the same session while
7
- * every entity's updatedAt remains at or before its attestation timestamp.
7
+ * the entity's context revision remains at or before its attestation timestamp.
8
8
  */
9
9
  type SessionInfoEntry = {
10
10
  sessionId: string;
@@ -12,11 +12,19 @@ type SessionInfoEntry = {
12
12
  };
13
13
  type ReadTrackedEntity = {
14
14
  updatedAt: string;
15
+ descriptionUpdatedAt?: string;
15
16
  sessionInfo?: SessionInfoEntry[];
16
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
+ };
17
24
  export type ContextReadEligibility = {
18
25
  eligible: boolean;
19
26
  reason: string;
27
+ requiredReads?: RequiredContextRead[];
20
28
  };
21
29
  export interface SessionContextReadInput {
22
30
  sessionId: string;
@@ -48,9 +56,10 @@ export declare function markRequirementRead(requirementId: string): void;
48
56
  /** Record that a requirement was explicitly read for an explicit session. */
49
57
  export declare function markRequirementReadForSessionId(sessionId: string, requirementId: string): void;
50
58
  /**
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.
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.
54
63
  */
55
64
  export declare function contextReadEligibilityForSession(input: SessionContextReadInput): ContextReadEligibility;
56
65
  /** Return true only when the persisted attestation covers the current revisions. */
@@ -1 +1 @@
1
- {"version":3,"file":"read-tracking.d.ts","sourceRoot":"","sources":["../src/read-tracking.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"read-tracking.d.ts","sourceRoot":"","sources":["../src/read-tracking.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@
4
4
  * The first complete read for a task must be task(full) → phase(full) →
5
5
  * feature(full), with linked requirements read independently. A persisted
6
6
  * sessionInfo attestation may satisfy later checks in the same session while
7
- * every entity's updatedAt remains at or before its attestation timestamp.
7
+ * the entity's context revision remains at or before its attestation timestamp.
8
8
  */
9
9
  const DEFAULT_SESSION_ID = "__default__";
10
10
  const newState = () => ({
@@ -93,37 +93,76 @@ function orderedEligibility(sessionId, taskId, phaseId, featureId) {
93
93
  }
94
94
  return { eligible: true, reason: "" };
95
95
  }
96
- function validSessionInfo(entity, sessionId) {
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) {
97
103
  const entry = entity?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
98
- return Boolean(entity && entry && entity.updatedAt <= entry.createdAt);
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";
99
110
  }
100
111
  function persistedEligibility(input) {
101
- if (!validSessionInfo(input.task, input.sessionId) || !validSessionInfo(input.phase, input.sessionId))
112
+ if (!validSessionInfo(input.task, input.sessionId, "task") || !validSessionInfo(input.phase, input.sessionId, "phase"))
102
113
  return false;
103
- if (input.featureId && !validSessionInfo(input.feature, input.sessionId))
114
+ if (input.featureId && !validSessionInfo(input.feature, input.sessionId, "feature"))
104
115
  return false;
105
116
  const requirementIds = input.requirementIds ?? [];
106
117
  return requirementIds.every((id) => validSessionInfo(input.requirements?.find((requirement) => requirement.id === id), input.sessionId));
107
118
  }
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));
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.`;
111
122
  }
112
123
  /**
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.
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.
116
128
  */
117
129
  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." };
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" });
125
137
  }
126
- return ordered;
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 };
127
166
  }
128
167
  /** Return true only when the persisted attestation covers the current revisions. */
129
168
  export function hasValidSessionAttestation(input) {
@@ -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);
@@ -27,7 +27,7 @@ export function checkExplicitTaskStart(features, phases, taskId, deviations = []
27
27
  return { eligible: false, reason: `Task is not startable from ${candidate.task.status}.` };
28
28
  const parentHasHardBlock = hardUnavailable.has(candidate.phase.status)
29
29
  || (candidate.feature && hardUnavailable.has(candidate.feature.status));
30
- if (parentHasHardBlock || (!isTemporaryOverride && (unavailable.has(candidate.phase.status) || (candidate.feature && unavailable.has(candidate.feature.status))))) {
30
+ if (parentHasHardBlock) {
31
31
  return { eligible: false, reason: "Task belongs to an unavailable phase or feature." };
32
32
  }
33
33
  const taskById = new Map(candidates.map((entry) => [entry.task.id, entry]));
@@ -91,7 +91,7 @@ export function recommendNextTask(features, phases, deviations = [], currentPhas
91
91
  const ready = candidates.filter(({ feature, phase, task }) => {
92
92
  if (task.status !== "planned")
93
93
  return false;
94
- if (unavailable.has(phase.status) || (feature && unavailable.has(feature.status)))
94
+ if (hardUnavailable.has(phase.status) || (feature && hardUnavailable.has(feature.status)))
95
95
  return false;
96
96
  return task.dependsOn.every((id) => byTaskId.get(id)?.task.status === "done");
97
97
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/core",
3
- "version": "0.2.24",
3
+ "version": "0.2.25",
4
4
  "private": false,
5
5
  "description": "Harness-agnostic core for Agent Plan: schemas, persistence, ordering, status rollups, and markdown rendering.",
6
6
  "license": "MIT",