@geraldmaron/construct 1.0.8 → 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 {}
|
package/package.json
CHANGED
|
@@ -138,8 +138,9 @@ if (validationErrors.length > 0) {
|
|
|
138
138
|
|
|
139
139
|
const DRY_RUN = process.argv.includes("--dry-run");
|
|
140
140
|
const COMPRESS_PERSONAS = process.argv.includes("--compress-personas");
|
|
141
|
-
const
|
|
142
|
-
const
|
|
141
|
+
const projectDir = process.argv.includes("--project") ? process.cwd() : null;
|
|
142
|
+
const lockPath = path.join(projectDir || root, ".cx", "sync.lock");
|
|
143
|
+
const stagingDir = path.join(projectDir || root, ".cx", "sync-staging");
|
|
143
144
|
|
|
144
145
|
/** Acquire an exclusive lockfile. Aborts if already held by a live process. */
|
|
145
146
|
function acquireLock() {
|
|
@@ -192,7 +193,7 @@ function writeFile(file, content) {
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
// Two-phase: write to staging, commit later.
|
|
195
|
-
const rel = path.relative(root, file);
|
|
196
|
+
const rel = path.relative(projectDir || root, file);
|
|
196
197
|
const stagingPath = path.join(stagingDir, rel);
|
|
197
198
|
mkdirp(path.dirname(stagingPath));
|
|
198
199
|
fs.writeFileSync(stagingPath, stamped);
|
|
@@ -1175,7 +1176,6 @@ function syncSkills() {
|
|
|
1175
1176
|
|
|
1176
1177
|
// --- Main ---
|
|
1177
1178
|
|
|
1178
|
-
const projectDir = process.argv.includes("--project") ? process.cwd() : null;
|
|
1179
1179
|
const entries = buildEntries();
|
|
1180
1180
|
|
|
1181
1181
|
if (COMPRESS_PERSONAS) {
|