@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
- process.stderr.write(`[pre-push-gate] PR body fails template policy:\n${out}\n`);
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: 'PR body fails template policy',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {