@geraldmaron/construct 1.0.12 → 1.0.14
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/README.md +8 -2
- package/bin/construct +259 -11
- package/bin/construct-postinstall.mjs +23 -2
- package/lib/auto-docs.mjs +11 -7
- package/lib/dashboard-static.mjs +7 -4
- package/lib/doc-stamp.mjs +16 -0
- package/lib/docs-verify.mjs +1 -8
- package/lib/doctor/watchers/bd-watch.mjs +6 -2
- package/lib/embed/daemon.mjs +20 -0
- package/lib/embed/docs-lifecycle.mjs +19 -0
- package/lib/embed/inbox.mjs +85 -2
- package/lib/embed/recommendation-store.mjs +29 -0
- package/lib/gates-audit.mjs +18 -12
- package/lib/hooks/_lib/input.mjs +52 -0
- package/lib/hooks/adaptive-lint.mjs +4 -0
- package/lib/hooks/agent-tracker.mjs +59 -15
- package/lib/hooks/audit-reads.mjs +83 -42
- package/lib/hooks/audit-trail.mjs +28 -18
- package/lib/hooks/bash-output-logger.mjs +8 -2
- package/lib/hooks/block-no-verify.mjs +4 -0
- package/lib/hooks/ci-status-check.mjs +4 -0
- package/lib/hooks/comment-lint.mjs +6 -4
- package/lib/hooks/config-protection.mjs +4 -0
- package/lib/hooks/context-watch.mjs +4 -0
- package/lib/hooks/context-window-recovery.mjs +4 -0
- package/lib/hooks/dep-audit.mjs +12 -5
- package/lib/hooks/doc-coupling-check.mjs +5 -1
- package/lib/hooks/edit-accumulator.mjs +25 -10
- package/lib/hooks/edit-error-recovery.mjs +4 -0
- package/lib/hooks/edit-guard.mjs +4 -0
- package/lib/hooks/guard-bash.mjs +4 -0
- package/lib/hooks/mcp-audit.mjs +4 -0
- package/lib/hooks/mcp-health-check.mjs +4 -0
- package/lib/hooks/model-fallback.mjs +7 -11
- package/lib/hooks/policy-engine.mjs +4 -0
- package/lib/hooks/post-merge-docs-check.mjs +4 -0
- package/lib/hooks/post-merge-tracking.mjs +82 -0
- package/lib/hooks/pre-compact.mjs +4 -0
- package/lib/hooks/pre-push-gate.mjs +84 -231
- package/lib/hooks/proactive-activation.mjs +5 -2
- package/lib/hooks/readme-age-check.mjs +4 -0
- package/lib/hooks/registry-sync.mjs +35 -20
- package/lib/hooks/rule-verifier.mjs +3 -0
- package/lib/hooks/scan-secrets.mjs +4 -0
- package/lib/hooks/session-optimize.mjs +4 -0
- package/lib/hooks/session-reflect.mjs +4 -0
- package/lib/hooks/session-start.mjs +48 -1
- package/lib/hooks/session-tracking-refresh.mjs +70 -0
- package/lib/hooks/stop-notify.mjs +13 -2
- package/lib/hooks/stop-typecheck.mjs +4 -0
- package/lib/hooks/test-watch.mjs +8 -4
- package/lib/init-unified.mjs +90 -23
- package/lib/intake/attribution.mjs +77 -0
- package/lib/intake/intake-config.mjs +3 -0
- package/lib/intake/manifest.mjs +107 -0
- package/lib/intake/poll-lock.mjs +136 -0
- package/lib/intake/prepare.mjs +42 -4
- package/lib/logging/rotate.mjs +394 -0
- package/lib/mcp/tools/project.mjs +2 -2
- package/lib/mcp/tools/telemetry.mjs +1 -1
- package/lib/opencode-config.mjs +10 -3
- package/lib/orchestration/routing-tables.mjs +176 -0
- package/lib/orchestration-policy.mjs +18 -106
- package/lib/parity.mjs +48 -7
- package/lib/profiles/lifecycle.mjs +19 -1
- package/lib/project-init-shared.mjs +11 -5
- package/lib/project-root.mjs +104 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/event-bus.mjs +29 -9
- package/lib/roles/router.mjs +8 -7
- package/lib/server/index.mjs +31 -9
- package/lib/server/static/index.html +1 -15
- package/lib/specialist-contracts-enforce.mjs +24 -10
- package/lib/status.mjs +1 -1
- package/lib/storage/backup.mjs +2 -2
- package/lib/telemetry/intent-verifications.mjs +16 -3
- package/lib/telemetry/skill-calls.mjs +12 -3
- package/lib/tracking-surfaces.mjs +375 -0
- package/lib/worker/trace.mjs +19 -2
- package/package.json +6 -2
- package/platforms/claude/settings.template.json +28 -27
- package/scripts/sync-specialists.mjs +269 -75
- package/specialists/registry.json +158 -13
- package/lib/hooks/env-check.mjs +0 -83
- package/lib/hooks/read-tracker.mjs +0 -61
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/server/static/assets/index-ab25c707.js +0 -70
- package/lib/server/static/assets/index-f0c80a2b.css +0 -1
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* For Bash, only records commands that mutate state (git/npm/docker/cargo etc.
|
|
8
8
|
* write operations, rm, mv, chmod). Read-only commands are skipped.
|
|
9
9
|
*
|
|
10
|
-
* Each record is a single JSONL line written to
|
|
10
|
+
* Each record is a single JSONL line written to
|
|
11
|
+
* <project>/.cx/audit-trail.jsonl (falls back to ~/.cx/audit-trail.jsonl
|
|
12
|
+
* when the hook fires outside a Construct project) with:
|
|
11
13
|
* - timestamp ISO
|
|
12
14
|
* - session_id (from hook input when available)
|
|
13
15
|
* - tool_name
|
|
@@ -21,18 +23,26 @@
|
|
|
21
23
|
* - prev_line_hash (chained tamper-evidence — hash of previous JSONL line)
|
|
22
24
|
*
|
|
23
25
|
* The prev_line_hash chain means any after-the-fact reordering or deletion
|
|
24
|
-
* breaks the chain and is detectable by a simple replay.
|
|
26
|
+
* breaks the chain and is detectable by a simple replay. Bounded via the
|
|
27
|
+
* `audit-trail` channel registered in lib/logging/rotate.mjs LIMITS.
|
|
25
28
|
*
|
|
26
29
|
* @p95ms 15
|
|
27
30
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
31
|
+
*
|
|
32
|
+
* @lifecycle PostToolUse
|
|
33
|
+
* @matcher Edit|Write|MultiEdit|NotebookEdit|Bash
|
|
34
|
+
* @exits 0 = pass
|
|
28
35
|
*/
|
|
29
|
-
import { readFileSync,
|
|
36
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
30
37
|
import { createHash } from 'node:crypto';
|
|
31
|
-
import { join, resolve } from 'node:path';
|
|
38
|
+
import { join, resolve, dirname } from 'node:path';
|
|
32
39
|
import { homedir } from 'node:os';
|
|
40
|
+
import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
|
|
41
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
42
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
33
43
|
|
|
34
44
|
const CX_DIR = join(homedir(), '.cx');
|
|
35
|
-
const AUDIT_FILE =
|
|
45
|
+
const AUDIT_FILE = resolveProjectScopedPath('audit-trail.jsonl', { ensureDir: false });
|
|
36
46
|
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
37
47
|
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
38
48
|
|
|
@@ -69,14 +79,9 @@ function readLastAgent() {
|
|
|
69
79
|
|
|
70
80
|
function readPrevLineHash() {
|
|
71
81
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const readFrom = Math.max(0, size - 2048);
|
|
76
|
-
const fs = readFileSync(AUDIT_FILE, 'utf8').slice(readFrom);
|
|
77
|
-
const lines = fs.split('\n').filter(Boolean);
|
|
78
|
-
if (lines.length === 0) return null;
|
|
79
|
-
return sha256(lines[lines.length - 1]);
|
|
82
|
+
const lastLine = readLastLineAcrossSegments(AUDIT_FILE);
|
|
83
|
+
if (lastLine === null) return null;
|
|
84
|
+
return sha256(lastLine);
|
|
80
85
|
} catch {
|
|
81
86
|
return null;
|
|
82
87
|
}
|
|
@@ -89,7 +94,8 @@ function truncateSnippet(s, limit = 280) {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
let input = {};
|
|
92
|
-
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
97
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
98
|
+
catch (err) { logHookFailure({ hook: 'audit-trail', err, phase: 'parse' }); process.exit(0); }
|
|
93
99
|
|
|
94
100
|
const toolName = input?.tool_name || '';
|
|
95
101
|
if (!MUTATING_TOOLS.has(toolName)) process.exit(0);
|
|
@@ -129,7 +135,9 @@ if (toolName !== 'Bash' && target) {
|
|
|
129
135
|
if (existsSync(absPath)) {
|
|
130
136
|
contentHash = sha256(readFileSync(absPath, 'utf8')).slice(0, 32);
|
|
131
137
|
}
|
|
132
|
-
} catch {
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logHookFailure({ hook: 'audit-trail', err, phase: 'hash', input: { target: absPath } });
|
|
140
|
+
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
const record = {
|
|
@@ -146,8 +154,10 @@ const record = {
|
|
|
146
154
|
};
|
|
147
155
|
|
|
148
156
|
try {
|
|
149
|
-
mkdirSync(
|
|
150
|
-
|
|
151
|
-
} catch {
|
|
157
|
+
mkdirSync(dirname(AUDIT_FILE), { recursive: true });
|
|
158
|
+
appendBounded('audit-trail', AUDIT_FILE, JSON.stringify(record) + '\n');
|
|
159
|
+
} catch (err) {
|
|
160
|
+
logHookFailure({ hook: 'audit-trail', err, phase: 'append' });
|
|
161
|
+
}
|
|
152
162
|
|
|
153
163
|
process.exit(0);
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @p95ms 20
|
|
16
16
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
17
|
+
*
|
|
18
|
+
* @lifecycle PostToolUse
|
|
19
|
+
* @matcher Bash
|
|
20
|
+
* @exits 0 = pass
|
|
17
21
|
*/
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync
|
|
22
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
19
23
|
import { join } from 'node:path';
|
|
20
24
|
import { homedir } from 'node:os';
|
|
25
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
26
|
|
|
22
27
|
const SIZE_THRESHOLD_CHARS = 4000;
|
|
23
28
|
const LOG_DIR = join(homedir(), '.cx', 'bash-logs');
|
|
@@ -61,7 +66,8 @@ try {
|
|
|
61
66
|
);
|
|
62
67
|
|
|
63
68
|
try {
|
|
64
|
-
|
|
69
|
+
appendBounded(
|
|
70
|
+
'bash-warn-flags',
|
|
65
71
|
WARN_FLAGS,
|
|
66
72
|
`Bash output ${kb} KB saved to ${logPath} — prefer grepping the log over re-running.\n`,
|
|
67
73
|
);
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @p95ms 4000
|
|
16
16
|
* @maxBlockingScope none (UserPromptSubmit, non-blocking)
|
|
17
|
+
*
|
|
18
|
+
* @lifecycle UserPromptSubmit
|
|
19
|
+
* @matcher *
|
|
20
|
+
* @exits 0 = pass
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* narrative voice, point-in-time notes, noise sentinels. The agent sees the
|
|
8
8
|
* block reason and is expected to fix the comment before continuing.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* No bypass. If the policy fires on a legitimate comment, the policy is wrong
|
|
11
|
+
* — fix the policy in lib/comment-lint.mjs, not the call site.
|
|
11
12
|
*
|
|
12
13
|
* Input: reads tool_input.file_path from either the legacy TOOL_INPUT_FILE_PATH
|
|
13
14
|
* env var or modern stdin JSON ({ tool_input: { file_path } }).
|
|
14
15
|
*
|
|
16
|
+
* @lifecycle PostToolUse
|
|
17
|
+
* @matcher Write|Edit|MultiEdit
|
|
15
18
|
* @p95ms 60
|
|
16
19
|
* @maxBlockingScope PostToolUse
|
|
20
|
+
* @exits 0 = pass | 2 = block tool call
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { readFileSync } from 'node:fs';
|
|
@@ -21,8 +25,6 @@ import path from 'node:path';
|
|
|
21
25
|
import { fileURLToPath } from 'node:url';
|
|
22
26
|
import { lintFile, formatResults } from '../comment-lint.mjs';
|
|
23
27
|
|
|
24
|
-
if (process.env.CONSTRUCT_SKIP_COMMENT_LINT === '1') process.exit(0);
|
|
25
|
-
|
|
26
28
|
let filePath = process.env.TOOL_INPUT_FILE_PATH;
|
|
27
29
|
if (!filePath) {
|
|
28
30
|
try {
|
|
@@ -59,6 +61,6 @@ if (artifactOnly) {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
process.stderr.write(
|
|
62
|
-
'\nComment policy blocked this edit. Remove the violations
|
|
64
|
+
'\nComment policy blocked this edit. Remove the violations before continuing.\n',
|
|
63
65
|
);
|
|
64
66
|
process.exit(2);
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @p95ms 5
|
|
18
18
|
* @maxBlockingScope PreToolUse
|
|
19
|
+
*
|
|
20
|
+
* @lifecycle PreToolUse
|
|
21
|
+
* @matcher Write|Edit|MultiEdit
|
|
22
|
+
* @exits 0 = pass | 2 = block tool call
|
|
19
23
|
*/
|
|
20
24
|
import { readFileSync } from 'fs';
|
|
21
25
|
import { logHookFailure } from './_lib/log.mjs';
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @p95ms 20
|
|
28
28
|
* @maxBlockingScope UserPromptSubmit
|
|
29
|
+
*
|
|
30
|
+
* @lifecycle UserPromptSubmit
|
|
31
|
+
* @matcher *
|
|
32
|
+
* @exits 0 = pass
|
|
29
33
|
*/
|
|
30
34
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
31
35
|
import { join } from 'node:path';
|
package/lib/hooks/dep-audit.mjs
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @p95ms 5000
|
|
10
10
|
* @maxBlockingScope none (PostToolUse, async, non-blocking)
|
|
11
|
+
*
|
|
12
|
+
* @lifecycle PostToolUse
|
|
13
|
+
* @matcher Write|Edit
|
|
14
|
+
* @exits 0 = pass
|
|
11
15
|
*/
|
|
12
|
-
import { readFileSync,
|
|
13
|
-
import { execSync } from 'child_process';
|
|
14
|
-
import { homedir } from 'os';
|
|
15
|
-
import { join, dirname, basename } from 'path';
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { execSync } from 'node:child_process';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, dirname, basename } from 'node:path';
|
|
20
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
16
22
|
|
|
17
23
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
18
24
|
if (!filePath) process.exit(0);
|
|
@@ -28,7 +34,8 @@ const projectDir = dirname(filePath);
|
|
|
28
34
|
const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
|
|
29
35
|
|
|
30
36
|
function appendWarn(msg) {
|
|
31
|
-
try {
|
|
37
|
+
try { appendBounded('bash-warn-flags', warnFlagsPath, msg + '\n'); }
|
|
38
|
+
catch (err) { logHookFailure({ hook: 'dep-audit', err, phase: 'append' }); }
|
|
32
39
|
process.stderr.write(`[dep-audit] ${msg}\n`);
|
|
33
40
|
emitVulnerability(msg);
|
|
34
41
|
}
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @p95ms 15
|
|
14
14
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
15
|
+
*
|
|
16
|
+
* @lifecycle PostToolUse
|
|
17
|
+
* @matcher Write|Edit|MultiEdit
|
|
18
|
+
* @exits 0 = pass
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
@@ -73,7 +77,7 @@ writeState(state);
|
|
|
73
77
|
|
|
74
78
|
if (advisory != null) {
|
|
75
79
|
process.stderr.write(
|
|
76
|
-
`[doc-coupling] ${state.codeEdits} code file edit(s) this session under lib/|bin/|src/|app/; CHANGELOG.md / docs / .cx/context still not touched. Update docs before the next commit (the pre-commit gate will block otherwise
|
|
80
|
+
`[doc-coupling] ${state.codeEdits} code file edit(s) this session under lib/|bin/|src/|app/; CHANGELOG.md / docs / .cx/context still not touched. Update docs before the next commit (the pre-commit gate will block otherwise).\n`,
|
|
77
81
|
);
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -1,39 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/edit-accumulator.mjs —
|
|
3
|
+
* lib/hooks/edit-accumulator.mjs — accumulates edited paths for the next
|
|
4
|
+
* typecheck batch.
|
|
4
5
|
*
|
|
5
|
-
* Runs as PostToolUse after Edit/Write. Maintains
|
|
6
|
+
* Runs as PostToolUse after Edit/Write. Maintains two pieces of session
|
|
7
|
+
* state at ~/.cx/: a total edit counter for surface-area summaries and a
|
|
8
|
+
* deduped queue of TS/JS files awaiting typecheck. The typecheck queue is
|
|
9
|
+
* bounded via the `edit-accumulator` channel in lib/logging/rotate.mjs.
|
|
6
10
|
*
|
|
7
11
|
* @p95ms 10
|
|
8
12
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
13
|
+
*
|
|
14
|
+
* @lifecycle PostToolUse
|
|
15
|
+
* @matcher Write|Edit|MultiEdit
|
|
16
|
+
* @exits 0 = pass
|
|
9
17
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { homedir } from 'os';
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
22
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
12
23
|
|
|
13
24
|
const TS_EXTS = new Set(['.ts','.tsx','.js','.mjs','.jsx','.cjs']);
|
|
14
25
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
15
26
|
|
|
16
27
|
if (!filePath) process.exit(0);
|
|
17
28
|
|
|
18
|
-
const ext =
|
|
29
|
+
const ext = path.extname(filePath);
|
|
19
30
|
const home = homedir();
|
|
20
|
-
const countPath =
|
|
31
|
+
const countPath = path.join(home, '.cx', 'files-changed-count.txt');
|
|
21
32
|
|
|
22
33
|
// Always increment the total files-changed counter for notify
|
|
23
34
|
try {
|
|
24
35
|
const current = existsSync(countPath) ? parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) : 0;
|
|
25
36
|
writeFileSync(countPath, String(current + 1));
|
|
26
|
-
} catch {
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logHookFailure({ hook: 'edit-accumulator', err, phase: 'count' });
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
// Only accumulate TS/JS files for typecheck
|
|
29
42
|
if (!TS_EXTS.has(ext)) process.exit(0);
|
|
30
43
|
|
|
31
|
-
const tcPath =
|
|
44
|
+
const tcPath = path.join(home, '.cx', 'pending-typecheck.txt');
|
|
32
45
|
try {
|
|
33
46
|
const existing = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
|
|
34
47
|
if (!existing.includes(filePath)) {
|
|
35
|
-
|
|
48
|
+
appendBounded('edit-accumulator', tcPath, filePath + '\n');
|
|
36
49
|
}
|
|
37
|
-
} catch {
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logHookFailure({ hook: 'edit-accumulator', err, phase: 'append' });
|
|
52
|
+
}
|
|
38
53
|
|
|
39
54
|
process.exit(0);
|
package/lib/hooks/edit-guard.mjs
CHANGED
package/lib/hooks/guard-bash.mjs
CHANGED
package/lib/hooks/mcp-audit.mjs
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @p95ms 10
|
|
9
9
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
10
|
+
*
|
|
11
|
+
* @lifecycle PostToolUse
|
|
12
|
+
* @matcher mcp__.*
|
|
13
|
+
* @exits 0 = pass
|
|
10
14
|
*/
|
|
11
15
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
12
16
|
import { join } from 'path';
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @p95ms 51
|
|
8
8
|
* @maxBlockingScope none (PreToolUse, warn-only)
|
|
9
|
+
*
|
|
10
|
+
* @lifecycle PreToolUse
|
|
11
|
+
* @matcher mcp__.*
|
|
12
|
+
* @exits 0 = pass | 2 = block tool call
|
|
9
13
|
*/
|
|
10
14
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
15
|
import { homedir } from 'os';
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @p95ms 150
|
|
17
17
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
18
|
+
*
|
|
19
|
+
* @lifecycle PostToolUseFailure
|
|
20
|
+
* @matcher *
|
|
21
|
+
* @exits 0 = pass
|
|
18
22
|
*/
|
|
19
|
-
import { existsSync, mkdirSync,
|
|
23
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
20
24
|
import { execFileSync } from 'node:child_process';
|
|
21
25
|
import { homedir } from 'node:os';
|
|
22
26
|
import { join } from 'node:path';
|
|
@@ -27,20 +31,12 @@ import {
|
|
|
27
31
|
selectFallbackModel,
|
|
28
32
|
writeProviderCooldown,
|
|
29
33
|
} from '../model-router.mjs';
|
|
34
|
+
import { readHookInput } from './_lib/input.mjs';
|
|
30
35
|
|
|
31
36
|
const cooldownPath = join(homedir(), '.cx', 'provider-cooldowns.json');
|
|
32
37
|
const envPath = join(process.cwd(), '.env');
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(0, 'utf8');
|
|
37
|
-
return raw ? JSON.parse(raw) : {};
|
|
38
|
-
} catch {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const input = readInput();
|
|
39
|
+
const input = readHookInput();
|
|
44
40
|
|
|
45
41
|
const classified = classifyProviderFailure(input);
|
|
46
42
|
if (!classified || !classified.retryable) process.exit(0);
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @p95ms 80
|
|
17
17
|
* @maxBlockingScope PreToolUse, Stop, UserPromptSubmit
|
|
18
|
+
*
|
|
19
|
+
* @lifecycle PreToolUse
|
|
20
|
+
* @matcher Write|Edit|MultiEdit|NotebookEdit|TodoWrite|Read|Grep|Glob|LS|NotebookRead
|
|
21
|
+
* @exits 0 = pass | 2 = block tool call
|
|
18
22
|
*/
|
|
19
23
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
24
|
import { execSync } from 'node:child_process';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/post-merge-tracking.mjs — close beads referenced by a merged PR.
|
|
4
|
+
*
|
|
5
|
+
* Runs as a PostToolUse hook on Bash. Watches for `gh pr merge <N>` (or
|
|
6
|
+
* `gh pr merge --auto`) invocations that exited successfully; parses the
|
|
7
|
+
* PR body via `gh pr view <N> --json body`; extracts every `Refs:` /
|
|
8
|
+
* `Closes:` / `Fixes:` line that names a `construct-XXX` bead; closes each
|
|
9
|
+
* still-open bead with reason `Merged via PR #N (<sha>)`. Idempotent on
|
|
10
|
+
* beads already closed.
|
|
11
|
+
*
|
|
12
|
+
* The PR-body parse is the source of truth — not the merge commit subject
|
|
13
|
+
* — because the bundle PR pattern stores the bead refs in the body's
|
|
14
|
+
* "Beads issue" section and the squash-merge subject often drops them.
|
|
15
|
+
*
|
|
16
|
+
* Best-effort. Exits 0 on any parse or shell failure so the surrounding
|
|
17
|
+
* tool call is never affected. Stderr lines are advisory; the actual
|
|
18
|
+
* outcome is the `bd close` state, which is what the next Stop hook's
|
|
19
|
+
* tracking refresh will read.
|
|
20
|
+
*
|
|
21
|
+
* @lifecycle PostToolUse
|
|
22
|
+
* @matcher Bash
|
|
23
|
+
* @p95ms 3000
|
|
24
|
+
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
25
|
+
* @exits 0 = pass
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { spawnSync } from 'node:child_process';
|
|
29
|
+
import { readHookInput } from './_lib/input.mjs';
|
|
30
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
31
|
+
|
|
32
|
+
const input = readHookInput();
|
|
33
|
+
const command = input?.tool_input?.command || input?.command || '';
|
|
34
|
+
const exitCode = input?.tool_response?.exit_code ?? input?.tool_response?.exitCode;
|
|
35
|
+
|
|
36
|
+
const ghPrMergeMatch = /\bgh\s+pr\s+merge\b(?:\s+(\d+))?/.exec(command);
|
|
37
|
+
if (!ghPrMergeMatch) process.exit(0);
|
|
38
|
+
if (exitCode !== undefined && exitCode !== 0) process.exit(0);
|
|
39
|
+
|
|
40
|
+
const cwd = input?.cwd || process.cwd();
|
|
41
|
+
const prNumber = await resolvePrNumber(ghPrMergeMatch[1], cwd);
|
|
42
|
+
if (!prNumber) process.exit(0);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const { closeBeadsFromPrRefs } = await import('../tracking-surfaces.mjs');
|
|
46
|
+
const mergeCommitSha = readMergeCommitSha(cwd);
|
|
47
|
+
const result = await closeBeadsFromPrRefs({ prNumber, mergeCommitSha, cwd });
|
|
48
|
+
if (result?.closed?.length > 0) {
|
|
49
|
+
process.stderr.write(`[post-merge-tracking] closed beads: ${result.closed.join(', ')}\n`);
|
|
50
|
+
}
|
|
51
|
+
if (result?.errors?.length > 0) {
|
|
52
|
+
process.stderr.write(`[post-merge-tracking] failed to close: ${result.errors.map((e) => e.id).join(', ')}\n`);
|
|
53
|
+
}
|
|
54
|
+
} catch (err) {
|
|
55
|
+
logHookFailure({ hook: 'post-merge-tracking', err, phase: 'close' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
59
|
+
|
|
60
|
+
async function resolvePrNumber(captured, cwd) {
|
|
61
|
+
if (captured) return captured;
|
|
62
|
+
// Bare `gh pr merge --auto` / `gh pr merge -s` invocations omit the PR
|
|
63
|
+
// number. Resolve via the most recent merge commit subject, which `gh`
|
|
64
|
+
// standardly tails with `(#N)`.
|
|
65
|
+
try {
|
|
66
|
+
const lastLog = spawnSync('git', ['log', '-1', '--pretty=format:%s'], { cwd, encoding: 'utf8', timeout: 2000 });
|
|
67
|
+
if (lastLog.status !== 0) return null;
|
|
68
|
+
const m = /#(\d+)/.exec(lastLog.stdout);
|
|
69
|
+
return m ? m[1] : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readMergeCommitSha(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 2000 });
|
|
78
|
+
return result.status === 0 ? result.stdout.trim() : '';
|
|
79
|
+
} catch {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|