@agent-plan/core 0.2.24 → 0.2.26

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 (50) hide show
  1. package/dist/display-status.d.ts +3 -3
  2. package/dist/display-status.d.ts.map +1 -1
  3. package/dist/display-status.js +5 -4
  4. package/dist/handoff-context.d.ts +91 -1
  5. package/dist/handoff-context.d.ts.map +1 -1
  6. package/dist/handoff-context.js +134 -2
  7. package/dist/index.d.ts +5 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -0
  10. package/dist/naming.d.ts +3 -0
  11. package/dist/naming.d.ts.map +1 -1
  12. package/dist/naming.js +7 -0
  13. package/dist/payload-fallback.d.ts +38 -0
  14. package/dist/payload-fallback.d.ts.map +1 -0
  15. package/dist/payload-fallback.js +73 -0
  16. package/dist/plan-store.d.ts +65 -8
  17. package/dist/plan-store.d.ts.map +1 -1
  18. package/dist/plan-store.js +391 -39
  19. package/dist/planner-rules.d.ts +3 -11
  20. package/dist/planner-rules.d.ts.map +1 -1
  21. package/dist/planner-rules.js +23 -6
  22. package/dist/planner-skill.d.ts +24 -0
  23. package/dist/planner-skill.d.ts.map +1 -0
  24. package/dist/planner-skill.js +113 -0
  25. package/dist/project-context-migration.d.ts +47 -0
  26. package/dist/project-context-migration.d.ts.map +1 -0
  27. package/dist/project-context-migration.js +168 -0
  28. package/dist/read-tracking.d.ts +27 -8
  29. package/dist/read-tracking.d.ts.map +1 -1
  30. package/dist/read-tracking.js +79 -34
  31. package/dist/recap.d.ts.map +1 -1
  32. package/dist/recap.js +2 -6
  33. package/dist/refs.d.ts +6 -1
  34. package/dist/refs.d.ts.map +1 -1
  35. package/dist/refs.js +25 -0
  36. package/dist/renderer.d.ts.map +1 -1
  37. package/dist/renderer.js +24 -1
  38. package/dist/requirement-macro-tasks.d.ts +18 -0
  39. package/dist/requirement-macro-tasks.d.ts.map +1 -0
  40. package/dist/requirement-macro-tasks.js +55 -0
  41. package/dist/schema.d.ts +1088 -273
  42. package/dist/schema.d.ts.map +1 -1
  43. package/dist/schema.js +65 -0
  44. package/dist/task-selection.js +2 -2
  45. package/dist/task-start-outcome.d.ts +1 -1
  46. package/dist/task-start-outcome.d.ts.map +1 -1
  47. package/dist/task-start-outcome.js +1 -0
  48. package/package.json +3 -1
  49. package/planner-skill.md +210 -0
  50. 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
- * every entity's updatedAt 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 = () => ({
@@ -77,53 +77,98 @@ export function markRequirementReadForSessionId(sessionId, requirementId) {
77
77
  }
78
78
  function orderedEligibility(sessionId, taskId, phaseId, featureId) {
79
79
  const state = stateFor(sessionId);
80
- const taskSequence = state.tasks.get(taskId);
81
- if (taskSequence === undefined) {
80
+ if (!state.tasks.has(taskId)) {
82
81
  return { eligible: false, reason: "Read this exact task with full=true first." };
83
82
  }
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." };
83
+ if (!state.phases.has(phaseId)) {
84
+ return { eligible: false, reason: "Read this task's parent phase with full=true." };
87
85
  }
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." };
86
+ if (featureId && !state.features.has(featureId)) {
87
+ return { eligible: false, reason: "Read this phase's parent feature with full=true." };
93
88
  }
94
89
  return { eligible: true, reason: "" };
95
90
  }
96
- function validSessionInfo(entity, sessionId) {
91
+ function entityRevision(entity, kind) {
92
+ if ((kind === "phase" || kind === "feature") && entity.descriptionUpdatedAt?.trim()) {
93
+ return entity.descriptionUpdatedAt;
94
+ }
95
+ return entity.updatedAt;
96
+ }
97
+ function storedReadState(entity, sessionId, kind) {
97
98
  const entry = entity?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
98
- return Boolean(entity && entry && entity.updatedAt <= entry.createdAt);
99
+ if (!entity || !entry)
100
+ return "missing";
101
+ return entityRevision(entity, kind) <= entry.createdAt ? "valid" : "stale";
102
+ }
103
+ function validSessionInfo(entity, sessionId, kind = "requirement") {
104
+ return storedReadState(entity, sessionId, kind) === "valid";
99
105
  }
100
106
  function persistedEligibility(input) {
101
- if (!validSessionInfo(input.task, input.sessionId) || !validSessionInfo(input.phase, input.sessionId))
107
+ if (!validSessionInfo(input.task, input.sessionId, "task") || !validSessionInfo(input.phase, input.sessionId, "phase"))
102
108
  return false;
103
- if (input.featureId && !validSessionInfo(input.feature, input.sessionId))
109
+ if (input.featureId && !validSessionInfo(input.feature, input.sessionId, "feature"))
104
110
  return false;
105
111
  const requirementIds = input.requirementIds ?? [];
106
112
  return requirementIds.every((id) => validSessionInfo(input.requirements?.find((requirement) => requirement.id === id), input.sessionId));
107
113
  }
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));
114
+ function requiredReadReason(requiredReads) {
115
+ const labels = requiredReads.map((read) => `${read.kind} ${read.id} (${read.state})`);
116
+ return `Read required context only for: ${labels.join(", ")}. Perform only these reads, in any order within the current session, then retry.`;
111
117
  }
112
118
  /**
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.
119
+ * Combine valid persisted attestations with fresh in-memory reads. An entity read
120
+ * in the current session satisfies eligibility regardless of sequence order,
121
+ * so agents can read task, phase, and feature in any order; only persisted
122
+ * attestations are checked for revision freshness (stale) and missing entities
123
+ * remain reported as missing.
116
124
  */
117
125
  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." };
126
+ const state = stateFor(input.sessionId);
127
+ const requiredReads = [];
128
+ const taskStored = storedReadState(input.task, input.sessionId, "task");
129
+ const taskSequence = state.tasks.get(input.taskId);
130
+ const taskReady = taskStored === "valid" || taskSequence !== undefined;
131
+ if (!taskReady) {
132
+ requiredReads.push({ kind: "task", id: input.taskId, state: taskStored === "stale" ? "stale" : "missing" });
133
+ }
134
+ const phaseStored = storedReadState(input.phase, input.sessionId, "phase");
135
+ const phaseSequence = state.phases.get(input.phaseId);
136
+ const phaseReady = phaseStored === "valid" || phaseSequence !== undefined;
137
+ if (!phaseReady) {
138
+ requiredReads.push({
139
+ kind: "phase",
140
+ id: input.phaseId,
141
+ state: phaseStored === "stale" ? "stale" : "missing",
142
+ });
125
143
  }
126
- return ordered;
144
+ if (input.featureId) {
145
+ const featureStored = storedReadState(input.feature, input.sessionId, "feature");
146
+ const featureSequence = state.features.get(input.featureId);
147
+ const featureReady = featureStored === "valid" || featureSequence !== undefined;
148
+ if (!featureReady) {
149
+ requiredReads.push({
150
+ kind: "feature",
151
+ id: input.featureId,
152
+ state: featureStored === "stale" ? "stale" : "missing",
153
+ });
154
+ }
155
+ }
156
+ if (requiredReads.length === 0)
157
+ return { eligible: true, reason: "" };
158
+ return { eligible: false, reason: requiredReadReason(requiredReads), requiredReads };
159
+ }
160
+ export function projectGuidelinesReadStateForSession(project, sessionId) {
161
+ const guidelines = project.projectGuidelines;
162
+ const content = guidelines?.content?.trim() ?? "";
163
+ if (!content)
164
+ return "not-required";
165
+ const entry = guidelines?.sessionInfo?.find((candidate) => candidate.sessionId === sessionId);
166
+ if (!entry)
167
+ return "missing";
168
+ const updatedAt = guidelines?.updatedAt?.trim() ?? "";
169
+ if (updatedAt && updatedAt > entry.createdAt)
170
+ return "stale";
171
+ return "valid";
127
172
  }
128
173
  /** Return true only when the persisted attestation covers the current revisions. */
129
174
  export function hasValidSessionAttestation(input) {
@@ -169,7 +214,7 @@ export function parentReadAdvisory(featureId, phaseId) {
169
214
  const state = stateFor(DEFAULT_SESSION_ID);
170
215
  if (state.phases.has(phaseId) && (!featureId || state.features.has(featureId)))
171
216
  return "";
172
- return "\n\n⚠️ READ REQUIRED before proceeding: read the parent phase and feature with full=true.";
217
+ return "\n\n⚠️ READ REQUIRED before proceeding: read only the missing parent phase and feature with full=true.";
173
218
  }
174
219
  /** Advisory text for the separate linked-requirements gate. */
175
220
  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,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,CA6OhH"}
package/dist/recap.js CHANGED
@@ -63,13 +63,11 @@ 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)";
72
69
  lines.push(`${italian ? "Progetto" : "Project"}: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
70
+ lines.push("", "## Project Guidelines", plan.project.projectGuidelines.content.trim() || "No project guidelines set.");
73
71
  lines.push(italian
74
72
  ? `Avanzamento: feature ${doneF}/${totalF} completate (${activeF} attive) · fasi ${doneP}/${totalP} completate (${activeP} attive) · task ${doneT}/${totalT} completati (${activeT} attivi, ${checkpointedT} con checkpoint)`
75
73
  : `Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active, ${checkpointedT} with checkpoints)`);
@@ -78,9 +76,7 @@ export async function buildRecap(st, web = {}, opts = {}) {
78
76
  const pr = formatPhaseRef(focusPhase.number, focusFeature?.number);
79
77
  const tr = tref(focusTask.task.number);
80
78
  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).`);
79
+ lines.push("", `Continue with ${taskStartCmd} ${tr}. If context reads are required, follow only the missing or stale reads in its nextActions, then retry.`);
84
80
  }
85
81
  else if (pendingResume) {
86
82
  const feature = feats.find((entry) => entry.id === pendingResume.phase.featureId);
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;IAgMvC,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("---");
@@ -0,0 +1,18 @@
1
+ import type { MacroTask, RequirementStatus } 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: RequirementStatus;
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,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMhE,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,iBAAiB,CAAC;CAC3B;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 REQUIREMENT_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 (!REQUIREMENT_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
+ }