@piecemaker-legal/piecemaker 2.0.11 → 2.0.12

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 (120) hide show
  1. package/dist/assets/{FindReplaceDialog-KU5TEIBJ-BucOHLdm.js → FindReplaceDialog-KU5TEIBJ-BBxf1he7.js} +1 -1
  2. package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js → FootnotePropertiesDialog-W3YHM4BV-BYqp6U4x.js} +1 -1
  3. package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js → HyperlinkDialog-ZYH7ZHRF-BVC9zOMs.js} +4 -4
  4. package/dist/assets/{ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js → ImagePositionDialog-HEWYR6Q3-Xn2mC64y.js} +1 -1
  5. package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D86PckZ2.js → ImagePropertiesDialog-RAB47C6M-CFcdqEif.js} +1 -1
  6. package/dist/assets/{PageSetupDialog-AIGIXO3E-DLETP-7v.js → PageSetupDialog-AIGIXO3E-DECaeRS9.js} +1 -1
  7. package/dist/assets/{SplitCellDialog-IFBKYCD6-CwdQBXeb.js → SplitCellDialog-IFBKYCD6-ByYa04H_.js} +1 -1
  8. package/dist/assets/{TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js → TablePropertiesDialog-TMLKGOOW-BuyqZB5A.js} +1 -1
  9. package/dist/assets/{WatermarkDialog-KRL7WMIS-XFJ9gTkC.js → WatermarkDialog-KRL7WMIS-ByZ1Acr2.js} +1 -1
  10. package/dist/assets/{abnfDiagram-VCTEODGH-DiCrND_Y.js → abnfDiagram-VCTEODGH-BtttzdZA.js} +1 -1
  11. package/dist/assets/{arc-DOjgbMYz.js → arc-DN4m1tLs.js} +1 -1
  12. package/dist/assets/{architectureDiagram-5GKGNRK7-brzIFVhl.js → architectureDiagram-5GKGNRK7-Bt-zFumG.js} +1 -1
  13. package/dist/assets/{blockDiagram-I7D4REHJ-C8JhTjQQ.js → blockDiagram-I7D4REHJ-D14V8v4j.js} +1 -1
  14. package/dist/assets/{c4Diagram-7LVT6UL2-jfYxccyk.js → c4Diagram-7LVT6UL2-BynDo1-W.js} +1 -1
  15. package/dist/assets/channel-C4gJXzYa.js +1 -0
  16. package/dist/assets/{chunk-2Q5K7J3B-CrlEShp9.js → chunk-2Q5K7J3B-B8gKhSj9.js} +1 -1
  17. package/dist/assets/{chunk-5VM5RSS4-DzPlUHGq.js → chunk-5VM5RSS4-GvOpglRR.js} +1 -1
  18. package/dist/assets/{chunk-F27PBJKO-Cp9cpeWU.js → chunk-F27PBJKO-CAgZy0qc.js} +1 -1
  19. package/dist/assets/{chunk-IMKFNOWR-BifUldEN.js → chunk-IMKFNOWR-gf8Ux8Zd.js} +1 -1
  20. package/dist/assets/{chunk-JWPE2WC7-ggPEIkWu.js → chunk-JWPE2WC7-D4zFFRMp.js} +1 -1
  21. package/dist/assets/{chunk-POPQ4Y6H-3h_nayRX.js → chunk-POPQ4Y6H-Ct8X_LaT.js} +1 -1
  22. package/dist/assets/{chunk-SVP7TREG-CH29aG6J.js → chunk-SVP7TREG-g5iH1bs5.js} +1 -1
  23. package/dist/assets/{chunk-TICWLB2K-PluJUM5k.js → chunk-TICWLB2K-89-VFKTn.js} +1 -1
  24. package/dist/assets/{chunk-XXDRQBXY-DXU39Vwg.js → chunk-XXDRQBXY-CaQbdObb.js} +1 -1
  25. package/dist/assets/classDiagram-ZZMXUADV-Cm08FeYk.js +1 -0
  26. package/dist/assets/classDiagram-v2-VYDZK3BY-Cm08FeYk.js +1 -0
  27. package/dist/assets/{cose-bilkent-JH36ORCC-Dz6roYrN.js → cose-bilkent-JH36ORCC-BiYOGJAU.js} +1 -1
  28. package/dist/assets/{cynefin-OW5HDTMX-BLzrpSit.js → cynefin-OW5HDTMX-F6mJ-4tj.js} +1 -1
  29. package/dist/assets/{cynefinDiagram-5FMLGOSQ-kfxnERDZ.js → cynefinDiagram-5FMLGOSQ-q-Rggn6r.js} +1 -1
  30. package/dist/assets/{dagre-GXQ25YYZ-DP57bp-N.js → dagre-GXQ25YYZ-COqImSUH.js} +1 -1
  31. package/dist/assets/{diagram-S7CK7UJ4-DAlsrQVW.js → diagram-S7CK7UJ4-BAg1dyEc.js} +1 -1
  32. package/dist/assets/{diagram-UQ7AKVKN-By5yrMer.js → diagram-UQ7AKVKN-DFJS8qCV.js} +1 -1
  33. package/dist/assets/{diagram-VSXAHHWV-Dx89kryN.js → diagram-VSXAHHWV-BfldL35r.js} +1 -1
  34. package/dist/assets/{diagram-VX7I27RA-B9fARitZ.js → diagram-VX7I27RA-kLjovpYe.js} +1 -1
  35. package/dist/assets/{diagram-Z3DM3KII-DC15CEH1.js → diagram-Z3DM3KII-Dhwvi3Iu.js} +1 -1
  36. package/dist/assets/{ebnfDiagram-PWID7BFC-BKQzghGF.js → ebnfDiagram-PWID7BFC-Bp4YE1i7.js} +1 -1
  37. package/dist/assets/{erDiagram-RLTQ6QDP-CPY6RM7G.js → erDiagram-RLTQ6QDP-BKDdaSWz.js} +1 -1
  38. package/dist/assets/{flowDiagram-HODETNUW-4dL4GJcf.js → flowDiagram-HODETNUW-zHProVxL.js} +1 -1
  39. package/dist/assets/{ganttDiagram-EL5Y4UJY-D4-cVcfb.js → ganttDiagram-EL5Y4UJY-B-B-BI6j.js} +1 -1
  40. package/dist/assets/{gitGraphDiagram-WWUBYQGX-BMeVIEiX.js → gitGraphDiagram-WWUBYQGX-BTHMg5CH.js} +1 -1
  41. package/dist/assets/index-C5slQNvE.css +1 -0
  42. package/dist/assets/{index-DNtFDOni.js → index-CJP4dZ-c.js} +146 -146
  43. package/dist/assets/{infoDiagram-27XIBGKW-D5qMSUEU.js → infoDiagram-27XIBGKW-mabp4H9I.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-5VMMS53U-BDnyKRHm.js → ishikawaDiagram-5VMMS53U-CLNhcCrM.js} +1 -1
  45. package/dist/assets/{journeyDiagram-3NMN7TZE-dGz5ZGzq.js → journeyDiagram-3NMN7TZE-CGEi2RB6.js} +1 -1
  46. package/dist/assets/{kanban-definition-UXKFOSKX-D7UAz7o_.js → kanban-definition-UXKFOSKX-B2fLX1fK.js} +1 -1
  47. package/dist/assets/{layout-DKrmEGrz.js → layout-CloKWsFq.js} +1 -1
  48. package/dist/assets/{linear-CrTdMY-p.js → linear-BFyydI0c.js} +1 -1
  49. package/dist/assets/{mermaid.core-CXQOT5G6.js → mermaid.core-Dxl3LVAh.js} +6 -6
  50. package/dist/assets/{mindmap-definition-YA3MSWOX-VgEiYfay.js → mindmap-definition-YA3MSWOX-FT4pm55s.js} +1 -1
  51. package/dist/assets/{pegDiagram-XKGWAZYB-9IqT8gGx.js → pegDiagram-XKGWAZYB-okyIfJWM.js} +1 -1
  52. package/dist/assets/{pieDiagram-E7YTZNPT-D98pRE-t.js → pieDiagram-E7YTZNPT-NF61CQz8.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-AXDQQJYC-b9PwkYzL.js → quadrantDiagram-AXDQQJYC-uVfvZzzt.js} +1 -1
  54. package/dist/assets/{railroadDiagram-O6MQD6OU-Dz87hsPE.js → railroadDiagram-O6MQD6OU-Dp8IpeoH.js} +1 -1
  55. package/dist/assets/{requirementDiagram-BXWQKSXE-CkcV73dK.js → requirementDiagram-BXWQKSXE-D8VV0oQe.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-P5KCCOFB-UBN31Yrs.js → sankeyDiagram-P5KCCOFB-D-xhxjob.js} +1 -1
  57. package/dist/assets/{sequenceDiagram-WJ2MYXX4-CpQxd5Za.js → sequenceDiagram-WJ2MYXX4-BAQBK95h.js} +1 -1
  58. package/dist/assets/{sizeCapture-INFHLROL-mCsFmyzw.js → sizeCapture-INFHLROL-C6rk9SVx.js} +1 -1
  59. package/dist/assets/{stateDiagram-D77RDMKH-B0MTBN-3.js → stateDiagram-D77RDMKH-DshqaEOj.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-MP3YSRHH-DzBPN39W.js → stateDiagram-v2-MP3YSRHH-BohZIrk5.js} +1 -1
  61. package/dist/assets/{swimlanes-42K2YHIH-Dzydjm0e.js → swimlanes-42K2YHIH-DgcPDy4l.js} +1 -1
  62. package/dist/assets/swimlanesDiagram-VR7AAH4N-CuM3Su0K.js +8 -0
  63. package/dist/assets/{timeline-definition-24CTP7MA-Dz0toOLu.js → timeline-definition-24CTP7MA-OgpIe-gQ.js} +1 -1
  64. package/dist/assets/{vennDiagram-4TSXK5OY-DOWp7dkf.js → vennDiagram-4TSXK5OY-BZzGd6jt.js} +1 -1
  65. package/dist/assets/{wardleyDiagram-VM6X3IG4-CUxsztUB.js → wardleyDiagram-VM6X3IG4-1tg-H7l_.js} +1 -1
  66. package/dist/assets/{xychartDiagram-S5SC5T6Z-DS96vNED.js → xychartDiagram-S5SC5T6Z-ByTsX8g0.js} +1 -1
  67. package/dist/index.html +2 -2
  68. package/dist/sw.js +1 -1
  69. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
  70. package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
  71. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
  72. package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
  73. package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
  74. package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
  75. package/dist-server/server/piecemaker/knowledge/service.js +1 -16
  76. package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
  77. package/dist-server/server/piecemaker/project-registry.js +5 -5
  78. package/dist-server/server/piecemaker/project-registry.js.map +1 -1
  79. package/package.json +1 -1
  80. package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
  81. package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
  82. package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
  83. package/server/piecemaker/anonymizer/service.cjs +3 -2
  84. package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +3 -31
  85. package/server/piecemaker/index.ts +1 -1
  86. package/server/piecemaker/knowledge/pipeline.ts +24 -31
  87. package/server/piecemaker/knowledge/service.ts +1 -14
  88. package/server/piecemaker/project-registry.ts +6 -6
  89. package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
  90. package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
  91. package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
  92. package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
  93. package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
  94. package/server/piecemaker/vendor/mcp/README.md +1 -1
  95. package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
  96. package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
  97. package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
  98. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
  99. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
  100. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
  101. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
  102. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
  103. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
  104. package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
  105. package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
  106. package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
  107. package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -150
  108. package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
  109. package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
  110. package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
  111. package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
  112. package/dist/assets/channel-Dajzu2W1.js +0 -1
  113. package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +0 -1
  114. package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +0 -1
  115. package/dist/assets/index-Dc9wNuS4.css +0 -1
  116. package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +0 -8
  117. package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
  118. package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
  119. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
  120. package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit.mjs +0 -83
@@ -1,194 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Billing / task-tracking hook, wired to both Stop and TaskCompleted in
4
- * hooks/hooks.json (a single script, dispatching on payload.hook_event_name).
5
- *
6
- * Confirmed payload shapes (https://code.claude.com/docs/en/hooks):
7
- * Stop -> { last_assistant_message, stop_reason, ...common }
8
- * TaskCompleted -> { task_id, task_name, task_status, ...common }
9
- * common fields -> session_id, transcript_path, cwd, hook_event_name, ...
10
- *
11
- * Stop already carries the final assistant text directly as
12
- * `last_assistant_message` — no transcript parsing is required to get it.
13
- * We still read transcript_path as a fallback (in case that field is ever
14
- * empty) and to derive tool-call counts and a rough session duration, since
15
- * neither event's payload provides those.
16
- *
17
- * - Appends one JSONL line per event to ~/.piecemaker/billing/<YYYY-MM>.jsonl
18
- * - On Stop, saves a copy of the final synthesis to
19
- * ~/.piecemaker/billing/synthese/<session_id>-<timestamp>.md
20
- *
21
- * This hook never returns JSON output — it only records; it never affects
22
- * the session. Every failure path is swallowed and ends in exit 0.
23
- */
24
-
25
- import fs from 'node:fs';
26
- import path from 'node:path';
27
- import { createRequire } from 'node:module';
28
- import {
29
- readHookPayload,
30
- loadPieceMakerConfig,
31
- runHook,
32
- noop,
33
- ensureDirSafe,
34
- appendJsonl,
35
- BILLING_DIR,
36
- SYNTHESIS_DIR,
37
- } from './lib/hook-io.mjs';
38
-
39
- const require = createRequire(import.meta.url);
40
- const { locateProjectCase } = require('./lib/case-folders.cjs');
41
-
42
- const DEFAULT_TIMEOUT_MS = 8000;
43
- const MAX_TRANSCRIPT_BYTES = 10 * 1024 * 1024; // skip duration/tool-count derivation beyond this
44
-
45
- /** Best-effort transcript scan: session duration bounds, tool-call counts, last assistant text. */
46
- async function analyzeTranscript(transcriptPath) {
47
- const empty = { firstTimestamp: null, lastTimestamp: null, toolCounts: {}, lastAssistantText: null };
48
- if (!transcriptPath) return empty;
49
-
50
- let stat;
51
- try {
52
- stat = await fs.promises.stat(transcriptPath);
53
- } catch {
54
- return empty;
55
- }
56
- if (stat.size > MAX_TRANSCRIPT_BYTES) return empty;
57
-
58
- let raw;
59
- try {
60
- raw = await fs.promises.readFile(transcriptPath, 'utf8');
61
- } catch {
62
- return empty;
63
- }
64
-
65
- let firstTimestamp = null;
66
- let lastTimestamp = null;
67
- const toolCounts = {};
68
- let lastAssistantText = null;
69
-
70
- for (const line of raw.split('\n')) {
71
- const trimmed = line.trim();
72
- if (!trimmed) continue;
73
- let entry;
74
- try {
75
- entry = JSON.parse(trimmed);
76
- } catch {
77
- continue;
78
- }
79
-
80
- const ts = entry.timestamp || entry.message?.timestamp;
81
- if (ts) {
82
- if (!firstTimestamp) firstTimestamp = ts;
83
- lastTimestamp = ts;
84
- }
85
-
86
- const role = entry.type || entry.message?.role;
87
- const content = entry.message?.content;
88
- if (Array.isArray(content)) {
89
- for (const block of content) {
90
- if (block?.type === 'tool_use' && block.name) {
91
- toolCounts[block.name] = (toolCounts[block.name] || 0) + 1;
92
- }
93
- if (role === 'assistant' && block?.type === 'text' && typeof block.text === 'string') {
94
- lastAssistantText = block.text;
95
- }
96
- }
97
- }
98
- }
99
-
100
- return { firstTimestamp, lastTimestamp, toolCounts, lastAssistantText };
101
- }
102
-
103
- function derivedDurationMs(analysis) {
104
- if (!analysis.firstTimestamp || !analysis.lastTimestamp) return null;
105
- const ms = Date.parse(analysis.lastTimestamp) - Date.parse(analysis.firstTimestamp);
106
- return Number.isFinite(ms) && ms >= 0 ? ms : null;
107
- }
108
-
109
- /** Name of the project folder containing cwd. */
110
- function detectDossier(cwd) {
111
- if (!cwd) return null;
112
- return locateProjectCase(cwd)?.caseName || null;
113
- }
114
-
115
- function synthesisFilePath(sessionId, isoTimestamp) {
116
- const safeSession = String(sessionId || 'unknown-session').replace(/[^a-zA-Z0-9_-]/g, '_');
117
- const safeTs = isoTimestamp.replace(/[:.]/g, '-');
118
- return path.join(SYNTHESIS_DIR, `${safeSession}-${safeTs}.md`);
119
- }
120
-
121
- async function main() {
122
- const payload = await readHookPayload(2000);
123
- if (!payload) return null;
124
-
125
- const eventName = payload.hook_event_name;
126
- if (eventName !== 'Stop' && eventName !== 'TaskCompleted') return null;
127
-
128
- const config = loadPieceMakerConfig();
129
- if (config.billing?.enabled === false) return null;
130
-
131
- ensureDirSafe(BILLING_DIR);
132
- ensureDirSafe(SYNTHESIS_DIR);
133
-
134
- const nowIso = new Date().toISOString();
135
- const monthFile = path.join(BILLING_DIR, `${nowIso.slice(0, 7)}.jsonl`); // YYYY-MM.jsonl
136
- const dossier = detectDossier(payload.cwd);
137
- const analysis = await analyzeTranscript(payload.transcript_path);
138
-
139
- if (eventName === 'TaskCompleted') {
140
- appendJsonl(monthFile, {
141
- timestamp: nowIso,
142
- session_id: payload.session_id || null,
143
- cwd: payload.cwd || null,
144
- dossier,
145
- task_label: payload.task_name || null,
146
- event: 'TaskCompleted',
147
- task_id: payload.task_id || null,
148
- task_status: payload.task_status || null,
149
- duration_ms: derivedDurationMs(analysis),
150
- tool_counts: analysis.toolCounts,
151
- synthesis_path: null,
152
- });
153
- return null;
154
- }
155
-
156
- // Stop: prefer the payload's own last_assistant_message; fall back to the
157
- // transcript only if that field is missing or empty.
158
- const synthesisText = (payload.last_assistant_message && payload.last_assistant_message.trim())
159
- || analysis.lastAssistantText
160
- || '';
161
-
162
- let synthesisPath = null;
163
- if (synthesisText) {
164
- const target = synthesisFilePath(payload.session_id, nowIso);
165
- try {
166
- fs.writeFileSync(target, synthesisText, 'utf8');
167
- synthesisPath = target;
168
- } catch {
169
- synthesisPath = null;
170
- }
171
- }
172
-
173
- const taskLabel = synthesisText ? synthesisText.split('\n')[0].slice(0, 80) : 'Synthèse de session';
174
-
175
- appendJsonl(monthFile, {
176
- timestamp: nowIso,
177
- session_id: payload.session_id || null,
178
- cwd: payload.cwd || null,
179
- dossier,
180
- task_label: taskLabel,
181
- event: 'Stop',
182
- stop_reason: payload.stop_reason || null,
183
- duration_ms: derivedDurationMs(analysis),
184
- tool_counts: analysis.toolCounts,
185
- synthesis_path: synthesisPath,
186
- });
187
-
188
- return null;
189
- }
190
-
191
- const config = loadPieceMakerConfig();
192
- const timeoutMs = config.billing?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
193
-
194
- runHook(main, { timeoutMs }).catch(() => noop());
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * PostToolUse hook — marque le dossier de cas touché par un Write/Edit.
4
- *
5
- * Ne commite plus rien lui-même (aucun appel git) : il note juste, pour la
6
- * session en cours, quel dossier de cas a bougé pendant CE tour, dans
7
- * `~/.piecemaker/pending/<session_id>.json`. Le hook `Stop`
8
- * (`session-commit.mjs`) déclenche à la fin du tour un worker détaché
9
- * (`session-commit-worker.mjs`) qui consomme ce fichier et fait le commit
10
- * réel — un seul commit par tour et par dossier, au lieu d'un commit par
11
- * Write/Edit.
12
- *
13
- * Chaque dossier de cas explicitement enregistré (et chaque enfant de
14
- * l'espace de travail historique) reste une affaire indépendante. Les pièces
15
- * originales ne sont ni ouvertes ni indexées ici.
16
- */
17
-
18
- import { createRequire } from 'node:module';
19
- import path from 'node:path';
20
- import {
21
- HOME_DIR,
22
- appendJsonl,
23
- loadPieceMakerConfig,
24
- noop,
25
- readHookPayload,
26
- runHook,
27
- } from './lib/hook-io.mjs';
28
-
29
- const require = createRequire(import.meta.url);
30
- const { locateCaseFile } = require('./lib/commits.cjs');
31
- const { locateProjectCase } = require('./lib/case-folders.cjs');
32
-
33
- const PENDING_DIR = path.join(HOME_DIR, 'pending');
34
-
35
- function sanitizeSessionId(sessionId) {
36
- return String(sessionId || 'unknown-session').replace(/[^a-zA-Z0-9_-]/g, '_');
37
- }
38
-
39
- async function main() {
40
- const payload = await readHookPayload(2000);
41
- if (!payload || !['Write', 'Edit'].includes(payload.tool_name)) return null;
42
- if (payload.tool_response?.success === false) return null;
43
-
44
- const config = loadPieceMakerConfig();
45
- if (config.commits?.enabled === false) return null;
46
-
47
- const cwd = payload.cwd || process.cwd();
48
- const filePath = payload.tool_input?.file_path;
49
- if (!filePath) return null;
50
- const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
51
- let located;
52
- try {
53
- const configured = locateProjectCase(absolute);
54
- if (!configured) return null;
55
- located = locateCaseFile(configured.casesRoot, absolute);
56
- } catch {
57
- return null;
58
- }
59
- if (!located?.safe || located.protected) return null;
60
-
61
- const pendingFile = path.join(PENDING_DIR, `${sanitizeSessionId(payload.session_id)}.json`);
62
- appendJsonl(pendingFile, {
63
- casesRoot: located.casesRoot,
64
- caseName: located.name,
65
- ts: new Date().toISOString(),
66
- });
67
- return null;
68
- }
69
-
70
- const timeoutMs = loadPieceMakerConfig().commits?.timeoutMs ?? 8000;
71
- runHook(main, { timeoutMs }).catch(() => noop());
@@ -1,260 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Worker détaché de l'auto-commit par tour.
4
- *
5
- * Lancé par `session-commit.mjs` (hook `Stop`) avec un unique argument : le
6
- * chemin du fichier de spool. Tout le travail Git — potentiellement lent —
7
- * vit ici, hors du tour de conversation :
8
- *
9
- * - Le PÉRIMÈTRE du tour est l'union des dossiers de cas marqués pendant le
10
- * tour (`~/.piecemaker/pending/<session_id>.json`, écrit par
11
- * `commit-track.mjs` à chaque Write/Edit) et du dossier de cas contenant
12
- * `cwd` — ce second terme rattrape ce qui a changé sans passer par
13
- * Write/Edit (script, modification manuelle).
14
- * - Le MESSAGE de chaque commit est la conclusion de l'IA
15
- * (`last_assistant_message`), ré-identifiée via le mapping central puis
16
- * complétée d'un pied déterministe (fichiers modifiés, durée de session,
17
- * horodatage).
18
- * - `createCommit` ne crée jamais de commit vide : aucun pré-test de
19
- * propreté n'est fait ici.
20
- *
21
- * Le worker survit à la session qui l'a lancé : c'est voulu (`detached` +
22
- * `unref()` côté hook). La concurrence entre workers d'un même dossier est
23
- * déjà gérée par `withCaseLock` dans `createCommit`.
24
- *
25
- * Journal : ~/.piecemaker/auto-commit.log — seul témoin en cas d'échec, ce
26
- * process n'a plus personne à qui remonter une erreur.
27
- */
28
-
29
- import fs from 'node:fs';
30
- import path from 'node:path';
31
- import { createRequire } from 'node:module';
32
- import { HOME_DIR, loadPieceMakerConfig } from './lib/hook-io.mjs';
33
-
34
- const require = createRequire(import.meta.url);
35
- const { createCommit, worktreeDetails } = require('./lib/commits.cjs');
36
- const { locateConfiguredCase } = require('./lib/case-folders.cjs');
37
- const { revertMapping } = require('./lib/mapping.cjs');
38
- const { readCentralMapping } = require('./lib/central-mapping.cjs');
39
- const { sessionElapsedMs, formatDurationFr } = require('./lib/session-timing.cjs');
40
-
41
- const LOG_FILE = path.join(HOME_DIR, 'auto-commit.log');
42
- // Une ligne par commit de tour ou presque : 512 Ko couvrent des mois. Au-delà,
43
- // on bascule vers `.1` (une seule génération : le journal est un outil de
44
- // diagnostic, pas une archive) — même politique que session-autocommit.mjs.
45
- const MAX_LOG_BYTES = 512 * 1024;
46
- const PENDING_DIR = path.join(HOME_DIR, 'pending');
47
- const FALLBACK_LABEL = 'Modifications du tour';
48
-
49
- let logContext = '';
50
-
51
- /** Journalise sans jamais échouer : un worker muet ne doit pas mourir de son log. */
52
- function log(level, message, extra) {
53
- try {
54
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
55
- try {
56
- if (fs.statSync(LOG_FILE).size > MAX_LOG_BYTES) fs.renameSync(LOG_FILE, `${LOG_FILE}.1`);
57
- } catch { /* pas de journal existant, ou rotation impossible */ }
58
- const details = extra ? ` ${JSON.stringify(extra)}` : '';
59
- fs.appendFileSync(
60
- LOG_FILE,
61
- `${new Date().toISOString()} [${process.pid}] ${level} ${logContext}${message}${details}\n`,
62
- );
63
- } catch { /* fail-open : un journal indisponible n'empêche pas le commit */ }
64
- }
65
-
66
- /** Détail exploitable d'une erreur (le stderr git est porté sur `.stderr`). */
67
- function describeError(error) {
68
- const stderr = (error?.stderr || '').toString().trim();
69
- const base = error?.message || String(error);
70
- return stderr ? `${base} | stderr: ${stderr}` : base;
71
- }
72
-
73
- function sanitizeSessionId(sessionId) {
74
- return String(sessionId || 'unknown-session').replace(/[^a-zA-Z0-9_-]/g, '_');
75
- }
76
-
77
- function readSpool(spoolFile) {
78
- return JSON.parse(fs.readFileSync(spoolFile, 'utf8'));
79
- }
80
-
81
- /**
82
- * Consomme le fichier pending de la session : les dossiers marqués pendant le
83
- * tour par `commit-track.mjs`. Renommage avant lecture pour ne pas se
84
- * disputer le fichier avec une écriture concurrente improbable ; le fichier
85
- * renommé est supprimé après lecture (best-effort).
86
- */
87
- function drainPendingCases(sessionId) {
88
- const file = path.join(PENDING_DIR, `${sanitizeSessionId(sessionId)}.json`);
89
- const staging = `${file}.${process.pid}.consuming`;
90
- try {
91
- fs.renameSync(file, staging);
92
- } catch (error) {
93
- if (error.code !== 'ENOENT') log('WARN', 'lecture du pending impossible', { file, error: describeError(error) });
94
- return [];
95
- }
96
- let cases = [];
97
- try {
98
- const raw = fs.readFileSync(staging, 'utf8');
99
- cases = raw.split('\n').map((line) => line.trim()).filter(Boolean).map((line) => {
100
- try {
101
- return JSON.parse(line);
102
- } catch {
103
- return null;
104
- }
105
- }).filter((entry) => entry?.casesRoot && entry?.caseName);
106
- } catch (error) {
107
- log('WARN', 'pending illisible', { file: staging, error: describeError(error) });
108
- } finally {
109
- try { fs.unlinkSync(staging); } catch { /* best-effort */ }
110
- }
111
- return cases;
112
- }
113
-
114
- /** Retire les marqueurs Markdown de tête (titres, listes, citations, gras…). */
115
- function cleanMarkdownHeading(line) {
116
- let text = String(line || '');
117
- let previous;
118
- do {
119
- previous = text;
120
- text = text.replace(/^\s*(?:#{1,6}|>{1,3}|[-*+•]{1,3}|`{1,3}|\*{1,3}|_{1,3})\s*/u, '');
121
- } while (text !== previous && text.length);
122
- return text.trim();
123
- }
124
-
125
- /** Première ligne (label) / reste (description) d'une conclusion d'IA. */
126
- function splitConclusion(text) {
127
- if (!text || typeof text !== 'string') return { label: '', description: '' };
128
- const normalized = text.replace(/\r\n/g, '\n').trim();
129
- if (!normalized) return { label: '', description: '' };
130
- const lines = normalized.split('\n');
131
- return { label: lines[0] || '', description: lines.slice(1).join('\n').trim() };
132
- }
133
-
134
- /** Pied déterministe, une ligne : fichiers modifiés, durée de session, horodatage. */
135
- function buildFooter({ fileCount, durationMs, timestamp }) {
136
- const bits = [];
137
- if (Number.isFinite(fileCount)) {
138
- bits.push(`${fileCount} fichier${fileCount > 1 ? 's' : ''} modifié${fileCount > 1 ? 's' : ''}`);
139
- }
140
- if (Number.isFinite(durationMs)) {
141
- const human = formatDurationFr(durationMs);
142
- if (human) bits.push(`durée de session ${human}`);
143
- }
144
- bits.push(timestamp);
145
- return `— ${bits.join(' · ')}`;
146
- }
147
-
148
- async function commitScopeCase({ casesRoot, caseName }, { label, descriptionBody, sessionId, durationMs }) {
149
- // Aperçu du nombre de fichiers modifiés pour le pied du message — avant le
150
- // commit lui-même, qui a besoin du message complet. `createCommit` reste
151
- // l'unique source de vérité sur ce qui est réellement commité : cet aperçu
152
- // ne sert qu'à documenter le message, jamais à décider s'il faut committer.
153
- let fileCount = null;
154
- try {
155
- const preview = await worktreeDetails(casesRoot, HOME_DIR, caseName);
156
- fileCount = preview.filesCount;
157
- } catch (error) {
158
- log('WARN', 'aperçu des modifications indisponible', { caseName, error: describeError(error) });
159
- }
160
-
161
- const footer = buildFooter({ fileCount, durationMs, timestamp: new Date().toISOString() });
162
- const description = descriptionBody ? `${descriptionBody}\n\n${footer}` : footer;
163
-
164
- try {
165
- const result = await createCommit({
166
- casesRoot,
167
- caseName,
168
- homeDir: HOME_DIR,
169
- label,
170
- description,
171
- paths: null, // arbre complet du dossier : tout ce qui a changé pendant le tour
172
- sessionId,
173
- durationMs,
174
- event: 'Stop',
175
- waitForLockMs: 10_000,
176
- });
177
- if (result?.skipped === 'busy') {
178
- log('SKIP', 'verrou occupé, commit de tour abandonné', { caseName });
179
- } else if (result?.created) {
180
- log('OK', 'commit de tour créé', {
181
- caseName,
182
- commit: result.commit ? result.commit.slice(0, 12) : null,
183
- files: result.files?.length ?? fileCount,
184
- });
185
- } else {
186
- log('SKIP', 'rien à committer', { caseName });
187
- }
188
- } catch (error) {
189
- log('ERROR', 'commit de tour échoué', { caseName, error: describeError(error) });
190
- }
191
- }
192
-
193
- async function main() {
194
- const spoolFile = process.argv[2];
195
- if (!spoolFile) {
196
- log('ERROR', 'aucun fichier de spool fourni');
197
- return;
198
- }
199
-
200
- let spoolData;
201
- try {
202
- spoolData = readSpool(spoolFile);
203
- } catch (error) {
204
- log('ERROR', 'lecture du spool échouée', { spoolFile, error: describeError(error) });
205
- return;
206
- } finally {
207
- // Le spool est un instantané transitoire : il a fait son office une fois lu.
208
- try { fs.unlinkSync(spoolFile); } catch { /* best-effort */ }
209
- }
210
-
211
- logContext = spoolData.session_id ? `session=${spoolData.session_id} ` : '';
212
- log('INFO', 'début du commit de tour', { cwd: spoolData.cwd });
213
-
214
- const config = loadPieceMakerConfig();
215
- if (config.commits?.enabled === false) {
216
- log('SKIP', 'commits désactivés par la configuration');
217
- return;
218
- }
219
-
220
- // Périmètre : dossiers marqués pendant le tour ∪ dossier de cas de cwd.
221
- const scope = new Map();
222
- for (const entry of drainPendingCases(spoolData.session_id)) {
223
- scope.set(`${entry.casesRoot}${entry.caseName}`, { casesRoot: entry.casesRoot, caseName: entry.caseName });
224
- }
225
- if (spoolData.cwd) {
226
- try {
227
- const cwdCase = locateConfiguredCase(config, spoolData.cwd);
228
- if (cwdCase) {
229
- scope.set(`${cwdCase.casesRoot}${cwdCase.caseName}`, { casesRoot: cwdCase.casesRoot, caseName: cwdCase.caseName });
230
- }
231
- } catch (error) {
232
- log('WARN', 'résolution du dossier courant impossible', { cwd: spoolData.cwd, error: describeError(error) });
233
- }
234
- }
235
-
236
- if (!scope.size) {
237
- log('SKIP', 'aucun dossier de cas dans le périmètre du tour');
238
- return;
239
- }
240
-
241
- // Message de commit = la conclusion de l'IA, ré-identifiée avec le mapping
242
- // central (le modèle n'a vu que des codes ; le cabinet ne voit que des noms).
243
- const central = readCentralMapping();
244
- const reverseMapping = central.reverse_mapping || {};
245
- const { label: rawLabel, description: rawDescription } = splitConclusion(spoolData.last_assistant_message);
246
- const revertedLabel = revertMapping(cleanMarkdownHeading(rawLabel), reverseMapping).trim();
247
- const label = (revertedLabel || FALLBACK_LABEL).slice(0, 140);
248
- const descriptionBody = revertMapping(rawDescription, reverseMapping).trim();
249
- const durationMs = sessionElapsedMs(spoolData.transcript_path);
250
-
251
- log('INFO', 'périmètre du tour établi', { cases: scope.size, label });
252
-
253
- for (const entry of scope.values()) {
254
- await commitScopeCase(entry, { label, descriptionBody, sessionId: spoolData.session_id, durationMs });
255
- }
256
- }
257
-
258
- main().catch((error) => {
259
- log('ERROR', 'échec inattendu du worker', { error: describeError(error) });
260
- });
@@ -1,83 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Hook Stop — déclenche l'auto-commit par tour, sans rien coûter au tour.
4
- *
5
- * Ce script ne fait que déposer un instantané minimal du tour (session, cwd,
6
- * transcript, conclusion de l'IA) dans le spool
7
- * (`~/.piecemaker/spool/<session_id>-<horodatage>.json`), puis lance le
8
- * worker détaché `session-commit-worker.mjs` qui fait tout le travail Git —
9
- * comparer les arbres, ré-identifier la conclusion, écrire le commit — hors
10
- * du tour (même motif de spawn détaché que `compile-recherche.mjs`).
11
- *
12
- * `Stop` refuse un code de sortie 2 : sur cet événement, un code 2 empêche
13
- * Claude de s'arrêter et boucle la session. Ce hook sort donc TOUJOURS en 0 —
14
- * fail-open intégral, toute exception est avalée (journalisée si possible).
15
- * Objectif de latence : quelques millisecondes, jamais de `git` synchrone ici.
16
- */
17
-
18
- import { spawn } from 'node:child_process';
19
- import { fileURLToPath } from 'node:url';
20
- import fs from 'node:fs';
21
- import path from 'node:path';
22
- import {
23
- HOME_DIR,
24
- noop,
25
- readHookPayload,
26
- runHook,
27
- } from './lib/hook-io.mjs';
28
-
29
- const HERE = path.dirname(fileURLToPath(import.meta.url));
30
- const SPOOL_DIR = path.join(HOME_DIR, 'spool');
31
- const LOG_FILE = path.join(HOME_DIR, 'auto-commit.log');
32
-
33
- function sanitizeSessionId(sessionId) {
34
- return String(sessionId || 'unknown-session').replace(/[^a-zA-Z0-9_-]/g, '_');
35
- }
36
-
37
- /**
38
- * Best-effort, jamais bloquant : une seule ligne append, pas de rotation ici
39
- * (le worker détaché tient le journal complet, y compris sa rotation).
40
- */
41
- function logFailure(message, error) {
42
- try {
43
- fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
44
- const detail = error ? ` ${String(error?.message || error)}` : '';
45
- fs.appendFileSync(
46
- LOG_FILE,
47
- `${new Date().toISOString()} [${process.pid}] ERROR (session-commit) ${message}${detail}\n`,
48
- );
49
- } catch { /* fail-open : un journal indisponible ne doit rien bloquer */ }
50
- }
51
-
52
- async function main() {
53
- const payload = await readHookPayload(2000);
54
- if (!payload || payload.hook_event_name !== 'Stop') return null;
55
-
56
- try {
57
- fs.mkdirSync(SPOOL_DIR, { recursive: true });
58
- const sessionId = sanitizeSessionId(payload.session_id);
59
- const spoolFile = path.join(SPOOL_DIR, `${sessionId}-${Date.now()}.json`);
60
- fs.writeFileSync(spoolFile, JSON.stringify({
61
- session_id: payload.session_id || null,
62
- cwd: payload.cwd || null,
63
- transcript_path: payload.transcript_path || null,
64
- last_assistant_message: payload.last_assistant_message || null,
65
- ts: new Date().toISOString(),
66
- }), 'utf8');
67
-
68
- const workerPath = path.join(HERE, 'session-commit-worker.mjs');
69
- const child = spawn(process.execPath, [workerPath, spoolFile], {
70
- detached: true,
71
- stdio: 'ignore',
72
- });
73
- child.unref();
74
- } catch (error) {
75
- // Fail-open intégral : jamais de code 2 sur Stop, jamais d'exception qui
76
- // remonte — au pire, ce tour n'aura pas de commit automatique.
77
- logFailure('commit de tour non lancé', error);
78
- }
79
-
80
- return null;
81
- }
82
-
83
- runHook(main, { timeoutMs: 2000 }).catch(() => noop());