@skillcap/gdh 0.26.1 → 0.26.3
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/INSTALL-BUNDLE.json +1 -1
- package/RELEASE-SPAN-UPDATE-CONTRACTS.json +115 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.d.ts +13 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.js +1 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-render.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/authoring-hook-state-path.d.ts +17 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-state-path.d.ts.map +1 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-state-path.js +23 -0
- package/node_modules/@gdh/adapters/dist/authoring-hook-state-path.js.map +1 -0
- package/node_modules/@gdh/adapters/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/adapters/dist/index.js +36 -0
- package/node_modules/@gdh/adapters/dist/index.js.map +1 -1
- package/node_modules/@gdh/adapters/dist/templates/authoring-hook.js.tpl +370 -88
- package/node_modules/@gdh/adapters/package.json +8 -8
- package/node_modules/@gdh/authoring/package.json +2 -2
- package/node_modules/@gdh/cli/package.json +10 -10
- package/node_modules/@gdh/core/dist/index.d.ts +1 -1
- package/node_modules/@gdh/core/dist/index.js +1 -1
- package/node_modules/@gdh/core/package.json +1 -1
- package/node_modules/@gdh/docs/package.json +2 -2
- package/node_modules/@gdh/mcp/package.json +8 -8
- package/node_modules/@gdh/observability/package.json +2 -2
- package/node_modules/@gdh/runtime/package.json +2 -2
- package/node_modules/@gdh/scan/package.json +3 -3
- package/node_modules/@gdh/verify/package.json +7 -7
- package/package.json +11 -11
|
@@ -4,9 +4,18 @@
|
|
|
4
4
|
const PINNED_VERSION = {{pinnedVersionJson}};
|
|
5
5
|
const TARGET_RELATIVE_PATH = {{targetRelativePathJson}};
|
|
6
6
|
const AGENT = {{agentJson}};
|
|
7
|
+
// POSIX-normalised relative path from this hook script's directory (__dirname)
|
|
8
|
+
// to the integration root's `.gdh-state/` directory. Baked at install-plan time
|
|
9
|
+
// from `path.relative(hookDirAbsolute, path.join(integrationRootAbsolute, ".gdh-state"))`.
|
|
10
|
+
// The embedded broker-snapshot reader (Task 2) resolves
|
|
11
|
+
// `path.resolve(__dirname, STATE_RELATIVE_PATH_FROM_HOOK)` to find
|
|
12
|
+
// `authoring/lsp-instance.json` and `authoring/diagnostics-broker/snapshot.json`.
|
|
13
|
+
const STATE_RELATIVE_PATH_FROM_HOOK = {{stateRelativePathFromHookJson}};
|
|
7
14
|
|
|
8
15
|
const fs = require('fs');
|
|
9
16
|
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { pathToFileURL } = require('url');
|
|
10
19
|
const { spawnSync } = require('child_process');
|
|
11
20
|
|
|
12
21
|
const AUTHORING_EXTENSIONS = new Set(['.gd', '.tscn', '.tres']);
|
|
@@ -49,32 +58,36 @@ function handlePostEdit(input, targetRoot) {
|
|
|
49
58
|
// within CHECK_TIMEOUT_MS regardless of warmup outcome (Pitfall 1: detached +
|
|
50
59
|
// stdio:'ignore' + .unref() means the parent never blocks on the child).
|
|
51
60
|
spawnDetachedWarmup(targetRoot);
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
// Quick task 260504-ix2: embedded broker-snapshot reader. Replaces the
|
|
62
|
+
// per-edit synchronous `npx -y @skillcap/gdh@PINNED authoring check ...`
|
|
63
|
+
// shellout (which was eating the full `CHECK_TIMEOUT_MS` budget on the npx
|
|
64
|
+
// bootstrap path even before any GDH code ran). The reader synthesises the
|
|
65
|
+
// same `[fresh]` / `[stale]` / `[pending]` bracketed-token vocabulary the
|
|
66
|
+
// existing dispatch already consumes, so the regex ladder below is unchanged.
|
|
67
|
+
let output;
|
|
68
|
+
try {
|
|
69
|
+
output = runEmbeddedDiagnosticsRead(targetRoot, authoring);
|
|
70
|
+
} catch (_err) {
|
|
71
|
+
return context('GDH post-edit hook embedded read threw; continuing without blocking. Run `gdh authoring check --mode final` before claiming code-validity.');
|
|
60
72
|
}
|
|
61
73
|
// Phase 81 / LSP-06 / D-01: dispatch on bracketed-token vocabulary.
|
|
62
74
|
// Pitfall 5: anchor regex to line-start with /m flag so per-diagnostic
|
|
63
75
|
// [error]/[warning]/[info] severity brackets do not match the status token.
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
// The embedded reader does not currently emit `[failed]` / `[partial]` /
|
|
77
|
+
// `[timeout]` (those statuses come from the legacy CLI compact formatter),
|
|
78
|
+
// but the dispatch retains them as forward-compat branches in case a future
|
|
79
|
+
// reader extension surfaces them.
|
|
80
|
+
if (/^\[failed\]/m.test(output) || /^\[partial\]/m.test(output)) {
|
|
81
|
+
return block(formatBlockingReason(output));
|
|
66
82
|
}
|
|
67
|
-
if (/^\[fresh\]/m.test(
|
|
83
|
+
if (/^\[fresh\]/m.test(output)) {
|
|
68
84
|
return allow();
|
|
69
85
|
}
|
|
70
|
-
if (/^\[pending\]/m.test(
|
|
71
|
-
return context(`GDH post-edit authoring check could not prove this edit quickly; continuing without blocking. ${compactOneLine(
|
|
72
|
-
}
|
|
73
|
-
if (/^\[timeout\]/m.test(result.output)) {
|
|
74
|
-
return context(`GDH post-edit authoring check returned a timeout status; continuing without blocking. ${compactOneLine(result.output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
86
|
+
if (/^\[pending\]/m.test(output) || /^\[stale\]/m.test(output)) {
|
|
87
|
+
return context(`GDH post-edit authoring check could not prove this edit quickly; continuing without blocking. ${compactOneLine(output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
75
88
|
}
|
|
76
|
-
if (
|
|
77
|
-
return context(`GDH post-edit authoring check
|
|
89
|
+
if (/^\[timeout\]/m.test(output)) {
|
|
90
|
+
return context(`GDH post-edit authoring check returned a timeout status; continuing without blocking. ${compactOneLine(output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
78
91
|
}
|
|
79
92
|
return allow();
|
|
80
93
|
}
|
|
@@ -122,58 +135,45 @@ function handlePostToolBatch(input, targetRoot) {
|
|
|
122
135
|
// returns warming without spawning a second Godot.
|
|
123
136
|
spawnDetachedWarmup(targetRoot);
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
// Quick task 260504-ix2: embedded broker-snapshot reader replaces the
|
|
139
|
+
// synchronous `npx ... authoring check` shellout. The reader is invoked
|
|
140
|
+
// ONCE with the full deduplicated authoring set — the LSP-09 coalescing
|
|
141
|
+
// invariant is preserved: the loop in `runEmbeddedDiagnosticsRead` walks
|
|
142
|
+
// every file but returns on the first non-fresh status (first-failure-wins
|
|
143
|
+
// is the documented contract; a [stale] in any one file routes the batch
|
|
144
|
+
// through the drift dispatch branch).
|
|
145
|
+
let output;
|
|
146
|
+
try {
|
|
147
|
+
output = runEmbeddedDiagnosticsRead(targetRoot, authoring);
|
|
148
|
+
} catch (_err) {
|
|
149
|
+
return context('GDH PostToolBatch hook embedded read threw; continuing without blocking. Run `gdh authoring check --mode final` before claiming code-validity.');
|
|
136
150
|
}
|
|
151
|
+
|
|
137
152
|
// Bracketed-token dispatch — IDENTICAL to handlePostEdit. Pitfall 5 from Phase 81
|
|
138
153
|
// applies: anchor regex with /^.../m so per-diagnostic [error]/[warning]/[info]
|
|
139
|
-
// severity brackets do not match the line-leading status token.
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
// severity brackets do not match the line-leading status token. Forward-compat
|
|
155
|
+
// [partial] / [timeout] branches retained per quick-task 260504-ix2 plan note.
|
|
156
|
+
if (/^\[failed\]/m.test(output) || /^\[partial\]/m.test(output)) {
|
|
157
|
+
return block(formatBlockingReason(output));
|
|
142
158
|
}
|
|
143
|
-
if (/^\[fresh\]/m.test(
|
|
159
|
+
if (/^\[fresh\]/m.test(output)) {
|
|
144
160
|
return allow();
|
|
145
161
|
}
|
|
146
|
-
if (/^\[pending\]/m.test(
|
|
147
|
-
return context(`GDH PostToolBatch authoring check could not prove this batch quickly; continuing without blocking. ${compactOneLine(
|
|
148
|
-
}
|
|
149
|
-
if (/^\[timeout\]/m.test(result.output)) {
|
|
150
|
-
return context(`GDH PostToolBatch authoring check returned a timeout status; continuing without blocking. ${compactOneLine(result.output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
162
|
+
if (/^\[pending\]/m.test(output) || /^\[stale\]/m.test(output)) {
|
|
163
|
+
return context(`GDH PostToolBatch authoring check could not prove this batch quickly; continuing without blocking. ${compactOneLine(output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
151
164
|
}
|
|
152
|
-
if (
|
|
153
|
-
return context(`GDH PostToolBatch authoring check
|
|
165
|
+
if (/^\[timeout\]/m.test(output)) {
|
|
166
|
+
return context(`GDH PostToolBatch authoring check returned a timeout status; continuing without blocking. ${compactOneLine(output || 'Run final authoring validation before claiming code-validity.')}`);
|
|
154
167
|
}
|
|
155
168
|
return allow();
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
const output = `${result.stdout || ''}${result.stderr || ''}`.trim();
|
|
166
|
-
const error = result.error ? String(result.error.message || result.error) : '';
|
|
167
|
-
return {
|
|
168
|
-
ok: result.status === 0 && !result.error,
|
|
169
|
-
output,
|
|
170
|
-
error,
|
|
171
|
-
timedOut: Boolean(
|
|
172
|
-
(result.error && result.error.code === 'ETIMEDOUT') ||
|
|
173
|
-
result.signal === 'SIGTERM'
|
|
174
|
-
),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
171
|
+
// Quick task 260504-ix2: `runGdh` deleted. The function previously wrapped
|
|
172
|
+
// `spawnSync('npx', ['-y', '@skillcap/gdh@PINNED', ...args])` for the three
|
|
173
|
+
// event handlers (handlePostEdit, handlePostToolBatch, handleStop). All three
|
|
174
|
+
// now call `runEmbeddedDiagnosticsRead` instead. `spawnDetachedWarmup` uses
|
|
175
|
+
// `child_process.spawn` directly (always did — it was never a `runGdh`
|
|
176
|
+
// caller), so removing the helper has no effect on the warmup path.
|
|
177
177
|
|
|
178
178
|
// Phase 82 / LSP-02. Fire-and-forget warmup spawn from the post-edit hook. The
|
|
179
179
|
// hook returns within CHECK_TIMEOUT_MS regardless of whether warmup has finished
|
|
@@ -202,24 +202,48 @@ function spawnDetachedWarmup(targetRoot) {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// Phase 82 / LSP-03
|
|
205
|
+
// Phase 82 / LSP-03 + Quick task 260504-ix2 — Stop-hook contract relaxation.
|
|
206
|
+
//
|
|
207
|
+
// Original Phase 82 contract: Stop synchronously invoked
|
|
208
|
+
// `gdh authoring check --mode final`, which calls
|
|
209
|
+
// `getManagedLspStatus(launch_if_needed)` and could spawn Godot LSP from the
|
|
210
|
+
// Stop event. Real-world dogfooding (2026-05-04 session) showed this path
|
|
211
|
+
// always timed out due to per-invocation `npx -y @skillcap/gdh@PINNED` cost
|
|
212
|
+
// (~9.3 s on the operator's measured machine). The hook ate the full
|
|
213
|
+
// `STOP_TIMEOUT_MS` budget on the npx bootstrap path before producing any
|
|
214
|
+
// output.
|
|
206
215
|
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
216
|
+
// Updated contract (RFC 0009 addendum): Stop reads the existing broker
|
|
217
|
+
// snapshot via the embedded dependency-free reader; it does NOT synchronously
|
|
218
|
+
// launch LSP. Cold/missing-broker case returns `additionalContext`
|
|
219
|
+
// recommending the user run `gdh authoring check --mode final` manually AND
|
|
220
|
+
// spawns detached warmup so the next session's PostToolUse events have a
|
|
221
|
+
// primed broker. This trades the rare "fresh session, no warm broker" case
|
|
222
|
+
// (where Stop would have produced final diagnostics synchronously) for the
|
|
223
|
+
// common case of sub-100ms hook return on every Stop.
|
|
212
224
|
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
225
|
+
// Hard invariants preserved:
|
|
226
|
+
// - GF3 (260504): zero authoring-files-in-target → noop (git gate, runs first)
|
|
227
|
+
// - Pitfall 3: NEVER returns decision: block (Stop hook must not block session end)
|
|
228
|
+
// - Pitfall 5: Claude additionalContext lives in hookSpecificOutput
|
|
229
|
+
// - Pitfall 6: additionalContext bounded to MAX_STOP_CONTEXT_CHARS = 8000
|
|
217
230
|
function handleStop(input, targetRoot) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
231
|
+
if (!hasAuthoringWorkInTarget(targetRoot)) return allow();
|
|
232
|
+
|
|
233
|
+
// Discover the authoring file set from the working tree under targetRoot.
|
|
234
|
+
// We feed this into the embedded reader so it can resolve broker entries
|
|
235
|
+
// by file:// URI. If the discovery fails (git unavailable, etc.) we fall
|
|
236
|
+
// through with an empty list, which routes to a `[pending]` status with
|
|
237
|
+
// reason `file_not_in_snapshot` — same effect as no broker primed.
|
|
238
|
+
const authoringFiles = listAuthoringFilesInTarget(targetRoot);
|
|
239
|
+
|
|
240
|
+
let output;
|
|
241
|
+
try {
|
|
242
|
+
output = runEmbeddedDiagnosticsRead(targetRoot, authoringFiles);
|
|
243
|
+
} catch (_err) {
|
|
244
|
+
output = '[pending]\nReasons: embedded_read_threw';
|
|
245
|
+
}
|
|
246
|
+
|
|
223
247
|
const truncatedSuffix = ' Run `gdh authoring check --mode final` for full output.';
|
|
224
248
|
function stopContext(message) {
|
|
225
249
|
const text = String(message || '').trim();
|
|
@@ -228,33 +252,254 @@ function handleStop(input, targetRoot) {
|
|
|
228
252
|
const headroom = ceiling - truncatedSuffix.length;
|
|
229
253
|
return context(text.slice(0, headroom).trimEnd() + truncatedSuffix);
|
|
230
254
|
}
|
|
231
|
-
|
|
232
|
-
return stopContext(
|
|
233
|
-
`GDH session-end check timed out after ${STOP_TIMEOUT_MS}ms; continuing without blocking.` +
|
|
234
|
-
' Run `gdh authoring check --mode final` manually before claiming code-validity.',
|
|
235
|
-
);
|
|
236
|
-
}
|
|
255
|
+
|
|
237
256
|
// Bracketed-token dispatch — same vocabulary as PostToolUse but EVERY branch
|
|
238
257
|
// routes to context() or allow(); decision: block is forbidden on Stop.
|
|
239
|
-
|
|
258
|
+
// Forward-compat [partial] / [timeout] / [failed] branches retained for
|
|
259
|
+
// potential future reader extensions; the embedded reader currently emits
|
|
260
|
+
// only [fresh] / [stale] / [pending].
|
|
261
|
+
if (/^\[fresh\]/m.test(output)) {
|
|
240
262
|
return allow();
|
|
241
263
|
}
|
|
242
|
-
if (/^\[failed\]/m.test(
|
|
243
|
-
return stopContext(`GDH session-end check found unresolved diagnostics: ${compactOneLine(
|
|
264
|
+
if (/^\[failed\]/m.test(output) || /^\[partial\]/m.test(output) || /^\[stale\]/m.test(output)) {
|
|
265
|
+
return stopContext(`GDH session-end check found unresolved diagnostics: ${compactOneLine(output)}`);
|
|
244
266
|
}
|
|
245
|
-
if (/^\[pending\]/m.test(
|
|
267
|
+
if (/^\[pending\]/m.test(output) || /^\[timeout\]/m.test(output)) {
|
|
268
|
+
// Cold/missing broker: spawn detached warmup so the NEXT session benefits.
|
|
269
|
+
// The current Stop returns additionalContext recommending manual
|
|
270
|
+
// `gdh authoring check --mode final` (the relaxed contract — see RFC 0009
|
|
271
|
+
// addendum 2026-05-04). spawnDetachedWarmup is fire-and-forget; it never
|
|
272
|
+
// blocks hook return.
|
|
273
|
+
spawnDetachedWarmup(targetRoot);
|
|
246
274
|
return stopContext(
|
|
247
|
-
'GDH session-end check
|
|
248
|
-
|
|
275
|
+
'GDH session-end check could not prove final code-validity from the cached broker snapshot; continuing without blocking. ' +
|
|
276
|
+
'Run `gdh authoring check --mode final` manually before claiming code-validity. ' +
|
|
277
|
+
compactOneLine(output),
|
|
249
278
|
);
|
|
250
279
|
}
|
|
251
|
-
// Unknown
|
|
280
|
+
// Unknown status: degrade to additionalContext (NEVER block).
|
|
281
|
+
spawnDetachedWarmup(targetRoot);
|
|
252
282
|
return stopContext(
|
|
253
283
|
'GDH session-end check could not produce a final result; continuing without blocking. ' +
|
|
254
|
-
|
|
284
|
+
'Run `gdh authoring check --mode final` manually before claiming code-validity. ' +
|
|
285
|
+
compactOneLine(output),
|
|
255
286
|
);
|
|
256
287
|
}
|
|
257
288
|
|
|
289
|
+
// Quick task 260504-ix2: collect authoring-relevant files in the working tree
|
|
290
|
+
// under `targetRoot`. Sibling helper to `hasAuthoringWorkInTarget` (which
|
|
291
|
+
// returns a boolean for the GF3 gate); this returns the list of files for the
|
|
292
|
+
// embedded broker-snapshot reader to look up by file:// URI. Order-stable.
|
|
293
|
+
//
|
|
294
|
+
// Fail-open: returns [] on any git failure (the embedded reader will then emit
|
|
295
|
+
// `[pending]` reasons, which Stop dispatches to additionalContext — never
|
|
296
|
+
// block).
|
|
297
|
+
function listAuthoringFilesInTarget(targetRoot) {
|
|
298
|
+
let result;
|
|
299
|
+
try {
|
|
300
|
+
result = spawnSync('git', ['-C', targetRoot, 'status', '--porcelain', '--', '.'], {
|
|
301
|
+
encoding: 'utf8',
|
|
302
|
+
timeout: 1500,
|
|
303
|
+
windowsHide: true,
|
|
304
|
+
});
|
|
305
|
+
} catch (_error) {
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
if (!result || result.error || result.status !== 0 || result.signal) return [];
|
|
309
|
+
const files = [];
|
|
310
|
+
for (const raw of String(result.stdout || '').split('\n')) {
|
|
311
|
+
if (!raw || raw.length < 4) continue;
|
|
312
|
+
let rest = raw.slice(3);
|
|
313
|
+
const arrow = rest.indexOf(' -> ');
|
|
314
|
+
if (arrow !== -1) rest = rest.slice(arrow + 4);
|
|
315
|
+
if (rest.startsWith('"') && rest.endsWith('"')) rest = rest.slice(1, -1);
|
|
316
|
+
if (isAuthoringValidationPath(rest)) files.push(rest);
|
|
317
|
+
}
|
|
318
|
+
return unique(files);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Quick task 260504-ix2: dependency-free embedded broker-snapshot reader.
|
|
322
|
+
//
|
|
323
|
+
// Replaces the per-edit `npx -y @skillcap/gdh@PINNED authoring check ...`
|
|
324
|
+
// shellout (which paid the ~9.3 s npx bootstrap cost on every hook fire).
|
|
325
|
+
// Reads `<stateRoot>/authoring/lsp-instance.json` and
|
|
326
|
+
// `<stateRoot>/authoring/diagnostics-broker/snapshot.json` directly,
|
|
327
|
+
// validates LSP instance identity + per-file SHA-256 content hash +
|
|
328
|
+
// freshness window, and synthesises the same bracketed-token output the
|
|
329
|
+
// existing dispatch consumes (`[fresh]` / `[stale]` / `[pending]`).
|
|
330
|
+
//
|
|
331
|
+
// Reasons vocabulary (kept aligned with packages/authoring/src/lsp.ts):
|
|
332
|
+
// lsp_instance_not_running, lsp_instance_parse_error,
|
|
333
|
+
// broker_not_yet_primed, snapshot_parse_error, unsupported_broker_schema,
|
|
334
|
+
// lsp_instance_identity_mismatch, content_hash_mismatch, freshness_expired,
|
|
335
|
+
// file_not_in_snapshot, state_root_unresolved.
|
|
336
|
+
//
|
|
337
|
+
// Hash parity: the broker writer (`packages/authoring/src/diagnostics-broker.ts`
|
|
338
|
+
// `hashText`) and the cache validator (`packages/authoring/src/lsp.ts`
|
|
339
|
+
// `hashFilesByUri`) both read files via `fs.readFile(path, "utf8")` and
|
|
340
|
+
// SHA-256 the resulting string. The embedded reader MUST do the same so a
|
|
341
|
+
// fresh snapshot covering an unmodified file matches by content hash.
|
|
342
|
+
function runEmbeddedDiagnosticsRead(targetRoot, authoringFiles) {
|
|
343
|
+
const stateRoot = resolveStateRoot(targetRoot);
|
|
344
|
+
if (stateRoot === null) {
|
|
345
|
+
return formatStatus('pending', ['state_root_unresolved']);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const lspInstancePath = path.join(stateRoot, 'authoring', 'lsp-instance.json');
|
|
349
|
+
let lspInstance;
|
|
350
|
+
try {
|
|
351
|
+
const raw = fs.readFileSync(lspInstancePath, 'utf8');
|
|
352
|
+
lspInstance = JSON.parse(raw);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err && err.code === 'ENOENT') return formatStatus('pending', ['lsp_instance_not_running']);
|
|
355
|
+
return formatStatus('pending', ['lsp_instance_parse_error']);
|
|
356
|
+
}
|
|
357
|
+
const expectedInstanceId = lspInstance && lspInstance.instance && lspInstance.instance.instanceId;
|
|
358
|
+
if (typeof expectedInstanceId !== 'string' || expectedInstanceId.length === 0) {
|
|
359
|
+
return formatStatus('pending', ['lsp_instance_not_running']);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const snapshotPath = path.join(stateRoot, 'authoring', 'diagnostics-broker', 'snapshot.json');
|
|
363
|
+
let snapshot;
|
|
364
|
+
try {
|
|
365
|
+
const raw = fs.readFileSync(snapshotPath, 'utf8');
|
|
366
|
+
snapshot = JSON.parse(raw);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
if (err && err.code === 'ENOENT') return formatStatus('pending', ['broker_not_yet_primed']);
|
|
369
|
+
return formatStatus('pending', ['snapshot_parse_error']);
|
|
370
|
+
}
|
|
371
|
+
if (!snapshot || snapshot.schemaVersion !== 1) {
|
|
372
|
+
return formatStatus('pending', ['unsupported_broker_schema']);
|
|
373
|
+
}
|
|
374
|
+
if (snapshot.lspInstanceId !== expectedInstanceId) {
|
|
375
|
+
return formatStatus('stale', ['lsp_instance_identity_mismatch']);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const filesByUri = new Map();
|
|
379
|
+
if (Array.isArray(snapshot.files)) {
|
|
380
|
+
for (const f of snapshot.files) if (f && typeof f.uri === 'string') filesByUri.set(f.uri, f);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Match the constant in `packages/authoring/src/lsp.ts`
|
|
384
|
+
// (`DIAGNOSTICS_BROKER_DEFAULT_FRESHNESS_STALE_AFTER_MS`). Hard-coded here
|
|
385
|
+
// because the hook is dependency-free and cannot import @gdh/authoring.
|
|
386
|
+
const STALE_AFTER_MS = 60000;
|
|
387
|
+
const nowMs = Date.now();
|
|
388
|
+
const allDiagnostics = [];
|
|
389
|
+
|
|
390
|
+
for (const relPath of authoringFiles) {
|
|
391
|
+
// Match the broker writer's URI canonicalisation: it calls
|
|
392
|
+
// `fs.realpath` on each requested file before `pathToFileURL.href`,
|
|
393
|
+
// which collapses macOS `/var/folders` → `/private/var/folders`
|
|
394
|
+
// symlinks (and similar real-symlink layouts on Linux/Windows). The
|
|
395
|
+
// hook's `__dirname` already comes through canonicalised by Node's
|
|
396
|
+
// module loader, but the legacy `path.resolve` in the resolver does
|
|
397
|
+
// NOT walk symlinks. Use `canonicalPath` (already-defined helper) so
|
|
398
|
+
// the URIs match a snapshot captured by `hashFilesByUri`.
|
|
399
|
+
const absolutePath = canonicalPath(path.resolve(targetRoot, relPath));
|
|
400
|
+
const expectedUri = pathToFileURL(absolutePath).href;
|
|
401
|
+
const entry = filesByUri.get(expectedUri);
|
|
402
|
+
if (!entry) return formatStatus('pending', ['file_not_in_snapshot']);
|
|
403
|
+
|
|
404
|
+
if (typeof entry.contentHash !== 'string' || entry.contentHash.length === 0) {
|
|
405
|
+
// Pre-content-hash snapshot entries cannot prove current file content.
|
|
406
|
+
// Plan info-note: route through `[pending]` (not `[stale]`) — the broker
|
|
407
|
+
// is not yet fully primed for this file even if the snapshot exists.
|
|
408
|
+
return formatStatus('pending', ['broker_not_yet_primed']);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let onDiskHash;
|
|
412
|
+
try {
|
|
413
|
+
const text = fs.readFileSync(absolutePath, 'utf8');
|
|
414
|
+
onDiskHash = crypto.createHash('sha256').update(text).digest('hex');
|
|
415
|
+
} catch (_err) {
|
|
416
|
+
// File deleted between hook trigger and read, or unreadable encoding.
|
|
417
|
+
// Fallthrough: treat as not-in-snapshot so we degrade to additionalContext.
|
|
418
|
+
return formatStatus('pending', ['file_not_in_snapshot']);
|
|
419
|
+
}
|
|
420
|
+
if (onDiskHash !== entry.contentHash) {
|
|
421
|
+
return formatStatus('stale', ['content_hash_mismatch']);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const updatedMs = Date.parse(entry.lastUpdatedAt);
|
|
425
|
+
if (!Number.isFinite(updatedMs) || (nowMs - updatedMs) > STALE_AFTER_MS) {
|
|
426
|
+
return formatStatus('stale', ['freshness_expired']);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (Array.isArray(entry.diagnostics)) {
|
|
430
|
+
for (const diag of entry.diagnostics) {
|
|
431
|
+
allDiagnostics.push({ uri: entry.uri, diag });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return formatFreshDiagnostics(allDiagnostics);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function formatStatus(token, reasons) {
|
|
440
|
+
return `[${token}]\nReasons: ${reasons.join(', ')}`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function formatFreshDiagnostics(items) {
|
|
444
|
+
let errors = 0;
|
|
445
|
+
let warnings = 0;
|
|
446
|
+
let infoCount = 0;
|
|
447
|
+
const lines = [];
|
|
448
|
+
for (const item of items) {
|
|
449
|
+
const diag = item.diag;
|
|
450
|
+
const sev = (diag && typeof diag.severity === 'string') ? diag.severity : 'info';
|
|
451
|
+
if (sev === 'error') errors += 1;
|
|
452
|
+
else if (sev === 'warning') warnings += 1;
|
|
453
|
+
else infoCount += 1;
|
|
454
|
+
const line = (diag && diag.range && diag.range.start && Number.isFinite(diag.range.start.line)) ? diag.range.start.line + 1 : 0;
|
|
455
|
+
const col = (diag && diag.range && diag.range.start && Number.isFinite(diag.range.start.character)) ? diag.range.start.character + 1 : 0;
|
|
456
|
+
const msg = (diag && typeof diag.message === 'string') ? diag.message : '';
|
|
457
|
+
lines.push(`${item.uri}:${line}:${col} [${sev}] ${msg}`);
|
|
458
|
+
}
|
|
459
|
+
// Mirror packages/authoring/src/diagnostics-summary.ts: with the default
|
|
460
|
+
// "error" severity policy, ANY error diagnostic produces a `blocked` summary
|
|
461
|
+
// → CLI emits `[failed]` → hook dispatch routes to `block()`. The embedded
|
|
462
|
+
// reader must produce the same status token so the hook still blocks edits
|
|
463
|
+
// that introduce errors. Warnings and info do not alter the status token.
|
|
464
|
+
const statusToken = errors > 0 ? 'failed' : 'fresh';
|
|
465
|
+
const completion = errors > 0 ? 'blocked' : 'allowed';
|
|
466
|
+
const head = lines.length ? `${lines.join('\n')}\n` : '';
|
|
467
|
+
return `${head}[${statusToken}] ${errors} errors, ${warnings} warnings, ${infoCount} info. Completion ${completion}.`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function resolveStateRoot(targetRoot) {
|
|
471
|
+
// 1. Baked render-time path — the canonical resolution for installed hooks.
|
|
472
|
+
try {
|
|
473
|
+
const baked = path.resolve(__dirname, STATE_RELATIVE_PATH_FROM_HOOK);
|
|
474
|
+
if (fs.existsSync(baked)) return baked;
|
|
475
|
+
} catch (_err) {
|
|
476
|
+
// Fall through to walk-up fallbacks if the baked path is malformed.
|
|
477
|
+
}
|
|
478
|
+
// 2. Walk up from __dirname looking for `.gdh/project.yaml`. Useful when the
|
|
479
|
+
// hook script has been moved or the bake-time relative path is wrong.
|
|
480
|
+
let dir = __dirname;
|
|
481
|
+
for (let i = 0; i < 10; i += 1) {
|
|
482
|
+
if (fs.existsSync(path.join(dir, '.gdh', 'project.yaml'))) {
|
|
483
|
+
return path.join(dir, '.gdh-state');
|
|
484
|
+
}
|
|
485
|
+
const parent = path.dirname(dir);
|
|
486
|
+
if (parent === dir) break;
|
|
487
|
+
dir = parent;
|
|
488
|
+
}
|
|
489
|
+
// 3. Walk up from targetRoot — covers root-launched cases where the hook
|
|
490
|
+
// might be loaded from outside the project tree.
|
|
491
|
+
dir = targetRoot;
|
|
492
|
+
for (let i = 0; i < 10; i += 1) {
|
|
493
|
+
if (fs.existsSync(path.join(dir, '.gdh', 'project.yaml'))) {
|
|
494
|
+
return path.join(dir, '.gdh-state');
|
|
495
|
+
}
|
|
496
|
+
const parent = path.dirname(dir);
|
|
497
|
+
if (parent === dir) break;
|
|
498
|
+
dir = parent;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
258
503
|
function collectChangedFiles(input, targetRoot) {
|
|
259
504
|
const baseCwd = input && typeof input.cwd === 'string' ? input.cwd : process.cwd();
|
|
260
505
|
const files = [];
|
|
@@ -310,6 +555,43 @@ function isAuthoringValidationPath(file) {
|
|
|
310
555
|
return AUTHORING_EXTENSIONS.has(path.extname(file).toLowerCase());
|
|
311
556
|
}
|
|
312
557
|
|
|
558
|
+
// GF3 (260504): session-end git-gate. Stop hook noops when zero authoring-file
|
|
559
|
+
// changes exist in the working tree under targetRoot. Fails closed (returns
|
|
560
|
+
// true) on any git failure so the freshness barrier never silently weakens.
|
|
561
|
+
//
|
|
562
|
+
// Honors GDH_AUTHORING_STOP_GIT_GATE=0 as an ops escape valve.
|
|
563
|
+
function hasAuthoringWorkInTarget(targetRoot) {
|
|
564
|
+
if (process.env['GDH_AUTHORING_STOP_GIT_GATE'] === '0') return true;
|
|
565
|
+
let result;
|
|
566
|
+
try {
|
|
567
|
+
result = spawnSync('git', ['-C', targetRoot, 'status', '--porcelain', '--', '.'], {
|
|
568
|
+
encoding: 'utf8',
|
|
569
|
+
timeout: 1500,
|
|
570
|
+
windowsHide: true,
|
|
571
|
+
});
|
|
572
|
+
} catch (_error) {
|
|
573
|
+
return true; // fail-closed: spawn threw (e.g. git missing on PATH)
|
|
574
|
+
}
|
|
575
|
+
if (!result || result.error || result.status !== 0 || result.signal) return true;
|
|
576
|
+
const lines = String(result.stdout || '').split('\n');
|
|
577
|
+
for (const raw of lines) {
|
|
578
|
+
if (!raw) continue;
|
|
579
|
+
// Porcelain v1: 2 status chars + space + path (or path -> path for renames).
|
|
580
|
+
// Skip lines shorter than the minimum "XY P" shape.
|
|
581
|
+
if (raw.length < 4) continue;
|
|
582
|
+
let rest = raw.slice(3);
|
|
583
|
+
// Renames / copies: "OLD -> NEW" — take NEW.
|
|
584
|
+
const arrow = rest.indexOf(' -> ');
|
|
585
|
+
if (arrow !== -1) rest = rest.slice(arrow + 4);
|
|
586
|
+
// Quoted paths (spaces / non-ASCII): strip surrounding quotes.
|
|
587
|
+
if (rest.startsWith('"') && rest.endsWith('"')) {
|
|
588
|
+
rest = rest.slice(1, -1);
|
|
589
|
+
}
|
|
590
|
+
if (isAuthoringValidationPath(rest)) return true;
|
|
591
|
+
}
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
|
|
313
595
|
function resolveTargetRoot(input) {
|
|
314
596
|
const base = path.resolve(__dirname, '..', '..');
|
|
315
597
|
const fromScript = path.resolve(base, TARGET_RELATIVE_PATH || '.');
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@gdh/authoring": "0.26.
|
|
15
|
-
"@gdh/core": "0.26.
|
|
16
|
-
"@gdh/docs": "0.26.
|
|
17
|
-
"@gdh/observability": "0.26.
|
|
18
|
-
"@gdh/runtime": "0.26.
|
|
19
|
-
"@gdh/scan": "0.26.
|
|
20
|
-
"@gdh/verify": "0.26.
|
|
14
|
+
"@gdh/authoring": "0.26.3",
|
|
15
|
+
"@gdh/core": "0.26.3",
|
|
16
|
+
"@gdh/docs": "0.26.3",
|
|
17
|
+
"@gdh/observability": "0.26.3",
|
|
18
|
+
"@gdh/runtime": "0.26.3",
|
|
19
|
+
"@gdh/scan": "0.26.3",
|
|
20
|
+
"@gdh/verify": "0.26.3"
|
|
21
21
|
},
|
|
22
|
-
"version": "0.26.
|
|
22
|
+
"version": "0.26.3"
|
|
23
23
|
}
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@clack/prompts": "^1.2.0",
|
|
18
|
-
"@gdh/adapters": "0.26.
|
|
19
|
-
"@gdh/authoring": "0.26.
|
|
20
|
-
"@gdh/core": "0.26.
|
|
21
|
-
"@gdh/docs": "0.26.
|
|
22
|
-
"@gdh/mcp": "0.26.
|
|
23
|
-
"@gdh/observability": "0.26.
|
|
24
|
-
"@gdh/runtime": "0.26.
|
|
25
|
-
"@gdh/scan": "0.26.
|
|
26
|
-
"@gdh/verify": "0.26.
|
|
18
|
+
"@gdh/adapters": "0.26.3",
|
|
19
|
+
"@gdh/authoring": "0.26.3",
|
|
20
|
+
"@gdh/core": "0.26.3",
|
|
21
|
+
"@gdh/docs": "0.26.3",
|
|
22
|
+
"@gdh/mcp": "0.26.3",
|
|
23
|
+
"@gdh/observability": "0.26.3",
|
|
24
|
+
"@gdh/runtime": "0.26.3",
|
|
25
|
+
"@gdh/scan": "0.26.3",
|
|
26
|
+
"@gdh/verify": "0.26.3",
|
|
27
27
|
"picocolors": "^1.1.1"
|
|
28
28
|
},
|
|
29
|
-
"version": "0.26.
|
|
29
|
+
"version": "0.26.3"
|
|
30
30
|
}
|
|
@@ -55,7 +55,7 @@ export declare const GDH_AUTHORING_DOGFOOD_VERSION = 1;
|
|
|
55
55
|
export declare const GDH_AUTHORING_SLICE_REPORT_VERSION = 1;
|
|
56
56
|
export declare const GDH_MCP_MANIFEST_VERSION = 1;
|
|
57
57
|
export declare const GDH_CURSOR_RULE_VERSION = 4;
|
|
58
|
-
export declare const GDH_UPDATE_HOOK_VERSION =
|
|
58
|
+
export declare const GDH_UPDATE_HOOK_VERSION = 12;
|
|
59
59
|
export declare const GDH_RUNTIME_RECIPE_RUN_VERSION = 1;
|
|
60
60
|
export declare const GDH_RUNTIME_RUN_BUNDLE_VERSION = 1;
|
|
61
61
|
export declare const GDH_RUNTIME_CORPUS_ARTIFACT_VERSION = 1;
|
|
@@ -47,7 +47,7 @@ export const GDH_AUTHORING_DOGFOOD_VERSION = 1;
|
|
|
47
47
|
export const GDH_AUTHORING_SLICE_REPORT_VERSION = 1;
|
|
48
48
|
export const GDH_MCP_MANIFEST_VERSION = 1;
|
|
49
49
|
export const GDH_CURSOR_RULE_VERSION = 4;
|
|
50
|
-
export const GDH_UPDATE_HOOK_VERSION =
|
|
50
|
+
export const GDH_UPDATE_HOOK_VERSION = 12;
|
|
51
51
|
export const GDH_RUNTIME_RECIPE_RUN_VERSION = 1;
|
|
52
52
|
export const GDH_RUNTIME_RUN_BUNDLE_VERSION = 1;
|
|
53
53
|
export const GDH_RUNTIME_CORPUS_ARTIFACT_VERSION = 1;
|