cleargate 0.11.5 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/MANIFEST.json +15 -15
  2. package/dist/admin-api/index.cjs +3 -1
  3. package/dist/admin-api/index.cjs.map +1 -1
  4. package/dist/admin-api/index.d.cts +1 -0
  5. package/dist/admin-api/index.d.ts +1 -0
  6. package/dist/admin-api/index.js +3 -1
  7. package/dist/admin-api/index.js.map +1 -1
  8. package/dist/{chunk-HZPJ5QX4.js → chunk-EG6YGT2O.js} +315 -33
  9. package/dist/chunk-EG6YGT2O.js.map +1 -0
  10. package/dist/cli.cjs +855 -359
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +316 -108
  13. package/dist/cli.js.map +1 -1
  14. package/dist/lib/lifecycle-reconcile.cjs +318 -34
  15. package/dist/lib/lifecycle-reconcile.cjs.map +1 -1
  16. package/dist/lib/lifecycle-reconcile.d.cts +55 -4
  17. package/dist/lib/lifecycle-reconcile.d.ts +55 -4
  18. package/dist/lib/lifecycle-reconcile.js +7 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/architect.md +6 -4
  20. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +1 -1
  21. package/dist/templates/cleargate-planning/.claude/agents/developer.md +8 -4
  22. package/dist/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +2 -0
  23. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
  24. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +162 -0
  25. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +1 -1
  26. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +1 -1
  27. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +1 -1
  28. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +1 -1
  29. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  30. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +1 -1
  31. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +1 -1
  32. package/dist/templates/cleargate-planning/CLAUDE.md +4 -0
  33. package/dist/templates/cleargate-planning/MANIFEST.json +15 -15
  34. package/package.json +8 -9
  35. package/templates/cleargate-planning/.claude/agents/architect.md +6 -4
  36. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +1 -1
  37. package/templates/cleargate-planning/.claude/agents/developer.md +8 -4
  38. package/templates/cleargate-planning/.claude/hooks/pre-commit-surface-gate.sh +2 -0
  39. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +138 -0
  40. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +162 -0
  41. package/templates/cleargate-planning/.cleargate/templates/Bug.md +1 -1
  42. package/templates/cleargate-planning/.cleargate/templates/CR.md +1 -1
  43. package/templates/cleargate-planning/.cleargate/templates/epic.md +1 -1
  44. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +1 -1
  45. package/templates/cleargate-planning/.cleargate/templates/initiative.md +1 -1
  46. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +1 -1
  47. package/templates/cleargate-planning/.cleargate/templates/story.md +1 -1
  48. package/templates/cleargate-planning/CLAUDE.md +4 -0
  49. package/templates/cleargate-planning/MANIFEST.json +15 -15
  50. package/dist/chunk-HZPJ5QX4.js.map +0 -1
@@ -642,3 +642,141 @@ wiki:
642
642
  ```
643
643
 
644
644
  Exceeding the ceiling fails `cleargate wiki lint` (enforcement mode). Under `--suggest`, the usage percentage is reported but the check does not fail. Reference: EPIC-015.
645
+
646
+ ---
647
+
648
+ ## Type & Payload Contract
649
+
650
+ Every item pushed to the MCP server (`cleargate_push_item`) must conform to the following contract. Adapter authors implementing new PM-tool integrations MUST satisfy all requirements below before the server accepts the payload.
651
+
652
+ ### Open-type validator
653
+
654
+ `type` is validated as an open vocabulary (new types do not require a server upgrade):
655
+
656
+ ```
657
+ z.string().min(1).max(64).regex(/^[a-z][a-z0-9_-]*$/)
658
+ ```
659
+
660
+ Applied after lowercase-normalize. Types that fail this regex are rejected at the MCP transport layer with an L1 `TYPE_INVALID` error.
661
+
662
+ ### KNOWN_TYPES — advisory registry (8 entries)
663
+
664
+ These 8 types have first-class gate and reporting support. Any type outside this list passes validation but triggers an L2 `TYPE_UNKNOWN` warning in the server log.
665
+
666
+ | Type | Description |
667
+ |---|---|
668
+ | `initiative` | High-level strategic investment |
669
+ | `epic` | Feature-level work breakdown |
670
+ | `story` | Executable unit of work (one sprint, one commit) |
671
+ | `bug` | Defect report + fix |
672
+ | `cr` | Change request — scope amendment or approach change |
673
+ | `proposal` | Stakeholder-authored item awaiting triage |
674
+ | `sprint` | Sprint plan artifact |
675
+ | `sprint_report` | Sprint closeout report |
676
+
677
+ ### RESERVED_PAYLOAD_KEYS (5 entries)
678
+
679
+ The following keys are set exclusively by the MCP server or the CLI sync engine. Callers MUST NOT include them in `payload`; the server strips or rejects them on receipt.
680
+
681
+ | Key | Owner | Reason |
682
+ |---|---|---|
683
+ | `cleargate_id` | CLI stamp engine | Must match the frontmatter `cleargate_id` — server validates; caller cannot override |
684
+ | `pushed_by` | Server | Set from JWT `sub` → member lookup; client-supplied value is ignored |
685
+ | `pushed_at` | Server | Set to server UTC timestamp; client-supplied value is ignored |
686
+ | `last_synced_body_sha` | Sync engine | Conflict-detection cursor; tampering causes spurious diffs |
687
+ | `_version` | Server | Internal optimistic-lock counter; client tampering causes 409 |
688
+
689
+ ### Minimum payload contract
690
+
691
+ Every push MUST include:
692
+
693
+ | Key | Level | Note |
694
+ |---|---|---|
695
+ | `cleargate_id` | **Required (L1 error if absent)** | `TYPE-NNN` or 5-digit numeric (see formats below) |
696
+ | `type` | **Required (L1 error if absent)** | Open vocabulary, validated by regex above |
697
+ | `title` | Recommended (L2 warning if absent) | Human-readable item title |
698
+ | `status` | Recommended (L2 warning if absent) | From §21 status vocabulary |
699
+
700
+ ### `payload.origin` convention
701
+
702
+ `payload.origin` identifies the surface that produced the push. Gates fire or are bypassed based on this value:
703
+
704
+ | Value pattern | Gate behavior |
705
+ |---|---|
706
+ | `cleargate-cli` | All readiness gates run (standard path) |
707
+ | `adapter:<vendor>` | Gates bypass — adapter is assumed to have already validated externally. Example: `adapter:linear`, `adapter:jira` |
708
+ | `system:<service>` | Gates bypass — system-generated push (e.g. `system:reporter`, `system:wiki-ingest`) |
709
+ | absent / null | Treated as `cleargate-cli`; gates run |
710
+
711
+ ### `cleargate_id` formats (2 valid forms)
712
+
713
+ | Format | Pattern | Example | Used for |
714
+ |---|---|---|---|
715
+ | Type-prefixed | `TYPE-NNN` (TYPE = uppercase letters, NNN = 1–5 digits) | `STORY-027-05`, `EPIC-003` | All standard work items |
716
+ | 5-digit numeric | Exactly 5 decimal digits | `00042` | Legacy PM-tool remote IDs; internal migration use only |
717
+
718
+ ### L1 errorCode taxonomy (7 codes)
719
+
720
+ L1 errors cause the MCP tool call to return a structured error payload with shape `{ code, message, hint }` and HTTP 422 (or equivalent MCP error).
721
+
722
+ | Code | Condition | Hint |
723
+ |---|---|---|
724
+ | `TYPE_INVALID` | `type` fails regex validation | Normalize to lowercase-kebab; max 64 chars |
725
+ | `ID_INVALID` | `cleargate_id` does not match either valid format | Use `TYPE-NNN` or 5-digit numeric |
726
+ | `ID_MISSING` | `cleargate_id` absent from payload | Required field — stamp it before push |
727
+ | `TYPE_MISSING` | `type` absent from payload | Required field |
728
+ | `RESERVED_KEY` | Payload contains a `RESERVED_PAYLOAD_KEYS` member | Remove the key; server owns it |
729
+ | `APPROVED_GATE` | `payload.approved !== true` and `skipApprovedGate` not set | Set `approved: true` after human sign-off |
730
+ | `PROJECT_NOT_FOUND` | JWT `project_id` claim has no matching project row | Re-join with a valid invite URL |
731
+
732
+ ### L2 warningCode taxonomy (3 codes)
733
+
734
+ L2 warnings are non-blocking. The push succeeds, but the server appends `[advisory: <code>]` to the item body and logs the warning.
735
+
736
+ | Code | Condition | Field |
737
+ |---|---|---|
738
+ | `TITLE_MISSING` | `title` absent or empty | `title` |
739
+ | `STATUS_MISSING` | `status` absent or not in §21 vocabulary | `status` |
740
+ | `TYPE_UNKNOWN` | `type` is not in `KNOWN_TYPES` advisory registry | `type` |
741
+
742
+ ---
743
+
744
+ ## Codebase / PM-Tool Boundary
745
+
746
+ **Rule:** `cleargate-cli/src/**` and `.claude/**` MUST NOT import any PM-tool SDK. PM-tool adapters live exclusively in `mcp/src/adapters/`. Credentials and connection config live in admin DB rows, configured via the admin console UI.
747
+
748
+ ### Rationale
749
+
750
+ ClearGate must remain PM-tool-agnostic at the CLI and agent-definition layer so that:
751
+ 1. The CLI can be installed in any repo regardless of which PM tool the team uses.
752
+ 2. Agent prompts (`.claude/**`) do not hard-code vendor assumptions.
753
+ 3. A new PM-tool adapter can be added by dropping a file in `mcp/src/adapters/` without touching `cleargate-cli/` or `.claude/`.
754
+
755
+ ### Forbidden import patterns
756
+
757
+ Any `import` or `require` statement in `cleargate-cli/src/**/*.ts` or `.claude/**/*.{ts,sh,md}` that references the following patterns fails CI:
758
+
759
+ | Pattern | PM tool |
760
+ |---|---|
761
+ | `@linear/sdk` | Linear |
762
+ | `linear-sdk` | Linear (alt package) |
763
+ | `jira-client` | Jira |
764
+ | `node-jira-client` | Jira (alt package) |
765
+ | `jira.js` | Jira (alt package) |
766
+ | `azure-devops` | Azure DevOps |
767
+ | `@atlassian/` | Atlassian family |
768
+
769
+ ### Allowed surface for PM-tool code
770
+
771
+ `mcp/src/adapters/` is the only surface where PM-tool SDK imports are permitted. Each adapter file is responsible for importing its SDK, mapping ClearGate payload shapes to PM-tool API calls, and surfacing errors as structured MCP errors.
772
+
773
+ ### CI enforcement
774
+
775
+ `scripts/ci-no-pm-sdk.mjs` scans the two forbidden surfaces and exits non-zero on any match. Run via:
776
+
777
+ ```
778
+ node scripts/ci-no-pm-sdk.mjs
779
+ npm run check:no-pm-sdk
780
+ ```
781
+
782
+ Comments (lines starting with `//`, `#`, `/*`, or `*`) are excluded from the scan. The script prints `✓ no forbidden PM-SDK imports` on a clean tree.
@@ -407,6 +407,79 @@ async function main() {
407
407
  process.stdout.write('Step 2.6b skipped: CLEARGATE_SKIP_LIFECYCLE_CHECK=1 set (test seam).\n');
408
408
  }
409
409
 
410
+ // ── Step 2.6c: Parent (Epic/Sprint) Rollup (CR-066) ──────────────────────
411
+ // Runs unconditionally (not gated by CLEARGATE_SKIP_LIFECYCLE_CHECK).
412
+ // For each active parent in delivery/pending-sync/, checks children coverage:
413
+ // auto-flip → rewrite status: Completed atomically, log one line.
414
+ // halt-partial → collect into haltList; exit 1 after processing all.
415
+ // halt-zero-children → collect into haltList; exit 1 after processing all.
416
+ // no-op / skip-deferred → silent.
417
+ // Defensive guard: if walkActiveParents is not a function (stale dist/), skip with warning.
418
+
419
+ /**
420
+ * Atomic in-place status rewrite (raw-bytes regex-replace).
421
+ * Follows FLASHCARD 2026-04-24 #frontmatter #write-back pattern.
422
+ * @param {string} filePath
423
+ * @param {string} newStatus
424
+ */
425
+ function setFrontmatterStatusAtomic(filePath, newStatus) {
426
+ const raw = fs.readFileSync(filePath, 'utf8');
427
+ const fm = raw.match(/^---\n([\s\S]*?)\n---/);
428
+ if (!fm) throw new Error(`No frontmatter in ${filePath}`);
429
+ const newFm = fm[1].replace(/^status:.*$/m, `status: ${newStatus}`);
430
+ const newRaw = raw.replace(fm[1], newFm);
431
+ const tmp = filePath + '.tmp.' + process.pid;
432
+ fs.writeFileSync(tmp, newRaw, 'utf8');
433
+ fs.renameSync(tmp, filePath);
434
+ }
435
+
436
+ process.stdout.write('Step 2.6c: rolling up parent statuses...\n');
437
+ try {
438
+ // Use __dirname-relative path so the import finds the ACTUAL built dist,
439
+ // not a fixture tmpdir override (CLEARGATE_REPO_ROOT may point elsewhere in tests).
440
+ const scriptRepoRoot26c = path.resolve(SCRIPTS_DIR, '..', '..');
441
+ const reconcilerMod26c = await import(
442
+ path.join(scriptRepoRoot26c, 'cleargate-cli', 'dist', 'lib', 'lifecycle-reconcile.js')
443
+ ).catch(() => null);
444
+
445
+ if (!reconcilerMod26c || typeof reconcilerMod26c.walkActiveParents !== 'function') {
446
+ process.stdout.write('Step 2.6c skipped: walkActiveParents not in built CLI — rebuild cleargate-cli/.\n');
447
+ } else {
448
+ // Delivery paths come from REPO_ROOT (may be fixture tmpdir in tests)
449
+ const deliveryRoot26c = path.join(REPO_ROOT, '.cleargate', 'delivery');
450
+ const archiveRoot26c = path.join(deliveryRoot26c, 'archive');
451
+ const results26c = await reconcilerMod26c.walkActiveParents({
452
+ deliveryRoot: deliveryRoot26c,
453
+ archiveRoot: archiveRoot26c,
454
+ });
455
+ const flips26c = results26c.filter((r) => r.verdict === 'auto-flip');
456
+ const halts26c = results26c.filter(
457
+ (r) => r.verdict === 'halt-partial' || r.verdict === 'halt-zero-children',
458
+ );
459
+
460
+ for (const f of flips26c) {
461
+ setFrontmatterStatusAtomic(f.parent_path, 'Completed');
462
+ process.stdout.write(
463
+ `Step 2.6c: ${f.parent_id} status ${f.current_status} → Completed` +
464
+ ` (${f.terminal_children.length}/${f.terminal_children.length} children Completed:` +
465
+ ` ${f.terminal_children.join(', ')})\n`,
466
+ );
467
+ }
468
+
469
+ if (halts26c.length > 0) {
470
+ process.stderr.write(`Step 2.6c HALT: ${halts26c.length} parent(s) require manual ack:\n`);
471
+ for (const h of halts26c) {
472
+ process.stderr.write(` - [${h.verdict}] ${h.halt_reason}\n`);
473
+ }
474
+ process.exit(1);
475
+ }
476
+
477
+ process.stdout.write(`Step 2.6c passed: ${flips26c.length} parent(s) auto-flipped; no halts.\n`);
478
+ }
479
+ } catch (e26c) {
480
+ process.stderr.write(`Step 2.6c warning: parent rollup unavailable: ${e26c.message}\n`);
481
+ }
482
+
410
483
  // ── Step 2.7: Worktree-Closed Check (CR-022 M1) ──────────────────────────
411
484
  // Block close if any .worktrees/STORY-* path is present.
412
485
  // v2 enforcing (exit 1); v1 advisory (warn + continue).
@@ -746,6 +819,95 @@ async function main() {
746
819
  process.stderr.write('Run `cleargate sync work-items` manually to retry.\n');
747
820
  }
748
821
 
822
+ // ── Step 7.4: MCP push of sprint plan + report ───────────────────────────────
823
+ // CR-064: mcp push sprint plan + report
824
+ // Runs after Step 7 (work-item sync) and BEFORE Step 7.5 (CR-063 wiki ingest).
825
+ // Per SDR-locked ordering: MCP push first, wiki ingest second (CR-064 §0.5 Q4 accepted).
826
+ // Non-fatal on failure: sprint stays Completed. MCP push can be retried manually.
827
+ process.stdout.write('Step 7.4: pushing sprint plan + report to MCP...\n');
828
+ try {
829
+ const cliBin74 = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
830
+ if (fs.existsSync(cliBin74)) {
831
+ // Resolve sprint plan path: prefer archive/, fall back to pending-sync/
832
+ const deliveryBase = path.join(REPO_ROOT, '.cleargate', 'delivery');
833
+ const archiveDir = path.join(deliveryBase, 'archive');
834
+ const pendingDir = path.join(deliveryBase, 'pending-sync');
835
+ let planPath = null;
836
+ for (const dir of [archiveDir, pendingDir]) {
837
+ if (!fs.existsSync(dir)) continue;
838
+ const match = fs.readdirSync(dir).find(
839
+ (f) => f.startsWith(sprintId + '_') && f.endsWith('.md'),
840
+ );
841
+ if (match) { planPath = path.join(dir, match); break; }
842
+ }
843
+ if (planPath && fs.existsSync(planPath)) {
844
+ try {
845
+ execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(planPath)}`, {
846
+ stdio: 'inherit',
847
+ env: process.env,
848
+ timeout: 60000,
849
+ });
850
+ process.stdout.write('Step 7.4a passed: sprint plan pushed to MCP.\n');
851
+ } catch (err74a) {
852
+ process.stderr.write(`Step 7.4a warning: sprint plan push failed: ${/** @type {Error} */ (err74a).message}\n`);
853
+ }
854
+ } else {
855
+ process.stdout.write('Step 7.4a skipped: sprint plan file not found.\n');
856
+ }
857
+ // Resolve sprint report path: reuses reportFile resolved via legacy-fallback in Step 4
858
+ if (reportFile && fs.existsSync(reportFile)) {
859
+ try {
860
+ execSync(`node ${JSON.stringify(cliBin74)} push ${JSON.stringify(reportFile)}`, {
861
+ stdio: 'inherit',
862
+ env: process.env,
863
+ timeout: 60000,
864
+ });
865
+ process.stdout.write('Step 7.4b passed: sprint report pushed to MCP.\n');
866
+ } catch (err74b) {
867
+ process.stderr.write(`Step 7.4b warning: sprint report push failed: ${/** @type {Error} */ (err74b).message}\n`);
868
+ }
869
+ } else {
870
+ process.stdout.write('Step 7.4b skipped: sprint report file not found (legacy sprint or report not yet written).\n');
871
+ }
872
+ } else {
873
+ process.stdout.write('Step 7.4 skipped: CLI binary not found (non-fatal).\n');
874
+ }
875
+ } catch (err) {
876
+ // Non-fatal — sprint stays Completed; MCP push can be retried manually
877
+ process.stderr.write(`Step 7.4 warning: MCP push failed: ${/** @type {Error} */ (err).message}\n`);
878
+ process.stderr.write('Run `cleargate push <plan-or-report-path>` manually to retry.\n');
879
+ }
880
+
881
+ // ── Step 7.5: wiki ingest sprint report ──────────────────────────────────
882
+ // CR-063: wiki ingest sprint report
883
+ // Runs after Step 7 (MCP sync). Non-fatal: sprint stays Completed on failure.
884
+ // CR-064 (Wave 3) inserts its MCP-push step at Step 7.4, immediately before this anchor.
885
+ process.stdout.write('Step 7.5: ingesting sprint report into wiki...\n');
886
+ try {
887
+ const cliBin = path.join(REPO_ROOT, 'cleargate-cli', 'dist', 'cli.js');
888
+ if (fs.existsSync(cliBin)) {
889
+ // Resolve report path using the same legacy-fallback rule as Step 4
890
+ // (SPRINT-NN_REPORT.md preferred; fall back to REPORT.md for legacy sprints)
891
+ const reportPath = reportFile; // reportFile is already resolved via legacy-fallback in Step 4
892
+ if (fs.existsSync(reportPath)) {
893
+ execSync(`node ${JSON.stringify(cliBin)} wiki ingest ${JSON.stringify(reportPath)}`, {
894
+ stdio: 'inherit',
895
+ env: process.env,
896
+ timeout: 60000,
897
+ });
898
+ process.stdout.write('Step 7.5 passed: sprint report ingested into wiki.\n');
899
+ } else {
900
+ process.stdout.write('Step 7.5 skipped: report file not found (legacy sprint or report not yet written).\n');
901
+ }
902
+ } else {
903
+ process.stdout.write('Step 7.5 skipped: CLI binary not found (non-fatal).\n');
904
+ }
905
+ } catch (err) {
906
+ // Non-fatal — sprint stays Completed; ingest can be retried manually
907
+ process.stderr.write(`Step 7.5 warning: wiki ingest sprint report failed: ${/** @type {Error} */ (err).message}\n`);
908
+ process.stderr.write('Run `cleargate wiki ingest <report-path>` manually to retry.\n');
909
+ }
910
+
749
911
  // ── Step 8: Verbose post-close handoff list ───────────────────────────────
750
912
  // Prints 6 explicit next-step items to stdout (CR-022 §3 M4).
751
913
  {
@@ -31,7 +31,7 @@ parent_ref: "EPIC-{ID} | STORY-{ID}"
31
31
  parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
32
32
  sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
33
33
  carry_over: false # set true to skip lifecycle reconciliation at sprint close
34
- status: "Draft | Triaged | In Fix | Verified"
34
+ status: "Draft | Triaged | In Fix | Completed"
35
35
  severity: "P0-Critical | P1-High | P2-Medium | P3-Low"
36
36
  reporter: "{name}"
37
37
  approved: false
@@ -31,7 +31,7 @@ parent_ref: "EPIC-{ID} | STORY-{ID}"
31
31
  parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
32
32
  sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
33
33
  carry_over: false # set true to skip lifecycle reconciliation at sprint close
34
- status: "Draft | In Review | Approved"
34
+ status: "Draft | In Review | Approved | Completed"
35
35
  approved: false
36
36
  created_at: "2026-04-17T00:00:00Z"
37
37
  updated_at: "2026-04-17T00:00:00Z"
@@ -36,7 +36,7 @@ epic_id: "EPIC-{ID}"
36
36
  parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
37
37
  sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
38
38
  carry_over: false # set true to skip lifecycle reconciliation at sprint close
39
- status: "Draft"
39
+ status: "Draft" # lifecycle: Draft → Active → Completed
40
40
  ambiguity: "🔴 High"
41
41
  context_source: "PROPOSAL-{ID}.md"
42
42
  owner: "{PM/PO name}"
@@ -32,7 +32,7 @@ hotfix_id: "{ID}"
32
32
  parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
33
33
  sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
34
34
  carry_over: false # set true to skip lifecycle reconciliation at sprint close
35
- status: "Draft"
35
+ status: "Draft" # lifecycle: Draft → In Fix → Completed
36
36
  severity: "P2"
37
37
  originating_signal: "user-report"
38
38
  created_at: "{ISO}"
@@ -29,7 +29,7 @@ DO NOT output these instructions in the rendered file.
29
29
  initiative_id: "INITIATIVE-{NNN}"
30
30
  remote_id: null
31
31
  source_tool: "linear | jira | github | manual-paste"
32
- status: "{PM native status — e.g. Discovery, In Triage, Triaged}"
32
+ status: "{PM native status — e.g. Discovery, In Triage, Triaged, Completed}"
33
33
  synced_at: null
34
34
  triaged_at: null
35
35
  spawned_items: []
@@ -46,7 +46,7 @@ template_version: 2
46
46
  Each CR:bug and UR:bug counts toward Bug-Fix Tax (§3). CR:scope-change increments arch_bounces. -->
47
47
 
48
48
  ### STORY-NNN-NN: <Title>
49
- - **Status:** Done | Escalated | Parking Lot | Carried Over
49
+ - **Status:** Completed | Escalated | Parking Lot | Carried Over
50
50
  - **Complexity:** L<n>
51
51
  - **Commit:** `<sha>`
52
52
  - **Bounce count:** qa=N arch=N total=N
@@ -59,7 +59,7 @@ parent_epic_ref: "EPIC-{ID}"
59
59
  parent_cleargate_id: null # canonical cleargate-id of parent work item; null for top-level
60
60
  sprint_cleargate_id: null # canonical cleargate-id of owning sprint; null for off-sprint items
61
61
  carry_over: false # set true to skip lifecycle reconciliation at sprint close
62
- status: "Draft"
62
+ status: "Draft" # lifecycle: Draft → In Review → Completed
63
63
  ambiguity: "🔴 High"
64
64
  context_source: "PROPOSAL-{ID}.md"
65
65
  actor: "{Persona Name}"
@@ -53,6 +53,10 @@ This repository uses **ClearGate** — a standalone planning framework for AI co
53
53
 
54
54
  **Cross-project orchestration.** When running an orchestrator from one project's repo against another project's sprint tree, export `ORCHESTRATOR_PROJECT_DIR=/absolute/path/to/target/repo` in the shell before launching the session. Overrides `CLAUDE_PROJECT_DIR`; sentinel + ledger writes route into the target's `.cleargate/sprint-runs/` tree. If the target has no `.cleargate/sprint-runs/.active` sentinel, writes land in the target's `_off-sprint` bucket — not the orchestrator's own repo.
55
55
 
56
+ **Codebase / PM-Tool Boundary (EPIC-027).** `cleargate-cli/src/**` and `.claude/**` MUST NOT import any PM-tool SDK (`@linear/sdk`, `jira-client`, `azure-devops`, `@atlassian/`, `linear-sdk`, `node-jira-client`, `jira.js`). PM-tool adapters live exclusively in `mcp/src/adapters/`; credentials live in admin DB rows. The type-and-payload contract (open-type validator, KNOWN_TYPES, RESERVED_PAYLOAD_KEYS, `payload.origin`, `cleargate_id` formats, L1 errorCode + L2 warningCode taxonomies) is fully documented in `.cleargate/knowledge/cleargate-protocol.md` §Type & Payload Contract and §Codebase/PM-Tool Boundary. CI enforcement: `npm run check:no-pm-sdk` (exits non-zero on any forbidden import). Target repos that ran `cleargate init` before SPRINT-27 should re-run `cleargate init` to pick up this boundary rule in their local bounded block.
57
+
58
+ **Single test runner (EPIC-028).** All three packages (mcp/, cleargate-cli/, admin/) use node:test exclusively — vitest is fully eliminated as of 2026-05-18. File naming: `*.node.test.ts`. Run via `tsx --test` (mcp/, cleargate-cli/) or `node --conditions browser --import tsx tests/run-tests.mjs` (admin/). The `--conditions browser` flag is required for admin/ — it enables jsdom-bootstrap via `setup-node-test.mjs`. Adding vitest back is forbidden; `check:no-vitest` pre-commit guard enforces this.
59
+
56
60
  **Project overrides.** Content OUTSIDE this `<!-- CLEARGATE:START -->...<!-- CLEARGATE:END -->` block takes precedence where it conflicts with ClearGate defaults.
57
61
 
58
62
  **Scope reminder.** ClearGate is a *planning* framework. It scaffolds how work gets planned and how the four-agent loop runs. It does not replace your project's build system, CI, test runner, or deployment tooling.
@@ -1,10 +1,10 @@
1
1
  {
2
- "cleargate_version": "0.11.5",
3
- "generated_at": "2026-05-11T22:02:23.454Z",
2
+ "cleargate_version": "0.13.0",
3
+ "generated_at": "2026-05-18T17:11:10.027Z",
4
4
  "files": [
5
5
  {
6
6
  "path": ".claude/agents/architect.md",
7
- "sha256": "a2d11d6a7f21bce22730ba9076af9ddde44b258082e33aa99fcd57c14ad75f55",
7
+ "sha256": "823db34b934d080e648955326dcc3cd31899e90f62ecfa4a9344f5b473237084",
8
8
  "tier": "agent",
9
9
  "overwrite_policy": "always",
10
10
  "preserve_on_uninstall": false
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": ".claude/agents/cleargate-wiki-lint.md",
28
- "sha256": "42067fd728e64ac1ff1ace5822b8f8c3ddf1ba70fd0fbddf0e8cb19d5d699e77",
28
+ "sha256": "2a9212d81df9a68e167ec7a18093a73c6da0208f13685c6887d5bd832b56fb3d",
29
29
  "tier": "agent",
30
30
  "overwrite_policy": "always",
31
31
  "preserve_on_uninstall": false
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "path": ".claude/agents/developer.md",
42
- "sha256": "36f75fe9f8dc8412eb22706dfd021f5db5694c81d73e0c938e701a83c0149126",
42
+ "sha256": "db7963778d68654f2dc96658d60433fa79305a10eec937807f2f0ed0cd05ce89",
43
43
  "tier": "agent",
44
44
  "overwrite_policy": "always",
45
45
  "preserve_on_uninstall": false
@@ -74,7 +74,7 @@
74
74
  },
75
75
  {
76
76
  "path": ".claude/hooks/pre-commit-surface-gate.sh",
77
- "sha256": "6741422cd8ca75b45e26bd0e9cb54126ad9e933f763c9094ceba6b84794b761a",
77
+ "sha256": "8dd817fbe75ee53753e2fe1fc2ccd2b6efdb2b3c0b6ad3191bebd9640afdd6f8",
78
78
  "tier": "hook",
79
79
  "overwrite_policy": "always",
80
80
  "preserve_on_uninstall": false
@@ -165,7 +165,7 @@
165
165
  },
166
166
  {
167
167
  "path": ".cleargate/knowledge/cleargate-protocol.md",
168
- "sha256": "50e999717b9c9e1c11638f3f2433f413d177ed8ddf3cffdbb20b50c8807b1fc5",
168
+ "sha256": "0380e74efa8aa782b77952fa68cc0171de035653e777d5c241d84c6e0414957b",
169
169
  "tier": "protocol",
170
170
  "overwrite_policy": "merge-3way",
171
171
  "preserve_on_uninstall": false
@@ -200,7 +200,7 @@
200
200
  },
201
201
  {
202
202
  "path": ".cleargate/scripts/close_sprint.mjs",
203
- "sha256": "1807e5b27f1501476c9dec41cbb350c432d2894cb0727d8da0c860ad3f9181c7",
203
+ "sha256": "b14b65f15c5ad57a845df89c069b5417195651b7d3aa7f7f5736416d0db0b868",
204
204
  "tier": "script",
205
205
  "overwrite_policy": "always",
206
206
  "preserve_on_uninstall": false
@@ -396,35 +396,35 @@
396
396
  },
397
397
  {
398
398
  "path": ".cleargate/templates/Bug.md",
399
- "sha256": "570112e3a60856c891652837307fc6f296cc39acc20451f8bde62da0fe033026",
399
+ "sha256": "cebca344b6b820525c603444cf52626e120ebaa1ac28da099dc19c9cff39302c",
400
400
  "tier": "template",
401
401
  "overwrite_policy": "merge-3way",
402
402
  "preserve_on_uninstall": false
403
403
  },
404
404
  {
405
405
  "path": ".cleargate/templates/CR.md",
406
- "sha256": "ab111792dc79fd5181b44a63c76ac696fa4e456161ac44e2c4d0dc609e8dd904",
406
+ "sha256": "ea5acf2087808e0d52806a87a6a7f1ced7473b591857f322fab5285adc80d25a",
407
407
  "tier": "template",
408
408
  "overwrite_policy": "merge-3way",
409
409
  "preserve_on_uninstall": false
410
410
  },
411
411
  {
412
412
  "path": ".cleargate/templates/epic.md",
413
- "sha256": "fd3b54de89befe7c5bcef65e2fa5550fc7f371d55c8f15143ae73d84f3ae6d36",
413
+ "sha256": "f9cf44db19288f0756b76bc8b13a075d4089990324db6febd072f5cf93d59cd0",
414
414
  "tier": "template",
415
415
  "overwrite_policy": "merge-3way",
416
416
  "preserve_on_uninstall": false
417
417
  },
418
418
  {
419
419
  "path": ".cleargate/templates/hotfix.md",
420
- "sha256": "b75dd154c156e68a44ae01b85eff7ec16f54050e4fe7eb71f59e17d87c03a298",
420
+ "sha256": "de788497b4d224500036773f854801c056d73b08c3c0d66fdb641f17a1610bca",
421
421
  "tier": "template",
422
422
  "overwrite_policy": "merge-3way",
423
423
  "preserve_on_uninstall": false
424
424
  },
425
425
  {
426
426
  "path": ".cleargate/templates/initiative.md",
427
- "sha256": "28680917228e5d9887c8820193cc21bfecc6aad3bc0658e8a59087adf63551fd",
427
+ "sha256": "1170e595f5813c62f86212d6ca1955d84465f396c81aaf34498b8e2d4595d681",
428
428
  "tier": "template",
429
429
  "overwrite_policy": "merge-3way",
430
430
  "preserve_on_uninstall": false
@@ -445,14 +445,14 @@
445
445
  },
446
446
  {
447
447
  "path": ".cleargate/templates/sprint_report.md",
448
- "sha256": "84e32e956eb1e8a9c97be1c346b695df316a9e47e17402f3d6581e3ffdbc4d2d",
448
+ "sha256": "5914d54080f6110be5a5e905e3312811f3d0f80978121b1e15707d5be05cc5b1",
449
449
  "tier": "template",
450
450
  "overwrite_policy": "merge-3way",
451
451
  "preserve_on_uninstall": false
452
452
  },
453
453
  {
454
454
  "path": ".cleargate/templates/story.md",
455
- "sha256": "56d793983614d832e84193d99ee5318c061d2f3ea4f019cefb861c026513cb31",
455
+ "sha256": "0badf01a080bca552a06fb64becd9b8c88fbf104e30f32667263522c3ff81051",
456
456
  "tier": "template",
457
457
  "overwrite_policy": "merge-3way",
458
458
  "preserve_on_uninstall": false
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/lifecycle-reconcile.ts","../src/wiki/parse-frontmatter.ts"],"sourcesContent":["/**\n * lifecycle-reconcile.ts — CR-017 Lifecycle Status Reconciliation + Decomposition Gate\n *\n * Public API:\n * reconcileLifecycle(opts) → { drift: DriftItem[], clean: number }\n * reconcileDecomposition(opts) → { missing: MissingDecomp[], clean: number }\n * parseCommitMessage(msg) → Array<{ verb, id, type }>\n * VERB_STATUS_MAP — verb-to-expected-status table\n *\n * TERMINAL_STATES referenced from .cleargate/scripts/constants.mjs:45.\n * Do NOT redefine; duplicate literal with source citation.\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { spawnSync } from 'node:child_process';\nimport { parseFrontmatter } from '../wiki/parse-frontmatter.js';\n\n// ─── Constants ─────────────────────────────────────────────────────────────────\n\n/**\n * Terminal statuses for artifact lifecycle.\n * Source: .cleargate/scripts/constants.mjs:45 TERMINAL_STATES.\n * NOTE: These are the *artifact* terminal statuses (Done, Completed, Verified, etc.),\n * not state.json story states (Done, Escalated, Parking Lot).\n */\nexport const ARTIFACT_TERMINAL_STATUSES = new Set([\n 'Done',\n 'Completed',\n 'Verified',\n 'Abandoned',\n 'Closed',\n 'Resolved',\n 'Escalated',\n 'Parking Lot',\n]);\n\n/**\n * Verb-to-expected-status map (v1).\n * Key: verb pattern (lower-case), Value: { types, expected }.\n * types: which artifact types this verb applies to.\n * expected: accepted terminal statuses for this verb.\n */\nexport const VERB_STATUS_MAP: Readonly<Record<string, { types: string[]; expected: string[] }>> = {\n feat: {\n types: ['STORY', 'EPIC', 'CR'],\n expected: ['Done', 'Completed'],\n },\n fix: {\n types: ['BUG', 'HOTFIX'],\n expected: ['Verified', 'Done', 'Completed'],\n },\n};\n\n// ─── Types ─────────────────────────────────────────────────────────────────────\n\nexport interface DriftItem {\n id: string;\n type: 'STORY' | 'CR' | 'BUG' | 'EPIC' | 'PROPOSAL' | 'HOTFIX';\n expected_status: string;\n actual_status: string | null;\n file_path: string | null;\n in_archive: boolean;\n commit_shas: string[];\n carry_over: boolean;\n}\n\nexport interface ReconcileLifecycleResult {\n drift: DriftItem[];\n clean: number;\n}\n\nexport interface ReconcileLifecycleOpts {\n since: Date;\n until?: Date;\n deliveryRoot: string;\n repoRoot: string;\n /** Test seam: replace spawnSync git calls */\n gitRunner?: (cmd: string, args: string[]) => string;\n}\n\nexport interface MissingDecomp {\n id: string;\n type: 'epic' | 'proposal';\n reason: 'no-child-stories' | 'no-decomposed-epic' | 'file-missing';\n expected_files: string[];\n}\n\nexport interface ReconcileDecompositionResult {\n missing: MissingDecomp[];\n clean: number;\n}\n\nexport interface ReconcileDecompositionOpts {\n sprintPlanPath: string;\n deliveryRoot: string;\n}\n\n// ─── ID shape regex (longest-alternative-first per BUG-010 + assert_story_files.mjs) ──\n\nconst ID_PATTERN = /\\b(STORY-\\d{3}-\\d{2}|(CR|BUG|EPIC|HOTFIX)-\\d{3}|(PROPOSAL|PROP)-\\d{3})\\b/g;\n\n/** Artifact type names recognized by the reconciler */\ntype ArtifactType = 'STORY' | 'CR' | 'BUG' | 'EPIC' | 'PROPOSAL' | 'HOTFIX';\n\nfunction normalizeId(raw: string): string {\n // PROP-NNN → PROPOSAL-NNN (BUG-009 lesson)\n return raw.replace(/^PROP-(\\d+)$/, 'PROPOSAL-$1');\n}\n\nfunction idType(id: string): ArtifactType | null {\n if (/^STORY-\\d{3}-\\d{2}$/.test(id)) return 'STORY';\n if (/^CR-\\d{3}$/.test(id)) return 'CR';\n if (/^BUG-\\d{3}$/.test(id)) return 'BUG';\n if (/^EPIC-\\d{3}$/.test(id)) return 'EPIC';\n if (/^PROPOSAL-\\d{3}$/.test(id)) return 'PROPOSAL';\n if (/^HOTFIX-\\d{3}$/.test(id)) return 'HOTFIX';\n return null;\n}\n\n// ─── parseCommitMessage ────────────────────────────────────────────────────────\n\n/**\n * Parse a commit message (subject + optional first body line) for work-item IDs.\n * Returns one entry per ID found with the verb inferred from conventional prefix.\n *\n * commit format: `<verb>(<scope>): <description>\\n\\n<body>`\n * multi-ID: `fix(cli)!: BUG-001 fix + CR-001 align`\n * merge: `merge: STORY-001-01 → main`\n */\nexport function parseCommitMessage(\n msg: string,\n): Array<{ verb: string; id: string; type: string }> {\n const lines = msg.split('\\n');\n const subject = lines[0] ?? '';\n\n // First non-empty body line (if any) after the blank separator\n let firstBodyLine = '';\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim()) {\n firstBodyLine = lines[i]!;\n break;\n }\n }\n\n // Extract verb from subject: `feat(...)`, `fix(...)`, `merge:`, `chore(...)`, etc.\n const verbMatch = /^(\\w+)[(!]/.exec(subject) ?? /^(\\w+):/.exec(subject);\n const verb = verbMatch ? verbMatch[1]!.toLowerCase() : '';\n\n // Scan subject + first body line for IDs\n const searchText = subject + (firstBodyLine ? '\\n' + firstBodyLine : '');\n const results: Array<{ verb: string; id: string; type: string }> = [];\n const seen = new Set<string>();\n\n let m: RegExpExecArray | null;\n ID_PATTERN.lastIndex = 0;\n while ((m = ID_PATTERN.exec(searchText)) !== null) {\n const rawId = m[0]!;\n const id = normalizeId(rawId);\n if (seen.has(id)) continue;\n seen.add(id);\n const type = idType(id);\n if (!type) continue;\n results.push({ verb, id, type });\n }\n\n return results;\n}\n\n// ─── File finders ─────────────────────────────────────────────────────────────\n\ninterface FoundFile {\n absPath: string;\n inArchive: boolean;\n relPath: string; // relative to deliveryRoot\n}\n\nfunction findArtifactFile(deliveryRoot: string, id: string): FoundFile | null {\n const prefix = `${id}_`;\n const dirs: Array<{ rel: string; inArchive: boolean }> = [\n { rel: 'pending-sync', inArchive: false },\n { rel: 'archive', inArchive: true },\n ];\n for (const { rel, inArchive } of dirs) {\n const dir = path.join(deliveryRoot, rel);\n let entries: string[];\n try {\n entries = fs.readdirSync(dir);\n } catch {\n continue;\n }\n // match `ID_*.md` OR `ID.md`\n const match = entries.find(\n (e) => (e.startsWith(prefix) || e === `${id}.md`) && e.endsWith('.md'),\n );\n if (match) {\n const absPath = path.join(dir, match);\n return { absPath, inArchive, relPath: `${rel}/${match}` };\n }\n }\n return null;\n}\n\nfunction readArtifactStatus(absPath: string): { status: string | null; carryOver: boolean } {\n let raw: string;\n try {\n raw = fs.readFileSync(absPath, 'utf8');\n } catch {\n return { status: null, carryOver: false };\n }\n try {\n const { fm } = parseFrontmatter(raw);\n const status = typeof fm['status'] === 'string' ? fm['status'] : null;\n const carryOver = fm['carry_over'] === true;\n return { status, carryOver };\n } catch {\n return { status: null, carryOver: false };\n }\n}\n\n// ─── reconcileLifecycle ────────────────────────────────────────────────────────\n\n/**\n * Scan git log in [since, until] range and reconcile artifact statuses.\n *\n * For each commit touching feat/fix verbs with IDs:\n * - Find the artifact file in pending-sync or archive\n * - Check if status is at expected terminal status\n * - Report drift items for non-terminal artifacts\n * - Skip artifacts with carry_over: true\n */\nexport function reconcileLifecycle(opts: ReconcileLifecycleOpts): ReconcileLifecycleResult {\n const { since, until = new Date(), deliveryRoot, repoRoot } = opts;\n\n const gitRunner =\n opts.gitRunner ??\n ((cmd: string, args: string[]) => {\n const result = spawnSync(cmd, args, { encoding: 'utf8', cwd: repoRoot });\n return (result.stdout ?? '') as string;\n });\n\n // git log --format=\"%H %s%n%b%n---COMMIT---\" --after=<since> --before=<until>\n const sinceIso = since.toISOString();\n const untilIso = until.toISOString();\n const logOutput = gitRunner('git', [\n 'log',\n `--after=${sinceIso}`,\n `--before=${untilIso}`,\n '--format=%H%x00%s%x00%b%x00---COMMIT---',\n '--',\n ]);\n\n // Map: id → DriftItem (accumulates SHAs for bundled-commit grouping)\n // We track each id independently; bundled-commit = multiple SHAs per id\n const idToItem = new Map<string, DriftItem>();\n // Track ids that were found CLEAN (fully reconciled)\n const cleanIds = new Set<string>();\n\n if (logOutput.trim()) {\n // Split by commit separator\n const rawCommits = logOutput.split('---COMMIT---\\n').filter((c) => c.trim());\n\n for (const raw of rawCommits) {\n // Each commit entry: sha\\0subject\\0body\\0\n const [sha = '', subject = '', body = ''] = raw.split('\\x00');\n const trimSha = sha.trim();\n const trimSubject = subject.trim();\n const trimBody = body.trim();\n\n if (!trimSha || !trimSubject) continue;\n\n const commitMsg = trimSubject + (trimBody ? '\\n\\n' + trimBody : '');\n const parsed = parseCommitMessage(commitMsg);\n\n for (const { verb, id, type } of parsed) {\n // Skip merge, chore, docs, refactor, test, file, plan verbs (no expectation)\n if (verb === 'merge' || verb === 'chore' || verb === 'docs' || verb === 'refactor'\n || verb === 'test' || verb === 'file' || verb === 'plan') {\n continue;\n }\n\n // Skip PROPOSAL types — proposals aren't shipped via feat/fix commits\n if (type === 'PROPOSAL') continue;\n\n const verbConfig = VERB_STATUS_MAP[verb];\n if (!verbConfig) continue;\n\n // Verb mismatch: feat(BUG-NNN) → soft warning only, handled at call site\n // We still need to find the file and check status for the call site to report\n\n // Find the artifact file\n const found = findArtifactFile(deliveryRoot, id);\n if (!found) {\n // Unknown ID — log once at info level (no drift)\n // We skip unknown IDs (no file found); call site logs info\n continue;\n }\n\n // Read status + carry_over from CURRENT frontmatter\n const { status, carryOver } = readArtifactStatus(found.absPath);\n\n // carry_over: true → skip silently\n if (carryOver) continue;\n\n // Determine expected statuses for this (verb, type) pair\n let expectedStatuses: string[];\n if (verb === 'feat' && type === 'BUG') {\n // verb mismatch — soft warning, does not block; still check status\n // Use 'Verified' as expected for BUG even with feat verb\n expectedStatuses = ['Verified', 'Done', 'Completed'];\n } else if (!verbConfig.types.includes(type)) {\n // Type not covered by this verb's map — skip\n continue;\n } else {\n expectedStatuses = verbConfig.expected;\n }\n\n const isTerminal = status !== null && expectedStatuses.includes(status);\n const isArchived = found.inArchive;\n\n if (isTerminal && isArchived) {\n // Clean\n cleanIds.add(id);\n // If we previously recorded drift for this id (from another commit), remove it\n // (Most recent status check wins — carry_over already handled above)\n idToItem.delete(id);\n } else if (!idToItem.has(id)) {\n // New drift item\n const expectedStr = expectedStatuses[0] ?? 'Done';\n idToItem.set(id, {\n id,\n type: type as DriftItem['type'],\n expected_status: expectedStr,\n actual_status: status,\n file_path: found.relPath,\n in_archive: isArchived,\n commit_shas: [trimSha],\n carry_over: carryOver,\n });\n } else {\n // Existing drift item — add SHA if not already present\n const existing = idToItem.get(id)!;\n if (!existing.commit_shas.includes(trimSha)) {\n existing.commit_shas.push(trimSha);\n }\n }\n }\n }\n }\n\n // Remove from drift any IDs that ended up in cleanIds\n for (const id of cleanIds) {\n idToItem.delete(id);\n }\n\n const drift = Array.from(idToItem.values());\n return { drift, clean: cleanIds.size };\n}\n\n// ─── reconcileCrossSprintOrphans ──────────────────────────────────────────────\n\n/**\n * Orphan drift item: a file in pending-sync/ with a non-terminal status\n * that has been marked Done (or another terminal state) in a closed sprint's\n * state.json — indicating it was completed but never archived.\n */\nexport interface OrphanDriftItem {\n id: string;\n type: 'CR' | 'STORY' | 'BUG' | 'EPIC' | 'HOTFIX';\n pending_sync_status: string;\n state_json_state: string;\n state_json_sprint: string;\n file_path: string;\n}\n\nexport interface ReconcileOrphansOpts {\n /** Path to .cleargate/delivery */\n deliveryRoot: string;\n /** Path to .cleargate/sprint-runs */\n sprintRunsRoot: string;\n}\n\nexport interface ReconcileOrphansResult {\n drift: OrphanDriftItem[];\n clean: number;\n}\n\n/**\n * Detect cross-sprint orphan drift: items in pending-sync/ with status: Ready\n * (or any non-terminal status) that are recorded as Done in a closed sprint's\n * state.json. These were completed but never archived at sprint close.\n *\n * Active-sprint exclusion: reads .active sentinel to identify the current\n * sprint and skips that sprint's state.json (in-flight items are not orphans).\n *\n * Scope: only scans pending-sync/*.md files matching the work-item-ID pattern.\n * Does NOT scan .script-incidents/ or any subdirectory.\n */\nexport function reconcileCrossSprintOrphans(opts: ReconcileOrphansOpts): ReconcileOrphansResult {\n const { deliveryRoot, sprintRunsRoot } = opts;\n\n // Terminal states from state.json (story-level states, not artifact statuses)\n const TERMINAL_STATE_JSON = new Set(['Done', 'Escalated', 'Parking Lot']);\n\n // Read the active sprint sentinel (to exclude it from orphan detection)\n let activeSprintId: string | null = null;\n try {\n activeSprintId = fs.readFileSync(path.join(sprintRunsRoot, '.active'), 'utf8').trim();\n } catch {\n // No .active file — no active sprint; scan all sprints\n }\n\n // Collect all pending-sync *.md files (no subdirectory traversal)\n const pendingDir = path.join(deliveryRoot, 'pending-sync');\n let pendingFiles: string[];\n try {\n pendingFiles = fs.readdirSync(pendingDir).filter(\n (f) => f.endsWith('.md') && !f.startsWith('.'),\n );\n } catch {\n pendingFiles = [];\n }\n\n // Build a map: id → { status, filePath } for each pending-sync item\n interface PendingItem {\n status: string;\n filePath: string;\n type: OrphanDriftItem['type'];\n }\n const pendingMap = new Map<string, PendingItem>();\n\n for (const fileName of pendingFiles) {\n const absPath = path.join(pendingDir, fileName);\n const { status } = readArtifactStatus(absPath);\n if (status === null) continue;\n // Skip already-terminal items in pending-sync (shouldn't be there but be safe)\n if (ARTIFACT_TERMINAL_STATUSES.has(status)) continue;\n\n // Extract ID from filename: filenames use <ID>_<slug>.md or <ID>.md format.\n // ID_PATTERN uses \\b word-boundaries which don't fire between a digit and '_'\n // (since '_' is a word char), so we extract the prefix before the first '_' or '.'.\n const fileNameNoExt = fileName.endsWith('.md') ? fileName.slice(0, -3) : fileName;\n const prefixPart = fileNameNoExt.split('_')[0] ?? fileNameNoExt;\n const rawId = prefixPart;\n const id = normalizeId(rawId);\n const type = idType(id);\n if (!type || type === 'PROPOSAL') continue;\n\n pendingMap.set(id, {\n status,\n filePath: path.join('pending-sync', fileName),\n type: type as OrphanDriftItem['type'],\n });\n }\n\n if (pendingMap.size === 0) {\n return { drift: [], clean: 0 };\n }\n\n // Walk sprint-runs directories for state.json files\n let sprintDirs: string[];\n try {\n sprintDirs = fs.readdirSync(sprintRunsRoot).filter((entry) => {\n // Skip the .active sentinel file and any hidden files\n if (entry.startsWith('.')) return false;\n // Skip non-directories (e.g. files in root)\n try {\n return fs.statSync(path.join(sprintRunsRoot, entry)).isDirectory();\n } catch {\n return false;\n }\n });\n } catch {\n sprintDirs = [];\n }\n\n const drift: OrphanDriftItem[] = [];\n // Track which IDs we've flagged to avoid duplicates (first sprint that shows Done wins)\n const flagged = new Set<string>();\n let clean = 0;\n\n for (const sprintDir of sprintDirs) {\n // Skip the active sprint\n if (activeSprintId && sprintDir === activeSprintId) continue;\n\n const stateFile = path.join(sprintRunsRoot, sprintDir, 'state.json');\n let stateJson: Record<string, unknown>;\n try {\n const raw = fs.readFileSync(stateFile, 'utf8');\n stateJson = JSON.parse(raw) as Record<string, unknown>;\n } catch {\n continue;\n }\n\n const stories = stateJson['stories'] as Record<string, { state: string }> | undefined;\n if (!stories || typeof stories !== 'object') continue;\n\n for (const [id, storyEntry] of Object.entries(stories)) {\n // Skip if already flagged from an earlier sprint\n if (flagged.has(id)) continue;\n\n const pending = pendingMap.get(id);\n if (!pending) continue; // not in pending-sync\n\n const stateInJson = storyEntry?.state ?? '';\n if (TERMINAL_STATE_JSON.has(stateInJson)) {\n // This item is Done in a closed sprint but still in pending-sync — orphan drift\n flagged.add(id);\n drift.push({\n id,\n type: pending.type,\n pending_sync_status: pending.status,\n state_json_state: stateInJson,\n state_json_sprint: sprintDir,\n file_path: pending.filePath,\n });\n } else {\n // Item is in pending-sync AND in state.json but NOT terminal — correctly in-flight\n clean++;\n }\n }\n }\n\n return { drift, clean };\n}\n\n// ─── reconcileDecomposition ───────────────────────────────────────────────────\n\n/**\n * Read the sprint plan's epics: and proposals: frontmatter arrays and verify\n * that each referenced epic has ≥1 child story file, and each proposal has\n * a decomposed epic.\n */\nexport function reconcileDecomposition(opts: ReconcileDecompositionOpts): ReconcileDecompositionResult {\n const { sprintPlanPath, deliveryRoot } = opts;\n\n // Parse sprint plan frontmatter\n let raw: string;\n try {\n raw = fs.readFileSync(sprintPlanPath, 'utf8');\n } catch {\n return { missing: [], clean: 0 };\n }\n\n let fm: Record<string, unknown>;\n try {\n ({ fm } = parseFrontmatter(raw));\n } catch {\n return { missing: [], clean: 0 };\n }\n\n const epics: string[] = Array.isArray(fm['epics']) ? fm['epics'].map(String) : [];\n const proposals: string[] = Array.isArray(fm['proposals']) ? fm['proposals'].map(String) : [];\n\n const pendingDir = path.join(deliveryRoot, 'pending-sync');\n const archiveDir = path.join(deliveryRoot, 'archive');\n\n // Read both dirs for all .md files\n function listMdFiles(dir: string): string[] {\n try {\n return fs.readdirSync(dir).filter((f) => f.endsWith('.md'));\n } catch {\n return [];\n }\n }\n const pendingFiles = listMdFiles(pendingDir);\n const archiveFiles = listMdFiles(archiveDir);\n const allFiles = [...pendingFiles, ...archiveFiles];\n\n const missing: MissingDecomp[] = [];\n let clean = 0;\n\n // Check epics\n for (const epicId of epics) {\n // Find the epic file\n const epicFile = allFiles.find(\n (f) => f.startsWith(`${epicId}_`) || f === `${epicId}.md`,\n );\n if (!epicFile) {\n missing.push({\n id: epicId,\n type: 'epic',\n reason: 'file-missing',\n expected_files: [`pending-sync/${epicId}_<name>.md`],\n });\n continue;\n }\n\n // Find child stories: any STORY-*.md with parent_epic_ref: epicId\n const childStories = findChildStories(\n epicId,\n pendingDir,\n pendingFiles,\n archiveDir,\n archiveFiles,\n );\n\n if (childStories.length === 0) {\n missing.push({\n id: epicId,\n type: 'epic',\n reason: 'no-child-stories',\n expected_files: [\n `pending-sync/${epicId.replace('EPIC-', 'STORY-')}-01_<name>.md`,\n ],\n });\n } else {\n clean++;\n }\n }\n\n // Check proposals\n for (const proposalId of proposals) {\n // Find a decomposed epic that cites this proposal in context_source\n const decomposedEpic = findDecomposedEpic(\n proposalId,\n pendingDir,\n pendingFiles,\n );\n if (!decomposedEpic) {\n missing.push({\n id: proposalId,\n type: 'proposal',\n reason: 'no-decomposed-epic',\n expected_files: [`pending-sync/EPIC-<NNN>_<name>.md with context_source citing ${proposalId}`],\n });\n } else {\n clean++;\n }\n }\n\n return { missing, clean };\n}\n\n/**\n * Find story files in pending-sync or archive that have parent_epic_ref: epicId.\n */\nfunction findChildStories(\n epicId: string,\n pendingDir: string,\n pendingFiles: string[],\n archiveDir: string,\n archiveFiles: string[],\n): string[] {\n const results: string[] = [];\n const epicNumMatch = /^EPIC-(\\d+)$/.exec(epicId);\n if (!epicNumMatch) return results;\n const epicNum = epicNumMatch[1]!;\n\n const storyPrefix = `STORY-${epicNum}-`;\n\n for (const [files, dir] of [[pendingFiles, pendingDir], [archiveFiles, archiveDir]] as const) {\n for (const f of files) {\n if (!f.startsWith(storyPrefix) && !f.startsWith('STORY-')) continue;\n // Quick filename match first\n if (!f.includes(storyPrefix)) continue;\n const absPath = path.join(dir, f);\n try {\n const raw = fs.readFileSync(absPath, 'utf8');\n const { fm } = parseFrontmatter(raw);\n const parentRef = fm['parent_epic_ref'];\n if (parentRef === epicId) {\n results.push(f);\n }\n } catch {\n // skip malformed files\n }\n }\n }\n return results;\n}\n\n/**\n * Find an epic file in pending-sync whose context_source cites proposalId.\n */\nfunction findDecomposedEpic(\n proposalId: string,\n pendingDir: string,\n pendingFiles: string[],\n): string | null {\n for (const f of pendingFiles) {\n if (!f.startsWith('EPIC-')) continue;\n const absPath = path.join(pendingDir, f);\n try {\n const raw = fs.readFileSync(absPath, 'utf8');\n const { fm } = parseFrontmatter(raw);\n const contextSource = fm['context_source'];\n if (\n typeof contextSource === 'string' &&\n contextSource.includes(proposalId)\n ) {\n return f;\n }\n } catch {\n // skip\n }\n }\n return null;\n}\n\n// ─── Verb mismatch checker (exported for test use) ────────────────────────────\n\n/**\n * Check if a (verb, type) combination is a mismatch (soft warning only in v1).\n * Returns a warning message or null if no mismatch.\n */\nexport function checkVerbMismatch(verb: string, type: string): string | null {\n if (verb === 'feat' && type === 'BUG') {\n return `verb 'feat' unusual for BUG; expected 'fix'`;\n }\n if (verb === 'fix' && (type === 'STORY' || type === 'EPIC' || type === 'CR')) {\n return `verb 'fix' unusual for ${type}; expected 'feat'`;\n }\n return null;\n}\n","/**\n * YAML frontmatter parser backed by js-yaml with CORE_SCHEMA (YAML 1.2 core).\n *\n * Parses `---\\n<yaml>\\n---\\n<body>` into a typed frontmatter map + body string.\n * Preserves native types (null, boolean, number, string), nested maps, and\n * arrays. Uses CORE_SCHEMA so ISO-8601 timestamp strings are NOT coerced to\n * Date objects (YAML 1.1's quirk).\n *\n * Historical note: an earlier hand-rolled parser flattened indented nested\n * maps into top-level keys and stringified null/boolean scalars. See\n * BUG-001 and FLASHCARD entry `#yaml #frontmatter`.\n */\n\nimport yaml from 'js-yaml';\n\nexport function parseFrontmatter(raw: string): { fm: Record<string, unknown>; body: string } {\n const lines = raw.split('\\n');\n if (lines[0] !== '---') {\n throw new Error('parseFrontmatter: input does not start with ---');\n }\n let closeIdx = -1;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i] === '---') { closeIdx = i; break; }\n }\n if (closeIdx === -1) {\n throw new Error('parseFrontmatter: missing closing ---');\n }\n\n const yamlText = lines.slice(1, closeIdx).join('\\n');\n const bodyLines = lines.slice(closeIdx + 1);\n // strip one leading blank line if present\n if (bodyLines[0] === '') bodyLines.shift();\n const body = bodyLines.join('\\n');\n\n if (yamlText.trim() === '') {\n return { fm: {}, body };\n }\n\n let parsed: unknown;\n try {\n parsed = yaml.load(yamlText, { schema: yaml.CORE_SCHEMA });\n } catch (err) {\n throw new Error(`parseFrontmatter: invalid YAML: ${(err as Error).message}`);\n }\n\n if (parsed === null || parsed === undefined) {\n return { fm: {}, body };\n }\n if (typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('parseFrontmatter: frontmatter is not a YAML mapping');\n }\n\n return { fm: parsed as Record<string, unknown>, body };\n}\n"],"mappings":";;;AAaA,YAAY,QAAQ;AACpB,YAAY,UAAU;AACtB,SAAS,iBAAiB;;;ACF1B,OAAO,UAAU;AAEV,SAAS,iBAAiB,KAA4D;AAC3F,QAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,MAAI,MAAM,CAAC,MAAM,OAAO;AACtB,UAAM,IAAI,MAAM,iDAAiD;AAAA,EACnE;AACA,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,MAAM,CAAC,MAAM,OAAO;AAAE,iBAAW;AAAG;AAAA,IAAO;AAAA,EACjD;AACA,MAAI,aAAa,IAAI;AACnB,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AAEA,QAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,EAAE,KAAK,IAAI;AACnD,QAAM,YAAY,MAAM,MAAM,WAAW,CAAC;AAE1C,MAAI,UAAU,CAAC,MAAM,GAAI,WAAU,MAAM;AACzC,QAAM,OAAO,UAAU,KAAK,IAAI;AAEhC,MAAI,SAAS,KAAK,MAAM,IAAI;AAC1B,WAAO,EAAE,IAAI,CAAC,GAAG,KAAK;AAAA,EACxB;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,KAAK,UAAU,EAAE,QAAQ,KAAK,YAAY,CAAC;AAAA,EAC3D,SAAS,KAAK;AACZ,UAAM,IAAI,MAAM,mCAAoC,IAAc,OAAO,EAAE;AAAA,EAC7E;AAEA,MAAI,WAAW,QAAQ,WAAW,QAAW;AAC3C,WAAO,EAAE,IAAI,CAAC,GAAG,KAAK;AAAA,EACxB;AACA,MAAI,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AACvD,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAEA,SAAO,EAAE,IAAI,QAAmC,KAAK;AACvD;;;AD3BO,IAAM,6BAA6B,oBAAI,IAAI;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQM,IAAM,kBAAqF;AAAA,EAChG,MAAM;AAAA,IACJ,OAAO,CAAC,SAAS,QAAQ,IAAI;AAAA,IAC7B,UAAU,CAAC,QAAQ,WAAW;AAAA,EAChC;AAAA,EACA,KAAK;AAAA,IACH,OAAO,CAAC,OAAO,QAAQ;AAAA,IACvB,UAAU,CAAC,YAAY,QAAQ,WAAW;AAAA,EAC5C;AACF;AAgDA,IAAM,aAAa;AAKnB,SAAS,YAAY,KAAqB;AAExC,SAAO,IAAI,QAAQ,gBAAgB,aAAa;AAClD;AAEA,SAAS,OAAO,IAAiC;AAC/C,MAAI,sBAAsB,KAAK,EAAE,EAAG,QAAO;AAC3C,MAAI,aAAa,KAAK,EAAE,EAAG,QAAO;AAClC,MAAI,cAAc,KAAK,EAAE,EAAG,QAAO;AACnC,MAAI,eAAe,KAAK,EAAE,EAAG,QAAO;AACpC,MAAI,mBAAmB,KAAK,EAAE,EAAG,QAAO;AACxC,MAAI,iBAAiB,KAAK,EAAE,EAAG,QAAO;AACtC,SAAO;AACT;AAYO,SAAS,mBACd,KACmD;AACnD,QAAM,QAAQ,IAAI,MAAM,IAAI;AAC5B,QAAM,UAAU,MAAM,CAAC,KAAK;AAG5B,MAAI,gBAAgB;AACpB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,MAAM,CAAC,GAAG,KAAK,GAAG;AACpB,sBAAgB,MAAM,CAAC;AACvB;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY,aAAa,KAAK,OAAO,KAAK,UAAU,KAAK,OAAO;AACtE,QAAM,OAAO,YAAY,UAAU,CAAC,EAAG,YAAY,IAAI;AAGvD,QAAM,aAAa,WAAW,gBAAgB,OAAO,gBAAgB;AACrE,QAAM,UAA6D,CAAC;AACpE,QAAM,OAAO,oBAAI,IAAY;AAE7B,MAAI;AACJ,aAAW,YAAY;AACvB,UAAQ,IAAI,WAAW,KAAK,UAAU,OAAO,MAAM;AACjD,UAAM,QAAQ,EAAE,CAAC;AACjB,UAAM,KAAK,YAAY,KAAK;AAC5B,QAAI,KAAK,IAAI,EAAE,EAAG;AAClB,SAAK,IAAI,EAAE;AACX,UAAM,OAAO,OAAO,EAAE;AACtB,QAAI,CAAC,KAAM;AACX,YAAQ,KAAK,EAAE,MAAM,IAAI,KAAK,CAAC;AAAA,EACjC;AAEA,SAAO;AACT;AAUA,SAAS,iBAAiB,cAAsB,IAA8B;AAC5E,QAAM,SAAS,GAAG,EAAE;AACpB,QAAM,OAAmD;AAAA,IACvD,EAAE,KAAK,gBAAgB,WAAW,MAAM;AAAA,IACxC,EAAE,KAAK,WAAW,WAAW,KAAK;AAAA,EACpC;AACA,aAAW,EAAE,KAAK,UAAU,KAAK,MAAM;AACrC,UAAM,MAAW,UAAK,cAAc,GAAG;AACvC,QAAI;AACJ,QAAI;AACF,gBAAa,eAAY,GAAG;AAAA,IAC9B,QAAQ;AACN;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ;AAAA,MACpB,CAAC,OAAO,EAAE,WAAW,MAAM,KAAK,MAAM,GAAG,EAAE,UAAU,EAAE,SAAS,KAAK;AAAA,IACvE;AACA,QAAI,OAAO;AACT,YAAM,UAAe,UAAK,KAAK,KAAK;AACpC,aAAO,EAAE,SAAS,WAAW,SAAS,GAAG,GAAG,IAAI,KAAK,GAAG;AAAA,IAC1D;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,mBAAmB,SAAgE;AAC1F,MAAI;AACJ,MAAI;AACF,UAAS,gBAAa,SAAS,MAAM;AAAA,EACvC,QAAQ;AACN,WAAO,EAAE,QAAQ,MAAM,WAAW,MAAM;AAAA,EAC1C;AACA,MAAI;AACF,UAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,UAAM,SAAS,OAAO,GAAG,QAAQ,MAAM,WAAW,GAAG,QAAQ,IAAI;AACjE,UAAM,YAAY,GAAG,YAAY,MAAM;AACvC,WAAO,EAAE,QAAQ,UAAU;AAAA,EAC7B,QAAQ;AACN,WAAO,EAAE,QAAQ,MAAM,WAAW,MAAM;AAAA,EAC1C;AACF;AAaO,SAAS,mBAAmB,MAAwD;AACzF,QAAM,EAAE,OAAO,QAAQ,oBAAI,KAAK,GAAG,cAAc,SAAS,IAAI;AAE9D,QAAM,YACJ,KAAK,cACJ,CAAC,KAAa,SAAmB;AAChC,UAAM,SAAS,UAAU,KAAK,MAAM,EAAE,UAAU,QAAQ,KAAK,SAAS,CAAC;AACvE,WAAQ,OAAO,UAAU;AAAA,EAC3B;AAGF,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,YAAY,UAAU,OAAO;AAAA,IACjC;AAAA,IACA,WAAW,QAAQ;AAAA,IACnB,YAAY,QAAQ;AAAA,IACpB;AAAA,IACA;AAAA,EACF,CAAC;AAID,QAAM,WAAW,oBAAI,IAAuB;AAE5C,QAAM,WAAW,oBAAI,IAAY;AAEjC,MAAI,UAAU,KAAK,GAAG;AAEpB,UAAM,aAAa,UAAU,MAAM,gBAAgB,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC;AAE3E,eAAW,OAAO,YAAY;AAE5B,YAAM,CAAC,MAAM,IAAI,UAAU,IAAI,OAAO,EAAE,IAAI,IAAI,MAAM,IAAM;AAC5D,YAAM,UAAU,IAAI,KAAK;AACzB,YAAM,cAAc,QAAQ,KAAK;AACjC,YAAM,WAAW,KAAK,KAAK;AAE3B,UAAI,CAAC,WAAW,CAAC,YAAa;AAE9B,YAAM,YAAY,eAAe,WAAW,SAAS,WAAW;AAChE,YAAM,SAAS,mBAAmB,SAAS;AAE3C,iBAAW,EAAE,MAAM,IAAI,KAAK,KAAK,QAAQ;AAEvC,YAAI,SAAS,WAAW,SAAS,WAAW,SAAS,UAAU,SAAS,cACnE,SAAS,UAAU,SAAS,UAAU,SAAS,QAAQ;AAC1D;AAAA,QACF;AAGA,YAAI,SAAS,WAAY;AAEzB,cAAM,aAAa,gBAAgB,IAAI;AACvC,YAAI,CAAC,WAAY;AAMjB,cAAM,QAAQ,iBAAiB,cAAc,EAAE;AAC/C,YAAI,CAAC,OAAO;AAGV;AAAA,QACF;AAGA,cAAM,EAAE,QAAQ,UAAU,IAAI,mBAAmB,MAAM,OAAO;AAG9D,YAAI,UAAW;AAGf,YAAI;AACJ,YAAI,SAAS,UAAU,SAAS,OAAO;AAGrC,6BAAmB,CAAC,YAAY,QAAQ,WAAW;AAAA,QACrD,WAAW,CAAC,WAAW,MAAM,SAAS,IAAI,GAAG;AAE3C;AAAA,QACF,OAAO;AACL,6BAAmB,WAAW;AAAA,QAChC;AAEA,cAAM,aAAa,WAAW,QAAQ,iBAAiB,SAAS,MAAM;AACtE,cAAM,aAAa,MAAM;AAEzB,YAAI,cAAc,YAAY;AAE5B,mBAAS,IAAI,EAAE;AAGf,mBAAS,OAAO,EAAE;AAAA,QACpB,WAAW,CAAC,SAAS,IAAI,EAAE,GAAG;AAE5B,gBAAM,cAAc,iBAAiB,CAAC,KAAK;AAC3C,mBAAS,IAAI,IAAI;AAAA,YACf;AAAA,YACA;AAAA,YACA,iBAAiB;AAAA,YACjB,eAAe;AAAA,YACf,WAAW,MAAM;AAAA,YACjB,YAAY;AAAA,YACZ,aAAa,CAAC,OAAO;AAAA,YACrB,YAAY;AAAA,UACd,CAAC;AAAA,QACH,OAAO;AAEL,gBAAM,WAAW,SAAS,IAAI,EAAE;AAChC,cAAI,CAAC,SAAS,YAAY,SAAS,OAAO,GAAG;AAC3C,qBAAS,YAAY,KAAK,OAAO;AAAA,UACnC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,aAAW,MAAM,UAAU;AACzB,aAAS,OAAO,EAAE;AAAA,EACpB;AAEA,QAAM,QAAQ,MAAM,KAAK,SAAS,OAAO,CAAC;AAC1C,SAAO,EAAE,OAAO,OAAO,SAAS,KAAK;AACvC;AAyCO,SAAS,4BAA4B,MAAoD;AAC9F,QAAM,EAAE,cAAc,eAAe,IAAI;AAGzC,QAAM,sBAAsB,oBAAI,IAAI,CAAC,QAAQ,aAAa,aAAa,CAAC;AAGxE,MAAI,iBAAgC;AACpC,MAAI;AACF,qBAAoB,gBAAkB,UAAK,gBAAgB,SAAS,GAAG,MAAM,EAAE,KAAK;AAAA,EACtF,QAAQ;AAAA,EAER;AAGA,QAAM,aAAkB,UAAK,cAAc,cAAc;AACzD,MAAI;AACJ,MAAI;AACF,mBAAkB,eAAY,UAAU,EAAE;AAAA,MACxC,CAAC,MAAM,EAAE,SAAS,KAAK,KAAK,CAAC,EAAE,WAAW,GAAG;AAAA,IAC/C;AAAA,EACF,QAAQ;AACN,mBAAe,CAAC;AAAA,EAClB;AAQA,QAAM,aAAa,oBAAI,IAAyB;AAEhD,aAAW,YAAY,cAAc;AACnC,UAAM,UAAe,UAAK,YAAY,QAAQ;AAC9C,UAAM,EAAE,OAAO,IAAI,mBAAmB,OAAO;AAC7C,QAAI,WAAW,KAAM;AAErB,QAAI,2BAA2B,IAAI,MAAM,EAAG;AAK5C,UAAM,gBAAgB,SAAS,SAAS,KAAK,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;AACzE,UAAM,aAAa,cAAc,MAAM,GAAG,EAAE,CAAC,KAAK;AAClD,UAAM,QAAQ;AACd,UAAM,KAAK,YAAY,KAAK;AAC5B,UAAM,OAAO,OAAO,EAAE;AACtB,QAAI,CAAC,QAAQ,SAAS,WAAY;AAElC,eAAW,IAAI,IAAI;AAAA,MACjB;AAAA,MACA,UAAe,UAAK,gBAAgB,QAAQ;AAAA,MAC5C;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,WAAW,SAAS,GAAG;AACzB,WAAO,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE;AAAA,EAC/B;AAGA,MAAI;AACJ,MAAI;AACF,iBAAgB,eAAY,cAAc,EAAE,OAAO,CAAC,UAAU;AAE5D,UAAI,MAAM,WAAW,GAAG,EAAG,QAAO;AAElC,UAAI;AACF,eAAU,YAAc,UAAK,gBAAgB,KAAK,CAAC,EAAE,YAAY;AAAA,MACnE,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC;AAAA,EACH,QAAQ;AACN,iBAAa,CAAC;AAAA,EAChB;AAEA,QAAM,QAA2B,CAAC;AAElC,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,QAAQ;AAEZ,aAAW,aAAa,YAAY;AAElC,QAAI,kBAAkB,cAAc,eAAgB;AAEpD,UAAM,YAAiB,UAAK,gBAAgB,WAAW,YAAY;AACnE,QAAI;AACJ,QAAI;AACF,YAAM,MAAS,gBAAa,WAAW,MAAM;AAC7C,kBAAY,KAAK,MAAM,GAAG;AAAA,IAC5B,QAAQ;AACN;AAAA,IACF;AAEA,UAAM,UAAU,UAAU,SAAS;AACnC,QAAI,CAAC,WAAW,OAAO,YAAY,SAAU;AAE7C,eAAW,CAAC,IAAI,UAAU,KAAK,OAAO,QAAQ,OAAO,GAAG;AAEtD,UAAI,QAAQ,IAAI,EAAE,EAAG;AAErB,YAAM,UAAU,WAAW,IAAI,EAAE;AACjC,UAAI,CAAC,QAAS;AAEd,YAAM,cAAc,YAAY,SAAS;AACzC,UAAI,oBAAoB,IAAI,WAAW,GAAG;AAExC,gBAAQ,IAAI,EAAE;AACd,cAAM,KAAK;AAAA,UACT;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,qBAAqB,QAAQ;AAAA,UAC7B,kBAAkB;AAAA,UAClB,mBAAmB;AAAA,UACnB,WAAW,QAAQ;AAAA,QACrB,CAAC;AAAA,MACH,OAAO;AAEL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM;AACxB;AASO,SAAS,uBAAuB,MAAgE;AACrG,QAAM,EAAE,gBAAgB,aAAa,IAAI;AAGzC,MAAI;AACJ,MAAI;AACF,UAAS,gBAAa,gBAAgB,MAAM;AAAA,EAC9C,QAAQ;AACN,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,EACjC;AAEA,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,GAAG,IAAI,iBAAiB,GAAG;AAAA,EAChC,QAAQ;AACN,WAAO,EAAE,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,EACjC;AAEA,QAAM,QAAkB,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,MAAM,IAAI,CAAC;AAChF,QAAM,YAAsB,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,GAAG,WAAW,EAAE,IAAI,MAAM,IAAI,CAAC;AAE5F,QAAM,aAAkB,UAAK,cAAc,cAAc;AACzD,QAAM,aAAkB,UAAK,cAAc,SAAS;AAGpD,WAAS,YAAY,KAAuB;AAC1C,QAAI;AACF,aAAU,eAAY,GAAG,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC;AAAA,IAC5D,QAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACA,QAAM,eAAe,YAAY,UAAU;AAC3C,QAAM,eAAe,YAAY,UAAU;AAC3C,QAAM,WAAW,CAAC,GAAG,cAAc,GAAG,YAAY;AAElD,QAAM,UAA2B,CAAC;AAClC,MAAI,QAAQ;AAGZ,aAAW,UAAU,OAAO;AAE1B,UAAM,WAAW,SAAS;AAAA,MACxB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,KAAK,MAAM,GAAG,MAAM;AAAA,IACtD;AACA,QAAI,CAAC,UAAU;AACb,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB,CAAC,gBAAgB,MAAM,YAAY;AAAA,MACrD,CAAC;AACD;AAAA,IACF;AAGA,UAAM,eAAe;AAAA,MACnB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,aAAa,WAAW,GAAG;AAC7B,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB;AAAA,UACd,gBAAgB,OAAO,QAAQ,SAAS,QAAQ,CAAC;AAAA,QACnD;AAAA,MACF,CAAC;AAAA,IACH,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAGA,aAAW,cAAc,WAAW;AAElC,UAAM,iBAAiB;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,gBAAgB;AACnB,cAAQ,KAAK;AAAA,QACX,IAAI;AAAA,QACJ,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,gBAAgB,CAAC,gEAAgE,UAAU,EAAE;AAAA,MAC/F,CAAC;AAAA,IACH,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,SAAS,MAAM;AAC1B;AAKA,SAAS,iBACP,QACA,YACA,cACA,YACA,cACU;AACV,QAAM,UAAoB,CAAC;AAC3B,QAAM,eAAe,eAAe,KAAK,MAAM;AAC/C,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,UAAU,aAAa,CAAC;AAE9B,QAAM,cAAc,SAAS,OAAO;AAEpC,aAAW,CAAC,OAAO,GAAG,KAAK,CAAC,CAAC,cAAc,UAAU,GAAG,CAAC,cAAc,UAAU,CAAC,GAAY;AAC5F,eAAW,KAAK,OAAO;AACrB,UAAI,CAAC,EAAE,WAAW,WAAW,KAAK,CAAC,EAAE,WAAW,QAAQ,EAAG;AAE3D,UAAI,CAAC,EAAE,SAAS,WAAW,EAAG;AAC9B,YAAM,UAAe,UAAK,KAAK,CAAC;AAChC,UAAI;AACF,cAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,cAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,cAAM,YAAY,GAAG,iBAAiB;AACtC,YAAI,cAAc,QAAQ;AACxB,kBAAQ,KAAK,CAAC;AAAA,QAChB;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,mBACP,YACA,YACA,cACe;AACf,aAAW,KAAK,cAAc;AAC5B,QAAI,CAAC,EAAE,WAAW,OAAO,EAAG;AAC5B,UAAM,UAAe,UAAK,YAAY,CAAC;AACvC,QAAI;AACF,YAAM,MAAS,gBAAa,SAAS,MAAM;AAC3C,YAAM,EAAE,GAAG,IAAI,iBAAiB,GAAG;AACnC,YAAM,gBAAgB,GAAG,gBAAgB;AACzC,UACE,OAAO,kBAAkB,YACzB,cAAc,SAAS,UAAU,GACjC;AACA,eAAO;AAAA,MACT;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AAQO,SAAS,kBAAkB,MAAc,MAA6B;AAC3E,MAAI,SAAS,UAAU,SAAS,OAAO;AACrC,WAAO;AAAA,EACT;AACA,MAAI,SAAS,UAAU,SAAS,WAAW,SAAS,UAAU,SAAS,OAAO;AAC5E,WAAO,0BAA0B,IAAI;AAAA,EACvC;AACA,SAAO;AACT;","names":[]}