@geraldmaron/construct 1.0.5 → 1.0.7

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.
Files changed (268) hide show
  1. package/README.md +24 -18
  2. package/agents/contracts.json +617 -2
  3. package/agents/contracts.schema.json +83 -0
  4. package/agents/prompts/cx-accessibility.md +5 -3
  5. package/agents/prompts/cx-ai-engineer.md +9 -7
  6. package/agents/prompts/cx-architect.md +8 -6
  7. package/agents/prompts/cx-business-strategist.md +8 -6
  8. package/agents/prompts/cx-data-analyst.md +9 -7
  9. package/agents/prompts/cx-data-engineer.md +5 -3
  10. package/agents/prompts/cx-debugger.md +8 -6
  11. package/agents/prompts/cx-designer.md +7 -5
  12. package/agents/prompts/cx-devil-advocate.md +6 -4
  13. package/agents/prompts/cx-docs-keeper.md +7 -5
  14. package/agents/prompts/cx-engineer.md +7 -5
  15. package/agents/prompts/cx-evaluator.md +4 -2
  16. package/agents/prompts/cx-explorer.md +10 -8
  17. package/agents/prompts/cx-legal-compliance.md +4 -2
  18. package/agents/prompts/cx-operations.md +7 -5
  19. package/agents/prompts/cx-orchestrator.md +12 -10
  20. package/agents/prompts/cx-platform-engineer.md +7 -5
  21. package/agents/prompts/cx-product-manager.md +6 -4
  22. package/agents/prompts/cx-qa.md +10 -8
  23. package/agents/prompts/cx-rd-lead.md +11 -9
  24. package/agents/prompts/cx-release-manager.md +7 -5
  25. package/agents/prompts/cx-researcher.md +24 -22
  26. package/agents/prompts/cx-reviewer.md +9 -7
  27. package/agents/prompts/cx-security.md +12 -10
  28. package/agents/prompts/cx-sre.md +9 -7
  29. package/agents/prompts/cx-test-automation.md +5 -3
  30. package/agents/prompts/cx-trace-reviewer.md +10 -8
  31. package/agents/prompts/cx-ux-researcher.md +5 -3
  32. package/agents/registry.json +8 -3
  33. package/bin/construct +444 -53
  34. package/commands/build/feature.md +4 -4
  35. package/commands/build/fix.md +8 -8
  36. package/commands/design/access.md +3 -3
  37. package/commands/design/flow.md +3 -3
  38. package/commands/design/ui.md +4 -4
  39. package/commands/measure/experiment.md +5 -5
  40. package/commands/measure/metrics.md +3 -3
  41. package/commands/measure/results.md +4 -4
  42. package/commands/plan/api.md +3 -3
  43. package/commands/plan/challenge.md +3 -3
  44. package/commands/plan/decide.md +3 -3
  45. package/commands/plan/feature.md +7 -7
  46. package/commands/plan/requirements.md +3 -3
  47. package/commands/remember/context.md +5 -5
  48. package/commands/remember/handoff.md +3 -3
  49. package/commands/remember/runbook.md +3 -3
  50. package/commands/review/code.md +8 -8
  51. package/commands/review/quality.md +4 -4
  52. package/commands/review/security.md +3 -3
  53. package/commands/ship/ready.md +3 -3
  54. package/commands/ship/release.md +3 -3
  55. package/commands/ship/status.md +4 -4
  56. package/commands/understand/docs.md +3 -3
  57. package/commands/understand/this.md +4 -4
  58. package/commands/understand/why.md +8 -8
  59. package/commands/work/clean.md +14 -14
  60. package/commands/work/drive.md +10 -10
  61. package/commands/work/optimize-prompts.md +9 -9
  62. package/commands/work/parallel-review.md +8 -8
  63. package/db/schema/006_graph.sql +24 -0
  64. package/examples/provider-plugin/README.md +7 -7
  65. package/examples/seed-observations/README.md +6 -6
  66. package/examples/seed-observations/anti-patterns.md +14 -14
  67. package/examples/seed-observations/decisions.md +4 -4
  68. package/examples/seed-observations/patterns.md +14 -14
  69. package/lib/auto-docs.mjs +13 -8
  70. package/lib/boundary.mjs +126 -0
  71. package/lib/cache-strategy-google.js +26 -31
  72. package/lib/cli-commands.mjs +2 -2
  73. package/lib/comment-lint.mjs +134 -0
  74. package/lib/contracts/validate.mjs +323 -0
  75. package/lib/daemons/contract.mjs +210 -0
  76. package/lib/docs-verify.mjs +59 -6
  77. package/lib/doctor/cli.mjs +16 -1
  78. package/lib/doctor/index.mjs +3 -1
  79. package/lib/doctor/watchers/consistency.mjs +310 -0
  80. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  81. package/lib/document-extract.mjs +211 -1
  82. package/lib/embed/cli.mjs +124 -3
  83. package/lib/embed/daemon.mjs +43 -4
  84. package/lib/embed/docs-lifecycle.mjs +1 -1
  85. package/lib/embed/inbox.mjs +2 -0
  86. package/lib/embed/scheduler.mjs +33 -5
  87. package/lib/evaluator-optimizer.mjs +2 -3
  88. package/lib/flavors/loader.mjs +1 -1
  89. package/lib/hooks/comment-lint.mjs +16 -0
  90. package/lib/hooks/mcp-audit.mjs +2 -1
  91. package/lib/hooks/proactive-activation.mjs +0 -14
  92. package/lib/hooks/rule-verifier.mjs +217 -0
  93. package/lib/hooks/session-optimize.mjs +2 -1
  94. package/lib/init-unified.mjs +55 -65
  95. package/lib/intake/classify.mjs +108 -24
  96. package/lib/intake/daemon.mjs +121 -0
  97. package/lib/intake/filesystem-queue.mjs +6 -1
  98. package/lib/intake/intake-config.mjs +2 -1
  99. package/lib/intake/prepare.mjs +0 -1
  100. package/lib/intake/session-prelude.mjs +7 -1
  101. package/lib/intake/traceability.mjs +90 -0
  102. package/lib/knowledge/graph.mjs +213 -0
  103. package/lib/knowledge/research-store.mjs +2 -0
  104. package/lib/maintenance/cleanup.mjs +315 -0
  105. package/lib/mcp/memory-bridge.mjs +276 -0
  106. package/lib/mcp/server.mjs +189 -1
  107. package/lib/mcp/tools/profile.mjs +270 -0
  108. package/lib/mcp/tools/workflow.mjs +25 -0
  109. package/lib/mcp-catalog.json +12 -8
  110. package/lib/mcp-platform-config.mjs +16 -8
  111. package/lib/migrations/index.mjs +106 -0
  112. package/lib/migrations/v1-baseline.mjs +33 -0
  113. package/lib/observation-store.mjs +9 -4
  114. package/lib/outcomes/record.mjs +2 -0
  115. package/lib/profiles/rebrand.mjs +46 -0
  116. package/lib/project-init-shared.mjs +12 -0
  117. package/lib/provider-capabilities.js +20 -7
  118. package/lib/providers/auth-manager.mjs +58 -17
  119. package/lib/reflect.mjs +49 -12
  120. package/lib/server/index.mjs +22 -28
  121. package/lib/session-store.mjs +6 -4
  122. package/lib/setup.mjs +14 -3
  123. package/lib/telemetry/client.mjs +5 -1
  124. package/lib/version.mjs +51 -0
  125. package/lib/worker/trace.mjs +5 -1
  126. package/package.json +4 -1
  127. package/personas/construct.md +3 -1
  128. package/platforms/claude/CLAUDE.md +6 -6
  129. package/rules/common/agents.md +2 -2
  130. package/rules/common/beads-hygiene.md +11 -11
  131. package/rules/common/code-review.md +1 -1
  132. package/rules/common/coding-style.md +1 -1
  133. package/rules/common/comments.md +8 -8
  134. package/rules/common/commit-approval.md +4 -4
  135. package/rules/common/cx-agent-routing.md +2 -2
  136. package/rules/common/cx-skill-routing.md +2 -2
  137. package/rules/common/development-workflow.md +3 -2
  138. package/rules/common/doc-ownership.md +2 -2
  139. package/rules/common/efficiency.md +3 -3
  140. package/rules/common/framing.md +1 -1
  141. package/rules/common/git-workflow.md +1 -1
  142. package/rules/common/no-fabrication.md +69 -0
  143. package/rules/common/patterns.md +1 -1
  144. package/rules/common/performance.md +1 -1
  145. package/rules/common/release-gates.md +7 -7
  146. package/rules/common/research.md +4 -4
  147. package/rules/common/review-before-change.md +58 -0
  148. package/rules/common/security.md +1 -1
  149. package/rules/common/skill-composition.md +8 -8
  150. package/rules/common/testing.md +1 -1
  151. package/rules/golang/coding-style.md +2 -2
  152. package/rules/golang/hooks.md +1 -1
  153. package/rules/golang/patterns.md +1 -1
  154. package/rules/golang/security.md +1 -1
  155. package/rules/golang/testing.md +1 -1
  156. package/rules/python/coding-style.md +1 -1
  157. package/rules/python/hooks.md +1 -1
  158. package/rules/python/patterns.md +1 -1
  159. package/rules/python/security.md +1 -1
  160. package/rules/python/testing.md +1 -1
  161. package/rules/swift/coding-style.md +3 -3
  162. package/rules/swift/hooks.md +2 -2
  163. package/rules/swift/patterns.md +2 -2
  164. package/rules/swift/security.md +4 -4
  165. package/rules/swift/testing.md +2 -2
  166. package/rules/typescript/coding-style.md +1 -1
  167. package/rules/typescript/hooks.md +1 -1
  168. package/rules/typescript/patterns.md +1 -1
  169. package/rules/typescript/security.md +1 -1
  170. package/rules/typescript/testing.md +1 -1
  171. package/rules/web/coding-style.md +1 -1
  172. package/rules/web/design-quality.md +1 -1
  173. package/rules/web/hooks.md +1 -1
  174. package/rules/web/patterns.md +1 -1
  175. package/rules/web/performance.md +1 -1
  176. package/rules/web/security.md +1 -1
  177. package/rules/web/testing.md +1 -1
  178. package/scripts/sync-agents.mjs +45 -14
  179. package/skills/ai/agent-dev.md +1 -1
  180. package/skills/ai/llm-security.md +1 -1
  181. package/skills/ai/ml-ops.md +6 -6
  182. package/skills/ai/orchestration-workflow.md +1 -1
  183. package/skills/ai/prompt-and-eval.md +1 -1
  184. package/skills/ai/prompt-optimizer.md +13 -13
  185. package/skills/ai/rag-system.md +1 -1
  186. package/skills/architecture/api-design.md +1 -1
  187. package/skills/architecture/caching.md +1 -1
  188. package/skills/architecture/cloud-native.md +1 -1
  189. package/skills/architecture/message-queue.md +1 -1
  190. package/skills/architecture/security-arch.md +1 -1
  191. package/skills/compliance/ai-disclosure.md +1 -1
  192. package/skills/compliance/data-privacy.md +1 -1
  193. package/skills/compliance/license-audit.md +2 -2
  194. package/skills/compliance/regulatory-review.md +1 -1
  195. package/skills/development/cpp.md +1 -1
  196. package/skills/development/go.md +1 -1
  197. package/skills/development/java.md +1 -1
  198. package/skills/development/kotlin.md +9 -9
  199. package/skills/development/mobile-crossplatform.md +13 -13
  200. package/skills/development/python.md +1 -1
  201. package/skills/development/rust.md +1 -1
  202. package/skills/development/shell.md +1 -1
  203. package/skills/development/swift.md +6 -6
  204. package/skills/development/typescript.md +1 -1
  205. package/skills/devops/ci-cd.md +5 -5
  206. package/skills/devops/containerization.md +9 -9
  207. package/skills/devops/cost-optimization.md +1 -1
  208. package/skills/devops/data-engineering.md +2 -2
  209. package/skills/devops/database.md +1 -1
  210. package/skills/devops/dependency-management.md +3 -3
  211. package/skills/devops/devsecops.md +1 -1
  212. package/skills/devops/git-workflow.md +1 -1
  213. package/skills/devops/incident-response.md +18 -18
  214. package/skills/devops/monorepo.md +5 -5
  215. package/skills/devops/observability.md +1 -1
  216. package/skills/devops/performance.md +1 -1
  217. package/skills/devops/testing.md +1 -1
  218. package/skills/docs/adr-workflow.md +2 -2
  219. package/skills/docs/backlog-proposal-workflow.md +1 -1
  220. package/skills/docs/customer-profile-workflow.md +1 -1
  221. package/skills/docs/document-ingest-workflow.md +1 -1
  222. package/skills/docs/evidence-ingest-workflow.md +1 -1
  223. package/skills/docs/init-docs.md +15 -15
  224. package/skills/docs/init-project.md +1 -1
  225. package/skills/docs/prd-workflow.md +3 -3
  226. package/skills/docs/prfaq-workflow.md +1 -1
  227. package/skills/docs/product-intelligence-review.md +1 -1
  228. package/skills/docs/product-intelligence-workflow.md +1 -1
  229. package/skills/docs/product-signal-workflow.md +9 -9
  230. package/skills/docs/research-workflow.md +10 -10
  231. package/skills/docs/runbook-workflow.md +2 -2
  232. package/skills/docs/strategy-workflow.md +3 -3
  233. package/skills/exploration/repo-map.md +11 -11
  234. package/skills/frameworks/django.md +15 -15
  235. package/skills/frameworks/nextjs.md +16 -16
  236. package/skills/frameworks/react.md +12 -12
  237. package/skills/frameworks/spring-boot.md +12 -12
  238. package/skills/frontend-design/accessibility.md +6 -6
  239. package/skills/frontend-design/component-patterns.md +1 -1
  240. package/skills/frontend-design/engineering.md +1 -1
  241. package/skills/frontend-design/state-management.md +1 -1
  242. package/skills/frontend-design/ui-aesthetics.md +1 -1
  243. package/skills/frontend-design/ux-principles.md +1 -1
  244. package/skills/operating/orchestration-reference.md +27 -27
  245. package/skills/quality-gates/review-work.md +3 -3
  246. package/skills/quality-gates/verify-change.md +1 -1
  247. package/skills/quality-gates/verify-module.md +1 -1
  248. package/skills/quality-gates/verify-quality.md +1 -1
  249. package/skills/quality-gates/verify-security.md +1 -1
  250. package/skills/routing.md +14 -14
  251. package/skills/security/blue-team.md +1 -1
  252. package/skills/security/code-audit.md +1 -1
  253. package/skills/security/pentest.md +1 -1
  254. package/skills/security/red-team.md +1 -1
  255. package/skills/security/threat-intel.md +1 -1
  256. package/skills/security/vuln-research.md +1 -1
  257. package/skills/utility/clean-code.md +2 -2
  258. package/templates/docs/changelog-entry.md +1 -1
  259. package/templates/docs/construct_guide.md +18 -18
  260. package/templates/docs/meta-prd.md +16 -16
  261. package/templates/docs/one-pager.md +1 -1
  262. package/templates/docs/prd-business.md +1 -1
  263. package/templates/docs/prd-platform.md +1 -1
  264. package/templates/docs/prd.md +17 -17
  265. package/templates/docs/research-brief.md +8 -8
  266. package/templates/docs/rfc.md +1 -1
  267. package/templates/docs/skill-artifact.md +1 -1
  268. package/templates/docs/strategy.md +1 -1
@@ -26,7 +26,7 @@
26
26
 
27
27
  import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
28
28
  import { homedir } from 'node:os';
29
- import { join } from 'node:path';
29
+ import { dirname, join } from 'node:path';
30
30
 
31
31
  import { loadEmbedConfig } from './config.mjs';
32
32
  import { Scheduler } from './scheduler.mjs';
@@ -109,8 +109,44 @@ async function resolveLLMKey(varName, env) {
109
109
 
110
110
  // ─── Root dir resolution ─────────────────────────────────────────────────────
111
111
 
112
- function resolveRootDir(env = process.env) {
113
- return (env.CX_DATA_DIR ?? '').trim() || homedir();
112
+ // Cap upward search so a daemon launched from deep in the filesystem cannot
113
+ // fan out to thousands of stat() calls in pathological layouts.
114
+
115
+ const PROJECT_WALKUP_MAX = 10;
116
+
117
+ /**
118
+ * Walk up from `startDir` looking for a directory that contains `.cx/context.md`.
119
+ * Returns the project root path, or null if nothing is found within the cap.
120
+ * Exported for unit tests.
121
+ */
122
+ export function findProjectRoot(startDir, { maxLevels = PROJECT_WALKUP_MAX } = {}) {
123
+ if (!startDir || typeof startDir !== 'string') return null;
124
+ let current = startDir;
125
+ for (let i = 0; i <= maxLevels; i++) {
126
+ try {
127
+ if (existsSync(join(current, '.cx', 'context.md'))) return current;
128
+ } catch { /* skip unreadable dir */ }
129
+ const parent = dirname(current);
130
+ if (!parent || parent === current) return null;
131
+ current = parent;
132
+ }
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Resolve the daemon's rootDir.
138
+ *
139
+ * Precedence:
140
+ * 1. `CX_DATA_DIR` env override (operator authority)
141
+ * 2. Project root discovered by walking up from `cwd` looking for `.cx/context.md`
142
+ * 3. `homedir()` fallback
143
+ */
144
+ export function resolveRootDir(env = process.env, cwd = process.cwd()) {
145
+ const override = (env.CX_DATA_DIR ?? '').trim();
146
+ if (override) return override;
147
+ const projectRoot = findProjectRoot(cwd);
148
+ if (projectRoot) return projectRoot;
149
+ return homedir();
114
150
  }
115
151
 
116
152
  function resolveWorkspaceDir(env = process.env) {
@@ -708,7 +744,10 @@ this.#scheduler.register(
708
744
  async () => {
709
745
  const result = await this.#inboxWatcher.poll();
710
746
  if (result.processed.length) {
711
- process.stderr.write(`[embed] Inbox: ingested ${result.processed.length} file(s) from [${result.dirs.join(', ')}]\n`);
747
+ const { getRebrand } = await import('../profiles/rebrand.mjs');
748
+ const { signalNoun } = getRebrand(this.#rootDir);
749
+ const noun = result.processed.length === 1 ? signalNoun : `${signalNoun}s`;
750
+ process.stderr.write(`[embed] Classified ${result.processed.length} ${noun} from [${result.dirs.join(', ')}]\n`);
712
751
  addObservation(this.#rootDir, {
713
752
  role: 'construct',
714
753
  category: 'insight',
@@ -64,7 +64,7 @@ export function detectDocGaps(target, opts = {}) {
64
64
  const gaps = [];
65
65
 
66
66
  if (target.access === 'remote') {
67
- // Remote-only targets: can't inspect filesystem, skip for now
67
+ // Remote-only targets have no local filesystem to inspect; gap detection requires the local docs tree.
68
68
  return gaps;
69
69
  }
70
70
 
@@ -29,6 +29,7 @@ import { addObservation } from '../observation-store.mjs';
29
29
  import { inferKnowledgeTarget, knowledgeDirForCategory } from '../knowledge/layout.mjs';
30
30
  import { docLaneDir, suggestDocsLaneForFile } from '../docs-routing.mjs';
31
31
  import { shouldCreateCx } from '../project-detection.mjs';
32
+ import { ensureCxDir } from '../project-init-shared.mjs';
32
33
  import { prepareIntakeForIngestedFile } from '../intake/prepare.mjs';
33
34
  import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-config.mjs';
34
35
 
@@ -90,6 +91,7 @@ function writeState(rootDir, state) {
90
91
 
91
92
  // Only create .cx directories if this is an initialized project
92
93
  if (shouldCreateCx(rootDir)) {
94
+ ensureCxDir(rootDir);
93
95
  mkdirSync(join(rootDir, '.cx', 'runtime'), { recursive: true });
94
96
  writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
95
97
  }
@@ -4,24 +4,30 @@
4
4
  * Lightweight alternative to a full cron library — runs tasks at fixed
5
5
  * intervals, tracks last-run times, and supports graceful shutdown.
6
6
  * Zero external deps.
7
+ *
8
+ * One-shot startup jobs MUST set `repeat: false`. With `repeat: true`
9
+ * (the default) and `intervalMs: 0`, the timer fires on every event-loop
10
+ * tick — the bug that filled embed-daemon.log to 34 GB before this option
11
+ * was added.
7
12
  */
8
13
 
9
14
  export class Scheduler {
10
- #tasks = new Map(); // id → { fn, intervalMs, lastRun, timer, label }
15
+ #tasks = new Map(); // id → { fn, intervalMs, lastRun, timer, label, repeat }
11
16
  #running = false;
12
17
 
13
18
  /**
14
19
  * Register a task. Returns the task id.
15
20
  * @param {string} label - Human-readable name for logging
16
- * @param {number} intervalMs - How often to run (ms)
21
+ * @param {number} intervalMs - How often to run (ms); ignored when repeat is false
17
22
  * @param {Function} fn - Async function to call; receives { label, lastRun }
18
23
  * @param {object} [opts]
19
24
  * @param {boolean} [opts.runImmediately=false] - Run once before first interval fires
20
25
  * @param {boolean} [opts.unref=false] - Call timer.unref() so this task does not keep the event loop alive (use in tests)
26
+ * @param {boolean} [opts.repeat=true] - When false, the task is one-shot: it runs once (immediately if runImmediately, otherwise after intervalMs) and never re-arms. Required for startup jobs that pass intervalMs: 0.
21
27
  */
22
- register(label, intervalMs, fn, { runImmediately = false, unref = false } = {}) {
28
+ register(label, intervalMs, fn, { runImmediately = false, unref = false, repeat = true } = {}) {
23
29
  const id = `${label}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
24
- const task = { label, intervalMs, fn, lastRun: null, timer: null, runImmediately, unref };
30
+ const task = { label, intervalMs, fn, lastRun: null, timer: null, runImmediately, unref, repeat };
25
31
  this.#tasks.set(id, task);
26
32
 
27
33
  if (this.#running) {
@@ -50,6 +56,7 @@ export class Scheduler {
50
56
  for (const task of this.#tasks.values()) {
51
57
  if (task.timer) {
52
58
  clearInterval(task.timer);
59
+ clearTimeout(task.timer);
53
60
  task.timer = null;
54
61
  }
55
62
  }
@@ -60,7 +67,10 @@ export class Scheduler {
60
67
  */
61
68
  unregister(id) {
62
69
  const task = this.#tasks.get(id);
63
- if (task?.timer) clearInterval(task.timer);
70
+ if (task?.timer) {
71
+ clearInterval(task.timer);
72
+ clearTimeout(task.timer);
73
+ }
64
74
  this.#tasks.delete(id);
65
75
  }
66
76
 
@@ -74,6 +84,7 @@ export class Scheduler {
74
84
  intervalMs: t.intervalMs,
75
85
  lastRun: t.lastRun,
76
86
  active: Boolean(t.timer),
87
+ repeat: t.repeat,
77
88
  }));
78
89
  }
79
90
 
@@ -83,6 +94,23 @@ export class Scheduler {
83
94
  process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
84
95
  });
85
96
  }
97
+
98
+ // One-shot jobs do not re-arm. When runImmediately is true, the task has
99
+ // already executed and no further scheduling is needed. When false, a
100
+ // single setTimeout fires once at intervalMs.
101
+ if (!task.repeat) {
102
+ if (!runImmediately) {
103
+ task.timer = setTimeout(() => {
104
+ task.timer = null;
105
+ this.#runTask(task).catch((err) => {
106
+ process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
107
+ });
108
+ }, task.intervalMs);
109
+ if (task.unref && task.timer.unref) task.timer.unref();
110
+ }
111
+ return;
112
+ }
113
+
86
114
  task.timer = setInterval(() => {
87
115
  this.#runTask(task).catch((err) => {
88
116
  process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
@@ -95,9 +95,8 @@ export async function evaluateDocument({ content, docType, evaluatorFn = null })
95
95
  return await evaluatorFn({ content, docType, rubric });
96
96
  }
97
97
 
98
- // Default: deterministic rubric-based evaluation
99
- // In production, this would call an LLM evaluator
100
- // For now, we use structural checks as a proxy
98
+ // Default path: deterministic structural-rubric evaluation.
99
+ // Consumers wanting LLM-graded evaluation supply `evaluatorFn` (handled above).
101
100
 
102
101
  const scores = [];
103
102
  const feedback = [];
@@ -130,7 +130,7 @@ export function validateFlavor(filePath) {
130
130
  errors.push(`${filePath}: applies_to must be a non-empty array`);
131
131
  }
132
132
  if (!Array.isArray(fm.profiles) || fm.profiles.length === 0) {
133
- errors.push(`${filePath}: profiles must be a non-empty array (run scripts/migrate-flavors.mjs)`);
133
+ errors.push(`${filePath}: profiles must be a non-empty array; add a frontmatter line like \`profiles: [rnd]\``);
134
134
  }
135
135
  return errors;
136
136
  }
@@ -40,8 +40,24 @@ const rootDir = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
40
40
  const result = lintFile(filePath, { rootDir });
41
41
  if (!result.errors.length && !result.warnings.length) process.exit(0);
42
42
 
43
+ // Artifact-prose violations are advisory at write-time (no-fabrication policy
44
+ // enforces them at CI/release-gate time via CONSTRUCT_ARTIFACT_LINT_MODE=block).
45
+ // Surface them on stderr but do not block the edit.
46
+ const artifactOnly = (
47
+ result.errors.length === 0 &&
48
+ result.warnings.every((w) => w.kind === 'artifact')
49
+ );
50
+
43
51
  const { output } = formatResults([result]);
44
52
  process.stderr.write(output);
53
+
54
+ if (artifactOnly) {
55
+ process.stderr.write(
56
+ '\nArtifact-lint advisory (no-fabrication). The edit is not blocked, but these will fail the release gate. Fix before push.\n',
57
+ );
58
+ process.exit(0);
59
+ }
60
+
45
61
  process.stderr.write(
46
62
  '\nComment policy blocked this edit. Remove the violations or set CONSTRUCT_SKIP_COMMENT_LINT=1.\n',
47
63
  );
@@ -10,6 +10,7 @@
10
10
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
11
11
  import { join } from 'path';
12
12
  import { shouldCreateCx } from '../project-detection.mjs';
13
+ import { ensureCxDir } from '../project-init-shared.mjs';
13
14
 
14
15
  let input = {};
15
16
  try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
@@ -47,7 +48,7 @@ if (audit[mcpServer].length > 200) audit[mcpServer] = audit[mcpServer].slice(-20
47
48
 
48
49
  try {
49
50
  if (shouldCreateCxDir) {
50
- mkdirSync(join(cwd, '.cx'), { recursive: true });
51
+ ensureCxDir(cwd);
51
52
  writeFileSync(auditPath, JSON.stringify(audit, null, 2));
52
53
  }
53
54
  } catch { /* best effort */ }
@@ -239,20 +239,6 @@ export function onScheduleCheck() {
239
239
  return events;
240
240
  }
241
241
 
242
- /**
243
- * Get all pending activations for a specialist.
244
- */
245
- export function getPendingActivations(specialist) {
246
- // In a full implementation, this would query a pending queue
247
- // For now, return recent activation history
248
- const state = getActivationState(specialist);
249
- return {
250
- pending: 0,
251
- recent: state.activations.length,
252
- total: state.totalActivations,
253
- };
254
- }
255
-
256
242
  /**
257
243
  * Clear activation state (for testing or manual reset).
258
244
  */
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/rule-verifier.mjs — Stop hook that audits the session for
4
+ * compliance with the construct rules the agent claimed to follow.
5
+ *
6
+ * Checks:
7
+ * - Each commit / push was preceded by an approval signal scoped to that
8
+ * branch + change.
9
+ * - Edits to protected files (CLAUDE.md table) had user confirmation.
10
+ * - Beads claims exist for any work that touched durable state.
11
+ *
12
+ * Approval detection is intent-based, not keyword-based. The verifier reads
13
+ * the conversational window before each consequential action and classifies
14
+ * the user's stance: APPROVED, REFUSED, INCONCLUSIVE. Direct affirmation,
15
+ * accepting a proposed plan that named the action, or "go ahead" all count
16
+ * as approval — no specific word is required.
17
+ *
18
+ * Output: { ok: 'pass' | 'fail' | 'inconclusive', findings[] } written to
19
+ * .cx/audit.jsonl. Inconclusive results surface as escalation questions to
20
+ * the user rather than silent passes or false-positive blocks.
21
+ *
22
+ * @p95ms 200
23
+ * @maxBlockingScope Stop
24
+ */
25
+
26
+ import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
27
+ import { dirname, join } from 'node:path';
28
+ import { logHookFailure } from './_lib/log.mjs';
29
+
30
+ const AUDIT_REL = '.cx/audit.jsonl';
31
+
32
+ function safeReadJsonLines(transcriptPath) {
33
+ try {
34
+ const raw = readFileSync(transcriptPath, 'utf8');
35
+ return raw.split('\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
36
+ } catch { return []; }
37
+ }
38
+
39
+ function lastN(entries, count) {
40
+ return entries.slice(Math.max(0, entries.length - count));
41
+ }
42
+
43
+ /**
44
+ * Identify consequential actions in the transcript tail. Returns a list of
45
+ * { kind, branch, target, atIndex } items. Each one needs an approval signal
46
+ * within the window of conversation immediately preceding it.
47
+ *
48
+ * Detected actions:
49
+ * - 'commit' — git commit invocation
50
+ * - 'push' — git push invocation
51
+ * - 'protected' — edit to a CLAUDE.md-protected file
52
+ * - 'durable-state' — bd close / claim against durable state
53
+ */
54
+ export function findConsequentialActions(entries) {
55
+ const actions = [];
56
+ for (let i = 0; i < entries.length; i++) {
57
+ const e = entries[i];
58
+ const cmd = extractBashCommand(e);
59
+ if (cmd) {
60
+ if (/^git\s+commit\b/.test(cmd) && !/--dry-run/.test(cmd)) {
61
+ actions.push({ kind: 'commit', target: cmd.slice(0, 200), atIndex: i });
62
+ } else if (/^git\s+push\b/.test(cmd) && !/--dry-run/.test(cmd)) {
63
+ actions.push({ kind: 'push', target: cmd.slice(0, 200), atIndex: i });
64
+ } else if (/^gh\s+pr\s+(create|merge)\b/.test(cmd)) {
65
+ actions.push({ kind: 'pr-mutation', target: cmd.slice(0, 200), atIndex: i });
66
+ }
67
+ }
68
+ const editPath = extractEditPath(e);
69
+ if (editPath && /CLAUDE\.md|install\.sh|claude\/settings\.template\.json|agents\/registry\.json/.test(editPath)) {
70
+ actions.push({ kind: 'protected', target: editPath, atIndex: i });
71
+ }
72
+ }
73
+ return actions;
74
+ }
75
+
76
+ function extractBashCommand(entry) {
77
+ const toolUse = entry?.message?.content?.find?.((c) => c?.type === 'tool_use' && c?.name === 'Bash');
78
+ return toolUse?.input?.command || null;
79
+ }
80
+
81
+ function extractEditPath(entry) {
82
+ const toolUse = entry?.message?.content?.find?.((c) => c?.type === 'tool_use' && (c?.name === 'Edit' || c?.name === 'Write'));
83
+ return toolUse?.input?.file_path || null;
84
+ }
85
+
86
+ /**
87
+ * Classify a conversational window for approval intent. This is intentionally
88
+ * conservative — it does not require any keyword. If the window contains a
89
+ * user turn that directly addresses the action being taken (mentioning the
90
+ * branch, the file, or accepting a proposed plan), that counts as approval.
91
+ *
92
+ * The default classifier is deterministic: it scans for plausible approval
93
+ * tokens AND for the action target appearing earlier in the conversation.
94
+ * A pluggable classifier can replace this with an LLM-graded inference at
95
+ * runtime — register via setApprovalClassifier(fn).
96
+ *
97
+ * Returns: 'APPROVED' | 'REFUSED' | 'INCONCLUSIVE'.
98
+ */
99
+ let _approvalClassifier = defaultApprovalClassifier;
100
+
101
+ export function setApprovalClassifier(fn) {
102
+ _approvalClassifier = typeof fn === 'function' ? fn : defaultApprovalClassifier;
103
+ }
104
+
105
+ export function classifyApproval(window, action) {
106
+ return _approvalClassifier(window, action);
107
+ }
108
+
109
+ function defaultApprovalClassifier(window, action) {
110
+ if (!Array.isArray(window) || window.length === 0) return 'INCONCLUSIVE';
111
+
112
+ let assistantProposedAction = false;
113
+ let userResponded = false;
114
+ let userRefused = false;
115
+ let userAffirmed = false;
116
+
117
+ for (const entry of window) {
118
+ const role = entry?.message?.role || entry?.role;
119
+ const text = extractText(entry).toLowerCase();
120
+ if (!text) continue;
121
+
122
+ if (role === 'assistant') {
123
+ if (actionMentioned(text, action)) assistantProposedAction = true;
124
+ }
125
+ if (role === 'user') {
126
+ userResponded = true;
127
+ if (/\b(no|stop|don'?t|cancel|wait|abort|hold off|never mind)\b/.test(text)) userRefused = true;
128
+ if (/\b(yes|yep|yeah|go ahead|do it|proceed|ship it|approve|confirmed|sounds good|that works|lgtm|sgtm|ok|sure|please do|merge it|continue)\b/.test(text)) userAffirmed = true;
129
+ // Even without explicit affirmation words, accepting a named plan is approval.
130
+ if (assistantProposedAction && !userRefused && text.length > 0 && !/\?$/.test(text.trim())) {
131
+ userAffirmed = userAffirmed || /\b(thanks|great|perfect|sounds|works|good)\b/.test(text);
132
+ }
133
+ }
134
+ }
135
+
136
+ if (userRefused) return 'REFUSED';
137
+ if (userAffirmed && assistantProposedAction) return 'APPROVED';
138
+ if (userAffirmed) return 'APPROVED';
139
+ if (!userResponded) return 'INCONCLUSIVE';
140
+ return 'INCONCLUSIVE';
141
+ }
142
+
143
+ function extractText(entry) {
144
+ const msg = entry?.message;
145
+ if (!msg) return '';
146
+ if (typeof msg.content === 'string') return msg.content;
147
+ if (Array.isArray(msg.content)) {
148
+ return msg.content.map((c) => (typeof c === 'string' ? c : c?.text || '')).join(' ');
149
+ }
150
+ return '';
151
+ }
152
+
153
+ function actionMentioned(text, action) {
154
+ if (!text) return false;
155
+ if (action.kind === 'commit' || action.kind === 'push' || action.kind === 'pr-mutation') {
156
+ return /\b(commit|push|merge|ship|pr|pull request)\b/.test(text);
157
+ }
158
+ if (action.kind === 'protected') {
159
+ const fname = action.target.split('/').pop().toLowerCase();
160
+ return text.includes(fname.toLowerCase()) || text.includes(action.target.toLowerCase());
161
+ }
162
+ return false;
163
+ }
164
+
165
+ /**
166
+ * Verify a transcript against the rule set. Returns a structured audit
167
+ * result. Side-effect free; the runtime entry point below writes the result
168
+ * to .cx/audit.jsonl.
169
+ */
170
+ export function verifyTranscript(entries, { windowSize = 20 } = {}) {
171
+ const findings = [];
172
+ const actions = findConsequentialActions(entries);
173
+
174
+ for (const action of actions) {
175
+ const window = lastN(entries.slice(0, action.atIndex), windowSize);
176
+ const verdict = classifyApproval(window, action);
177
+ findings.push({
178
+ action: action.kind,
179
+ target: action.target,
180
+ verdict,
181
+ atIndex: action.atIndex,
182
+ });
183
+ }
184
+
185
+ const failed = findings.some((f) => f.verdict === 'REFUSED');
186
+ const inconclusive = findings.some((f) => f.verdict === 'INCONCLUSIVE');
187
+ let ok;
188
+ if (failed) ok = 'fail';
189
+ else if (inconclusive) ok = 'inconclusive';
190
+ else ok = 'pass';
191
+
192
+ return { ok, findings, actionCount: actions.length };
193
+ }
194
+
195
+ function writeAudit(cwd, payload) {
196
+ const path = join(cwd, AUDIT_REL);
197
+ try { mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ }
198
+ try { appendFileSync(path, JSON.stringify(payload) + '\n'); }
199
+ catch (err) { logHookFailure({ hook: 'rule-verifier', err, phase: 'append-audit' }); }
200
+ }
201
+
202
+ if (import.meta.url === `file://${process.argv[1]}`) {
203
+ let input = {};
204
+ try { input = JSON.parse(readFileSync(0, 'utf8')); }
205
+ catch (err) { logHookFailure({ hook: 'rule-verifier', err, phase: 'parse-stdin' }); process.exit(0); }
206
+
207
+ const cwd = input?.cwd || process.cwd();
208
+ const transcriptPath = input?.transcript_path;
209
+ if (!transcriptPath || !existsSync(transcriptPath)) process.exit(0);
210
+
211
+ const entries = safeReadJsonLines(transcriptPath);
212
+ const result = verifyTranscript(entries);
213
+ writeAudit(cwd, { ts: new Date().toISOString(), hook: 'rule-verifier', ...result });
214
+
215
+ if (result.ok === 'fail' && process.env.CONSTRUCT_RULE_VERIFIER === 'block') process.exit(2);
216
+ process.exit(0);
217
+ }
@@ -237,7 +237,8 @@ function main() {
237
237
  console.log(` • ${agent.name}: avgScore=${agent.avgScore}, invocations=${agent.invocations}`);
238
238
  });
239
239
 
240
- // Run optimization for each agent (serial for now)
240
+ // Sequential optimization keeps prompt-tuning deterministic and avoids
241
+ // colliding writes to per-agent prompt files.
241
242
  let optimizedCount = 0;
242
243
  let failedCount = 0;
243
244
 
@@ -615,87 +615,76 @@ function discoverProjectDirs(targetPath) {
615
615
  return dirs.sort();
616
616
  }
617
617
 
618
- const INTAKE_DIR_PRESETS = [
619
- { value: 'src', label: 'src/', reason: 'Source code most projects put code here' },
620
- { value: 'lib', label: 'lib/', reason: 'Library code' },
621
- { value: 'packages', label: 'packages/', reason: 'Monorepo packages' },
622
- { value: 'services', label: 'services/', reason: 'Microservices or backend services' },
623
- { value: 'apps', label: 'apps/', reason: 'Monorepo apps' },
624
- { value: 'docs', label: 'docs/', reason: 'Documentation source' },
625
- { value: 'tests', label: 'tests/', reason: 'Test files' },
626
- { value: 'spec', label: 'spec/', reason: 'Specifications' },
627
- { value: 'infra', label: 'infra/', reason: 'Infrastructure code (Terraform, etc.)' },
628
- { value: 'config', label: 'config/', reason: 'Configuration files' },
629
- { value: 'scripts', label: 'scripts/', reason: 'Build/ops scripts' },
630
- { value: 'tools', label: 'tools/', reason: 'Internal tooling' },
631
- ];
618
+ // Extra inbox-style directories the user can opt into. These are NOT
619
+ // source-code, doc, or test directories: they're places where unsorted
620
+ // signals tend to land (downloads, scratch notes, exported research). The
621
+ // built-in `.cx/inbox/` and `docs/intake/` are always watched and not
622
+ // listed here.
623
+ //
624
+ // Historically this list included `src/`, `docs/`, `tests/`, etc., and
625
+ // `init --yes` auto-enabled every one that existed in the project. That
626
+ // turned every code change, doc edit, and test file into a synthetic
627
+ // "intake signal" and polluted the queue with hundreds of false positives.
628
+ // Watching project artifact directories is opt-in only; an empty list is
629
+ // the correct default.
630
+ const INTAKE_DIR_PRESETS = [];
632
631
 
633
632
  async function askIntakeCollection(targetPath, skipInteractive) {
633
+ // Non-interactive default: empty parentDirs. The built-in zones
634
+ // (.cx/inbox/ and docs/intake/ when present) handle every well-defined
635
+ // signal path. Users who want to watch additional directories opt in
636
+ // explicitly via `construct intake config set --add-dir=<path>`.
634
637
  if (skipInteractive) {
635
- // Non-interactive: auto-detect and suggest based on what exists
636
- const existingDirs = discoverProjectDirs(targetPath);
637
- const selected = INTAKE_DIR_PRESETS
638
- .filter(preset => existingDirs.includes(preset.value))
639
- .map(p => p.value);
640
-
641
- if (selected.length === 0) return null;
642
-
643
- console.log('');
644
- console.log('═══════════════════════════════════════════════════════════');
645
- console.log(' INTAKE COLLECTION');
646
- console.log('═══════════════════════════════════════════════════════════');
647
- console.log('');
648
- console.log(`Auto-detected ${selected.length} directory(ies) to watch for context:`);
649
- for (const d of selected) console.log(` • ${d}/`);
650
- console.log('');
651
- console.log('The inbox watcher will scan these directories for new files');
652
- console.log('and route them through R&D triage automatically.');
653
- console.log('');
654
-
655
- return { parentDirs: selected, maxDepth: 4 };
638
+ return null;
656
639
  }
657
-
658
- // Interactive mode
640
+
641
+ // Interactive mode: no presets to surface — the user must name the
642
+ // directory they want watched. Skipped entirely when the preset list
643
+ // is empty (the current default after the auto-include regression).
644
+ if (INTAKE_DIR_PRESETS.length === 0) {
645
+ return null;
646
+ }
647
+
659
648
  const existingDirs = discoverProjectDirs(targetPath);
660
649
  const presetOptions = INTAKE_DIR_PRESETS.map(p => ({
661
650
  label: p.label,
662
651
  value: p.value,
663
- checked: existingDirs.includes(p.value),
652
+ checked: false,
664
653
  description: p.reason,
665
654
  }));
666
-
655
+
667
656
  console.log('');
668
657
  console.log('═══════════════════════════════════════════════════════════');
669
658
  console.log(' INTAKE COLLECTION');
670
659
  console.log('═══════════════════════════════════════════════════════════');
671
660
  console.log('');
672
- console.log('Intake watches directories for new files and routes them');
673
- console.log('through R&D triage. Select which directories to watch.');
674
- console.log('(directories already found in your project are pre-checked)');
661
+ console.log('Intake watches `.cx/inbox/` and `docs/intake/` by default.');
662
+ console.log('Add extra directories ONLY if signals (customer notes, exports,');
663
+ console.log('PDFs, etc.) regularly land there. Do not select code or finished');
664
+ console.log('artifact directories like src/, docs/, or tests/.');
675
665
  console.log('');
676
-
666
+
667
+ void existingDirs;
677
668
  const selected = await multiSelect({
678
- title: 'Directories to Watch',
679
- instructions: 'Press Enter to confirm your selection',
669
+ title: 'Additional Inbox Directories',
670
+ instructions: 'Press Enter to confirm your selection (or pick none)',
680
671
  options: presetOptions,
681
672
  });
682
-
673
+
683
674
  if (selected.length === 0) {
684
675
  console.log('');
685
- console.log('No directories selected. The inbox watcher will only scan');
686
- console.log('.cx/inbox/ and docs/intake/ (built-in zones).');
687
- console.log('');
688
- console.log('You can always add more later with:');
689
- console.log(' construct intake config set --add-dir=src');
676
+ console.log('No extra directories selected. The inbox watcher will scan');
677
+ console.log('.cx/inbox/ and docs/intake/ only. Add more later with:');
678
+ console.log(' construct intake config set --add-dir=<path>');
690
679
  console.log('');
691
680
  return null;
692
681
  }
693
-
682
+
694
683
  console.log('');
695
- console.log(`Watching ${selected.length} directory(ies):`);
684
+ console.log(`Watching ${selected.length} extra directory(ies):`);
696
685
  for (const d of selected) console.log(` • ${d}/`);
697
686
  console.log('');
698
-
687
+
699
688
  return { parentDirs: selected, maxDepth: 4 };
700
689
  }
701
690
 
@@ -949,18 +938,19 @@ async function main() {
949
938
  console.log('');
950
939
  }
951
940
 
952
- // Intake collection — ask which directories to watch for context
941
+ // Intake collection — always write intake-config.json with safe defaults
942
+ // so the user can inspect what's watched and edit the file directly. Empty
943
+ // parentDirs is the correct default: the inbox watcher always scans
944
+ // .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
953
945
  console.log('[TRACE init:intake-ask]');
954
-
955
- const intakeConfig = await askIntakeCollection(target, skipInteractive);
956
- if (intakeConfig) {
957
- const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
958
- try {
959
- saveIntakeConfig(target, intakeConfig);
960
- created.push('.cx/intake-config.json');
961
- } catch (err) {
962
- console.warn(`⚠️ Could not write intake config: ${err.message}`);
963
- }
946
+
947
+ const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
948
+ const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
949
+ try {
950
+ saveIntakeConfig(target, intakeConfig);
951
+ created.push('.cx/intake-config.json');
952
+ } catch (err) {
953
+ console.warn(`⚠️ Could not write intake config: ${err.message}`);
964
954
  }
965
955
 
966
956
  // Ask about documentation system