@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.
- package/dist/display-status.d.ts +3 -3
- package/dist/display-status.d.ts.map +1 -1
- package/dist/display-status.js +5 -4
- package/dist/handoff-context.d.ts +91 -1
- package/dist/handoff-context.d.ts.map +1 -1
- package/dist/handoff-context.js +134 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/naming.d.ts +3 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +7 -0
- package/dist/payload-fallback.d.ts +38 -0
- package/dist/payload-fallback.d.ts.map +1 -0
- package/dist/payload-fallback.js +73 -0
- package/dist/plan-store.d.ts +65 -8
- package/dist/plan-store.d.ts.map +1 -1
- package/dist/plan-store.js +391 -39
- package/dist/planner-rules.d.ts +3 -11
- package/dist/planner-rules.d.ts.map +1 -1
- package/dist/planner-rules.js +23 -6
- package/dist/planner-skill.d.ts +24 -0
- package/dist/planner-skill.d.ts.map +1 -0
- package/dist/planner-skill.js +113 -0
- package/dist/project-context-migration.d.ts +47 -0
- package/dist/project-context-migration.d.ts.map +1 -0
- package/dist/project-context-migration.js +168 -0
- package/dist/read-tracking.d.ts +27 -8
- package/dist/read-tracking.d.ts.map +1 -1
- package/dist/read-tracking.js +79 -34
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +2 -6
- package/dist/refs.d.ts +6 -1
- package/dist/refs.d.ts.map +1 -1
- package/dist/refs.js +25 -0
- package/dist/renderer.d.ts.map +1 -1
- package/dist/renderer.js +24 -1
- package/dist/requirement-macro-tasks.d.ts +18 -0
- package/dist/requirement-macro-tasks.d.ts.map +1 -0
- package/dist/requirement-macro-tasks.js +55 -0
- package/dist/schema.d.ts +1088 -273
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +65 -0
- package/dist/task-selection.js +2 -2
- package/dist/task-start-outcome.d.ts +1 -1
- package/dist/task-start-outcome.d.ts.map +1 -1
- package/dist/task-start-outcome.js +1 -0
- package/package.json +3 -1
- package/planner-skill.md +210 -0
- package/skills/grill-me/SKILL.md +10 -0
package/dist/read-tracking.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session-scoped
|
|
2
|
+
* Session-scoped context-read enforcement for agent lifecycle operations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
109
|
-
const
|
|
110
|
-
return
|
|
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
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/recap.d.ts.map
CHANGED
|
@@ -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,
|
|
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("",
|
|
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.
|
package/dist/refs.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
package/dist/renderer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,OAAO,
|
|
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
|
+
}
|