@papi-ai/server 0.3.0-alpha.1 → 0.3.1-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1742 -122
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1122,6 +1122,17 @@ function serializeReview(review) {
1122
1122
  );
1123
1123
  if (review.handoffRevision !== void 0) lines.push(`- **Handoff Revision:** ${review.handoffRevision}`);
1124
1124
  if (review.buildCommitSha) lines.push(`- **Build Commit SHA:** ${review.buildCommitSha}`);
1125
+ if (review.autoReview) {
1126
+ lines.push("", `#### Auto-Review (${review.autoReview.verdict})`);
1127
+ lines.push(`> ${review.autoReview.summary}`);
1128
+ if (review.autoReview.findings.length > 0) {
1129
+ lines.push("");
1130
+ for (const f of review.autoReview.findings) {
1131
+ const loc = f.file ? f.line ? `${f.file}:${f.line}` : f.file : "";
1132
+ lines.push(`- \`${f.severity}\`${loc ? ` ${loc}` : ""}: ${f.message}`);
1133
+ }
1134
+ }
1135
+ }
1125
1136
  return lines.join("\n");
1126
1137
  }
1127
1138
  function prependReview(review, content) {
@@ -4344,6 +4355,7 @@ __export(dist_exports, {
4344
4355
  PgAdapter: () => PgAdapter,
4345
4356
  PgPapiAdapter: () => PgPapiAdapter,
4346
4357
  configFromEnv: () => configFromEnv,
4358
+ ensureSchema: () => ensureSchema,
4347
4359
  normaliseComplexity: () => normaliseComplexity,
4348
4360
  normaliseEffort: () => normaliseEffort
4349
4361
  });
@@ -4374,6 +4386,29 @@ function definedEntries(obj) {
4374
4386
  }
4375
4387
  return result;
4376
4388
  }
4389
+ async function ensureSchema(config2) {
4390
+ const sql = src_default(config2.connectionString, {
4391
+ max: 1,
4392
+ connect_timeout: 10
4393
+ });
4394
+ try {
4395
+ const [result] = await sql`
4396
+ SELECT EXISTS (
4397
+ SELECT 1 FROM information_schema.tables
4398
+ WHERE table_schema = 'public' AND table_name = 'projects'
4399
+ ) as exists
4400
+ `;
4401
+ if (result.exists) {
4402
+ return false;
4403
+ }
4404
+ console.error("[papi] Fresh database detected \u2014 creating PAPI schema...");
4405
+ await sql.unsafe(BASE_SCHEMA_SQL);
4406
+ console.error("[papi] \u2713 Schema created (31 tables)");
4407
+ return true;
4408
+ } finally {
4409
+ await sql.end();
4410
+ }
4411
+ }
4377
4412
  function rowToTask(row) {
4378
4413
  const task = {
4379
4414
  uuid: row.id,
@@ -4449,6 +4484,7 @@ function rowToReview(row) {
4449
4484
  };
4450
4485
  if (row.handoff_revision != null) review.handoffRevision = row.handoff_revision;
4451
4486
  if (row.build_commit_sha != null) review.buildCommitSha = row.build_commit_sha;
4487
+ if (row.auto_review != null) review.autoReview = row.auto_review;
4452
4488
  return review;
4453
4489
  }
4454
4490
  function rowToActiveDecision(row) {
@@ -4644,12 +4680,13 @@ function normaliseComplexity(value) {
4644
4680
  function normaliseEffort(value) {
4645
4681
  return EFFORT_MAP[value] ?? value;
4646
4682
  }
4647
- var PgAdapter, rowToCycle, COMPLEXITY_MAP, EFFORT_MAP, PgPapiAdapter;
4683
+ var PgAdapter, BASE_SCHEMA_SQL, rowToCycle, COMPLEXITY_MAP, EFFORT_MAP, PgPapiAdapter;
4648
4684
  var init_dist3 = __esm({
4649
4685
  "../adapter-pg/dist/index.js"() {
4650
4686
  "use strict";
4651
4687
  init_src();
4652
4688
  init_src();
4689
+ init_src();
4653
4690
  init_dist2();
4654
4691
  PgAdapter = class {
4655
4692
  sql;
@@ -5169,6 +5206,736 @@ var init_dist3 = __esm({
5169
5206
  await this.sql.end();
5170
5207
  }
5171
5208
  };
5209
+ BASE_SCHEMA_SQL = `
5210
+ -- =============================================================================
5211
+ -- Core tables (no foreign key dependencies)
5212
+ -- =============================================================================
5213
+
5214
+ CREATE TABLE IF NOT EXISTS projects (
5215
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5216
+ slug TEXT NOT NULL,
5217
+ name TEXT NOT NULL,
5218
+ repo_url TEXT,
5219
+ papi_dir TEXT,
5220
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5221
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5222
+ site_url TEXT,
5223
+ PRIMARY KEY (id),
5224
+ UNIQUE (slug)
5225
+ );
5226
+
5227
+ CREATE TABLE IF NOT EXISTS shared_milestones (
5228
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5229
+ title TEXT NOT NULL,
5230
+ description TEXT DEFAULT ''::text NOT NULL,
5231
+ status TEXT DEFAULT 'not_started'::text NOT NULL,
5232
+ target_date DATE,
5233
+ completed_at TIMESTAMPTZ,
5234
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5235
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5236
+ archived_at TIMESTAMPTZ,
5237
+ PRIMARY KEY (id)
5238
+ );
5239
+
5240
+ CREATE TABLE IF NOT EXISTS shared_decisions (
5241
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5242
+ display_id TEXT NOT NULL,
5243
+ title TEXT NOT NULL,
5244
+ decision TEXT NOT NULL,
5245
+ confidence TEXT NOT NULL,
5246
+ status TEXT DEFAULT 'proposed'::text NOT NULL,
5247
+ origin_project_id UUID NOT NULL REFERENCES projects(id),
5248
+ evidence TEXT DEFAULT ''::text NOT NULL,
5249
+ superseded_by_id UUID REFERENCES shared_decisions(id),
5250
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5251
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5252
+ archived_at TIMESTAMPTZ,
5253
+ PRIMARY KEY (id),
5254
+ UNIQUE (display_id)
5255
+ );
5256
+
5257
+ -- =============================================================================
5258
+ -- Project-scoped tables
5259
+ -- =============================================================================
5260
+
5261
+ CREATE TABLE IF NOT EXISTS product_briefs (
5262
+ project_id UUID NOT NULL REFERENCES projects(id),
5263
+ content TEXT DEFAULT ''::text NOT NULL,
5264
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5265
+ landscape_references JSONB,
5266
+ user_journeys JSONB,
5267
+ mvp_boundary TEXT,
5268
+ assumptions_open_questions JSONB,
5269
+ success_signals JSONB,
5270
+ PRIMARY KEY (project_id)
5271
+ );
5272
+
5273
+ CREATE TABLE IF NOT EXISTS cycles (
5274
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5275
+ project_id UUID NOT NULL REFERENCES projects(id),
5276
+ number INTEGER NOT NULL,
5277
+ status TEXT DEFAULT 'planning'::text NOT NULL,
5278
+ start_date TIMESTAMPTZ NOT NULL,
5279
+ end_date TIMESTAMPTZ,
5280
+ goals TEXT[] DEFAULT '{}'::text[] NOT NULL,
5281
+ board_health TEXT DEFAULT ''::text NOT NULL,
5282
+ task_ids TEXT[] DEFAULT '{}'::uuid[] NOT NULL,
5283
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5284
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5285
+ user_id UUID,
5286
+ context_hashes JSONB,
5287
+ PRIMARY KEY (id),
5288
+ UNIQUE (project_id, number)
5289
+ );
5290
+
5291
+ CREATE TABLE IF NOT EXISTS horizons (
5292
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5293
+ slug TEXT NOT NULL,
5294
+ label TEXT NOT NULL,
5295
+ description TEXT,
5296
+ status TEXT DEFAULT 'active'::text NOT NULL,
5297
+ sort_order INTEGER DEFAULT 0 NOT NULL,
5298
+ project_id UUID NOT NULL REFERENCES projects(id),
5299
+ user_id UUID,
5300
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5301
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5302
+ PRIMARY KEY (id),
5303
+ UNIQUE (project_id, slug)
5304
+ );
5305
+
5306
+ CREATE TABLE IF NOT EXISTS stages (
5307
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5308
+ slug TEXT NOT NULL,
5309
+ label TEXT NOT NULL,
5310
+ description TEXT,
5311
+ status TEXT DEFAULT 'active'::text NOT NULL,
5312
+ sort_order INTEGER DEFAULT 0 NOT NULL,
5313
+ horizon_id UUID NOT NULL REFERENCES horizons(id),
5314
+ project_id UUID NOT NULL REFERENCES projects(id),
5315
+ user_id UUID,
5316
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5317
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5318
+ PRIMARY KEY (id),
5319
+ UNIQUE (project_id, slug)
5320
+ );
5321
+
5322
+ CREATE TABLE IF NOT EXISTS phases (
5323
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5324
+ project_id UUID NOT NULL REFERENCES projects(id),
5325
+ slug TEXT NOT NULL,
5326
+ label TEXT NOT NULL,
5327
+ description TEXT DEFAULT ''::text NOT NULL,
5328
+ status TEXT DEFAULT 'Not Started'::text NOT NULL,
5329
+ sort_order INTEGER DEFAULT 0 NOT NULL,
5330
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5331
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5332
+ user_id UUID,
5333
+ stage_id UUID REFERENCES stages(id),
5334
+ PRIMARY KEY (id),
5335
+ UNIQUE (project_id, slug)
5336
+ );
5337
+
5338
+ CREATE TABLE IF NOT EXISTS cycle_tasks (
5339
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5340
+ project_id UUID NOT NULL REFERENCES projects(id),
5341
+ display_id TEXT NOT NULL,
5342
+ title TEXT NOT NULL,
5343
+ status TEXT DEFAULT 'Backlog'::text NOT NULL,
5344
+ priority TEXT DEFAULT 'P3 Low'::text NOT NULL,
5345
+ complexity TEXT DEFAULT 'Medium'::text NOT NULL,
5346
+ module TEXT DEFAULT ''::text NOT NULL,
5347
+ epic TEXT DEFAULT ''::text NOT NULL,
5348
+ phase TEXT DEFAULT ''::text NOT NULL,
5349
+ owner TEXT DEFAULT 'TBD'::text NOT NULL,
5350
+ reviewed BOOLEAN DEFAULT false NOT NULL,
5351
+ sprint INTEGER,
5352
+ created_sprint INTEGER,
5353
+ why TEXT,
5354
+ depends_on TEXT,
5355
+ notes TEXT,
5356
+ closure_reason TEXT,
5357
+ state_history JSONB DEFAULT '[]'::jsonb,
5358
+ build_handoff JSONB,
5359
+ build_report TEXT,
5360
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5361
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5362
+ user_id UUID,
5363
+ task_type TEXT,
5364
+ maturity TEXT,
5365
+ cycle_number INTEGER,
5366
+ created_cycle INTEGER,
5367
+ stage_id UUID REFERENCES stages(id),
5368
+ PRIMARY KEY (id),
5369
+ UNIQUE (project_id, display_id)
5370
+ );
5371
+
5372
+ CREATE TABLE IF NOT EXISTS active_decisions (
5373
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5374
+ project_id UUID NOT NULL REFERENCES projects(id),
5375
+ display_id TEXT NOT NULL,
5376
+ title TEXT NOT NULL,
5377
+ confidence TEXT DEFAULT 'MEDIUM'::text NOT NULL,
5378
+ superseded BOOLEAN DEFAULT false NOT NULL,
5379
+ superseded_by TEXT,
5380
+ created_sprint INTEGER,
5381
+ modified_sprint INTEGER,
5382
+ body TEXT DEFAULT ''::text NOT NULL,
5383
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5384
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5385
+ outcome TEXT DEFAULT 'pending'::text NOT NULL,
5386
+ revision_count INTEGER DEFAULT 0 NOT NULL,
5387
+ last_referenced_sprint INTEGER,
5388
+ user_id UUID,
5389
+ created_cycle INTEGER,
5390
+ modified_cycle INTEGER,
5391
+ last_referenced_cycle INTEGER,
5392
+ PRIMARY KEY (id),
5393
+ UNIQUE (project_id, display_id)
5394
+ );
5395
+
5396
+ CREATE TABLE IF NOT EXISTS north_stars (
5397
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5398
+ project_id UUID NOT NULL REFERENCES projects(id),
5399
+ statement TEXT NOT NULL,
5400
+ set_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5401
+ superseded_by_id UUID REFERENCES north_stars(id),
5402
+ superseded_at TIMESTAMPTZ,
5403
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5404
+ user_id UUID,
5405
+ PRIMARY KEY (id)
5406
+ );
5407
+
5408
+ CREATE TABLE IF NOT EXISTS planning_log_entries (
5409
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5410
+ project_id UUID NOT NULL REFERENCES projects(id),
5411
+ sprint_number INTEGER NOT NULL,
5412
+ title TEXT NOT NULL,
5413
+ content TEXT DEFAULT ''::text NOT NULL,
5414
+ carry_forward TEXT,
5415
+ notes TEXT,
5416
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5417
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5418
+ user_id UUID,
5419
+ cycle_number INTEGER,
5420
+ PRIMARY KEY (id),
5421
+ UNIQUE (project_id, sprint_number)
5422
+ );
5423
+
5424
+ CREATE TABLE IF NOT EXISTS build_handoffs (
5425
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5426
+ project_id UUID NOT NULL REFERENCES projects(id),
5427
+ display_id TEXT NOT NULL,
5428
+ task_id TEXT NOT NULL,
5429
+ task_title TEXT NOT NULL,
5430
+ sprint INTEGER NOT NULL,
5431
+ why_now TEXT DEFAULT ''::text NOT NULL,
5432
+ scope TEXT[] DEFAULT '{}'::text[] NOT NULL,
5433
+ scope_boundary TEXT[] DEFAULT '{}'::text[] NOT NULL,
5434
+ acceptance_criteria TEXT[] DEFAULT '{}'::text[] NOT NULL,
5435
+ security_considerations TEXT DEFAULT ''::text NOT NULL,
5436
+ files_likely_touched TEXT[] DEFAULT '{}'::text[] NOT NULL,
5437
+ effort TEXT DEFAULT 'M'::text NOT NULL,
5438
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5439
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5440
+ user_id UUID,
5441
+ PRIMARY KEY (id),
5442
+ UNIQUE (project_id, display_id)
5443
+ );
5444
+
5445
+ CREATE TABLE IF NOT EXISTS build_reports (
5446
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5447
+ project_id UUID NOT NULL REFERENCES projects(id),
5448
+ display_id TEXT NOT NULL,
5449
+ task_id TEXT NOT NULL,
5450
+ task_name TEXT NOT NULL,
5451
+ date TEXT NOT NULL,
5452
+ sprint INTEGER NOT NULL,
5453
+ completed TEXT DEFAULT 'No'::text NOT NULL,
5454
+ actual_effort TEXT NOT NULL,
5455
+ estimated_effort TEXT NOT NULL,
5456
+ scope_accuracy TEXT DEFAULT 'accurate'::text NOT NULL,
5457
+ surprises TEXT DEFAULT ''::text NOT NULL,
5458
+ discovered_issues TEXT DEFAULT ''::text NOT NULL,
5459
+ architecture_notes TEXT DEFAULT ''::text NOT NULL,
5460
+ commit_sha TEXT,
5461
+ files_changed TEXT[] DEFAULT '{}'::text[],
5462
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5463
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5464
+ related_decisions TEXT[] DEFAULT '{}'::text[],
5465
+ user_id UUID,
5466
+ handoff_accuracy JSONB,
5467
+ corrections_count INTEGER DEFAULT 0 NOT NULL,
5468
+ brief_implications JSONB,
5469
+ cycle_number INTEGER,
5470
+ dead_ends TEXT,
5471
+ PRIMARY KEY (id),
5472
+ UNIQUE (project_id, display_id)
5473
+ );
5474
+
5475
+ CREATE TABLE IF NOT EXISTS reviews (
5476
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5477
+ project_id UUID NOT NULL REFERENCES projects(id),
5478
+ display_id TEXT NOT NULL,
5479
+ task_id TEXT NOT NULL,
5480
+ stage TEXT NOT NULL,
5481
+ reviewer TEXT DEFAULT ''::text NOT NULL,
5482
+ verdict TEXT NOT NULL,
5483
+ sprint INTEGER NOT NULL,
5484
+ date TEXT NOT NULL,
5485
+ comments TEXT DEFAULT ''::text NOT NULL,
5486
+ handoff_revision INTEGER,
5487
+ build_commit_sha TEXT,
5488
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5489
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5490
+ user_id UUID,
5491
+ cycle_number INTEGER,
5492
+ auto_review JSONB,
5493
+ PRIMARY KEY (id),
5494
+ UNIQUE (project_id, display_id)
5495
+ );
5496
+
5497
+ CREATE TABLE IF NOT EXISTS registries (
5498
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5499
+ project_id UUID NOT NULL REFERENCES projects(id),
5500
+ modules TEXT[] DEFAULT '{}'::text[] NOT NULL,
5501
+ epics TEXT[] DEFAULT '{}'::text[] NOT NULL,
5502
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5503
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5504
+ PRIMARY KEY (id),
5505
+ UNIQUE (project_id)
5506
+ );
5507
+
5508
+ CREATE TABLE IF NOT EXISTS tool_call_metrics (
5509
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5510
+ project_id UUID NOT NULL REFERENCES projects(id),
5511
+ timestamp TEXT NOT NULL,
5512
+ tool TEXT NOT NULL,
5513
+ duration_ms INTEGER NOT NULL,
5514
+ input_tokens INTEGER,
5515
+ output_tokens INTEGER,
5516
+ estimated_cost_usd NUMERIC,
5517
+ model TEXT,
5518
+ sprint_number INTEGER,
5519
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5520
+ user_id UUID,
5521
+ context_bytes INTEGER,
5522
+ context_utilisation NUMERIC,
5523
+ cycle_number INTEGER,
5524
+ PRIMARY KEY (id)
5525
+ );
5526
+
5527
+ CREATE TABLE IF NOT EXISTS cost_snapshots (
5528
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5529
+ project_id UUID NOT NULL REFERENCES projects(id),
5530
+ sprint INTEGER NOT NULL,
5531
+ date TEXT NOT NULL,
5532
+ total_cost_usd NUMERIC DEFAULT 0 NOT NULL,
5533
+ total_input_tokens BIGINT DEFAULT 0 NOT NULL,
5534
+ total_output_tokens BIGINT DEFAULT 0 NOT NULL,
5535
+ total_calls INTEGER DEFAULT 0 NOT NULL,
5536
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5537
+ user_id UUID,
5538
+ cycle_number INTEGER,
5539
+ PRIMARY KEY (id),
5540
+ UNIQUE (project_id, sprint)
5541
+ );
5542
+
5543
+ CREATE TABLE IF NOT EXISTS cycle_metrics_snapshots (
5544
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5545
+ project_id UUID NOT NULL REFERENCES projects(id),
5546
+ sprint INTEGER NOT NULL,
5547
+ date TEXT NOT NULL,
5548
+ accuracy JSONB DEFAULT '[]'::jsonb NOT NULL,
5549
+ velocity JSONB DEFAULT '[]'::jsonb NOT NULL,
5550
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5551
+ user_id UUID,
5552
+ cycle_number INTEGER,
5553
+ PRIMARY KEY (id),
5554
+ UNIQUE (project_id, sprint)
5555
+ );
5556
+
5557
+ CREATE TABLE IF NOT EXISTS strategy_reviews (
5558
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5559
+ project_id UUID NOT NULL REFERENCES projects(id),
5560
+ sprint_number INTEGER NOT NULL,
5561
+ title TEXT NOT NULL,
5562
+ content TEXT NOT NULL,
5563
+ notes TEXT,
5564
+ board_health TEXT,
5565
+ strategic_direction TEXT,
5566
+ recommendations JSONB,
5567
+ user_id UUID,
5568
+ created_at TIMESTAMPTZ DEFAULT now(),
5569
+ sprint_range TEXT,
5570
+ full_analysis TEXT,
5571
+ velocity_assessment TEXT,
5572
+ structured_data JSONB,
5573
+ cycle_number INTEGER,
5574
+ cycle_range TEXT,
5575
+ PRIMARY KEY (id)
5576
+ );
5577
+
5578
+ CREATE TABLE IF NOT EXISTS strategy_recommendations (
5579
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5580
+ project_id UUID NOT NULL REFERENCES projects(id),
5581
+ type TEXT NOT NULL,
5582
+ status TEXT DEFAULT 'pending'::text NOT NULL,
5583
+ content TEXT DEFAULT ''::text NOT NULL,
5584
+ created_sprint INTEGER NOT NULL,
5585
+ actioned_sprint INTEGER,
5586
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5587
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5588
+ dismissal_reason TEXT,
5589
+ created_cycle INTEGER,
5590
+ actioned_cycle INTEGER,
5591
+ target TEXT,
5592
+ PRIMARY KEY (id)
5593
+ );
5594
+
5595
+ CREATE TABLE IF NOT EXISTS decision_events (
5596
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5597
+ project_id UUID NOT NULL REFERENCES projects(id),
5598
+ decision_id TEXT NOT NULL,
5599
+ event_type TEXT NOT NULL,
5600
+ sprint INTEGER NOT NULL,
5601
+ source TEXT NOT NULL,
5602
+ source_ref TEXT,
5603
+ detail TEXT,
5604
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5605
+ cycle_number INTEGER,
5606
+ PRIMARY KEY (id)
5607
+ );
5608
+
5609
+ CREATE TABLE IF NOT EXISTS decision_scores (
5610
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5611
+ project_id UUID NOT NULL REFERENCES projects(id),
5612
+ decision_id TEXT NOT NULL,
5613
+ sprint INTEGER NOT NULL,
5614
+ effort SMALLINT NOT NULL,
5615
+ risk SMALLINT NOT NULL,
5616
+ reversibility SMALLINT NOT NULL,
5617
+ scale_cost SMALLINT NOT NULL,
5618
+ lock_in SMALLINT NOT NULL,
5619
+ total_score SMALLINT,
5620
+ rationale TEXT,
5621
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5622
+ cycle_number INTEGER,
5623
+ PRIMARY KEY (id),
5624
+ UNIQUE (project_id, decision_id, sprint)
5625
+ );
5626
+
5627
+ CREATE TABLE IF NOT EXISTS entity_references (
5628
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5629
+ project_id UUID NOT NULL REFERENCES projects(id),
5630
+ user_id UUID,
5631
+ entity_type TEXT DEFAULT 'active_decision'::text NOT NULL,
5632
+ entity_id TEXT NOT NULL,
5633
+ reference_context TEXT NOT NULL,
5634
+ tool_name TEXT NOT NULL,
5635
+ sprint_number INTEGER NOT NULL,
5636
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5637
+ cycle_number INTEGER,
5638
+ PRIMARY KEY (id)
5639
+ );
5640
+
5641
+ CREATE TABLE IF NOT EXISTS dogfood_log (
5642
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5643
+ project_id UUID NOT NULL REFERENCES projects(id),
5644
+ cycle_number INTEGER NOT NULL,
5645
+ category TEXT NOT NULL,
5646
+ content TEXT NOT NULL,
5647
+ source_tool TEXT DEFAULT 'strategy_review'::text NOT NULL,
5648
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5649
+ status TEXT DEFAULT 'observed'::text NOT NULL,
5650
+ linked_task_id TEXT,
5651
+ PRIMARY KEY (id)
5652
+ );
5653
+
5654
+ CREATE TABLE IF NOT EXISTS task_comments (
5655
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5656
+ project_id UUID NOT NULL,
5657
+ task_id TEXT NOT NULL,
5658
+ author TEXT DEFAULT 'human'::text NOT NULL,
5659
+ content TEXT NOT NULL,
5660
+ user_id UUID,
5661
+ created_at TIMESTAMPTZ DEFAULT now(),
5662
+ PRIMARY KEY (id)
5663
+ );
5664
+
5665
+ CREATE TABLE IF NOT EXISTS task_transitions (
5666
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5667
+ project_id UUID NOT NULL REFERENCES projects(id),
5668
+ task_id VARCHAR(50) NOT NULL,
5669
+ from_status VARCHAR(50) NOT NULL,
5670
+ to_status VARCHAR(50) NOT NULL,
5671
+ changed_by VARCHAR(100) DEFAULT 'system'::character varying NOT NULL,
5672
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5673
+ PRIMARY KEY (id)
5674
+ );
5675
+
5676
+ -- =============================================================================
5677
+ -- Cross-project tables
5678
+ -- =============================================================================
5679
+
5680
+ CREATE TABLE IF NOT EXISTS acknowledgements (
5681
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5682
+ decision_id UUID NOT NULL REFERENCES shared_decisions(id),
5683
+ project_id UUID NOT NULL REFERENCES projects(id),
5684
+ status TEXT DEFAULT 'pending'::text NOT NULL,
5685
+ comments TEXT,
5686
+ responded_at TIMESTAMPTZ,
5687
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5688
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5689
+ PRIMARY KEY (id),
5690
+ UNIQUE (decision_id, project_id)
5691
+ );
5692
+
5693
+ CREATE TABLE IF NOT EXISTS milestone_dependencies (
5694
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5695
+ milestone_id UUID NOT NULL REFERENCES shared_milestones(id),
5696
+ depends_on_id UUID NOT NULL REFERENCES shared_milestones(id),
5697
+ PRIMARY KEY (id),
5698
+ UNIQUE (milestone_id, depends_on_id)
5699
+ );
5700
+
5701
+ CREATE TABLE IF NOT EXISTS project_contributions (
5702
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5703
+ milestone_id UUID NOT NULL REFERENCES shared_milestones(id),
5704
+ project_id UUID NOT NULL REFERENCES projects(id),
5705
+ status TEXT DEFAULT 'blocked'::text NOT NULL,
5706
+ required_phases TEXT[] DEFAULT '{}'::text[] NOT NULL,
5707
+ notes TEXT,
5708
+ delivered_at TIMESTAMPTZ,
5709
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5710
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5711
+ PRIMARY KEY (id),
5712
+ UNIQUE (milestone_id, project_id)
5713
+ );
5714
+
5715
+ CREATE TABLE IF NOT EXISTS conflict_alerts (
5716
+ id UUID DEFAULT gen_random_uuid() NOT NULL,
5717
+ conflict_type TEXT NOT NULL,
5718
+ title TEXT NOT NULL,
5719
+ description TEXT NOT NULL,
5720
+ status TEXT DEFAULT 'open'::text NOT NULL,
5721
+ decision_a_id UUID REFERENCES shared_decisions(id),
5722
+ decision_b_id UUID REFERENCES shared_decisions(id),
5723
+ north_star_id UUID REFERENCES north_stars(id),
5724
+ resolution_decision_id UUID REFERENCES shared_decisions(id),
5725
+ raised_by TEXT NOT NULL,
5726
+ created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5727
+ updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
5728
+ resolved_at TIMESTAMPTZ,
5729
+ PRIMARY KEY (id)
5730
+ );
5731
+
5732
+ -- =============================================================================
5733
+ -- Indexes
5734
+ -- =============================================================================
5735
+
5736
+ CREATE INDEX IF NOT EXISTS idx_cycles_project ON cycles(project_id);
5737
+ CREATE INDEX IF NOT EXISTS idx_cycles_status ON cycles(status);
5738
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_project ON cycle_tasks(project_id);
5739
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_status ON cycle_tasks(status);
5740
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_sprint ON cycle_tasks(sprint);
5741
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_priority ON cycle_tasks(priority);
5742
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_phase ON cycle_tasks(phase);
5743
+ CREATE INDEX IF NOT EXISTS idx_cycle_tasks_user_id ON cycle_tasks(user_id);
5744
+ CREATE INDEX IF NOT EXISTS idx_phases_project ON phases(project_id);
5745
+ CREATE INDEX IF NOT EXISTS idx_phases_stage_id ON phases(stage_id);
5746
+ CREATE INDEX IF NOT EXISTS idx_horizons_project_id ON horizons(project_id);
5747
+ CREATE INDEX IF NOT EXISTS idx_stages_project_id ON stages(project_id);
5748
+ CREATE INDEX IF NOT EXISTS idx_stages_horizon_id ON stages(horizon_id);
5749
+ CREATE INDEX IF NOT EXISTS idx_active_decisions_project ON active_decisions(project_id);
5750
+ CREATE INDEX IF NOT EXISTS idx_active_decisions_confidence ON active_decisions(confidence);
5751
+ CREATE INDEX IF NOT EXISTS idx_active_decisions_user_id ON active_decisions(user_id);
5752
+ CREATE INDEX IF NOT EXISTS idx_north_stars_project ON north_stars(project_id);
5753
+ CREATE INDEX IF NOT EXISTS idx_north_stars_active ON north_stars(project_id) WHERE superseded_by_id IS NULL;
5754
+ CREATE INDEX IF NOT EXISTS idx_planning_log_entries_project ON planning_log_entries(project_id);
5755
+ CREATE INDEX IF NOT EXISTS idx_planning_log_entries_sprint ON planning_log_entries(sprint_number);
5756
+ CREATE INDEX IF NOT EXISTS idx_build_handoffs_project ON build_handoffs(project_id);
5757
+ CREATE INDEX IF NOT EXISTS idx_build_handoffs_sprint ON build_handoffs(sprint);
5758
+ CREATE INDEX IF NOT EXISTS idx_build_handoffs_task_id ON build_handoffs(task_id);
5759
+ CREATE INDEX IF NOT EXISTS idx_build_reports_project ON build_reports(project_id);
5760
+ CREATE INDEX IF NOT EXISTS idx_build_reports_sprint ON build_reports(sprint);
5761
+ CREATE INDEX IF NOT EXISTS idx_build_reports_task_id ON build_reports(task_id);
5762
+ CREATE INDEX IF NOT EXISTS idx_build_reports_user_id ON build_reports(user_id);
5763
+ CREATE INDEX IF NOT EXISTS idx_reviews_project ON reviews(project_id);
5764
+ CREATE INDEX IF NOT EXISTS idx_reviews_sprint ON reviews(sprint);
5765
+ CREATE INDEX IF NOT EXISTS idx_reviews_task_id ON reviews(task_id);
5766
+ CREATE INDEX IF NOT EXISTS idx_registries_project ON registries(project_id);
5767
+ CREATE INDEX IF NOT EXISTS idx_tool_call_metrics_project ON tool_call_metrics(project_id);
5768
+ CREATE INDEX IF NOT EXISTS idx_tool_call_metrics_sprint ON tool_call_metrics(sprint_number);
5769
+ CREATE INDEX IF NOT EXISTS idx_tool_call_metrics_tool ON tool_call_metrics(tool);
5770
+ CREATE INDEX IF NOT EXISTS idx_tool_call_metrics_user_id ON tool_call_metrics(user_id);
5771
+ CREATE INDEX IF NOT EXISTS idx_cost_snapshots_project ON cost_snapshots(project_id);
5772
+ CREATE INDEX IF NOT EXISTS idx_cost_snapshots_user_id ON cost_snapshots(user_id);
5773
+ CREATE INDEX IF NOT EXISTS idx_cycle_metrics_snapshots_project ON cycle_metrics_snapshots(project_id);
5774
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_strategy_reviews_project_sprint ON strategy_reviews(project_id, sprint_number);
5775
+ CREATE INDEX IF NOT EXISTS idx_strategy_reviews_user_id ON strategy_reviews(user_id);
5776
+ CREATE INDEX IF NOT EXISTS idx_strategy_recommendations_sprint ON strategy_recommendations(project_id, created_sprint);
5777
+ CREATE INDEX IF NOT EXISTS idx_strategy_recommendations_project_status ON strategy_recommendations(project_id, status);
5778
+ CREATE INDEX IF NOT EXISTS idx_decision_events_project_decision ON decision_events(project_id, decision_id);
5779
+ CREATE INDEX IF NOT EXISTS idx_decision_events_project_sprint ON decision_events(project_id, sprint);
5780
+ CREATE INDEX IF NOT EXISTS idx_decision_scores_project_decision ON decision_scores(project_id, decision_id);
5781
+ CREATE INDEX IF NOT EXISTS idx_entity_references_project ON entity_references(project_id);
5782
+ CREATE INDEX IF NOT EXISTS idx_entity_references_entity ON entity_references(entity_id, entity_type);
5783
+ CREATE INDEX IF NOT EXISTS idx_entity_references_sprint ON entity_references(sprint_number);
5784
+ CREATE INDEX IF NOT EXISTS idx_dogfood_log_project ON dogfood_log(project_id);
5785
+ CREATE INDEX IF NOT EXISTS idx_dogfood_log_cycle ON dogfood_log(cycle_number);
5786
+ CREATE INDEX IF NOT EXISTS idx_dogfood_log_status ON dogfood_log(project_id, status) WHERE status = 'observed';
5787
+ CREATE INDEX IF NOT EXISTS idx_task_comments_project_task ON task_comments(project_id, task_id);
5788
+ CREATE INDEX IF NOT EXISTS idx_task_comments_user_id ON task_comments(user_id);
5789
+ CREATE INDEX IF NOT EXISTS idx_task_transitions_project_task ON task_transitions(project_id, task_id);
5790
+ CREATE INDEX IF NOT EXISTS idx_task_transitions_created ON task_transitions(project_id, created_at DESC);
5791
+ CREATE INDEX IF NOT EXISTS idx_acknowledgements_decision ON acknowledgements(decision_id);
5792
+ CREATE INDEX IF NOT EXISTS idx_acknowledgements_project ON acknowledgements(project_id);
5793
+ CREATE INDEX IF NOT EXISTS idx_shared_decisions_origin_project ON shared_decisions(origin_project_id);
5794
+ CREATE INDEX IF NOT EXISTS idx_shared_decisions_status ON shared_decisions(status);
5795
+ CREATE INDEX IF NOT EXISTS idx_shared_decisions_superseded_by ON shared_decisions(superseded_by_id);
5796
+ CREATE INDEX IF NOT EXISTS idx_shared_milestones_status ON shared_milestones(status);
5797
+ CREATE INDEX IF NOT EXISTS idx_milestone_deps_milestone ON milestone_dependencies(milestone_id);
5798
+ CREATE INDEX IF NOT EXISTS idx_milestone_deps_depends_on ON milestone_dependencies(depends_on_id);
5799
+ CREATE INDEX IF NOT EXISTS idx_project_contributions_project ON project_contributions(project_id);
5800
+ CREATE INDEX IF NOT EXISTS idx_project_contributions_milestone ON project_contributions(milestone_id);
5801
+ CREATE INDEX IF NOT EXISTS idx_project_contributions_status ON project_contributions(status);
5802
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_decision_a ON conflict_alerts(decision_a_id);
5803
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_decision_b ON conflict_alerts(decision_b_id);
5804
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_north_star ON conflict_alerts(north_star_id);
5805
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_resolution ON conflict_alerts(resolution_decision_id);
5806
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_status ON conflict_alerts(status);
5807
+ CREATE INDEX IF NOT EXISTS idx_conflict_alerts_type ON conflict_alerts(conflict_type);
5808
+
5809
+ -- =============================================================================
5810
+ -- Row Level Security \u2014 permissive for alpha (service role bypasses RLS)
5811
+ -- =============================================================================
5812
+
5813
+ ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
5814
+ ALTER TABLE cycles ENABLE ROW LEVEL SECURITY;
5815
+ ALTER TABLE cycle_tasks ENABLE ROW LEVEL SECURITY;
5816
+ ALTER TABLE phases ENABLE ROW LEVEL SECURITY;
5817
+ ALTER TABLE horizons ENABLE ROW LEVEL SECURITY;
5818
+ ALTER TABLE stages ENABLE ROW LEVEL SECURITY;
5819
+ ALTER TABLE active_decisions ENABLE ROW LEVEL SECURITY;
5820
+ ALTER TABLE north_stars ENABLE ROW LEVEL SECURITY;
5821
+ ALTER TABLE planning_log_entries ENABLE ROW LEVEL SECURITY;
5822
+ ALTER TABLE build_handoffs ENABLE ROW LEVEL SECURITY;
5823
+ ALTER TABLE build_reports ENABLE ROW LEVEL SECURITY;
5824
+ ALTER TABLE reviews ENABLE ROW LEVEL SECURITY;
5825
+ ALTER TABLE registries ENABLE ROW LEVEL SECURITY;
5826
+ ALTER TABLE tool_call_metrics ENABLE ROW LEVEL SECURITY;
5827
+ ALTER TABLE cost_snapshots ENABLE ROW LEVEL SECURITY;
5828
+ ALTER TABLE cycle_metrics_snapshots ENABLE ROW LEVEL SECURITY;
5829
+ ALTER TABLE strategy_reviews ENABLE ROW LEVEL SECURITY;
5830
+ ALTER TABLE strategy_recommendations ENABLE ROW LEVEL SECURITY;
5831
+ ALTER TABLE decision_events ENABLE ROW LEVEL SECURITY;
5832
+ ALTER TABLE decision_scores ENABLE ROW LEVEL SECURITY;
5833
+ ALTER TABLE entity_references ENABLE ROW LEVEL SECURITY;
5834
+ ALTER TABLE dogfood_log ENABLE ROW LEVEL SECURITY;
5835
+ ALTER TABLE task_comments ENABLE ROW LEVEL SECURITY;
5836
+ ALTER TABLE task_transitions ENABLE ROW LEVEL SECURITY;
5837
+ ALTER TABLE product_briefs ENABLE ROW LEVEL SECURITY;
5838
+ ALTER TABLE acknowledgements ENABLE ROW LEVEL SECURITY;
5839
+ ALTER TABLE shared_decisions ENABLE ROW LEVEL SECURITY;
5840
+ ALTER TABLE shared_milestones ENABLE ROW LEVEL SECURITY;
5841
+ ALTER TABLE milestone_dependencies ENABLE ROW LEVEL SECURITY;
5842
+ ALTER TABLE project_contributions ENABLE ROW LEVEL SECURITY;
5843
+ ALTER TABLE conflict_alerts ENABLE ROW LEVEL SECURITY;
5844
+
5845
+ DO $$ BEGIN
5846
+ CREATE POLICY "allow_all_projects" ON projects FOR ALL USING (true) WITH CHECK (true);
5847
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5848
+ DO $$ BEGIN
5849
+ CREATE POLICY "allow_all_cycles" ON cycles FOR ALL USING (true) WITH CHECK (true);
5850
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5851
+ DO $$ BEGIN
5852
+ CREATE POLICY "allow_all_cycle_tasks" ON cycle_tasks FOR ALL USING (true) WITH CHECK (true);
5853
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5854
+ DO $$ BEGIN
5855
+ CREATE POLICY "allow_all_phases" ON phases FOR ALL USING (true) WITH CHECK (true);
5856
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5857
+ DO $$ BEGIN
5858
+ CREATE POLICY "allow_all_horizons" ON horizons FOR ALL USING (true) WITH CHECK (true);
5859
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5860
+ DO $$ BEGIN
5861
+ CREATE POLICY "allow_all_stages" ON stages FOR ALL USING (true) WITH CHECK (true);
5862
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5863
+ DO $$ BEGIN
5864
+ CREATE POLICY "allow_all_active_decisions" ON active_decisions FOR ALL USING (true) WITH CHECK (true);
5865
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5866
+ DO $$ BEGIN
5867
+ CREATE POLICY "allow_all_north_stars" ON north_stars FOR ALL USING (true) WITH CHECK (true);
5868
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5869
+ DO $$ BEGIN
5870
+ CREATE POLICY "allow_all_planning_log_entries" ON planning_log_entries FOR ALL USING (true) WITH CHECK (true);
5871
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5872
+ DO $$ BEGIN
5873
+ CREATE POLICY "allow_all_build_handoffs" ON build_handoffs FOR ALL USING (true) WITH CHECK (true);
5874
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5875
+ DO $$ BEGIN
5876
+ CREATE POLICY "allow_all_build_reports" ON build_reports FOR ALL USING (true) WITH CHECK (true);
5877
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5878
+ DO $$ BEGIN
5879
+ CREATE POLICY "allow_all_reviews" ON reviews FOR ALL USING (true) WITH CHECK (true);
5880
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5881
+ DO $$ BEGIN
5882
+ CREATE POLICY "allow_all_registries" ON registries FOR ALL USING (true) WITH CHECK (true);
5883
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5884
+ DO $$ BEGIN
5885
+ CREATE POLICY "allow_all_tool_call_metrics" ON tool_call_metrics FOR ALL USING (true) WITH CHECK (true);
5886
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5887
+ DO $$ BEGIN
5888
+ CREATE POLICY "allow_all_cost_snapshots" ON cost_snapshots FOR ALL USING (true) WITH CHECK (true);
5889
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5890
+ DO $$ BEGIN
5891
+ CREATE POLICY "allow_all_cycle_metrics_snapshots" ON cycle_metrics_snapshots FOR ALL USING (true) WITH CHECK (true);
5892
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5893
+ DO $$ BEGIN
5894
+ CREATE POLICY "allow_all_strategy_reviews" ON strategy_reviews FOR ALL USING (true) WITH CHECK (true);
5895
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5896
+ DO $$ BEGIN
5897
+ CREATE POLICY "allow_all_strategy_recommendations" ON strategy_recommendations FOR ALL USING (true) WITH CHECK (true);
5898
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5899
+ DO $$ BEGIN
5900
+ CREATE POLICY "allow_all_decision_events" ON decision_events FOR ALL USING (true) WITH CHECK (true);
5901
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5902
+ DO $$ BEGIN
5903
+ CREATE POLICY "allow_all_decision_scores" ON decision_scores FOR ALL USING (true) WITH CHECK (true);
5904
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5905
+ DO $$ BEGIN
5906
+ CREATE POLICY "allow_all_entity_references" ON entity_references FOR ALL USING (true) WITH CHECK (true);
5907
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5908
+ DO $$ BEGIN
5909
+ CREATE POLICY "allow_all_dogfood_log" ON dogfood_log FOR ALL USING (true) WITH CHECK (true);
5910
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5911
+ DO $$ BEGIN
5912
+ CREATE POLICY "allow_all_task_comments" ON task_comments FOR ALL USING (true) WITH CHECK (true);
5913
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5914
+ DO $$ BEGIN
5915
+ CREATE POLICY "allow_all_task_transitions" ON task_transitions FOR ALL USING (true) WITH CHECK (true);
5916
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5917
+ DO $$ BEGIN
5918
+ CREATE POLICY "allow_all_product_briefs" ON product_briefs FOR ALL USING (true) WITH CHECK (true);
5919
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5920
+ DO $$ BEGIN
5921
+ CREATE POLICY "allow_all_acknowledgements" ON acknowledgements FOR ALL USING (true) WITH CHECK (true);
5922
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5923
+ DO $$ BEGIN
5924
+ CREATE POLICY "allow_all_shared_decisions" ON shared_decisions FOR ALL USING (true) WITH CHECK (true);
5925
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5926
+ DO $$ BEGIN
5927
+ CREATE POLICY "allow_all_shared_milestones" ON shared_milestones FOR ALL USING (true) WITH CHECK (true);
5928
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5929
+ DO $$ BEGIN
5930
+ CREATE POLICY "allow_all_milestone_dependencies" ON milestone_dependencies FOR ALL USING (true) WITH CHECK (true);
5931
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5932
+ DO $$ BEGIN
5933
+ CREATE POLICY "allow_all_project_contributions" ON project_contributions FOR ALL USING (true) WITH CHECK (true);
5934
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5935
+ DO $$ BEGIN
5936
+ CREATE POLICY "allow_all_conflict_alerts" ON conflict_alerts FOR ALL USING (true) WITH CHECK (true);
5937
+ EXCEPTION WHEN duplicate_object THEN NULL; END $$;
5938
+ `;
5172
5939
  rowToCycle = rowToSprint;
5173
5940
  COMPLEXITY_MAP = {
5174
5941
  "XS": "Small",
@@ -5596,6 +6363,12 @@ ${newParts.join("\n")}` : newParts.join("\n");
5596
6363
  modified_sprint = EXCLUDED.modified_sprint,
5597
6364
  revision_count = active_decisions.revision_count + 1,
5598
6365
  outcome = CASE WHEN active_decisions.outcome = 'pending' THEN 'revised' ELSE active_decisions.outcome END
6366
+ `;
6367
+ }
6368
+ async deleteActiveDecision(id) {
6369
+ await this.sql`
6370
+ DELETE FROM active_decisions
6371
+ WHERE project_id = ${this.projectId} AND display_id = ${id}
5599
6372
  `;
5600
6373
  }
5601
6374
  // -------------------------------------------------------------------------
@@ -5819,12 +6592,13 @@ ${newParts.join("\n")}` : newParts.join("\n");
5819
6592
  await this.sql`
5820
6593
  INSERT INTO reviews (
5821
6594
  project_id, display_id, task_id, stage, reviewer, verdict,
5822
- sprint, date, comments, handoff_revision, build_commit_sha
6595
+ sprint, date, comments, handoff_revision, build_commit_sha, auto_review
5823
6596
  ) VALUES (
5824
6597
  ${this.projectId}, ${displayId}, ${review.taskId}, ${review.stage},
5825
6598
  ${review.reviewer}, ${review.verdict}, ${review.sprint},
5826
6599
  ${review.date}, ${review.comments},
5827
- ${review.handoffRevision ?? null}, ${review.buildCommitSha ?? null}
6600
+ ${review.handoffRevision ?? null}, ${review.buildCommitSha ?? null},
6601
+ ${review.autoReview ? JSON.stringify(review.autoReview) : null}
5828
6602
  )
5829
6603
  `;
5830
6604
  }
@@ -6854,6 +7628,331 @@ ${newParts.join("\n")}` : newParts.join("\n");
6854
7628
  }
6855
7629
  });
6856
7630
 
7631
+ // src/proxy-adapter.ts
7632
+ var proxy_adapter_exports = {};
7633
+ __export(proxy_adapter_exports, {
7634
+ ProxyPapiAdapter: () => ProxyPapiAdapter
7635
+ });
7636
+ var ProxyPapiAdapter;
7637
+ var init_proxy_adapter = __esm({
7638
+ "src/proxy-adapter.ts"() {
7639
+ "use strict";
7640
+ ProxyPapiAdapter = class {
7641
+ endpoint;
7642
+ apiKey;
7643
+ projectId;
7644
+ constructor(config2) {
7645
+ this.endpoint = config2.endpoint.replace(/\/$/, "");
7646
+ this.apiKey = config2.apiKey;
7647
+ this.projectId = config2.projectId;
7648
+ }
7649
+ /**
7650
+ * Send an adapter method call to the proxy Edge Function.
7651
+ * Serializes { projectId, method, args } and deserializes the response.
7652
+ */
7653
+ async invoke(method, args = []) {
7654
+ const url = `${this.endpoint}/invoke`;
7655
+ const response = await fetch(url, {
7656
+ method: "POST",
7657
+ headers: {
7658
+ "Content-Type": "application/json",
7659
+ "Authorization": `Bearer ${this.apiKey}`
7660
+ },
7661
+ body: JSON.stringify({
7662
+ projectId: this.projectId,
7663
+ method,
7664
+ args
7665
+ })
7666
+ });
7667
+ if (!response.ok) {
7668
+ const errorBody = await response.text();
7669
+ let message;
7670
+ try {
7671
+ const parsed = JSON.parse(errorBody);
7672
+ message = parsed.error ?? errorBody;
7673
+ } catch {
7674
+ message = errorBody;
7675
+ }
7676
+ throw new Error(`Proxy error (${response.status}) on ${method}: ${message}`);
7677
+ }
7678
+ const body = await response.json();
7679
+ if (!body.ok && body.error) {
7680
+ throw new Error(`Proxy error on ${method}: ${body.error}`);
7681
+ }
7682
+ return body.result;
7683
+ }
7684
+ /** Check if the proxy is reachable. */
7685
+ async probeConnection() {
7686
+ try {
7687
+ const response = await fetch(`${this.endpoint}/health`, {
7688
+ signal: AbortSignal.timeout(5e3)
7689
+ });
7690
+ return response.ok;
7691
+ } catch {
7692
+ return false;
7693
+ }
7694
+ }
7695
+ // --- Planning & Health ---
7696
+ readPlanningLog() {
7697
+ return this.invoke("readPlanningLog");
7698
+ }
7699
+ getSprintHealth() {
7700
+ return this.invoke("getSprintHealth");
7701
+ }
7702
+ getActiveDecisions() {
7703
+ return this.invoke("getActiveDecisions");
7704
+ }
7705
+ getSprintLog(limit) {
7706
+ return this.invoke("getSprintLog", [limit]);
7707
+ }
7708
+ getSprintLogSince(sprintNumber) {
7709
+ return this.invoke("getSprintLogSince", [sprintNumber]);
7710
+ }
7711
+ setSprintHealth(updates) {
7712
+ return this.invoke("setSprintHealth", [updates]);
7713
+ }
7714
+ writeSprintLogEntry(entry) {
7715
+ return this.invoke("writeSprintLogEntry", [entry]);
7716
+ }
7717
+ updateActiveDecision(id, body, sprintNumber) {
7718
+ return this.invoke("updateActiveDecision", [id, body, sprintNumber]);
7719
+ }
7720
+ upsertActiveDecision(id, body, title, confidence, sprintNumber) {
7721
+ return this.invoke("upsertActiveDecision", [id, body, title, confidence, sprintNumber]);
7722
+ }
7723
+ deleteActiveDecision(id) {
7724
+ return this.invoke("deleteActiveDecision", [id]);
7725
+ }
7726
+ // --- Board / Tasks ---
7727
+ queryBoard(options) {
7728
+ return this.invoke("queryBoard", [options]);
7729
+ }
7730
+ getTask(id) {
7731
+ return this.invoke("getTask", [id]);
7732
+ }
7733
+ getTasks(ids) {
7734
+ return this.invoke("getTasks", [ids]);
7735
+ }
7736
+ createTask(task) {
7737
+ return this.invoke("createTask", [task]);
7738
+ }
7739
+ updateTask(id, updates, options) {
7740
+ return this.invoke("updateTask", [id, updates, options]);
7741
+ }
7742
+ updateTaskStatus(id, status) {
7743
+ return this.invoke("updateTaskStatus", [id, status]);
7744
+ }
7745
+ recordTransition(taskId, fromStatus, toStatus, changedBy) {
7746
+ return this.invoke("recordTransition", [taskId, fromStatus, toStatus, changedBy]);
7747
+ }
7748
+ // --- Build Reports ---
7749
+ appendBuildReport(report) {
7750
+ return this.invoke("appendBuildReport", [report]);
7751
+ }
7752
+ getRecentBuildReports(count) {
7753
+ return this.invoke("getRecentBuildReports", [count]);
7754
+ }
7755
+ getBuildReportsSince(sprintNumber) {
7756
+ return this.invoke("getBuildReportsSince", [sprintNumber]);
7757
+ }
7758
+ // --- Reviews ---
7759
+ getRecentReviews(count) {
7760
+ return this.invoke("getRecentReviews", [count]);
7761
+ }
7762
+ writeReview(review) {
7763
+ return this.invoke("writeReview", [review]);
7764
+ }
7765
+ // --- Compression / Archive ---
7766
+ compressSprintLog(threshold, summary) {
7767
+ return this.invoke("compressSprintLog", [threshold, summary]);
7768
+ }
7769
+ compressBuildReports(threshold, summary) {
7770
+ return this.invoke("compressBuildReports", [threshold, summary]);
7771
+ }
7772
+ archiveTasks(phases, statuses) {
7773
+ return this.invoke("archiveTasks", [phases, statuses]);
7774
+ }
7775
+ // --- Product Brief & Discovery ---
7776
+ readProductBrief() {
7777
+ return this.invoke("readProductBrief");
7778
+ }
7779
+ updateProductBrief(content) {
7780
+ return this.invoke("updateProductBrief", [content]);
7781
+ }
7782
+ readDiscoveryCanvas() {
7783
+ return this.invoke("readDiscoveryCanvas");
7784
+ }
7785
+ updateDiscoveryCanvas(canvas) {
7786
+ return this.invoke("updateDiscoveryCanvas", [canvas]);
7787
+ }
7788
+ // --- Phases & Hierarchy ---
7789
+ readPhases() {
7790
+ return this.invoke("readPhases");
7791
+ }
7792
+ writePhases(phases) {
7793
+ return this.invoke("writePhases", [phases]);
7794
+ }
7795
+ readHorizons() {
7796
+ return this.invoke("readHorizons");
7797
+ }
7798
+ readStages(horizonId) {
7799
+ return this.invoke("readStages", [horizonId]);
7800
+ }
7801
+ updateStageStatus(stageId, status) {
7802
+ return this.invoke("updateStageStatus", [stageId, status]);
7803
+ }
7804
+ updateHorizonStatus(horizonId, status) {
7805
+ return this.invoke("updateHorizonStatus", [horizonId, status]);
7806
+ }
7807
+ getActiveStage() {
7808
+ return this.invoke("getActiveStage");
7809
+ }
7810
+ createHorizon(horizon) {
7811
+ return this.invoke("createHorizon", [horizon]);
7812
+ }
7813
+ createStage(stage) {
7814
+ return this.invoke("createStage", [stage]);
7815
+ }
7816
+ linkPhasesToStage(stageId) {
7817
+ return this.invoke("linkPhasesToStage", [stageId]);
7818
+ }
7819
+ // --- Metrics ---
7820
+ appendToolMetric(metric) {
7821
+ return this.invoke("appendToolMetric", [metric]);
7822
+ }
7823
+ readToolMetrics() {
7824
+ return this.invoke("readToolMetrics");
7825
+ }
7826
+ getCostSummary(sprintNumber) {
7827
+ return this.invoke("getCostSummary", [sprintNumber]);
7828
+ }
7829
+ writeCostSnapshot(snapshot) {
7830
+ return this.invoke("writeCostSnapshot", [snapshot]);
7831
+ }
7832
+ getCostSnapshots() {
7833
+ return this.invoke("getCostSnapshots");
7834
+ }
7835
+ appendSprintMetrics(snapshot) {
7836
+ return this.invoke("appendSprintMetrics", [snapshot]);
7837
+ }
7838
+ readSprintMetrics() {
7839
+ return this.invoke("readSprintMetrics");
7840
+ }
7841
+ // --- Sprints / Cycles ---
7842
+ readSprints() {
7843
+ return this.invoke("readSprints");
7844
+ }
7845
+ createSprint(sprint) {
7846
+ return this.invoke("createSprint", [sprint]);
7847
+ }
7848
+ readCycles() {
7849
+ return this.invoke("readCycles");
7850
+ }
7851
+ createCycle(cycle) {
7852
+ return this.invoke("createCycle", [cycle]);
7853
+ }
7854
+ getContextHashes(sprintNumber) {
7855
+ return this.invoke("getContextHashes", [sprintNumber]);
7856
+ }
7857
+ // --- Registries ---
7858
+ readRegistries() {
7859
+ return this.invoke("readRegistries");
7860
+ }
7861
+ updateRegistries(registries) {
7862
+ return this.invoke("updateRegistries", [registries]);
7863
+ }
7864
+ // --- Recommendations ---
7865
+ writeRecommendation(rec) {
7866
+ return this.invoke("writeRecommendation", [rec]);
7867
+ }
7868
+ getPendingRecommendations() {
7869
+ return this.invoke("getPendingRecommendations");
7870
+ }
7871
+ actionRecommendation(id, sprintNumber) {
7872
+ return this.invoke("actionRecommendation", [id, sprintNumber]);
7873
+ }
7874
+ // --- Decision Events & Scores ---
7875
+ appendDecisionEvent(event) {
7876
+ return this.invoke("appendDecisionEvent", [event]);
7877
+ }
7878
+ getDecisionEvents(decisionId, limit) {
7879
+ return this.invoke("getDecisionEvents", [decisionId, limit]);
7880
+ }
7881
+ getDecisionEventsSince(sprint) {
7882
+ return this.invoke("getDecisionEventsSince", [sprint]);
7883
+ }
7884
+ writeDecisionScore(score) {
7885
+ return this.invoke("writeDecisionScore", [score]);
7886
+ }
7887
+ getDecisionScores(decisionId) {
7888
+ return this.invoke("getDecisionScores", [decisionId]);
7889
+ }
7890
+ getLatestDecisionScores() {
7891
+ return this.invoke("getLatestDecisionScores");
7892
+ }
7893
+ logEntityReferences(refs) {
7894
+ return this.invoke("logEntityReferences", [refs]);
7895
+ }
7896
+ getDecisionUsage(currentSprint) {
7897
+ return this.invoke("getDecisionUsage", [currentSprint]);
7898
+ }
7899
+ getContextUtilisation() {
7900
+ return this.invoke("getContextUtilisation");
7901
+ }
7902
+ // --- Strategy Reviews ---
7903
+ writeStrategyReview(review) {
7904
+ return this.invoke("writeStrategyReview", [review]);
7905
+ }
7906
+ getLastStrategyReviewSprint() {
7907
+ return this.invoke("getLastStrategyReviewSprint");
7908
+ }
7909
+ getStrategyReviews(limit, includeFullAnalysis) {
7910
+ return this.invoke("getStrategyReviews", [limit, includeFullAnalysis]);
7911
+ }
7912
+ // --- Dogfood ---
7913
+ writeDogfoodEntries(entries) {
7914
+ return this.invoke("writeDogfoodEntries", [entries]);
7915
+ }
7916
+ getDogfoodLog(limit) {
7917
+ return this.invoke("getDogfoodLog", [limit]);
7918
+ }
7919
+ getUnactionedDogfoodEntries(limit) {
7920
+ return this.invoke("getUnactionedDogfoodEntries", [limit]);
7921
+ }
7922
+ updateDogfoodEntryStatus(id, status, linkedTaskId) {
7923
+ return this.invoke("updateDogfoodEntryStatus", [id, status, linkedTaskId]);
7924
+ }
7925
+ // --- North Star ---
7926
+ getCurrentNorthStar() {
7927
+ return this.invoke("getCurrentNorthStar");
7928
+ }
7929
+ getNorthStarSetCycle() {
7930
+ return this.invoke("getNorthStarSetCycle");
7931
+ }
7932
+ // --- Optional pg-only methods ---
7933
+ getEstimationCalibration() {
7934
+ return this.invoke("getEstimationCalibration");
7935
+ }
7936
+ getRecentTaskComments(limit) {
7937
+ return this.invoke("getRecentTaskComments", [limit]);
7938
+ }
7939
+ getRecommendationEffectiveness() {
7940
+ return this.invoke("getRecommendationEffectiveness");
7941
+ }
7942
+ getPlanContextSummary() {
7943
+ return this.invoke("getPlanContextSummary");
7944
+ }
7945
+ projectExists() {
7946
+ return this.invoke("projectExists");
7947
+ }
7948
+ // --- Atomic plan write-back ---
7949
+ planWriteBack(payload) {
7950
+ return this.invoke("planWriteBack", [payload]);
7951
+ }
7952
+ };
7953
+ }
7954
+ });
7955
+
6857
7956
  // src/index.ts
6858
7957
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6859
7958
 
@@ -6872,7 +7971,10 @@ function loadConfig() {
6872
7971
  const baseBranch = process.env.PAPI_BASE_BRANCH ?? "main";
6873
7972
  const autoPR = process.env.PAPI_AUTO_PR !== "false";
6874
7973
  const papiEndpoint = process.env.PAPI_ENDPOINT;
6875
- const adapterType = papiEndpoint ? "pg" : process.env.PAPI_ADAPTER ?? "md";
7974
+ const dataEndpoint = process.env.PAPI_DATA_ENDPOINT;
7975
+ const databaseUrl = process.env.DATABASE_URL;
7976
+ const explicitAdapter = process.env.PAPI_ADAPTER;
7977
+ const adapterType = papiEndpoint ? "pg" : databaseUrl && explicitAdapter === "pg" ? "pg" : dataEndpoint ? "proxy" : explicitAdapter ? explicitAdapter : databaseUrl ? "pg" : "proxy";
6876
7978
  return {
6877
7979
  projectRoot,
6878
7980
  papiDir: path.join(projectRoot, ".papi"),
@@ -6888,6 +7990,8 @@ function loadConfig() {
6888
7990
  // src/adapter-factory.ts
6889
7991
  init_dist2();
6890
7992
  import path2 from "path";
7993
+ var HOSTED_PROXY_ENDPOINT = "https://guewgygcpcmrcoppihzx.supabase.co/functions/v1/data-proxy";
7994
+ var HOSTED_PROXY_KEY = "e9891a0a2225ac376f88ebdad78b4814b52ce0a39a41c5ec";
6891
7995
  var PLACEHOLDER_PATTERNS = [
6892
7996
  "<YOUR_DATABASE_URL>",
6893
7997
  "your-database-url",
@@ -6933,6 +8037,11 @@ async function createAdapter(optionsOrType, maybePapiDir) {
6933
8037
  }
6934
8038
  const config2 = papiEndpoint ? { connectionString: papiEndpoint } : configFromEnv2();
6935
8039
  validateDatabaseUrl(config2.connectionString);
8040
+ try {
8041
+ const { ensureSchema: ensureSchema2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
8042
+ await ensureSchema2(config2);
8043
+ } catch {
8044
+ }
6936
8045
  try {
6937
8046
  const pgAdapter = new PgAdapter2(config2);
6938
8047
  const existing = await pgAdapter.getProject(projectId);
@@ -6960,10 +8069,36 @@ async function createAdapter(optionsOrType, maybePapiDir) {
6960
8069
  }
6961
8070
  return adapter2;
6962
8071
  }
8072
+ case "proxy": {
8073
+ const { ProxyPapiAdapter: ProxyPapiAdapter2 } = await Promise.resolve().then(() => (init_proxy_adapter(), proxy_adapter_exports));
8074
+ const projectId = process.env["PAPI_PROJECT_ID"];
8075
+ if (!projectId) {
8076
+ throw new Error(
8077
+ "PAPI_PROJECT_ID is required. Generate a UUID (run `uuidgen` in terminal) and set it in your .mcp.json env config."
8078
+ );
8079
+ }
8080
+ const dataEndpoint = process.env["PAPI_DATA_ENDPOINT"] || HOSTED_PROXY_ENDPOINT;
8081
+ const dataApiKey = process.env["PAPI_DATA_API_KEY"] || HOSTED_PROXY_KEY;
8082
+ const adapter2 = new ProxyPapiAdapter2({
8083
+ endpoint: dataEndpoint,
8084
+ apiKey: dataApiKey,
8085
+ projectId
8086
+ });
8087
+ const connected = await adapter2.probeConnection();
8088
+ if (connected) {
8089
+ _connectionStatus = "connected";
8090
+ console.error("[papi] \u2713 Data proxy connected");
8091
+ } else {
8092
+ _connectionStatus = "degraded";
8093
+ console.error("[papi] \u2717 Data proxy unreachable \u2014 running in degraded mode");
8094
+ console.error("[papi] Check your PAPI_DATA_ENDPOINT configuration.");
8095
+ }
8096
+ return adapter2;
8097
+ }
6963
8098
  default: {
6964
8099
  const _exhaustive = adapterType;
6965
8100
  throw new Error(
6966
- `Unknown PAPI_ADAPTER value: "${_exhaustive}". Valid options: "md", "pg".`
8101
+ `Unknown PAPI_ADAPTER value: "${_exhaustive}". Valid options: "md", "pg", "proxy".`
6967
8102
  );
6968
8103
  }
6969
8104
  }
@@ -7981,6 +9116,7 @@ Standard planning sprint with full board review.
7981
9116
  - **Surprises:** If a surprise reveals a gap (e.g. "schema assumed but not verified"), propose a task to close it.
7982
9117
  - **Architecture Notes:** If a pattern was established that needs follow-up (e.g. "shared service layer created, MCP migration needed"), propose the follow-up.
7983
9118
  - **Strategy gaps:** If an Active Decision has no board tasks supporting it, propose one.
9119
+ - **Dogfood observations:** If unactioned dogfood entries are listed in context (with IDs), check if any map to existing tasks. If not, propose a new task. Include \`dogfood:ID\` in the new task's notes so the pipeline can link them.
7984
9120
  Create new tasks via the \`newTasks\` array in Part 2. Use \`new-N\` IDs in \`cycleHandoffs\` to reference them. **Limit: 3 new tasks per sprint** to prevent backlog bloat.
7985
9121
  **\u26A0\uFE0F DUPLICATE CHECK:** Before adding a task to \`newTasks\`, scan the Sprint Board above for any existing task with the same or very similar title/scope. If a matching task already exists (even with slightly different wording), do NOT create a duplicate \u2014 reference the existing task ID instead. The board already contains all active tasks; re-creating them wastes IDs and bloats the board.
7986
9122
  **\u26A0\uFE0F ALREADY-BUILT CHECK:** Before creating a task, check the recent build reports and sprint log for evidence that this capability was already shipped. If a recent build report shows this feature was completed (even under a different task name), do NOT create a new task for it. This is especially important for UI features, data models, and integrations that may already exist.
@@ -8280,8 +9416,8 @@ After your natural language output, include this EXACT format on its own line:
8280
9416
  "activeDecisionUpdates": [
8281
9417
  {
8282
9418
  "id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
8283
- "action": "confidence_change | modify | resolve | supersede | new",
8284
- "body": "string \u2014 full AD block including ### heading, confidence tag, and body text"
9419
+ "action": "confidence_change | modify | resolve | supersede | new | delete",
9420
+ "body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)"
8285
9421
  }
8286
9422
  ],
8287
9423
  "decisionScores": [
@@ -8346,7 +9482,7 @@ Everything in Part 1 (natural language) is **display-only**. Part 2 (structured
8346
9482
 
8347
9483
  **If you analysed it in Part 1, it MUST appear in Part 2 to persist. Empty arrays/null = nothing saved.**
8348
9484
 
8349
- - Recommended AD changes in Part 1? \u2192 Put them in \`activeDecisionUpdates\` with full body including ### heading
9485
+ - Recommended AD changes in Part 1? \u2192 Put them in \`activeDecisionUpdates\` with full body including ### heading. Use \`delete\` action (with empty body) to permanently remove non-strategic ADs (implementation details, resolved decisions, library choices). Use \`supersede\` when a decision is replaced by a new one.
8350
9486
  - Scored ADs in Part 1? \u2192 Put scores in \`decisionScores\` array with id, dimensions, and rationale
8351
9487
  - Identified proven insights, direction changes, or deprecated approaches? \u2192 Put the full updated product brief in \`productBriefUpdates\`. This is how strategic learnings get locked into the project's institutional memory. Common triggers: a phase completing, a hypothesis being validated/invalidated, a new constraint emerging, or the North Star evolving.${compressionPersistence}
8352
9488
  - Wrote a strategy review in Part 1? \u2192 \`sessionLogTitle\`, \`sessionLogContent\`, \`velocityAssessment\`, \`strategicRecommendations\` must all be populated
@@ -8480,8 +9616,8 @@ After your natural language output, include this EXACT format on its own line:
8480
9616
  "activeDecisionUpdates": [
8481
9617
  {
8482
9618
  "id": "string \u2014 AD-N (existing) or new AD-N (for new decisions)",
8483
- "action": "confidence_change | modify | resolve | supersede | new",
8484
- "body": "string \u2014 full AD block including ### heading, confidence tag, and body text"
9619
+ "action": "confidence_change | modify | resolve | supersede | new | delete",
9620
+ "body": "string \u2014 full AD block including ### heading, confidence tag, and body text (empty string for delete)"
8485
9621
  }
8486
9622
  ],
8487
9623
  "phaseUpdates": [
@@ -8794,6 +9930,60 @@ ${inputs.codebaseContext}
8794
9930
  Return a JSON array of 3-10 tasks based on gaps, improvements, and next steps visible from the codebase analysis above.`;
8795
9931
  }
8796
9932
 
9933
+ // src/lib/prompt-registry.ts
9934
+ var LOCAL_PROMPTS = {
9935
+ "plan-system": PLAN_SYSTEM,
9936
+ "plan-bootstrap": PLAN_BOOTSTRAP_INSTRUCTIONS,
9937
+ "plan-full": PLAN_FULL_INSTRUCTIONS,
9938
+ "review-system": buildReviewSystemPrompt(),
9939
+ "strategy-change-system": STRATEGY_CHANGE_SYSTEM,
9940
+ "handoff-regen-system": HANDOFF_REGEN_SYSTEM
9941
+ };
9942
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
9943
+ var cache = /* @__PURE__ */ new Map();
9944
+ function getEndpoint() {
9945
+ return process.env.PAPI_PROMPT_ENDPOINT;
9946
+ }
9947
+ function getApiKey() {
9948
+ return process.env.PAPI_PROMPT_API_KEY;
9949
+ }
9950
+ async function getPrompt(name) {
9951
+ const endpoint = getEndpoint();
9952
+ if (!endpoint) {
9953
+ return LOCAL_PROMPTS[name];
9954
+ }
9955
+ const cached = cache.get(name);
9956
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
9957
+ return cached.content;
9958
+ }
9959
+ try {
9960
+ const url = endpoint.endsWith("/") ? `${endpoint}${name}` : `${endpoint}/${name}`;
9961
+ const apiKey = getApiKey();
9962
+ const headers = {
9963
+ "Accept": "application/json"
9964
+ };
9965
+ if (apiKey) {
9966
+ headers["Authorization"] = `Bearer ${apiKey}`;
9967
+ }
9968
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(1e4) });
9969
+ if (!response.ok) {
9970
+ if (cached) return cached.content;
9971
+ return LOCAL_PROMPTS[name];
9972
+ }
9973
+ const data = await response.json();
9974
+ const content = data.content;
9975
+ if (typeof content !== "string" || content.length === 0) {
9976
+ if (cached) return cached.content;
9977
+ return LOCAL_PROMPTS[name];
9978
+ }
9979
+ cache.set(name, { content, fetchedAt: Date.now() });
9980
+ return content;
9981
+ } catch {
9982
+ if (cached) return cached.content;
9983
+ return LOCAL_PROMPTS[name];
9984
+ }
9985
+ }
9986
+
8797
9987
  // src/services/plan.ts
8798
9988
  function determineMode(totalSprints) {
8799
9989
  if (totalSprints === 0) return "bootstrap";
@@ -9007,6 +10197,15 @@ function applyContextDiff(ctx, prevHashes) {
9007
10197
  return { ctx, newHashes, savedBytes };
9008
10198
  }
9009
10199
  async function readDogfoodEntries(projectRoot, count = 5, adapter2) {
10200
+ if (adapter2?.getUnactionedDogfoodEntries) {
10201
+ try {
10202
+ const entries = await adapter2.getUnactionedDogfoodEntries(count);
10203
+ if (entries.length > 0) {
10204
+ return "**Unactioned observations (status: observed):**\n" + entries.map((e) => `- **[${e.category}]** (Cycle ${e.cycleNumber}, id: ${e.id}) ${e.content}`).join("\n");
10205
+ }
10206
+ } catch {
10207
+ }
10208
+ }
9010
10209
  if (adapter2?.getDogfoodLog) {
9011
10210
  try {
9012
10211
  const entries = await adapter2.getDogfoodLog(count);
@@ -9462,6 +10661,15 @@ ${cleanContent}`;
9462
10661
  notes: task.notes || ""
9463
10662
  });
9464
10663
  newTaskIdMap.set(`new-${i + 1}`, created.id);
10664
+ if (adapter2.updateDogfoodEntryStatus && task.notes) {
10665
+ const dogfoodRefs = task.notes.match(/dogfood:([a-f0-9-]+)/gi);
10666
+ if (dogfoodRefs) {
10667
+ for (const ref of dogfoodRefs) {
10668
+ const entryId = ref.split(":")[1];
10669
+ await adapter2.updateDogfoodEntryStatus(entryId, "backlog-created", created.id);
10670
+ }
10671
+ }
10672
+ }
9465
10673
  }
9466
10674
  })();
9467
10675
  const priorityLockNotes = [];
@@ -9769,10 +10977,11 @@ async function preparePlan(adapter2, config2, filters, focus, force) {
9769
10977
  }
9770
10978
  } catch {
9771
10979
  }
10980
+ const planSystemPrompt = await getPrompt("plan-system");
9772
10981
  return {
9773
10982
  mode,
9774
10983
  cycleNumber: sprintNumber,
9775
- systemPrompt: PLAN_SYSTEM,
10984
+ systemPrompt: planSystemPrompt,
9776
10985
  userMessage,
9777
10986
  strategyReviewWarning,
9778
10987
  contextBytes,
@@ -10408,14 +11617,14 @@ async function assembleContext2(adapter2, sprintNumber, sprintsSinceLastReview,
10408
11617
  northStar: currentNorthStar ?? void 0,
10409
11618
  recommendationEffectiveness: recEffectivenessText
10410
11619
  };
10411
- const BUDGET_SOFT = 8e4;
10412
- const BUDGET_HARD = 1e5;
11620
+ const BUDGET_SOFT2 = 8e4;
11621
+ const BUDGET_HARD2 = 1e5;
10413
11622
  const compressionSteps = [];
10414
11623
  function measureContext(ctx) {
10415
11624
  return Object.values(ctx).filter((v) => typeof v === "string").reduce((sum, s) => sum + s.length, 0);
10416
11625
  }
10417
11626
  let contextSize = measureContext(context);
10418
- if (contextSize > BUDGET_SOFT && context.previousReviews) {
11627
+ if (contextSize > BUDGET_SOFT2 && context.previousReviews) {
10419
11628
  const reviews2 = previousStrategyReviews;
10420
11629
  if (reviews2.length > 1) {
10421
11630
  const firstFull = formatPreviousReviews([reviews2[0]]) ?? "";
@@ -10429,7 +11638,7 @@ async function assembleContext2(adapter2, sprintNumber, sprintsSinceLastReview,
10429
11638
  compressionSteps.push(`Step 1: previous reviews capped (1 full + ${reviews2.length - 1} headers)`);
10430
11639
  }
10431
11640
  }
10432
- if (contextSize > BUDGET_SOFT && context.humanReviews) {
11641
+ if (contextSize > BUDGET_SOFT2 && context.humanReviews) {
10433
11642
  const trimmed = formatReviews(reviews.slice(0, 5));
10434
11643
  if (trimmed.length < context.humanReviews.length) {
10435
11644
  context.humanReviews = trimmed;
@@ -10437,7 +11646,7 @@ async function assembleContext2(adapter2, sprintNumber, sprintsSinceLastReview,
10437
11646
  compressionSteps.push("Step 2: human reviews capped to 5");
10438
11647
  }
10439
11648
  }
10440
- if (contextSize > BUDGET_SOFT && context.allBuildReports) {
11649
+ if (contextSize > BUDGET_SOFT2 && context.allBuildReports) {
10441
11650
  const summary = formatRecentReportsSummary(reports, 10);
10442
11651
  if (summary.length < context.allBuildReports.length) {
10443
11652
  context.allBuildReports = summary;
@@ -10445,11 +11654,11 @@ async function assembleContext2(adapter2, sprintNumber, sprintsSinceLastReview,
10445
11654
  compressionSteps.push("Step 3: build reports summarized");
10446
11655
  }
10447
11656
  }
10448
- if (contextSize > BUDGET_HARD) {
11657
+ if (contextSize > BUDGET_HARD2) {
10449
11658
  const entries = Object.entries(context).filter((e) => typeof e[1] === "string").sort((a, b2) => b2[1].length - a[1].length);
10450
11659
  if (entries.length > 0) {
10451
11660
  const [key, value] = entries[0];
10452
- const excess = contextSize - BUDGET_SOFT;
11661
+ const excess = contextSize - BUDGET_SOFT2;
10453
11662
  const newLength = Math.max(value.length - excess, 1e3);
10454
11663
  context[key] = value.slice(0, newLength) + "\n\n[truncated \u2014 context budget exceeded]";
10455
11664
  contextSize = measureContext(context);
@@ -10506,8 +11715,12 @@ ${cleanContent}`;
10506
11715
  }
10507
11716
  if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
10508
11717
  for (const ad of data.activeDecisionUpdates) {
10509
- await adapter2.updateActiveDecision(ad.id, ad.body, sprintNumber);
10510
- const eventType = ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
11718
+ if (ad.action === "delete" && adapter2.deleteActiveDecision) {
11719
+ await adapter2.deleteActiveDecision(ad.id);
11720
+ } else {
11721
+ await adapter2.updateActiveDecision(ad.id, ad.body, sprintNumber);
11722
+ }
11723
+ const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
10511
11724
  try {
10512
11725
  await adapter2.appendDecisionEvent({
10513
11726
  decisionId: ad.id,
@@ -10939,8 +12152,12 @@ ${cleanContent}`;
10939
12152
  });
10940
12153
  if (data.activeDecisionUpdates && data.activeDecisionUpdates.length > 0) {
10941
12154
  for (const ad of data.activeDecisionUpdates) {
10942
- await adapter2.updateActiveDecision(ad.id, ad.body, sprintNumber);
10943
- const eventType = ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
12155
+ if (ad.action === "delete" && adapter2.deleteActiveDecision) {
12156
+ await adapter2.deleteActiveDecision(ad.id);
12157
+ } else {
12158
+ await adapter2.updateActiveDecision(ad.id, ad.body, sprintNumber);
12159
+ }
12160
+ const eventType = ad.action === "delete" ? "deleted" : ad.action === "confidence_change" ? "confidence_changed" : ad.action === "supersede" ? "superseded" : ad.action === "new" ? "created" : "modified";
10944
12161
  try {
10945
12162
  await adapter2.appendDecisionEvent({
10946
12163
  decisionId: ad.id,
@@ -11005,9 +12222,10 @@ async function prepareStrategyChange(adapter2, text) {
11005
12222
  const message = err instanceof Error ? err.message : String(err);
11006
12223
  throw new Error(`reading project state: ${message}`);
11007
12224
  }
12225
+ const strategyChangePrompt = await getPrompt("strategy-change-system");
11008
12226
  return {
11009
12227
  sprintNumber,
11010
- systemPrompt: STRATEGY_CHANGE_SYSTEM,
12228
+ systemPrompt: strategyChangePrompt,
11011
12229
  userMessage: buildStrategyChangeUserMessage(sprintNumber, text, productBrief, activeDecisions, phases, tasks, buildReports, previousReviewsText)
11012
12230
  };
11013
12231
  }
@@ -11426,25 +12644,34 @@ var boardViewTool = {
11426
12644
  };
11427
12645
  var boardDeprioritiseTool = {
11428
12646
  name: "board_deprioritise",
11429
- description: "Deprioritise a task \u2014 resets status to Backlog (preserving its BUILD HANDOFF) or Deferred if defer=true (clears handoff). Optionally sets a new priority or phase. Deferred tasks are excluded from plan context and default board views. Does not call the Anthropic API.",
12647
+ description: `Remove a task from the current cycle. Three actions: "backlog" (not now, maybe later \u2014 preserves handoff), "defer" (valid but premature \u2014 hidden from 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.`,
11430
12648
  inputSchema: {
11431
12649
  type: "object",
11432
12650
  properties: {
11433
12651
  task_id: {
11434
12652
  type: "string",
11435
- description: "The task ID to deprioritise."
12653
+ description: "The task ID to act on."
12654
+ },
12655
+ action: {
12656
+ type: "string",
12657
+ enum: ["backlog", "defer", "cancel"],
12658
+ description: `"backlog" = not now, maybe later (preserves handoff). "defer" = valid but premature (hidden from planner). "cancel" = don't want this at all (permanently closed). If omitted, defaults to "backlog" for backwards compatibility.`
12659
+ },
12660
+ reason: {
12661
+ type: "string",
12662
+ description: 'Why this task is being removed. Required for "cancel", recommended for "defer".'
11436
12663
  },
11437
12664
  defer: {
11438
12665
  type: "boolean",
11439
- description: "If true, sets status to Deferred instead of Backlog. Deferred tasks are invisible to plan and default board_view."
12666
+ description: 'DEPRECATED \u2014 use action instead. If true, equivalent to action="defer".'
11440
12667
  },
11441
12668
  priority: {
11442
12669
  type: "string",
11443
- description: 'Optional new priority for the deprioritised task (e.g. "P3 Low").'
12670
+ description: 'Optional new priority (only applies to "backlog" action).'
11444
12671
  },
11445
12672
  phase: {
11446
12673
  type: "string",
11447
- description: "Optional new phase for the deprioritised task."
12674
+ description: 'Optional new phase (only applies to "backlog" and "defer" actions).'
11448
12675
  }
11449
12676
  },
11450
12677
  required: ["task_id"]
@@ -11543,19 +12770,43 @@ async function handleBoardDeprioritise(adapter2, args) {
11543
12770
  if (!taskId) {
11544
12771
  return errorResponse("task_id is required for deprioritise.");
11545
12772
  }
11546
- const defer = args.defer;
12773
+ const explicitAction = args.action;
12774
+ const legacyDefer = args.defer;
12775
+ const action = explicitAction ?? (legacyDefer ? "defer" : "backlog");
12776
+ const reason = args.reason;
11547
12777
  const newPriority = args.priority;
11548
12778
  const newPhase = args.phase;
12779
+ if (action === "cancel") {
12780
+ if (!reason) {
12781
+ return errorResponse("reason is required when cancelling a task.");
12782
+ }
12783
+ try {
12784
+ const task = await adapter2.getTask(taskId);
12785
+ if (!task) return errorResponse(`Task ${taskId} not found.`);
12786
+ await adapter2.updateTask(taskId, {
12787
+ status: "Cancelled",
12788
+ buildHandoff: void 0,
12789
+ closureReason: reason
12790
+ });
12791
+ return textResponse(`Cancelled **${taskId}** (${task.title}).
12792
+
12793
+ Reason: ${reason}`);
12794
+ } catch (err) {
12795
+ return errorResponse(err instanceof Error ? err.message : String(err));
12796
+ }
12797
+ }
11549
12798
  try {
11550
12799
  const { task, changes } = await deprioritiseTask(adapter2, taskId, {
11551
- defer,
12800
+ defer: action === "defer",
11552
12801
  priority: newPriority,
11553
12802
  phase: newPhase
11554
12803
  });
11555
- const action = defer ? "Deferred" : "Deprioritised";
11556
- return textResponse(`${action} **${taskId}** (${task.title}).
12804
+ const label = action === "defer" ? "Deferred" : "Moved to Backlog";
12805
+ const reasonNote = reason ? `
12806
+ Reason: ${reason}` : "";
12807
+ return textResponse(`${label} **${taskId}** (${task.title}).
11557
12808
 
11558
- Changes: ${changes.join(", ")}.`);
12809
+ Changes: ${changes.join(", ")}.${reasonNote}`);
11559
12810
  } catch (err) {
11560
12811
  return errorResponse(err instanceof Error ? err.message : String(err));
11561
12812
  }
@@ -11946,51 +13197,62 @@ function substitute(template, vars) {
11946
13197
  return result;
11947
13198
  }
11948
13199
  async function scaffoldPapiDir(adapter2, config2, input) {
11949
- if (config2.adapterType === "pg") {
11950
- return false;
11951
- }
11952
- try {
11953
- await access2(config2.papiDir);
11954
- return false;
11955
- } catch {
11956
- await mkdir(config2.papiDir, { recursive: true });
11957
- const vars = { project_name: input.projectName, description: input.description };
11958
- for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
11959
- const content = substitute(template, vars);
11960
- await writeFile2(join2(config2.papiDir, filename), content, "utf-8");
11961
- }
11962
- const commandsDir = join2(config2.projectRoot, ".claude", "commands");
11963
- const docsDir = join2(config2.projectRoot, "docs");
11964
- await mkdir(commandsDir, { recursive: true });
11965
- await mkdir(docsDir, { recursive: true });
11966
- const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
11967
- let claudeMdExists = false;
13200
+ const vars = { project_name: input.projectName, description: input.description };
13201
+ const isPg = config2.adapterType === "pg";
13202
+ if (!isPg) {
11968
13203
  try {
11969
- await access2(claudeMdPath);
11970
- claudeMdExists = true;
13204
+ await access2(config2.papiDir);
13205
+ return false;
11971
13206
  } catch {
13207
+ await mkdir(config2.papiDir, { recursive: true });
13208
+ for (const [filename, template] of Object.entries(FILE_TEMPLATES)) {
13209
+ const content = substitute(template, vars);
13210
+ await writeFile2(join2(config2.papiDir, filename), content, "utf-8");
13211
+ }
11972
13212
  }
11973
- const docsIndexPath = join2(docsDir, "INDEX.md");
11974
- let docsIndexExists = false;
13213
+ }
13214
+ const commandsDir = join2(config2.projectRoot, ".claude", "commands");
13215
+ const docsDir = join2(config2.projectRoot, "docs");
13216
+ await mkdir(commandsDir, { recursive: true });
13217
+ await mkdir(docsDir, { recursive: true });
13218
+ const claudeMdPath = join2(config2.projectRoot, "CLAUDE.md");
13219
+ let claudeMdExists = false;
13220
+ try {
13221
+ await access2(claudeMdPath);
13222
+ claudeMdExists = true;
13223
+ } catch {
13224
+ }
13225
+ const docsIndexPath = join2(docsDir, "INDEX.md");
13226
+ let docsIndexExists = false;
13227
+ try {
13228
+ await access2(docsIndexPath);
13229
+ docsIndexExists = true;
13230
+ } catch {
13231
+ }
13232
+ const scaffoldFiles = {
13233
+ [join2(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
13234
+ [join2(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
13235
+ [join2(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
13236
+ };
13237
+ if (!docsIndexExists) {
13238
+ scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
13239
+ }
13240
+ if (!claudeMdExists) {
13241
+ scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_TEMPLATE, vars);
13242
+ } else {
11975
13243
  try {
11976
- await access2(docsIndexPath);
11977
- docsIndexExists = true;
13244
+ const existing = await readFile3(claudeMdPath, "utf-8");
13245
+ if (!existing.includes("## Workflow Sequences") && !existing.includes("### The Cycle (main flow)")) {
13246
+ const papiSection = "\n\n" + substitute(CLAUDE_MD_TEMPLATE, vars).split("\n").slice(1).join("\n");
13247
+ scaffoldFiles[claudeMdPath] = existing + papiSection;
13248
+ }
11978
13249
  } catch {
11979
13250
  }
11980
- const scaffoldFiles = {
11981
- [join2(commandsDir, "papi-audit.md")]: PAPI_AUDIT_COMMAND_TEMPLATE,
11982
- [join2(commandsDir, "test.md")]: TEST_COMMAND_TEMPLATE,
11983
- [join2(docsDir, "README.md")]: substitute(DOCS_README_TEMPLATE, vars)
11984
- };
11985
- if (!docsIndexExists) {
11986
- scaffoldFiles[docsIndexPath] = substitute(DOCS_INDEX_TEMPLATE, vars);
11987
- }
11988
- if (!claudeMdExists) {
11989
- scaffoldFiles[claudeMdPath] = substitute(CLAUDE_MD_TEMPLATE, vars);
11990
- }
11991
- for (const [filepath, content] of Object.entries(scaffoldFiles)) {
11992
- await writeFile2(filepath, content, "utf-8");
11993
- }
13251
+ }
13252
+ for (const [filepath, content] of Object.entries(scaffoldFiles)) {
13253
+ await writeFile2(filepath, content, "utf-8");
13254
+ }
13255
+ if (!isPg) {
11994
13256
  await adapter2.writePhases([{
11995
13257
  id: "phase-0",
11996
13258
  slug: "setup",
@@ -11999,9 +13261,9 @@ async function scaffoldPapiDir(adapter2, config2, input) {
11999
13261
  status: "In Progress",
12000
13262
  order: 0
12001
13263
  }]);
12002
- await ensurePapiPermission(config2.projectRoot);
12003
- return true;
12004
13264
  }
13265
+ await ensurePapiPermission(config2.projectRoot);
13266
+ return true;
12005
13267
  }
12006
13268
  var PAPI_PERMISSION = "mcp__papi__*";
12007
13269
  async function ensurePapiPermission(projectRoot) {
@@ -12745,7 +14007,7 @@ async function describeTask(adapter2, taskId) {
12745
14007
  }
12746
14008
  return { task };
12747
14009
  }
12748
- async function startBuild(adapter2, config2, taskId) {
14010
+ async function startBuild(adapter2, config2, taskId, options = {}) {
12749
14011
  const task = await adapter2.getTask(taskId);
12750
14012
  if (!task) {
12751
14013
  throw new Error(`Task "${taskId}" not found on the Sprint Board.`);
@@ -12772,7 +14034,9 @@ async function startBuild(adapter2, config2, taskId) {
12772
14034
  throw err;
12773
14035
  }
12774
14036
  const branchLines = [];
12775
- if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
14037
+ if (options.light) {
14038
+ branchLines.push("Light mode: skipping branch creation \u2014 working on current branch.");
14039
+ } else if (config2.autoCommit && isGitAvailable() && isGitRepo(config2.projectRoot)) {
12776
14040
  const featureBranch = taskBranchName(taskId);
12777
14041
  const currentBranch = getCurrentBranch(config2.projectRoot);
12778
14042
  if (currentBranch === featureBranch) {
@@ -12806,11 +14070,21 @@ async function startBuild(adapter2, config2, taskId) {
12806
14070
  branchLines.push(
12807
14071
  checkout.success ? `Checked out existing branch '${featureBranch}'.` : `Warning: ${checkout.message}`
12808
14072
  );
14073
+ if (checkout.success) {
14074
+ branchLines.push(
14075
+ 'Note: IDE may report files as "modified by linter" after branch switch \u2014 this is normal git checkout behaviour, not reverted changes. Ignore these notifications.'
14076
+ );
14077
+ }
12809
14078
  } else {
12810
14079
  const create = createAndCheckoutBranch(config2.projectRoot, featureBranch);
12811
14080
  branchLines.push(
12812
14081
  create.success ? `Created branch '${featureBranch}'.` : `Warning: ${create.message}`
12813
14082
  );
14083
+ if (create.success) {
14084
+ branchLines.push(
14085
+ 'Note: IDE may report files as "modified by linter" after branch switch \u2014 this is normal git checkout behaviour, not reverted changes. Ignore these notifications.'
14086
+ );
14087
+ }
12814
14088
  }
12815
14089
  }
12816
14090
  }
@@ -12824,7 +14098,7 @@ async function startBuild(adapter2, config2, taskId) {
12824
14098
  }
12825
14099
  return { task, branchLines, phaseChanges };
12826
14100
  }
12827
- async function completeBuild(adapter2, config2, taskId, input) {
14101
+ async function completeBuild(adapter2, config2, taskId, input, options = {}) {
12828
14102
  const task = await adapter2.getTask(taskId);
12829
14103
  if (!task) {
12830
14104
  throw new Error(`Task "${taskId}" not found on the Sprint Board.`);
@@ -12884,9 +14158,13 @@ async function completeBuild(adapter2, config2, taskId, input) {
12884
14158
  const buildReportSummary = `${capitalizeCompleted(input.completed)}. Effort ${input.effort} vs estimated ${input.estimatedEffort}.${surpriseNote}${issueNote}`;
12885
14159
  await adapter2.updateTask(taskId, { buildReport: buildReportSummary });
12886
14160
  if (input.completed === "yes") {
12887
- await adapter2.updateTaskStatus(taskId, "In Review");
14161
+ if (options.light) {
14162
+ await adapter2.updateTaskStatus(taskId, "Done");
14163
+ } else {
14164
+ await adapter2.updateTaskStatus(taskId, "In Review");
14165
+ }
12888
14166
  }
12889
- const statusNote = input.completed === "yes" ? `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}).`;
14167
+ 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}).`;
12890
14168
  let commitLine;
12891
14169
  if (config2.autoCommit) {
12892
14170
  commitLine = autoCommit(config2, taskId, task.title);
@@ -12901,7 +14179,9 @@ async function completeBuild(adapter2, config2, taskId, input) {
12901
14179
  if (changed.length > 0) report.filesChanged = changed;
12902
14180
  }
12903
14181
  let prLines = [];
12904
- if (config2.autoCommit && input.completed === "yes") {
14182
+ if (options.light) {
14183
+ prLines.push("Light mode: skipping push and PR creation.");
14184
+ } else if (config2.autoCommit && input.completed === "yes") {
12905
14185
  prLines = pushAndCreatePR(config2, taskId, task.title);
12906
14186
  }
12907
14187
  const allTasks = await adapter2.queryBoard();
@@ -12976,6 +14256,10 @@ var buildExecuteTool = {
12976
14256
  type: "string",
12977
14257
  description: "The task ID to execute."
12978
14258
  },
14259
+ light: {
14260
+ type: "boolean",
14261
+ description: "Light-ceremony mode for XS/S tasks. Skips feature branch creation and PR. Work stays on current branch. Build report is still captured. Default false."
14262
+ },
12979
14263
  completed: {
12980
14264
  type: "string",
12981
14265
  enum: ["yes", "no", "partial"],
@@ -13130,11 +14414,12 @@ async function handleBuildExecute(adapter2, config2, args) {
13130
14414
  if (!taskId) {
13131
14415
  return errorResponse("task_id is required.");
13132
14416
  }
14417
+ const light = args.light === true;
13133
14418
  if (hasReportFields(args)) {
13134
- return handleExecuteComplete(adapter2, config2, taskId, args);
14419
+ return handleExecuteComplete(adapter2, config2, taskId, args, light);
13135
14420
  }
13136
14421
  try {
13137
- const result = await startBuild(adapter2, config2, taskId);
14422
+ const result = await startBuild(adapter2, config2, taskId, { light });
13138
14423
  const branchInfo = result.branchLines.length > 0 ? result.branchLines.map((l) => `> ${l}`).join("\n") + "\n\n" : "";
13139
14424
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
13140
14425
  const header = `${branchInfo}**Executing ${result.task.id}: ${result.task.title}** \u2014 marked In Progress.
@@ -13167,7 +14452,7 @@ async function handleBuildExecute(adapter2, config2, args) {
13167
14452
  return errorResponse(err instanceof Error ? err.message : String(err));
13168
14453
  }
13169
14454
  }
13170
- async function handleExecuteComplete(adapter2, config2, taskId, args) {
14455
+ async function handleExecuteComplete(adapter2, config2, taskId, args, light = false) {
13171
14456
  const completed = args.completed;
13172
14457
  const effort = args.effort;
13173
14458
  const estimatedEffort = args.estimated_effort;
@@ -13214,7 +14499,7 @@ async function handleExecuteComplete(adapter2, config2, taskId, args) {
13214
14499
  type: bi.type ?? "new",
13215
14500
  detail: bi.detail ?? ""
13216
14501
  }))
13217
- });
14502
+ }, { light });
13218
14503
  return textResponse(formatCompleteResult(result));
13219
14504
  } catch (err) {
13220
14505
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -13366,6 +14651,131 @@ function resolveCurrentPhase(phases) {
13366
14651
  const sorted = [...phases].sort((a, b2) => a.order - b2.order);
13367
14652
  return sorted[0].label;
13368
14653
  }
14654
+ var STOP_WORDS = /* @__PURE__ */ new Set([
14655
+ "a",
14656
+ "an",
14657
+ "the",
14658
+ "and",
14659
+ "or",
14660
+ "but",
14661
+ "in",
14662
+ "on",
14663
+ "at",
14664
+ "to",
14665
+ "for",
14666
+ "of",
14667
+ "with",
14668
+ "by",
14669
+ "from",
14670
+ "is",
14671
+ "are",
14672
+ "was",
14673
+ "were",
14674
+ "be",
14675
+ "been",
14676
+ "has",
14677
+ "have",
14678
+ "had",
14679
+ "do",
14680
+ "does",
14681
+ "did",
14682
+ "will",
14683
+ "would",
14684
+ "could",
14685
+ "should",
14686
+ "may",
14687
+ "might",
14688
+ "can",
14689
+ "not",
14690
+ "no",
14691
+ "if",
14692
+ "then",
14693
+ "than",
14694
+ "that",
14695
+ "this",
14696
+ "it",
14697
+ "its",
14698
+ "all",
14699
+ "each",
14700
+ "every",
14701
+ "both",
14702
+ "as",
14703
+ "so",
14704
+ "up",
14705
+ "out",
14706
+ "about",
14707
+ "into",
14708
+ "over",
14709
+ "after",
14710
+ "before",
14711
+ "between",
14712
+ "under",
14713
+ "above",
14714
+ "such",
14715
+ "only",
14716
+ "also",
14717
+ "just",
14718
+ "more",
14719
+ "most",
14720
+ "other",
14721
+ "some",
14722
+ "any",
14723
+ "new",
14724
+ "when",
14725
+ "how",
14726
+ "what",
14727
+ "which",
14728
+ "who",
14729
+ "add",
14730
+ "create",
14731
+ "build",
14732
+ "implement",
14733
+ "make",
14734
+ "update",
14735
+ "fix",
14736
+ "use",
14737
+ "via",
14738
+ "show",
14739
+ "display",
14740
+ "view",
14741
+ "page",
14742
+ "data",
14743
+ "based",
14744
+ "using"
14745
+ ]);
14746
+ function extractKeywords(text) {
14747
+ return new Set(
14748
+ text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w))
14749
+ );
14750
+ }
14751
+ async function findSimilarTasks(adapter2, ideaTitle) {
14752
+ const ideaKeywords = extractKeywords(ideaTitle);
14753
+ if (ideaKeywords.size < 2) return [];
14754
+ const [backlog, done] = await Promise.all([
14755
+ adapter2.queryBoard({ status: ["Backlog", "In Cycle", "In Progress", "In Review"] }),
14756
+ adapter2.queryBoard({ status: ["Done"] })
14757
+ ]);
14758
+ const allTasks = [...backlog, ...done];
14759
+ const matches = [];
14760
+ for (const task of allTasks) {
14761
+ const taskKeywords = extractKeywords(task.title);
14762
+ if (taskKeywords.size < 2) continue;
14763
+ let covered = 0;
14764
+ for (const word of ideaKeywords) {
14765
+ if (taskKeywords.has(word)) covered++;
14766
+ }
14767
+ const coverage = covered / ideaKeywords.size;
14768
+ if (coverage >= 0.4) {
14769
+ matches.push({
14770
+ id: task.id,
14771
+ title: task.title,
14772
+ status: task.status,
14773
+ coverage
14774
+ });
14775
+ }
14776
+ }
14777
+ return matches.sort((a, b2) => b2.coverage - a.coverage).slice(0, 3);
14778
+ }
13369
14779
  async function captureIdea(adapter2, input) {
13370
14780
  if (input.discovery) {
13371
14781
  const routing = classifyIdea(input.text, input.notes);
@@ -13373,6 +14783,37 @@ async function captureIdea(adapter2, input) {
13373
14783
  return routeToDiscovery(adapter2, routing, input);
13374
14784
  }
13375
14785
  }
14786
+ let similarWarning = "";
14787
+ try {
14788
+ const similar = await findSimilarTasks(adapter2, input.text);
14789
+ if (similar.length > 0) {
14790
+ const highOverlap = similar.filter((s) => s.coverage >= 0.7);
14791
+ if (highOverlap.length > 0 && !input.force) {
14792
+ const lines = highOverlap.map(
14793
+ (s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
14794
+ );
14795
+ const doneMatch = highOverlap.find((s) => s.status === "Done");
14796
+ const reason = doneMatch ? `This looks like it was **already done** as ${doneMatch.id}.` : `This looks like a **duplicate** of ${highOverlap[0].id}.`;
14797
+ return {
14798
+ routing: "task",
14799
+ message: `\u26D4 **Blocked \u2014 ${reason}**
14800
+
14801
+ High-overlap tasks:
14802
+ ${lines.join("\n")}
14803
+
14804
+ If this is genuinely different, re-run with \`force: true\`.`
14805
+ };
14806
+ }
14807
+ const warnLines = similar.map(
14808
+ (s) => ` - **${s.id}** [${s.status}]: "${s.title}" (${Math.round(s.coverage * 100)}% keyword overlap)`
14809
+ );
14810
+ similarWarning = `
14811
+
14812
+ \u26A0\uFE0F **Similar tasks found** \u2014 check before scheduling:
14813
+ ${warnLines.join("\n")}`;
14814
+ }
14815
+ } catch {
14816
+ }
13376
14817
  const [health, phases] = await Promise.all([
13377
14818
  adapter2.getSprintHealth(),
13378
14819
  adapter2.readPhases()
@@ -13396,7 +14837,7 @@ async function captureIdea(adapter2, input) {
13396
14837
  taskType: "idea",
13397
14838
  maturity: "raw"
13398
14839
  });
13399
- return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog` };
14840
+ return { routing: "task", task, message: `${task.id}: "${task.title}" \u2014 added to backlog${similarWarning}` };
13400
14841
  }
13401
14842
  var CANVAS_SECTION_LABELS = {
13402
14843
  landscape: "Landscape References",
@@ -13463,6 +14904,10 @@ var ideaTool = {
13463
14904
  discovery: {
13464
14905
  type: "boolean",
13465
14906
  description: "When true, classify the idea and route to Discovery Canvas instead of backlog. Default: false (always creates a backlog task)."
14907
+ },
14908
+ force: {
14909
+ type: "boolean",
14910
+ description: "Force creation even if a high-overlap duplicate or already-done task is detected. Default: false."
13466
14911
  }
13467
14912
  },
13468
14913
  required: ["text"]
@@ -13486,7 +14931,8 @@ async function handleIdea(adapter2, config2, args) {
13486
14931
  epic: args.epic,
13487
14932
  phase: args.phase,
13488
14933
  notes: rawNotes,
13489
- discovery: args.discovery === true
14934
+ discovery: args.discovery === true,
14935
+ force: args.force === true
13490
14936
  };
13491
14937
  const useGit = isGitAvailable() && isGitRepo(config2.projectRoot);
13492
14938
  const currentBranch = useGit ? getCurrentBranch(config2.projectRoot) : null;
@@ -13860,6 +15306,40 @@ async function prepareReconcile(adapter2) {
13860
15306
  }
13861
15307
  lines.push("");
13862
15308
  }
15309
+ const doneTasks = await adapter2.queryBoard({ status: ["Done"] });
15310
+ const doneOverlaps = [];
15311
+ for (const bt of allTasks) {
15312
+ const bWords = titleKeywords(bt.title);
15313
+ if (bWords.size < 3) continue;
15314
+ for (const dt of doneTasks) {
15315
+ const dWords = titleKeywords(dt.title);
15316
+ if (dWords.size < 3) continue;
15317
+ let covered = 0;
15318
+ for (const w of bWords) {
15319
+ if (dWords.has(w)) covered++;
15320
+ }
15321
+ const coverage = covered / Math.min(bWords.size, dWords.size);
15322
+ if (coverage >= 0.6) {
15323
+ doneOverlaps.push({ backlog: bt, done: dt, coverage });
15324
+ }
15325
+ }
15326
+ }
15327
+ if (doneOverlaps.length > 0) {
15328
+ const bestMatches = /* @__PURE__ */ new Map();
15329
+ for (const { backlog, done, coverage } of doneOverlaps) {
15330
+ const existing = bestMatches.get(backlog.id);
15331
+ if (!existing || coverage > existing.coverage) {
15332
+ bestMatches.set(backlog.id, { done, coverage });
15333
+ }
15334
+ }
15335
+ lines.push("### Already Done? (backlog tasks overlapping with completed work)");
15336
+ lines.push("These backlog tasks may duplicate work already shipped. **Cancel unless the scope is genuinely different.**");
15337
+ for (const [backlogId, { done, coverage }] of bestMatches) {
15338
+ const bt = allTasks.find((t) => t.id === backlogId);
15339
+ lines.push(`- **${bt.id}** "${bt.title.slice(0, 60)}" \u2190 already done as **${done.id}** "${done.title.slice(0, 60)}" (${Math.round(coverage * 100)}% overlap)`);
15340
+ }
15341
+ lines.push("");
15342
+ }
13863
15343
  lines.push("### All Backlog Tasks by Module");
13864
15344
  lines.push("");
13865
15345
  for (const [mod, tasks] of byModule) {
@@ -13873,11 +15353,16 @@ async function prepareReconcile(adapter2) {
13873
15353
  }
13874
15354
  return lines.join("\n");
13875
15355
  }
15356
+ var STOP_WORDS2 = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
15357
+ function tokenize(s) {
15358
+ return s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w));
15359
+ }
15360
+ function titleKeywords(title) {
15361
+ return new Set(tokenize(title));
15362
+ }
13876
15363
  function titleKeywordOverlap(a, b2) {
13877
- const STOP_WORDS = /* @__PURE__ */ new Set(["the", "a", "an", "and", "or", "for", "in", "on", "to", "of", "is", "with", "from", "by", "vs", "not", "no", "do"]);
13878
- const tokenize = (s) => s.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
13879
- const wordsA = new Set(tokenize(a));
13880
- const wordsB = new Set(tokenize(b2));
15364
+ const wordsA = titleKeywords(a);
15365
+ const wordsB = titleKeywords(b2);
13881
15366
  return [...wordsA].filter((w) => wordsB.has(w)).length;
13882
15367
  }
13883
15368
  async function applyReconcile(adapter2, corrections) {
@@ -13938,7 +15423,17 @@ async function applyReconcile(adapter2, corrections) {
13938
15423
  skipped++;
13939
15424
  }
13940
15425
  }
13941
- return { applied, skipped, details };
15426
+ let phaseChanges = [];
15427
+ if (applied > 0) {
15428
+ try {
15429
+ phaseChanges = await propagatePhaseStatus(adapter2);
15430
+ for (const c of phaseChanges) {
15431
+ details.push(`Phase "${c.phaseId}": ${c.oldStatus} \u2192 ${c.newStatus}`);
15432
+ }
15433
+ } catch {
15434
+ }
15435
+ }
15436
+ return { applied, skipped, details, phaseChanges };
13942
15437
  }
13943
15438
 
13944
15439
  // src/tools/board-reconcile.ts
@@ -14555,9 +16050,10 @@ async function listPendingReviews(adapter2) {
14555
16050
  const pendingBuilds = tasks.filter((t) => t.status === "In Review");
14556
16051
  return { pendingBuilds };
14557
16052
  }
14558
- function prepareHandoffRegen(task, reviewerComments) {
16053
+ async function prepareHandoffRegen(task, reviewerComments) {
16054
+ const systemPrompt = await getPrompt("handoff-regen-system");
14559
16055
  return {
14560
- systemPrompt: HANDOFF_REGEN_SYSTEM,
16056
+ systemPrompt,
14561
16057
  userMessage: buildHandoffRegenMessage({
14562
16058
  existingHandoff: task.buildHandoff ? serializeBuildHandoff(task.buildHandoff) : "",
14563
16059
  reviewerComments,
@@ -14610,6 +16106,9 @@ async function submitReview(adapter2, input) {
14610
16106
  review.buildCommitSha = taskReport.commitSha;
14611
16107
  }
14612
16108
  }
16109
+ if (input.autoReview) {
16110
+ review.autoReview = input.autoReview;
16111
+ }
14613
16112
  await adapter2.writeReview(review);
14614
16113
  const newStatus = resolveStatus(input.stage, input.verdict);
14615
16114
  if (newStatus) {
@@ -14635,7 +16134,7 @@ async function submitReview(adapter2, input) {
14635
16134
  const handoffRegenerated = false;
14636
16135
  let handoffRegenPrompt;
14637
16136
  if (input.stage === "handoff-review" && input.verdict === "request-changes" && task.buildHandoff) {
14638
- handoffRegenPrompt = prepareHandoffRegen(task, input.comments);
16137
+ handoffRegenPrompt = await prepareHandoffRegen(task, input.comments);
14639
16138
  }
14640
16139
  const stageLabel = input.stage === "handoff-review" ? "Handoff Review" : "Build Acceptance";
14641
16140
  const slackWarning = void 0;
@@ -14706,6 +16205,29 @@ var reviewSubmitTool = {
14706
16205
  notify: {
14707
16206
  type: "boolean",
14708
16207
  description: "Send Slack notification. Default true. Set false for batch middle reviews to avoid spam."
16208
+ },
16209
+ auto_review: {
16210
+ type: "object",
16211
+ description: "Optional automated code review results to attach to this review. Run PR analysis first, then pass findings here.",
16212
+ properties: {
16213
+ verdict: { type: "string", enum: ["pass", "warn", "fail"], description: "Auto-review verdict." },
16214
+ summary: { type: "string", description: "One-line summary of auto-review findings." },
16215
+ findings: {
16216
+ type: "array",
16217
+ description: "Structured findings from automated review.",
16218
+ items: {
16219
+ type: "object",
16220
+ properties: {
16221
+ severity: { type: "string", enum: ["error", "warning", "info"] },
16222
+ file: { type: "string", description: "File path (optional)." },
16223
+ line: { type: "integer", description: "Line number (optional)." },
16224
+ message: { type: "string", description: "Finding description." }
16225
+ },
16226
+ required: ["severity", "message"]
16227
+ }
16228
+ }
16229
+ },
16230
+ required: ["verdict", "summary", "findings"]
14709
16231
  }
14710
16232
  },
14711
16233
  required: ["task_id", "stage", "verdict", "comments"]
@@ -14784,6 +16306,20 @@ async function handleReviewSubmit(adapter2, config2, args) {
14784
16306
  const comments = args.comments;
14785
16307
  const reviewer = args.reviewer ?? "human";
14786
16308
  const notify = args.notify !== false;
16309
+ const rawAutoReview = args.auto_review;
16310
+ let autoReview;
16311
+ if (rawAutoReview?.verdict && rawAutoReview?.summary && Array.isArray(rawAutoReview?.findings)) {
16312
+ autoReview = {
16313
+ verdict: rawAutoReview.verdict,
16314
+ summary: rawAutoReview.summary,
16315
+ findings: rawAutoReview.findings.map((f) => ({
16316
+ severity: f.severity ?? "info",
16317
+ ...f.file ? { file: f.file } : {},
16318
+ ...f.line ? { line: f.line } : {},
16319
+ message: f.message ?? ""
16320
+ }))
16321
+ };
16322
+ }
14787
16323
  if (!taskId) {
14788
16324
  return errorResponse("task_id is required.");
14789
16325
  }
@@ -14812,7 +16348,7 @@ async function handleReviewSubmit(adapter2, config2, args) {
14812
16348
  try {
14813
16349
  const result = await submitReview(
14814
16350
  adapter2,
14815
- { taskId, stage, verdict, comments, reviewer, notify }
16351
+ { taskId, stage, verdict, comments, reviewer, notify, autoReview }
14816
16352
  );
14817
16353
  const statusNote = result.newStatus ? ` Task status updated to **${result.newStatus}**.` : " Task status unchanged.";
14818
16354
  const unblockNote = result.unblockedTasks.length > 0 ? `
@@ -14879,13 +16415,16 @@ Run \`plan\` to create Cycle ${result.currentSprint + 1}.`;
14879
16415
  }
14880
16416
  }
14881
16417
  const phaseNote = result.phaseChanges.length > 0 ? "\n\n" + result.phaseChanges.map((c) => `Phase auto-updated: ${c.phaseId} ${c.oldStatus} \u2192 ${c.newStatus}`).join("\n") : "";
16418
+ const autoReviewNote = autoReview ? `
16419
+
16420
+ **Auto-Review:** ${autoReview.verdict} \u2014 ${autoReview.summary}` + (autoReview.findings.length > 0 ? ` (${autoReview.findings.length} finding${autoReview.findings.length === 1 ? "" : "s"})` : "") : "";
14882
16421
  return textResponse(
14883
16422
  `**${result.stageLabel}** recorded for ${result.taskId}.
14884
16423
 
14885
16424
  - **Verdict:** ${result.verdict}
14886
16425
  - **Comments:** ${result.comments}
14887
16426
 
14888
- ${statusNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${phaseNote}`
16427
+ ${statusNote}${autoReviewNote}${unblockNote}${regenNote}${mergeNote}${slackNote}${autoReleaseNote}${phaseNote}`
14889
16428
  );
14890
16429
  } catch (err) {
14891
16430
  return errorResponse(err instanceof Error ? err.message : String(err));
@@ -15275,6 +16814,20 @@ async function handleHierarchyUpdate(adapter2, args) {
15275
16814
  }
15276
16815
 
15277
16816
  // src/services/zoom-out.ts
16817
+ var BUDGET_SOFT = 12e4;
16818
+ var BUDGET_HARD = 15e4;
16819
+ function measureSections(secs) {
16820
+ return secs.reduce((sum, s) => sum + s.length, 0);
16821
+ }
16822
+ function summariseBuildReports(reports, count) {
16823
+ const recent = reports.slice(0, count);
16824
+ if (recent.length === 0) return "No recent build reports.";
16825
+ return recent.map((r) => {
16826
+ const effort = `${r.actualEffort} vs ${r.estimatedEffort}`;
16827
+ const surprises = r.surprises && r.surprises !== "None" ? ` \u2014 ${r.surprises.slice(0, 80)}${r.surprises.length > 80 ? "..." : ""}` : "";
16828
+ return `- S${r.sprint} ${r.taskName}: ${effort}${surprises}`;
16829
+ }).join("\n");
16830
+ }
15278
16831
  async function assembleZoomOutContext(adapter2, sprintNumber, projectRoot) {
15279
16832
  const [
15280
16833
  productBrief,
@@ -15288,12 +16841,15 @@ async function assembleZoomOutContext(adapter2, sprintNumber, projectRoot) {
15288
16841
  ] = await Promise.all([
15289
16842
  adapter2.readProductBrief(),
15290
16843
  adapter2.getActiveDecisions(),
15291
- adapter2.getRecentBuildReports(100),
15292
- adapter2.getSprintLog(50),
16844
+ adapter2.getRecentBuildReports(50),
16845
+ // Cap: 50 (was 100)
16846
+ adapter2.getSprintLog(30),
16847
+ // Cap: 30 (was 50)
15293
16848
  adapter2.queryBoard({
15294
16849
  status: ["Backlog", "In Cycle", "In Progress", "In Review", "Blocked", "Deferred"]
15295
16850
  }),
15296
- adapter2.getStrategyReviews(10, true),
16851
+ adapter2.getStrategyReviews(5, true),
16852
+ // Cap: 5 (was 10)
15297
16853
  readDogfoodEntries(projectRoot, 20, adapter2),
15298
16854
  adapter2.getCurrentNorthStar?.() ?? Promise.resolve(null)
15299
16855
  ]);
@@ -15333,7 +16889,8 @@ async function assembleZoomOutContext(adapter2, sprintNumber, projectRoot) {
15333
16889
  }
15334
16890
  let adLifecycleText = "";
15335
16891
  try {
15336
- const events = await adapter2.getDecisionEventsSince(1);
16892
+ const sinceSprintNumber = Math.max(1, sprintNumber - 20);
16893
+ const events = await adapter2.getDecisionEventsSince(sinceSprintNumber);
15337
16894
  if (events.length > 0) {
15338
16895
  const byDecision = /* @__PURE__ */ new Map();
15339
16896
  for (const e of events) {
@@ -15351,14 +16908,30 @@ async function assembleZoomOutContext(adapter2, sprintNumber, projectRoot) {
15351
16908
  }
15352
16909
  let reviewsText = "";
15353
16910
  if (strategyReviews.length > 0) {
15354
- const lines = strategyReviews.map((r) => {
16911
+ const fullReviews = strategyReviews.slice(0, 3).map((r) => {
15355
16912
  const summary = r.fullAnalysis ? r.fullAnalysis.slice(0, 500) + (r.fullAnalysis.length > 500 ? "..." : "") : r.content;
15356
16913
  return `### Cycle ${r.sprintNumber}${r.sprintRange ? ` (${r.sprintRange})` : ""}
15357
16914
  ${summary}`;
15358
16915
  });
15359
- reviewsText = lines.join("\n\n");
16916
+ const headerReviews = strategyReviews.slice(3).map((r) => {
16917
+ const range = r.sprintRange ? `Cycles ${r.sprintRange}` : `Cycle ${r.sprintNumber}`;
16918
+ return `#### ${range} \u2014 ${r.title}
16919
+ **Direction:** ${r.strategicDirection ?? "N/A"}`;
16920
+ });
16921
+ reviewsText = [...fullReviews, ...headerReviews].join("\n\n");
16922
+ }
16923
+ let doneTasksSummary = "";
16924
+ try {
16925
+ const doneTasks = await adapter2.queryBoard({ status: ["Done", "Cancelled"] });
16926
+ if (doneTasks.length > 0) {
16927
+ const doneCount = doneTasks.filter((t) => t.status === "Done").length;
16928
+ const cancelledCount = doneTasks.filter((t) => t.status === "Cancelled").length;
16929
+ doneTasksSummary = `**Completed:** ${doneCount} Done, ${cancelledCount} Cancelled`;
16930
+ }
16931
+ } catch {
15360
16932
  }
15361
16933
  const sections = [];
16934
+ const compressionSteps = [];
15362
16935
  sections.push(`## Project State \u2014 Cycle ${sprintNumber}`);
15363
16936
  if (northStar) sections.push(`**North Star:** ${northStar}`);
15364
16937
  if (productBrief) {
@@ -15371,54 +16944,101 @@ ${summary}`;
15371
16944
  sections.push("## Active Decisions\n" + formatActiveDecisionsForReview(decisions));
15372
16945
  }
15373
16946
  if (adLifecycleText) {
15374
- sections.push("## AD Lifecycle Events (full history)\n" + adLifecycleText);
16947
+ sections.push(`## AD Lifecycle Events (last 20 cycles)
16948
+ ${adLifecycleText}`);
15375
16949
  }
15376
16950
  if (reviewsText) {
15377
16951
  sections.push("## Strategy Review History\n" + reviewsText);
15378
16952
  }
15379
16953
  if (log.length > 0) {
15380
- sections.push("## Sprint Log (recent 50)\n" + formatSprintLog(log));
16954
+ sections.push("## Sprint Log (recent 30)\n" + formatSprintLog(log));
15381
16955
  }
15382
- if (reports.length > 0) {
15383
- sections.push("## Build Reports (recent 100)\n" + formatBuildReports(reports));
16956
+ let buildReportsText = reports.length > 0 ? formatBuildReports(reports) : "";
16957
+ if (buildReportsText) {
16958
+ sections.push("## Build Reports (recent 50)\n" + buildReportsText);
15384
16959
  }
15385
16960
  if (activeTasks.length > 0) {
15386
16961
  sections.push("## Current Board\n" + formatBoardForReview(activeTasks));
15387
16962
  }
16963
+ if (doneTasksSummary) {
16964
+ sections.push("## Historical Task Counts\n" + doneTasksSummary);
16965
+ }
15388
16966
  if (metricsText) {
15389
16967
  sections.push("## Velocity & Estimation Trends\n" + metricsText);
15390
16968
  }
15391
16969
  if (dogfoodLog) {
15392
16970
  sections.push("## Dogfood Log (recent 20)\n" + dogfoodLog);
15393
16971
  }
16972
+ let contextSize = measureSections(sections);
16973
+ if (contextSize > BUDGET_SOFT && buildReportsText) {
16974
+ const summaryText = summariseBuildReports(reports, 30);
16975
+ const idx = sections.findIndex((s) => s.startsWith("## Build Reports"));
16976
+ if (idx >= 0) {
16977
+ sections[idx] = "## Build Reports (summarised, recent 30)\n" + summaryText;
16978
+ contextSize = measureSections(sections);
16979
+ compressionSteps.push("Step 1: build reports summarised");
16980
+ }
16981
+ }
16982
+ if (contextSize > BUDGET_SOFT) {
16983
+ const logIdx = sections.findIndex((s) => s.startsWith("## Sprint Log"));
16984
+ if (logIdx >= 0 && log.length > 15) {
16985
+ sections[logIdx] = "## Sprint Log (recent 15)\n" + formatSprintLog(log.slice(0, 15));
16986
+ contextSize = measureSections(sections);
16987
+ compressionSteps.push("Step 2: sprint log trimmed to 15");
16988
+ }
16989
+ }
16990
+ if (contextSize > BUDGET_HARD) {
16991
+ const longest = sections.map((s, i) => ({ i, len: s.length })).sort((a, b2) => b2.len - a.len)[0];
16992
+ if (longest) {
16993
+ const excess = contextSize - BUDGET_SOFT;
16994
+ const newLen = Math.max(longest.len - excess, 1e3);
16995
+ sections[longest.i] = sections[longest.i].slice(0, newLen) + "\n\n[truncated \u2014 context budget exceeded]";
16996
+ contextSize = measureSections(sections);
16997
+ compressionSteps.push(`Step 3: truncated section ${longest.i} (was ${longest.len} chars)`);
16998
+ }
16999
+ }
17000
+ const estimatedTokens = Math.ceil(contextSize / 4);
17001
+ if (compressionSteps.length > 0) {
17002
+ console.error(`[zoom_out] Context compressed: ${compressionSteps.join(" \u2192 ")}`);
17003
+ }
17004
+ console.error(`[zoom_out] Context size: ${contextSize.toLocaleString()} chars (~${estimatedTokens.toLocaleString()} tokens)`);
15394
17005
  return sections.join("\n\n");
15395
17006
  }
15396
- var ZOOM_OUT_SYSTEM_PROMPT = `You are the PAPI Zoom-Out Analyst \u2014 a meta-retrospective engine that operates above individual strategy reviews.
17007
+ var ZOOM_OUT_SYSTEM_PROMPT = `You are the PAPI Zoom-Out Analyst \u2014 a strategic meta-retrospective engine that operates above individual strategy reviews.
15397
17008
 
15398
- Strategy reviews look at 5-cycle windows. You look at the ENTIRE project history \u2014 every cycle, every decision, every pivot \u2014 and surface patterns that are invisible at the sprint level.
17009
+ Strategy reviews look at 5-cycle windows. You look at the full project arc \u2014 horizons, stages, decisions, pivots \u2014 and surface strategic patterns invisible at the sprint level. Focus on WHERE the project is going, not WHAT it recently did.
15399
17010
 
15400
17011
  IMPORTANT: You are running as a non-interactive API call. Do NOT ask questions or wait for confirmation. Be decisive and specific.
15401
17012
 
15402
17013
  ## OUTPUT FORMAT
15403
17014
 
15404
- Produce a structured retrospective with these sections:
17015
+ Produce a strategic retrospective with these sections:
17016
+
17017
+ ### 1. Strategic Position
17018
+ Where is the project on its horizon/stage map? What stage gates have been passed and which are approaching? What has changed about the product's strategic position since the last zoom-out? How does the current trajectory compare to the North Star? 2-3 paragraphs, not a project history.
17019
+
17020
+ ### 2. Hard Questions
17021
+ 5-7 questions the project must answer to advance to the next stage. These should be questions that strategy reviews are too narrow to surface \u2014 they require the full arc. For each question: state it, explain why it matters now, and what the cost of deferring it is.
15405
17022
 
15406
- ### 1. Project Arc
15407
- Tell the story of this project in 3-5 paragraphs. What started as what? How did the vision evolve? What were the major inflection points? Where is it now? This should read like a concise project history that someone new could understand in 2 minutes.
17023
+ ### 3. Horizon Assessment
17024
+ For each active horizon and stage in the hierarchy: what's the honest completion status? Are stages progressing or stalled? Are phases within stages actually delivering toward the stage goal, or is work scattered? Identify the 1-2 stages that are most at risk.
15408
17025
 
15409
- ### 2. Blind Spots
15410
- What has the project consistently NOT looked at? Patterns of avoidance, areas that keep getting deferred, questions that keep being raised but never answered. These are the things that strategy reviews at the 5-cycle level miss because they lack the full arc.
17026
+ ### 4. Blind Spots & Deferred Patterns
17027
+ What has the project consistently NOT addressed? Patterns of avoidance, questions raised but never answered, tasks deferred 3+ times. These compound invisibly. Focus on strategic blind spots (market, users, competition) not operational ones (code quality, test coverage).
15411
17028
 
15412
- ### 3. Risks
15413
- Long-term risks that compound over time. Technical debt trends, strategic assumptions that haven't been validated, dependencies that are growing, scope patterns that suggest drift. Focus on risks that only become visible when you look at 20+ cycles of history.
17029
+ ### 5. Opportunities
17030
+ Capabilities built but underutilised. Success patterns that could be amplified. Strategic positions that could be strengthened. What would a board advisor say the project should double down on?
15414
17031
 
15415
- ### 4. Opportunities
15416
- What has the project proven that it hasn't capitalised on? Capabilities that were built but underutilised, patterns of success that could be amplified, strategic positions that could be strengthened. What would a board advisor say the project should double down on?
17032
+ ### 6. Active Decision Health
17033
+ Audit every AD provided in context. For each, assess:
17034
+ - **Still relevant?** Does the decision actively guide current work, or is it stale/resolved?
17035
+ - **Correct confidence?** Does the evidence support the stated confidence level?
17036
+ - **Missing ADs?** Are there strategic conclusions from recent cycles that should be captured as new ADs but aren't?
17037
+ - **Wrong scope?** Are any ADs actually implementation details, config preferences, or process choices rather than product/architecture decisions?
15417
17038
 
15418
- ### 5. Weak Spots
15419
- Where is the methodology itself failing? Are strategy reviews catching real issues? Are build reports accurate? Is the planning cycle producing the right tasks? Are decisions being made at the right time? Meta-level critique of how PAPI is being used on this project.
17039
+ Output a table: AD ID | Title | Verdict (Keep/Update/Supersede) | Reason. Then list any recommended new ADs with a one-line rationale.
15420
17040
 
15421
- ### 6. Recommendations
17041
+ ### 7. Recommendations
15422
17042
  3-5 specific, actionable recommendations. Each must include:
15423
17043
  - **What:** The specific action
15424
17044
  - **Why:** What evidence from the full history supports this
@@ -15428,9 +17048,9 @@ Where is the methodology itself failing? Are strategy reviews catching real issu
15428
17048
  ## PRINCIPLES
15429
17049
  - Back every claim with specific cycle numbers, task IDs, or AD references.
15430
17050
  - Compare early project decisions with current reality \u2014 did assumptions hold?
15431
- - Look for oscillation patterns (doing X, then undoing X, then re-doing X).
17051
+ - Focus on stages and horizons, not individual tasks or sprints.
15432
17052
  - Identify the 2-3 decisions that had the most outsized impact (positive or negative).
15433
- - Be brutally honest. This is a retrospective, not a progress report.`;
17053
+ - Be brutally honest. This is a strategic assessment, not a progress report.`;
15434
17054
  async function prepareZoomOut(adapter2, projectRoot) {
15435
17055
  const health = await adapter2.getSprintHealth();
15436
17056
  const sprintNumber = health.totalSprints;