@piecemaker-legal/piecemaker 2.0.11 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{FindReplaceDialog-KU5TEIBJ-BucOHLdm.js → FindReplaceDialog-KU5TEIBJ-DiS1F6O4.js} +1 -1
- package/dist/assets/{FootnotePropertiesDialog-W3YHM4BV-D5UqbelV.js → FootnotePropertiesDialog-W3YHM4BV-BMRWztwh.js} +1 -1
- package/dist/assets/{HyperlinkDialog-ZYH7ZHRF-8yEhLigd.js → HyperlinkDialog-ZYH7ZHRF-D5r9CZf2.js} +4 -4
- package/dist/assets/{ImagePositionDialog-HEWYR6Q3-wGIBYxCm.js → ImagePositionDialog-HEWYR6Q3-EeK_GFbX.js} +1 -1
- package/dist/assets/{ImagePropertiesDialog-RAB47C6M-D86PckZ2.js → ImagePropertiesDialog-RAB47C6M-BgrZEZLp.js} +1 -1
- package/dist/assets/{PageSetupDialog-AIGIXO3E-DLETP-7v.js → PageSetupDialog-AIGIXO3E-DdFpkcLH.js} +1 -1
- package/dist/assets/{SplitCellDialog-IFBKYCD6-CwdQBXeb.js → SplitCellDialog-IFBKYCD6-B2J0yPH6.js} +1 -1
- package/dist/assets/{TablePropertiesDialog-TMLKGOOW-j2nA7KJs.js → TablePropertiesDialog-TMLKGOOW-iTyD5Vwt.js} +1 -1
- package/dist/assets/{WatermarkDialog-KRL7WMIS-XFJ9gTkC.js → WatermarkDialog-KRL7WMIS-CdwIWeEi.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-DiCrND_Y.js → abnfDiagram-VCTEODGH-CCQgv7yc.js} +1 -1
- package/dist/assets/{arc-DOjgbMYz.js → arc-B-Y10mRQ.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-brzIFVhl.js → architectureDiagram-5GKGNRK7-C-mYZ4DW.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-C8JhTjQQ.js → blockDiagram-I7D4REHJ-Bby14xNO.js} +1 -1
- package/dist/assets/{c4Diagram-7LVT6UL2-jfYxccyk.js → c4Diagram-7LVT6UL2-CoqTzQi7.js} +1 -1
- package/dist/assets/channel-BxbXkYrr.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CrlEShp9.js → chunk-2Q5K7J3B-CYlmti0k.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-DzPlUHGq.js → chunk-5VM5RSS4-W35KZUQj.js} +1 -1
- package/dist/assets/{chunk-F27PBJKO-Cp9cpeWU.js → chunk-F27PBJKO-BVIu5FJA.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-BifUldEN.js → chunk-IMKFNOWR-DRf1mnVb.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-ggPEIkWu.js → chunk-JWPE2WC7-CUJ1N2G9.js} +1 -1
- package/dist/assets/{chunk-POPQ4Y6H-3h_nayRX.js → chunk-POPQ4Y6H-p6IVobio.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-CH29aG6J.js → chunk-SVP7TREG-DOM_pdOM.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-PluJUM5k.js → chunk-TICWLB2K-VqAu84r8.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DXU39Vwg.js → chunk-XXDRQBXY-vaUsyH7g.js} +1 -1
- package/dist/assets/classDiagram-ZZMXUADV-COu_XNpX.js +1 -0
- package/dist/assets/classDiagram-v2-VYDZK3BY-COu_XNpX.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-Dz6roYrN.js → cose-bilkent-JH36ORCC-B_sTVfyq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BLzrpSit.js → cynefin-OW5HDTMX-DvnWaxw0.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-kfxnERDZ.js → cynefinDiagram-5FMLGOSQ-DriBDRYQ.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-DP57bp-N.js → dagre-GXQ25YYZ-BAwYXMGG.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-DAlsrQVW.js → diagram-S7CK7UJ4-DX03OYpD.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-By5yrMer.js → diagram-UQ7AKVKN-rdUs_hF8.js} +1 -1
- package/dist/assets/{diagram-VSXAHHWV-Dx89kryN.js → diagram-VSXAHHWV-CobmLXkM.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-B9fARitZ.js → diagram-VX7I27RA-B18D7USE.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-DC15CEH1.js → diagram-Z3DM3KII-CRrNHL6r.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-BKQzghGF.js → ebnfDiagram-PWID7BFC-HXzGHTn6.js} +1 -1
- package/dist/assets/{erDiagram-RLTQ6QDP-CPY6RM7G.js → erDiagram-RLTQ6QDP-lbfD5sxC.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-4dL4GJcf.js → flowDiagram-HODETNUW-Dd0lb1Fm.js} +1 -1
- package/dist/assets/{ganttDiagram-EL5Y4UJY-D4-cVcfb.js → ganttDiagram-EL5Y4UJY-CKBO9TRh.js} +1 -1
- package/dist/assets/{gitGraphDiagram-WWUBYQGX-BMeVIEiX.js → gitGraphDiagram-WWUBYQGX-Ds_AlKrJ.js} +1 -1
- package/dist/assets/index-C5slQNvE.css +1 -0
- package/dist/assets/{index-DNtFDOni.js → index-C9XXF765.js} +146 -146
- package/dist/assets/{infoDiagram-27XIBGKW-D5qMSUEU.js → infoDiagram-27XIBGKW-BbNgbn6W.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BDnyKRHm.js → ishikawaDiagram-5VMMS53U-B9FZGWPM.js} +1 -1
- package/dist/assets/{journeyDiagram-3NMN7TZE-dGz5ZGzq.js → journeyDiagram-3NMN7TZE-9z26Mgkz.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-D7UAz7o_.js → kanban-definition-UXKFOSKX-dQoIQkFu.js} +1 -1
- package/dist/assets/{layout-DKrmEGrz.js → layout-C03dZv1L.js} +1 -1
- package/dist/assets/{linear-CrTdMY-p.js → linear-BAgwn459.js} +1 -1
- package/dist/assets/{mermaid.core-CXQOT5G6.js → mermaid.core-Bb-zdGYl.js} +6 -6
- package/dist/assets/{mindmap-definition-YA3MSWOX-VgEiYfay.js → mindmap-definition-YA3MSWOX-BZ77-g1t.js} +1 -1
- package/dist/assets/{pegDiagram-XKGWAZYB-9IqT8gGx.js → pegDiagram-XKGWAZYB-vlBCwMpy.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-D98pRE-t.js → pieDiagram-E7YTZNPT-DGaLzhN2.js} +1 -1
- package/dist/assets/{quadrantDiagram-AXDQQJYC-b9PwkYzL.js → quadrantDiagram-AXDQQJYC-J517wJ3s.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-Dz87hsPE.js → railroadDiagram-O6MQD6OU-MiMqTB2q.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-CkcV73dK.js → requirementDiagram-BXWQKSXE-ClFWrfsX.js} +1 -1
- package/dist/assets/{sankeyDiagram-P5KCCOFB-UBN31Yrs.js → sankeyDiagram-P5KCCOFB-BQjzvuzZ.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-CpQxd5Za.js → sequenceDiagram-WJ2MYXX4-BVI9Mtfy.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-mCsFmyzw.js → sizeCapture-INFHLROL-npWe2anw.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-B0MTBN-3.js → stateDiagram-D77RDMKH-BE4pwhgm.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-DzBPN39W.js → stateDiagram-v2-MP3YSRHH-PdR5r-x_.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-Dzydjm0e.js → swimlanes-42K2YHIH-DJMuv4Fv.js} +1 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-Cxv8b9_h.js +8 -0
- package/dist/assets/{timeline-definition-24CTP7MA-Dz0toOLu.js → timeline-definition-24CTP7MA-BLHKdXUW.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-DOWp7dkf.js → vennDiagram-4TSXK5OY-vpWJbaKf.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-CUxsztUB.js → wardleyDiagram-VM6X3IG4-BC1q3_Z5.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-DS96vNED.js → xychartDiagram-S5SC5T6Z--yDqjI6l.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js +61 -1
- package/dist-server/plugins/piecemaker-dossier/src/knowledge.js.map +1 -1
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js +0 -48
- package/dist-server/plugins/piecemaker-dossier/src/scan-result.js.map +1 -1
- package/dist-server/server/piecemaker/desktop-update/service.js +2 -1
- package/dist-server/server/piecemaker/desktop-update/service.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/pipeline.js +25 -29
- package/dist-server/server/piecemaker/knowledge/pipeline.js.map +1 -1
- package/dist-server/server/piecemaker/knowledge/service.js +1 -16
- package/dist-server/server/piecemaker/knowledge/service.js.map +1 -1
- package/dist-server/server/piecemaker/project-registry.js +5 -5
- package/dist-server/server/piecemaker/project-registry.js.map +1 -1
- package/package.json +1 -1
- package/server/piecemaker/anonymizer/hudsucker-fixture.cjs +1 -1
- package/server/piecemaker/anonymizer/hudsucker-proxy/src/main.rs +2 -1
- package/server/piecemaker/anonymizer/rewrite.cjs +2 -2
- package/server/piecemaker/anonymizer/service.cjs +3 -2
- package/server/piecemaker/anonymizer/sqlite-dictionary.cjs +3 -31
- package/server/piecemaker/desktop-update/service.ts +2 -1
- package/server/piecemaker/index.ts +1 -1
- package/server/piecemaker/knowledge/pipeline.ts +24 -31
- package/server/piecemaker/knowledge/service.ts +1 -14
- package/server/piecemaker/project-registry.ts +6 -6
- package/server/piecemaker/vendor/installer/bin/piecemaker.mjs +9 -162
- package/server/piecemaker/vendor/installer/lib/commandes.mjs +1 -2
- package/server/piecemaker/vendor/installer/lib/conversion-client.mjs +62 -3
- package/server/piecemaker/vendor/installer/steps/06-hooks.mjs +23 -69
- package/server/piecemaker/vendor/installer/steps/12-mcp-piecemaker.mjs +3 -3
- package/server/piecemaker/vendor/mcp/README.md +1 -1
- package/server/piecemaker/vendor/mcp/piecemaker/server.mjs +12 -75
- package/server/piecemaker/vendor/piecemaker-plugin/hooks/hooks.json +0 -40
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/compile-recherche.mjs +2 -2
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/case-folders.cjs +26 -7
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/commits.cjs +0 -28
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/hook-io.mjs +0 -13
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/institutional-terms.cjs +3 -3
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/mapping.cjs +10 -143
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/protection.cjs +1 -1
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/lib/session-timing.cjs +0 -65
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/protect-originals.mjs +11 -2
- package/server/piecemaker/vendor/piecemaker-plugin/skills/conversion-md/SKILL.md +9 -5
- package/server/piecemaker/vendor/websocket-server/admin-routes.cjs +2 -150
- package/server/piecemaker/vendor/websocket-server/claude-hooks.cjs +3 -0
- package/server/piecemaker/vendor/websocket-server/originals-pipeline.cjs +23 -372
- package/server/piecemaker/vendor/websocket-server/renamed-originals.cjs +136 -0
- package/server/piecemaker/vendor/websocket-server/scripts/convert_and_scan_pipeline.py +115 -96
- package/dist/assets/channel-Dajzu2W1.js +0 -1
- package/dist/assets/classDiagram-ZZMXUADV-dDEP_giv.js +0 -1
- package/dist/assets/classDiagram-v2-VYDZK3BY-dDEP_giv.js +0 -1
- package/dist/assets/index-Dc9wNuS4.css +0 -1
- package/dist/assets/swimlanesDiagram-VR7AAH4N-BXQpbUMD.js +0 -8
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/billing-track.mjs +0 -194
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/commit-track.mjs +0 -71
- package/server/piecemaker/vendor/piecemaker-plugin/scripts/session-commit-worker.mjs +0 -260
- 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());
|