@agent-plan/core 0.2.25 → 0.2.27

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.
Files changed (66) hide show
  1. package/dist/description-freshness.d.ts +37 -0
  2. package/dist/description-freshness.d.ts.map +1 -0
  3. package/dist/description-freshness.js +84 -0
  4. package/dist/display-status.d.ts +3 -3
  5. package/dist/display-status.d.ts.map +1 -1
  6. package/dist/display-status.js +5 -4
  7. package/dist/handoff-context.d.ts +222 -1
  8. package/dist/handoff-context.d.ts.map +1 -1
  9. package/dist/handoff-context.js +461 -11
  10. package/dist/index.d.ts +8 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +8 -1
  13. package/dist/naming.d.ts +3 -0
  14. package/dist/naming.d.ts.map +1 -1
  15. package/dist/naming.js +7 -0
  16. package/dist/package-version.d.ts +2 -0
  17. package/dist/package-version.d.ts.map +1 -1
  18. package/dist/package-version.js +1 -1
  19. package/dist/payload-fallback.d.ts +38 -0
  20. package/dist/payload-fallback.d.ts.map +1 -0
  21. package/dist/payload-fallback.js +79 -0
  22. package/dist/plan-store.d.ts +192 -36
  23. package/dist/plan-store.d.ts.map +1 -1
  24. package/dist/plan-store.js +1048 -126
  25. package/dist/planner-rules.d.ts.map +1 -1
  26. package/dist/planner-rules.js +9 -3
  27. package/dist/planner-skill.d.ts +24 -0
  28. package/dist/planner-skill.d.ts.map +1 -0
  29. package/dist/planner-skill.js +113 -0
  30. package/dist/project-context-migration.d.ts +47 -0
  31. package/dist/project-context-migration.d.ts.map +1 -0
  32. package/dist/project-context-migration.js +168 -0
  33. package/dist/read-tracking.d.ts +47 -13
  34. package/dist/read-tracking.d.ts.map +1 -1
  35. package/dist/read-tracking.js +88 -33
  36. package/dist/recap.d.ts.map +1 -1
  37. package/dist/recap.js +34 -9
  38. package/dist/refs.d.ts +6 -1
  39. package/dist/refs.d.ts.map +1 -1
  40. package/dist/refs.js +25 -0
  41. package/dist/renderer.d.ts.map +1 -1
  42. package/dist/renderer.js +24 -2
  43. package/dist/requirement-macro-tasks.d.ts +18 -0
  44. package/dist/requirement-macro-tasks.d.ts.map +1 -0
  45. package/dist/requirement-macro-tasks.js +55 -0
  46. package/dist/runtime-diagnostics.d.ts +34 -0
  47. package/dist/runtime-diagnostics.d.ts.map +1 -0
  48. package/dist/runtime-diagnostics.js +39 -0
  49. package/dist/schema.d.ts +1575 -290
  50. package/dist/schema.d.ts.map +1 -1
  51. package/dist/schema.js +89 -4
  52. package/dist/task-context.d.ts +41 -2
  53. package/dist/task-context.d.ts.map +1 -1
  54. package/dist/task-context.js +102 -4
  55. package/dist/task-selection.d.ts +44 -1
  56. package/dist/task-selection.d.ts.map +1 -1
  57. package/dist/task-selection.js +158 -7
  58. package/dist/task-start-outcome.d.ts +1 -1
  59. package/dist/task-start-outcome.d.ts.map +1 -1
  60. package/dist/task-start-outcome.js +1 -0
  61. package/dist/write-coordination.d.ts +27 -0
  62. package/dist/write-coordination.d.ts.map +1 -0
  63. package/dist/write-coordination.js +223 -0
  64. package/package.json +3 -1
  65. package/planner-skill.md +226 -0
  66. package/skills/grill-me/SKILL.md +10 -0
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Session-scoped, ordered context-read enforcement for agent lifecycle operations.
2
+ * Session-scoped context-read enforcement for agent lifecycle operations.
3
3
  *
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.
4
+ * Task/phase/feature reads and linked requirements remain explicit, but fresh
5
+ * in-session reads may be performed in any order. Persisted sessionInfo
6
+ * attestations may satisfy later checks while the entity's current revision
7
+ * remains at or before the attested timestamp.
8
8
  */
9
9
  const DEFAULT_SESSION_ID = "__default__";
10
10
  const newState = () => ({
@@ -51,6 +51,40 @@ export function markFeatureRead(featureId) {
51
51
  export function markFeatureReadForSessionId(sessionId, featureId) {
52
52
  markFeatureReadForSession(sessionId, featureId);
53
53
  }
54
+ function deliveredAcceptedDecisionField(content, value) {
55
+ const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
56
+ return lines.every((line) => content.includes(line));
57
+ }
58
+ /**
59
+ * Record a canonical feature/phase/task full read only after its visible tool
60
+ * content contains every field of every Accepted Decision. This prevents an
61
+ * adapter from certifying a title-only or otherwise incomplete decision read.
62
+ */
63
+ export function markCanonicalFullReadForSessionId(sessionId, kind, entity, deliveredContent) {
64
+ const acceptedDecisions = entity.acceptedDecisions ?? [];
65
+ const omitted = [
66
+ ...(!deliveredContent.includes(`Accepted Decisions (${acceptedDecisions.length}):`) ? ["context-marker"] : []),
67
+ ...acceptedDecisions.flatMap((acceptedDecision) => [
68
+ ["id", acceptedDecision.id],
69
+ ["title", acceptedDecision.title],
70
+ ["decision", acceptedDecision.decision],
71
+ ["rationale", acceptedDecision.rationale],
72
+ ["implementationNotes", acceptedDecision.implementationNotes],
73
+ ["acceptedAt", acceptedDecision.acceptedAt],
74
+ ]
75
+ .filter(([, value]) => !deliveredAcceptedDecisionField(deliveredContent, value))
76
+ .map(([field]) => `${acceptedDecision.id}.${field}`)),
77
+ ];
78
+ if (omitted.length > 0) {
79
+ throw new Error(`Cannot attest ${kind} ${entity.id} as fully read: omitted Accepted Decision fields: ${omitted.join(", ")}.`);
80
+ }
81
+ if (kind === "feature")
82
+ markFeatureReadForSession(sessionId, entity.id);
83
+ else if (kind === "phase")
84
+ markPhaseReadForSession(sessionId, entity.id);
85
+ else
86
+ markTaskReadForSession(sessionId, entity.id);
87
+ }
54
88
  /** Record a full phase read in the default compatibility session. */
55
89
  export function markPhaseRead(phaseId, _featureId) {
56
90
  markPhaseReadForSession(DEFAULT_SESSION_ID, phaseId);
@@ -77,19 +111,14 @@ export function markRequirementReadForSessionId(sessionId, requirementId) {
77
111
  }
78
112
  function orderedEligibility(sessionId, taskId, phaseId, featureId) {
79
113
  const state = stateFor(sessionId);
80
- const taskSequence = state.tasks.get(taskId);
81
- if (taskSequence === undefined) {
114
+ if (!state.tasks.has(taskId)) {
82
115
  return { eligible: false, reason: "Read this exact task with full=true first." };
83
116
  }
84
- const phaseSequence = state.phases.get(phaseId);
85
- if (phaseSequence === undefined || phaseSequence <= taskSequence) {
86
- return { eligible: false, reason: "After reading the task, read its parent phase with full=true." };
117
+ if (!state.phases.has(phaseId)) {
118
+ return { eligible: false, reason: "Read this task's parent phase with full=true." };
87
119
  }
88
- if (!featureId)
89
- return { eligible: true, reason: "" };
90
- const featureSequence = state.features.get(featureId);
91
- if (featureSequence === undefined || featureSequence <= phaseSequence) {
92
- return { eligible: false, reason: "After reading the phase, read its parent feature with full=true." };
120
+ if (featureId && !state.features.has(featureId)) {
121
+ return { eligible: false, reason: "Read this phase's parent feature with full=true." };
93
122
  }
94
123
  return { eligible: true, reason: "" };
95
124
  }
@@ -118,13 +147,14 @@ function persistedEligibility(input) {
118
147
  }
119
148
  function requiredReadReason(requiredReads) {
120
149
  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.`;
150
+ return `Read required context only for: ${labels.join(", ")}. Perform only these reads, in any order within the current session, then retry.`;
122
151
  }
123
152
  /**
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.
153
+ * Combine valid persisted attestations with fresh in-memory reads. An entity read
154
+ * in the current session satisfies eligibility regardless of sequence order,
155
+ * so agents can read task, phase, and feature in any order; only persisted
156
+ * attestations are checked for revision freshness (stale) and missing entities
157
+ * remain reported as missing.
128
158
  */
129
159
  export function contextReadEligibilityForSession(input) {
130
160
  const state = stateFor(input.sessionId);
@@ -137,26 +167,23 @@ export function contextReadEligibilityForSession(input) {
137
167
  }
138
168
  const phaseStored = storedReadState(input.phase, input.sessionId, "phase");
139
169
  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;
170
+ const phaseReady = phaseStored === "valid" || phaseSequence !== undefined;
143
171
  if (!phaseReady) {
144
172
  requiredReads.push({
145
173
  kind: "phase",
146
174
  id: input.phaseId,
147
- state: phaseStored === "stale" ? "stale" : phaseSequence !== undefined ? "out-of-order" : "missing",
175
+ state: phaseStored === "stale" ? "stale" : "missing",
148
176
  });
149
177
  }
150
178
  if (input.featureId) {
151
179
  const featureStored = storedReadState(input.feature, input.sessionId, "feature");
152
180
  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) {
181
+ const featureReady = featureStored === "valid" || featureSequence !== undefined;
182
+ if (!featureReady) {
156
183
  requiredReads.push({
157
184
  kind: "feature",
158
185
  id: input.featureId,
159
- state: featureStored === "stale" ? "stale" : featureSequence !== undefined ? "out-of-order" : "missing",
186
+ state: featureStored === "stale" ? "stale" : "missing",
160
187
  });
161
188
  }
162
189
  }
@@ -164,6 +191,19 @@ export function contextReadEligibilityForSession(input) {
164
191
  return { eligible: true, reason: "" };
165
192
  return { eligible: false, reason: requiredReadReason(requiredReads), requiredReads };
166
193
  }
194
+ export function projectGuidelinesReadStateForSession(project, sessionId) {
195
+ const guidelines = project.projectGuidelines;
196
+ const content = guidelines?.content?.trim() ?? "";
197
+ if (!content)
198
+ return "not-required";
199
+ const entry = guidelines?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
200
+ if (!entry)
201
+ return "missing";
202
+ const updatedAt = guidelines?.updatedAt?.trim() ?? "";
203
+ if (updatedAt && updatedAt > entry.createdAt)
204
+ return "stale";
205
+ return "valid";
206
+ }
167
207
  /** Return true only when the persisted attestation covers the current revisions. */
168
208
  export function hasValidSessionAttestation(input) {
169
209
  return persistedEligibility(input);
@@ -178,12 +218,27 @@ export function hasReadParents(featureId, phaseId) {
178
218
  return state.phases.has(phaseId) && (!featureId || state.features.has(featureId));
179
219
  }
180
220
  /**
181
- * Whether linked requirements are read in memory or have valid persisted
182
- * attestations for the current session and entity revisions.
221
+ * Report the exact linked requirements whose delivered read evidence is missing
222
+ * or stale for this session. A broad requirement inventory must not call the
223
+ * mark function; only a target-scoped response that actually includes the
224
+ * linked requirement records may create fresh in-memory evidence.
183
225
  */
184
- export function hasReadRequirementsForSession(sessionId, requirementIds, requirements = []) {
226
+ export function requirementReadEligibilityForSession(sessionId, requirementIds, requirements = []) {
185
227
  const state = stateFor(sessionId);
186
- return requirementIds.every((id) => state.requirements.has(id) || validSessionInfo(requirements.find((requirement) => requirement.id === id), sessionId));
228
+ const requiredReads = [];
229
+ for (const id of [...new Set(requirementIds)]) {
230
+ const stored = storedReadState(requirements.find((requirement) => requirement.id === id), sessionId, "requirement");
231
+ if (state.requirements.has(id) || stored === "valid")
232
+ continue;
233
+ requiredReads.push({ kind: "requirement", id, state: stored === "stale" ? "stale" : "missing" });
234
+ }
235
+ if (requiredReads.length === 0)
236
+ return { eligible: true, reason: "" };
237
+ return { eligible: false, reason: requiredReadReason(requiredReads), requiredReads };
238
+ }
239
+ /** Whether every linked requirement has fresh delivered read evidence. */
240
+ export function hasReadRequirementsForSession(sessionId, requirementIds, requirements = []) {
241
+ return requirementReadEligibilityForSession(sessionId, requirementIds, requirements).eligible;
187
242
  }
188
243
  /** Legacy requirement check for the default compatibility session. */
189
244
  export function hasReadRequirements(requirementIds) {
@@ -208,7 +263,7 @@ export function parentReadAdvisory(featureId, phaseId) {
208
263
  const state = stateFor(DEFAULT_SESSION_ID);
209
264
  if (state.phases.has(phaseId) && (!featureId || state.features.has(featureId)))
210
265
  return "";
211
- return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
266
+ return "\n\n⚠️ READ REQUIRED before proceeding: read only the missing parent phase and feature with full=true.";
212
267
  }
213
268
  /** Advisory text for the separate linked-requirements gate. */
214
269
  export function requirementReadAdvisory(requirementIds) {
@@ -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,CAuOhH"}
1
+ {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../src/recap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD;;;;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,CA4QhH"}
package/dist/recap.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { formatPhaseRef, formatTwoDigitNumber } from "./naming.js";
2
+ import { buildPhaseWorkMap } from "./task-context.js";
2
3
  const fref = (n) => `F${formatTwoDigitNumber(n)}`;
3
4
  const tref = (n) => `T${formatTwoDigitNumber(n)}`;
4
5
  /**
@@ -32,7 +33,8 @@ export async function buildRecap(st, web = {}, opts = {}) {
32
33
  const activeP = phases.filter((x) => x.status === "in-progress" || x.status === "discovery").length;
33
34
  const totalT = allTasks.length;
34
35
  const doneT = allTasks.filter(({ task }) => task.status === "done").length;
35
- const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
36
+ const activeTasks = allTasks.filter(({ task }) => task.status === "in-progress");
37
+ const activeT = activeTasks.length;
36
38
  const checkpointedTasks = allTasks.filter(({ task }) => !["done", "canceled", "rejected"].includes(task.status) && task.pauseSnapshot);
37
39
  const checkpointedT = checkpointedTasks.length;
38
40
  const pendingDeviation = [...plan.project.workDeviations]
@@ -50,8 +52,9 @@ export async function buildRecap(st, web = {}, opts = {}) {
50
52
  // Plan is fully complete: there is work and all of it is done, nothing active.
51
53
  // (totalT > 0 guards the empty/unstarted case from looking "complete".)
52
54
  const planComplete = totalT > 0 && doneT === totalT && doneP === totalP && doneF === totalF;
53
- // Current focus = first in-progress task (and its phase/feature).
54
- const focusTask = allTasks.find(({ task }) => task.status === "in-progress");
55
+ // Current focus is authoritative only when exactly one task is in progress.
56
+ // Multiple active tasks are an explicit conflict, never a reason to pick the first silently.
57
+ const focusTask = activeTasks.length === 1 ? activeTasks[0] : undefined;
55
58
  const focusPhase = focusTask?.phase;
56
59
  const focusFeature = focusPhase ? feats.find((x) => x.id === focusPhase.featureId) : undefined;
57
60
  const italian = (plan.project.chatLanguage || "").toLowerCase().includes("ital");
@@ -67,10 +70,20 @@ export async function buildRecap(st, web = {}, opts = {}) {
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 : ""}`);
73
+ lines.push("", "## Project Guidelines", plan.project.projectGuidelines.content.trim() || "No project guidelines set.");
70
74
  lines.push(italian
71
75
  ? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${checkpointedT} con checkpoint)`
72
76
  : `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${checkpointedT} with checkpoints)`);
73
- if (focusTask && focusPhase) {
77
+ if (activeTasks.length > 1) {
78
+ const conflicts = activeTasks.map(({ phase, task }) => {
79
+ const feature = feats.find((entry) => entry.id === phase.featureId);
80
+ return `${formatPhaseRef(phase.number, feature?.number)}/${tref(task.number)} — ${task.title}`;
81
+ });
82
+ lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "CONFLITTO TASK ATTIVI" : "ACTIVE TASK CONFLICT"} — ${activeTasks.length} ${italian ? "task risultano in-progress" : "tasks are in progress"}: ${conflicts.join("; ")}`, "", italian
83
+ ? `Esegui ${cmd("task_recommend", "planner-task-recommend")} e riconcilia esplicitamente il conflitto prima di dichiarare un focus.`
84
+ : `Run ${cmd("task_recommend", "planner-task-recommend")} and explicitly reconcile the conflict before claiming a current focus.`);
85
+ }
86
+ else if (focusTask && focusPhase) {
74
87
  const fr = focusFeature ? fref(focusFeature.number) : "?";
75
88
  const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
76
89
  const tr = tref(focusTask.task.number);
@@ -89,7 +102,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
89
102
  }
90
103
  else if (handoffs.length > 0) {
91
104
  const top = handoffs[0];
92
- lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo" : "no active task"} · ${italian ? "handoff pendente più recente" : "most recent pending handoff"}: ${top.compositeRef} — "${top.firstLine}"`);
105
+ lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo (verificato dagli stati persistiti di tutti i task)" : "no active task (verified from all persisted task statuses)"} · ${italian ? "handoff pendente più recente" : "most recent pending handoff"}: ${top.compositeRef} — "${top.firstLine}" (${top.resumeReady ? "resume-ready" : italian ? "verifica read-back richiesta" : "read-back verification required"})`);
93
106
  }
94
107
  else if (planComplete) {
95
108
  lines.push(italian
@@ -97,7 +110,14 @@ export async function buildRecap(st, web = {}, opts = {}) {
97
110
  : "Current focus: plan complete — all features/phases/tasks are done.");
98
111
  }
99
112
  else {
100
- 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"}`);
113
+ lines.push(`${italian ? "Focus corrente" : "Current focus"}: ${italian ? "nessun task attivo (verificato dagli stati persistiti di tutti i task) — rivedi il piano e scegli il prossimo task concreto" : "no active task (verified from all persisted task statuses) — review the plan and pick the next concrete task"}`);
114
+ }
115
+ const orientation = focusTask ?? pendingResume ?? latestStandaloneCheckpoint;
116
+ if (orientation) {
117
+ const feature = feats.find((entry) => entry.id === orientation.phase.featureId);
118
+ lines.push("", buildPhaseWorkMap(orientation.phase, feature?.number, orientation.task.id, 4_000).content, italian
119
+ ? "Prima di proporre nuovo lavoro, rileggi la fase canonica e il task fratello pertinente: non duplicare capability già assegnate."
120
+ : "Before proposing new work, reread the canonical phase and the relevant sibling task: do not duplicate an already-owned capability.");
101
121
  }
102
122
  // Next step: the phase handoff (phase.handoff) is the authoritative,
103
123
  // actively-managed resume context. When a handoff is pending, point to it
@@ -119,9 +139,14 @@ export async function buildRecap(st, web = {}, opts = {}) {
119
139
  : `Resume advisory: evaluate the newest checkpoint, ${ref}, with ${taskStartCmd} before deciding whether to return to it or start other work.`);
120
140
  }
121
141
  else if (handoffs.length > 0) {
122
- lines.push(italian
123
- ? `Prossimo step: leggi l'handoff di fase pendente sotto (fonte autorevole, gestita attivamente). I nextSteps legacy di resume.json sono soppressi perché possono essere stale.`
124
- : `Next step: read the pending phase handoff below (authoritative, actively maintained). Legacy resume.json nextSteps are suppressed because they may be stale.`);
142
+ const unverified = handoffs.filter((handoff) => !handoff.resumeReady);
143
+ lines.push(unverified.length > 0
144
+ ? (italian
145
+ ? `Prossimo step: ${unverified.length} handoff candidato richiede ancora verifica read-back; non trattarlo come completo o autorevole finché handoff_verify non restituisce resumeReady=true.`
146
+ : `Next step: ${unverified.length} handoff candidate still requires read-back verification; do not treat it as complete or authoritative until ${cmd("handoff_verify", "planner-handoff-verify")} returns resumeReady=true.`)
147
+ : (italian
148
+ ? `Prossimo step: leggi l'handoff di fase pendente sotto (fonte autorevole, gestita attivamente). I nextSteps legacy di resume.json sono soppressi perché possono essere stale.`
149
+ : `Next step: read the pending phase handoff below (authoritative, actively maintained). Legacy resume.json nextSteps are suppressed because they may be stale.`));
125
150
  }
126
151
  else if (!planComplete && resume?.nextSteps?.length) {
127
152
  lines.push(`${italian ? "Prossimo step" : "Next step"}: ${resume.nextSteps[0]}`);
package/dist/refs.d.ts CHANGED
@@ -11,7 +11,12 @@
11
11
  * - Compos: "P002(F001)" -> phase.number with parent feature validation
12
12
  * - Title: exact match, then includes (backward-compat fallback)
13
13
  */
14
- import type { Phase, Feature } from "./schema.js";
14
+ import type { Phase, Feature, Idea } from "./schema.js";
15
+ /**
16
+ * Resolve an idea by UUID, I00x number, shortId, exact title, then title
17
+ * inclusion. Ideas are top-level and never require feature/phase context.
18
+ */
19
+ export declare function findIdeaByRef(ideas: Idea[], ref: string): Idea | undefined;
15
20
  /**
16
21
  * Resolve a phase reference to a Phase. Returns `undefined` when not found or
17
22
  * when a composite (F00x) parent does not match the phase's featureId.
@@ -1 +1 @@
1
- {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../src/refs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAKlD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,KAAK,EAAE,EACf,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,MAAM,GACV,KAAK,GAAG,SAAS,CAgCnB;AACD;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAiBxC,wBAAgB,aAAa,CAC3B,MAAM,EAAE,KAAK,EAAE,EACf,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,SAAS,CA2D1C"}
1
+ {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../src/refs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAMxD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAmB1E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,KAAK,EAAE,EACf,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,MAAM,GACV,KAAK,GAAG,SAAS,CAgCnB;AACD;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAiBxC,wBAAgB,aAAa,CAC3B,MAAM,EAAE,KAAK,EAAE,EACf,QAAQ,EAAE,OAAO,EAAE,EACnB,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GAAG,SAAS,CA2D1C"}
package/dist/refs.js CHANGED
@@ -1,5 +1,30 @@
1
1
  // P00x or P00x(F00x) — accept 1+ digits so "p1" == "p001".
2
2
  const PHASE_REF_RE = /^p(\d+)(?:\(f(\d+)\))?$/;
3
+ const IDEA_REF_RE = /^i(\d+)$/;
4
+ /**
5
+ * Resolve an idea by UUID, I00x number, shortId, exact title, then title
6
+ * inclusion. Ideas are top-level and never require feature/phase context.
7
+ */
8
+ export function findIdeaByRef(ideas, ref) {
9
+ const normalized = ref.trim().toLowerCase();
10
+ if (!normalized)
11
+ return undefined;
12
+ const byId = ideas.find((idea) => idea.id.toLowerCase() === normalized);
13
+ if (byId)
14
+ return byId;
15
+ const match = normalized.match(IDEA_REF_RE);
16
+ if (match) {
17
+ const number = parseInt(match[1], 10);
18
+ const byNumber = ideas.find((idea) => idea.number === number);
19
+ if (byNumber)
20
+ return byNumber;
21
+ }
22
+ const byShortId = ideas.find((idea) => idea.shortId.toLowerCase() === normalized);
23
+ if (byShortId)
24
+ return byShortId;
25
+ return ideas.find((idea) => idea.title.toLowerCase() === normalized)
26
+ ?? ideas.find((idea) => idea.title.toLowerCase().includes(normalized));
27
+ }
3
28
  /**
4
29
  * Resolve a phase reference to a Phase. Returns `undefined` when not found or
5
30
  * when a composite (F00x) parent does not match the phase's featureId.
@@ -1 +1 @@
1
- {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,EAAE,KAAK,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAC;AAmBtG,qBAAa,YAAY;IACvB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM;IA2KvC,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM;IAIjC,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;IA6BxB,OAAO,CAAC,eAAe;IA2GvB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM;IAmDxD,mEAAmE;IACnE,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAejD"}
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,EAAQ,KAAK,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAC;AAuB5G,qBAAa,YAAY;IACvB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM;IA+LvC,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM;IAIjC,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,gBAAgB;IA6BxB,OAAO,CAAC,eAAe;IA2GvB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM;IAmDxD,mEAAmE;IACnE,MAAM,CAAC,IAAI,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAejD"}
package/dist/renderer.js CHANGED
@@ -11,9 +11,12 @@ function phaseLabel(phase) {
11
11
  function taskLabel(task) {
12
12
  return `T${seq(task.number)} — ${task.title}`;
13
13
  }
14
+ function ideaLabel(idea) {
15
+ return `I${seq(idea.number)} — ${idea.title}`;
16
+ }
14
17
  export class PlanRenderer {
15
18
  renderPlan(plan) {
16
- const { project, manifest, features, requirements, phases } = plan;
19
+ const { project, manifest, features, requirements, ideas, phases } = plan;
17
20
  const lines = [];
18
21
  lines.push(`# ${project.name} — Project Plan`);
19
22
  lines.push("");
@@ -72,6 +75,13 @@ export class PlanRenderer {
72
75
  lines.push(...renderAcceptedDecisions(project.acceptedDecisions));
73
76
  lines.push("");
74
77
  }
78
+ // ── Project Guidelines ──────────────────────────────────────────
79
+ if (project.projectGuidelines.content.trim()) {
80
+ lines.push("## Project Guidelines");
81
+ lines.push("");
82
+ lines.push(project.projectGuidelines.content.trim());
83
+ lines.push("");
84
+ }
75
85
  // ── Global Rules ─────────────────────────────────────────────────
76
86
  if (project.globalRules.length > 0) {
77
87
  lines.push("## Global Rules");
@@ -98,6 +108,19 @@ export class PlanRenderer {
98
108
  lines.push(bullet(wr.afterPhaseComplete));
99
109
  lines.push("");
100
110
  }
111
+ // ── Ideas Inbox ─────────────────────────────────────────────────
112
+ if (ideas.ideas.length > 0) {
113
+ lines.push("---");
114
+ lines.push("## Ideas Inbox");
115
+ lines.push("");
116
+ for (const idea of ideas.ideas) {
117
+ const promotion = idea.promotion ? ` → ${idea.promotion.targetRef}` : "";
118
+ lines.push(`- **${ideaLabel(idea)}**${promotion}`);
119
+ if (idea.description)
120
+ lines.push(` - ${idea.description}`);
121
+ }
122
+ lines.push("");
123
+ }
101
124
  // ── Features ────────────────────────────────────────────────────
102
125
  if (features.features.length > 0) {
103
126
  lines.push("---");
@@ -146,7 +169,6 @@ export class PlanRenderer {
146
169
  lines.push(req.description);
147
170
  }
148
171
  lines.push("");
149
- lines.push(`Status: ${statusBadge(req.status)}`);
150
172
  if (req.linkedPhaseIds.length > 0) {
151
173
  lines.push(`Phases: ${req.linkedPhaseIds.map((p) => `\`${p}\``).join(", ")}`);
152
174
  }
@@ -0,0 +1,18 @@
1
+ import type { MacroTask, MacroTaskStatus } from "./schema.js";
2
+ export interface MacroTaskMutationInput {
3
+ /** Existing persisted ID only. Omit it for a new macro task. */
4
+ id?: string;
5
+ title: string;
6
+ description?: string;
7
+ status: MacroTaskStatus;
8
+ }
9
+ export declare class RequirementMacroTaskError extends Error {
10
+ constructor(message: string);
11
+ }
12
+ /**
13
+ * Reconciles user-authored macro-task fields while retaining all system-owned
14
+ * identity and timestamp values. The input array order is canonical, so a
15
+ * successful mutation also performs an explicit reorder atomically.
16
+ */
17
+ export declare function reconcileRequirementMacroTasks(existing: readonly MacroTask[], inputs: readonly MacroTaskMutationInput[], timestamp: string): MacroTask[];
18
+ //# sourceMappingURL=requirement-macro-tasks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requirement-macro-tasks.d.ts","sourceRoot":"","sources":["../src/requirement-macro-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9D,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,eAAe,CAAC;CACzB;AAED,qBAAa,yBAA0B,SAAQ,KAAK;gBACtC,OAAO,EAAE,MAAM;CAI5B;AAUD;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,SAAS,SAAS,EAAE,EAC9B,MAAM,EAAE,SAAS,sBAAsB,EAAE,EACzC,SAAS,EAAE,MAAM,GAChB,SAAS,EAAE,CA+Bb"}
@@ -0,0 +1,55 @@
1
+ const MACRO_TASK_STATUSES = new Set([
2
+ "planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting",
3
+ ]);
4
+ export class RequirementMacroTaskError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = "RequirementMacroTaskError";
8
+ }
9
+ }
10
+ function nextMacroTaskId(used) {
11
+ for (let number = 1; number <= 999; number += 1) {
12
+ const candidate = `MT-${String(number).padStart(3, "0")}`;
13
+ if (!used.has(candidate))
14
+ return candidate;
15
+ }
16
+ throw new RequirementMacroTaskError("A requirement cannot contain more than 999 macro tasks.");
17
+ }
18
+ /**
19
+ * Reconciles user-authored macro-task fields while retaining all system-owned
20
+ * identity and timestamp values. The input array order is canonical, so a
21
+ * successful mutation also performs an explicit reorder atomically.
22
+ */
23
+ export function reconcileRequirementMacroTasks(existing, inputs, timestamp) {
24
+ const existingById = new Map(existing.map((task) => [task.id, task]));
25
+ const usedIds = new Set(existingById.keys());
26
+ const seenIds = new Set();
27
+ return inputs.map((input, index) => {
28
+ const title = input.title.trim();
29
+ if (!title)
30
+ throw new RequirementMacroTaskError(`Macro task ${index + 1} requires a title.`);
31
+ if (!MACRO_TASK_STATUSES.has(input.status)) {
32
+ throw new RequirementMacroTaskError(`Macro task ${index + 1} has an invalid status: ${input.status}.`);
33
+ }
34
+ const requestedId = input.id?.trim();
35
+ if (requestedId && seenIds.has(requestedId)) {
36
+ throw new RequirementMacroTaskError(`Macro task ID ${requestedId} appears more than once.`);
37
+ }
38
+ if (requestedId)
39
+ seenIds.add(requestedId);
40
+ const previous = requestedId ? existingById.get(requestedId) : undefined;
41
+ if (requestedId && !previous) {
42
+ throw new RequirementMacroTaskError(`Macro task ID ${requestedId} is not owned by this requirement.`);
43
+ }
44
+ const id = previous?.id ?? nextMacroTaskId(usedIds);
45
+ usedIds.add(id);
46
+ return {
47
+ id,
48
+ title,
49
+ description: input.description?.trim() ?? "",
50
+ status: input.status,
51
+ createdAt: previous?.createdAt ?? timestamp,
52
+ updatedAt: timestamp,
53
+ };
54
+ });
55
+ }
@@ -0,0 +1,34 @@
1
+ import type { RuntimePackageVersion } from "./package-version.js";
2
+ export declare const PLAN_SCHEMA_VERSION = 1;
3
+ export declare const ALLOCATION_REGISTRY_VERSION = 1;
4
+ export declare const SUPPORTED_ALLOCATION_KINDS: readonly ["feature", "phase", "task", "idea"];
5
+ export type AllocationKind = typeof SUPPORTED_ALLOCATION_KINDS[number];
6
+ export interface RuntimePackageDiagnostic {
7
+ name: string;
8
+ installedVersion: string;
9
+ loadedVersion: string;
10
+ runtimeState: "loaded";
11
+ versionSource: "loaded-package-manifest";
12
+ packageJsonPath?: string | undefined;
13
+ }
14
+ export interface RuntimeCapabilities {
15
+ planSchema: {
16
+ manifestSchemaVersion: typeof PLAN_SCHEMA_VERSION;
17
+ };
18
+ allocationRegistry: {
19
+ version: typeof ALLOCATION_REGISTRY_VERSION;
20
+ supportedKinds: readonly AllocationKind[];
21
+ };
22
+ }
23
+ export declare function runtimePackageDiagnostic(pkg: RuntimePackageVersion): RuntimePackageDiagnostic;
24
+ export declare function runtimePackagesDiagnostic(packages: RuntimePackageVersion[]): Record<string, RuntimePackageDiagnostic>;
25
+ export declare function runtimeCapabilities(): RuntimeCapabilities;
26
+ export declare function isSupportedAllocationKind(kind: string): kind is AllocationKind;
27
+ export declare function unsupportedAllocationKindDetails(kind: string): {
28
+ errorCode: "PLAN_UNSUPPORTED_ALLOCATION_KIND";
29
+ kind: string;
30
+ supportedKinds: readonly ["feature", "phase", "task", "idea"];
31
+ requiredCapability: string;
32
+ action: string;
33
+ };
34
+ //# sourceMappingURL=runtime-diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-diagnostics.d.ts","sourceRoot":"","sources":["../src/runtime-diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAC7C,eAAO,MAAM,0BAA0B,+CAAgD,CAAC;AAExF,MAAM,MAAM,cAAc,GAAG,OAAO,0BAA0B,CAAC,MAAM,CAAC,CAAC;AAEvE,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,QAAQ,CAAC;IACvB,aAAa,EAAE,yBAAyB,CAAC;IACzC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE;QACV,qBAAqB,EAAE,OAAO,mBAAmB,CAAC;KACnD,CAAC;IACF,kBAAkB,EAAE;QAClB,OAAO,EAAE,OAAO,2BAA2B,CAAC;QAC5C,cAAc,EAAE,SAAS,cAAc,EAAE,CAAC;KAC3C,CAAC;CACH;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,qBAAqB,GAAG,wBAAwB,CAS7F;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAErH;AAED,wBAAgB,mBAAmB,IAAI,mBAAmB,CAUzD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,IAAI,cAAc,CAE9E;AAED,wBAAgB,gCAAgC,CAAC,IAAI,EAAE,MAAM;;;;;;EAQ5D"}
@@ -0,0 +1,39 @@
1
+ export const PLAN_SCHEMA_VERSION = 1;
2
+ export const ALLOCATION_REGISTRY_VERSION = 1;
3
+ export const SUPPORTED_ALLOCATION_KINDS = ["feature", "phase", "task", "idea"];
4
+ export function runtimePackageDiagnostic(pkg) {
5
+ return {
6
+ name: pkg.name,
7
+ installedVersion: pkg.version,
8
+ loadedVersion: pkg.version,
9
+ runtimeState: "loaded",
10
+ versionSource: "loaded-package-manifest",
11
+ ...(pkg.packageJsonPath ? { packageJsonPath: pkg.packageJsonPath } : {}),
12
+ };
13
+ }
14
+ export function runtimePackagesDiagnostic(packages) {
15
+ return Object.fromEntries(packages.map((pkg) => [pkg.name, runtimePackageDiagnostic(pkg)]));
16
+ }
17
+ export function runtimeCapabilities() {
18
+ return {
19
+ planSchema: {
20
+ manifestSchemaVersion: PLAN_SCHEMA_VERSION,
21
+ },
22
+ allocationRegistry: {
23
+ version: ALLOCATION_REGISTRY_VERSION,
24
+ supportedKinds: SUPPORTED_ALLOCATION_KINDS,
25
+ },
26
+ };
27
+ }
28
+ export function isSupportedAllocationKind(kind) {
29
+ return SUPPORTED_ALLOCATION_KINDS.includes(kind);
30
+ }
31
+ export function unsupportedAllocationKindDetails(kind) {
32
+ return {
33
+ errorCode: "PLAN_UNSUPPORTED_ALLOCATION_KIND",
34
+ kind,
35
+ supportedKinds: SUPPORTED_ALLOCATION_KINDS,
36
+ requiredCapability: "allocationRegistry.supportedKinds",
37
+ action: "Upgrade all Agent Plan packages and reload the harness so the loaded runtime supports this allocation kind before retrying the mutation.",
38
+ };
39
+ }