@papi-ai/server 0.7.5 → 0.7.7
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/index.js +1541 -825
- package/dist/prompts.js +48 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215,6 +215,30 @@ ${after}`;
|
|
|
215
215
|
function parseNorthStar(content) {
|
|
216
216
|
return extractSection(content, "North Star").replace(/^## North Star\s*/m, "").trim();
|
|
217
217
|
}
|
|
218
|
+
function upsertNorthStarInContent(content, statement) {
|
|
219
|
+
const headingPattern = /^## North Star\s*$/m;
|
|
220
|
+
const start = content.search(headingPattern);
|
|
221
|
+
if (start === -1) {
|
|
222
|
+
const cycleLogIdx = content.search(/^## (?:Cycle Log|Sprint Log)/m);
|
|
223
|
+
const newSection = `## North Star
|
|
224
|
+
|
|
225
|
+
${statement}
|
|
226
|
+
|
|
227
|
+
`;
|
|
228
|
+
if (cycleLogIdx === -1) {
|
|
229
|
+
return content.trimEnd() + "\n\n" + newSection;
|
|
230
|
+
}
|
|
231
|
+
return content.slice(0, cycleLogIdx) + newSection + content.slice(cycleLogIdx);
|
|
232
|
+
}
|
|
233
|
+
const afterHeading = content.slice(start);
|
|
234
|
+
const nextSection = afterHeading.slice(1).search(/^## /m);
|
|
235
|
+
const sectionEnd = nextSection === -1 ? content.length : start + nextSection + 1;
|
|
236
|
+
return content.slice(0, start) + `## North Star
|
|
237
|
+
|
|
238
|
+
${statement}
|
|
239
|
+
|
|
240
|
+
` + content.slice(sectionEnd);
|
|
241
|
+
}
|
|
218
242
|
function parseDeferred(content) {
|
|
219
243
|
const section = extractSection(content, "Deferred / Parking Lot");
|
|
220
244
|
return section.split("\n").filter((line) => line.match(/^-\s+/)).map((line) => line.replace(/^-\s+/, "").trim());
|
|
@@ -1428,7 +1452,8 @@ var init_dist2 = __esm({
|
|
|
1428
1452
|
task: 1,
|
|
1429
1453
|
research: 2,
|
|
1430
1454
|
spike: 2,
|
|
1431
|
-
idea: 3
|
|
1455
|
+
idea: 3,
|
|
1456
|
+
discovery: 1
|
|
1432
1457
|
};
|
|
1433
1458
|
VALID_EFFORT_SIZES = /* @__PURE__ */ new Set(["XS", "S", "M", "L", "XL"]);
|
|
1434
1459
|
SECTION_HEADERS = [
|
|
@@ -2174,6 +2199,23 @@ ${footer}`);
|
|
|
2174
2199
|
async getDecisionUsage(_currentCycle) {
|
|
2175
2200
|
return [];
|
|
2176
2201
|
}
|
|
2202
|
+
// --- North Star ---
|
|
2203
|
+
async getCurrentNorthStar() {
|
|
2204
|
+
const content = await this.read("PLANNING_LOG.md");
|
|
2205
|
+
const ns = parseNorthStar(content);
|
|
2206
|
+
return ns || null;
|
|
2207
|
+
}
|
|
2208
|
+
async getNorthStarSetCycle() {
|
|
2209
|
+
return null;
|
|
2210
|
+
}
|
|
2211
|
+
async getNorthStarStaleness() {
|
|
2212
|
+
return null;
|
|
2213
|
+
}
|
|
2214
|
+
async upsertNorthStar(statement, _cycleNumber) {
|
|
2215
|
+
const content = await this.read("PLANNING_LOG.md");
|
|
2216
|
+
const updated = upsertNorthStarInContent(content, statement);
|
|
2217
|
+
await this.write("PLANNING_LOG.md", updated);
|
|
2218
|
+
}
|
|
2177
2219
|
};
|
|
2178
2220
|
NONE_PATTERN2 = /^none\b/i;
|
|
2179
2221
|
}
|
|
@@ -4468,6 +4510,7 @@ function rowToTask(row) {
|
|
|
4468
4510
|
if (row.stage_id != null) task.stageId = row.stage_id;
|
|
4469
4511
|
if (row.doc_ref != null) task.docRef = row.doc_ref;
|
|
4470
4512
|
if (row.source != null) task.source = row.source;
|
|
4513
|
+
if (row.opportunity != null) task.opportunity = row.opportunity;
|
|
4471
4514
|
return task;
|
|
4472
4515
|
}
|
|
4473
4516
|
function rowToBuildReport(row) {
|
|
@@ -4494,6 +4537,7 @@ function rowToBuildReport(row) {
|
|
|
4494
4537
|
if (row.handoff_accuracy != null) report.handoffAccuracy = row.handoff_accuracy;
|
|
4495
4538
|
if (row.brief_implications != null) report.briefImplications = row.brief_implications;
|
|
4496
4539
|
if (row.dead_ends != null) report.deadEnds = row.dead_ends;
|
|
4540
|
+
if (row.tool_call_count != null) report.toolCallCount = row.tool_call_count;
|
|
4497
4541
|
return report;
|
|
4498
4542
|
}
|
|
4499
4543
|
function rowToReview(row) {
|
|
@@ -4747,18 +4791,20 @@ var init_dist3 = __esm({
|
|
|
4747
4791
|
}
|
|
4748
4792
|
async getProject(id) {
|
|
4749
4793
|
const [row] = await this.sql`
|
|
4750
|
-
SELECT
|
|
4794
|
+
SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE id = ${id}
|
|
4751
4795
|
`;
|
|
4752
4796
|
return row ?? null;
|
|
4753
4797
|
}
|
|
4754
4798
|
async listProjects(filter) {
|
|
4755
4799
|
if (filter?.slug) {
|
|
4756
4800
|
return this.sql`
|
|
4757
|
-
SELECT
|
|
4801
|
+
SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects WHERE slug = ${filter.slug} ORDER BY created_at
|
|
4802
|
+
LIMIT 200 -- bounded: project list per user
|
|
4758
4803
|
`;
|
|
4759
4804
|
}
|
|
4760
4805
|
return this.sql`
|
|
4761
|
-
SELECT
|
|
4806
|
+
SELECT id, slug, name, repo_url, papi_dir, user_id, created_at, updated_at FROM projects ORDER BY created_at
|
|
4807
|
+
LIMIT 200 -- bounded: project list per user
|
|
4762
4808
|
`;
|
|
4763
4809
|
}
|
|
4764
4810
|
async updateProject(id, updates) {
|
|
@@ -4818,7 +4864,7 @@ var init_dist3 = __esm({
|
|
|
4818
4864
|
}
|
|
4819
4865
|
async getSharedDecision(id) {
|
|
4820
4866
|
const [row] = await this.sql`
|
|
4821
|
-
SELECT
|
|
4867
|
+
SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE id = ${id}
|
|
4822
4868
|
`;
|
|
4823
4869
|
return row ?? null;
|
|
4824
4870
|
}
|
|
@@ -4838,7 +4884,8 @@ var init_dist3 = __esm({
|
|
|
4838
4884
|
}
|
|
4839
4885
|
if (conditions.length === 0) {
|
|
4840
4886
|
return this.sql`
|
|
4841
|
-
SELECT
|
|
4887
|
+
SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE archived_at IS NULL ORDER BY created_at
|
|
4888
|
+
LIMIT 200 -- cross-project shared decisions; 200 is ample
|
|
4842
4889
|
`;
|
|
4843
4890
|
}
|
|
4844
4891
|
let where = conditions[0];
|
|
@@ -4846,7 +4893,8 @@ var init_dist3 = __esm({
|
|
|
4846
4893
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
4847
4894
|
}
|
|
4848
4895
|
return this.sql`
|
|
4849
|
-
SELECT
|
|
4896
|
+
SELECT id, display_id, title, decision, confidence, status, origin_project_id, evidence, superseded_by_id, created_at, updated_at, archived_at FROM shared_decisions WHERE ${where} ORDER BY created_at
|
|
4897
|
+
LIMIT 200
|
|
4850
4898
|
`;
|
|
4851
4899
|
}
|
|
4852
4900
|
async updateSharedDecision(id, updates) {
|
|
@@ -4915,7 +4963,7 @@ var init_dist3 = __esm({
|
|
|
4915
4963
|
}
|
|
4916
4964
|
async getAcknowledgement(id) {
|
|
4917
4965
|
const [row] = await this.sql`
|
|
4918
|
-
SELECT
|
|
4966
|
+
SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE id = ${id}
|
|
4919
4967
|
`;
|
|
4920
4968
|
return row ?? null;
|
|
4921
4969
|
}
|
|
@@ -4932,7 +4980,8 @@ var init_dist3 = __esm({
|
|
|
4932
4980
|
}
|
|
4933
4981
|
if (conditions.length === 0) {
|
|
4934
4982
|
return this.sql`
|
|
4935
|
-
SELECT
|
|
4983
|
+
SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements ORDER BY created_at
|
|
4984
|
+
LIMIT 500 -- acknowledgements grow with decisions × projects
|
|
4936
4985
|
`;
|
|
4937
4986
|
}
|
|
4938
4987
|
let where = conditions[0];
|
|
@@ -4940,7 +4989,8 @@ var init_dist3 = __esm({
|
|
|
4940
4989
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
4941
4990
|
}
|
|
4942
4991
|
return this.sql`
|
|
4943
|
-
SELECT
|
|
4992
|
+
SELECT id, decision_id, project_id, status, comments, responded_at, created_at, updated_at FROM acknowledgements WHERE ${where} ORDER BY created_at
|
|
4993
|
+
LIMIT 500
|
|
4944
4994
|
`;
|
|
4945
4995
|
}
|
|
4946
4996
|
// -------------------------------------------------------------------------
|
|
@@ -4956,7 +5006,7 @@ var init_dist3 = __esm({
|
|
|
4956
5006
|
}
|
|
4957
5007
|
async getSharedMilestone(id) {
|
|
4958
5008
|
const [row] = await this.sql`
|
|
4959
|
-
SELECT
|
|
5009
|
+
SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE id = ${id}
|
|
4960
5010
|
`;
|
|
4961
5011
|
return row ?? null;
|
|
4962
5012
|
}
|
|
@@ -4970,7 +5020,8 @@ var init_dist3 = __esm({
|
|
|
4970
5020
|
}
|
|
4971
5021
|
if (conditions.length === 0) {
|
|
4972
5022
|
return this.sql`
|
|
4973
|
-
SELECT
|
|
5023
|
+
SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE archived_at IS NULL ORDER BY created_at
|
|
5024
|
+
LIMIT 200
|
|
4974
5025
|
`;
|
|
4975
5026
|
}
|
|
4976
5027
|
let where = conditions[0];
|
|
@@ -4978,7 +5029,8 @@ var init_dist3 = __esm({
|
|
|
4978
5029
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
4979
5030
|
}
|
|
4980
5031
|
return this.sql`
|
|
4981
|
-
SELECT
|
|
5032
|
+
SELECT id, title, description, status, target_date, completed_at, created_at, updated_at, archived_at FROM shared_milestones WHERE ${where} ORDER BY created_at
|
|
5033
|
+
LIMIT 200
|
|
4982
5034
|
`;
|
|
4983
5035
|
}
|
|
4984
5036
|
async updateSharedMilestone(id, updates) {
|
|
@@ -5016,7 +5068,7 @@ var init_dist3 = __esm({
|
|
|
5016
5068
|
}
|
|
5017
5069
|
async listMilestoneDependencies(milestoneId) {
|
|
5018
5070
|
return this.sql`
|
|
5019
|
-
SELECT
|
|
5071
|
+
SELECT id, milestone_id, depends_on_id FROM milestone_dependencies WHERE milestone_id = ${milestoneId}
|
|
5020
5072
|
`;
|
|
5021
5073
|
}
|
|
5022
5074
|
async deleteMilestoneDependency(id) {
|
|
@@ -5045,7 +5097,7 @@ var init_dist3 = __esm({
|
|
|
5045
5097
|
}
|
|
5046
5098
|
async getProjectContribution(id) {
|
|
5047
5099
|
const [row] = await this.sql`
|
|
5048
|
-
SELECT
|
|
5100
|
+
SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions WHERE id = ${id}
|
|
5049
5101
|
`;
|
|
5050
5102
|
return row ?? null;
|
|
5051
5103
|
}
|
|
@@ -5062,7 +5114,8 @@ var init_dist3 = __esm({
|
|
|
5062
5114
|
}
|
|
5063
5115
|
if (conditions.length === 0) {
|
|
5064
5116
|
return this.sql`
|
|
5065
|
-
SELECT
|
|
5117
|
+
SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions ORDER BY created_at
|
|
5118
|
+
LIMIT 500
|
|
5066
5119
|
`;
|
|
5067
5120
|
}
|
|
5068
5121
|
let where = conditions[0];
|
|
@@ -5070,7 +5123,8 @@ var init_dist3 = __esm({
|
|
|
5070
5123
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
5071
5124
|
}
|
|
5072
5125
|
return this.sql`
|
|
5073
|
-
SELECT
|
|
5126
|
+
SELECT id, milestone_id, project_id, status, required_phases, notes, delivered_at, created_at, updated_at FROM project_contributions WHERE ${where} ORDER BY created_at
|
|
5127
|
+
LIMIT 500
|
|
5074
5128
|
`;
|
|
5075
5129
|
}
|
|
5076
5130
|
async updateProjectContribution(id, updates) {
|
|
@@ -5108,7 +5162,7 @@ var init_dist3 = __esm({
|
|
|
5108
5162
|
}
|
|
5109
5163
|
async getActiveNorthStar(projectId) {
|
|
5110
5164
|
const [row] = await this.sql`
|
|
5111
|
-
SELECT
|
|
5165
|
+
SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars
|
|
5112
5166
|
WHERE project_id = ${projectId}
|
|
5113
5167
|
AND superseded_by_id IS NULL
|
|
5114
5168
|
ORDER BY created_at DESC
|
|
@@ -5126,7 +5180,8 @@ var init_dist3 = __esm({
|
|
|
5126
5180
|
}
|
|
5127
5181
|
if (conditions.length === 0) {
|
|
5128
5182
|
return this.sql`
|
|
5129
|
-
SELECT
|
|
5183
|
+
SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars ORDER BY created_at
|
|
5184
|
+
LIMIT 200
|
|
5130
5185
|
`;
|
|
5131
5186
|
}
|
|
5132
5187
|
let where = conditions[0];
|
|
@@ -5134,7 +5189,8 @@ var init_dist3 = __esm({
|
|
|
5134
5189
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
5135
5190
|
}
|
|
5136
5191
|
return this.sql`
|
|
5137
|
-
SELECT
|
|
5192
|
+
SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE ${where} ORDER BY created_at
|
|
5193
|
+
LIMIT 200
|
|
5138
5194
|
`;
|
|
5139
5195
|
}
|
|
5140
5196
|
/**
|
|
@@ -5148,7 +5204,7 @@ var init_dist3 = __esm({
|
|
|
5148
5204
|
return this.sql.begin(async (_tx) => {
|
|
5149
5205
|
const tx = _tx;
|
|
5150
5206
|
const [old] = await tx`
|
|
5151
|
-
SELECT
|
|
5207
|
+
SELECT id, project_id, statement, set_at, superseded_by_id, superseded_at, created_at FROM north_stars WHERE id = ${id}
|
|
5152
5208
|
`;
|
|
5153
5209
|
if (!old) {
|
|
5154
5210
|
throw new Error(`NorthStar not found: ${id}`);
|
|
@@ -5191,7 +5247,7 @@ var init_dist3 = __esm({
|
|
|
5191
5247
|
}
|
|
5192
5248
|
async getConflictAlert(id) {
|
|
5193
5249
|
const [row] = await this.sql`
|
|
5194
|
-
SELECT
|
|
5250
|
+
SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts WHERE id = ${id}
|
|
5195
5251
|
`;
|
|
5196
5252
|
return row ?? null;
|
|
5197
5253
|
}
|
|
@@ -5205,7 +5261,8 @@ var init_dist3 = __esm({
|
|
|
5205
5261
|
}
|
|
5206
5262
|
if (conditions.length === 0) {
|
|
5207
5263
|
return this.sql`
|
|
5208
|
-
SELECT
|
|
5264
|
+
SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts ORDER BY created_at
|
|
5265
|
+
LIMIT 200
|
|
5209
5266
|
`;
|
|
5210
5267
|
}
|
|
5211
5268
|
let where = conditions[0];
|
|
@@ -5213,7 +5270,8 @@ var init_dist3 = __esm({
|
|
|
5213
5270
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
5214
5271
|
}
|
|
5215
5272
|
return this.sql`
|
|
5216
|
-
SELECT
|
|
5273
|
+
SELECT id, conflict_type, title, description, status, decision_a_id, decision_b_id, north_star_id, resolution_decision_id, raised_by, created_at, updated_at, resolved_at FROM conflict_alerts WHERE ${where} ORDER BY created_at
|
|
5274
|
+
LIMIT 200
|
|
5217
5275
|
`;
|
|
5218
5276
|
}
|
|
5219
5277
|
async updateConflictAlert(id, updates) {
|
|
@@ -6080,6 +6138,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6080
6138
|
`;
|
|
6081
6139
|
return rows.length > 0 ? { setCycle: rows[0].set_cycle, setAt: rows[0].set_at } : null;
|
|
6082
6140
|
}
|
|
6141
|
+
async upsertNorthStar(statement, _cycleNumber) {
|
|
6142
|
+
const [newRow] = await this.sql`
|
|
6143
|
+
INSERT INTO north_stars (project_id, statement, set_at)
|
|
6144
|
+
VALUES (${this.projectId}, ${statement}, now())
|
|
6145
|
+
RETURNING id
|
|
6146
|
+
`;
|
|
6147
|
+
if (newRow) {
|
|
6148
|
+
await this.sql`
|
|
6149
|
+
UPDATE north_stars
|
|
6150
|
+
SET superseded_by_id = ${newRow.id}, superseded_at = now()
|
|
6151
|
+
WHERE project_id = ${this.projectId}
|
|
6152
|
+
AND superseded_by_id IS NULL
|
|
6153
|
+
AND id != ${newRow.id}
|
|
6154
|
+
`;
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6083
6157
|
async getEstimationCalibration() {
|
|
6084
6158
|
const rows = await this.sql`
|
|
6085
6159
|
SELECT estimated_effort, actual_effort, accuracy_label, COUNT(*)::text AS count
|
|
@@ -6176,19 +6250,23 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6176
6250
|
}
|
|
6177
6251
|
async getActiveDecisions() {
|
|
6178
6252
|
const rows = await this.sql`
|
|
6179
|
-
SELECT
|
|
6253
|
+
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count
|
|
6254
|
+
FROM active_decisions
|
|
6180
6255
|
WHERE project_id = ${this.projectId}
|
|
6181
6256
|
ORDER BY display_id
|
|
6257
|
+
LIMIT 200 -- bounded: ADs are bounded by project lifecycle, 200 is a safe ceiling
|
|
6182
6258
|
`;
|
|
6183
6259
|
return rows.map(rowToActiveDecision);
|
|
6184
6260
|
}
|
|
6185
6261
|
async getSiblingAds(projectIds) {
|
|
6186
6262
|
if (projectIds.length === 0) return [];
|
|
6187
6263
|
const rows = await this.sql`
|
|
6188
|
-
SELECT
|
|
6264
|
+
SELECT id, display_id, title, confidence, superseded, superseded_by, created_cycle, modified_cycle, body, outcome, revision_count, project_id
|
|
6265
|
+
FROM active_decisions
|
|
6189
6266
|
WHERE project_id = ANY(${projectIds}::uuid[])
|
|
6190
6267
|
AND superseded = false
|
|
6191
6268
|
ORDER BY project_id, display_id
|
|
6269
|
+
LIMIT 500 -- cross-project query: 500 covers up to 10 projects × 50 ADs each
|
|
6192
6270
|
`;
|
|
6193
6271
|
return rows.map((row) => ({
|
|
6194
6272
|
...rowToActiveDecision(row),
|
|
@@ -6198,7 +6276,8 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6198
6276
|
async getCycleLog(limit) {
|
|
6199
6277
|
if (limit != null) {
|
|
6200
6278
|
const rows2 = await this.sql`
|
|
6201
|
-
SELECT
|
|
6279
|
+
SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
|
|
6280
|
+
FROM planning_log_entries
|
|
6202
6281
|
WHERE project_id = ${this.projectId}
|
|
6203
6282
|
ORDER BY cycle_number DESC
|
|
6204
6283
|
LIMIT ${limit}
|
|
@@ -6206,18 +6285,22 @@ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
|
|
6206
6285
|
return rows2.map(rowToCycleLogEntry);
|
|
6207
6286
|
}
|
|
6208
6287
|
const rows = await this.sql`
|
|
6209
|
-
SELECT
|
|
6288
|
+
SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
|
|
6289
|
+
FROM planning_log_entries
|
|
6210
6290
|
WHERE project_id = ${this.projectId}
|
|
6211
6291
|
ORDER BY cycle_number DESC
|
|
6292
|
+
LIMIT 500 -- 500 cycles is ~years of history, sufficient ceiling
|
|
6212
6293
|
`;
|
|
6213
6294
|
return rows.map(rowToCycleLogEntry);
|
|
6214
6295
|
}
|
|
6215
6296
|
async getCycleLogSince(cycleNumber) {
|
|
6216
6297
|
const rows = await this.sql`
|
|
6217
|
-
SELECT
|
|
6298
|
+
SELECT id, cycle_number, title, content, carry_forward, notes, task_count, effort_points
|
|
6299
|
+
FROM planning_log_entries
|
|
6218
6300
|
WHERE project_id = ${this.projectId}
|
|
6219
6301
|
AND cycle_number >= ${cycleNumber}
|
|
6220
6302
|
ORDER BY cycle_number DESC
|
|
6303
|
+
LIMIT 500 -- bounded by cycle range, 500 is a safe ceiling
|
|
6221
6304
|
`;
|
|
6222
6305
|
return rows.map(rowToCycleLogEntry);
|
|
6223
6306
|
}
|
|
@@ -6449,7 +6532,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6449
6532
|
const sinceCycle = input.sinceCycle ?? 0;
|
|
6450
6533
|
const hasPending = input.hasPendingActions ?? false;
|
|
6451
6534
|
const rows = await this.sql`
|
|
6452
|
-
SELECT
|
|
6535
|
+
SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
|
|
6536
|
+
FROM doc_registry
|
|
6453
6537
|
WHERE project_id = ${this.projectId}
|
|
6454
6538
|
AND (${matchAllStatuses} OR status = ${status})
|
|
6455
6539
|
AND (${input.type ?? null}::text IS NULL OR type = ${input.type ?? null})
|
|
@@ -6479,9 +6563,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6479
6563
|
async getDoc(idOrPath) {
|
|
6480
6564
|
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrPath);
|
|
6481
6565
|
const rows = isUuid ? await this.sql`
|
|
6482
|
-
SELECT
|
|
6566
|
+
SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
|
|
6567
|
+
FROM doc_registry WHERE id = ${idOrPath} AND project_id = ${this.projectId}
|
|
6483
6568
|
` : await this.sql`
|
|
6484
|
-
SELECT
|
|
6569
|
+
SELECT id, title, type, path, status, summary, tags, cycle_created, cycle_updated, superseded_by, actions, created_at, updated_at
|
|
6570
|
+
FROM doc_registry WHERE path = ${idOrPath} AND project_id = ${this.projectId}
|
|
6485
6571
|
`;
|
|
6486
6572
|
if (rows.length === 0) return null;
|
|
6487
6573
|
const r = rows[0];
|
|
@@ -6640,9 +6726,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6640
6726
|
async queryBoard(options) {
|
|
6641
6727
|
if (!options) {
|
|
6642
6728
|
const rows2 = await this.sql`
|
|
6643
|
-
SELECT
|
|
6729
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6730
|
+
FROM cycle_tasks
|
|
6644
6731
|
WHERE project_id = ${this.projectId}
|
|
6645
6732
|
ORDER BY display_id
|
|
6733
|
+
LIMIT 2000 -- hard ceiling; single project task count won't approach this
|
|
6646
6734
|
`;
|
|
6647
6735
|
return rows2.map(rowToTask);
|
|
6648
6736
|
}
|
|
@@ -6679,22 +6767,28 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6679
6767
|
where = this.sql`${where} AND ${conditions[i]}`;
|
|
6680
6768
|
}
|
|
6681
6769
|
const rows = await this.sql`
|
|
6682
|
-
SELECT
|
|
6770
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6771
|
+
FROM cycle_tasks WHERE ${where} ORDER BY display_id
|
|
6772
|
+
LIMIT 2000 -- matches no-options path ceiling
|
|
6683
6773
|
`;
|
|
6684
6774
|
return rows.map(rowToTask);
|
|
6685
6775
|
}
|
|
6686
6776
|
async getTask(id) {
|
|
6687
6777
|
const [row] = await this.sql`
|
|
6688
|
-
SELECT
|
|
6778
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6779
|
+
FROM cycle_tasks
|
|
6689
6780
|
WHERE project_id = ${this.projectId} AND display_id = ${id}
|
|
6781
|
+
LIMIT 1
|
|
6690
6782
|
`;
|
|
6691
6783
|
return row ? rowToTask(row) : null;
|
|
6692
6784
|
}
|
|
6693
6785
|
async getTasks(ids) {
|
|
6694
6786
|
if (ids.length === 0) return [];
|
|
6695
6787
|
const rows = await this.sql`
|
|
6696
|
-
SELECT
|
|
6788
|
+
SELECT id, project_id, display_id, title, status, priority, complexity, module, epic, phase, owner, reviewed, cycle, created_cycle, created_at, why, depends_on, notes, closure_reason, state_history, build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity, updated_at
|
|
6789
|
+
FROM cycle_tasks
|
|
6697
6790
|
WHERE project_id = ${this.projectId} AND display_id = ANY(${ids})
|
|
6791
|
+
LIMIT 2000 -- matches board ceiling; ids[] won't exceed this in practice
|
|
6698
6792
|
`;
|
|
6699
6793
|
return rows.map(rowToTask);
|
|
6700
6794
|
}
|
|
@@ -6711,7 +6805,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6711
6805
|
project_id, display_id, title, status, priority, complexity,
|
|
6712
6806
|
module, epic, phase, owner, reviewed, cycle, created_cycle,
|
|
6713
6807
|
why, depends_on, notes, closure_reason, state_history,
|
|
6714
|
-
build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source
|
|
6808
|
+
build_handoff, build_report, task_type, maturity, stage_id, doc_ref, source, opportunity
|
|
6715
6809
|
) VALUES (
|
|
6716
6810
|
${this.projectId}, ${displayId}, ${task.title}, ${task.status}, ${task.priority},
|
|
6717
6811
|
${normaliseComplexity(task.complexity)}, ${task.module}, ${task.epic ?? null}, ${task.phase}, ${task.owner},
|
|
@@ -6725,7 +6819,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6725
6819
|
${task.maturity ?? null},
|
|
6726
6820
|
${task.stageId ?? null},
|
|
6727
6821
|
${task.docRef ?? null},
|
|
6728
|
-
${task.source ?? null}
|
|
6822
|
+
${task.source ?? null},
|
|
6823
|
+
${task.opportunity ?? null}
|
|
6729
6824
|
)
|
|
6730
6825
|
RETURNING *
|
|
6731
6826
|
`;
|
|
@@ -6757,6 +6852,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6757
6852
|
if (updates.stageId !== void 0) columnMap["stage_id"] = updates.stageId;
|
|
6758
6853
|
if (updates.docRef !== void 0) columnMap["doc_ref"] = updates.docRef;
|
|
6759
6854
|
if (updates.source !== void 0) columnMap["source"] = updates.source;
|
|
6855
|
+
if (updates.opportunity !== void 0) columnMap["opportunity"] = updates.opportunity;
|
|
6760
6856
|
const keys = Object.keys(columnMap);
|
|
6761
6857
|
if (keys.length === 0) return;
|
|
6762
6858
|
await this.sql`
|
|
@@ -6804,7 +6900,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6804
6900
|
completed, actual_effort, estimated_effort, scope_accuracy,
|
|
6805
6901
|
surprises, discovered_issues, architecture_notes,
|
|
6806
6902
|
commit_sha, files_changed, related_decisions, handoff_accuracy,
|
|
6807
|
-
corrections_count, brief_implications, dead_ends
|
|
6903
|
+
corrections_count, brief_implications, dead_ends,
|
|
6904
|
+
started_at, completed_at, tool_call_count
|
|
6808
6905
|
) VALUES (
|
|
6809
6906
|
${this.projectId}, ${displayId}, ${report.taskId}, ${report.taskName},
|
|
6810
6907
|
${report.date}, ${report.cycle}, ${report.completed},
|
|
@@ -6814,13 +6911,15 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6814
6911
|
${report.handoffAccuracy ? this.sql.json(report.handoffAccuracy) : null},
|
|
6815
6912
|
${report.correctionsCount ?? 0},
|
|
6816
6913
|
${report.briefImplications ? this.sql.json(report.briefImplications) : null},
|
|
6817
|
-
${report.deadEnds ?? null}
|
|
6914
|
+
${report.deadEnds ?? null},
|
|
6915
|
+
${report.startedAt ?? null}, ${report.completedAt ?? null}, ${report.toolCallCount ?? 0}
|
|
6818
6916
|
)
|
|
6819
6917
|
`;
|
|
6820
6918
|
}
|
|
6821
6919
|
async getRecentBuildReports(count) {
|
|
6822
6920
|
const rows = await this.sql`
|
|
6823
|
-
SELECT
|
|
6921
|
+
SELECT id, display_id, task_id, task_name, date, cycle, completed, actual_effort, estimated_effort, scope_accuracy, surprises, discovered_issues, architecture_notes, commit_sha, files_changed, related_decisions, corrections_count, handoff_accuracy, brief_implications, dead_ends, created_at
|
|
6922
|
+
FROM build_reports
|
|
6824
6923
|
WHERE project_id = ${this.projectId}
|
|
6825
6924
|
ORDER BY created_at DESC
|
|
6826
6925
|
LIMIT ${count}
|
|
@@ -6836,9 +6935,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6836
6935
|
}
|
|
6837
6936
|
async getBuildReportsSince(cycleNumber) {
|
|
6838
6937
|
const rows = await this.sql`
|
|
6839
|
-
SELECT
|
|
6938
|
+
SELECT id, display_id, task_id, task_name, date, cycle, completed, actual_effort, estimated_effort, scope_accuracy, surprises, discovered_issues, architecture_notes, commit_sha, files_changed, related_decisions, corrections_count, handoff_accuracy, brief_implications, dead_ends, created_at
|
|
6939
|
+
FROM build_reports
|
|
6840
6940
|
WHERE project_id = ${this.projectId} AND cycle >= ${cycleNumber}
|
|
6841
6941
|
ORDER BY created_at
|
|
6942
|
+
LIMIT 1000 -- bounded by cycle range; 1000 covers ~200 cycles × 5 tasks
|
|
6842
6943
|
`;
|
|
6843
6944
|
return rows.map(rowToBuildReport);
|
|
6844
6945
|
}
|
|
@@ -6865,25 +6966,29 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6865
6966
|
let rows;
|
|
6866
6967
|
if (opts?.cycleNumber && opts?.category) {
|
|
6867
6968
|
rows = await this.sql`
|
|
6868
|
-
SELECT
|
|
6969
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
6970
|
+
FROM cycle_learnings
|
|
6869
6971
|
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber} AND category = ${opts.category}
|
|
6870
6972
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
6871
6973
|
`;
|
|
6872
6974
|
} else if (opts?.cycleNumber) {
|
|
6873
6975
|
rows = await this.sql`
|
|
6874
|
-
SELECT
|
|
6976
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
6977
|
+
FROM cycle_learnings
|
|
6875
6978
|
WHERE project_id = ${this.projectId} AND cycle_number = ${opts.cycleNumber}
|
|
6876
6979
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
6877
6980
|
`;
|
|
6878
6981
|
} else if (opts?.category) {
|
|
6879
6982
|
rows = await this.sql`
|
|
6880
|
-
SELECT
|
|
6983
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
6984
|
+
FROM cycle_learnings
|
|
6881
6985
|
WHERE project_id = ${this.projectId} AND category = ${opts.category}
|
|
6882
6986
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
6883
6987
|
`;
|
|
6884
6988
|
} else {
|
|
6885
6989
|
rows = await this.sql`
|
|
6886
|
-
SELECT
|
|
6990
|
+
SELECT id, project_id, task_id, cycle_number, category, severity, summary, detail, tags, related_decision, action_taken, action_ref, created_at
|
|
6991
|
+
FROM cycle_learnings
|
|
6887
6992
|
WHERE project_id = ${this.projectId}
|
|
6888
6993
|
ORDER BY created_at DESC LIMIT ${limit}
|
|
6889
6994
|
`;
|
|
@@ -6937,7 +7042,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
6937
7042
|
async getRecentReviews(count) {
|
|
6938
7043
|
const limit = count ?? 20;
|
|
6939
7044
|
const rows = await this.sql`
|
|
6940
|
-
SELECT
|
|
7045
|
+
SELECT id, display_id, task_id, stage, reviewer, verdict, cycle, date, comments, handoff_revision, build_commit_sha, auto_review
|
|
7046
|
+
FROM reviews
|
|
6941
7047
|
WHERE project_id = ${this.projectId}
|
|
6942
7048
|
ORDER BY created_at DESC
|
|
6943
7049
|
LIMIT ${limit}
|
|
@@ -7071,9 +7177,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7071
7177
|
// -------------------------------------------------------------------------
|
|
7072
7178
|
async readPhases() {
|
|
7073
7179
|
const rows = await this.sql`
|
|
7074
|
-
SELECT
|
|
7180
|
+
SELECT id, slug, label, description, status, sort_order, stage_id
|
|
7181
|
+
FROM phases
|
|
7075
7182
|
WHERE project_id = ${this.projectId}
|
|
7076
7183
|
ORDER BY sort_order
|
|
7184
|
+
LIMIT 200 -- phases are bounded by project structure
|
|
7077
7185
|
`;
|
|
7078
7186
|
return rows.map(rowToPhase);
|
|
7079
7187
|
}
|
|
@@ -7123,33 +7231,48 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7123
7231
|
INSERT INTO tool_call_metrics (
|
|
7124
7232
|
project_id, timestamp, tool, duration_ms,
|
|
7125
7233
|
input_tokens, output_tokens, estimated_cost_usd, model, cycle_number,
|
|
7126
|
-
context_bytes, context_utilisation
|
|
7234
|
+
context_bytes, context_utilisation, success
|
|
7127
7235
|
) VALUES (
|
|
7128
7236
|
${this.projectId}, ${metric.timestamp}, ${metric.tool}, ${metric.durationMs},
|
|
7129
7237
|
${metric.inputTokens ?? null}, ${metric.outputTokens ?? null},
|
|
7130
7238
|
${metric.estimatedCostUsd ?? null}, ${metric.model ?? null},
|
|
7131
7239
|
${metric.cycleNumber ?? null},
|
|
7132
|
-
${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null}
|
|
7240
|
+
${metric.contextBytes ?? null}, ${metric.contextUtilisation ?? null},
|
|
7241
|
+
${metric.success ?? true}
|
|
7133
7242
|
)
|
|
7134
7243
|
`;
|
|
7135
7244
|
}
|
|
7136
7245
|
async readToolMetrics() {
|
|
7137
7246
|
const rows = await this.sql`
|
|
7138
|
-
SELECT
|
|
7247
|
+
SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
|
|
7248
|
+
FROM tool_call_metrics
|
|
7139
7249
|
WHERE project_id = ${this.projectId}
|
|
7140
7250
|
ORDER BY timestamp
|
|
7251
|
+
LIMIT 5000 -- metrics are high-volume; task-1189 adds pagination for dashboard
|
|
7141
7252
|
`;
|
|
7142
7253
|
return rows.map(rowToToolCallMetric);
|
|
7143
7254
|
}
|
|
7255
|
+
async getToolCallCount(startedAt, completedAt) {
|
|
7256
|
+
const rows = await this.sql`
|
|
7257
|
+
SELECT COUNT(*)::text AS count
|
|
7258
|
+
FROM tool_call_metrics
|
|
7259
|
+
WHERE project_id = ${this.projectId}
|
|
7260
|
+
AND timestamp >= ${startedAt}
|
|
7261
|
+
AND timestamp <= ${completedAt}
|
|
7262
|
+
`;
|
|
7263
|
+
return parseInt(rows[0]?.count ?? "0", 10);
|
|
7264
|
+
}
|
|
7144
7265
|
// -------------------------------------------------------------------------
|
|
7145
7266
|
// Cost Summary
|
|
7146
7267
|
// -------------------------------------------------------------------------
|
|
7147
7268
|
async getCostSummary(cycleNumber) {
|
|
7148
7269
|
const metrics = cycleNumber != null ? await this.sql`
|
|
7149
|
-
SELECT
|
|
7270
|
+
SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
|
|
7271
|
+
FROM tool_call_metrics
|
|
7150
7272
|
WHERE project_id = ${this.projectId} AND cycle_number = ${cycleNumber}
|
|
7151
7273
|
` : await this.sql`
|
|
7152
|
-
SELECT
|
|
7274
|
+
SELECT timestamp, tool, duration_ms, input_tokens, output_tokens, estimated_cost_usd, model, cycle_number, context_bytes, context_utilisation
|
|
7275
|
+
FROM tool_call_metrics
|
|
7153
7276
|
WHERE project_id = ${this.projectId}
|
|
7154
7277
|
`;
|
|
7155
7278
|
let totalCostUsd = 0;
|
|
@@ -7186,7 +7309,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7186
7309
|
}
|
|
7187
7310
|
async getCostSnapshots() {
|
|
7188
7311
|
const rows = await this.sql`
|
|
7189
|
-
SELECT
|
|
7312
|
+
SELECT cycle, date, total_cost_usd, total_input_tokens, total_output_tokens, total_calls
|
|
7313
|
+
FROM cost_snapshots
|
|
7190
7314
|
WHERE project_id = ${this.projectId}
|
|
7191
7315
|
ORDER BY cycle
|
|
7192
7316
|
`;
|
|
@@ -7213,9 +7337,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7213
7337
|
}
|
|
7214
7338
|
async readCycleMetrics() {
|
|
7215
7339
|
const rows = await this.sql`
|
|
7216
|
-
SELECT
|
|
7340
|
+
SELECT cycle, date, accuracy, velocity
|
|
7341
|
+
FROM cycle_metrics_snapshots
|
|
7217
7342
|
WHERE project_id = ${this.projectId}
|
|
7218
7343
|
ORDER BY cycle
|
|
7344
|
+
LIMIT 500 -- one row per cycle; 500 is ~years of history
|
|
7219
7345
|
`;
|
|
7220
7346
|
return rows.map(rowToCycleMetrics);
|
|
7221
7347
|
}
|
|
@@ -7224,9 +7350,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7224
7350
|
// -------------------------------------------------------------------------
|
|
7225
7351
|
async readCycles() {
|
|
7226
7352
|
const rows = await this.sql`
|
|
7227
|
-
SELECT
|
|
7353
|
+
SELECT id, number, status, start_date, end_date, goals, board_health, task_ids
|
|
7354
|
+
FROM cycles
|
|
7228
7355
|
WHERE project_id = ${this.projectId}
|
|
7229
7356
|
ORDER BY number
|
|
7357
|
+
LIMIT 500 -- one row per cycle; 500 is years of history
|
|
7230
7358
|
`;
|
|
7231
7359
|
return rows.map(rowToCycle);
|
|
7232
7360
|
}
|
|
@@ -7273,25 +7401,31 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7273
7401
|
// -------------------------------------------------------------------------
|
|
7274
7402
|
async readHorizons() {
|
|
7275
7403
|
const rows = await this.sql`
|
|
7276
|
-
SELECT
|
|
7404
|
+
SELECT id, slug, label, description, status, sort_order, project_id, created_at, updated_at
|
|
7405
|
+
FROM horizons
|
|
7277
7406
|
WHERE project_id = ${this.projectId}
|
|
7278
7407
|
ORDER BY sort_order, created_at
|
|
7408
|
+
LIMIT 50 -- horizons are high-level; 50 is an ample ceiling
|
|
7279
7409
|
`;
|
|
7280
7410
|
return rows.map(rowToHorizon);
|
|
7281
7411
|
}
|
|
7282
7412
|
async readStages(horizonId) {
|
|
7283
7413
|
if (horizonId) {
|
|
7284
7414
|
const rows2 = await this.sql`
|
|
7285
|
-
SELECT
|
|
7415
|
+
SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
|
|
7416
|
+
FROM stages
|
|
7286
7417
|
WHERE project_id = ${this.projectId} AND horizon_id = ${horizonId}
|
|
7287
7418
|
ORDER BY sort_order, created_at
|
|
7419
|
+
LIMIT 100 -- stages per horizon are bounded; 100 is ample
|
|
7288
7420
|
`;
|
|
7289
7421
|
return rows2.map(rowToStage);
|
|
7290
7422
|
}
|
|
7291
7423
|
const rows = await this.sql`
|
|
7292
|
-
SELECT
|
|
7424
|
+
SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
|
|
7425
|
+
FROM stages
|
|
7293
7426
|
WHERE project_id = ${this.projectId}
|
|
7294
7427
|
ORDER BY sort_order, created_at
|
|
7428
|
+
LIMIT 200 -- all stages across horizons; 200 is a safe ceiling
|
|
7295
7429
|
`;
|
|
7296
7430
|
return rows.map(rowToStage);
|
|
7297
7431
|
}
|
|
@@ -7321,7 +7455,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7321
7455
|
}
|
|
7322
7456
|
async getActiveStage() {
|
|
7323
7457
|
const rows = await this.sql`
|
|
7324
|
-
SELECT
|
|
7458
|
+
SELECT id, slug, label, description, status, sort_order, horizon_id, project_id, exit_criteria, created_at, updated_at
|
|
7459
|
+
FROM stages
|
|
7325
7460
|
WHERE project_id = ${this.projectId} AND status = 'Active'
|
|
7326
7461
|
ORDER BY sort_order
|
|
7327
7462
|
LIMIT 1
|
|
@@ -7433,7 +7568,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7433
7568
|
}
|
|
7434
7569
|
async getDecisionEvents(decisionId, limit) {
|
|
7435
7570
|
const rows = await this.sql`
|
|
7436
|
-
SELECT
|
|
7571
|
+
SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
|
|
7572
|
+
FROM decision_events
|
|
7437
7573
|
WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
|
|
7438
7574
|
ORDER BY created_at DESC
|
|
7439
7575
|
LIMIT ${limit ?? 50}
|
|
@@ -7442,9 +7578,11 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7442
7578
|
}
|
|
7443
7579
|
async getDecisionEventsSince(cycle) {
|
|
7444
7580
|
const rows = await this.sql`
|
|
7445
|
-
SELECT
|
|
7581
|
+
SELECT id, decision_id, event_type, cycle, source, source_ref, detail, created_at
|
|
7582
|
+
FROM decision_events
|
|
7446
7583
|
WHERE project_id = ${this.projectId} AND cycle >= ${cycle}
|
|
7447
7584
|
ORDER BY created_at
|
|
7585
|
+
LIMIT 1000 -- bounded by cycle range; 1000 is ample
|
|
7448
7586
|
`;
|
|
7449
7587
|
return rows.map(rowToDecisionEvent);
|
|
7450
7588
|
}
|
|
@@ -7471,7 +7609,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7471
7609
|
}
|
|
7472
7610
|
async getDecisionScores(decisionId) {
|
|
7473
7611
|
const rows = await this.sql`
|
|
7474
|
-
SELECT
|
|
7612
|
+
SELECT id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
|
|
7613
|
+
FROM decision_scores
|
|
7475
7614
|
WHERE project_id = ${this.projectId} AND decision_id = ${decisionId}
|
|
7476
7615
|
ORDER BY cycle
|
|
7477
7616
|
`;
|
|
@@ -7479,7 +7618,7 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7479
7618
|
}
|
|
7480
7619
|
async getLatestDecisionScores() {
|
|
7481
7620
|
const rows = await this.sql`
|
|
7482
|
-
SELECT DISTINCT ON (decision_id)
|
|
7621
|
+
SELECT DISTINCT ON (decision_id) id, decision_id, cycle, effort, risk, reversibility, scale_cost, lock_in, total_score, rationale, created_at
|
|
7483
7622
|
FROM decision_scores
|
|
7484
7623
|
WHERE project_id = ${this.projectId}
|
|
7485
7624
|
ORDER BY decision_id, cycle DESC
|
|
@@ -7862,7 +8001,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7862
8001
|
build_report: task.buildReport ?? null,
|
|
7863
8002
|
task_type: task.taskType ?? null,
|
|
7864
8003
|
maturity: task.maturity ?? null,
|
|
7865
|
-
stage_id: task.stageId ?? null
|
|
8004
|
+
stage_id: task.stageId ?? null,
|
|
8005
|
+
opportunity: task.opportunity ?? null
|
|
7866
8006
|
};
|
|
7867
8007
|
});
|
|
7868
8008
|
const taskCols = [
|
|
@@ -7888,7 +8028,8 @@ ${newParts.join("\n")}` : newParts.join("\n");
|
|
|
7888
8028
|
"build_report",
|
|
7889
8029
|
"task_type",
|
|
7890
8030
|
"maturity",
|
|
7891
|
-
"stage_id"
|
|
8031
|
+
"stage_id",
|
|
8032
|
+
"opportunity"
|
|
7892
8033
|
];
|
|
7893
8034
|
const insertedRows = await tx`
|
|
7894
8035
|
INSERT INTO cycle_tasks ${tx(taskRows, ...taskCols)}
|
|
@@ -8394,6 +8535,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8394
8535
|
readToolMetrics() {
|
|
8395
8536
|
return this.invoke("readToolMetrics");
|
|
8396
8537
|
}
|
|
8538
|
+
insertPlanRun(entry) {
|
|
8539
|
+
return this.invoke("insertPlanRun", [entry]);
|
|
8540
|
+
}
|
|
8397
8541
|
getCostSummary(cycleNumber) {
|
|
8398
8542
|
return this.invoke("getCostSummary", [cycleNumber]);
|
|
8399
8543
|
}
|
|
@@ -8497,6 +8641,9 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8497
8641
|
getNorthStarStaleness() {
|
|
8498
8642
|
return this.invoke("getNorthStarStaleness");
|
|
8499
8643
|
}
|
|
8644
|
+
upsertNorthStar(statement, cycleNumber) {
|
|
8645
|
+
return this.invoke("upsertNorthStar", [statement, cycleNumber]);
|
|
8646
|
+
}
|
|
8500
8647
|
// --- Optional pg-only methods ---
|
|
8501
8648
|
getEstimationCalibration() {
|
|
8502
8649
|
return this.invoke("getEstimationCalibration");
|
|
@@ -8520,6 +8667,46 @@ Check PAPI_PROJECT_ID in your .mcp.json config. Find your project ID in the PAPI
|
|
|
8520
8667
|
submitBugReport(report) {
|
|
8521
8668
|
return this.invoke("submitBugReport", [report]);
|
|
8522
8669
|
}
|
|
8670
|
+
// --- Doc Registry ---
|
|
8671
|
+
registerDoc(entry) {
|
|
8672
|
+
return this.invoke("registerDoc", [entry]);
|
|
8673
|
+
}
|
|
8674
|
+
searchDocs(input) {
|
|
8675
|
+
return this.invoke("searchDocs", [input]);
|
|
8676
|
+
}
|
|
8677
|
+
getDoc(idOrPath) {
|
|
8678
|
+
return this.invoke("getDoc", [idOrPath]);
|
|
8679
|
+
}
|
|
8680
|
+
updateDocStatus(id, status, supersededBy) {
|
|
8681
|
+
return this.invoke("updateDocStatus", [id, status, supersededBy]);
|
|
8682
|
+
}
|
|
8683
|
+
// --- Cycle Learnings ---
|
|
8684
|
+
appendCycleLearnings(learnings) {
|
|
8685
|
+
return this.invoke("appendCycleLearnings", [learnings]);
|
|
8686
|
+
}
|
|
8687
|
+
getCycleLearnings(opts) {
|
|
8688
|
+
return this.invoke("getCycleLearnings", [opts]);
|
|
8689
|
+
}
|
|
8690
|
+
getCycleLearningPatterns() {
|
|
8691
|
+
return this.invoke("getCycleLearningPatterns", []);
|
|
8692
|
+
}
|
|
8693
|
+
updateCycleLearningActionRef(learningId, taskDisplayId) {
|
|
8694
|
+
return this.invoke("updateCycleLearningActionRef", [learningId, taskDisplayId]);
|
|
8695
|
+
}
|
|
8696
|
+
// --- Strategy Review Drafts ---
|
|
8697
|
+
savePendingReviewResponse(cycleNumber, rawResponse) {
|
|
8698
|
+
return this.invoke("savePendingReviewResponse", [cycleNumber, rawResponse]);
|
|
8699
|
+
}
|
|
8700
|
+
getPendingReviewResponse() {
|
|
8701
|
+
return this.invoke("getPendingReviewResponse", []);
|
|
8702
|
+
}
|
|
8703
|
+
clearPendingReviewResponse() {
|
|
8704
|
+
return this.invoke("clearPendingReviewResponse", []);
|
|
8705
|
+
}
|
|
8706
|
+
// --- Active Decisions ---
|
|
8707
|
+
confirmPendingActiveDecisions(cycleNumber) {
|
|
8708
|
+
return this.invoke("confirmPendingActiveDecisions", [cycleNumber]);
|
|
8709
|
+
}
|
|
8523
8710
|
// --- Atomic plan write-back ---
|
|
8524
8711
|
async planWriteBack(payload) {
|
|
8525
8712
|
const raw = await this.invoke("planWriteBack", [payload]);
|
|
@@ -8539,6 +8726,7 @@ __export(git_exports, {
|
|
|
8539
8726
|
createAndCheckoutBranch: () => createAndCheckoutBranch,
|
|
8540
8727
|
createPullRequest: () => createPullRequest,
|
|
8541
8728
|
createTag: () => createTag,
|
|
8729
|
+
cycleBranchName: () => cycleBranchName,
|
|
8542
8730
|
deleteLocalBranch: () => deleteLocalBranch,
|
|
8543
8731
|
detectBoardMismatches: () => detectBoardMismatches,
|
|
8544
8732
|
detectUnrecordedCommits: () => detectUnrecordedCommits,
|
|
@@ -8975,8 +9163,10 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
8975
9163
|
// release commits
|
|
8976
9164
|
/^[a-f0-9]+ Merge /,
|
|
8977
9165
|
// merge commits from PRs
|
|
8978
|
-
/chore\(task
|
|
9166
|
+
/chore\(task-/,
|
|
8979
9167
|
// task-related housekeeping
|
|
9168
|
+
/chore: dogfood log/
|
|
9169
|
+
// automated dogfood log entries post-release
|
|
8980
9170
|
];
|
|
8981
9171
|
return output.split("\n").filter((line) => line.trim() && !CYCLE_PATTERNS.some((p) => p.test(line))).map((line) => {
|
|
8982
9172
|
const spaceIdx = line.indexOf(" ");
|
|
@@ -8992,6 +9182,9 @@ function detectUnrecordedCommits(cwd, baseBranch) {
|
|
|
8992
9182
|
function taskBranchName(taskId) {
|
|
8993
9183
|
return `feat/${taskId}`;
|
|
8994
9184
|
}
|
|
9185
|
+
function cycleBranchName(cycleNumber, module) {
|
|
9186
|
+
return `feat/cycle-${cycleNumber}-${module.toLowerCase().replace(/\s+/g, "-")}`;
|
|
9187
|
+
}
|
|
8995
9188
|
function getHeadCommitSha(cwd) {
|
|
8996
9189
|
try {
|
|
8997
9190
|
return execFileSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8" }).trim() || null;
|
|
@@ -9426,7 +9619,7 @@ function formatDetailedTask(t) {
|
|
|
9426
9619
|
return `- **${t.id}:** ${t.title}
|
|
9427
9620
|
Status: ${t.status} | Priority: ${t.priority} | Complexity: ${t.complexity}${typeTag}
|
|
9428
9621
|
Module: ${t.module} | Epic: ${t.epic} | Phase: ${t.phase} | Owner: ${t.owner}
|
|
9429
|
-
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${notes ? `
|
|
9622
|
+
Reviewed: ${t.reviewed}${t.dependsOn ? ` | Depends on: ${t.dependsOn}` : ""}${hasHandoff ? " | Has BUILD HANDOFF: yes" : ""}${t.docRef ? ` | Doc ref: ${t.docRef}` : ""}${t.opportunity ? ` | Opportunity: ${t.opportunity}` : ""}${notes ? `
|
|
9430
9623
|
Notes: ${notes}` : ""}`;
|
|
9431
9624
|
}
|
|
9432
9625
|
function formatBoardForPlan(tasks, filters, currentCycle) {
|
|
@@ -9833,6 +10026,191 @@ function logDataSourceSummary(service, sources) {
|
|
|
9833
10026
|
console.error(`[data-sources] ${service}: ${populated.length}/${sources.length} sources have data \u2014 empty: ${emptyLabels}`);
|
|
9834
10027
|
}
|
|
9835
10028
|
|
|
10029
|
+
// src/lib/codebase-scan.ts
|
|
10030
|
+
import { execSync as execSync2 } from "child_process";
|
|
10031
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
10032
|
+
"a",
|
|
10033
|
+
"an",
|
|
10034
|
+
"the",
|
|
10035
|
+
"and",
|
|
10036
|
+
"or",
|
|
10037
|
+
"but",
|
|
10038
|
+
"in",
|
|
10039
|
+
"on",
|
|
10040
|
+
"at",
|
|
10041
|
+
"to",
|
|
10042
|
+
"for",
|
|
10043
|
+
"of",
|
|
10044
|
+
"with",
|
|
10045
|
+
"by",
|
|
10046
|
+
"from",
|
|
10047
|
+
"is",
|
|
10048
|
+
"are",
|
|
10049
|
+
"was",
|
|
10050
|
+
"were",
|
|
10051
|
+
"be",
|
|
10052
|
+
"been",
|
|
10053
|
+
"has",
|
|
10054
|
+
"have",
|
|
10055
|
+
"had",
|
|
10056
|
+
"do",
|
|
10057
|
+
"does",
|
|
10058
|
+
"did",
|
|
10059
|
+
"will",
|
|
10060
|
+
"would",
|
|
10061
|
+
"could",
|
|
10062
|
+
"should",
|
|
10063
|
+
"may",
|
|
10064
|
+
"might",
|
|
10065
|
+
"can",
|
|
10066
|
+
"not",
|
|
10067
|
+
"no",
|
|
10068
|
+
"if",
|
|
10069
|
+
"then",
|
|
10070
|
+
"than",
|
|
10071
|
+
"that",
|
|
10072
|
+
"this",
|
|
10073
|
+
"it",
|
|
10074
|
+
"its",
|
|
10075
|
+
"all",
|
|
10076
|
+
"each",
|
|
10077
|
+
"every",
|
|
10078
|
+
"both",
|
|
10079
|
+
"as",
|
|
10080
|
+
"so",
|
|
10081
|
+
"up",
|
|
10082
|
+
"out",
|
|
10083
|
+
"about",
|
|
10084
|
+
"into",
|
|
10085
|
+
"over",
|
|
10086
|
+
"after",
|
|
10087
|
+
"before",
|
|
10088
|
+
"between",
|
|
10089
|
+
"under",
|
|
10090
|
+
"above",
|
|
10091
|
+
"such",
|
|
10092
|
+
"only",
|
|
10093
|
+
"also",
|
|
10094
|
+
"just",
|
|
10095
|
+
"more",
|
|
10096
|
+
"most",
|
|
10097
|
+
"other",
|
|
10098
|
+
"some",
|
|
10099
|
+
"any",
|
|
10100
|
+
"new",
|
|
10101
|
+
"when",
|
|
10102
|
+
"how",
|
|
10103
|
+
"what",
|
|
10104
|
+
"which",
|
|
10105
|
+
"who",
|
|
10106
|
+
"add",
|
|
10107
|
+
"create",
|
|
10108
|
+
"build",
|
|
10109
|
+
"implement",
|
|
10110
|
+
"make",
|
|
10111
|
+
"update",
|
|
10112
|
+
"fix",
|
|
10113
|
+
"use",
|
|
10114
|
+
"via",
|
|
10115
|
+
"show",
|
|
10116
|
+
"display",
|
|
10117
|
+
"view",
|
|
10118
|
+
"page",
|
|
10119
|
+
"data",
|
|
10120
|
+
"based",
|
|
10121
|
+
"using",
|
|
10122
|
+
"task",
|
|
10123
|
+
"feature",
|
|
10124
|
+
"system",
|
|
10125
|
+
"tool",
|
|
10126
|
+
"mode",
|
|
10127
|
+
"field",
|
|
10128
|
+
"type",
|
|
10129
|
+
"status",
|
|
10130
|
+
"current",
|
|
10131
|
+
"default",
|
|
10132
|
+
"existing",
|
|
10133
|
+
"need",
|
|
10134
|
+
"instead",
|
|
10135
|
+
"allow",
|
|
10136
|
+
"change"
|
|
10137
|
+
]);
|
|
10138
|
+
function extractSearchTerms(title, notes) {
|
|
10139
|
+
const combined = `${title} ${notes ?? ""}`;
|
|
10140
|
+
const camelCase = combined.match(/[a-z][a-zA-Z]{5,}/g) ?? [];
|
|
10141
|
+
const snakeCase = combined.match(/[a-z]+_[a-z_]+/g) ?? [];
|
|
10142
|
+
const hyphenated = combined.match(/[a-z]+-[a-z]+-?[a-z]*/g) ?? [];
|
|
10143
|
+
const filePaths = combined.match(/[\w/.-]+\.(ts|tsx|js|jsx|sql|md)/g) ?? [];
|
|
10144
|
+
const words = combined.toLowerCase().replace(/[^a-z0-9\s_-]/g, " ").split(/\s+/).filter((w) => w.length >= 4 && !STOP_WORDS.has(w));
|
|
10145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10146
|
+
const terms = [];
|
|
10147
|
+
for (const group of [filePaths, camelCase, snakeCase, hyphenated, words]) {
|
|
10148
|
+
for (const term of group) {
|
|
10149
|
+
const normalized = term.toLowerCase();
|
|
10150
|
+
if (!seen.has(normalized) && normalized.length >= 4) {
|
|
10151
|
+
seen.add(normalized);
|
|
10152
|
+
terms.push(term);
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
return terms.slice(0, 8);
|
|
10157
|
+
}
|
|
10158
|
+
function grepForTerm(projectRoot, term) {
|
|
10159
|
+
try {
|
|
10160
|
+
const result = execSync2(
|
|
10161
|
+
`grep -rl --include='*.ts' --include='*.tsx' --include='*.js' --include='*.sql' --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.git --exclude-dir=.next ${JSON.stringify(term)} ${JSON.stringify(projectRoot)} 2>/dev/null | head -5`,
|
|
10162
|
+
{ encoding: "utf-8", timeout: 3e3 }
|
|
10163
|
+
);
|
|
10164
|
+
return result.trim().split("\n").filter(Boolean).map(
|
|
10165
|
+
(p) => p.replace(projectRoot + "/", "")
|
|
10166
|
+
);
|
|
10167
|
+
} catch {
|
|
10168
|
+
return [];
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
function scanCodebaseForTasks(projectRoot, tasks) {
|
|
10172
|
+
if (tasks.length === 0) return "";
|
|
10173
|
+
const startTime = Date.now();
|
|
10174
|
+
const results = [];
|
|
10175
|
+
for (const task of tasks) {
|
|
10176
|
+
const terms = extractSearchTerms(task.title, task.notes);
|
|
10177
|
+
if (terms.length === 0) continue;
|
|
10178
|
+
const matches = [];
|
|
10179
|
+
for (const term of terms) {
|
|
10180
|
+
if (term.length < 4) continue;
|
|
10181
|
+
const files = grepForTerm(projectRoot, term);
|
|
10182
|
+
if (files.length > 0) {
|
|
10183
|
+
matches.push({ term, files });
|
|
10184
|
+
}
|
|
10185
|
+
if (Date.now() - startTime > 5e3) {
|
|
10186
|
+
console.error(`[codebase-scan] timeout after ${Date.now() - startTime}ms \u2014 partial results returned`);
|
|
10187
|
+
break;
|
|
10188
|
+
}
|
|
10189
|
+
}
|
|
10190
|
+
if (matches.length > 0) {
|
|
10191
|
+
results.push({ taskId: task.id, terms, matches });
|
|
10192
|
+
}
|
|
10193
|
+
if (Date.now() - startTime > 5e3) break;
|
|
10194
|
+
}
|
|
10195
|
+
if (results.length === 0) return "";
|
|
10196
|
+
const elapsed = Date.now() - startTime;
|
|
10197
|
+
console.error(`[codebase-scan] scanned ${tasks.length} tasks in ${elapsed}ms \u2014 ${results.length} with matches`);
|
|
10198
|
+
const lines = [
|
|
10199
|
+
`Codebase scan found existing implementations for ${results.length}/${tasks.length} candidate tasks (${elapsed}ms):`,
|
|
10200
|
+
""
|
|
10201
|
+
];
|
|
10202
|
+
for (const result of results) {
|
|
10203
|
+
lines.push(`**${result.taskId}:**`);
|
|
10204
|
+
for (const match of result.matches.slice(0, 3)) {
|
|
10205
|
+
const fileList = match.files.slice(0, 3).join(", ");
|
|
10206
|
+
const moreCount = match.files.length > 3 ? ` (+${match.files.length - 3} more)` : "";
|
|
10207
|
+
lines.push(` - "${match.term}" \u2192 ${fileList}${moreCount}`);
|
|
10208
|
+
}
|
|
10209
|
+
lines.push("");
|
|
10210
|
+
}
|
|
10211
|
+
return lines.join("\n").trim();
|
|
10212
|
+
}
|
|
10213
|
+
|
|
9836
10214
|
// src/lib/slack.ts
|
|
9837
10215
|
async function sendSlackWebhook(webhookUrl, summary, header = "PAPI Strategy Review") {
|
|
9838
10216
|
if (!webhookUrl) return void 0;
|
|
@@ -9892,6 +10270,9 @@ Task: [title]
|
|
|
9892
10270
|
Cycle: [N]
|
|
9893
10271
|
Why now: [justification]
|
|
9894
10272
|
|
|
10273
|
+
DEPENDS ON
|
|
10274
|
+
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
10275
|
+
|
|
9895
10276
|
SCOPE (DO THIS)
|
|
9896
10277
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
9897
10278
|
|
|
@@ -10080,8 +10461,11 @@ Standard planning cycle with full board review.
|
|
|
10080
10461
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10081
10462
|
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
10082
10463
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
10083
|
-
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge
|
|
10464
|
+
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
10465
|
+
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
10084
10466
|
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
10467
|
+
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
10468
|
+
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
10085
10469
|
|
|
10086
10470
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
10087
10471
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
@@ -10100,11 +10484,17 @@ Standard planning cycle with full board review.
|
|
|
10100
10484
|
**Scope pre-check:** Before writing the SCOPE section of each handoff, cross-reference the task against the "Recently Shipped Capabilities" section in the context below (if present). For each candidate task: (1) check if the task's title or scope overlaps with any recently shipped task, (2) check if the FILES LIKELY TOUCHED overlap with files already modified in recent builds, (3) check the architecture notes from recent builds for patterns that already cover this task's scope. If >80% of a task's scope appears in recently shipped capabilities, recommend cancellation via \`boardCorrections\` or reduce the handoff scope to only the missing pieces \u2014 explicitly note what already exists. C126 task-728 was over-scoped because the planner assumed Blocked status needed creating from scratch \u2014 it already existed in types, DB, orient, and build_list. Over-scoped handoffs waste builder time on verification and cause estimation mismatches.
|
|
10101
10485
|
**Simplest Viable Path rule:** Before writing each BUILD HANDOFF, identify the simplest approach that satisfies the task's goal \u2014 the minimum change, fewest new abstractions, and smallest blast radius. Write the SCOPE (DO THIS) section for that simplest path FIRST. If you believe a more complex approach is warranted (new abstractions, multi-file refactors, framework changes), you MUST include a "WHY NOT SIMPLER" line in the handoff explaining why the simple path is insufficient. If you cannot articulate a concrete reason, use the simpler path. Pay special attention to tasks involving auth, data access, multi-user features, and infrastructure \u2014 these are the most common over-engineering targets.
|
|
10102
10486
|
**Maturity gate applies here:** Do NOT generate BUILD HANDOFFs for tasks that failed the maturity gate in step 6 (phase prerequisites not met, dependency chain incomplete). Raw tasks that the planner has scoped and upgraded to "investigated" in step 6 ARE eligible for handoffs.
|
|
10487
|
+
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected:
|
|
10488
|
+
- Populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s).
|
|
10489
|
+
- Add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch.
|
|
10490
|
+
- Keep the SCOPE sections independent (each task still has its own deliverable) but note the ordering in "Why now" \u2014 e.g. "depends on task-123 completing the adapter method".
|
|
10491
|
+
Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 do not attempt to resolve multi-level graphs. When in doubt, omit the dependency and let the builder discover it.
|
|
10103
10492
|
**Security section guidance:** Each handoff includes a SECURITY CONSIDERATIONS section. Populate it when the task involves: data exposure risks (PII, secrets in logs/storage), secrets or credentials handling (API keys, tokens, env vars), auth/access control changes, or dependency security risks (new packages, version changes). For pure refactoring, documentation, prompt-text, or UI-only tasks, write "None \u2014 no security-relevant changes".
|
|
10104
10493
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10105
10494
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10106
10495
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10107
10496
|
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10497
|
+
**Build order in cycle log:** If any intra-cycle dependencies were detected in this cycle, include a "Build Order" paragraph in \`cycleLogNotes\` showing the recommended build sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip this paragraph when no dependencies exist.
|
|
10108
10498
|
**Research task detection:** When a task's title starts with "Research:" or the task type is "research", add a RESEARCH OUTPUT section to the BUILD HANDOFF after ACCEPTANCE CRITERIA:
|
|
10109
10499
|
|
|
10110
10500
|
RESEARCH OUTPUT
|
|
@@ -10122,7 +10512,8 @@ Standard planning cycle with full board review.
|
|
|
10122
10512
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10123
10513
|
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."
|
|
10124
10514
|
|
|
10125
|
-
**UI/visual task detection:**
|
|
10515
|
+
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
10516
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10126
10517
|
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
10127
10518
|
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
10128
10519
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
@@ -10177,6 +10568,11 @@ var PLAN_FRAGMENT_BUG = `
|
|
|
10177
10568
|
var PLAN_FRAGMENT_IDEA = `
|
|
10178
10569
|
**Idea task detection:** When a task's task type is "idea", add a scope clarification note to the BUILD HANDOFF:
|
|
10179
10570
|
- Add to SCOPE (DO THIS): "This task originated as an idea. Confirm the exact deliverable before implementing \u2014 check task notes and any referenced docs for intent. If scope is unclear, flag it in the build report surprises."`;
|
|
10571
|
+
var PLAN_FRAGMENT_TASK = `
|
|
10572
|
+
**Task type detection:** When a task's task type is "task" (generic implementation task), add these handoff sections:
|
|
10573
|
+
- SCOPE (DO THIS) must include: a clear deliverable statement and what "done" looks like (e.g. "User can X", "Function returns Y", "Page renders Z").
|
|
10574
|
+
- Add to ACCEPTANCE CRITERIA: "[ ] Scope matches handoff \u2014 no unrelated code changed" and "[ ] Out-of-scope items documented if discovered during implementation."
|
|
10575
|
+
- Add a SCOPE BOUNDARY (DO NOT DO THIS) section with at least one explicit exclusion \u2014 state what this task is NOT responsible for.`;
|
|
10180
10576
|
var PLAN_FRAGMENT_SPIKE = `
|
|
10181
10577
|
**Spike task detection:** When a task's task type is "spike" or the title starts with "Spike:", apply these rules:
|
|
10182
10578
|
- Spikes are time-boxed investigations, not implementation tasks. The deliverable is a FINDING, not code.
|
|
@@ -10188,7 +10584,8 @@ var PLAN_FRAGMENT_SPIKE = `
|
|
|
10188
10584
|
- Keep SCOPE BOUNDARY, SECURITY CONSIDERATIONS, and PRE-BUILD VERIFICATION as normal.
|
|
10189
10585
|
- Spikes should be estimated conservatively: XS or S. If a spike needs M+ effort, it's not a spike \u2014 reclassify as a research task.`;
|
|
10190
10586
|
var PLAN_FRAGMENT_UI = `
|
|
10191
|
-
**UI/visual task detection:**
|
|
10587
|
+
**UI/visual task detection:** Apply these additions ONLY to tasks whose PRIMARY scope is frontend visual work \u2014 the task's main deliverable must be a UI change, new component, visual design, or page. Do NOT apply to backend tasks, DB migrations, or prompt/config changes that merely mention a dashboard or page in passing. Signal: the task would fail if no .tsx/.css files were changed. If uncertain, skip the UI additions.
|
|
10588
|
+
When a task IS a UI task (primary scope is visual/frontend):
|
|
10192
10589
|
- Add to SCOPE: "Read \`.impeccable.md\` for brand palette, design principles, and audience context before writing any code. Use the \`frontend-design\` skill for implementation."
|
|
10193
10590
|
- For M/L UI tasks, add to SCOPE: "Use the full UI toolchain: Playground (design preview) \u2192 Frontend-design (build) \u2192 Playwright (verify). The playground is the quality bar. Expect 2-3 iterations."
|
|
10194
10591
|
- Add to ACCEPTANCE CRITERIA: "[ ] Visually verify rendered output in browser \u2014 provide localhost URL or screenshot to user for review." and "[ ] No raw IDs, abbreviations, or jargon visible without human-readable labels or tooltips."
|
|
@@ -10267,8 +10664,11 @@ Standard planning cycle with full board review.
|
|
|
10267
10664
|
- **P3 Low** \u2014 Nice-to-have, speculative, or future-horizon work.
|
|
10268
10665
|
Within the same priority level, prefer tasks with the highest **impact-to-effort ratio**. Impact is measured by: (a) strategic alignment \u2014 does it advance the current horizon/phase? (b) unlocks other work \u2014 are tasks blocked by this? (c) user-facing \u2014 does it change what users see? (d) compounds over time \u2014 does it make future cycles faster? A high-impact Medium task beats a low-impact Small task at the same priority level. Justify in 2-3 sentences.
|
|
10269
10666
|
**Blocked tasks:** Tasks with status "Blocked" MUST be skipped during task selection \u2014 they are waiting on external dependencies or gates and cannot be built. Do NOT generate BUILD HANDOFFs for blocked tasks. Do NOT recommend blocked tasks. If a blocked task's gate has been resolved (check the notes and recent build reports), emit a \`boardCorrections\` entry to move it back to Backlog. Report blocked task count in the cycle log.
|
|
10270
|
-
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge
|
|
10667
|
+
**Cycle sizing:** Size the cycle based on what the selected tasks actually require \u2014 not a fixed budget. Select the highest-priority unblocked tasks, estimate each one's effort from its scope, and let the total emerge. The historical average effort from Methodology Trends is a reference point for calibration, not a target or floor. A healthy cycle has 6-10 tasks. Cycles with fewer than 5 tasks require explicit justification in the cycle log \u2014 explain why more tasks could not be included. When the backlog has 10+ tasks, the cycle SHOULD have 6+ tasks \u2014 undersized cycles waste planning overhead relative to the available work. If fewer than 5 tasks qualify after filtering (blocked, deferred, raw), check Deferred tasks \u2014 some may be ready to un-defer via a \`boardCorrections\` entry. A 1-task cycle is almost never correct. Prefer grouping tasks by module or similarity \u2014 reduces context switching and enables shared branches during the build phase.
|
|
10668
|
+
**Theme-driven sizing:** Single-theme cycles (all tasks in the same module or epic) can absorb 25-30 effort points because builders maintain context across tasks. Mixed-theme cycles should stay at 15-20 effort points to limit context switching. Use the theme to determine the budget, not a fixed number.
|
|
10271
10669
|
**Theme coherence:** After selecting candidate tasks, check whether they form a coherent theme \u2014 all serving one goal, phase, or module. Single-theme cycles produce better build quality and less context switching. If the top candidates touch 3+ unrelated modules or epics, prefer regrouping around the highest-priority theme and deferring the outliers. Mixed-theme cycles are acceptable when justified (e.g. a P0 fix alongside P1 feature work), but the justification must appear in the cycle log. Name the theme in 3-5 words \u2014 it becomes the \`cycleLogTitle\`.
|
|
10670
|
+
**Epic-aware batching:** Epic is the primary grouping signal for theme coherence. When multiple candidate tasks share the same epic (e.g. "Onboarding Redesign", "Dashboard Polish"), prefer co-scheduling them \u2014 they solve connected problems and benefit from shared context during the build. Steps: (1) After filtering by priority, group eligible tasks by epic. (2) If an epic has 3+ eligible tasks, prefer scheduling 2-4 of them together over cherry-picking across epics. (3) Report the epic distribution in the cycle log (e.g. "4 tasks from Onboarding epic, 1 from Platform"). Priority still overrides: a P0 fix from a different epic always takes precedence.
|
|
10671
|
+
**Opportunity clustering:** If backlog tasks have an \`opportunity\` field populated, group them by opportunity before selecting. Tasks sharing the same opportunity solve the same user problem \u2014 co-scheduling them produces more coherent cycles. Report opportunity clusters in the cycle log when present (e.g. "3 tasks clustered under 'planner accuracy' opportunity").
|
|
10272
10672
|
|
|
10273
10673
|
8. **Cycle Log** \u2014 Write 5-10 line entry: what was triaged, what was recommended and why, observations, AD updates. Include a **Priority Recalibration** paragraph if any unreviewed task priorities were changed during triage (Step 2) \u2014 list each by ID with old \u2192 new priority and rationale. Include a **Priority Drift Suggestions** paragraph if reviewed task drift was detected (Step 3).
|
|
10274
10674
|
**Cycle Notes** \u2014 Optionally include 1-3 lines of cycle-level observations in \`cycleLogNotes\`: estimation accuracy patterns, recurring blockers, velocity trends, or dependency signals. These notes persist across cycles so future planning runs can learn from them. Use null if there are no noteworthy observations this cycle.
|
|
@@ -10291,11 +10691,14 @@ Standard planning cycle with full board review.
|
|
|
10291
10691
|
**Estimation calibration:** Estimate **XS** for: copy/text-only changes, single string replacements, config tweaks, and any task where the scope is "change words in an existing file" with no logic changes. Estimate **S** for: wiring existing adapter methods, adding API routes following established patterns, modifying prompts, or documentation-only changes. Default to S for pattern-following work. Only use M when genuine new architecture, new DB tables, or multi-file architectural changes are needed. Historical data shows systematic over-estimation (198 over vs 8 under out of 528 tasks) \u2014 when in doubt, estimate smaller. If an "Estimation Calibration (Historical)" section is provided in the context below, use its data to adjust your estimates \u2014 it shows how often each estimated size matched the actual effort. Pay special attention to systematic over/under-estimation patterns (e.g. if M\u2192S happens frequently, estimate S instead of M for similar work).
|
|
10292
10692
|
**Reference docs:** If a task's notes include a \`Reference:\` path (e.g. \`Reference: docs/architecture/papi-brain-v1.md\`), include a REFERENCE DOCS section in the BUILD HANDOFF with those paths. This tells the builder to read the referenced doc for background context before implementing. Do NOT omit or summarise the reference \u2014 pass it through so the builder can access the full document. Only tasks with explicit \`Reference:\` paths in their notes should have this section.
|
|
10293
10693
|
**Pre-build verification:** EVERY handoff MUST include a PRE-BUILD VERIFICATION section listing 2-5 specific file paths the builder should read before implementing. Derive these from FILES LIKELY TOUCHED \u2014 pick the files most likely to already contain the target functionality. This is the #1 prevention mechanism for wasted build slots (C120, C125, C126 all scheduled already-shipped work). If the builder finds >80% of the scope already implemented, they report "already built" instead of re-implementing.
|
|
10294
|
-
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles
|
|
10694
|
+
**Pre-mortem:** For projects with 10+ cycles, include a PRE-MORTEM section in every BUILD HANDOFF with 1-3 bullet points: (a) most likely technical blocker based on module history, (b) integration risk with adjacent systems, (c) scope creep signal \u2014 what the builder might be tempted to expand beyond scope. Draw from \`dead_ends\` and \`surprises\` in recent build reports for the same module. Omit this section entirely for projects with fewer than 10 cycles.
|
|
10695
|
+
**Intra-cycle dependency detection:** After selecting cycle tasks, check every pair for build-order dependencies. Two tasks A and B have an intra-cycle dependency when A must be built before B because B consumes an artifact A creates \u2014 e.g. A adds a new adapter method that B calls, A creates a DB migration B depends on, A introduces a new shared type B imports, A refactors a utility B modifies. Signals: same module + adjacent scope (one is "add X", another is "use X"), or notes explicitly reference the other task. For each dependency detected: (a) populate the DEPENDS ON section in the dependent task's BUILD HANDOFF with the upstream task ID(s); (b) add a \`boardCorrections\` entry for the dependent task with \`updates.dependsOn\` set to the comma-separated upstream IDs \u2014 this persists the dependency so the builder's runtime can reuse the upstream branch; (c) keep SCOPE sections independent but note the ordering in "Why now". Do NOT invent dependencies where tasks merely share a module \u2014 only real build-order coupling counts. Linear chains only \u2014 no multi-level graph resolution. When in doubt, omit.
|
|
10696
|
+
**Build order in cycle log:** If intra-cycle dependencies were detected, include a "Build order:" line in \`cycleLogNotes\` showing the recommended sequence as arrow chains (e.g. "Build order: task-123 \u2192 task-124; task-130 standalone"). Skip when no dependencies exist.`);
|
|
10295
10697
|
if (flags.hasResearchTasks) parts.push(PLAN_FRAGMENT_RESEARCH);
|
|
10296
10698
|
if (flags.hasBugTasks) parts.push(PLAN_FRAGMENT_BUG);
|
|
10297
10699
|
if (flags.hasIdeaTasks) parts.push(PLAN_FRAGMENT_IDEA);
|
|
10298
10700
|
if (flags.hasSpikeTasks) parts.push(PLAN_FRAGMENT_SPIKE);
|
|
10701
|
+
if (flags.hasTaskTasks) parts.push(PLAN_FRAGMENT_TASK);
|
|
10299
10702
|
if (flags.hasUITasks) parts.push(PLAN_FRAGMENT_UI);
|
|
10300
10703
|
parts.push(`
|
|
10301
10704
|
11. **New Tasks (max 3 per cycle)** \u2014 Actively mine the Recent Build Reports for task candidates. For each report, check:
|
|
@@ -10360,6 +10763,9 @@ function buildPlanUserMessage(ctx) {
|
|
|
10360
10763
|
);
|
|
10361
10764
|
}
|
|
10362
10765
|
parts.push("", "---", "", "## PROJECT CONTEXT", "");
|
|
10766
|
+
if (ctx.contextTier) {
|
|
10767
|
+
parts.push(`**Context tier:** ${ctx.contextTier}`, "");
|
|
10768
|
+
}
|
|
10363
10769
|
parts.push("### Product Brief", "", ctx.productBrief, "");
|
|
10364
10770
|
if (ctx.northStar) {
|
|
10365
10771
|
parts.push("### North Star (current)", "", ctx.northStar, "");
|
|
@@ -10377,12 +10783,18 @@ function buildPlanUserMessage(ctx) {
|
|
|
10377
10783
|
if (ctx.cycleLog) {
|
|
10378
10784
|
parts.push("### Cycle Log", "", ctx.cycleLog, "");
|
|
10379
10785
|
}
|
|
10786
|
+
if (ctx.strategyReviewCadence) {
|
|
10787
|
+
parts.push("### Strategy Review Cadence (computed from DB)", "", ctx.strategyReviewCadence, "");
|
|
10788
|
+
}
|
|
10380
10789
|
if (ctx.board) {
|
|
10381
10790
|
parts.push("### Board", "", ctx.board, "");
|
|
10382
10791
|
}
|
|
10383
10792
|
if (ctx.preAssignedTasks) {
|
|
10384
10793
|
parts.push("### Pre-Assigned Tasks", "", ctx.preAssignedTasks, "");
|
|
10385
10794
|
}
|
|
10795
|
+
if (ctx.codebaseScan) {
|
|
10796
|
+
parts.push("### Codebase Scan (existing implementations)", "", ctx.codebaseScan, "");
|
|
10797
|
+
}
|
|
10386
10798
|
if (ctx.buildPatterns) {
|
|
10387
10799
|
parts.push("### Build Patterns", "", ctx.buildPatterns, "");
|
|
10388
10800
|
}
|
|
@@ -10624,6 +11036,8 @@ You MUST cover these 5 sections. Each is mandatory.
|
|
|
10624
11036
|
- Note any hierarchy/phase issues worth correcting (1-2 bullets max)
|
|
10625
11037
|
- Delete ADs that are legacy, process-level, or redundant without discussion
|
|
10626
11038
|
|
|
11039
|
+
**Registered Documents:** If a "### Registered Documents" section is present in context, scan it for: (a) research findings that contradict current ADs or strategy, (b) unactioned research that should influence the next plan. Reference relevant docs by title in your review. If unregistered docs are listed, flag 1-2 that look strategically relevant and suggest registering them.
|
|
11040
|
+
|
|
10627
11041
|
## CONDITIONAL SECTIONS (include only when genuinely useful \u2014 most reviews should have 0-2 of these)
|
|
10628
11042
|
|
|
10629
11043
|
6. **Security Posture Review** \u2014 Only if \`[SECURITY]\` tags exist in recent cycle logs.
|
|
@@ -10731,7 +11145,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
10731
11145
|
"category": "friction | methodology | signal | commercial",
|
|
10732
11146
|
"content": "string \u2014 specific observation from using PAPI on this project (e.g. 'deprioritise clears handoffs unnecessarily, wasting planner tokens')"
|
|
10733
11147
|
}
|
|
10734
|
-
]
|
|
11148
|
+
],
|
|
11149
|
+
"northStar": "string or null \u2014 the current North Star statement. Include if you assessed it in section 4 and it is still accurate (copy the current statement verbatim). Include the updated version if you revised it. Use null ONLY if no North Star has ever been set for this project."
|
|
10735
11150
|
}
|
|
10736
11151
|
\`\`\`
|
|
10737
11152
|
|
|
@@ -10918,7 +11333,8 @@ After your natural language output, include this EXACT format on its own line:
|
|
|
10918
11333
|
},
|
|
10919
11334
|
"oldLabel": "string \u2014 only for modify/remove: the previous phase label so tasks can be migrated"
|
|
10920
11335
|
}
|
|
10921
|
-
]
|
|
11336
|
+
],
|
|
11337
|
+
"northStar": "string or null \u2014 include the North Star statement if this strategic change defines or revises the project North Star. Use null if the change does not affect the North Star."
|
|
10922
11338
|
}
|
|
10923
11339
|
\`\`\`
|
|
10924
11340
|
|
|
@@ -10974,6 +11390,9 @@ Task: [title]
|
|
|
10974
11390
|
Cycle: [N]
|
|
10975
11391
|
Why now: [justification]
|
|
10976
11392
|
|
|
11393
|
+
DEPENDS ON
|
|
11394
|
+
[Optional \u2014 comma-separated task IDs this task depends on (e.g. "task-123, task-124"). Include only when another task in this same cycle must be built first because this task consumes artifacts it creates (e.g. new adapter method, new type, new migration). The builder will reuse the upstream task's branch so dependent commits stack on the same branch for a single PR. Omit this section entirely if there are no intra-cycle dependencies.]
|
|
11395
|
+
|
|
10977
11396
|
SCOPE (DO THIS)
|
|
10978
11397
|
[specific deliverables \u2014 write for the simplest viable path first]
|
|
10979
11398
|
|
|
@@ -11274,6 +11693,32 @@ async function getPrompt(name) {
|
|
|
11274
11693
|
}
|
|
11275
11694
|
|
|
11276
11695
|
// src/services/plan.ts
|
|
11696
|
+
function determineContextTier(cycleCount) {
|
|
11697
|
+
if (cycleCount <= 5) return 1;
|
|
11698
|
+
if (cycleCount <= 20) return 2;
|
|
11699
|
+
return 3;
|
|
11700
|
+
}
|
|
11701
|
+
function applyContextTier(ctx, cycleCount) {
|
|
11702
|
+
const tier = determineContextTier(cycleCount);
|
|
11703
|
+
const label = tier === 1 ? "Tier 1 (cycles 1-5)" : tier === 2 ? "Tier 2 (cycles 6-20)" : "Tier 3 (cycles 21+)";
|
|
11704
|
+
if (tier <= 2) {
|
|
11705
|
+
ctx.strategyRecommendations = void 0;
|
|
11706
|
+
ctx.dogfoodEntries = void 0;
|
|
11707
|
+
}
|
|
11708
|
+
if (tier === 1) {
|
|
11709
|
+
ctx.methodologyMetrics = void 0;
|
|
11710
|
+
ctx.carryForwardStaleness = void 0;
|
|
11711
|
+
ctx.discoveryCanvas = void 0;
|
|
11712
|
+
ctx.estimationCalibration = void 0;
|
|
11713
|
+
ctx.buildPatterns = void 0;
|
|
11714
|
+
ctx.reviewPatterns = void 0;
|
|
11715
|
+
ctx.horizonContext = void 0;
|
|
11716
|
+
ctx.registeredDocs = void 0;
|
|
11717
|
+
ctx.recentReviews = void 0;
|
|
11718
|
+
ctx.strategyReviewCadence = void 0;
|
|
11719
|
+
}
|
|
11720
|
+
return { tier, label };
|
|
11721
|
+
}
|
|
11277
11722
|
function determineMode(totalCycles) {
|
|
11278
11723
|
if (totalCycles === 0) return "bootstrap";
|
|
11279
11724
|
return "full";
|
|
@@ -11508,6 +11953,7 @@ function detectBoardFlags(tasks) {
|
|
|
11508
11953
|
let hasResearchTasks = false;
|
|
11509
11954
|
let hasIdeaTasks = false;
|
|
11510
11955
|
let hasSpikeTasks = false;
|
|
11956
|
+
let hasTaskTasks = false;
|
|
11511
11957
|
let hasUITasks = false;
|
|
11512
11958
|
const uiKeywords = /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i;
|
|
11513
11959
|
for (const t of tasks) {
|
|
@@ -11515,9 +11961,10 @@ function detectBoardFlags(tasks) {
|
|
|
11515
11961
|
if (t.taskType === "research" || /^Research:/i.test(t.title)) hasResearchTasks = true;
|
|
11516
11962
|
if (t.taskType === "idea") hasIdeaTasks = true;
|
|
11517
11963
|
if (t.taskType === "spike" || /^Spike:/i.test(t.title)) hasSpikeTasks = true;
|
|
11964
|
+
if (t.taskType === "task") hasTaskTasks = true;
|
|
11518
11965
|
if (uiKeywords.test(t.title) || uiKeywords.test(t.notes ?? "")) hasUITasks = true;
|
|
11519
11966
|
}
|
|
11520
|
-
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasUITasks };
|
|
11967
|
+
return { hasBugTasks, hasResearchTasks, hasIdeaTasks, hasSpikeTasks, hasTaskTasks, hasUITasks };
|
|
11521
11968
|
}
|
|
11522
11969
|
function detectBoardFlagsFromText(boardText) {
|
|
11523
11970
|
return {
|
|
@@ -11525,6 +11972,7 @@ function detectBoardFlagsFromText(boardText) {
|
|
|
11525
11972
|
hasResearchTasks: /\b(research|Research:)\b/i.test(boardText),
|
|
11526
11973
|
hasIdeaTasks: /\bidea\b/i.test(boardText),
|
|
11527
11974
|
hasSpikeTasks: /\b(spike|Spike:)\b/i.test(boardText),
|
|
11975
|
+
hasTaskTasks: /\btask\b/i.test(boardText),
|
|
11528
11976
|
hasUITasks: /\b(visual|design|UI|styling|refresh|frontend|landing page|hero|carousel|theme|layout|cockpit|dashboard|page)\b/i.test(boardText)
|
|
11529
11977
|
};
|
|
11530
11978
|
}
|
|
@@ -11735,6 +12183,9 @@ ${lines.join("\n")}`;
|
|
|
11735
12183
|
]);
|
|
11736
12184
|
timings["total"] = totalTimer();
|
|
11737
12185
|
console.error(`[plan-perf] assembleContext (lean): ${JSON.stringify(timings)}ms`);
|
|
12186
|
+
const gap = health.cyclesSinceLastStrategyReview;
|
|
12187
|
+
const lastReviewCycle = health.totalCycles - gap;
|
|
12188
|
+
const strategyReviewCadence = gap <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gap < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycle} (${gap} cycle(s) ago). Next due: C${lastReviewCycle + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycle} (${gap} cycles ago). Due now.`;
|
|
11738
12189
|
let ctx2 = {
|
|
11739
12190
|
mode,
|
|
11740
12191
|
cycleNumber: health.totalCycles,
|
|
@@ -11756,8 +12207,12 @@ ${lines.join("\n")}`;
|
|
|
11756
12207
|
boardFlags,
|
|
11757
12208
|
carryForwardStaleness: carryForwardStalenessLean,
|
|
11758
12209
|
preAssignedTasks: preAssignedTextLean,
|
|
11759
|
-
recentlyShippedCapabilities: recentlyShippedLean
|
|
12210
|
+
recentlyShippedCapabilities: recentlyShippedLean,
|
|
12211
|
+
strategyReviewCadence
|
|
11760
12212
|
};
|
|
12213
|
+
const { label: leanTierLabel } = applyContextTier(ctx2, health.totalCycles);
|
|
12214
|
+
ctx2.contextTier = leanTierLabel;
|
|
12215
|
+
console.error(`[plan-perf] context tier: ${leanTierLabel} (cycle ${health.totalCycles})`);
|
|
11761
12216
|
t = startTimer();
|
|
11762
12217
|
const prevHashes2 = contextHashesResult.status === "fulfilled" ? contextHashesResult.value : null;
|
|
11763
12218
|
const { ctx: diffedCtx2, newHashes: newHashes2, savedBytes: savedBytes2 } = applyContextDiff(ctx2, prevHashes2);
|
|
@@ -11819,7 +12274,8 @@ ${lines.join("\n")}`;
|
|
|
11819
12274
|
if (pendingRecsResultFull.status === "fulfilled" && pendingRecsResultFull.value.length > 0) {
|
|
11820
12275
|
strategyRecommendationsText = formatStrategyRecommendations(pendingRecsResultFull.value);
|
|
11821
12276
|
}
|
|
11822
|
-
const
|
|
12277
|
+
const filteredRaw = rawMetricsSnapshots.filter((s) => s.accuracy.length > 0 || s.velocity.length > 0);
|
|
12278
|
+
const metricsSnapshots = filteredRaw.length > 0 ? filteredRaw : computeSnapshotsFromBuildReports(allReportsForPatterns);
|
|
11823
12279
|
const discoveryCanvasTextFull = discoveryCanvasResultFull.status === "fulfilled" ? discoveryCanvasResultFull.value : void 0;
|
|
11824
12280
|
const taskCommentsTextFull = taskCommentsResultFull.status === "fulfilled" ? taskCommentsResultFull.value : void 0;
|
|
11825
12281
|
let registeredDocsTextFull;
|
|
@@ -11855,6 +12311,9 @@ ${lines.join("\n")}`;
|
|
|
11855
12311
|
const targetCycle = health.totalCycles + 1;
|
|
11856
12312
|
const preAssigned = strippedTasks.filter((t2) => t2.cycle === targetCycle);
|
|
11857
12313
|
const preAssignedText = formatPreAssignedTasks(preAssigned, targetCycle);
|
|
12314
|
+
const gapFull = health.cyclesSinceLastStrategyReview;
|
|
12315
|
+
const lastReviewCycleFull = health.totalCycles - gapFull;
|
|
12316
|
+
const strategyReviewCadenceFull = gapFull <= 0 ? `\u2713 Strategy review completed this cycle (C${health.totalCycles}). No carry-forward needed.` : gapFull < 5 ? `\u2713 Strategy review on track \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycle(s) ago). Next due: C${lastReviewCycleFull + 5}.` : `\u26A0\uFE0F Strategy review overdue \u2014 last review was C${lastReviewCycleFull} (${gapFull} cycles ago). Due now.`;
|
|
11858
12317
|
let ctx = {
|
|
11859
12318
|
mode,
|
|
11860
12319
|
cycleNumber: health.totalCycles,
|
|
@@ -11878,8 +12337,12 @@ ${lines.join("\n")}`;
|
|
|
11878
12337
|
boardFlags: boardFlagsFull,
|
|
11879
12338
|
carryForwardStaleness: computeCarryForwardStaleness(log),
|
|
11880
12339
|
preAssignedTasks: preAssignedText,
|
|
11881
|
-
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(
|
|
12340
|
+
recentlyShippedCapabilities: formatRecentlyShippedCapabilities(reports),
|
|
12341
|
+
strategyReviewCadence: strategyReviewCadenceFull
|
|
11882
12342
|
};
|
|
12343
|
+
const { label: fullTierLabel } = applyContextTier(ctx, health.totalCycles);
|
|
12344
|
+
ctx.contextTier = fullTierLabel;
|
|
12345
|
+
console.error(`[plan-perf] context tier: ${fullTierLabel} (cycle ${health.totalCycles})`);
|
|
11883
12346
|
const prevHashes = contextHashesResultFull.status === "fulfilled" ? contextHashesResultFull.value : null;
|
|
11884
12347
|
const { ctx: diffedCtx, newHashes, savedBytes } = applyContextDiff(ctx, prevHashes);
|
|
11885
12348
|
ctx = diffedCtx;
|
|
@@ -12069,7 +12532,15 @@ ${cleanContent}`;
|
|
|
12069
12532
|
taskCount: cycleTaskCount > 0 ? cycleTaskCount : void 0,
|
|
12070
12533
|
effortPoints: cycleEffortPoints > 0 ? cycleEffortPoints : void 0
|
|
12071
12534
|
});
|
|
12072
|
-
const
|
|
12535
|
+
const healthUpdates = {
|
|
12536
|
+
totalCycles: newCycleNumber,
|
|
12537
|
+
boardHealth: data.boardHealth,
|
|
12538
|
+
strategicDirection: data.strategicDirection
|
|
12539
|
+
};
|
|
12540
|
+
if (data.nextMode === "Full") {
|
|
12541
|
+
healthUpdates.lastFullMode = newCycleNumber;
|
|
12542
|
+
}
|
|
12543
|
+
const healthPromise = adapter2.setCycleHealth(healthUpdates);
|
|
12073
12544
|
const newTaskIdMap = /* @__PURE__ */ new Map();
|
|
12074
12545
|
const createTasksPromise = (async () => {
|
|
12075
12546
|
if (!data.newTasks || data.newTasks.length === 0) return;
|
|
@@ -12489,6 +12960,17 @@ async function preparePlan(adapter2, config2, filters, focus, force, handoffsOnl
|
|
|
12489
12960
|
}
|
|
12490
12961
|
if (skipHandoffs) context.skipHandoffs = true;
|
|
12491
12962
|
t = startTimer();
|
|
12963
|
+
try {
|
|
12964
|
+
const scanTasks = await adapter2.queryBoard({ status: ["Backlog", "In Cycle", "Ready"] });
|
|
12965
|
+
const candidates = scanTasks.filter((task) => task.priority !== "P3 Low").slice(0, 15).map((task) => ({ id: task.id, title: task.title, notes: task.notes }));
|
|
12966
|
+
const scanResult = scanCodebaseForTasks(config2.projectRoot, candidates);
|
|
12967
|
+
if (scanResult) context.codebaseScan = scanResult;
|
|
12968
|
+
} catch (err) {
|
|
12969
|
+
console.error(`[plan] codebase scan failed (non-critical): ${err instanceof Error ? err.message : err}`);
|
|
12970
|
+
}
|
|
12971
|
+
const scanMs = t();
|
|
12972
|
+
console.error(`[plan-perf] codebaseScan: ${scanMs}ms`);
|
|
12973
|
+
t = startTimer();
|
|
12492
12974
|
const userMessage = buildPlanUserMessage(context);
|
|
12493
12975
|
const buildMessageMs = t();
|
|
12494
12976
|
const totalMs = prepareTimer();
|
|
@@ -12665,6 +13147,7 @@ var lastPrepareSkipHandoffs;
|
|
|
12665
13147
|
var planTool = {
|
|
12666
13148
|
name: "plan",
|
|
12667
13149
|
description: 'Run once per cycle to select tasks and generate BUILD HANDOFFs. Call after setup (first time) or after completing all builds AND running release for the previous cycle. Returns prioritised task recommendations with detailed implementation specs. NEVER call when unbuilt cycle tasks exist \u2014 build and release first. First call returns a planning prompt for you to execute (prepare phase). Then call again with mode "apply" and your output to write results. Use skip_handoffs=true for large backlogs \u2014 handoffs are then generated separately via `handoff_generate`.',
|
|
13150
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
12668
13151
|
inputSchema: {
|
|
12669
13152
|
type: "object",
|
|
12670
13153
|
properties: {
|
|
@@ -12792,24 +13275,30 @@ async function handlePlan(adapter2, config2, args) {
|
|
|
12792
13275
|
return errorResponse('llm_response is required for mode "apply". Pass your complete plan output including the <!-- PAPI_STRUCTURED_OUTPUT --> block.');
|
|
12793
13276
|
}
|
|
12794
13277
|
const planMode = args.plan_mode || "full";
|
|
12795
|
-
const
|
|
13278
|
+
const rawCycleNumber = args.cycle_number != null ? Number(args.cycle_number) : NaN;
|
|
12796
13279
|
const strategyReviewWarning = args.strategy_review_warning || "";
|
|
12797
13280
|
const contextHashes = lastPrepareContextHashes;
|
|
12798
13281
|
const inputContext = lastPrepareUserMessage;
|
|
12799
13282
|
const contextBytes = lastPrepareContextBytes;
|
|
12800
13283
|
const expectedCycleNumber = lastPrepareCycleNumber;
|
|
12801
13284
|
const skipHandoffsCached = lastPrepareSkipHandoffs;
|
|
12802
|
-
lastPrepareContextHashes = void 0;
|
|
12803
|
-
lastPrepareUserMessage = void 0;
|
|
12804
|
-
lastPrepareContextBytes = void 0;
|
|
12805
|
-
lastPrepareCycleNumber = void 0;
|
|
12806
|
-
lastPrepareSkipHandoffs = void 0;
|
|
12807
13285
|
const skipHandoffs = args.skip_handoffs === true || skipHandoffsCached === true;
|
|
13286
|
+
const cycleNumber = !isNaN(rawCycleNumber) ? rawCycleNumber : expectedCycleNumber !== void 0 ? expectedCycleNumber : NaN;
|
|
13287
|
+
if (isNaN(cycleNumber)) {
|
|
13288
|
+
return errorResponse(
|
|
13289
|
+
"cycle_number is required for apply mode. Pass the cycle_number from the prepare phase output."
|
|
13290
|
+
);
|
|
13291
|
+
}
|
|
12808
13292
|
if (expectedCycleNumber !== void 0 && cycleNumber !== expectedCycleNumber) {
|
|
12809
13293
|
return errorResponse(
|
|
12810
13294
|
`cycle_number mismatch: prepare phase returned cycle ${expectedCycleNumber} but apply received ${cycleNumber}. Pass cycle_number: ${expectedCycleNumber} to match the prepare output.`
|
|
12811
13295
|
);
|
|
12812
13296
|
}
|
|
13297
|
+
lastPrepareContextHashes = void 0;
|
|
13298
|
+
lastPrepareUserMessage = void 0;
|
|
13299
|
+
lastPrepareContextBytes = void 0;
|
|
13300
|
+
lastPrepareCycleNumber = void 0;
|
|
13301
|
+
lastPrepareSkipHandoffs = void 0;
|
|
12813
13302
|
const result = await applyPlan(adapter2, config2, llmResponse, planMode, cycleNumber, strategyReviewWarning, contextHashes, { contextBytes: contextBytes ?? void 0, skipHandoffs: skipHandoffs || void 0 });
|
|
12814
13303
|
let utilisation;
|
|
12815
13304
|
if (inputContext) {
|
|
@@ -13008,7 +13497,7 @@ function classifyRecommendation(text) {
|
|
|
13008
13497
|
if (lower.includes("new task") || lower.includes("create task") || lower.includes("add task") || lower.includes("spike")) {
|
|
13009
13498
|
return "new_task";
|
|
13010
13499
|
}
|
|
13011
|
-
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood")) {
|
|
13500
|
+
if (lower.includes("process") || lower.includes("workflow") || lower.includes("methodology") || lower.includes("retrospective") || lower.includes("dogfood") || lower.includes("refine")) {
|
|
13012
13501
|
return "process_improvement";
|
|
13013
13502
|
}
|
|
13014
13503
|
if (lower.includes("infrastructure") || lower.includes("deploy") || lower.includes("ci/cd") || lower.includes("pipeline") || lower.includes("hosting") || lower.includes("database") || lower.includes("migration")) {
|
|
@@ -13610,21 +14099,6 @@ ${cleanContent}`;
|
|
|
13610
14099
|
});
|
|
13611
14100
|
} catch {
|
|
13612
14101
|
}
|
|
13613
|
-
try {
|
|
13614
|
-
const cycleLog = await adapter2.getCycleLogSince(cycleNumber);
|
|
13615
|
-
const currentEntry = cycleLog.find((e) => e.cycleNumber === cycleNumber);
|
|
13616
|
-
if (currentEntry) {
|
|
13617
|
-
const reviewCompleteText = `Strategy review completed Cycle ${cycleNumber}. Next due ~Cycle ${cycleNumber + 5}.`;
|
|
13618
|
-
const existingCf = currentEntry.carryForward ?? "";
|
|
13619
|
-
const updatedCf = existingCf ? existingCf.replace(/strategy review (?:due|available|overdue)[^.]*\.[^.]*/gi, reviewCompleteText).trim() : reviewCompleteText;
|
|
13620
|
-
const finalCf = updatedCf === existingCf ? `${existingCf} ${reviewCompleteText}`.trim() : updatedCf;
|
|
13621
|
-
await adapter2.writeCycleLogEntry({
|
|
13622
|
-
...currentEntry,
|
|
13623
|
-
carryForward: finalCf
|
|
13624
|
-
});
|
|
13625
|
-
}
|
|
13626
|
-
} catch {
|
|
13627
|
-
}
|
|
13628
14102
|
if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
|
|
13629
14103
|
await Promise.all(data.activeDecisionUpdates.map(async (ad) => {
|
|
13630
14104
|
if (ad.action === "delete" && adapter2.deleteActiveDecision) {
|
|
@@ -13683,6 +14157,12 @@ ${cleanContent}`;
|
|
|
13683
14157
|
} catch {
|
|
13684
14158
|
}
|
|
13685
14159
|
}
|
|
14160
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14161
|
+
try {
|
|
14162
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14163
|
+
} catch {
|
|
14164
|
+
}
|
|
14165
|
+
}
|
|
13686
14166
|
const compressionThreshold = cycleNumber - 5;
|
|
13687
14167
|
if (compressionThreshold > 0 && data.sessionLogCompressionSummary) {
|
|
13688
14168
|
await adapter2.compressCycleLog(compressionThreshold, data.sessionLogCompressionSummary);
|
|
@@ -13742,7 +14222,7 @@ ${cleanContent}`;
|
|
|
13742
14222
|
try {
|
|
13743
14223
|
const canvas = await adapter2.readDiscoveryCanvas();
|
|
13744
14224
|
const updates = {};
|
|
13745
|
-
|
|
14225
|
+
const populatedSections = [];
|
|
13746
14226
|
if (!canvas.landscapeReferences || canvas.landscapeReferences.length === 0) {
|
|
13747
14227
|
if (data.activeDecisionUpdates?.length) {
|
|
13748
14228
|
const entries = data.activeDecisionUpdates.filter((ad) => ad.body && ad.action !== "delete").slice(0, 3).map((ad) => ({ name: ad.id, notes: ad.body.slice(0, 200) }));
|
|
@@ -14005,10 +14485,17 @@ function formatVelocitySummary(reports, cycleCount) {
|
|
|
14005
14485
|
function formatRecentReportsSummary(reports, count) {
|
|
14006
14486
|
const recent = reports.sort((a, b2) => b2.cycle - a.cycle || b2.date.localeCompare(a.date)).slice(0, count);
|
|
14007
14487
|
if (recent.length === 0) return "No recent build reports.";
|
|
14488
|
+
const trunc = (s, max) => s && s !== "None" ? s.length > max ? s.slice(0, max) + "..." : s : null;
|
|
14008
14489
|
return recent.map((r) => {
|
|
14009
14490
|
const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
|
|
14010
|
-
const
|
|
14011
|
-
|
|
14491
|
+
const lines = [`- C${r.cycle} ${r.taskName}: ${effort}`];
|
|
14492
|
+
const surprises = trunc(r.surprises, 200);
|
|
14493
|
+
if (surprises) lines.push(` _Surprises:_ ${surprises}`);
|
|
14494
|
+
const issues = trunc(r.discoveredIssues, 200);
|
|
14495
|
+
if (issues) lines.push(` _Issues:_ ${issues}`);
|
|
14496
|
+
const arch = trunc(r.architectureNotes, 200);
|
|
14497
|
+
if (arch) lines.push(` _Architecture:_ ${arch}`);
|
|
14498
|
+
return lines.join("\n");
|
|
14012
14499
|
}).join("\n");
|
|
14013
14500
|
}
|
|
14014
14501
|
function formatPhasesForReview(phases, currentCycle) {
|
|
@@ -14034,7 +14521,7 @@ async function formatHierarchyForReview(adapter2, currentCycle, prefetchedTasks)
|
|
|
14034
14521
|
} catch {
|
|
14035
14522
|
}
|
|
14036
14523
|
if (horizons.length === 0 && phases.length === 0) return void 0;
|
|
14037
|
-
|
|
14524
|
+
const tasksByPhase = /* @__PURE__ */ new Map();
|
|
14038
14525
|
try {
|
|
14039
14526
|
const allTasks = prefetchedTasks ?? await adapter2.queryBoard();
|
|
14040
14527
|
for (const t of allTasks) {
|
|
@@ -14179,6 +14666,12 @@ ${cleanContent}`;
|
|
|
14179
14666
|
const currentPhases = await adapter2.readPhases();
|
|
14180
14667
|
await applyPhaseUpdates(adapter2, currentPhases, data.phaseUpdates);
|
|
14181
14668
|
}
|
|
14669
|
+
if (data.northStar && adapter2.upsertNorthStar) {
|
|
14670
|
+
try {
|
|
14671
|
+
await adapter2.upsertNorthStar(data.northStar, cycleNumber);
|
|
14672
|
+
} catch {
|
|
14673
|
+
}
|
|
14674
|
+
}
|
|
14182
14675
|
} catch (err) {
|
|
14183
14676
|
writeBackFailed = err instanceof Error ? err.message : String(err);
|
|
14184
14677
|
}
|
|
@@ -14301,6 +14794,7 @@ var lastReviewContextBytes;
|
|
|
14301
14794
|
var strategyReviewTool = {
|
|
14302
14795
|
name: "strategy_review",
|
|
14303
14796
|
description: 'Run a Strategy Review \u2014 assesses project direction, velocity, and Active Decisions. Produces recommendations and potential AD updates that feed into the next plan. Offered every 5 cycles; hard-blocked at 7+ overdue cycles. Run in its own dedicated session \u2014 do not mix with building. First call returns a review prompt for you to execute (prepare phase). Then call again with mode "apply" and your output. Pass `force: true` to run before the cadence gate.',
|
|
14797
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14304
14798
|
inputSchema: {
|
|
14305
14799
|
type: "object",
|
|
14306
14800
|
properties: {
|
|
@@ -14328,6 +14822,7 @@ var strategyReviewTool = {
|
|
|
14328
14822
|
var strategyChangeTool = {
|
|
14329
14823
|
name: "strategy_change",
|
|
14330
14824
|
description: 'Apply a strategic shift to the project. Three modes: "capture" for lightweight mid-conversation decision capture (no LLM round-trip), "prepare" to get a change prompt for full analysis, "apply" to persist analysis output. Use "capture" when you detect a strategic decision in conversation and want to persist it quickly without disrupting the build flow.',
|
|
14825
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14331
14826
|
inputSchema: {
|
|
14332
14827
|
type: "object",
|
|
14333
14828
|
properties: {
|
|
@@ -14665,6 +15160,7 @@ async function archiveTasks(adapter2, phases, statuses) {
|
|
|
14665
15160
|
var boardViewTool = {
|
|
14666
15161
|
name: "board_view",
|
|
14667
15162
|
description: 'View the Board. By default shows active tasks only (excludes Done/Cancelled), sorted by priority, limited to 50. Use status="all" to see everything. Use mode="summary" for counts only (no task details). Does not call the Anthropic API.',
|
|
15163
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
14668
15164
|
inputSchema: {
|
|
14669
15165
|
type: "object",
|
|
14670
15166
|
properties: {
|
|
@@ -14696,6 +15192,7 @@ var boardViewTool = {
|
|
|
14696
15192
|
var boardDeprioritiseTool = {
|
|
14697
15193
|
name: "board_deprioritise",
|
|
14698
15194
|
description: `Remove a task from the current cycle. Four actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from planner), "block" (waiting on external dependency \u2014 visible on board but skipped by planner), "cancel" (don't want this \u2014 permanently closed with reason). When a user rejects a task, ALWAYS ask which action they want. Does not call the Anthropic API.`,
|
|
15195
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14699
15196
|
inputSchema: {
|
|
14700
15197
|
type: "object",
|
|
14701
15198
|
properties: {
|
|
@@ -14731,6 +15228,7 @@ var boardDeprioritiseTool = {
|
|
|
14731
15228
|
var boardArchiveTool = {
|
|
14732
15229
|
name: "board_archive",
|
|
14733
15230
|
description: "Archive tasks from the Board to the archive file. When both phase and status are provided, only tasks matching BOTH are archived (AND logic). When only one is provided, all matching tasks are archived. Does not call the Anthropic API.",
|
|
15231
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
14734
15232
|
inputSchema: {
|
|
14735
15233
|
type: "object",
|
|
14736
15234
|
properties: {
|
|
@@ -14749,6 +15247,7 @@ var boardArchiveTool = {
|
|
|
14749
15247
|
var boardEditTool = {
|
|
14750
15248
|
name: "board_edit",
|
|
14751
15249
|
description: "Edit fields on an existing task. Supports title, priority, complexity, module, epic, phase, notes, status, and maturity. Pass task_id plus any fields to update. Does not call the Anthropic API.",
|
|
15250
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
14752
15251
|
inputSchema: {
|
|
14753
15252
|
type: "object",
|
|
14754
15253
|
properties: {
|
|
@@ -15015,7 +15514,7 @@ async function handleBoardEdit(adapter2, args) {
|
|
|
15015
15514
|
try {
|
|
15016
15515
|
const dogfoodLog = await adapter2.getDogfoodLog?.(50) ?? [];
|
|
15017
15516
|
const linked = dogfoodLog.filter((e) => e.linkedTaskId === taskId || e.linkedTaskId === task.id);
|
|
15018
|
-
const newStatus =
|
|
15517
|
+
const newStatus = "resolved";
|
|
15019
15518
|
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, newStatus)));
|
|
15020
15519
|
} catch {
|
|
15021
15520
|
}
|
|
@@ -15387,6 +15886,7 @@ When the system compresses prior messages, immediately:
|
|
|
15387
15886
|
|
|
15388
15887
|
- **XS/S tasks in the same cycle and module:** Group on shared branch. One PR, one merge.
|
|
15389
15888
|
- **M/L tasks or different modules:** Own branch per task. Isolated PRs.
|
|
15889
|
+
- **Dependent tasks (any size):** When a task's BUILD HANDOFF lists a \`DEPENDS ON\` task from the same cycle, \`build_execute\` automatically reuses the upstream task's branch so commits stack for a single PR. Do not create a separate branch manually.
|
|
15390
15890
|
- **Commit per task within grouped branches** \u2014 traceable git history.
|
|
15391
15891
|
- **Never use \`build_execute\` with \`light=true\` on shared branches.** Light mode commits directly to the current branch without creating a PR. When a shared branch is squash-merged, those commits are collapsed \u2014 any CLAUDE.md or documentation changes are stripped. Use light mode only on isolated single-task branches where no squash-merge will occur.
|
|
15392
15892
|
|
|
@@ -16186,6 +16686,7 @@ async function applySetup(adapter2, config2, input, briefText, adSeedText, conve
|
|
|
16186
16686
|
var setupTool = {
|
|
16187
16687
|
name: "setup",
|
|
16188
16688
|
description: `Initialise a new PAPI project or adopt an existing codebase. Only 3 inputs needed: project name, what it does, and who it's for. Set existing_project: true to adopt an existing codebase \u2014 PAPI scans the project structure and generates a context-aware brief, hierarchy, and initial backlog tasks. First call returns prompts (prepare phase), then call again with mode "apply" and your outputs. After setup, run plan to start your first cycle.`,
|
|
16689
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
16189
16690
|
inputSchema: {
|
|
16190
16691
|
type: "object",
|
|
16191
16692
|
properties: {
|
|
@@ -16454,6 +16955,9 @@ init_git();
|
|
|
16454
16955
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
16455
16956
|
import { readdirSync as readdirSync3, existsSync as existsSync3, readFileSync } from "fs";
|
|
16456
16957
|
import { join as join5 } from "path";
|
|
16958
|
+
var buildStartTimes = /* @__PURE__ */ new Map();
|
|
16959
|
+
var taskBranchMap = /* @__PURE__ */ new Map();
|
|
16960
|
+
var SHARED_BRANCH_COMPLEXITIES = /* @__PURE__ */ new Set(["XS", "Small"]);
|
|
16457
16961
|
function capitalizeCompleted(value) {
|
|
16458
16962
|
const map = {
|
|
16459
16963
|
yes: "Yes",
|
|
@@ -16473,7 +16977,7 @@ function pushAndCreatePR(config2, taskId, taskTitle) {
|
|
|
16473
16977
|
if (!isGitAvailable() || !isGitRepo(config2.projectRoot)) {
|
|
16474
16978
|
return lines;
|
|
16475
16979
|
}
|
|
16476
|
-
const featureBranch = taskBranchName(taskId);
|
|
16980
|
+
const featureBranch = taskBranchMap.get(taskId) ?? taskBranchName(taskId);
|
|
16477
16981
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16478
16982
|
if (currentBranch !== featureBranch) {
|
|
16479
16983
|
return lines;
|
|
@@ -16605,10 +17109,38 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16605
17109
|
if (options.light) {
|
|
16606
17110
|
branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
|
|
16607
17111
|
} else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
16608
|
-
const
|
|
17112
|
+
const cycleHealth = await adapter2.getCycleHealth().catch(() => null);
|
|
17113
|
+
const cycleNumber = cycleHealth?.totalCycles ?? 0;
|
|
17114
|
+
let depBranchReuse = null;
|
|
17115
|
+
if (task.dependsOn) {
|
|
17116
|
+
const depIds = task.dependsOn.split(",").map((d) => d.trim()).filter(Boolean);
|
|
17117
|
+
for (const depId of depIds) {
|
|
17118
|
+
const mappedBranch = taskBranchMap.get(depId);
|
|
17119
|
+
if (mappedBranch && branchExists(config2.projectRoot, mappedBranch)) {
|
|
17120
|
+
depBranchReuse = { branch: mappedBranch, upstreamId: depId };
|
|
17121
|
+
break;
|
|
17122
|
+
}
|
|
17123
|
+
const fallbackBranch = taskBranchName(depId);
|
|
17124
|
+
if (branchExists(config2.projectRoot, fallbackBranch)) {
|
|
17125
|
+
depBranchReuse = { branch: fallbackBranch, upstreamId: depId };
|
|
17126
|
+
break;
|
|
17127
|
+
}
|
|
17128
|
+
}
|
|
17129
|
+
}
|
|
17130
|
+
const useSharedBranch = !depBranchReuse && SHARED_BRANCH_COMPLEXITIES.has(task.complexity) && !!task.module && cycleNumber > 0;
|
|
17131
|
+
const featureBranch = depBranchReuse ? depBranchReuse.branch : useSharedBranch ? cycleBranchName(cycleNumber, task.module) : taskBranchName(taskId);
|
|
17132
|
+
if (depBranchReuse) {
|
|
17133
|
+
branchLines.push(
|
|
17134
|
+
`Reusing branch '${depBranchReuse.branch}' from dependency ${depBranchReuse.upstreamId} \u2014 commits will stack for a single PR.`
|
|
17135
|
+
);
|
|
17136
|
+
}
|
|
17137
|
+
taskBranchMap.set(taskId, featureBranch);
|
|
16609
17138
|
const currentBranch = getCurrentBranch(config2.projectRoot);
|
|
16610
17139
|
if (currentBranch === featureBranch) {
|
|
16611
17140
|
branchLines.push(`Already on branch '${featureBranch}'.`);
|
|
17141
|
+
if (useSharedBranch) {
|
|
17142
|
+
branchLines.push(`Reusing shared cycle branch for ${task.complexity} ${task.module} task.`);
|
|
17143
|
+
}
|
|
16612
17144
|
} else {
|
|
16613
17145
|
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
16614
17146
|
throw new Error("Working directory has uncommitted changes. Please commit or stash them before running `build_execute`.");
|
|
@@ -16617,13 +17149,14 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16617
17149
|
if (baseBranch !== config2.baseBranch) {
|
|
16618
17150
|
branchLines.push(`Base branch '${config2.baseBranch}' not found \u2014 using '${baseBranch}'.`);
|
|
16619
17151
|
}
|
|
16620
|
-
|
|
17152
|
+
const featureBranchExists = branchExists(config2.projectRoot, featureBranch);
|
|
17153
|
+
if (currentBranch !== baseBranch && !featureBranchExists) {
|
|
16621
17154
|
const checkout = checkoutBranch(config2.projectRoot, baseBranch);
|
|
16622
17155
|
if (!checkout.success) {
|
|
16623
17156
|
branchLines.push(`Warning: ${checkout.message} Proceeding on current branch '${currentBranch}'.`);
|
|
16624
17157
|
}
|
|
16625
17158
|
}
|
|
16626
|
-
if (hasRemote(config2.projectRoot)) {
|
|
17159
|
+
if (hasRemote(config2.projectRoot) && !featureBranchExists) {
|
|
16627
17160
|
const pull = gitPull(config2.projectRoot);
|
|
16628
17161
|
branchLines.push(pull.success ? pull.message : `Warning: ${pull.message}`);
|
|
16629
17162
|
}
|
|
@@ -16633,10 +17166,10 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16633
17166
|
`Warning: ${unmerged.length} unmerged feature branch${unmerged.length === 1 ? "" : "es"}: ${unmerged.join(", ")}. New branch may diverge if these contain changes needed here.`
|
|
16634
17167
|
);
|
|
16635
17168
|
}
|
|
16636
|
-
if (
|
|
17169
|
+
if (featureBranchExists) {
|
|
16637
17170
|
const checkout = checkoutBranch(config2.projectRoot, featureBranch);
|
|
16638
17171
|
branchLines.push(
|
|
16639
|
-
checkout.success ? `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
|
|
17172
|
+
checkout.success ? useSharedBranch ? `Checked out shared cycle branch '${featureBranch}' \u2014 reusing for ${task.complexity} ${task.module} task.` : `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
|
|
16640
17173
|
);
|
|
16641
17174
|
if (checkout.success) {
|
|
16642
17175
|
branchLines.push(
|
|
@@ -16646,7 +17179,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16646
17179
|
} else {
|
|
16647
17180
|
const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
|
|
16648
17181
|
branchLines.push(
|
|
16649
|
-
create.success ? `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
17182
|
+
create.success ? useSharedBranch ? `Created shared cycle branch '${featureBranch}' for ${task.module} XS/S tasks.` : `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
|
|
16650
17183
|
);
|
|
16651
17184
|
if (create.success) {
|
|
16652
17185
|
branchLines.push(
|
|
@@ -16659,6 +17192,7 @@ async function startBuild(adapter2, config2, taskId, options = {}) {
|
|
|
16659
17192
|
if (task.status !== "In Progress") {
|
|
16660
17193
|
await adapter2.updateTaskStatus(taskId, "In Progress");
|
|
16661
17194
|
}
|
|
17195
|
+
buildStartTimes.set(taskId, (/* @__PURE__ */ new Date()).toISOString());
|
|
16662
17196
|
let phaseChanges = [];
|
|
16663
17197
|
try {
|
|
16664
17198
|
phaseChanges = await propagatePhaseStatus(adapter2);
|
|
@@ -16724,13 +17258,32 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16724
17258
|
correctionsCount: input.correctionsCount,
|
|
16725
17259
|
briefImplications: input.briefImplications,
|
|
16726
17260
|
deadEnds: input.deadEnds,
|
|
16727
|
-
iterationCount
|
|
17261
|
+
iterationCount,
|
|
17262
|
+
startedAt: buildStartTimes.get(taskId) ?? void 0,
|
|
17263
|
+
completedAt: now.toISOString()
|
|
16728
17264
|
};
|
|
17265
|
+
buildStartTimes.delete(taskId);
|
|
17266
|
+
taskBranchMap.delete(taskId);
|
|
16729
17267
|
if (input.relatedDecisions) {
|
|
16730
17268
|
const adIds = input.relatedDecisions.split(",").map((s) => s.trim()).filter(Boolean);
|
|
16731
17269
|
if (adIds.length > 0) report.relatedDecisions = adIds;
|
|
16732
17270
|
}
|
|
17271
|
+
if (report.startedAt && report.completedAt && typeof adapter2.getToolCallCount === "function") {
|
|
17272
|
+
try {
|
|
17273
|
+
const count = await adapter2.getToolCallCount(report.startedAt, report.completedAt);
|
|
17274
|
+
if (count > 0) report.toolCallCount = count;
|
|
17275
|
+
} catch {
|
|
17276
|
+
}
|
|
17277
|
+
}
|
|
16733
17278
|
await adapter2.appendBuildReport(report);
|
|
17279
|
+
let reportWriteVerified;
|
|
17280
|
+
if (typeof adapter2.getBuildReportCountForTask === "function") {
|
|
17281
|
+
try {
|
|
17282
|
+
const postWriteCount = await adapter2.getBuildReportCountForTask(taskId);
|
|
17283
|
+
reportWriteVerified = postWriteCount >= iterationCount;
|
|
17284
|
+
} catch {
|
|
17285
|
+
}
|
|
17286
|
+
}
|
|
16734
17287
|
if (adapter2.appendCycleLearnings) {
|
|
16735
17288
|
const learnings = [];
|
|
16736
17289
|
const taskModule = task.module ?? "";
|
|
@@ -16777,6 +17330,39 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16777
17330
|
}
|
|
16778
17331
|
}
|
|
16779
17332
|
}
|
|
17333
|
+
let autoTriagedCount = 0;
|
|
17334
|
+
if (input.discoveredIssues && input.discoveredIssues !== "None" && typeof adapter2.createTask === "function") {
|
|
17335
|
+
const issueLines = input.discoveredIssues.split(/\n|;/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17336
|
+
for (const line of issueLines) {
|
|
17337
|
+
const sevMatch = line.match(/^(P[0-3])[\s:]+/i);
|
|
17338
|
+
if (!sevMatch) continue;
|
|
17339
|
+
const severityLabel = sevMatch[1].toUpperCase();
|
|
17340
|
+
const priority = severityLabel === "P0" || severityLabel === "P1" ? "P1 High" : severityLabel === "P2" ? "P2 Medium" : "P3 Low";
|
|
17341
|
+
const titleRaw = line.replace(/^P[0-3][\s:]+/i, "").trim();
|
|
17342
|
+
const title = titleRaw.length > 120 ? titleRaw.slice(0, 120) : titleRaw;
|
|
17343
|
+
if (!title) continue;
|
|
17344
|
+
try {
|
|
17345
|
+
await adapter2.createTask({
|
|
17346
|
+
uuid: "",
|
|
17347
|
+
displayId: "",
|
|
17348
|
+
title: `[Auto-triaged] ${title}`,
|
|
17349
|
+
status: "Backlog",
|
|
17350
|
+
priority,
|
|
17351
|
+
complexity: "Small",
|
|
17352
|
+
module: task.module ?? "",
|
|
17353
|
+
phase: task.phase ?? "",
|
|
17354
|
+
owner: "papi",
|
|
17355
|
+
reviewed: false,
|
|
17356
|
+
taskType: "discovery",
|
|
17357
|
+
source: "build_complete",
|
|
17358
|
+
notes: `Origin: ${task.displayId} (${task.title}), cycle ${cycleNumber}. Original issue: ${line}`,
|
|
17359
|
+
createdCycle: cycleNumber
|
|
17360
|
+
});
|
|
17361
|
+
autoTriagedCount++;
|
|
17362
|
+
} catch {
|
|
17363
|
+
}
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
16780
17366
|
if (adapter2.updateCycleLearningActionRef && task.notes) {
|
|
16781
17367
|
const learningRefs = task.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
16782
17368
|
if (learningRefs) {
|
|
@@ -16819,14 +17405,60 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16819
17405
|
await adapter2.updateTaskStatus(taskId, "In Review");
|
|
16820
17406
|
}
|
|
16821
17407
|
}
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
17408
|
+
let dogfoodResolvedCount = 0;
|
|
17409
|
+
if (input.completed === "yes" && adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
17410
|
+
try {
|
|
17411
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17412
|
+
const linked = dogfoodLog.filter(
|
|
17413
|
+
(e) => (e.linkedTaskId === taskId || e.linkedTaskId === task.id) && e.status !== "resolved"
|
|
17414
|
+
);
|
|
17415
|
+
if (linked.length > 0) {
|
|
17416
|
+
await Promise.all(linked.map((e) => adapter2.updateDogfoodEntryStatus(e.id, "resolved")));
|
|
17417
|
+
dogfoodResolvedCount = linked.length;
|
|
17418
|
+
}
|
|
17419
|
+
} catch {
|
|
17420
|
+
}
|
|
17421
|
+
}
|
|
17422
|
+
let learningsLinkedCount = 0;
|
|
17423
|
+
if (input.completed === "yes" && adapter2.getCycleLearnings && adapter2.updateCycleLearningActionRef) {
|
|
17424
|
+
try {
|
|
17425
|
+
const recentLearnings = await adapter2.getCycleLearnings({ limit: 30 });
|
|
17426
|
+
const unactioned = recentLearnings.filter(
|
|
17427
|
+
(l) => !l.actionRef && l.cycleNumber >= cycleNumber - 5
|
|
17428
|
+
);
|
|
17429
|
+
if (unactioned.length > 0) {
|
|
17430
|
+
const taskText = `${task.title} ${task.notes ?? ""}`.toLowerCase();
|
|
17431
|
+
const taskWords = new Set(
|
|
17432
|
+
taskText.match(/\b[a-z]{4,}\b/g) ?? []
|
|
17433
|
+
);
|
|
17434
|
+
const taskModule = (task.module ?? "").toLowerCase();
|
|
17435
|
+
for (const learning of unactioned) {
|
|
17436
|
+
const learningModule = (learning.tags[0] ?? "").toLowerCase();
|
|
17437
|
+
if (!taskModule || !learningModule || taskModule !== learningModule) continue;
|
|
17438
|
+
const learningText = `${learning.summary} ${learning.detail ?? ""}`.toLowerCase();
|
|
17439
|
+
const learningWords = learningText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
17440
|
+
const hasKeywordOverlap = learningWords.some((w) => taskWords.has(w));
|
|
17441
|
+
if (!hasKeywordOverlap) continue;
|
|
17442
|
+
if (learning.id) {
|
|
17443
|
+
try {
|
|
17444
|
+
await adapter2.updateCycleLearningActionRef(learning.id, task.id);
|
|
17445
|
+
learningsLinkedCount++;
|
|
17446
|
+
} catch {
|
|
17447
|
+
}
|
|
17448
|
+
}
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17451
|
+
} catch {
|
|
17452
|
+
}
|
|
17453
|
+
}
|
|
17454
|
+
const statusNote = input.completed === "yes" ? options.light ? `Task "${task.title}" (${taskId}) marked Done (light mode \u2014 no review needed).` : `Task "${task.title}" (${taskId}) marked In Review \u2014 ready for your sign-off via \`review_submit\`.` : `Task "${task.title}" (${taskId}) status unchanged (completed: ${input.completed}).`;
|
|
17455
|
+
let commitLine;
|
|
17456
|
+
if (config2.autoCommit) {
|
|
17457
|
+
commitLine = autoCommit(config2, taskId, task.title);
|
|
17458
|
+
} else {
|
|
17459
|
+
commitLine = "Auto-commit: skipped (PAPI_AUTO_COMMIT=false).";
|
|
17460
|
+
}
|
|
17461
|
+
if (isGitAvailable() && isGitRepo(config2.projectRoot)) {
|
|
16830
17462
|
const sha = getHeadCommitSha(config2.projectRoot);
|
|
16831
17463
|
if (sha) report.commitSha = sha;
|
|
16832
17464
|
const baseBranch = resolveBaseBranch(config2.projectRoot, config2.baseBranch);
|
|
@@ -16919,7 +17551,11 @@ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
|
|
|
16919
17551
|
completed: input.completed,
|
|
16920
17552
|
scopeAccuracy: input.scopeAccuracy,
|
|
16921
17553
|
phaseChanges,
|
|
16922
|
-
docWarning
|
|
17554
|
+
docWarning,
|
|
17555
|
+
dogfoodResolvedCount: dogfoodResolvedCount > 0 ? dogfoodResolvedCount : void 0,
|
|
17556
|
+
learningsLinkedCount: learningsLinkedCount > 0 ? learningsLinkedCount : void 0,
|
|
17557
|
+
autoTriagedCount: autoTriagedCount > 0 ? autoTriagedCount : void 0,
|
|
17558
|
+
reportWriteVerified
|
|
16923
17559
|
};
|
|
16924
17560
|
}
|
|
16925
17561
|
async function cancelBuild(adapter2, taskId, reason) {
|
|
@@ -17012,6 +17648,7 @@ ${instructions}`;
|
|
|
17012
17648
|
var buildListTool = {
|
|
17013
17649
|
name: "build_list",
|
|
17014
17650
|
description: "List cycle tasks that have BUILD HANDOFFs ready for execution. Shows task ID, title, status, priority, and complexity. In Progress tasks appear first, then Backlog. Does not call the Anthropic API.",
|
|
17651
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17015
17652
|
inputSchema: {
|
|
17016
17653
|
type: "object",
|
|
17017
17654
|
properties: {},
|
|
@@ -17021,6 +17658,7 @@ var buildListTool = {
|
|
|
17021
17658
|
var buildDescribeTool = {
|
|
17022
17659
|
name: "build_describe",
|
|
17023
17660
|
description: "Show the full BUILD HANDOFF for a specific task, including scope, acceptance criteria, and implementation guidance. Does not call the Anthropic API.",
|
|
17661
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
17024
17662
|
inputSchema: {
|
|
17025
17663
|
type: "object",
|
|
17026
17664
|
properties: {
|
|
@@ -17035,6 +17673,7 @@ var buildDescribeTool = {
|
|
|
17035
17673
|
var buildExecuteTool = {
|
|
17036
17674
|
name: "build_execute",
|
|
17037
17675
|
description: "Start or complete a build task. Call with just task_id to start (returns BUILD HANDOFF, creates feature branch, marks In Progress). After implementing the task, you MUST call build_execute again with all report fields (completed, effort, estimated_effort, surprises, discovered_issues, architecture_notes) to finish \u2014 do not wait for user confirmation between start and complete. Never call on tasks that are already In Review or Done. Does not call the Anthropic API. Set light=true to skip branch/PR creation (commits to current branch). Set PAPI_LIGHT_MODE=true in env to default all builds to light mode.",
|
|
17676
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17038
17677
|
inputSchema: {
|
|
17039
17678
|
type: "object",
|
|
17040
17679
|
properties: {
|
|
@@ -17067,7 +17706,7 @@ var buildExecuteTool = {
|
|
|
17067
17706
|
},
|
|
17068
17707
|
discovered_issues: {
|
|
17069
17708
|
type: "string",
|
|
17070
|
-
description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete.`
|
|
17709
|
+
description: `Problems found DURING this build that are OUTSIDE this task's scope. Include severity (P0-P3). Good: "P2: Auth middleware doesn't validate token expiry \u2014 affects all protected routes." Bad: "Had to install a dependency." Only real bugs or gaps that need their own task. Use "None" if none. Required for complete. TIP: When submitting a follow-up idea for a discovered issue, include "learning:<uuid>" in the idea notes to link it to this cycle learning entry \u2014 use the UUID returned in the build completion output.`
|
|
17071
17710
|
},
|
|
17072
17711
|
architecture_notes: {
|
|
17073
17712
|
type: "string",
|
|
@@ -17119,6 +17758,7 @@ var buildExecuteTool = {
|
|
|
17119
17758
|
var buildCancelTool = {
|
|
17120
17759
|
name: "build_cancel",
|
|
17121
17760
|
description: "Cancel a build task with a reason. Sets the task status to Cancelled and records the closure reason. Does not call the Anthropic API.",
|
|
17761
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
17122
17762
|
inputSchema: {
|
|
17123
17763
|
type: "object",
|
|
17124
17764
|
properties: {
|
|
@@ -17286,9 +17926,28 @@ If >80% of the scope is already implemented, call \`build_execute\` with complet
|
|
|
17286
17926
|
adSection = formatRelevantADs(relevant);
|
|
17287
17927
|
} catch {
|
|
17288
17928
|
}
|
|
17929
|
+
let dogfoodSection = "";
|
|
17930
|
+
try {
|
|
17931
|
+
if (adapter2.getDogfoodLog) {
|
|
17932
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
17933
|
+
const linked = dogfoodLog.filter(
|
|
17934
|
+
(e) => e.linkedTaskId === result.task.id || e.linkedTaskId === result.task.displayId
|
|
17935
|
+
);
|
|
17936
|
+
if (linked.length > 0) {
|
|
17937
|
+
const entries = linked.map((e) => `- [${e.category}] ${e.content}`).join("\n");
|
|
17938
|
+
dogfoodSection = `
|
|
17939
|
+
|
|
17940
|
+
---
|
|
17941
|
+
|
|
17942
|
+
**DOGFOOD CONTEXT** \u2014 This task was linked to ${linked.length} observation(s):
|
|
17943
|
+
${entries}`;
|
|
17944
|
+
}
|
|
17945
|
+
}
|
|
17946
|
+
} catch {
|
|
17947
|
+
}
|
|
17289
17948
|
const moduleInstructions = getModuleInstructions(result.task.module);
|
|
17290
17949
|
const moduleContext = await getModuleContext(adapter2, result.task);
|
|
17291
|
-
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + verificationNote + chainInstruction + phaseNote);
|
|
17950
|
+
return textResponse(header + serializeBuildHandoff(result.task.buildHandoff) + adSection + moduleInstructions + moduleContext + dogfoodSection + verificationNote + chainInstruction + phaseNote);
|
|
17292
17951
|
} catch (err) {
|
|
17293
17952
|
if (isNoHandoffError(err)) {
|
|
17294
17953
|
const lines = [
|
|
@@ -17395,6 +18054,18 @@ function formatCompleteResult(result) {
|
|
|
17395
18054
|
lines.push(`Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`);
|
|
17396
18055
|
}
|
|
17397
18056
|
}
|
|
18057
|
+
if (result.dogfoodResolvedCount) {
|
|
18058
|
+
lines.push("", `Resolved ${result.dogfoodResolvedCount} dogfood observation(s) linked to this task.`);
|
|
18059
|
+
}
|
|
18060
|
+
if (result.learningsLinkedCount) {
|
|
18061
|
+
lines.push("", `Linked ${result.learningsLinkedCount} unactioned learning(s) to this task.`);
|
|
18062
|
+
}
|
|
18063
|
+
if (result.autoTriagedCount) {
|
|
18064
|
+
lines.push("", `\u{1F516} Auto-triaged ${result.autoTriagedCount} discovered issue(s) to Backlog.`);
|
|
18065
|
+
}
|
|
18066
|
+
if (result.reportWriteVerified === false) {
|
|
18067
|
+
lines.push("", "\u26A0\uFE0F Build report write could not be verified \u2014 the report may not have been persisted. Run `build_list` to check, and resubmit if missing.");
|
|
18068
|
+
}
|
|
17398
18069
|
if (result.docWarning) {
|
|
17399
18070
|
lines.push("", `\u{1F4C4} ${result.docWarning}`);
|
|
17400
18071
|
}
|
|
@@ -17534,7 +18205,7 @@ function resolveCurrentPhase(phases) {
|
|
|
17534
18205
|
const sorted = [...phases].sort((a, b2) => a.order - b2.order);
|
|
17535
18206
|
return sorted[0].label;
|
|
17536
18207
|
}
|
|
17537
|
-
var
|
|
18208
|
+
var STOP_WORDS2 = /* @__PURE__ */ new Set([
|
|
17538
18209
|
"a",
|
|
17539
18210
|
"an",
|
|
17540
18211
|
"the",
|
|
@@ -17628,7 +18299,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
17628
18299
|
]);
|
|
17629
18300
|
function extractKeywords(text) {
|
|
17630
18301
|
return new Set(
|
|
17631
|
-
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !
|
|
18302
|
+
text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w))
|
|
17632
18303
|
);
|
|
17633
18304
|
}
|
|
17634
18305
|
async function findSimilarTasks(adapter2, ideaTitle) {
|
|
@@ -17699,9 +18370,10 @@ ${lines.join("\n")}
|
|
|
17699
18370
|
const VALID_COMPLEXITIES2 = /* @__PURE__ */ new Set(["XS", "Small", "Medium", "Large", "XL"]);
|
|
17700
18371
|
const priority = input.priority && VALID_PRIORITIES2.has(input.priority) ? input.priority : "P2 Medium";
|
|
17701
18372
|
const complexity = input.complexity && VALID_COMPLEXITIES2.has(input.complexity) ? input.complexity : "Small";
|
|
17702
|
-
const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike"]);
|
|
18373
|
+
const VALID_TYPES = /* @__PURE__ */ new Set(["task", "bug", "research", "idea", "spike", "discovery"]);
|
|
17703
18374
|
let taskTitle = input.text;
|
|
17704
18375
|
let taskType = "idea";
|
|
18376
|
+
let typeInferred = false;
|
|
17705
18377
|
if (input.type && VALID_TYPES.has(input.type)) {
|
|
17706
18378
|
taskType = input.type;
|
|
17707
18379
|
} else {
|
|
@@ -17716,6 +18388,20 @@ ${lines.join("\n")}
|
|
|
17716
18388
|
taskType = PREFIX_MAP[key];
|
|
17717
18389
|
taskTitle = input.text.slice(prefixMatch[0].length);
|
|
17718
18390
|
}
|
|
18391
|
+
} else {
|
|
18392
|
+
const searchText = `${input.text} ${input.notes ?? ""}`.toLowerCase();
|
|
18393
|
+
if (/\b(bug|fix|broken|crash|error)\b/.test(searchText)) {
|
|
18394
|
+
taskType = "bug";
|
|
18395
|
+
} else if (/\b(research|investigate|explore|spike)\b/.test(searchText)) {
|
|
18396
|
+
taskType = "research";
|
|
18397
|
+
} else if (/\b(performance|optimize|speed|latency)\b/.test(searchText)) {
|
|
18398
|
+
taskType = "task";
|
|
18399
|
+
} else if (/\b(verify|confirm)\b/.test(searchText)) {
|
|
18400
|
+
taskType = "spike";
|
|
18401
|
+
} else {
|
|
18402
|
+
taskType = "task";
|
|
18403
|
+
}
|
|
18404
|
+
typeInferred = true;
|
|
17719
18405
|
}
|
|
17720
18406
|
}
|
|
17721
18407
|
const task = await adapter2.createTask({
|
|
@@ -17735,7 +18421,8 @@ ${lines.join("\n")}
|
|
|
17735
18421
|
taskType,
|
|
17736
18422
|
maturity: "raw",
|
|
17737
18423
|
docRef: input.docRef,
|
|
17738
|
-
source: "llm"
|
|
18424
|
+
source: "llm",
|
|
18425
|
+
opportunity: input.opportunity
|
|
17739
18426
|
});
|
|
17740
18427
|
if (input.notes && adapter2.updateCycleLearningActionRef) {
|
|
17741
18428
|
const learningRefs = input.notes.match(/learning:([a-f0-9-]+)/gi);
|
|
@@ -17761,7 +18448,27 @@ ${lines.join("\n")}
|
|
|
17761
18448
|
}
|
|
17762
18449
|
}
|
|
17763
18450
|
}
|
|
17764
|
-
|
|
18451
|
+
if (adapter2.getDogfoodLog && adapter2.updateDogfoodEntryStatus) {
|
|
18452
|
+
try {
|
|
18453
|
+
const dogfoodLog = await adapter2.getDogfoodLog(50);
|
|
18454
|
+
const unlinked = dogfoodLog.filter((e) => e.status === "observed" && !e.linkedTaskId);
|
|
18455
|
+
if (unlinked.length > 0) {
|
|
18456
|
+
const taskText = `${task.title} ${input.notes ?? ""}`.toLowerCase();
|
|
18457
|
+
const taskKeywords = taskText.match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18458
|
+
const taskKeywordSet = new Set(taskKeywords);
|
|
18459
|
+
for (const entry of unlinked) {
|
|
18460
|
+
const entryKeywords = entry.content.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [];
|
|
18461
|
+
const overlap = entryKeywords.filter((w) => taskKeywordSet.has(w));
|
|
18462
|
+
if (overlap.length >= 2) {
|
|
18463
|
+
await adapter2.updateDogfoodEntryStatus(entry.id, "backlog-created", task.id);
|
|
18464
|
+
}
|
|
18465
|
+
}
|
|
18466
|
+
}
|
|
18467
|
+
} catch {
|
|
18468
|
+
}
|
|
18469
|
+
}
|
|
18470
|
+
const typeNote = typeInferred ? ` [type: ${taskType} \u2014 inferred from text]` : "";
|
|
18471
|
+
return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${typeNote}` };
|
|
17765
18472
|
}
|
|
17766
18473
|
var CANVAS_SECTION_LABELS = {
|
|
17767
18474
|
landscape: "Landscape References",
|
|
@@ -17801,6 +18508,7 @@ async function routeToDiscovery(adapter2, section, input) {
|
|
|
17801
18508
|
// src/tools/idea.ts
|
|
17802
18509
|
var ideaTool = {
|
|
17803
18510
|
name: "idea",
|
|
18511
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17804
18512
|
description: "Capture an idea as a Backlog task. The next plan run will triage and scope it. Use anytime to log bugs, feature requests, or improvements without interrupting the current cycle. IMPORTANT: If this idea originates from a research or planning session, you MUST include a Reference: line in notes pointing to the source doc. Without it, the planner has no context and will misinterpret the intent. Does not call the Anthropic API.",
|
|
17805
18513
|
inputSchema: {
|
|
17806
18514
|
type: "object",
|
|
@@ -17811,7 +18519,7 @@ var ideaTool = {
|
|
|
17811
18519
|
},
|
|
17812
18520
|
notes: {
|
|
17813
18521
|
type: "string",
|
|
17814
|
-
description: 'Additional context, constraints, or reasoning. MANDATORY: If this idea comes from a research or planning session, include a "Reference: <path>" line pointing to the source doc. Tasks submitted without references get misinterpreted by the planner \u2014 this is the #1 cause of wasted build slots (C146: task-807 was scoped as landing page copy when it was actually a dashboard UX task, because the source research doc was missing). Use doc_search to find relevant docs before submitting.'
|
|
18522
|
+
description: 'Additional context, constraints, or reasoning. MANDATORY: If this idea comes from a research or planning session, include a "Reference: <path>" line pointing to the source doc. Tasks submitted without references get misinterpreted by the planner \u2014 this is the #1 cause of wasted build slots (C146: task-807 was scoped as landing page copy when it was actually a dashboard UX task, because the source research doc was missing). Use doc_search to find relevant docs before submitting. TIP: If this idea addresses a known cycle learning, include "learning:<uuid>" in the notes (e.g. "learning:abc12345-..."). This links the idea to the learning entry and marks it actioned in the pipeline. Example: "Addresses recurring friction. learning:3f9a1c2e-..."'
|
|
17815
18523
|
},
|
|
17816
18524
|
module: {
|
|
17817
18525
|
type: "string",
|
|
@@ -17845,12 +18553,16 @@ var ideaTool = {
|
|
|
17845
18553
|
},
|
|
17846
18554
|
type: {
|
|
17847
18555
|
type: "string",
|
|
17848
|
-
enum: ["task", "bug", "research", "spike"],
|
|
17849
|
-
description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments. The planner uses this to generate type-specific BUILD HANDOFFs.'
|
|
18556
|
+
enum: ["task", "bug", "research", "spike", "discovery"],
|
|
18557
|
+
description: 'Task type. Defaults to "task". Use "bug" for defects, "research" for investigation tasks, "spike" for time-boxed experiments, "discovery" for issues found during a build that need their own task. The planner uses this to generate type-specific BUILD HANDOFFs.'
|
|
17850
18558
|
},
|
|
17851
18559
|
doc_ref: {
|
|
17852
18560
|
type: "string",
|
|
17853
18561
|
description: 'Path to a reference document (e.g. "docs/research/foo.md"). Stored as a structured field \u2014 replaces the fragile "Reference:" line in notes.'
|
|
18562
|
+
},
|
|
18563
|
+
opportunity: {
|
|
18564
|
+
type: "string",
|
|
18565
|
+
description: "What user problem does this solve? Auto-fill from problem context in notes when submitting ideas that describe a user pain point. The planner uses this to cluster backlog tasks by opportunity."
|
|
17854
18566
|
}
|
|
17855
18567
|
},
|
|
17856
18568
|
required: ["text"]
|
|
@@ -17879,7 +18591,8 @@ async function handleIdea(adapter2, config2, args) {
|
|
|
17879
18591
|
discovery: args.discovery === true,
|
|
17880
18592
|
force: args.force === true,
|
|
17881
18593
|
docRef: args.doc_ref?.trim(),
|
|
17882
|
-
type: args.type
|
|
18594
|
+
type: args.type,
|
|
18595
|
+
opportunity: args.opportunity?.trim()
|
|
17883
18596
|
};
|
|
17884
18597
|
const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
|
|
17885
18598
|
const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
|
|
@@ -17986,6 +18699,7 @@ function collectDiagnostics(config2) {
|
|
|
17986
18699
|
var bugTool = {
|
|
17987
18700
|
name: "bug",
|
|
17988
18701
|
description: "Report a bug. Two modes: (1) Default \u2014 creates a Backlog task with severity-based priority for the project board. (2) With report=true \u2014 submits a diagnostic bug report with system info for cross-project visibility (external user issue reporting). Does not call the Anthropic API.",
|
|
18702
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
17989
18703
|
inputSchema: {
|
|
17990
18704
|
type: "object",
|
|
17991
18705
|
properties: {
|
|
@@ -18166,6 +18880,7 @@ var VALID_EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
|
18166
18880
|
var adHocTool = {
|
|
18167
18881
|
name: "ad_hoc",
|
|
18168
18882
|
description: "Record work done outside the normal cycle. Creates a Done task with a lightweight build report, or associates work with an existing task if task_id is provided (without changing task status \u2014 use build_execute for status transitions). Use for quick fixes, bug patches, or ad-hoc changes. Does not call the Anthropic API.",
|
|
18883
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18169
18884
|
inputSchema: {
|
|
18170
18885
|
type: "object",
|
|
18171
18886
|
properties: {
|
|
@@ -18376,9 +19091,9 @@ async function prepareReconcile(adapter2) {
|
|
|
18376
19091
|
}
|
|
18377
19092
|
return lines.join("\n");
|
|
18378
19093
|
}
|
|
18379
|
-
var
|
|
19094
|
+
var STOP_WORDS3 = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
|
|
18380
19095
|
function tokenize(s) {
|
|
18381
|
-
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !
|
|
19096
|
+
return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS3.has(w));
|
|
18382
19097
|
}
|
|
18383
19098
|
function titleKeywords(title) {
|
|
18384
19099
|
return new Set(tokenize(title));
|
|
@@ -18555,6 +19270,7 @@ async function applyRetriage(adapter2, retriages) {
|
|
|
18555
19270
|
var boardReconcileTool = {
|
|
18556
19271
|
name: "board_reconcile",
|
|
18557
19272
|
description: 'Holistic backlog review to group, merge, cancel, defer, or retriage tasks. "prepare"/"apply" for cleanup. "retriage-prepare"/"retriage-apply" to reassess priority and complexity on existing backlog tasks. Does not call the Anthropic API.',
|
|
19273
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
18558
19274
|
inputSchema: {
|
|
18559
19275
|
type: "object",
|
|
18560
19276
|
properties: {
|
|
@@ -18885,432 +19601,99 @@ Assess each task above and produce your retriage output. Then call \`board_recon
|
|
|
18885
19601
|
return errorResponse(`Unknown mode: ${mode}. Use "prepare", "apply", "retriage-prepare", or "retriage-apply".`);
|
|
18886
19602
|
}
|
|
18887
19603
|
|
|
18888
|
-
// src/services/
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
|
|
18892
|
-
|
|
18893
|
-
|
|
18894
|
-
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
|
|
18902
|
-
|
|
18903
|
-
|
|
18904
|
-
|
|
18905
|
-
|
|
18906
|
-
|
|
18907
|
-
|
|
18908
|
-
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
|
|
18917
|
-
|
|
18918
|
-
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
|
|
19604
|
+
// src/services/release.ts
|
|
19605
|
+
init_git();
|
|
19606
|
+
import { writeFile as writeFile3 } from "fs/promises";
|
|
19607
|
+
import { join as join6 } from "path";
|
|
19608
|
+
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
19609
|
+
|
|
19610
|
+
## v0.1.0-alpha \u2014 Initial Release
|
|
19611
|
+
|
|
19612
|
+
PAPI MCP Server \u2014 the AI-powered project planning framework.
|
|
19613
|
+
|
|
19614
|
+
### Commands
|
|
19615
|
+
- **setup** \u2014 Initialise a new PAPI project with Product Brief generation
|
|
19616
|
+
- **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
|
|
19617
|
+
- **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
|
|
19618
|
+
- **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
|
|
19619
|
+
- **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
|
|
19620
|
+
- **review_list / review_submit** \u2014 Human review loop for handoffs and builds
|
|
19621
|
+
- **idea** \u2014 Capture ideas as backlog tasks for future triage
|
|
19622
|
+
- **health** \u2014 Cycle Health Summary dashboard
|
|
19623
|
+
- **release** \u2014 Cut versioned releases with git tags and changelogs
|
|
19624
|
+
|
|
19625
|
+
### Features
|
|
19626
|
+
- .md file persistence in .papi/ directory
|
|
19627
|
+
- Bootstrap + Full planning modes with Anthropic API integration
|
|
19628
|
+
- Embedded BUILD HANDOFFs with dual write-back build reports
|
|
19629
|
+
- Auto-commit and auto-PR after builds
|
|
19630
|
+
- Board corrections and Active Decision persistence
|
|
19631
|
+
- Single-purpose MCP tools for optimal LLM tool selection
|
|
19632
|
+
- Consistent error handling across all tools
|
|
19633
|
+
`;
|
|
19634
|
+
function generateChangelog(version, commits) {
|
|
19635
|
+
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
19636
|
+
const commitList = commits.map((c) => `- ${c}`).join("\n");
|
|
19637
|
+
return `# Changelog
|
|
19638
|
+
|
|
19639
|
+
## ${version} \u2014 ${date}
|
|
19640
|
+
|
|
19641
|
+
${commitList}
|
|
19642
|
+
`;
|
|
19643
|
+
}
|
|
19644
|
+
async function createRelease(config2, branch, version, adapter2) {
|
|
19645
|
+
if (!isGitAvailable()) {
|
|
19646
|
+
throw new Error("git is not available.");
|
|
18922
19647
|
}
|
|
18923
|
-
if (
|
|
18924
|
-
|
|
18925
|
-
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
18926
|
-
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
18927
|
-
} else {
|
|
18928
|
-
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
19648
|
+
if (!isGitRepo(config2.projectRoot)) {
|
|
19649
|
+
throw new Error("not a git repository.");
|
|
18929
19650
|
}
|
|
18930
|
-
|
|
18931
|
-
|
|
18932
|
-
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
18933
|
-
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
18934
|
-
return { score: totalScore, status, reason };
|
|
18935
|
-
}
|
|
18936
|
-
function countByStatus(tasks) {
|
|
18937
|
-
const counts = /* @__PURE__ */ new Map();
|
|
18938
|
-
for (const task of tasks) {
|
|
18939
|
-
counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
19651
|
+
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19652
|
+
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
18940
19653
|
}
|
|
18941
|
-
|
|
18942
|
-
|
|
18943
|
-
|
|
18944
|
-
|
|
18945
|
-
|
|
18946
|
-
|
|
18947
|
-
|
|
18948
|
-
|
|
18949
|
-
|
|
18950
|
-
|
|
18951
|
-
|
|
18952
|
-
|
|
18953
|
-
|
|
18954
|
-
|
|
18955
|
-
|
|
18956
|
-
|
|
18957
|
-
|
|
18958
|
-
|
|
18959
|
-
|
|
18960
|
-
|
|
18961
|
-
|
|
18962
|
-
for (const [status, count] of statusCounts) {
|
|
18963
|
-
parts.push(`${count} ${status}`);
|
|
18964
|
-
}
|
|
18965
|
-
boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
|
|
18966
|
-
if (deferredCount > 0) {
|
|
18967
|
-
boardSummary += ` + ${deferredCount} deferred`;
|
|
19654
|
+
const warnings = [];
|
|
19655
|
+
if (adapter2) {
|
|
19656
|
+
try {
|
|
19657
|
+
const versionMatch = version.match(/^v0\.(\d+)\./);
|
|
19658
|
+
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
19659
|
+
if (currentCycle > 0) {
|
|
19660
|
+
await adapter2.createCycle({
|
|
19661
|
+
id: `cycle-${currentCycle}`,
|
|
19662
|
+
number: currentCycle,
|
|
19663
|
+
status: "complete",
|
|
19664
|
+
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19665
|
+
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19666
|
+
goals: [],
|
|
19667
|
+
boardHealth: "",
|
|
19668
|
+
taskIds: []
|
|
19669
|
+
});
|
|
19670
|
+
}
|
|
19671
|
+
} catch (err) {
|
|
19672
|
+
const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
19673
|
+
console.error(`[release] ${msg}`);
|
|
19674
|
+
warnings.push(msg);
|
|
18968
19675
|
}
|
|
18969
19676
|
}
|
|
18970
|
-
const
|
|
18971
|
-
|
|
18972
|
-
|
|
18973
|
-
const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
|
|
18974
|
-
let carryForward = "None found";
|
|
18975
|
-
if (logEntries.length > 0) {
|
|
18976
|
-
const latest = logEntries[0];
|
|
18977
|
-
if (latest.carryForward) {
|
|
18978
|
-
carryForward = latest.carryForward;
|
|
18979
|
-
} else {
|
|
18980
|
-
carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
|
|
18981
|
-
}
|
|
19677
|
+
const checkout = checkoutBranch(config2.projectRoot, branch);
|
|
19678
|
+
if (!checkout.success) {
|
|
19679
|
+
throw new Error(checkout.message);
|
|
18982
19680
|
}
|
|
18983
|
-
|
|
18984
|
-
|
|
18985
|
-
|
|
18986
|
-
|
|
19681
|
+
if (hasRemote(config2.projectRoot)) {
|
|
19682
|
+
const pull = gitPull(config2.projectRoot);
|
|
19683
|
+
if (!pull.success) {
|
|
19684
|
+
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19685
|
+
}
|
|
18987
19686
|
}
|
|
18988
|
-
if (
|
|
18989
|
-
|
|
19687
|
+
if (tagExists(config2.projectRoot, version)) {
|
|
19688
|
+
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
18990
19689
|
}
|
|
18991
|
-
const
|
|
18992
|
-
|
|
18993
|
-
)
|
|
18994
|
-
|
|
18995
|
-
(t) => t.cycle === cycleNumber && t.status === "In Progress"
|
|
18996
|
-
);
|
|
18997
|
-
const inReviewCycleTasks = activeTasks.filter(
|
|
18998
|
-
(t) => t.cycle === cycleNumber && t.status === "In Review"
|
|
18999
|
-
);
|
|
19000
|
-
if (reasons.length > 0) {
|
|
19001
|
-
recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
|
|
19002
|
-
} else if (unbuiltCycleTasks.length > 0) {
|
|
19003
|
-
recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
|
|
19004
|
-
} else if (inProgressCycleTasks.length > 0) {
|
|
19005
|
-
recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
|
|
19006
|
-
} else if (inReviewCycleTasks.length > 0) {
|
|
19007
|
-
recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
|
|
19690
|
+
const latestTag = getLatestTag(config2.projectRoot);
|
|
19691
|
+
let changelogContent;
|
|
19692
|
+
if (!latestTag) {
|
|
19693
|
+
changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19008
19694
|
} else {
|
|
19009
|
-
|
|
19010
|
-
|
|
19011
|
-
let metricsSection;
|
|
19012
|
-
let derivedMetricsSection = "";
|
|
19013
|
-
let snapshots = [];
|
|
19014
|
-
try {
|
|
19015
|
-
try {
|
|
19016
|
-
const reports = await adapter2.getRecentBuildReports(50);
|
|
19017
|
-
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
19018
|
-
} catch {
|
|
19019
|
-
}
|
|
19020
|
-
metricsSection = formatCycleMetrics(snapshots);
|
|
19021
|
-
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
19022
|
-
} catch (_err) {
|
|
19023
|
-
metricsSection = "Could not read methodology metrics.";
|
|
19024
|
-
}
|
|
19025
|
-
try {
|
|
19026
|
-
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
19027
|
-
if (recentReports.length > 0) {
|
|
19028
|
-
const taskCounts = /* @__PURE__ */ new Map();
|
|
19029
|
-
for (const r of recentReports) {
|
|
19030
|
-
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
19031
|
-
}
|
|
19032
|
-
const iterCounts = [...taskCounts.values()];
|
|
19033
|
-
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
19034
|
-
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
19035
|
-
if (avgIter > 1 || multiIterTasks > 0) {
|
|
19036
|
-
derivedMetricsSection += `
|
|
19037
|
-
|
|
19038
|
-
**Rework**
|
|
19039
|
-
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
19040
|
-
}
|
|
19041
|
-
}
|
|
19042
|
-
} catch {
|
|
19043
|
-
}
|
|
19044
|
-
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
19045
|
-
let decisionUsageSection = "";
|
|
19046
|
-
let decisionUsageEntries = [];
|
|
19047
|
-
try {
|
|
19048
|
-
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
19049
|
-
decisionUsageEntries = usage;
|
|
19050
|
-
if (usage.length > 0) {
|
|
19051
|
-
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
19052
|
-
if (stale.length > 0) {
|
|
19053
|
-
const lines = stale.map(
|
|
19054
|
-
(u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
|
|
19055
|
-
);
|
|
19056
|
-
decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
|
|
19057
|
-
${lines.join("\n")}`;
|
|
19058
|
-
} else {
|
|
19059
|
-
decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
|
|
19060
|
-
}
|
|
19061
|
-
}
|
|
19062
|
-
} catch {
|
|
19063
|
-
}
|
|
19064
|
-
let decisionLifecycleSection = "";
|
|
19065
|
-
try {
|
|
19066
|
-
const decisions = await adapter2.getActiveDecisions();
|
|
19067
|
-
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
19068
|
-
if (lifecycleSummary) {
|
|
19069
|
-
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
19070
|
-
}
|
|
19071
|
-
} catch {
|
|
19072
|
-
}
|
|
19073
|
-
const decisionScoresSection = "";
|
|
19074
|
-
let contextUtilisationSection = "";
|
|
19075
|
-
try {
|
|
19076
|
-
const utilData = await adapter2.getContextUtilisation?.();
|
|
19077
|
-
if (utilData && utilData.length > 0) {
|
|
19078
|
-
const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
|
|
19079
|
-
if (lines.length > 0) {
|
|
19080
|
-
contextUtilisationSection = `**Current cycle:**
|
|
19081
|
-
${lines.join("\n")}`;
|
|
19082
|
-
}
|
|
19083
|
-
}
|
|
19084
|
-
} catch {
|
|
19085
|
-
}
|
|
19086
|
-
let northStarSection = "";
|
|
19087
|
-
try {
|
|
19088
|
-
const staleness = await adapter2.getNorthStarStaleness?.();
|
|
19089
|
-
if (staleness) {
|
|
19090
|
-
const cycleGap = cycleNumber - staleness.setCycle;
|
|
19091
|
-
const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
|
|
19092
|
-
northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
|
|
19093
|
-
} else {
|
|
19094
|
-
const setAtCycle = await adapter2.getNorthStarSetCycle?.();
|
|
19095
|
-
if (setAtCycle != null) {
|
|
19096
|
-
northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
|
|
19097
|
-
} else if (adapter2.getCurrentNorthStar) {
|
|
19098
|
-
const ns = await adapter2.getCurrentNorthStar();
|
|
19099
|
-
northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
|
|
19100
|
-
}
|
|
19101
|
-
}
|
|
19102
|
-
} catch {
|
|
19103
|
-
}
|
|
19104
|
-
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
19105
|
-
return {
|
|
19106
|
-
cycleNumber,
|
|
19107
|
-
latestCycleStatus: health.latestCycleStatus,
|
|
19108
|
-
connectionStatus: getConnectionStatus(),
|
|
19109
|
-
reviewWarning,
|
|
19110
|
-
boardSummary,
|
|
19111
|
-
staleTasks,
|
|
19112
|
-
inReviewSummary,
|
|
19113
|
-
carryForward,
|
|
19114
|
-
recommendedMode,
|
|
19115
|
-
metricsSection,
|
|
19116
|
-
derivedMetricsSection,
|
|
19117
|
-
costSection,
|
|
19118
|
-
decisionUsageSection,
|
|
19119
|
-
decisionLifecycleSection,
|
|
19120
|
-
decisionScoresSection,
|
|
19121
|
-
contextUtilisationSection,
|
|
19122
|
-
northStarSection,
|
|
19123
|
-
healthScore: healthResult?.score ?? null,
|
|
19124
|
-
healthStatus: healthResult?.status ?? null,
|
|
19125
|
-
healthReason: healthResult?.reason ?? null
|
|
19126
|
-
};
|
|
19127
|
-
}
|
|
19128
|
-
|
|
19129
|
-
// src/tools/health.ts
|
|
19130
|
-
var healthTool = {
|
|
19131
|
-
name: "health",
|
|
19132
|
-
description: "Cycle Health Summary \u2014 shows current cycle number, Strategy Review cadence status (AD-5), board health (task counts by status, stale tasks), last carry-forward items, and recommended next mode. Read-only, does not modify any files.",
|
|
19133
|
-
inputSchema: {
|
|
19134
|
-
type: "object",
|
|
19135
|
-
properties: {},
|
|
19136
|
-
required: []
|
|
19137
|
-
}
|
|
19138
|
-
};
|
|
19139
|
-
function formatHealthSummary(summary) {
|
|
19140
|
-
const lines = [];
|
|
19141
|
-
lines.push(`# Cycle ${summary.cycleNumber} \u2014 Health`);
|
|
19142
|
-
lines.push("");
|
|
19143
|
-
if (summary.connectionStatus !== "offline") {
|
|
19144
|
-
const statusIcon = summary.connectionStatus === "connected" ? "\u2713" : "\u26A0\uFE0F";
|
|
19145
|
-
const statusLabel = summary.connectionStatus === "connected" ? "Supabase connected" : "Supabase degraded \u2014 data may be stale. Check DATABASE_URL in .mcp.json";
|
|
19146
|
-
lines.push(`**Connection:** ${statusIcon} ${statusLabel}`);
|
|
19147
|
-
lines.push("");
|
|
19148
|
-
}
|
|
19149
|
-
lines.push(`> **Next action:** ${summary.recommendedMode}`);
|
|
19150
|
-
lines.push("");
|
|
19151
|
-
lines.push(`**Strategy Review:** ${summary.reviewWarning}`);
|
|
19152
|
-
lines.push("");
|
|
19153
|
-
lines.push(`## Board`);
|
|
19154
|
-
lines.push(summary.boardSummary);
|
|
19155
|
-
const hasInProgress = !summary.staleTasks.startsWith("No tasks");
|
|
19156
|
-
const hasInReview = !summary.inReviewSummary.startsWith("No tasks");
|
|
19157
|
-
if (hasInProgress || hasInReview) {
|
|
19158
|
-
lines.push("");
|
|
19159
|
-
if (hasInProgress) lines.push(`- **In Progress:** ${summary.staleTasks}`);
|
|
19160
|
-
if (hasInReview) lines.push(`- **In Review:** ${summary.inReviewSummary}`);
|
|
19161
|
-
}
|
|
19162
|
-
lines.push("");
|
|
19163
|
-
const hasCarryForward = summary.carryForward !== "None found" && !summary.carryForward.startsWith("No carry-forward");
|
|
19164
|
-
if (hasCarryForward) {
|
|
19165
|
-
lines.push(`## Carry-Forward`);
|
|
19166
|
-
lines.push(summary.carryForward);
|
|
19167
|
-
lines.push("");
|
|
19168
|
-
}
|
|
19169
|
-
const hasMetrics = summary.metricsSection !== "Could not read methodology metrics." && !summary.metricsSection.includes("undefined");
|
|
19170
|
-
if (hasMetrics) {
|
|
19171
|
-
lines.push(`## Trends`);
|
|
19172
|
-
lines.push(summary.metricsSection);
|
|
19173
|
-
lines.push("");
|
|
19174
|
-
}
|
|
19175
|
-
if (summary.derivedMetricsSection) {
|
|
19176
|
-
lines.push(`## Insights`);
|
|
19177
|
-
lines.push(summary.derivedMetricsSection);
|
|
19178
|
-
lines.push("");
|
|
19179
|
-
}
|
|
19180
|
-
const hasCost = summary.costSection !== "No metrics data yet." && summary.costSection !== "Could not read metrics data.";
|
|
19181
|
-
if (hasCost) {
|
|
19182
|
-
lines.push(`## Cost`);
|
|
19183
|
-
lines.push(summary.costSection);
|
|
19184
|
-
lines.push("");
|
|
19185
|
-
}
|
|
19186
|
-
if (summary.northStarSection) {
|
|
19187
|
-
lines.push(`## North Star`);
|
|
19188
|
-
lines.push(summary.northStarSection);
|
|
19189
|
-
lines.push("");
|
|
19190
|
-
}
|
|
19191
|
-
if (summary.decisionUsageSection) {
|
|
19192
|
-
lines.push(`## Decision Usage`);
|
|
19193
|
-
lines.push(summary.decisionUsageSection);
|
|
19194
|
-
if (summary.decisionLifecycleSection) {
|
|
19195
|
-
lines.push(summary.decisionLifecycleSection);
|
|
19196
|
-
}
|
|
19197
|
-
lines.push("");
|
|
19198
|
-
}
|
|
19199
|
-
if (summary.contextUtilisationSection) {
|
|
19200
|
-
lines.push(`## Context Utilisation`);
|
|
19201
|
-
lines.push(summary.contextUtilisationSection);
|
|
19202
|
-
lines.push("");
|
|
19203
|
-
}
|
|
19204
|
-
if (summary.decisionScoresSection) {
|
|
19205
|
-
lines.push(`## Decision Scores`);
|
|
19206
|
-
lines.push(summary.decisionScoresSection);
|
|
19207
|
-
lines.push("");
|
|
19208
|
-
}
|
|
19209
|
-
return lines.join("\n").trimEnd();
|
|
19210
|
-
}
|
|
19211
|
-
async function handleHealth(adapter2) {
|
|
19212
|
-
try {
|
|
19213
|
-
const summary = await getHealthSummary(adapter2);
|
|
19214
|
-
return textResponse(formatHealthSummary(summary));
|
|
19215
|
-
} catch (err) {
|
|
19216
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
19217
|
-
return errorResponse(`Could not read cycle health. Run \`setup\` first to initialise your project. (${message})`);
|
|
19218
|
-
}
|
|
19219
|
-
}
|
|
19220
|
-
|
|
19221
|
-
// src/services/release.ts
|
|
19222
|
-
init_git();
|
|
19223
|
-
import { writeFile as writeFile3 } from "fs/promises";
|
|
19224
|
-
import { join as join6 } from "path";
|
|
19225
|
-
var INITIAL_RELEASE_NOTES = `# Changelog
|
|
19226
|
-
|
|
19227
|
-
## v0.1.0-alpha \u2014 Initial Release
|
|
19228
|
-
|
|
19229
|
-
PAPI MCP Server \u2014 the AI-powered project planning framework.
|
|
19230
|
-
|
|
19231
|
-
### Commands
|
|
19232
|
-
- **setup** \u2014 Initialise a new PAPI project with Product Brief generation
|
|
19233
|
-
- **plan** \u2014 Run cycle planning with embedded BUILD HANDOFFs (Bootstrap + Full modes)
|
|
19234
|
-
- **build_list / build_describe / build_execute / build_cancel** \u2014 Manage build tasks
|
|
19235
|
-
- **board_view / board_deprioritise / board_archive** \u2014 View and manage the Board
|
|
19236
|
-
- **strategy_review / strategy_change** \u2014 Run Strategy Reviews and apply strategic changes
|
|
19237
|
-
- **review_list / review_submit** \u2014 Human review loop for handoffs and builds
|
|
19238
|
-
- **idea** \u2014 Capture ideas as backlog tasks for future triage
|
|
19239
|
-
- **health** \u2014 Cycle Health Summary dashboard
|
|
19240
|
-
- **release** \u2014 Cut versioned releases with git tags and changelogs
|
|
19241
|
-
|
|
19242
|
-
### Features
|
|
19243
|
-
- .md file persistence in .papi/ directory
|
|
19244
|
-
- Bootstrap + Full planning modes with Anthropic API integration
|
|
19245
|
-
- Embedded BUILD HANDOFFs with dual write-back build reports
|
|
19246
|
-
- Auto-commit and auto-PR after builds
|
|
19247
|
-
- Board corrections and Active Decision persistence
|
|
19248
|
-
- Single-purpose MCP tools for optimal LLM tool selection
|
|
19249
|
-
- Consistent error handling across all tools
|
|
19250
|
-
`;
|
|
19251
|
-
function generateChangelog(version, commits) {
|
|
19252
|
-
const date = (/* @__PURE__ */ new Date()).toISOString();
|
|
19253
|
-
const commitList = commits.map((c) => `- ${c}`).join("\n");
|
|
19254
|
-
return `# Changelog
|
|
19255
|
-
|
|
19256
|
-
## ${version} \u2014 ${date}
|
|
19257
|
-
|
|
19258
|
-
${commitList}
|
|
19259
|
-
`;
|
|
19260
|
-
}
|
|
19261
|
-
async function createRelease(config2, branch, version, adapter2) {
|
|
19262
|
-
if (!isGitAvailable()) {
|
|
19263
|
-
throw new Error("git is not available.");
|
|
19264
|
-
}
|
|
19265
|
-
if (!isGitRepo(config2.projectRoot)) {
|
|
19266
|
-
throw new Error("not a git repository.");
|
|
19267
|
-
}
|
|
19268
|
-
if (hasUncommittedChanges(config2.projectRoot, AUTO_WRITTEN_PATHS)) {
|
|
19269
|
-
throw new Error("working directory has uncommitted changes. Commit or stash them before releasing.");
|
|
19270
|
-
}
|
|
19271
|
-
const warnings = [];
|
|
19272
|
-
if (adapter2) {
|
|
19273
|
-
try {
|
|
19274
|
-
const versionMatch = version.match(/^v0\.(\d+)\./);
|
|
19275
|
-
const currentCycle = versionMatch ? parseInt(versionMatch[1], 10) : 0;
|
|
19276
|
-
if (currentCycle > 0) {
|
|
19277
|
-
await adapter2.createCycle({
|
|
19278
|
-
id: `cycle-${currentCycle}`,
|
|
19279
|
-
number: currentCycle,
|
|
19280
|
-
status: "complete",
|
|
19281
|
-
startDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19282
|
-
endDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19283
|
-
goals: [],
|
|
19284
|
-
boardHealth: "",
|
|
19285
|
-
taskIds: []
|
|
19286
|
-
});
|
|
19287
|
-
}
|
|
19288
|
-
} catch (err) {
|
|
19289
|
-
const msg = `createCycle (mark complete) failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
19290
|
-
console.error(`[release] ${msg}`);
|
|
19291
|
-
warnings.push(msg);
|
|
19292
|
-
}
|
|
19293
|
-
}
|
|
19294
|
-
const checkout = checkoutBranch(config2.projectRoot, branch);
|
|
19295
|
-
if (!checkout.success) {
|
|
19296
|
-
throw new Error(checkout.message);
|
|
19297
|
-
}
|
|
19298
|
-
if (hasRemote(config2.projectRoot)) {
|
|
19299
|
-
const pull = gitPull(config2.projectRoot);
|
|
19300
|
-
if (!pull.success) {
|
|
19301
|
-
warnings.push(`git pull failed: ${pull.message}. Run manually.`);
|
|
19302
|
-
}
|
|
19303
|
-
}
|
|
19304
|
-
if (tagExists(config2.projectRoot, version)) {
|
|
19305
|
-
throw new Error(`tag "${version}" already exists. Use a different version.`);
|
|
19306
|
-
}
|
|
19307
|
-
const latestTag = getLatestTag(config2.projectRoot);
|
|
19308
|
-
let changelogContent;
|
|
19309
|
-
if (!latestTag) {
|
|
19310
|
-
changelogContent = INITIAL_RELEASE_NOTES.replace("v0.1.0-alpha", version);
|
|
19311
|
-
} else {
|
|
19312
|
-
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
19313
|
-
changelogContent = generateChangelog(version, commits);
|
|
19695
|
+
const commits = getCommitsSinceTag(config2.projectRoot, latestTag);
|
|
19696
|
+
changelogContent = generateChangelog(version, commits);
|
|
19314
19697
|
}
|
|
19315
19698
|
const changelogPath = join6(config2.projectRoot, "CHANGELOG.md");
|
|
19316
19699
|
await writeFile3(changelogPath, changelogContent, "utf-8");
|
|
@@ -19345,6 +19728,7 @@ async function createRelease(config2, branch, version, adapter2) {
|
|
|
19345
19728
|
var releaseTool = {
|
|
19346
19729
|
name: "release",
|
|
19347
19730
|
description: "Cut a versioned release \u2014 creates a git tag, generates CHANGELOG.md, and pushes to remote.",
|
|
19731
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
19348
19732
|
inputSchema: {
|
|
19349
19733
|
type: "object",
|
|
19350
19734
|
properties: {
|
|
@@ -19355,14 +19739,36 @@ var releaseTool = {
|
|
|
19355
19739
|
version: {
|
|
19356
19740
|
type: "string",
|
|
19357
19741
|
description: 'The version tag to create (e.g. "v0.1.0-alpha"). Must start with "v".'
|
|
19358
|
-
}
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
|
|
19742
|
+
},
|
|
19743
|
+
observations: {
|
|
19744
|
+
type: "array",
|
|
19745
|
+
description: "Optional dogfood observations from this cycle to persist to the DB. Each entry records friction, methodology signals, or commercial insights.",
|
|
19746
|
+
items: {
|
|
19747
|
+
type: "object",
|
|
19748
|
+
properties: {
|
|
19749
|
+
content: { type: "string", description: "The observation text." },
|
|
19750
|
+
category: {
|
|
19751
|
+
type: "string",
|
|
19752
|
+
enum: ["friction", "methodology", "signal", "commercial"],
|
|
19753
|
+
description: "Observation category."
|
|
19754
|
+
},
|
|
19755
|
+
severity: {
|
|
19756
|
+
type: "string",
|
|
19757
|
+
enum: ["P0", "P1", "P2", "P3"],
|
|
19758
|
+
description: "Optional severity for friction/signal observations."
|
|
19759
|
+
}
|
|
19760
|
+
},
|
|
19761
|
+
required: ["content", "category"]
|
|
19762
|
+
}
|
|
19763
|
+
}
|
|
19764
|
+
},
|
|
19765
|
+
required: ["branch", "version"]
|
|
19766
|
+
}
|
|
19767
|
+
};
|
|
19768
|
+
async function handleRelease(adapter2, config2, args) {
|
|
19364
19769
|
const branch = args.branch;
|
|
19365
19770
|
const version = args.version;
|
|
19771
|
+
const rawObservations = args.observations;
|
|
19366
19772
|
if (!branch || !version) {
|
|
19367
19773
|
return errorResponse('both branch and version are required. Example: release branch="main" version="v0.1.0-alpha"');
|
|
19368
19774
|
}
|
|
@@ -19400,6 +19806,23 @@ async function handleRelease(adapter2, config2, args) {
|
|
|
19400
19806
|
}
|
|
19401
19807
|
} catch {
|
|
19402
19808
|
}
|
|
19809
|
+
if (rawObservations && rawObservations.length > 0 && adapter2.writeDogfoodEntries) {
|
|
19810
|
+
try {
|
|
19811
|
+
const cycleMatch = version.match(/^v0\.(\d+)\./);
|
|
19812
|
+
const cycleNum = cycleMatch ? parseInt(cycleMatch[1], 10) : 0;
|
|
19813
|
+
const entries = rawObservations.map((obs) => ({
|
|
19814
|
+
cycleNumber: cycleNum,
|
|
19815
|
+
category: obs.category,
|
|
19816
|
+
content: obs.content,
|
|
19817
|
+
sourceTool: "release",
|
|
19818
|
+
status: "observed"
|
|
19819
|
+
}));
|
|
19820
|
+
await adapter2.writeDogfoodEntries(entries);
|
|
19821
|
+
lines.push("", `Dogfood: ${entries.length} observation(s) saved to DB.`);
|
|
19822
|
+
} catch {
|
|
19823
|
+
lines.push("", "\u26A0\uFE0F Dogfood observations could not be saved to DB \u2014 log them manually in DOGFOOD_LOG.md.");
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19403
19826
|
lines.push("", `Next: cycle released! Run \`plan\` to start your next planning cycle.`);
|
|
19404
19827
|
return textResponse(lines.join("\n"));
|
|
19405
19828
|
} catch (err) {
|
|
@@ -19525,7 +19948,6 @@ async function submitReview(adapter2, input) {
|
|
|
19525
19948
|
handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
|
|
19526
19949
|
}
|
|
19527
19950
|
const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
|
|
19528
|
-
const slackWarning = void 0;
|
|
19529
19951
|
let phaseChanges = [];
|
|
19530
19952
|
if (newStatus) {
|
|
19531
19953
|
try {
|
|
@@ -19541,7 +19963,6 @@ async function submitReview(adapter2, input) {
|
|
|
19541
19963
|
newStatus,
|
|
19542
19964
|
unblockedTasks,
|
|
19543
19965
|
handoffRegenerated,
|
|
19544
|
-
slackWarning,
|
|
19545
19966
|
handoffRegenPrompt,
|
|
19546
19967
|
currentCycle: cycle,
|
|
19547
19968
|
phaseChanges
|
|
@@ -19552,6 +19973,7 @@ async function submitReview(adapter2, input) {
|
|
|
19552
19973
|
var reviewListTool = {
|
|
19553
19974
|
name: "review_list",
|
|
19554
19975
|
description: "List tasks ready for your sign-off \u2014 shows completed builds waiting for approval or feedback. Does not call the Anthropic API.",
|
|
19976
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
19555
19977
|
inputSchema: {
|
|
19556
19978
|
type: "object",
|
|
19557
19979
|
properties: {},
|
|
@@ -19561,6 +19983,7 @@ var reviewListTool = {
|
|
|
19561
19983
|
var reviewSubmitTool = {
|
|
19562
19984
|
name: "review_submit",
|
|
19563
19985
|
description: "Record a review verdict on a completed build (build-acceptance) or task plan (handoff-review). ALWAYS ask the human for their verdict before calling \u2014 never auto-submit without human input. Accept moves the task to Done, request-changes sends it back for rework, reject discards the build. Updates task status based on the verdict. On handoff-review with suggested changes, returns a prompt to revise the BUILD HANDOFF.",
|
|
19986
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19564
19987
|
inputSchema: {
|
|
19565
19988
|
type: "object",
|
|
19566
19989
|
properties: {
|
|
@@ -19590,10 +20013,6 @@ var reviewSubmitTool = {
|
|
|
19590
20013
|
type: "string",
|
|
19591
20014
|
description: "Your locally-generated BUILD HANDOFF regen output. Pass this to save a handoff that was regenerated in local mode (no API key)."
|
|
19592
20015
|
},
|
|
19593
|
-
notify: {
|
|
19594
|
-
type: "boolean",
|
|
19595
|
-
description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
|
|
19596
|
-
},
|
|
19597
20016
|
auto_review: {
|
|
19598
20017
|
type: "object",
|
|
19599
20018
|
description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
|
|
@@ -19710,7 +20129,6 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19710
20129
|
const verdict = args.verdict;
|
|
19711
20130
|
const comments = args.comments;
|
|
19712
20131
|
const reviewer = args.reviewer ?? "human";
|
|
19713
|
-
const notify = args.notify !== false;
|
|
19714
20132
|
const rawAutoReview = args.auto_review;
|
|
19715
20133
|
let autoReview;
|
|
19716
20134
|
if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
|
|
@@ -19753,7 +20171,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
|
|
|
19753
20171
|
try {
|
|
19754
20172
|
const result = await submitReview(
|
|
19755
20173
|
adapter2,
|
|
19756
|
-
{ taskId, stage, verdict, comments, reviewer,
|
|
20174
|
+
{ taskId, stage, verdict, comments, reviewer, autoReview }
|
|
19757
20175
|
);
|
|
19758
20176
|
const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
|
|
19759
20177
|
const unblockNote = result.unblockedTasks.length > 0 ? `
|
|
@@ -19786,9 +20204,6 @@ ${result.handoffRegenPrompt.userMessage}
|
|
|
19786
20204
|
mergeNote = "\n\n" + mergeLines.map((l) => `> ${l}`).join("\n");
|
|
19787
20205
|
}
|
|
19788
20206
|
}
|
|
19789
|
-
const slackNote = result.slackWarning ? `
|
|
19790
|
-
|
|
19791
|
-
${result.slackWarning}` : "";
|
|
19792
20207
|
let autoReleaseNote = "";
|
|
19793
20208
|
if (stage === "build-acceptance" && verdict === "accept" && result.newStatus === "Done" && result.currentCycle > 0) {
|
|
19794
20209
|
try {
|
|
@@ -19839,7 +20254,7 @@ Next: address the feedback, then run \`build_execute ${taskId}\` to resubmit.`;
|
|
|
19839
20254
|
- **Verdict:** ${result.verdict}
|
|
19840
20255
|
- **Comments:** ${result.comments}
|
|
19841
20256
|
|
|
19842
|
-
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${
|
|
20257
|
+
${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${autoReleaseNote}${nextStepNote}${phaseNote}`
|
|
19843
20258
|
);
|
|
19844
20259
|
} catch (err) {
|
|
19845
20260
|
return errorResponse(err instanceof Error ? err.message : String(err));
|
|
@@ -19853,6 +20268,7 @@ import path4 from "path";
|
|
|
19853
20268
|
var initTool = {
|
|
19854
20269
|
name: "init",
|
|
19855
20270
|
description: "Initialise PAPI in the current project. Generates a .mcp.json config file with pg adapter settings pointed at the hosted Supabase instance. Run once per project to get started.",
|
|
20271
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
19856
20272
|
inputSchema: {
|
|
19857
20273
|
type: "object",
|
|
19858
20274
|
properties: {
|
|
@@ -19978,32 +20394,491 @@ Your existing API key and project ID have been saved to .mcp.json.
|
|
|
19978
20394
|
].join("\n");
|
|
19979
20395
|
return textResponse(output2);
|
|
19980
20396
|
}
|
|
19981
|
-
const output = [
|
|
19982
|
-
`# PAPI \u2014 Account Required`,
|
|
19983
|
-
"",
|
|
19984
|
-
`PAPI needs an account to store your project data.`,
|
|
19985
|
-
"",
|
|
19986
|
-
"## Get Started in 3 Steps",
|
|
19987
|
-
"",
|
|
19988
|
-
"1. **Sign up** at https://getpapi.ai/login",
|
|
19989
|
-
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
19990
|
-
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
19991
|
-
"",
|
|
19992
|
-
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
19993
|
-
"",
|
|
19994
|
-
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
19995
|
-
].join("\n");
|
|
19996
|
-
return textResponse(output);
|
|
20397
|
+
const output = [
|
|
20398
|
+
`# PAPI \u2014 Account Required`,
|
|
20399
|
+
"",
|
|
20400
|
+
`PAPI needs an account to store your project data.`,
|
|
20401
|
+
"",
|
|
20402
|
+
"## Get Started in 3 Steps",
|
|
20403
|
+
"",
|
|
20404
|
+
"1. **Sign up** at https://getpapi.ai/login",
|
|
20405
|
+
"2. **Complete the onboarding wizard** \u2014 it generates your `.mcp.json` config with your API key and project ID",
|
|
20406
|
+
"3. **Download the config**, place it in your project root, and restart your MCP client",
|
|
20407
|
+
"",
|
|
20408
|
+
"The onboarding wizard generates everything you need \u2014 no manual configuration required.",
|
|
20409
|
+
"",
|
|
20410
|
+
`> Already have an account? Make sure both \`PAPI_PROJECT_ID\` and \`PAPI_DATA_API_KEY\` are set in your .mcp.json.`
|
|
20411
|
+
].join("\n");
|
|
20412
|
+
return textResponse(output);
|
|
20413
|
+
}
|
|
20414
|
+
|
|
20415
|
+
// src/services/health.ts
|
|
20416
|
+
function computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsage) {
|
|
20417
|
+
if (cycleNumber < 3) return null;
|
|
20418
|
+
const scores = [];
|
|
20419
|
+
const recentSnaps = snapshots.slice(-3);
|
|
20420
|
+
const baselineSnaps = snapshots.slice(-10);
|
|
20421
|
+
if (recentSnaps.length > 0 && baselineSnaps.length > 0) {
|
|
20422
|
+
const avg = (snaps) => snaps.reduce((s, sn) => s + (sn.velocity[0]?.effortPoints ?? 0), 0) / snaps.length;
|
|
20423
|
+
const recentAvg = avg(recentSnaps);
|
|
20424
|
+
const baselineAvg = avg(baselineSnaps);
|
|
20425
|
+
const velocityScore = baselineAvg > 0 ? Math.min(100, Math.round(recentAvg / baselineAvg * 100)) : 50;
|
|
20426
|
+
scores.push({ name: "Velocity", score: velocityScore, weight: 0.25 });
|
|
20427
|
+
} else {
|
|
20428
|
+
scores.push({ name: "Velocity", score: 50, weight: 0.25 });
|
|
20429
|
+
}
|
|
20430
|
+
if (recentSnaps.length > 0) {
|
|
20431
|
+
const avgMatchRate = recentSnaps.reduce((s, sn) => s + (sn.accuracy[0]?.matchRate ?? 0), 0) / recentSnaps.length;
|
|
20432
|
+
scores.push({ name: "Estimation accuracy", score: Math.round(avgMatchRate), weight: 0.25 });
|
|
20433
|
+
} else {
|
|
20434
|
+
scores.push({ name: "Estimation accuracy", score: 50, weight: 0.25 });
|
|
20435
|
+
}
|
|
20436
|
+
const inReviewCount = activeTasks.filter((t) => t.status === "In Review").length;
|
|
20437
|
+
const reviewScore = inReviewCount === 0 ? 100 : inReviewCount <= 2 ? 60 : 20;
|
|
20438
|
+
scores.push({ name: "Review throughput", score: reviewScore, weight: 0.2 });
|
|
20439
|
+
const backlogTasks = activeTasks.filter((t) => t.status === "Backlog");
|
|
20440
|
+
if (backlogTasks.length > 0) {
|
|
20441
|
+
const criticalCount = backlogTasks.filter(
|
|
20442
|
+
(t) => t.priority === "P0 Critical" || t.priority === "P1 High"
|
|
20443
|
+
).length;
|
|
20444
|
+
const criticalRatio = criticalCount / backlogTasks.length;
|
|
20445
|
+
const backlogScore = criticalRatio > 0.5 ? 40 : criticalRatio > 0.3 ? 70 : 90;
|
|
20446
|
+
scores.push({ name: "Backlog health", score: backlogScore, weight: 0.15 });
|
|
20447
|
+
} else {
|
|
20448
|
+
scores.push({ name: "Backlog health", score: 80, weight: 0.15 });
|
|
20449
|
+
}
|
|
20450
|
+
if (decisionUsage.length > 0) {
|
|
20451
|
+
const staleCount = decisionUsage.filter((u) => u.cyclesSinceLastReference >= 10).length;
|
|
20452
|
+
const freshRatio = (decisionUsage.length - staleCount) / decisionUsage.length;
|
|
20453
|
+
scores.push({ name: "AD freshness", score: Math.round(freshRatio * 100), weight: 0.15 });
|
|
20454
|
+
} else {
|
|
20455
|
+
scores.push({ name: "AD freshness", score: 70, weight: 0.15 });
|
|
20456
|
+
}
|
|
20457
|
+
const totalScore = Math.round(scores.reduce((sum, s) => sum + s.score * s.weight, 0));
|
|
20458
|
+
const status = totalScore >= 70 ? "GREEN" : totalScore >= 50 ? "AMBER" : "RED";
|
|
20459
|
+
const worst = scores.reduce((min, s) => s.score < min.score ? s : min, scores[0]);
|
|
20460
|
+
const reason = status === "GREEN" ? "All components healthy" : `${worst.name} below target (${worst.score}/100)`;
|
|
20461
|
+
return { score: totalScore, status, reason };
|
|
20462
|
+
}
|
|
20463
|
+
function countByStatus(tasks) {
|
|
20464
|
+
const counts = /* @__PURE__ */ new Map();
|
|
20465
|
+
for (const task of tasks) {
|
|
20466
|
+
counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
20467
|
+
}
|
|
20468
|
+
return counts;
|
|
20469
|
+
}
|
|
20470
|
+
async function getHealthSummary(adapter2) {
|
|
20471
|
+
const health = await adapter2.getCycleHealth();
|
|
20472
|
+
const activeTasks = await adapter2.queryBoard({
|
|
20473
|
+
status: ["Backlog", "In Cycle", "Ready", "In Progress", "In Review", "Blocked"]
|
|
20474
|
+
});
|
|
20475
|
+
const logEntries = await adapter2.getCycleLog(3);
|
|
20476
|
+
const cycleNumber = health.totalCycles;
|
|
20477
|
+
const cyclesSinceReview = health.cyclesSinceLastStrategyReview;
|
|
20478
|
+
const reviewDue = health.strategyReviewDue;
|
|
20479
|
+
const reviewGateBlocking = cyclesSinceReview >= 5;
|
|
20480
|
+
const reviewWarning = reviewGateBlocking ? `\u26A0\uFE0F GATE \u2014 ${cyclesSinceReview} cycles since last Strategy Review. \`plan\` is blocked until \`strategy_review\` runs (or \`force: true\`).` : `\u2713 On track \u2014 ${cyclesSinceReview} cycle(s) since last review. Next due: ${reviewDue}`;
|
|
20481
|
+
const deferredCount = activeTasks.filter((t) => t.status === "Deferred").length;
|
|
20482
|
+
const nonDeferredTasks = activeTasks.filter((t) => t.status !== "Deferred");
|
|
20483
|
+
const statusCounts = countByStatus(nonDeferredTasks);
|
|
20484
|
+
let boardSummary;
|
|
20485
|
+
if (nonDeferredTasks.length === 0 && deferredCount === 0) {
|
|
20486
|
+
boardSummary = "0 tasks \u2014 board may need reloading";
|
|
20487
|
+
} else {
|
|
20488
|
+
const parts = [];
|
|
20489
|
+
for (const [status, count] of statusCounts) {
|
|
20490
|
+
parts.push(`${count} ${status}`);
|
|
20491
|
+
}
|
|
20492
|
+
boardSummary = `${nonDeferredTasks.length} active tasks \u2014 ${parts.join(", ")}`;
|
|
20493
|
+
if (deferredCount > 0) {
|
|
20494
|
+
boardSummary += ` + ${deferredCount} deferred`;
|
|
20495
|
+
}
|
|
20496
|
+
}
|
|
20497
|
+
const inProgressTasks = activeTasks.filter((t) => t.status === "In Progress");
|
|
20498
|
+
const staleTasks = inProgressTasks.length > 0 ? `${inProgressTasks.length} task(s) In Progress: ${inProgressTasks.map((t) => t.id).join(", ")}` : "No tasks currently In Progress";
|
|
20499
|
+
const inReviewTasks = activeTasks.filter((t) => t.status === "In Review");
|
|
20500
|
+
const inReviewSummary = inReviewTasks.length > 0 ? `${inReviewTasks.length} task(s) ready for sign-off: ${inReviewTasks.map((t) => t.id).join(", ")}` : "No tasks waiting for sign-off";
|
|
20501
|
+
let carryForward = "None found";
|
|
20502
|
+
if (logEntries.length > 0) {
|
|
20503
|
+
const latest = logEntries[0];
|
|
20504
|
+
if (latest.carryForward) {
|
|
20505
|
+
carryForward = latest.carryForward;
|
|
20506
|
+
} else {
|
|
20507
|
+
carryForward = `No carry-forward in Cycle ${latest.cycleNumber}`;
|
|
20508
|
+
}
|
|
20509
|
+
}
|
|
20510
|
+
let recommendedMode;
|
|
20511
|
+
const reasons = [];
|
|
20512
|
+
if (reviewGateBlocking) {
|
|
20513
|
+
reasons.push(`Strategy Review overdue (${cyclesSinceReview} cycles)`);
|
|
20514
|
+
}
|
|
20515
|
+
if (activeTasks.length === 0) {
|
|
20516
|
+
reasons.push("Board is empty \u2014 needs task reload/triage");
|
|
20517
|
+
}
|
|
20518
|
+
const unbuiltCycleTasks = activeTasks.filter(
|
|
20519
|
+
(t) => t.cycle === cycleNumber && (t.status === "In Cycle" || t.status === "Ready")
|
|
20520
|
+
);
|
|
20521
|
+
const inProgressCycleTasks = activeTasks.filter(
|
|
20522
|
+
(t) => t.cycle === cycleNumber && t.status === "In Progress"
|
|
20523
|
+
);
|
|
20524
|
+
const inReviewCycleTasks = activeTasks.filter(
|
|
20525
|
+
(t) => t.cycle === cycleNumber && t.status === "In Review"
|
|
20526
|
+
);
|
|
20527
|
+
if (reasons.length > 0) {
|
|
20528
|
+
recommendedMode = `**Full** \u2014 ${reasons.join("; ")}`;
|
|
20529
|
+
} else if (unbuiltCycleTasks.length > 0) {
|
|
20530
|
+
recommendedMode = `**Build** \u2014 ${unbuiltCycleTasks.length} cycle task(s) not yet started`;
|
|
20531
|
+
} else if (inProgressCycleTasks.length > 0) {
|
|
20532
|
+
recommendedMode = `**Build** \u2014 ${inProgressCycleTasks.length} task(s) in progress`;
|
|
20533
|
+
} else if (inReviewCycleTasks.length > 0) {
|
|
20534
|
+
recommendedMode = `**Review** \u2014 ${inReviewCycleTasks.length} task(s) awaiting review`;
|
|
20535
|
+
} else {
|
|
20536
|
+
recommendedMode = `**Full** \u2014 ready for next cycle`;
|
|
20537
|
+
}
|
|
20538
|
+
let metricsSection;
|
|
20539
|
+
let derivedMetricsSection = "";
|
|
20540
|
+
let snapshots = [];
|
|
20541
|
+
try {
|
|
20542
|
+
try {
|
|
20543
|
+
const reports = await adapter2.getRecentBuildReports(50);
|
|
20544
|
+
snapshots = computeSnapshotsFromBuildReports(reports);
|
|
20545
|
+
} catch {
|
|
20546
|
+
}
|
|
20547
|
+
metricsSection = formatCycleMetrics(snapshots);
|
|
20548
|
+
derivedMetricsSection = formatDerivedMetrics(snapshots, activeTasks);
|
|
20549
|
+
} catch (_err) {
|
|
20550
|
+
metricsSection = "Could not read methodology metrics.";
|
|
20551
|
+
}
|
|
20552
|
+
try {
|
|
20553
|
+
const recentReports = await adapter2.getRecentBuildReports(50);
|
|
20554
|
+
if (recentReports.length > 0) {
|
|
20555
|
+
const taskCounts = /* @__PURE__ */ new Map();
|
|
20556
|
+
for (const r of recentReports) {
|
|
20557
|
+
taskCounts.set(r.taskId, (taskCounts.get(r.taskId) ?? 0) + 1);
|
|
20558
|
+
}
|
|
20559
|
+
const iterCounts = [...taskCounts.values()];
|
|
20560
|
+
const avgIter = iterCounts.reduce((s, c) => s + c, 0) / iterCounts.length;
|
|
20561
|
+
const multiIterTasks = iterCounts.filter((c) => c > 1).length;
|
|
20562
|
+
if (avgIter > 1 || multiIterTasks > 0) {
|
|
20563
|
+
derivedMetricsSection += `
|
|
20564
|
+
|
|
20565
|
+
**Rework**
|
|
20566
|
+
- Average iterations: ${avgIter.toFixed(1)} (${multiIterTasks} task${multiIterTasks !== 1 ? "s" : ""} with pushbacks)`;
|
|
20567
|
+
}
|
|
20568
|
+
}
|
|
20569
|
+
} catch {
|
|
20570
|
+
}
|
|
20571
|
+
const costSection = "Disabled \u2014 local MCP, no API costs.";
|
|
20572
|
+
let decisionUsageSection = "";
|
|
20573
|
+
let decisionUsageEntries = [];
|
|
20574
|
+
try {
|
|
20575
|
+
const usage = await adapter2.getDecisionUsage(cycleNumber);
|
|
20576
|
+
decisionUsageEntries = usage;
|
|
20577
|
+
if (usage.length > 0) {
|
|
20578
|
+
const stale = usage.filter((u) => u.cyclesSinceLastReference >= 5);
|
|
20579
|
+
if (stale.length > 0) {
|
|
20580
|
+
const lines = stale.map(
|
|
20581
|
+
(u) => `- ${u.decisionId}: last referenced Cycle ${u.lastReferencedCycle} (${u.cyclesSinceLastReference} cycles ago)`
|
|
20582
|
+
);
|
|
20583
|
+
decisionUsageSection = `**Stale ADs (5+ cycles unreferenced):**
|
|
20584
|
+
${lines.join("\n")}`;
|
|
20585
|
+
} else {
|
|
20586
|
+
decisionUsageSection = `All ${usage.length} tracked ADs referenced within last 5 cycles.`;
|
|
20587
|
+
}
|
|
20588
|
+
}
|
|
20589
|
+
} catch {
|
|
20590
|
+
}
|
|
20591
|
+
let decisionLifecycleSection = "";
|
|
20592
|
+
try {
|
|
20593
|
+
const decisions = await adapter2.getActiveDecisions();
|
|
20594
|
+
const lifecycleSummary = formatDecisionLifecycleSummary(decisions);
|
|
20595
|
+
if (lifecycleSummary) {
|
|
20596
|
+
decisionLifecycleSection = `**Lifecycle:** ${lifecycleSummary}`;
|
|
20597
|
+
}
|
|
20598
|
+
} catch {
|
|
20599
|
+
}
|
|
20600
|
+
const decisionScoresSection = "";
|
|
20601
|
+
let contextUtilisationSection = "";
|
|
20602
|
+
try {
|
|
20603
|
+
const utilData = await adapter2.getContextUtilisation?.();
|
|
20604
|
+
if (utilData && utilData.length > 0) {
|
|
20605
|
+
const lines = utilData.filter((u) => u.cycleNumber === cycleNumber).map((u) => `- ${u.tool}: ${(u.avgUtilisation * 100).toFixed(0)}% utilisation (${(u.avgContextBytes / 1024).toFixed(1)}KB avg context)`);
|
|
20606
|
+
if (lines.length > 0) {
|
|
20607
|
+
contextUtilisationSection = `**Current cycle:**
|
|
20608
|
+
${lines.join("\n")}`;
|
|
20609
|
+
}
|
|
20610
|
+
}
|
|
20611
|
+
} catch {
|
|
20612
|
+
}
|
|
20613
|
+
let northStarSection = "";
|
|
20614
|
+
try {
|
|
20615
|
+
const staleness = await adapter2.getNorthStarStaleness?.();
|
|
20616
|
+
if (staleness) {
|
|
20617
|
+
const cycleGap = cycleNumber - staleness.setCycle;
|
|
20618
|
+
const daysSinceSet = Math.floor((Date.now() - new Date(staleness.setAt).getTime()) / (1e3 * 60 * 60 * 24));
|
|
20619
|
+
northStarSection = `\u2713 North Star set Cycle ${staleness.setCycle} (${cycleGap} cycles, ${daysSinceSet} days ago)`;
|
|
20620
|
+
} else {
|
|
20621
|
+
const setAtCycle = await adapter2.getNorthStarSetCycle?.();
|
|
20622
|
+
if (setAtCycle != null) {
|
|
20623
|
+
northStarSection = `\u2713 North Star set Cycle ${setAtCycle}`;
|
|
20624
|
+
} else if (adapter2.getCurrentNorthStar) {
|
|
20625
|
+
const ns = await adapter2.getCurrentNorthStar();
|
|
20626
|
+
northStarSection = ns ? "" : "\u26A0\uFE0F No North Star set \u2014 consider defining one";
|
|
20627
|
+
}
|
|
20628
|
+
}
|
|
20629
|
+
} catch {
|
|
20630
|
+
}
|
|
20631
|
+
const healthResult = computeHealthScore(cycleNumber, snapshots, activeTasks, decisionUsageEntries);
|
|
20632
|
+
return {
|
|
20633
|
+
cycleNumber,
|
|
20634
|
+
latestCycleStatus: health.latestCycleStatus,
|
|
20635
|
+
connectionStatus: getConnectionStatus(),
|
|
20636
|
+
reviewWarning,
|
|
20637
|
+
boardSummary,
|
|
20638
|
+
staleTasks,
|
|
20639
|
+
inReviewSummary,
|
|
20640
|
+
carryForward,
|
|
20641
|
+
recommendedMode,
|
|
20642
|
+
metricsSection,
|
|
20643
|
+
derivedMetricsSection,
|
|
20644
|
+
costSection,
|
|
20645
|
+
decisionUsageSection,
|
|
20646
|
+
decisionLifecycleSection,
|
|
20647
|
+
decisionScoresSection,
|
|
20648
|
+
contextUtilisationSection,
|
|
20649
|
+
northStarSection,
|
|
20650
|
+
healthScore: healthResult?.score ?? null,
|
|
20651
|
+
healthStatus: healthResult?.status ?? null,
|
|
20652
|
+
healthReason: healthResult?.reason ?? null
|
|
20653
|
+
};
|
|
20654
|
+
}
|
|
20655
|
+
|
|
20656
|
+
// src/tools/orient.ts
|
|
20657
|
+
init_git();
|
|
20658
|
+
|
|
20659
|
+
// src/tools/doc-registry.ts
|
|
20660
|
+
import { readdirSync as readdirSync4, existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
|
|
20661
|
+
import { join as join8, relative } from "path";
|
|
20662
|
+
import { homedir as homedir2 } from "os";
|
|
20663
|
+
var docRegisterTool = {
|
|
20664
|
+
name: "doc_register",
|
|
20665
|
+
description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
|
|
20666
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20667
|
+
inputSchema: {
|
|
20668
|
+
type: "object",
|
|
20669
|
+
properties: {
|
|
20670
|
+
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
20671
|
+
title: { type: "string", description: "Document title." },
|
|
20672
|
+
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
20673
|
+
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
20674
|
+
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
20675
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
20676
|
+
cycle: { type: "number", description: "Current cycle number." },
|
|
20677
|
+
actions: {
|
|
20678
|
+
type: "array",
|
|
20679
|
+
items: {
|
|
20680
|
+
type: "object",
|
|
20681
|
+
properties: {
|
|
20682
|
+
description: { type: "string" },
|
|
20683
|
+
status: { type: "string", enum: ["pending", "resolved"] },
|
|
20684
|
+
linkedTaskId: { type: "string" }
|
|
20685
|
+
},
|
|
20686
|
+
required: ["description", "status"]
|
|
20687
|
+
},
|
|
20688
|
+
description: "Actionable findings from the document."
|
|
20689
|
+
},
|
|
20690
|
+
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
20691
|
+
},
|
|
20692
|
+
required: ["path", "title", "type", "summary", "cycle"]
|
|
20693
|
+
}
|
|
20694
|
+
};
|
|
20695
|
+
var docSearchTool = {
|
|
20696
|
+
name: "doc_search",
|
|
20697
|
+
description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
|
|
20698
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20699
|
+
inputSchema: {
|
|
20700
|
+
type: "object",
|
|
20701
|
+
properties: {
|
|
20702
|
+
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
20703
|
+
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
20704
|
+
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
20705
|
+
keyword: { type: "string", description: "Search title and summary text." },
|
|
20706
|
+
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
20707
|
+
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
20708
|
+
limit: { type: "number", description: "Max results (default: 10)." }
|
|
20709
|
+
},
|
|
20710
|
+
required: []
|
|
20711
|
+
}
|
|
20712
|
+
};
|
|
20713
|
+
var docScanTool = {
|
|
20714
|
+
name: "doc_scan",
|
|
20715
|
+
description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
|
|
20716
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20717
|
+
inputSchema: {
|
|
20718
|
+
type: "object",
|
|
20719
|
+
properties: {
|
|
20720
|
+
include_plans: {
|
|
20721
|
+
type: "boolean",
|
|
20722
|
+
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
20723
|
+
}
|
|
20724
|
+
},
|
|
20725
|
+
required: []
|
|
20726
|
+
}
|
|
20727
|
+
};
|
|
20728
|
+
async function handleDocRegister(adapter2, args) {
|
|
20729
|
+
if (!adapter2.registerDoc) {
|
|
20730
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20731
|
+
}
|
|
20732
|
+
const path5 = args.path;
|
|
20733
|
+
const title = args.title;
|
|
20734
|
+
const type = args.type;
|
|
20735
|
+
const status = args.status ?? "active";
|
|
20736
|
+
const summary = args.summary;
|
|
20737
|
+
const tags = args.tags ?? [];
|
|
20738
|
+
const cycle = args.cycle;
|
|
20739
|
+
const actions = args.actions;
|
|
20740
|
+
const supersededByPath = args.superseded_by_path;
|
|
20741
|
+
if (!path5 || !title || !type || !summary || !cycle) {
|
|
20742
|
+
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
20743
|
+
}
|
|
20744
|
+
let supersededBy;
|
|
20745
|
+
if (supersededByPath) {
|
|
20746
|
+
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
20747
|
+
if (existing) {
|
|
20748
|
+
supersededBy = existing.id;
|
|
20749
|
+
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
20750
|
+
}
|
|
20751
|
+
}
|
|
20752
|
+
const entry = await adapter2.registerDoc({
|
|
20753
|
+
title,
|
|
20754
|
+
type,
|
|
20755
|
+
path: path5,
|
|
20756
|
+
status: supersededByPath ? "superseded" : status,
|
|
20757
|
+
summary,
|
|
20758
|
+
tags,
|
|
20759
|
+
cycleCreated: cycle,
|
|
20760
|
+
cycleUpdated: cycle,
|
|
20761
|
+
supersededBy,
|
|
20762
|
+
actions
|
|
20763
|
+
});
|
|
20764
|
+
return textResponse(
|
|
20765
|
+
`**Registered:** ${entry.title}
|
|
20766
|
+
- **Path:** ${entry.path}
|
|
20767
|
+
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
20768
|
+
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
20769
|
+
- **Actions:** ${actions?.length ?? 0} items
|
|
20770
|
+
- **ID:** ${entry.id}`
|
|
20771
|
+
);
|
|
20772
|
+
}
|
|
20773
|
+
async function handleDocSearch(adapter2, args) {
|
|
20774
|
+
if (!adapter2.searchDocs) {
|
|
20775
|
+
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20776
|
+
}
|
|
20777
|
+
const input = {
|
|
20778
|
+
type: args.type,
|
|
20779
|
+
status: args.status,
|
|
20780
|
+
tags: args.tags,
|
|
20781
|
+
keyword: args.keyword,
|
|
20782
|
+
hasPendingActions: args.has_pending_actions,
|
|
20783
|
+
sinceCycle: args.since_cycle,
|
|
20784
|
+
limit: args.limit
|
|
20785
|
+
};
|
|
20786
|
+
const docs = await adapter2.searchDocs(input);
|
|
20787
|
+
if (docs.length === 0) {
|
|
20788
|
+
return textResponse("No documents found matching the search criteria.");
|
|
20789
|
+
}
|
|
20790
|
+
const lines = docs.map((d) => {
|
|
20791
|
+
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
20792
|
+
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
20793
|
+
return `### ${d.title}
|
|
20794
|
+
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
20795
|
+
**Path:** ${d.path}
|
|
20796
|
+
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
20797
|
+
${d.summary}
|
|
20798
|
+
`;
|
|
20799
|
+
});
|
|
20800
|
+
return textResponse(`**${docs.length} document(s) found:**
|
|
20801
|
+
|
|
20802
|
+
${lines.join("\n---\n\n")}`);
|
|
20803
|
+
}
|
|
20804
|
+
function scanMdFiles(dir, rootDir) {
|
|
20805
|
+
if (!existsSync5(dir)) return [];
|
|
20806
|
+
const files = [];
|
|
20807
|
+
try {
|
|
20808
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20809
|
+
for (const entry of entries) {
|
|
20810
|
+
const full = join8(dir, entry.name);
|
|
20811
|
+
if (entry.isDirectory()) {
|
|
20812
|
+
files.push(...scanMdFiles(full, rootDir));
|
|
20813
|
+
} else if (entry.name.endsWith(".md")) {
|
|
20814
|
+
files.push(relative(rootDir, full));
|
|
20815
|
+
}
|
|
20816
|
+
}
|
|
20817
|
+
} catch {
|
|
20818
|
+
}
|
|
20819
|
+
return files;
|
|
20820
|
+
}
|
|
20821
|
+
function extractTitle(filePath) {
|
|
20822
|
+
try {
|
|
20823
|
+
const content = readFileSync2(filePath, "utf-8").slice(0, 1e3);
|
|
20824
|
+
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
20825
|
+
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
20826
|
+
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
20827
|
+
if (headingMatch) return headingMatch[1].trim();
|
|
20828
|
+
} catch {
|
|
20829
|
+
}
|
|
20830
|
+
return void 0;
|
|
20831
|
+
}
|
|
20832
|
+
async function handleDocScan(adapter2, config2, args) {
|
|
20833
|
+
if (!adapter2.searchDocs) {
|
|
20834
|
+
return errorResponse("Doc registry not available on this adapter.");
|
|
20835
|
+
}
|
|
20836
|
+
const includePlans = args.include_plans ?? false;
|
|
20837
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
20838
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
20839
|
+
const docsDir = join8(config2.projectRoot, "docs");
|
|
20840
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
20841
|
+
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
20842
|
+
let unregisteredPlans = [];
|
|
20843
|
+
if (includePlans) {
|
|
20844
|
+
const plansDir = join8(homedir2(), ".claude", "plans");
|
|
20845
|
+
if (existsSync5(plansDir)) {
|
|
20846
|
+
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
20847
|
+
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
20848
|
+
path: f,
|
|
20849
|
+
title: extractTitle(join8(plansDir, f.replace("plans/", "")))
|
|
20850
|
+
}));
|
|
20851
|
+
}
|
|
20852
|
+
}
|
|
20853
|
+
const lines = [];
|
|
20854
|
+
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
20855
|
+
return textResponse("All docs are registered. No unregistered files found.");
|
|
20856
|
+
}
|
|
20857
|
+
if (unregisteredDocs.length > 0) {
|
|
20858
|
+
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
20859
|
+
for (const f of unregisteredDocs) {
|
|
20860
|
+
const title = extractTitle(join8(config2.projectRoot, f));
|
|
20861
|
+
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
20862
|
+
}
|
|
20863
|
+
}
|
|
20864
|
+
if (unregisteredPlans.length > 0) {
|
|
20865
|
+
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
20866
|
+
for (const p of unregisteredPlans) {
|
|
20867
|
+
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
20868
|
+
}
|
|
20869
|
+
}
|
|
20870
|
+
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
20871
|
+
return textResponse(lines.join("\n"));
|
|
19997
20872
|
}
|
|
19998
20873
|
|
|
19999
20874
|
// src/tools/orient.ts
|
|
20000
|
-
init_git();
|
|
20001
20875
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
20002
|
-
import { readFileSync as
|
|
20003
|
-
import { join as
|
|
20876
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync6 } from "fs";
|
|
20877
|
+
import { join as join9 } from "path";
|
|
20004
20878
|
var orientTool = {
|
|
20005
20879
|
name: "orient",
|
|
20006
20880
|
description: "Session orientation \u2014 run this FIRST at session start before any other tool. Single call that replaces build_list + health. Returns: cycle number, task counts by status, in-progress/in-review tasks, strategy review cadence, velocity snapshot, recommended next action, and a release reminder when all cycle tasks are Done but release has not run. Read-only, does not modify any files.",
|
|
20881
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
20007
20882
|
inputSchema: {
|
|
20008
20883
|
type: "object",
|
|
20009
20884
|
properties: {},
|
|
@@ -20179,8 +21054,8 @@ function getLatestGitTag(projectRoot) {
|
|
|
20179
21054
|
}
|
|
20180
21055
|
function checkNpmVersionDrift() {
|
|
20181
21056
|
try {
|
|
20182
|
-
const pkgPath =
|
|
20183
|
-
const pkg = JSON.parse(
|
|
21057
|
+
const pkgPath = join9(new URL(".", import.meta.url).pathname, "..", "..", "package.json");
|
|
21058
|
+
const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
|
|
20184
21059
|
const localVersion = pkg.version;
|
|
20185
21060
|
const packageName = pkg.name;
|
|
20186
21061
|
const published = execFileSync3("npm", ["view", packageName, "version"], {
|
|
@@ -20220,6 +21095,17 @@ async function handleOrient(adapter2, config2) {
|
|
|
20220
21095
|
if (!cycleIsComplete && cycleTotal === 0 && cycleDone > 0) {
|
|
20221
21096
|
buildResult.warnings.unshift(`\u26A0\uFE0F Cycle ${currentCycle} is complete \u2014 all ${cycleDone} task${cycleDone !== 1 ? "s" : ""} Done. Release has not been run. Run \`release\` now.`);
|
|
20222
21097
|
}
|
|
21098
|
+
try {
|
|
21099
|
+
const p1BacklogTasks = await adapter2.queryBoard({ status: ["Backlog"], priority: ["P1 High"] });
|
|
21100
|
+
const stalledP1 = p1BacklogTasks.filter(
|
|
21101
|
+
(t) => t.createdCycle != null && currentCycle - t.createdCycle >= 3
|
|
21102
|
+
);
|
|
21103
|
+
if (stalledP1.length > 0) {
|
|
21104
|
+
const ids = stalledP1.map((t) => `${t.displayId} (${currentCycle - (t.createdCycle ?? currentCycle)}+ cycles)`).join(", ");
|
|
21105
|
+
buildResult.warnings.push(`\u26A0\uFE0F P1 tasks stalled 3+ cycles: ${ids}`);
|
|
21106
|
+
}
|
|
21107
|
+
} catch {
|
|
21108
|
+
}
|
|
20223
21109
|
const inProgressItems = buildResult.inProgress.map(
|
|
20224
21110
|
(t) => `- **${t.id}:** ${t.title} (${t.priority} | ${t.complexity})`
|
|
20225
21111
|
);
|
|
@@ -20275,8 +21161,15 @@ ${versionDrift}` : "";
|
|
|
20275
21161
|
try {
|
|
20276
21162
|
const unrecorded = detectUnrecordedCommits(config2.projectRoot, config2.baseBranch);
|
|
20277
21163
|
if (unrecorded.length > 0) {
|
|
21164
|
+
const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
|
|
21165
|
+
const adHocDoneTasks = doneTasks.filter((t) => t.cycle == null);
|
|
21166
|
+
const alreadyRecorded = adHocDoneTasks.length >= unrecorded.length;
|
|
20278
21167
|
const lines = ["\n\n## Unrecorded Work"];
|
|
20279
|
-
|
|
21168
|
+
if (alreadyRecorded) {
|
|
21169
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not matched to \`build_execute\` commits. ${adHocDoneTasks.length} ad_hoc task(s) already recorded \u2014 this work may already be captured. Verify before running \`ad_hoc\` again.`);
|
|
21170
|
+
} else {
|
|
21171
|
+
lines.push(`${unrecorded.length} commit(s) on ${config2.baseBranch} since last release not captured by \`build_execute\`. Run \`ad_hoc\` to record them.`);
|
|
21172
|
+
}
|
|
20280
21173
|
for (const c of unrecorded) {
|
|
20281
21174
|
lines.push(`- \`${c.hash}\` ${c.message}`);
|
|
20282
21175
|
}
|
|
@@ -20284,6 +21177,23 @@ ${versionDrift}` : "";
|
|
|
20284
21177
|
}
|
|
20285
21178
|
} catch {
|
|
20286
21179
|
}
|
|
21180
|
+
let unregisteredDocsNote = "";
|
|
21181
|
+
try {
|
|
21182
|
+
if (adapter2.searchDocs) {
|
|
21183
|
+
const docsDir = join9(config2.projectRoot, "docs");
|
|
21184
|
+
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
21185
|
+
if (docsFiles.length > 0) {
|
|
21186
|
+
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
21187
|
+
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
21188
|
+
const unregisteredCount = docsFiles.filter((f) => !registeredPaths.has(f)).length;
|
|
21189
|
+
if (unregisteredCount > 0) {
|
|
21190
|
+
unregisteredDocsNote = `
|
|
21191
|
+
\u26A0\uFE0F **${unregisteredCount} unregistered doc(s) in docs/** \u2014 run \`doc_scan\` to review, then \`doc_register\` to index them.`;
|
|
21192
|
+
}
|
|
21193
|
+
}
|
|
21194
|
+
}
|
|
21195
|
+
} catch {
|
|
21196
|
+
}
|
|
20287
21197
|
let recsNote = "";
|
|
20288
21198
|
try {
|
|
20289
21199
|
const pendingRecs = await adapter2.getPendingRecommendations();
|
|
@@ -20317,16 +21227,33 @@ ${versionDrift}` : "";
|
|
|
20317
21227
|
}
|
|
20318
21228
|
} catch {
|
|
20319
21229
|
}
|
|
20320
|
-
|
|
21230
|
+
let unactionedIssuesNote = "";
|
|
21231
|
+
try {
|
|
21232
|
+
const learnings = await adapter2.getCycleLearnings?.({ category: "issue", limit: 20 });
|
|
21233
|
+
if (learnings) {
|
|
21234
|
+
const unactioned = learnings.filter((l) => !l.actionTaken && l.severity && ["P0", "P1", "P2"].includes(l.severity)).slice(0, 5);
|
|
21235
|
+
if (unactioned.length > 0) {
|
|
21236
|
+
const lines = ["\n\n## Unactioned Issues"];
|
|
21237
|
+
for (const issue of unactioned) {
|
|
21238
|
+
const desc = issue.summary.length > 100 ? `${issue.summary.slice(0, 97)}\u2026` : issue.summary;
|
|
21239
|
+
lines.push(`- **${issue.severity}** (C${issue.cycleNumber} / ${issue.taskId}): ${desc}`);
|
|
21240
|
+
}
|
|
21241
|
+
lines.push("_Run `idea` to log these as backlog tasks, or `board_edit` if already handled._");
|
|
21242
|
+
unactionedIssuesNote = lines.join("\n");
|
|
21243
|
+
}
|
|
21244
|
+
}
|
|
21245
|
+
} catch {
|
|
21246
|
+
}
|
|
21247
|
+
return textResponse(formatOrientSummary(healthResult, buildInfo, hierarchy, latestTag, config2.projectRoot) + ttfvNote + reconciliationNote + unrecordedNote + unregisteredDocsNote + recsNote + pendingReviewNote + patternsNote + unactionedIssuesNote + versionNote + enrichmentNote);
|
|
20321
21248
|
} catch (err) {
|
|
20322
21249
|
const message = err instanceof Error ? err.message : String(err);
|
|
20323
21250
|
return errorResponse(`Orient failed: ${message}`);
|
|
20324
21251
|
}
|
|
20325
21252
|
}
|
|
20326
21253
|
function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
20327
|
-
const claudeMdPath =
|
|
20328
|
-
if (!
|
|
20329
|
-
const content =
|
|
21254
|
+
const claudeMdPath = join9(projectRoot, "CLAUDE.md");
|
|
21255
|
+
if (!existsSync6(claudeMdPath)) return "";
|
|
21256
|
+
const content = readFileSync3(claudeMdPath, "utf-8");
|
|
20330
21257
|
const additions = [];
|
|
20331
21258
|
if (cycleNumber >= 6 && !content.includes(CLAUDE_MD_ENRICHMENT_SENTINEL_T1)) {
|
|
20332
21259
|
additions.push(CLAUDE_MD_TIER_1);
|
|
@@ -20348,6 +21275,7 @@ function enrichClaudeMd(projectRoot, cycleNumber) {
|
|
|
20348
21275
|
var hierarchyUpdateTool = {
|
|
20349
21276
|
name: "hierarchy_update",
|
|
20350
21277
|
description: "Update the status of a phase, stage, or horizon in the project hierarchy (AD-14). Accepts a level (phase, stage, or horizon), a name or ID, and a new status. For stages, optionally set exit_criteria \u2014 a checklist defining when the stage is considered done. Does not call the Anthropic API.",
|
|
21278
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20351
21279
|
inputSchema: {
|
|
20352
21280
|
type: "object",
|
|
20353
21281
|
properties: {
|
|
@@ -20362,7 +21290,7 @@ var hierarchyUpdateTool = {
|
|
|
20362
21290
|
},
|
|
20363
21291
|
status: {
|
|
20364
21292
|
type: "string",
|
|
20365
|
-
enum: ["
|
|
21293
|
+
enum: ["Not Started", "In Progress", "Done", "Deferred"],
|
|
20366
21294
|
description: "The new status to set."
|
|
20367
21295
|
},
|
|
20368
21296
|
exit_criteria: {
|
|
@@ -20374,7 +21302,7 @@ var hierarchyUpdateTool = {
|
|
|
20374
21302
|
required: ["level", "name"]
|
|
20375
21303
|
}
|
|
20376
21304
|
};
|
|
20377
|
-
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["
|
|
21305
|
+
var VALID_STATUSES3 = /* @__PURE__ */ new Set(["Not Started", "In Progress", "Done", "Deferred"]);
|
|
20378
21306
|
async function handleHierarchyUpdate(adapter2, args) {
|
|
20379
21307
|
const level = args.level;
|
|
20380
21308
|
const name = args.name;
|
|
@@ -20390,7 +21318,7 @@ async function handleHierarchyUpdate(adapter2, args) {
|
|
|
20390
21318
|
return errorResponse(`Invalid level "${level}". Must be "phase", "stage", or "horizon".`);
|
|
20391
21319
|
}
|
|
20392
21320
|
if (status && !VALID_STATUSES3.has(status)) {
|
|
20393
|
-
return errorResponse(`Invalid status "${status}". Must be one of:
|
|
21321
|
+
return errorResponse(`Invalid status "${status}". Must be one of: Not Started, In Progress, Done, Deferred.`);
|
|
20394
21322
|
}
|
|
20395
21323
|
if (exitCriteria !== void 0 && level !== "stage") {
|
|
20396
21324
|
return errorResponse("exit_criteria can only be set on stages.");
|
|
@@ -20743,6 +21671,7 @@ async function applyZoomOut(adapter2, llmResponse, cycleNumber) {
|
|
|
20743
21671
|
var zoomOutTool = {
|
|
20744
21672
|
name: "zoom_out",
|
|
20745
21673
|
description: 'Run a Zoom-Out Retrospective \u2014 a higher-level meta-retrospective that sits above strategy reviews. Analyses the full project arc: every cycle, decision, and pivot. Use when you want to step back and see the big picture after many cycles. First call returns a prompt (prepare phase). Then call again with mode "apply" and your output.',
|
|
21674
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20746
21675
|
inputSchema: {
|
|
20747
21676
|
type: "object",
|
|
20748
21677
|
properties: {
|
|
@@ -20817,222 +21746,11 @@ ${result.userMessage}
|
|
|
20817
21746
|
}
|
|
20818
21747
|
}
|
|
20819
21748
|
|
|
20820
|
-
// src/tools/doc-registry.ts
|
|
20821
|
-
import { readdirSync as readdirSync4, existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
|
|
20822
|
-
import { join as join9, relative } from "path";
|
|
20823
|
-
import { homedir as homedir2 } from "os";
|
|
20824
|
-
var docRegisterTool = {
|
|
20825
|
-
name: "doc_register",
|
|
20826
|
-
description: "Register a document in the doc registry. Called after finalising a research/planning doc, or when build_execute detects unregistered docs. Stores metadata and structured summary \u2014 not full content.",
|
|
20827
|
-
inputSchema: {
|
|
20828
|
-
type: "object",
|
|
20829
|
-
properties: {
|
|
20830
|
-
path: { type: "string", description: 'Relative path from project root (e.g. "docs/research/funding-landscape.md").' },
|
|
20831
|
-
title: { type: "string", description: "Document title." },
|
|
20832
|
-
type: { type: "string", enum: ["research", "audit", "spec", "guide", "architecture", "positioning", "framework", "reference"], description: "Document type." },
|
|
20833
|
-
status: { type: "string", enum: ["active", "draft", "superseded", "actioned", "legacy", "archived"], description: 'Document status. Defaults to "active".' },
|
|
20834
|
-
summary: { type: "string", description: 'Structured 2-4 sentence summary. Format: "Conclusions: ... Open questions: ... Unactioned: ..."' },
|
|
20835
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags from project vocabulary." },
|
|
20836
|
-
cycle: { type: "number", description: "Current cycle number." },
|
|
20837
|
-
actions: {
|
|
20838
|
-
type: "array",
|
|
20839
|
-
items: {
|
|
20840
|
-
type: "object",
|
|
20841
|
-
properties: {
|
|
20842
|
-
description: { type: "string" },
|
|
20843
|
-
status: { type: "string", enum: ["pending", "resolved"] },
|
|
20844
|
-
linkedTaskId: { type: "string" }
|
|
20845
|
-
},
|
|
20846
|
-
required: ["description", "status"]
|
|
20847
|
-
},
|
|
20848
|
-
description: "Actionable findings from the document."
|
|
20849
|
-
},
|
|
20850
|
-
superseded_by_path: { type: "string", description: "Path of the doc that supersedes this one (sets status to superseded)." }
|
|
20851
|
-
},
|
|
20852
|
-
required: ["path", "title", "type", "summary", "cycle"]
|
|
20853
|
-
}
|
|
20854
|
-
};
|
|
20855
|
-
var docSearchTool = {
|
|
20856
|
-
name: "doc_search",
|
|
20857
|
-
description: "Search the doc registry for documents by type, tags, keyword, or pending actions. Returns summaries, not full content. Use for context gathering in plan, strategy review, and idea dedup.",
|
|
20858
|
-
inputSchema: {
|
|
20859
|
-
type: "object",
|
|
20860
|
-
properties: {
|
|
20861
|
-
type: { type: "string", description: 'Filter by doc type (e.g. "research", "architecture").' },
|
|
20862
|
-
status: { type: "string", description: 'Filter by status. Defaults to "active".' },
|
|
20863
|
-
tags: { type: "array", items: { type: "string" }, description: "Filter by tags (OR match)." },
|
|
20864
|
-
keyword: { type: "string", description: "Search title and summary text." },
|
|
20865
|
-
has_pending_actions: { type: "boolean", description: "Only docs with unresolved action items." },
|
|
20866
|
-
since_cycle: { type: "number", description: "Docs updated since this cycle." },
|
|
20867
|
-
limit: { type: "number", description: "Max results (default: 10)." }
|
|
20868
|
-
},
|
|
20869
|
-
required: []
|
|
20870
|
-
}
|
|
20871
|
-
};
|
|
20872
|
-
var docScanTool = {
|
|
20873
|
-
name: "doc_scan",
|
|
20874
|
-
description: "Scan docs/ and plans directories for unregistered .md files. Returns a list of files not yet in the doc registry. Use this to find docs that need registration.",
|
|
20875
|
-
inputSchema: {
|
|
20876
|
-
type: "object",
|
|
20877
|
-
properties: {
|
|
20878
|
-
include_plans: {
|
|
20879
|
-
type: "boolean",
|
|
20880
|
-
description: "Also scan ~/.claude/plans/ for plan files (default: false)."
|
|
20881
|
-
}
|
|
20882
|
-
},
|
|
20883
|
-
required: []
|
|
20884
|
-
}
|
|
20885
|
-
};
|
|
20886
|
-
async function handleDocRegister(adapter2, args) {
|
|
20887
|
-
if (!adapter2.registerDoc) {
|
|
20888
|
-
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20889
|
-
}
|
|
20890
|
-
const path5 = args.path;
|
|
20891
|
-
const title = args.title;
|
|
20892
|
-
const type = args.type;
|
|
20893
|
-
const status = args.status ?? "active";
|
|
20894
|
-
const summary = args.summary;
|
|
20895
|
-
const tags = args.tags ?? [];
|
|
20896
|
-
const cycle = args.cycle;
|
|
20897
|
-
const actions = args.actions;
|
|
20898
|
-
const supersededByPath = args.superseded_by_path;
|
|
20899
|
-
if (!path5 || !title || !type || !summary || !cycle) {
|
|
20900
|
-
return errorResponse("Required fields: path, title, type, summary, cycle.");
|
|
20901
|
-
}
|
|
20902
|
-
let supersededBy;
|
|
20903
|
-
if (supersededByPath) {
|
|
20904
|
-
const existing = await adapter2.getDoc?.(supersededByPath);
|
|
20905
|
-
if (existing) {
|
|
20906
|
-
supersededBy = existing.id;
|
|
20907
|
-
await adapter2.updateDocStatus?.(existing.id, "superseded", void 0);
|
|
20908
|
-
}
|
|
20909
|
-
}
|
|
20910
|
-
const entry = await adapter2.registerDoc({
|
|
20911
|
-
title,
|
|
20912
|
-
type,
|
|
20913
|
-
path: path5,
|
|
20914
|
-
status: supersededByPath ? "superseded" : status,
|
|
20915
|
-
summary,
|
|
20916
|
-
tags,
|
|
20917
|
-
cycleCreated: cycle,
|
|
20918
|
-
cycleUpdated: cycle,
|
|
20919
|
-
supersededBy,
|
|
20920
|
-
actions
|
|
20921
|
-
});
|
|
20922
|
-
return textResponse(
|
|
20923
|
-
`**Registered:** ${entry.title}
|
|
20924
|
-
- **Path:** ${entry.path}
|
|
20925
|
-
- **Type:** ${entry.type} | **Status:** ${entry.status}
|
|
20926
|
-
- **Tags:** ${entry.tags.length > 0 ? entry.tags.join(", ") : "none"}
|
|
20927
|
-
- **Actions:** ${actions?.length ?? 0} items
|
|
20928
|
-
- **ID:** ${entry.id}`
|
|
20929
|
-
);
|
|
20930
|
-
}
|
|
20931
|
-
async function handleDocSearch(adapter2, args) {
|
|
20932
|
-
if (!adapter2.searchDocs) {
|
|
20933
|
-
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20934
|
-
}
|
|
20935
|
-
const input = {
|
|
20936
|
-
type: args.type,
|
|
20937
|
-
status: args.status,
|
|
20938
|
-
tags: args.tags,
|
|
20939
|
-
keyword: args.keyword,
|
|
20940
|
-
hasPendingActions: args.has_pending_actions,
|
|
20941
|
-
sinceCycle: args.since_cycle,
|
|
20942
|
-
limit: args.limit
|
|
20943
|
-
};
|
|
20944
|
-
const docs = await adapter2.searchDocs(input);
|
|
20945
|
-
if (docs.length === 0) {
|
|
20946
|
-
return textResponse("No documents found matching the search criteria.");
|
|
20947
|
-
}
|
|
20948
|
-
const lines = docs.map((d) => {
|
|
20949
|
-
const actionCount = d.actions?.filter((a) => a.status === "pending").length ?? 0;
|
|
20950
|
-
const actionNote = actionCount > 0 ? ` | ${actionCount} pending action(s)` : "";
|
|
20951
|
-
return `### ${d.title}
|
|
20952
|
-
**Type:** ${d.type} | **Status:** ${d.status} | **Cycle:** ${d.cycleCreated}${d.cycleUpdated ? `\u2192${d.cycleUpdated}` : ""}${actionNote}
|
|
20953
|
-
**Path:** ${d.path}
|
|
20954
|
-
**Tags:** ${d.tags.length > 0 ? d.tags.join(", ") : "none"}
|
|
20955
|
-
${d.summary}
|
|
20956
|
-
`;
|
|
20957
|
-
});
|
|
20958
|
-
return textResponse(`**${docs.length} document(s) found:**
|
|
20959
|
-
|
|
20960
|
-
${lines.join("\n---\n\n")}`);
|
|
20961
|
-
}
|
|
20962
|
-
function scanMdFiles(dir, rootDir) {
|
|
20963
|
-
if (!existsSync6(dir)) return [];
|
|
20964
|
-
const files = [];
|
|
20965
|
-
try {
|
|
20966
|
-
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
20967
|
-
for (const entry of entries) {
|
|
20968
|
-
const full = join9(dir, entry.name);
|
|
20969
|
-
if (entry.isDirectory()) {
|
|
20970
|
-
files.push(...scanMdFiles(full, rootDir));
|
|
20971
|
-
} else if (entry.name.endsWith(".md")) {
|
|
20972
|
-
files.push(relative(rootDir, full));
|
|
20973
|
-
}
|
|
20974
|
-
}
|
|
20975
|
-
} catch {
|
|
20976
|
-
}
|
|
20977
|
-
return files;
|
|
20978
|
-
}
|
|
20979
|
-
function extractTitle(filePath) {
|
|
20980
|
-
try {
|
|
20981
|
-
const content = readFileSync3(filePath, "utf-8").slice(0, 1e3);
|
|
20982
|
-
const fmMatch = content.match(/^---[\s\S]*?title:\s*(.+?)$/m);
|
|
20983
|
-
if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
20984
|
-
const headingMatch = content.match(/^#+\s+(.+)$/m);
|
|
20985
|
-
if (headingMatch) return headingMatch[1].trim();
|
|
20986
|
-
} catch {
|
|
20987
|
-
}
|
|
20988
|
-
return void 0;
|
|
20989
|
-
}
|
|
20990
|
-
async function handleDocScan(adapter2, config2, args) {
|
|
20991
|
-
if (!adapter2.searchDocs) {
|
|
20992
|
-
return errorResponse("Doc registry not available \u2014 requires pg adapter.");
|
|
20993
|
-
}
|
|
20994
|
-
const includePlans = args.include_plans ?? false;
|
|
20995
|
-
const registered = await adapter2.searchDocs({ limit: 500, status: "all" });
|
|
20996
|
-
const registeredPaths = new Set(registered.map((d) => d.path));
|
|
20997
|
-
const docsDir = join9(config2.projectRoot, "docs");
|
|
20998
|
-
const docsFiles = scanMdFiles(docsDir, config2.projectRoot);
|
|
20999
|
-
const unregisteredDocs = docsFiles.filter((f) => !registeredPaths.has(f));
|
|
21000
|
-
let unregisteredPlans = [];
|
|
21001
|
-
if (includePlans) {
|
|
21002
|
-
const plansDir = join9(homedir2(), ".claude", "plans");
|
|
21003
|
-
if (existsSync6(plansDir)) {
|
|
21004
|
-
const planFiles = scanMdFiles(plansDir, plansDir);
|
|
21005
|
-
unregisteredPlans = planFiles.map((f) => `plans/${f}`).filter((f) => !registeredPaths.has(f)).map((f) => ({
|
|
21006
|
-
path: f,
|
|
21007
|
-
title: extractTitle(join9(plansDir, f.replace("plans/", "")))
|
|
21008
|
-
}));
|
|
21009
|
-
}
|
|
21010
|
-
}
|
|
21011
|
-
const lines = [];
|
|
21012
|
-
if (unregisteredDocs.length === 0 && unregisteredPlans.length === 0) {
|
|
21013
|
-
return textResponse("All docs are registered. No unregistered files found.");
|
|
21014
|
-
}
|
|
21015
|
-
if (unregisteredDocs.length > 0) {
|
|
21016
|
-
lines.push(`## Unregistered Docs (${unregisteredDocs.length})`);
|
|
21017
|
-
for (const f of unregisteredDocs) {
|
|
21018
|
-
const title = extractTitle(join9(config2.projectRoot, f));
|
|
21019
|
-
lines.push(`- \`${f}\`${title ? ` \u2014 ${title}` : ""}`);
|
|
21020
|
-
}
|
|
21021
|
-
}
|
|
21022
|
-
if (unregisteredPlans.length > 0) {
|
|
21023
|
-
lines.push("", `## Unregistered Plans (${unregisteredPlans.length})`);
|
|
21024
|
-
for (const p of unregisteredPlans) {
|
|
21025
|
-
lines.push(`- \`${p.path}\`${p.title ? ` \u2014 ${p.title}` : ""}`);
|
|
21026
|
-
}
|
|
21027
|
-
}
|
|
21028
|
-
lines.push("", `Use \`doc_register\` to register these files.`);
|
|
21029
|
-
return textResponse(lines.join("\n"));
|
|
21030
|
-
}
|
|
21031
|
-
|
|
21032
21749
|
// src/tools/sibling-ads.ts
|
|
21033
21750
|
var getSiblingAdsTool = {
|
|
21034
21751
|
name: "get_sibling_ads",
|
|
21035
21752
|
description: "Read Active Decisions from sibling PAPI projects that share the same Supabase instance. Requires PAPI_SIBLING_PROJECT_IDS env var (comma-separated project UUIDs). Returns ADs labelled by source project \u2014 useful for cross-project architectural alignment. pg adapter only \u2014 returns an error if using md or proxy adapter.",
|
|
21753
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
21036
21754
|
inputSchema: {
|
|
21037
21755
|
type: "object",
|
|
21038
21756
|
properties: {
|
|
@@ -21214,6 +21932,7 @@ var lastPrepareContextBytes2;
|
|
|
21214
21932
|
var handoffGenerateTool = {
|
|
21215
21933
|
name: "handoff_generate",
|
|
21216
21934
|
description: "Generate BUILD HANDOFFs for cycle tasks that don't have one yet. Run after `plan` (with skip_handoffs=true) or to regenerate stale handoffs. Uses the prepare/apply pattern \u2014 first call returns a prompt, second call persists results.",
|
|
21935
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
21217
21936
|
inputSchema: {
|
|
21218
21937
|
type: "object",
|
|
21219
21938
|
properties: {
|
|
@@ -21324,12 +22043,14 @@ function isEnabled() {
|
|
|
21324
22043
|
}
|
|
21325
22044
|
function emitTelemetryEvent(event) {
|
|
21326
22045
|
if (!isEnabled()) return;
|
|
22046
|
+
const userId = event.user_id ?? process.env["PAPI_USER_ID"] ?? void 0;
|
|
21327
22047
|
const body = {
|
|
21328
22048
|
project_id: event.project_id,
|
|
21329
22049
|
tool_name: event.tool_name,
|
|
21330
22050
|
event_type: event.event_type,
|
|
21331
22051
|
metadata: event.metadata ?? {}
|
|
21332
22052
|
};
|
|
22053
|
+
if (userId) body["user_id"] = userId;
|
|
21333
22054
|
fetch(`${TELEMETRY_SUPABASE_URL}/rest/v1/telemetry_events`, {
|
|
21334
22055
|
method: "POST",
|
|
21335
22056
|
headers: {
|
|
@@ -21376,7 +22097,6 @@ var TOOLS_REQUIRING_PAPI = /* @__PURE__ */ new Set([
|
|
|
21376
22097
|
"idea",
|
|
21377
22098
|
"bug",
|
|
21378
22099
|
"ad_hoc",
|
|
21379
|
-
"health",
|
|
21380
22100
|
"board_reconcile",
|
|
21381
22101
|
"review_list",
|
|
21382
22102
|
"review_submit",
|
|
@@ -21458,7 +22178,6 @@ function createServer(adapter2, config2) {
|
|
|
21458
22178
|
bugTool,
|
|
21459
22179
|
adHocTool,
|
|
21460
22180
|
boardReconcileTool,
|
|
21461
|
-
healthTool,
|
|
21462
22181
|
releaseTool,
|
|
21463
22182
|
reviewListTool,
|
|
21464
22183
|
reviewSubmitTool,
|
|
@@ -21550,9 +22269,6 @@ function createServer(adapter2, config2) {
|
|
|
21550
22269
|
case "board_reconcile":
|
|
21551
22270
|
result = await handleBoardReconcile(adapter2, config2, safeArgs);
|
|
21552
22271
|
break;
|
|
21553
|
-
case "health":
|
|
21554
|
-
result = await handleHealth(adapter2);
|
|
21555
|
-
break;
|
|
21556
22272
|
case "release":
|
|
21557
22273
|
result = await handleRelease(adapter2, config2, safeArgs);
|
|
21558
22274
|
break;
|
|
@@ -21599,12 +22315,13 @@ function createServer(adapter2, config2) {
|
|
|
21599
22315
|
delete result._usage;
|
|
21600
22316
|
delete result._contextBytes;
|
|
21601
22317
|
delete result._contextUtilisation;
|
|
21602
|
-
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
}
|
|
22318
|
+
const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
|
|
22319
|
+
try {
|
|
22320
|
+
const metric = buildMetric(name, elapsed, usage, void 0, contextBytes, contextUtilisation);
|
|
22321
|
+
metric.success = !isError;
|
|
22322
|
+
adapter2.appendToolMetric(metric).catch(() => {
|
|
22323
|
+
});
|
|
22324
|
+
} catch {
|
|
21608
22325
|
}
|
|
21609
22326
|
const telemetryProjectId = process.env["PAPI_PROJECT_ID"];
|
|
21610
22327
|
if (telemetryProjectId) {
|
|
@@ -21612,7 +22329,6 @@ function createServer(adapter2, config2) {
|
|
|
21612
22329
|
adapter_type: config2.adapterType
|
|
21613
22330
|
});
|
|
21614
22331
|
const isApplyMode = safeArgs.mode === "apply";
|
|
21615
|
-
const isError = result.content.some((c) => c.text.startsWith("Error:") || c.text.startsWith("\u274C"));
|
|
21616
22332
|
if (!isError) {
|
|
21617
22333
|
if (name === "setup" && isApplyMode) {
|
|
21618
22334
|
emitMilestone(telemetryProjectId, "setup_completed");
|