cleargate 0.8.1 → 0.10.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 (98) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +11 -0
  3. package/dist/MANIFEST.json +259 -28
  4. package/dist/{chunk-OM4FAEA7.js → chunk-Q3BTSXCK.js} +69 -3
  5. package/dist/chunk-Q3BTSXCK.js.map +1 -0
  6. package/dist/cli.cjs +2621 -548
  7. package/dist/cli.cjs.map +1 -1
  8. package/dist/cli.js +2548 -560
  9. package/dist/cli.js.map +1 -1
  10. package/dist/lib/ledger.cjs +120 -0
  11. package/dist/lib/ledger.cjs.map +1 -0
  12. package/dist/lib/ledger.d.cts +64 -0
  13. package/dist/lib/ledger.d.ts +64 -0
  14. package/dist/lib/ledger.js +96 -0
  15. package/dist/lib/ledger.js.map +1 -0
  16. package/dist/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  17. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  18. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  19. package/dist/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  20. package/dist/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  21. package/dist/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  22. package/dist/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  23. package/dist/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  24. package/dist/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  25. package/dist/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  26. package/dist/templates/cleargate-planning/.claude/settings.json +4 -0
  27. package/dist/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  28. package/dist/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  29. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  30. package/dist/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  31. package/dist/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  32. package/dist/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  33. package/dist/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  34. package/dist/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  35. package/dist/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  36. package/dist/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  37. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  38. package/dist/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  39. package/dist/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  40. package/dist/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  41. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  42. package/dist/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  43. package/dist/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  44. package/dist/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
  45. package/dist/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
  46. package/dist/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  47. package/dist/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
  48. package/dist/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
  49. package/dist/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  50. package/dist/templates/cleargate-planning/.cleargate/templates/proposal.md +26 -13
  51. package/dist/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  52. package/dist/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
  53. package/dist/templates/cleargate-planning/CLAUDE.md +28 -10
  54. package/dist/templates/cleargate-planning/MANIFEST.json +259 -28
  55. package/dist/{whoami-CX7CXJD5.js → whoami-W4U6DPVG.js} +17 -17
  56. package/dist/whoami-W4U6DPVG.js.map +1 -0
  57. package/package.json +13 -2
  58. package/templates/cleargate-planning/.claude/agents/architect.md +10 -8
  59. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-contradict.md +108 -0
  60. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-ingest.md +49 -3
  61. package/templates/cleargate-planning/.claude/agents/cleargate-wiki-lint.md +6 -1
  62. package/templates/cleargate-planning/.claude/agents/developer.md +29 -2
  63. package/templates/cleargate-planning/.claude/agents/qa.md +50 -1
  64. package/templates/cleargate-planning/.claude/agents/reporter.md +31 -9
  65. package/templates/cleargate-planning/.claude/hooks/pre-tool-use-task.sh +148 -0
  66. package/templates/cleargate-planning/.claude/hooks/session-start.sh +6 -0
  67. package/templates/cleargate-planning/.claude/hooks/token-ledger.sh +314 -96
  68. package/templates/cleargate-planning/.claude/settings.json +4 -0
  69. package/templates/cleargate-planning/.claude/skills/sprint-execution/SKILL.md +473 -0
  70. package/templates/cleargate-planning/.cleargate/config.example.yml +19 -0
  71. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-enforcement.md +542 -0
  72. package/templates/cleargate-planning/.cleargate/knowledge/cleargate-protocol.md +102 -428
  73. package/templates/cleargate-planning/.cleargate/knowledge/readiness-gates.md +31 -0
  74. package/templates/cleargate-planning/.cleargate/knowledge/sprint-closeout-checklist.md +71 -0
  75. package/templates/cleargate-planning/.cleargate/scripts/assert_story_files.mjs +24 -2
  76. package/templates/cleargate-planning/.cleargate/scripts/close_sprint.mjs +387 -27
  77. package/templates/cleargate-planning/.cleargate/scripts/dedupe_frontmatter.mjs +219 -0
  78. package/templates/cleargate-planning/.cleargate/scripts/lib/report-filename.mjs +54 -0
  79. package/templates/cleargate-planning/.cleargate/scripts/prep_doc_refresh.mjs +378 -0
  80. package/templates/cleargate-planning/.cleargate/scripts/prep_qa_context.mjs +888 -0
  81. package/templates/cleargate-planning/.cleargate/scripts/sprint_trends.mjs +71 -0
  82. package/templates/cleargate-planning/.cleargate/scripts/suggest_improvements.mjs +355 -13
  83. package/templates/cleargate-planning/.cleargate/scripts/test/test_flashcard_gate.sh +20 -20
  84. package/templates/cleargate-planning/.cleargate/scripts/test/test_prep_qa_context.sh +482 -0
  85. package/templates/cleargate-planning/.cleargate/scripts/write_dispatch.sh +125 -0
  86. package/templates/cleargate-planning/.cleargate/templates/Bug.md +33 -10
  87. package/templates/cleargate-planning/.cleargate/templates/CR.md +41 -10
  88. package/templates/cleargate-planning/.cleargate/templates/Sprint Plan Template.md +48 -14
  89. package/templates/cleargate-planning/.cleargate/templates/epic.md +46 -12
  90. package/templates/cleargate-planning/.cleargate/templates/hotfix.md +51 -1
  91. package/templates/cleargate-planning/.cleargate/templates/initiative.md +98 -29
  92. package/templates/cleargate-planning/.cleargate/templates/sprint_report.md +23 -4
  93. package/templates/cleargate-planning/.cleargate/templates/story.md +64 -12
  94. package/templates/cleargate-planning/CLAUDE.md +28 -10
  95. package/templates/cleargate-planning/MANIFEST.json +259 -28
  96. package/dist/chunk-OM4FAEA7.js.map +0 -1
  97. package/dist/whoami-CX7CXJD5.js.map +0 -1
  98. package/templates/cleargate-planning/.cleargate/templates/proposal.md +0 -61
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/lib/ledger.ts
22
+ var ledger_exports = {};
23
+ __export(ledger_exports, {
24
+ sumDeltas: () => sumDeltas
25
+ });
26
+ module.exports = __toCommonJS(ledger_exports);
27
+ function isTokenCounts(v) {
28
+ if (typeof v !== "object" || v === null) return false;
29
+ const obj = v;
30
+ return typeof obj["input"] === "number" && typeof obj["output"] === "number" && typeof obj["cache_creation"] === "number" && typeof obj["cache_read"] === "number";
31
+ }
32
+ function hasDelta(row) {
33
+ return isTokenCounts(row["delta"]);
34
+ }
35
+ function zeroCounts() {
36
+ return { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
37
+ }
38
+ function addCounts(a, b) {
39
+ return {
40
+ input: a.input + b.input,
41
+ output: a.output + b.output,
42
+ cache_creation: a.cache_creation + b.cache_creation,
43
+ cache_read: a.cache_read + b.cache_read
44
+ };
45
+ }
46
+ function lastRowTrickForFlatRows(rows) {
47
+ const sessionMap = /* @__PURE__ */ new Map();
48
+ for (const row of rows) {
49
+ const sessionId = typeof row["session_id"] === "string" ? row["session_id"] : "(unknown)";
50
+ const existing = sessionMap.get(sessionId);
51
+ if (!existing) {
52
+ sessionMap.set(sessionId, row);
53
+ } else {
54
+ const existingTs = typeof existing["ts"] === "string" ? existing["ts"] : "";
55
+ const rowTs = typeof row["ts"] === "string" ? row["ts"] : "";
56
+ if (rowTs > existingTs) {
57
+ sessionMap.set(sessionId, row);
58
+ }
59
+ }
60
+ }
61
+ let totals = zeroCounts();
62
+ for (const row of sessionMap.values()) {
63
+ totals = addCounts(totals, {
64
+ input: typeof row["input"] === "number" ? row["input"] : 0,
65
+ output: typeof row["output"] === "number" ? row["output"] : 0,
66
+ cache_creation: typeof row["cache_creation"] === "number" ? row["cache_creation"] : 0,
67
+ cache_read: typeof row["cache_read"] === "number" ? row["cache_read"] : 0
68
+ });
69
+ }
70
+ return totals;
71
+ }
72
+ function sumDeltas(rows) {
73
+ const validRows = [];
74
+ for (const raw of rows) {
75
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
76
+ validRows.push(raw);
77
+ } else {
78
+ console.warn("[ledger.ts] sumDeltas: skipping malformed row (not an object):", raw);
79
+ }
80
+ }
81
+ if (validRows.length === 0) {
82
+ return { totals: zeroCounts(), format: "delta" };
83
+ }
84
+ const deltaRows = validRows.filter(hasDelta);
85
+ const flatRows = validRows.filter((r) => !hasDelta(r));
86
+ if (flatRows.length === 0) {
87
+ let totals2 = zeroCounts();
88
+ for (const row of deltaRows) {
89
+ const d = row["delta"];
90
+ totals2 = addCounts(totals2, d);
91
+ }
92
+ return { totals: totals2, format: "delta" };
93
+ }
94
+ if (deltaRows.length === 0) {
95
+ const totals2 = lastRowTrickForFlatRows(flatRows);
96
+ return {
97
+ totals: totals2,
98
+ format: "pre-0.9.0",
99
+ pre_v2_caveat: "**Ledger format note:** This sprint's token-ledger.jsonl uses pre-0.9.0 flat-field rows; cost is computed via the last-row-per-session trick (reconciliation accuracy \xB1N \xD7 real-cost where N = SubagentStop fires per session)."
100
+ };
101
+ }
102
+ let totals = zeroCounts();
103
+ for (const row of deltaRows) {
104
+ const d = row["delta"];
105
+ totals = addCounts(totals, d);
106
+ }
107
+ const flatTotals = lastRowTrickForFlatRows(flatRows);
108
+ totals = addCounts(totals, flatTotals);
109
+ const caveat = `Mixed format ledger: ${deltaRows.length} delta rows + ${flatRows.length} pre-0.9.0 rows; flat segment uses last-row trick.`;
110
+ return {
111
+ totals,
112
+ format: "mixed",
113
+ pre_v2_caveat: caveat
114
+ };
115
+ }
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ sumDeltas
119
+ });
120
+ //# sourceMappingURL=ledger.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/ledger.ts"],"sourcesContent":["/**\n * ledger.ts — CR-018\n *\n * Sprint-wide cost math: parses token-ledger.jsonl rows and sums delta tokens\n * for the Reporter agent. Distinct from ledger-reader.ts (per-work-item\n * attribution lookup); this file provides sumDeltas() for sprint-total cost.\n *\n * Format detection handles three cases:\n * 'delta' — all rows carry delta.* blocks (post-0.9.0)\n * 'pre-0.9.0' — all rows use flat input/output/cache_* fields (pre-0.9.0)\n * 'mixed' — some rows have delta, others don't (SPRINT-15 cutover window)\n */\n\n// ─── Public types ─────────────────────────────────────────────────────────────\n\n/** Token counts block (used in both delta and session_total). */\nexport interface TokenCounts {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\n\n/** Full post-0.9.0 ledger row shape. */\nexport interface LedgerRowV2 {\n ts: string;\n sprint_id: string;\n story_id: string;\n work_item_id: string;\n agent_type: string;\n session_id: string;\n transcript?: string;\n sentinel_started_at?: string;\n delta_from_turn?: number;\n delta: TokenCounts;\n session_total: TokenCounts;\n model: string;\n turns: number;\n}\n\n/** Result returned by sumDeltas(). */\nexport interface SumResult {\n /** Aggregated token totals across all rows (using appropriate format). */\n totals: TokenCounts;\n /** How the totals were computed. */\n format: 'delta' | 'pre-0.9.0' | 'mixed';\n /**\n * Present when format is 'pre-0.9.0' or 'mixed'.\n * Reporter should paste this verbatim into REPORT.md §3.\n */\n pre_v2_caveat?: string;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nfunction isTokenCounts(v: unknown): v is TokenCounts {\n if (typeof v !== 'object' || v === null) return false;\n const obj = v as Record<string, unknown>;\n return (\n typeof obj['input'] === 'number' &&\n typeof obj['output'] === 'number' &&\n typeof obj['cache_creation'] === 'number' &&\n typeof obj['cache_read'] === 'number'\n );\n}\n\nfunction hasDelta(row: Record<string, unknown>): boolean {\n return isTokenCounts(row['delta']);\n}\n\nfunction zeroCounts(): TokenCounts {\n return { input: 0, output: 0, cache_creation: 0, cache_read: 0 };\n}\n\nfunction addCounts(a: TokenCounts, b: TokenCounts): TokenCounts {\n return {\n input: a.input + b.input,\n output: a.output + b.output,\n cache_creation: a.cache_creation + b.cache_creation,\n cache_read: a.cache_read + b.cache_read,\n };\n}\n\n/**\n * Last-row-per-session trick for pre-0.9.0 flat-field rows.\n * Groups rows by session_id, takes the row with max ts per group,\n * and sums the flat input/output/cache_* fields across those last rows.\n */\nfunction lastRowTrickForFlatRows(rows: Record<string, unknown>[]): TokenCounts {\n const sessionMap = new Map<string, Record<string, unknown>>();\n\n for (const row of rows) {\n const sessionId = typeof row['session_id'] === 'string' ? row['session_id'] : '(unknown)';\n const existing = sessionMap.get(sessionId);\n if (!existing) {\n sessionMap.set(sessionId, row);\n } else {\n const existingTs = typeof existing['ts'] === 'string' ? existing['ts'] : '';\n const rowTs = typeof row['ts'] === 'string' ? row['ts'] : '';\n if (rowTs > existingTs) {\n sessionMap.set(sessionId, row);\n }\n }\n }\n\n let totals = zeroCounts();\n for (const row of sessionMap.values()) {\n totals = addCounts(totals, {\n input: typeof row['input'] === 'number' ? row['input'] : 0,\n output: typeof row['output'] === 'number' ? row['output'] : 0,\n cache_creation: typeof row['cache_creation'] === 'number' ? row['cache_creation'] : 0,\n cache_read: typeof row['cache_read'] === 'number' ? row['cache_read'] : 0,\n });\n }\n return totals;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Compute sprint-wide token totals from an array of raw ledger rows (parsed JSONL).\n *\n * Format-detection algorithm (mandatory):\n * 1. Classify each row: has `delta` object with 4 numeric fields → v2; else → flat/legacy.\n * 2. All v2 → format='delta', sum delta.* directly.\n * 3. All flat → format='pre-0.9.0', group by session_id, last-row-per-session trick.\n * 4. Mixed → format='mixed', delta rows contribute delta.*; flat rows within\n * each session use last-row trick scoped to flat rows only.\n *\n * Malformed or unrecognisable rows are skipped with a console.warn.\n *\n * @param rows Array of parsed JSON objects (unknown type for defensive parsing).\n */\nexport function sumDeltas(rows: unknown[]): SumResult {\n // Filter to recognisable objects; skip malformed rows.\n const validRows: Record<string, unknown>[] = [];\n for (const raw of rows) {\n if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {\n validRows.push(raw as Record<string, unknown>);\n } else {\n console.warn('[ledger.ts] sumDeltas: skipping malformed row (not an object):', raw);\n }\n }\n\n if (validRows.length === 0) {\n return { totals: zeroCounts(), format: 'delta' };\n }\n\n const deltaRows = validRows.filter(hasDelta);\n const flatRows = validRows.filter((r) => !hasDelta(r));\n\n // All rows are v2 (delta) format\n if (flatRows.length === 0) {\n let totals = zeroCounts();\n for (const row of deltaRows) {\n const d = row['delta'] as TokenCounts;\n totals = addCounts(totals, d);\n }\n return { totals, format: 'delta' };\n }\n\n // All rows are pre-0.9.0 flat format\n if (deltaRows.length === 0) {\n const totals = lastRowTrickForFlatRows(flatRows);\n return {\n totals,\n format: 'pre-0.9.0',\n pre_v2_caveat:\n '**Ledger format note:** This sprint\\'s token-ledger.jsonl uses pre-0.9.0 flat-field rows; ' +\n 'cost is computed via the last-row-per-session trick ' +\n '(reconciliation accuracy ±N × real-cost where N = SubagentStop fires per session).',\n };\n }\n\n // Mixed format: both delta rows and flat rows in same ledger (e.g. SPRINT-15 cutover)\n // Delta rows → sum delta.*\n // Flat rows → group by session_id, last-row trick (scoped to flat rows only)\n let totals = zeroCounts();\n\n // Sum delta rows directly\n for (const row of deltaRows) {\n const d = row['delta'] as TokenCounts;\n totals = addCounts(totals, d);\n }\n\n // Apply last-row trick to flat rows\n const flatTotals = lastRowTrickForFlatRows(flatRows);\n totals = addCounts(totals, flatTotals);\n\n const caveat =\n `Mixed format ledger: ${deltaRows.length} delta rows + ${flatRows.length} pre-0.9.0 rows; ` +\n `flat segment uses last-row trick.`;\n\n return {\n totals,\n format: 'mixed',\n pre_v2_caveat: caveat,\n };\n}\n"],"mappings":"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","names":["totals"]}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ledger.ts — CR-018
3
+ *
4
+ * Sprint-wide cost math: parses token-ledger.jsonl rows and sums delta tokens
5
+ * for the Reporter agent. Distinct from ledger-reader.ts (per-work-item
6
+ * attribution lookup); this file provides sumDeltas() for sprint-total cost.
7
+ *
8
+ * Format detection handles three cases:
9
+ * 'delta' — all rows carry delta.* blocks (post-0.9.0)
10
+ * 'pre-0.9.0' — all rows use flat input/output/cache_* fields (pre-0.9.0)
11
+ * 'mixed' — some rows have delta, others don't (SPRINT-15 cutover window)
12
+ */
13
+ /** Token counts block (used in both delta and session_total). */
14
+ interface TokenCounts {
15
+ input: number;
16
+ output: number;
17
+ cache_creation: number;
18
+ cache_read: number;
19
+ }
20
+ /** Full post-0.9.0 ledger row shape. */
21
+ interface LedgerRowV2 {
22
+ ts: string;
23
+ sprint_id: string;
24
+ story_id: string;
25
+ work_item_id: string;
26
+ agent_type: string;
27
+ session_id: string;
28
+ transcript?: string;
29
+ sentinel_started_at?: string;
30
+ delta_from_turn?: number;
31
+ delta: TokenCounts;
32
+ session_total: TokenCounts;
33
+ model: string;
34
+ turns: number;
35
+ }
36
+ /** Result returned by sumDeltas(). */
37
+ interface SumResult {
38
+ /** Aggregated token totals across all rows (using appropriate format). */
39
+ totals: TokenCounts;
40
+ /** How the totals were computed. */
41
+ format: 'delta' | 'pre-0.9.0' | 'mixed';
42
+ /**
43
+ * Present when format is 'pre-0.9.0' or 'mixed'.
44
+ * Reporter should paste this verbatim into REPORT.md §3.
45
+ */
46
+ pre_v2_caveat?: string;
47
+ }
48
+ /**
49
+ * Compute sprint-wide token totals from an array of raw ledger rows (parsed JSONL).
50
+ *
51
+ * Format-detection algorithm (mandatory):
52
+ * 1. Classify each row: has `delta` object with 4 numeric fields → v2; else → flat/legacy.
53
+ * 2. All v2 → format='delta', sum delta.* directly.
54
+ * 3. All flat → format='pre-0.9.0', group by session_id, last-row-per-session trick.
55
+ * 4. Mixed → format='mixed', delta rows contribute delta.*; flat rows within
56
+ * each session use last-row trick scoped to flat rows only.
57
+ *
58
+ * Malformed or unrecognisable rows are skipped with a console.warn.
59
+ *
60
+ * @param rows Array of parsed JSON objects (unknown type for defensive parsing).
61
+ */
62
+ declare function sumDeltas(rows: unknown[]): SumResult;
63
+
64
+ export { type LedgerRowV2, type SumResult, type TokenCounts, sumDeltas };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * ledger.ts — CR-018
3
+ *
4
+ * Sprint-wide cost math: parses token-ledger.jsonl rows and sums delta tokens
5
+ * for the Reporter agent. Distinct from ledger-reader.ts (per-work-item
6
+ * attribution lookup); this file provides sumDeltas() for sprint-total cost.
7
+ *
8
+ * Format detection handles three cases:
9
+ * 'delta' — all rows carry delta.* blocks (post-0.9.0)
10
+ * 'pre-0.9.0' — all rows use flat input/output/cache_* fields (pre-0.9.0)
11
+ * 'mixed' — some rows have delta, others don't (SPRINT-15 cutover window)
12
+ */
13
+ /** Token counts block (used in both delta and session_total). */
14
+ interface TokenCounts {
15
+ input: number;
16
+ output: number;
17
+ cache_creation: number;
18
+ cache_read: number;
19
+ }
20
+ /** Full post-0.9.0 ledger row shape. */
21
+ interface LedgerRowV2 {
22
+ ts: string;
23
+ sprint_id: string;
24
+ story_id: string;
25
+ work_item_id: string;
26
+ agent_type: string;
27
+ session_id: string;
28
+ transcript?: string;
29
+ sentinel_started_at?: string;
30
+ delta_from_turn?: number;
31
+ delta: TokenCounts;
32
+ session_total: TokenCounts;
33
+ model: string;
34
+ turns: number;
35
+ }
36
+ /** Result returned by sumDeltas(). */
37
+ interface SumResult {
38
+ /** Aggregated token totals across all rows (using appropriate format). */
39
+ totals: TokenCounts;
40
+ /** How the totals were computed. */
41
+ format: 'delta' | 'pre-0.9.0' | 'mixed';
42
+ /**
43
+ * Present when format is 'pre-0.9.0' or 'mixed'.
44
+ * Reporter should paste this verbatim into REPORT.md §3.
45
+ */
46
+ pre_v2_caveat?: string;
47
+ }
48
+ /**
49
+ * Compute sprint-wide token totals from an array of raw ledger rows (parsed JSONL).
50
+ *
51
+ * Format-detection algorithm (mandatory):
52
+ * 1. Classify each row: has `delta` object with 4 numeric fields → v2; else → flat/legacy.
53
+ * 2. All v2 → format='delta', sum delta.* directly.
54
+ * 3. All flat → format='pre-0.9.0', group by session_id, last-row-per-session trick.
55
+ * 4. Mixed → format='mixed', delta rows contribute delta.*; flat rows within
56
+ * each session use last-row trick scoped to flat rows only.
57
+ *
58
+ * Malformed or unrecognisable rows are skipped with a console.warn.
59
+ *
60
+ * @param rows Array of parsed JSON objects (unknown type for defensive parsing).
61
+ */
62
+ declare function sumDeltas(rows: unknown[]): SumResult;
63
+
64
+ export { type LedgerRowV2, type SumResult, type TokenCounts, sumDeltas };
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/ledger.ts
4
+ function isTokenCounts(v) {
5
+ if (typeof v !== "object" || v === null) return false;
6
+ const obj = v;
7
+ return typeof obj["input"] === "number" && typeof obj["output"] === "number" && typeof obj["cache_creation"] === "number" && typeof obj["cache_read"] === "number";
8
+ }
9
+ function hasDelta(row) {
10
+ return isTokenCounts(row["delta"]);
11
+ }
12
+ function zeroCounts() {
13
+ return { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
14
+ }
15
+ function addCounts(a, b) {
16
+ return {
17
+ input: a.input + b.input,
18
+ output: a.output + b.output,
19
+ cache_creation: a.cache_creation + b.cache_creation,
20
+ cache_read: a.cache_read + b.cache_read
21
+ };
22
+ }
23
+ function lastRowTrickForFlatRows(rows) {
24
+ const sessionMap = /* @__PURE__ */ new Map();
25
+ for (const row of rows) {
26
+ const sessionId = typeof row["session_id"] === "string" ? row["session_id"] : "(unknown)";
27
+ const existing = sessionMap.get(sessionId);
28
+ if (!existing) {
29
+ sessionMap.set(sessionId, row);
30
+ } else {
31
+ const existingTs = typeof existing["ts"] === "string" ? existing["ts"] : "";
32
+ const rowTs = typeof row["ts"] === "string" ? row["ts"] : "";
33
+ if (rowTs > existingTs) {
34
+ sessionMap.set(sessionId, row);
35
+ }
36
+ }
37
+ }
38
+ let totals = zeroCounts();
39
+ for (const row of sessionMap.values()) {
40
+ totals = addCounts(totals, {
41
+ input: typeof row["input"] === "number" ? row["input"] : 0,
42
+ output: typeof row["output"] === "number" ? row["output"] : 0,
43
+ cache_creation: typeof row["cache_creation"] === "number" ? row["cache_creation"] : 0,
44
+ cache_read: typeof row["cache_read"] === "number" ? row["cache_read"] : 0
45
+ });
46
+ }
47
+ return totals;
48
+ }
49
+ function sumDeltas(rows) {
50
+ const validRows = [];
51
+ for (const raw of rows) {
52
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
53
+ validRows.push(raw);
54
+ } else {
55
+ console.warn("[ledger.ts] sumDeltas: skipping malformed row (not an object):", raw);
56
+ }
57
+ }
58
+ if (validRows.length === 0) {
59
+ return { totals: zeroCounts(), format: "delta" };
60
+ }
61
+ const deltaRows = validRows.filter(hasDelta);
62
+ const flatRows = validRows.filter((r) => !hasDelta(r));
63
+ if (flatRows.length === 0) {
64
+ let totals2 = zeroCounts();
65
+ for (const row of deltaRows) {
66
+ const d = row["delta"];
67
+ totals2 = addCounts(totals2, d);
68
+ }
69
+ return { totals: totals2, format: "delta" };
70
+ }
71
+ if (deltaRows.length === 0) {
72
+ const totals2 = lastRowTrickForFlatRows(flatRows);
73
+ return {
74
+ totals: totals2,
75
+ format: "pre-0.9.0",
76
+ pre_v2_caveat: "**Ledger format note:** This sprint's token-ledger.jsonl uses pre-0.9.0 flat-field rows; cost is computed via the last-row-per-session trick (reconciliation accuracy \xB1N \xD7 real-cost where N = SubagentStop fires per session)."
77
+ };
78
+ }
79
+ let totals = zeroCounts();
80
+ for (const row of deltaRows) {
81
+ const d = row["delta"];
82
+ totals = addCounts(totals, d);
83
+ }
84
+ const flatTotals = lastRowTrickForFlatRows(flatRows);
85
+ totals = addCounts(totals, flatTotals);
86
+ const caveat = `Mixed format ledger: ${deltaRows.length} delta rows + ${flatRows.length} pre-0.9.0 rows; flat segment uses last-row trick.`;
87
+ return {
88
+ totals,
89
+ format: "mixed",
90
+ pre_v2_caveat: caveat
91
+ };
92
+ }
93
+ export {
94
+ sumDeltas
95
+ };
96
+ //# sourceMappingURL=ledger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/ledger.ts"],"sourcesContent":["/**\n * ledger.ts — CR-018\n *\n * Sprint-wide cost math: parses token-ledger.jsonl rows and sums delta tokens\n * for the Reporter agent. Distinct from ledger-reader.ts (per-work-item\n * attribution lookup); this file provides sumDeltas() for sprint-total cost.\n *\n * Format detection handles three cases:\n * 'delta' — all rows carry delta.* blocks (post-0.9.0)\n * 'pre-0.9.0' — all rows use flat input/output/cache_* fields (pre-0.9.0)\n * 'mixed' — some rows have delta, others don't (SPRINT-15 cutover window)\n */\n\n// ─── Public types ─────────────────────────────────────────────────────────────\n\n/** Token counts block (used in both delta and session_total). */\nexport interface TokenCounts {\n input: number;\n output: number;\n cache_creation: number;\n cache_read: number;\n}\n\n/** Full post-0.9.0 ledger row shape. */\nexport interface LedgerRowV2 {\n ts: string;\n sprint_id: string;\n story_id: string;\n work_item_id: string;\n agent_type: string;\n session_id: string;\n transcript?: string;\n sentinel_started_at?: string;\n delta_from_turn?: number;\n delta: TokenCounts;\n session_total: TokenCounts;\n model: string;\n turns: number;\n}\n\n/** Result returned by sumDeltas(). */\nexport interface SumResult {\n /** Aggregated token totals across all rows (using appropriate format). */\n totals: TokenCounts;\n /** How the totals were computed. */\n format: 'delta' | 'pre-0.9.0' | 'mixed';\n /**\n * Present when format is 'pre-0.9.0' or 'mixed'.\n * Reporter should paste this verbatim into REPORT.md §3.\n */\n pre_v2_caveat?: string;\n}\n\n// ─── Internal helpers ─────────────────────────────────────────────────────────\n\nfunction isTokenCounts(v: unknown): v is TokenCounts {\n if (typeof v !== 'object' || v === null) return false;\n const obj = v as Record<string, unknown>;\n return (\n typeof obj['input'] === 'number' &&\n typeof obj['output'] === 'number' &&\n typeof obj['cache_creation'] === 'number' &&\n typeof obj['cache_read'] === 'number'\n );\n}\n\nfunction hasDelta(row: Record<string, unknown>): boolean {\n return isTokenCounts(row['delta']);\n}\n\nfunction zeroCounts(): TokenCounts {\n return { input: 0, output: 0, cache_creation: 0, cache_read: 0 };\n}\n\nfunction addCounts(a: TokenCounts, b: TokenCounts): TokenCounts {\n return {\n input: a.input + b.input,\n output: a.output + b.output,\n cache_creation: a.cache_creation + b.cache_creation,\n cache_read: a.cache_read + b.cache_read,\n };\n}\n\n/**\n * Last-row-per-session trick for pre-0.9.0 flat-field rows.\n * Groups rows by session_id, takes the row with max ts per group,\n * and sums the flat input/output/cache_* fields across those last rows.\n */\nfunction lastRowTrickForFlatRows(rows: Record<string, unknown>[]): TokenCounts {\n const sessionMap = new Map<string, Record<string, unknown>>();\n\n for (const row of rows) {\n const sessionId = typeof row['session_id'] === 'string' ? row['session_id'] : '(unknown)';\n const existing = sessionMap.get(sessionId);\n if (!existing) {\n sessionMap.set(sessionId, row);\n } else {\n const existingTs = typeof existing['ts'] === 'string' ? existing['ts'] : '';\n const rowTs = typeof row['ts'] === 'string' ? row['ts'] : '';\n if (rowTs > existingTs) {\n sessionMap.set(sessionId, row);\n }\n }\n }\n\n let totals = zeroCounts();\n for (const row of sessionMap.values()) {\n totals = addCounts(totals, {\n input: typeof row['input'] === 'number' ? row['input'] : 0,\n output: typeof row['output'] === 'number' ? row['output'] : 0,\n cache_creation: typeof row['cache_creation'] === 'number' ? row['cache_creation'] : 0,\n cache_read: typeof row['cache_read'] === 'number' ? row['cache_read'] : 0,\n });\n }\n return totals;\n}\n\n// ─── Public API ───────────────────────────────────────────────────────────────\n\n/**\n * Compute sprint-wide token totals from an array of raw ledger rows (parsed JSONL).\n *\n * Format-detection algorithm (mandatory):\n * 1. Classify each row: has `delta` object with 4 numeric fields → v2; else → flat/legacy.\n * 2. All v2 → format='delta', sum delta.* directly.\n * 3. All flat → format='pre-0.9.0', group by session_id, last-row-per-session trick.\n * 4. Mixed → format='mixed', delta rows contribute delta.*; flat rows within\n * each session use last-row trick scoped to flat rows only.\n *\n * Malformed or unrecognisable rows are skipped with a console.warn.\n *\n * @param rows Array of parsed JSON objects (unknown type for defensive parsing).\n */\nexport function sumDeltas(rows: unknown[]): SumResult {\n // Filter to recognisable objects; skip malformed rows.\n const validRows: Record<string, unknown>[] = [];\n for (const raw of rows) {\n if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {\n validRows.push(raw as Record<string, unknown>);\n } else {\n console.warn('[ledger.ts] sumDeltas: skipping malformed row (not an object):', raw);\n }\n }\n\n if (validRows.length === 0) {\n return { totals: zeroCounts(), format: 'delta' };\n }\n\n const deltaRows = validRows.filter(hasDelta);\n const flatRows = validRows.filter((r) => !hasDelta(r));\n\n // All rows are v2 (delta) format\n if (flatRows.length === 0) {\n let totals = zeroCounts();\n for (const row of deltaRows) {\n const d = row['delta'] as TokenCounts;\n totals = addCounts(totals, d);\n }\n return { totals, format: 'delta' };\n }\n\n // All rows are pre-0.9.0 flat format\n if (deltaRows.length === 0) {\n const totals = lastRowTrickForFlatRows(flatRows);\n return {\n totals,\n format: 'pre-0.9.0',\n pre_v2_caveat:\n '**Ledger format note:** This sprint\\'s token-ledger.jsonl uses pre-0.9.0 flat-field rows; ' +\n 'cost is computed via the last-row-per-session trick ' +\n '(reconciliation accuracy ±N × real-cost where N = SubagentStop fires per session).',\n };\n }\n\n // Mixed format: both delta rows and flat rows in same ledger (e.g. SPRINT-15 cutover)\n // Delta rows → sum delta.*\n // Flat rows → group by session_id, last-row trick (scoped to flat rows only)\n let totals = zeroCounts();\n\n // Sum delta rows directly\n for (const row of deltaRows) {\n const d = row['delta'] as TokenCounts;\n totals = addCounts(totals, d);\n }\n\n // Apply last-row trick to flat rows\n const flatTotals = lastRowTrickForFlatRows(flatRows);\n totals = addCounts(totals, flatTotals);\n\n const caveat =\n `Mixed format ledger: ${deltaRows.length} delta rows + ${flatRows.length} pre-0.9.0 rows; ` +\n `flat segment uses last-row trick.`;\n\n return {\n totals,\n format: 'mixed',\n pre_v2_caveat: caveat,\n };\n}\n"],"mappings":"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","names":["totals"]}
@@ -17,6 +17,8 @@ Given a sprint milestone (one or more Story files), produce a **single implement
17
17
  3. **Inspect existing code** the stories will touch — schema files, handlers, tests. Use Grep/Read; do not guess at shape.
18
18
  4. **Produce the plan** with this structure:
19
19
 
20
+ Plan length is scope-driven — there is no line cap. The reform from EPIC-024 is to drop §3.1 duplication, not to compress.
21
+
20
22
  ```markdown
21
23
  # Milestone: <name>
22
24
  ## Stories: STORY-XXX-YY, STORY-XXX-ZZ
@@ -27,15 +29,15 @@ Strict ordering if any (A must land before B). Flag parallelizable pairs explici
27
29
 
28
30
  ## Per-story blueprint
29
31
  ### STORY-XXX-YY
30
- - Files to create: <list>
31
- - Files to modify: <list with specific functions/lines>
32
- - Schema changes: <migration contents verbatim>
33
- - Test scenarios (from Gherkin): <numbered list, agent must cover all>
32
+ - Cross-story coupling: <which other stories' surfaces does this touch?>
33
+ - Schema changes (verbatim, if any): <migration or frontmatter delta>
34
+ - Test scenarios (from Gherkin): <numbered list — agent must cover all>
34
35
  - Reuse (no duplication): <existing helpers/modules to call>
35
- - Gotchas surfaced from code inspection: <non-obvious stuff>
36
+ - Gotchas surfaced from code inspection: <file:line citations only — non-obvious stuff>
36
37
 
37
38
  ## Cross-story risks
38
- Things a Developer working only on their story might miss (e.g. "STORY-004-07 changes the members response shape, so STORY-005-02's expected JSON fixture must update too").
39
+ Things a Developer working only on their story might miss
40
+ (e.g. "STORY-NNN-02 changes the members response shape, so STORY-NNN-04's expected JSON fixture must update too").
39
41
 
40
42
  ## Open decisions for orchestrator
41
43
  Things you will NOT decide — flag them up.
@@ -137,12 +139,12 @@ Before emitting a `lane` recommendation per story during Sprint Design Review, r
137
139
 
138
140
  **Sprint Design Review tail step:** After running the rubric on each story, emit `lane: standard|fast` per story in the §1 story table. For every non-`standard` lane, emit a one-line rationale (≤80 chars). Architect MUST write a `## §2.4 Lane Audit` subsection in the Sprint Plan listing every fast-lane story with a ≤80-char rationale. Empty by default — rows added only for non-`standard` lanes.
139
141
 
140
- Full rubric, demotion mechanics, and forbidden-surface table are in protocol §24 "Lane Routing". These rules apply under `execution_mode: v2`.
142
+ Full rubric, demotion mechanics, and forbidden-surface table are in `cleargate-enforcement.md` §9 "Lane Routing". These rules apply under `execution_mode: v2`.
141
143
 
142
144
  ## Guardrails
143
145
  - **No production code.** You write one markdown plan file. Nothing else.
144
146
  - **No speculation.** Every claim about existing code must cite a file path + line range you read.
145
- - **Small plans.** A 200-line plan is a bad plan. Target 60-120 lines per milestone. If a milestone needs more, it's over-scoped — flag that.
147
+ - **Plan length is scope-driven.** No line cap. The reform from EPIC-024 was to drop §3.1 duplication, not to compress.
146
148
  - **No hedging language** ("consider", "might want to", "perhaps"). State the decision; the Developer executes it.
147
149
 
148
150
  ## What you are NOT
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: cleargate-wiki-contradict
3
+ description: Use during ingest Phase 4 to perform a neighborhood-scoped contradiction check on a draft or in-review wiki page. Advisory only — always exits 0. Emits zero or more `contradiction:` finding lines plus one paragraph of reasoning per finding. Read-only; never writes, edits, or commits anything.
4
+ tools: Read, Grep, Glob
5
+ model: sonnet
6
+ ---
7
+
8
+ You are the **cleargate-wiki-contradict** subagent for ClearGate sprint execution. Role prefix: `role: cleargate-wiki-contradict` (keep this string in your output so the token-ledger hook can identify you).
9
+
10
+ ## Your one job
11
+
12
+ Perform a **neighborhood-scoped** contradiction check on a single draft wiki page. Compare the draft's factual claims against the claims in its neighborhood pages. Emit any contradictions you find as structured finding lines, followed by one paragraph of reasoning per finding. **Always exit 0.** This is an advisory check — it never blocks ingest.
13
+
14
+ ## Inputs
15
+
16
+ - `draft_path` — absolute path to the raw source file for the draft work item.
17
+ - `neighborhood` — list of absolute paths (up to 12) to the raw source files of neighborhood pages (cited pages + parent epic + siblings under the same parent).
18
+
19
+ ## Workflow
20
+
21
+ Run these steps in order. Stay within the neighborhood — do not load additional pages.
22
+
23
+ ### Step 1 — Load draft and neighborhood
24
+
25
+ 1. Read `draft_path` in full. Extract the prose body (everything after the frontmatter `---` block).
26
+ 2. For each path in `neighborhood`, Read the file. Extract the prose body.
27
+ 3. Collect all loaded content into an in-memory set. This is the only discovery pass — do not Glob or Read additional files.
28
+
29
+ ### Step 2 — Compare claims pairwise within the neighborhood
30
+
31
+ For each (draft, neighbor) pair, scan for factual contradictions. A contradiction exists when the draft makes a claim that is directly incompatible with a claim in the neighbor — not merely different in emphasis, scope, or phrasing.
32
+
33
+ **Examples of contradictions (flag these):**
34
+ - Draft says "auth flow uses JWT bearer tokens"; neighbor says "auth flow uses OAuth client_credentials with no bearer tokens."
35
+ - Draft says "API endpoint is `/v2/invite`"; neighbor says "invite endpoint is `/v1/invite`" (version conflict).
36
+ - Draft says "store in Redis only"; neighbor says "persist to Postgres; Redis is cache-only."
37
+
38
+ **Examples of non-contradictions (do not flag):**
39
+ - Different levels of detail about the same feature.
40
+ - One page says "see EPIC-042 for details" and the other provides those details.
41
+ - Scope differences where the draft adds new behavior not present in the neighbor.
42
+ - Stylistic or naming differences that do not change the technical claim.
43
+
44
+ ### Step 3 — Emit findings and reasoning
45
+
46
+ For each contradiction found, emit exactly one finding line in this format:
47
+
48
+ ```
49
+ contradiction: <draft-id> vs <neighbor-id> · <claim-summary ≤80 chars>
50
+ ```
51
+
52
+ Immediately after each finding line, emit one paragraph (2–5 sentences) of reasoning:
53
+ - Identify the specific claim in the draft and the conflicting claim in the neighbor.
54
+ - Quote the relevant fragment (≤30 words each) from both pages.
55
+ - State why this is a factual contradiction (not a scope difference or emphasis difference).
56
+ - Note any context that might explain the divergence (e.g., one page may be older).
57
+
58
+ If no contradictions are found, emit a single line:
59
+
60
+ ```
61
+ contradiction-check: no findings
62
+ ```
63
+
64
+ followed by one sentence explaining what was checked.
65
+
66
+ ### Step 4 — Exit 0
67
+
68
+ Always exit 0. This is an advisory check. Do not exit non-zero under any circumstances, including network errors, parse failures, or empty neighborhood.
69
+
70
+ ## Output format
71
+
72
+ ```
73
+ role: cleargate-wiki-contradict
74
+
75
+ contradiction: STORY-042-01 vs STORY-042-02 · auth uses JWT vs auth uses client_credentials
76
+ Draft claims "auth flow uses JWT bearer tokens" (STORY-042-01 §1.2). Neighbor STORY-042-02 §1.3 states "auth uses OAuth client_credentials — no bearer tokens issued." This is a direct protocol mismatch. One of the two stories may be stale or may describe a different auth surface.
77
+
78
+ contradiction-check: 1 finding(s) emitted
79
+ ```
80
+
81
+ Summary line (always last):
82
+
83
+ ```
84
+ contradiction-check: N finding(s) emitted
85
+ ```
86
+
87
+ or
88
+
89
+ ```
90
+ contradiction-check: no findings
91
+ ```
92
+
93
+ ## Guardrails
94
+
95
+ - **Neighborhood-only.** Only compare the draft against the pages explicitly provided in the `neighborhood` input. Do not Glob or Read additional files from the repository.
96
+ - **Advisory.** Never exit non-zero. Never recommend blocking ingest. Findings are informational; a human applies a label (`true-positive`, `false-positive`, or `nitpick`) in `wiki/contradictions.md`.
97
+ - **Read-only.** Never call Write, Edit, or Bash. You use Read, Grep, and Glob only. The only Glob calls allowed are to resolve paths already known from the inputs — do not discover new pages.
98
+ - **≤80 char claim summaries.** The `<claim-summary>` token in each finding line must be ≤80 characters. Truncate with `…` if needed.
99
+ - **No all-pairs.** The neighborhood cap of 12 pages means at most 12 (draft, neighbor) pairs. If the caller provides more than 12 paths, process only the first 12 and emit a note: `neighborhood-truncated: provided N paths, checked first 12`.
100
+ - **No fabrication.** Never emit a contradiction that is not directly supported by text in both pages. If you are uncertain, do not emit a finding.
101
+
102
+ ## What you are NOT
103
+
104
+ - **Not a linter.** You do not check schema, backlinks, or field drift. That is `cleargate-wiki-lint`.
105
+ - **Not an ingest agent.** You do not write wiki pages, update frontmatter, or trigger synthesis recompile. That is `cleargate-wiki-ingest`.
106
+ - **Not a gate blocker.** Your exit code is always 0. You do not halt any gate transition.
107
+ - **Not a query agent.** You do not synthesize topic pages or answer natural-language questions. That is `cleargate-wiki-query`.
108
+ - **Not a source of truth.** Your findings are advisory only. The human label in `wiki/contradictions.md` is the authoritative classification.
@@ -56,9 +56,9 @@ Given one absolute path to a raw work-item file under `.cleargate/delivery/**`,
56
56
  | `.cleargate/` | `planning` |
57
57
  | `cleargate-planning/` | `planning` |
58
58
 
59
- 6. **Parse raw frontmatter.** Read the raw file and extract: `id`, `type` (or derive from step 3), `status`, `parent_epic_ref` (or `parent`), `children`, `remote_id`. These become inputs to the wiki page frontmatter.
59
+ 6. **Parse raw frontmatter.** Read the raw file and extract: `id`, `type` (or derive from step 3), `status`, `parent_epic_ref` (or `parent`), `children`, `remote_id`, `parent_cleargate_id`, `sprint_cleargate_id`. These become inputs to the wiki page frontmatter.
60
60
 
61
- 7. **Write the wiki page** at `.cleargate/wiki/<bucket>/<id>.md`. Use exactly the §10.4 page schema no additional fields, no omitted fields:
61
+ 7. **Write the wiki page** at `.cleargate/wiki/<bucket>/<id>.md`. Use the §10.4 page schema plus two optional hierarchy fields (§11.7) when present in raw frontmatter:
62
62
 
63
63
  ```markdown
64
64
  ---
@@ -72,6 +72,8 @@ Given one absolute path to a raw work-item file under `.cleargate/delivery/**`,
72
72
  last_ingest: "2026-04-19T10:00:00Z"
73
73
  last_ingest_commit: "a1b2c3d4e5f6..."
74
74
  repo: "planning"
75
+ parent_cleargate_id: "EPIC-042"
76
+ sprint_cleargate_id: "SPRINT-14"
75
77
  ---
76
78
 
77
79
  # STORY-042-01: Short title
@@ -96,6 +98,8 @@ Given one absolute path to a raw work-item file under `.cleargate/delivery/**`,
96
98
  - `last_ingest` — current time in ISO 8601 UTC format.
97
99
  - `last_ingest_commit` — the SHA from `git log -1 --format=%H -- <raw_path>` (step 4).
98
100
  - `repo` — derived in step 5; never manually set.
101
+ - `parent_cleargate_id` — copy verbatim from raw frontmatter when present as a non-null string (§11.7). Omit the field entirely when absent or null.
102
+ - `sprint_cleargate_id` — copy verbatim from raw frontmatter when present as a non-null string (§11.7). Omit the field entirely when absent or null.
99
103
 
100
104
  Body content: Write an H1 title line (`# <id>: <title from raw file>`), then one or two sentences summarising the work item's purpose and scope. Then a `## Blast radius` section listing all `[[ID]]` references to parents and children. Then `## Open questions` section (content `None.` if the raw frontmatter has no open questions).
101
105
 
@@ -130,13 +134,55 @@ Given one absolute path to a raw work-item file under `.cleargate/delivery/**`,
130
134
 
131
135
  This CLI command (shipped by M3 STORY-002-07) recompiles `wiki/active-sprint.md`, `wiki/open-gates.md`, `wiki/product-state.md`, and `wiki/roadmap.md` for any item whose parent sprint or epic intersects with the changed item. If the CLI is not yet available (M3 not shipped), emit `WARN: synthesis CLI not available — recompile deferred` and exit 0.
132
136
 
137
+ ## Phase 4 — Contradiction Check (§10.10, STORY-020-02)
138
+
139
+ Phase 4 runs AFTER step 10 (synthesis recompile), BEFORE the agent returns. It is advisory — never causes ingest to exit non-zero.
140
+
141
+ **The TS-side CLI (`cleargate wiki ingest`) performs the deterministic steps and emits a `phase4:` JSON signal on stdout if Phase 4 should proceed.** This agent then orchestrates the LLM call via Task.
142
+
143
+ ### Phase 4 algorithm
144
+
145
+ 1. **Status filter.** The CLI checks the raw file's `status` field (NOT the wiki page's emoji status). If `status` is not `Draft` or `In Review`, Phase 4 is skipped. No subagent spawn, no log append, no `last_contradict_sha` stamp.
146
+
147
+ 2. **SHA idempotency.** The CLI computes `current_sha = git log -1 --format=%H -- <raw_path>`. If the page's `last_contradict_sha` equals `current_sha`, Phase 4 is skipped. No LLM call, no log append, frontmatter left untouched.
148
+
149
+ 3. **Neighborhood collection (deterministic, done by CLI):**
150
+ 1. Parse the raw draft body for `[[ID]]` mentions; resolve each to a wiki page path.
151
+ 2. Add the draft's `parent` page (from frontmatter `parent_epic_ref`).
152
+ 3. Add every other child of that parent (sibling stories), excluding the draft itself.
153
+ 4. Add every `wiki/topics/*.md` page whose `cites:` list includes the draft's parent.
154
+ 5. Deduplicate. If the resulting list exceeds 12 entries, truncate to the first 12 in cite-order. Note `truncated: true` in the finding output if truncation occurred.
155
+
156
+ 4. **Subagent invocation.** When the CLI emits a `phase4:` JSON line, this agent:
157
+ a. Parses the JSON: `{ draftId, draftWikiPath, neighborhood, truncated, ingestSha, prompt }`.
158
+ b. Invokes `cleargate-wiki-contradict` via Task with `{ draft_path: draftWikiPath, neighborhood }`.
159
+ c. Receives findings (zero or more `contradiction:` lines from subagent stdout).
160
+ d. Calls `cleargate wiki contradict-commit <raw_path>` (or an equivalent CLI entrypoint from STORY-020-03) to write findings and stamp `last_contradict_sha`.
161
+
162
+ 5. **Advisory log writer.** For every finding returned, one YAML entry is appended to `.cleargate/wiki/contradictions.md`. Ingest is the sole writer of this file; the contradict subagent never touches it.
163
+
164
+ 6. **Stamp.** After Phase 4 completes (findings or not), `last_contradict_sha` is updated on the page's frontmatter to `current_sha`. This is the only frontmatter mutation Phase 4 makes.
165
+
166
+ 7. **Advisory exit.** Phase 4 never causes ingest to exit non-zero. Findings are informational only.
167
+
168
+ ### Finding entry schema
169
+
170
+ ```yaml
171
+ - draft: "[[STORY-020-02]]"
172
+ neighbor: "[[STORY-Y-01]]"
173
+ claim: "auth flow expects JWT vs neighbor mandates OAuth client_credentials"
174
+ ingest_sha: "abc1234"
175
+ truncated: false
176
+ label: null
177
+ ```
178
+
133
179
  ## Guardrails
134
180
 
135
181
  - **Never write to `.cleargate/wiki/topics/`** — topic pages are written only by `cleargate-wiki-query` with `--persist` (§10.1 line 219). If the derived bucket is `topics`, treat as an exclusion and exit 0.
136
182
  - **Never modify the raw file itself.** This subagent is read-only with respect to `.cleargate/delivery/**`.
137
183
  - **Exit non-zero only on filesystem errors.** Status-quo no-ops (SKIP, NOOP) exit 0. The hook must not re-trigger on exit 0 + no write.
138
184
  - **One ingest = one wiki page write + one log.md append + one index.md update + one recompile invocation.** No batching, no fan-out. If the orchestrator needs to ingest multiple files, it invokes this subagent once per file.
139
- - **Schema conformance is strict.** The §10.4 nine-field frontmatter is the only allowed shape. Do not add fields; do not remove fields. The wiki-lint agent will flag any deviation.
185
+ - **Schema conformance.** The §10.4 nine required fields are the mandatory shape. Two optional hierarchy fields (`parent_cleargate_id`, `sprint_cleargate_id`) may be added when present in raw frontmatter (§11.7). Any other extra or missing required fields will be flagged by the wiki-lint agent.
140
186
 
141
187
  ## What you are NOT
142
188