@geraldmaron/construct 1.0.9 → 1.0.10
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.
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hooks/_lib/specialist-agent.mjs — Detect whether a Construct specialist
|
|
3
|
+
* sub-agent is currently active.
|
|
4
|
+
*
|
|
5
|
+
* A specialist is "active" when one of:
|
|
6
|
+
* - process.env.CONSTRUCT_AGENT_ID is set (explicit fence dispatch)
|
|
7
|
+
* - ~/.cx/last-agent-<id>.json or ~/.cx/last-agent.json carries a
|
|
8
|
+
* timestamp inside the 10-minute fence window (mirrors guard-bash.mjs)
|
|
9
|
+
*
|
|
10
|
+
* Differentiates "subagent driving the session, hold the line" from "main
|
|
11
|
+
* session (likely a human), warn but allow".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, existsSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
|
|
18
|
+
const FENCE_WINDOW_MS = 10 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
export function isSpecialistAgentActive({ now = Date.now, home = homedir, env = process.env } = {}) {
|
|
21
|
+
if (env.CONSTRUCT_AGENT_ID) return true;
|
|
22
|
+
try {
|
|
23
|
+
const cxDir = join(home(), '.cx');
|
|
24
|
+
const id = String(env.CONSTRUCT_AGENT_ID || '').replace(/^cx-/, '');
|
|
25
|
+
const candidates = [];
|
|
26
|
+
if (id) candidates.push(join(cxDir, `last-agent-${id.replace(/[^a-z0-9._-]/gi, '_')}.json`));
|
|
27
|
+
candidates.push(join(cxDir, 'last-agent.json'));
|
|
28
|
+
for (const path of candidates) {
|
|
29
|
+
if (!existsSync(path)) continue;
|
|
30
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
31
|
+
const lastTs = data?.ts ? Date.parse(data.ts) : 0;
|
|
32
|
+
if (lastTs && (now() - lastTs) < FENCE_WINDOW_MS) return true;
|
|
33
|
+
}
|
|
34
|
+
} catch { /* tracker missing or unreadable — treat as no agent active */ }
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* template policy before the PR is opened
|
|
10
10
|
* gh pr edit same body lint when --body / --body-file is changed
|
|
11
11
|
*
|
|
12
|
+
* Template-policy severity is conditional: a hard block (exit 2) when a
|
|
13
|
+
* specialist sub-agent is active (CONSTRUCT_AGENT_ID set, or the cx-tracker
|
|
14
|
+
* shows a fresh dispatch in the last 10 minutes); a warning otherwise so a
|
|
15
|
+
* human driving the session can override editorial nits without ceremony.
|
|
16
|
+
* CI still enforces the same rules on the resulting PR.
|
|
17
|
+
*
|
|
12
18
|
* Silent and instant when there's nothing to run.
|
|
13
19
|
*
|
|
14
20
|
* Bypasses (env vars):
|
|
@@ -24,6 +30,7 @@ import { execSync, spawn, spawnSync } from 'child_process';
|
|
|
24
30
|
import { dirname, join, resolve } from 'path';
|
|
25
31
|
import { tmpdir } from 'os';
|
|
26
32
|
import { logHookFailure } from './_lib/log.mjs';
|
|
33
|
+
import { isSpecialistAgentActive } from './_lib/specialist-agent.mjs';
|
|
27
34
|
import { emitRoleEvent } from '../roles/hook-emit.mjs';
|
|
28
35
|
|
|
29
36
|
let input = {};
|
|
@@ -72,16 +79,21 @@ if ((isGhPrCreate || isGhPrEdit) && process.env.CONSTRUCT_SKIP_PR_LINT !== '1')
|
|
|
72
79
|
});
|
|
73
80
|
const out = (result.stdout?.toString() || '') + (result.stderr?.toString() || '');
|
|
74
81
|
if (result.status !== 0) {
|
|
75
|
-
|
|
82
|
+
const agentActive = isSpecialistAgentActive();
|
|
83
|
+
const severity = agentActive ? 'fail' : 'warn';
|
|
84
|
+
const banner = agentActive
|
|
85
|
+
? '[pre-push-gate] PR body fails template policy (specialist agent active — blocking):'
|
|
86
|
+
: '[pre-push-gate] PR body fails template policy (no specialist agent active — warning only):';
|
|
87
|
+
process.stderr.write(`${banner}\n${out}\n`);
|
|
76
88
|
process.stderr.write(`Override: CONSTRUCT_SKIP_PR_LINT=1 <command>\n`);
|
|
77
89
|
emitRoleEvent({
|
|
78
|
-
type: 'push_gate.fail',
|
|
79
|
-
summary:
|
|
90
|
+
type: agentActive ? 'push_gate.fail' : 'push_gate.warn',
|
|
91
|
+
summary: `PR body fails template policy (${severity})`,
|
|
80
92
|
hookInput: input,
|
|
81
|
-
context: { command: isGhPrCreate ? 'gh pr create' : 'gh pr edit', detail: out.slice(0, 800) },
|
|
93
|
+
context: { command: isGhPrCreate ? 'gh pr create' : 'gh pr edit', detail: out.slice(0, 800), agentActive },
|
|
82
94
|
});
|
|
83
95
|
if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
|
|
84
|
-
process.exit(2);
|
|
96
|
+
if (agentActive) process.exit(2);
|
|
85
97
|
}
|
|
86
98
|
} finally {
|
|
87
99
|
if (tmpFile) try { rmSync(dirname(tmpFile), { recursive: true, force: true }); } catch {}
|