@geraldmaron/construct 1.0.12 → 1.0.15

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 (337) hide show
  1. package/README.md +14 -2
  2. package/bin/construct +262 -14
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/commands/build/feature.md +0 -6
  5. package/commands/build/fix.md +0 -6
  6. package/commands/design/access.md +0 -6
  7. package/commands/design/flow.md +0 -6
  8. package/commands/design/ui.md +0 -6
  9. package/commands/measure/experiment.md +0 -6
  10. package/commands/measure/metrics.md +0 -6
  11. package/commands/measure/results.md +0 -6
  12. package/commands/plan/api.md +0 -6
  13. package/commands/plan/challenge.md +0 -6
  14. package/commands/plan/decide.md +0 -6
  15. package/commands/plan/feature.md +0 -7
  16. package/commands/plan/requirements.md +0 -6
  17. package/commands/remember/context.md +0 -6
  18. package/commands/remember/handoff.md +0 -6
  19. package/commands/remember/runbook.md +0 -6
  20. package/commands/review/code.md +0 -6
  21. package/commands/review/quality.md +0 -6
  22. package/commands/review/security.md +0 -6
  23. package/commands/ship/ready.md +0 -6
  24. package/commands/ship/release.md +0 -6
  25. package/commands/ship/status.md +0 -6
  26. package/commands/understand/docs.md +0 -6
  27. package/commands/understand/research.md +0 -6
  28. package/commands/understand/this.md +0 -6
  29. package/commands/understand/why.md +0 -6
  30. package/commands/work/clean.md +0 -6
  31. package/commands/work/drive.md +0 -6
  32. package/commands/work/optimize-prompts.md +0 -6
  33. package/commands/work/parallel-review.md +0 -6
  34. package/lib/auto-docs.mjs +11 -7
  35. package/lib/beads-client.mjs +19 -1
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +54 -10
  39. package/lib/contracts/violation-log.mjs +154 -0
  40. package/lib/dashboard-static.mjs +7 -4
  41. package/lib/doc-stamp.mjs +16 -0
  42. package/lib/docs-verify.mjs +1 -8
  43. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  44. package/lib/document-extract/docling-client.mjs +114 -0
  45. package/lib/document-extract/docling-sidecar.py +122 -0
  46. package/lib/document-extract/whisper-client.mjs +79 -0
  47. package/lib/document-extract.mjs +73 -8
  48. package/lib/document-ingest.mjs +26 -4
  49. package/lib/embed/daemon.mjs +26 -1
  50. package/lib/embed/docs-lifecycle.mjs +19 -0
  51. package/lib/embed/inbox.mjs +85 -2
  52. package/lib/embed/recommendation-store.mjs +29 -0
  53. package/lib/flavors/loader.mjs +1 -1
  54. package/lib/gates-audit.mjs +18 -12
  55. package/lib/handoffs/contract.mjs +2 -2
  56. package/lib/hooks/_lib/input.mjs +52 -0
  57. package/lib/hooks/adaptive-lint.mjs +4 -0
  58. package/lib/hooks/agent-tracker.mjs +59 -15
  59. package/lib/hooks/audit-reads.mjs +83 -42
  60. package/lib/hooks/audit-trail.mjs +28 -18
  61. package/lib/hooks/bash-output-logger.mjs +8 -2
  62. package/lib/hooks/block-no-verify.mjs +4 -0
  63. package/lib/hooks/ci-status-check.mjs +4 -0
  64. package/lib/hooks/comment-lint.mjs +6 -4
  65. package/lib/hooks/config-protection.mjs +4 -0
  66. package/lib/hooks/context-watch.mjs +4 -0
  67. package/lib/hooks/context-window-recovery.mjs +4 -0
  68. package/lib/hooks/dep-audit.mjs +12 -5
  69. package/lib/hooks/doc-coupling-check.mjs +5 -1
  70. package/lib/hooks/edit-accumulator.mjs +25 -10
  71. package/lib/hooks/edit-error-recovery.mjs +4 -0
  72. package/lib/hooks/edit-guard.mjs +4 -0
  73. package/lib/hooks/guard-bash.mjs +4 -0
  74. package/lib/hooks/mcp-audit.mjs +4 -0
  75. package/lib/hooks/mcp-health-check.mjs +4 -0
  76. package/lib/hooks/model-fallback.mjs +7 -11
  77. package/lib/hooks/policy-engine.mjs +4 -0
  78. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  79. package/lib/hooks/post-merge-tracking.mjs +82 -0
  80. package/lib/hooks/pre-compact.mjs +4 -0
  81. package/lib/hooks/pre-push-gate.mjs +84 -231
  82. package/lib/hooks/proactive-activation.mjs +5 -2
  83. package/lib/hooks/readme-age-check.mjs +4 -0
  84. package/lib/hooks/registry-sync.mjs +35 -20
  85. package/lib/hooks/rule-verifier.mjs +3 -0
  86. package/lib/hooks/scan-secrets.mjs +4 -0
  87. package/lib/hooks/session-optimize.mjs +4 -0
  88. package/lib/hooks/session-reflect.mjs +4 -0
  89. package/lib/hooks/session-start.mjs +49 -2
  90. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  91. package/lib/hooks/stop-notify.mjs +13 -2
  92. package/lib/hooks/stop-typecheck.mjs +4 -0
  93. package/lib/hooks/test-watch.mjs +8 -4
  94. package/lib/ingest/chunker.mjs +94 -0
  95. package/lib/ingest/pipeline.mjs +53 -0
  96. package/lib/ingest/store.mjs +82 -0
  97. package/lib/init-unified.mjs +90 -23
  98. package/lib/intake/attribution.mjs +77 -0
  99. package/lib/intake/intake-config.mjs +3 -0
  100. package/lib/intake/manifest.mjs +107 -0
  101. package/lib/intake/poll-lock.mjs +136 -0
  102. package/lib/intake/prepare.mjs +42 -4
  103. package/lib/knowledge/search.mjs +12 -0
  104. package/lib/logging/rotate.mjs +394 -0
  105. package/lib/mcp/server.mjs +142 -1
  106. package/lib/mcp/tools/project.mjs +2 -2
  107. package/lib/mcp/tools/skills.mjs +6 -3
  108. package/lib/mcp/tools/telemetry.mjs +1 -1
  109. package/lib/mcp/tools/workflow.mjs +6 -2
  110. package/lib/observation-store.mjs +14 -5
  111. package/lib/ollama-manager.mjs +32 -28
  112. package/lib/opencode-config.mjs +10 -3
  113. package/lib/orchestration/routing-tables.mjs +176 -0
  114. package/lib/orchestration-policy.mjs +18 -106
  115. package/lib/parity.mjs +48 -7
  116. package/lib/profiles/lifecycle.mjs +19 -1
  117. package/lib/project-init-shared.mjs +11 -5
  118. package/lib/project-root.mjs +104 -0
  119. package/lib/prompt-composer.js +11 -1
  120. package/lib/roles/catalog.mjs +1 -1
  121. package/lib/roles/event-bus.mjs +29 -9
  122. package/lib/roles/router.mjs +8 -7
  123. package/lib/runtime/uv-bootstrap.mjs +129 -0
  124. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  125. package/lib/server/index.mjs +53 -10
  126. package/lib/server/static/index.html +1 -15
  127. package/lib/specialists/postconditions.mjs +1 -1
  128. package/lib/status.mjs +1 -1
  129. package/lib/storage/backup.mjs +2 -2
  130. package/lib/sync/skill-frontmatter.mjs +113 -21
  131. package/lib/telemetry/intent-verifications.mjs +16 -3
  132. package/lib/telemetry/skill-calls.mjs +12 -3
  133. package/lib/tracking-surfaces.mjs +377 -0
  134. package/lib/validators/skills.mjs +86 -54
  135. package/lib/worker/trace.mjs +19 -2
  136. package/package.json +10 -6
  137. package/personas/construct.md +4 -7
  138. package/platforms/claude/settings.template.json +29 -28
  139. package/rules/common/beads-hygiene.md +3 -9
  140. package/rules/common/code-review.md +3 -6
  141. package/rules/common/coding-style.md +3 -6
  142. package/rules/common/comments.md +3 -7
  143. package/rules/common/commit-approval.md +3 -6
  144. package/rules/common/cx-agent-routing.md +3 -7
  145. package/rules/common/cx-skill-routing.md +3 -3
  146. package/rules/common/doc-ownership.md +3 -8
  147. package/rules/common/efficiency.md +3 -8
  148. package/rules/common/framing.md +3 -8
  149. package/rules/common/git-workflow.md +3 -5
  150. package/rules/common/no-fabrication.md +4 -12
  151. package/rules/common/patterns.md +3 -5
  152. package/rules/common/release-gates.md +3 -8
  153. package/rules/common/research.md +3 -8
  154. package/rules/common/review-before-change.md +3 -9
  155. package/rules/common/security.md +3 -6
  156. package/rules/common/skill-composition.md +3 -7
  157. package/rules/common/testing.md +3 -6
  158. package/rules/golang/coding-style.md +1 -5
  159. package/rules/golang/hooks.md +1 -5
  160. package/rules/golang/patterns.md +1 -5
  161. package/rules/golang/security.md +1 -5
  162. package/rules/golang/testing.md +1 -5
  163. package/rules/python/coding-style.md +1 -5
  164. package/rules/python/hooks.md +1 -5
  165. package/rules/python/patterns.md +1 -5
  166. package/rules/python/security.md +1 -5
  167. package/rules/python/testing.md +1 -5
  168. package/rules/swift/coding-style.md +1 -5
  169. package/rules/swift/hooks.md +1 -5
  170. package/rules/swift/patterns.md +1 -5
  171. package/rules/swift/security.md +1 -5
  172. package/rules/swift/testing.md +1 -5
  173. package/rules/typescript/coding-style.md +1 -5
  174. package/rules/typescript/hooks.md +1 -5
  175. package/rules/typescript/patterns.md +1 -5
  176. package/rules/typescript/security.md +1 -5
  177. package/rules/typescript/testing.md +1 -5
  178. package/rules/web/coding-style.md +3 -5
  179. package/rules/web/design-quality.md +3 -5
  180. package/rules/web/hooks.md +3 -5
  181. package/rules/web/patterns.md +3 -5
  182. package/rules/web/performance.md +3 -5
  183. package/rules/web/security.md +3 -5
  184. package/rules/web/testing.md +3 -5
  185. package/scripts/sync-specialists.mjs +269 -75
  186. package/skills/ai/agent-dev.md +4 -5
  187. package/skills/ai/llm-security.md +4 -5
  188. package/skills/ai/ml-ops.md +4 -5
  189. package/skills/ai/orchestration-workflow.md +4 -5
  190. package/skills/ai/prompt-and-eval.md +4 -5
  191. package/skills/ai/prompt-optimizer.md +4 -6
  192. package/skills/ai/rag-system.md +4 -5
  193. package/skills/architecture/api-design.md +4 -5
  194. package/skills/architecture/caching.md +4 -5
  195. package/skills/architecture/cloud-native.md +4 -5
  196. package/skills/architecture/message-queue.md +4 -5
  197. package/skills/architecture/security-arch.md +4 -5
  198. package/skills/compliance/ai-disclosure.md +4 -5
  199. package/skills/compliance/data-privacy.md +4 -5
  200. package/skills/compliance/license-audit.md +4 -5
  201. package/skills/compliance/regulatory-review.md +4 -5
  202. package/skills/development/cpp.md +4 -5
  203. package/skills/development/go.md +4 -5
  204. package/skills/development/java.md +4 -5
  205. package/skills/development/kotlin.md +4 -5
  206. package/skills/development/mobile-crossplatform.md +4 -5
  207. package/skills/development/python.md +4 -5
  208. package/skills/development/rust.md +4 -5
  209. package/skills/development/shell.md +4 -5
  210. package/skills/development/swift.md +4 -5
  211. package/skills/development/typescript.md +4 -5
  212. package/skills/devops/ci-cd.md +4 -5
  213. package/skills/devops/containerization.md +4 -5
  214. package/skills/devops/cost-optimization.md +4 -5
  215. package/skills/devops/data-engineering.md +4 -5
  216. package/skills/devops/database.md +4 -5
  217. package/skills/devops/dependency-management.md +4 -5
  218. package/skills/devops/devsecops.md +4 -5
  219. package/skills/devops/git-workflow.md +4 -5
  220. package/skills/devops/incident-response.md +4 -5
  221. package/skills/devops/monorepo.md +4 -5
  222. package/skills/devops/observability.md +4 -5
  223. package/skills/devops/performance.md +4 -5
  224. package/skills/devops/testing.md +4 -5
  225. package/skills/docs/adr-workflow.md +4 -7
  226. package/skills/docs/backlog-proposal-workflow.md +4 -3
  227. package/skills/docs/customer-profile-workflow.md +4 -3
  228. package/skills/docs/document-ingest-workflow.md +4 -3
  229. package/skills/docs/evidence-ingest-workflow.md +4 -3
  230. package/skills/docs/init-docs.md +4 -5
  231. package/skills/docs/init-project.md +4 -5
  232. package/skills/docs/prd-workflow.md +4 -7
  233. package/skills/docs/prfaq-workflow.md +4 -3
  234. package/skills/docs/product-intelligence-review.md +4 -3
  235. package/skills/docs/product-intelligence-workflow.md +4 -6
  236. package/skills/docs/product-signal-workflow.md +4 -5
  237. package/skills/docs/research-workflow.md +4 -5
  238. package/skills/docs/runbook-workflow.md +4 -5
  239. package/skills/docs/strategy-workflow.md +4 -5
  240. package/skills/exploration/dependency-graph-reading.md +4 -7
  241. package/skills/exploration/repo-map.md +4 -5
  242. package/skills/exploration/tracer-bullet-method.md +4 -7
  243. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  244. package/skills/frameworks/django.md +4 -5
  245. package/skills/frameworks/nextjs.md +4 -5
  246. package/skills/frameworks/react.md +4 -5
  247. package/skills/frameworks/spring-boot.md +4 -5
  248. package/skills/frontend-design/accessibility.md +4 -5
  249. package/skills/frontend-design/component-patterns.md +4 -5
  250. package/skills/frontend-design/engineering.md +4 -5
  251. package/skills/frontend-design/state-management.md +4 -5
  252. package/skills/frontend-design/ui-aesthetics.md +4 -5
  253. package/skills/frontend-design/ux-principles.md +4 -5
  254. package/skills/operating/change-management.md +4 -8
  255. package/skills/operating/incident-response.md +4 -8
  256. package/skills/operating/oncall-rotation.md +4 -7
  257. package/skills/operating/orchestration-reference.md +4 -10
  258. package/skills/quality-gates/review-work.md +4 -5
  259. package/skills/quality-gates/verify-change.md +4 -5
  260. package/skills/quality-gates/verify-module.md +4 -5
  261. package/skills/quality-gates/verify-quality.md +4 -5
  262. package/skills/quality-gates/verify-security.md +4 -5
  263. package/skills/roles/architect.ai-systems.md +6 -9
  264. package/skills/roles/architect.data.md +6 -9
  265. package/skills/roles/architect.enterprise.md +6 -9
  266. package/skills/roles/architect.integration.md +6 -9
  267. package/skills/roles/architect.md +7 -14
  268. package/skills/roles/architect.platform.md +6 -9
  269. package/skills/roles/data-analyst.experiment.md +6 -9
  270. package/skills/roles/data-analyst.md +6 -9
  271. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  272. package/skills/roles/data-analyst.product.md +6 -9
  273. package/skills/roles/data-analyst.telemetry.md +7 -9
  274. package/skills/roles/data-engineer.pipeline.md +6 -9
  275. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  276. package/skills/roles/data-engineer.warehouse.md +6 -9
  277. package/skills/roles/debugger.md +6 -9
  278. package/skills/roles/designer.accessibility.md +6 -9
  279. package/skills/roles/designer.md +7 -9
  280. package/skills/roles/engineer.ai.md +6 -9
  281. package/skills/roles/engineer.data.md +6 -9
  282. package/skills/roles/engineer.md +9 -9
  283. package/skills/roles/engineer.platform.md +6 -9
  284. package/skills/roles/operator.docs.md +6 -9
  285. package/skills/roles/operator.md +9 -9
  286. package/skills/roles/operator.release.md +6 -9
  287. package/skills/roles/operator.sre.md +6 -9
  288. package/skills/roles/orchestrator.md +6 -9
  289. package/skills/roles/product-manager.ai-product.md +6 -9
  290. package/skills/roles/product-manager.business-strategy.md +6 -9
  291. package/skills/roles/product-manager.enterprise.md +6 -9
  292. package/skills/roles/product-manager.growth.md +7 -9
  293. package/skills/roles/product-manager.md +7 -9
  294. package/skills/roles/product-manager.platform.md +6 -9
  295. package/skills/roles/product-manager.product.md +6 -9
  296. package/skills/roles/qa.ai-eval.md +8 -9
  297. package/skills/roles/qa.api-contract.md +7 -9
  298. package/skills/roles/qa.data-pipeline.md +7 -9
  299. package/skills/roles/qa.md +7 -9
  300. package/skills/roles/qa.test-automation.md +6 -9
  301. package/skills/roles/qa.web-ui.md +7 -9
  302. package/skills/roles/researcher.explorer.md +6 -9
  303. package/skills/roles/researcher.md +8 -9
  304. package/skills/roles/researcher.ux.md +6 -9
  305. package/skills/roles/reviewer.devil-advocate.md +6 -9
  306. package/skills/roles/reviewer.evaluator.md +6 -9
  307. package/skills/roles/reviewer.md +9 -9
  308. package/skills/roles/reviewer.trace.md +6 -9
  309. package/skills/roles/security.ai.md +7 -9
  310. package/skills/roles/security.appsec.md +6 -9
  311. package/skills/roles/security.cloud.md +6 -9
  312. package/skills/roles/security.legal-compliance.md +6 -9
  313. package/skills/roles/security.md +7 -9
  314. package/skills/roles/security.privacy.md +7 -9
  315. package/skills/roles/security.supply-chain.md +7 -9
  316. package/skills/security/blue-team.md +4 -5
  317. package/skills/security/code-audit.md +4 -5
  318. package/skills/security/pentest.md +4 -5
  319. package/skills/security/red-team.md +4 -5
  320. package/skills/security/threat-intel.md +4 -5
  321. package/skills/security/vuln-research.md +4 -5
  322. package/skills/strategy/competitive-landscape.md +4 -8
  323. package/skills/strategy/market-research-methods.md +4 -8
  324. package/skills/strategy/narrative-arc.md +4 -8
  325. package/skills/strategy/pricing-positioning.md +4 -8
  326. package/skills/utility/clean-code.md +4 -5
  327. package/specialists/policy-inventory.json +14 -0
  328. package/specialists/registry.json +158 -13
  329. package/lib/hooks/env-check.mjs +0 -83
  330. package/lib/hooks/read-tracker.mjs +0 -61
  331. package/lib/policy/unified-gates.mjs +0 -96
  332. package/lib/server/static/assets/index-ab25c707.js +0 -70
  333. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
  334. package/lib/specialist-contracts-enforce.mjs +0 -158
  335. package/rules/common/agents.md +0 -28
  336. package/rules/common/development-workflow.md +0 -32
  337. package/rules/common/performance.md +0 -55
@@ -0,0 +1,77 @@
1
+ /**
2
+ * lib/intake/attribution.mjs — provenance metadata for intake-archetype artifacts.
3
+ *
4
+ * Two-field attribution. Human identity comes from `git config user.name` +
5
+ * `user.email`; agent identity comes from CONSTRUCT_AGENT_ID (set by hosts
6
+ * like Claude Code, Codex, Copilot before they invoke the binary). Both are
7
+ * captured because either can be the proximate cause of a write and the
8
+ * audit reader needs to know which.
9
+ *
10
+ * Pure, side-effect free. No git/cli call when the env var
11
+ * CONSTRUCT_ATTRIBUTION_DISABLE=1 — useful for deterministic tests and for
12
+ * privacy-sensitive setups that prefer no identity capture.
13
+ */
14
+
15
+ import { spawnSync } from 'node:child_process';
16
+
17
+ function gitConfigValue(key, cwd) {
18
+ const result = spawnSync('git', ['config', '--get', key], {
19
+ cwd,
20
+ encoding: 'utf8',
21
+ timeout: 2000,
22
+ });
23
+ if (result.status !== 0) return null;
24
+ const value = (result.stdout || '').trim();
25
+ return value || null;
26
+ }
27
+
28
+ function humanIdentity(cwd) {
29
+ const name = gitConfigValue('user.name', cwd);
30
+ const email = gitConfigValue('user.email', cwd);
31
+ if (!name && !email) return null;
32
+ if (name && email) return `${name} <${email}>`;
33
+ return name || email;
34
+ }
35
+
36
+ function agentIdentity() {
37
+ return (
38
+ process.env.CONSTRUCT_AGENT_ID
39
+ || process.env.CLAUDE_AGENT_ID
40
+ || null
41
+ );
42
+ }
43
+
44
+ export function gatherAttribution({ now = new Date(), cwd } = {}) {
45
+ if (process.env.CONSTRUCT_ATTRIBUTION_DISABLE === '1') {
46
+ return {
47
+ createdBy: null,
48
+ createdByAgent: null,
49
+ createdAt: now.toISOString(),
50
+ };
51
+ }
52
+ return {
53
+ createdBy: humanIdentity(cwd),
54
+ createdByAgent: agentIdentity(),
55
+ createdAt: now.toISOString(),
56
+ };
57
+ }
58
+
59
+ export function stampAttribution(record, attribution = gatherAttribution()) {
60
+ if (!record || typeof record !== 'object') return record;
61
+ return {
62
+ ...record,
63
+ createdBy: attribution.createdBy,
64
+ createdByAgent: attribution.createdByAgent,
65
+ createdAt: attribution.createdAt,
66
+ };
67
+ }
68
+
69
+ export function touchAttribution(record, attribution = gatherAttribution()) {
70
+ if (!record || typeof record !== 'object') return record;
71
+ return {
72
+ ...record,
73
+ lastModifiedBy: attribution.createdBy,
74
+ lastModifiedByAgent: attribution.createdByAgent,
75
+ lastModifiedAt: attribution.createdAt,
76
+ };
77
+ }
@@ -44,6 +44,7 @@ export const DEFAULT_INTAKE_CONFIG = Object.freeze({
44
44
  maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
45
45
  includeProjectInbox: true,
46
46
  includeDocsIntake: true,
47
+ includeArchetypeInbox: false,
47
48
  });
48
49
 
49
50
  export function intakeConfigPath(rootDir) {
@@ -98,6 +99,7 @@ export function loadIntakeConfig(rootDir, env = process.env) {
98
99
  maxDepth,
99
100
  includeProjectInbox: stored.includeProjectInbox !== false,
100
101
  includeDocsIntake: stored.includeDocsIntake !== false,
102
+ includeArchetypeInbox: stored.includeArchetypeInbox === true,
101
103
  };
102
104
  }
103
105
 
@@ -113,6 +115,7 @@ export function saveIntakeConfig(rootDir, patch = {}) {
113
115
  maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
114
116
  includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
115
117
  includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
118
+ includeArchetypeInbox: patch.includeArchetypeInbox !== undefined ? Boolean(patch.includeArchetypeInbox) : current.includeArchetypeInbox,
116
119
  };
117
120
 
118
121
  const file = intakeConfigPath(rootDir);
@@ -0,0 +1,107 @@
1
+ /**
2
+ * lib/intake/manifest.mjs — SHA-256 dedup manifest for intake-archetype projects.
3
+ *
4
+ * Projects that scaffold the intake archetype (profile.capabilities.intake)
5
+ * drop raw source files into `inbox/`. The manifest records which source
6
+ * files have already been processed (by content hash, not path) so the
7
+ * intake runtime can refuse to reprocess an unchanged file and so a moved
8
+ * or renamed file still dedups correctly.
9
+ *
10
+ * Storage: <projectRoot>/.cx/intake/manifest.json
11
+ *
12
+ * Shape:
13
+ * {
14
+ * "version": 1,
15
+ * "files": {
16
+ * "<sha256>": {
17
+ * "sourcePath": "inbox/notes-2026-05-29.md",
18
+ * "processedAt": "2026-05-29T19:00:00.000Z",
19
+ * "intakeId": "intake-1779164832122-onxe",
20
+ * "createdBy": "Gerald Dagher <gerald@example.com>",
21
+ * "createdByAgent": "claude-opus-4-7"
22
+ * }
23
+ * }
24
+ * }
25
+ *
26
+ * Pure functions over the JSON. Callers own the rootDir; no implicit cwd.
27
+ * Atomic writes go through write-rename so a crash mid-write cannot leave
28
+ * the manifest half-written.
29
+ */
30
+
31
+ import { createHash } from 'node:crypto';
32
+ import {
33
+ existsSync,
34
+ mkdirSync,
35
+ readFileSync,
36
+ renameSync,
37
+ writeFileSync,
38
+ } from 'node:fs';
39
+ import { join } from 'node:path';
40
+
41
+ export const MANIFEST_REL_PATH = '.cx/intake/manifest.json';
42
+ export const MANIFEST_VERSION = 1;
43
+
44
+ function manifestPath(rootDir) {
45
+ return join(rootDir, MANIFEST_REL_PATH);
46
+ }
47
+
48
+ function emptyManifest() {
49
+ return { version: MANIFEST_VERSION, files: {} };
50
+ }
51
+
52
+ export function loadManifest(rootDir) {
53
+ const p = manifestPath(rootDir);
54
+ if (!existsSync(p)) return emptyManifest();
55
+ try {
56
+ const raw = JSON.parse(readFileSync(p, 'utf8'));
57
+ if (!raw || typeof raw !== 'object' || typeof raw.files !== 'object') {
58
+ return emptyManifest();
59
+ }
60
+ return raw;
61
+ } catch {
62
+ return emptyManifest();
63
+ }
64
+ }
65
+
66
+ export function saveManifest(rootDir, manifest) {
67
+ const p = manifestPath(rootDir);
68
+ const dir = join(rootDir, '.cx', 'intake');
69
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
70
+ const payload = JSON.stringify(manifest, null, 2) + '\n';
71
+ const tmp = `${p}.tmp`;
72
+ writeFileSync(tmp, payload, 'utf8');
73
+ renameSync(tmp, p);
74
+ }
75
+
76
+ export function sha256Of(buffer) {
77
+ return createHash('sha256').update(buffer).digest('hex');
78
+ }
79
+
80
+ export function hasFile(rootDir, sha) {
81
+ if (!sha) return false;
82
+ const manifest = loadManifest(rootDir);
83
+ return Boolean(manifest.files[sha]);
84
+ }
85
+
86
+ export function recordFile(rootDir, sha, entry) {
87
+ if (!sha) throw new Error('recordFile: sha is required');
88
+ const manifest = loadManifest(rootDir);
89
+ manifest.files[sha] = {
90
+ sourcePath: entry?.sourcePath ?? null,
91
+ processedAt: entry?.processedAt ?? new Date().toISOString(),
92
+ intakeId: entry?.intakeId ?? null,
93
+ createdBy: entry?.createdBy ?? null,
94
+ createdByAgent: entry?.createdByAgent ?? null,
95
+ };
96
+ saveManifest(rootDir, manifest);
97
+ return manifest.files[sha];
98
+ }
99
+
100
+ export function manifestStats(rootDir) {
101
+ const manifest = loadManifest(rootDir);
102
+ const entries = Object.values(manifest.files);
103
+ return {
104
+ total: entries.length,
105
+ version: manifest.version,
106
+ };
107
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * lib/intake/poll-lock.mjs — pid-stamped file lock around InboxWatcher.poll().
3
+ *
4
+ * Two pollers (the embed daemon and a manual `construct intake process`)
5
+ * share the same state file + manifest. Without serialization, both can
6
+ * decide a freshly-dropped file is unprocessed in the window between
7
+ * "read state" and "write state," producing two intake packets for one
8
+ * source. The lock makes one poll iteration atomic from the system's
9
+ * perspective: the second caller waits, retries until the timeout, or
10
+ * raises POLL_LOCK_BUSY for the CLI to render a clear refusal.
11
+ *
12
+ * Lockfile shape (JSON at <rootDir>/.cx/runtime/inbox-poll.lock):
13
+ * { pid, actor, startedAt, timeoutAt? }
14
+ *
15
+ * Stale detection: a lock whose pid is gone, or whose timeoutAt has
16
+ * passed, is cleared on the next acquire attempt. Mirrors the
17
+ * lib/beads-lock.mjs API shape so callers familiar with one know the other.
18
+ */
19
+
20
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
21
+ import { dirname, join } from 'node:path';
22
+
23
+ export const POLL_LOCK_REL_PATH = '.cx/runtime/inbox-poll.lock';
24
+ export const POLL_LOCK_BUSY = 'POLL_LOCK_BUSY';
25
+
26
+ function lockPathFor(rootDir) {
27
+ return join(rootDir, POLL_LOCK_REL_PATH);
28
+ }
29
+
30
+ function nowIso() {
31
+ return new Date().toISOString();
32
+ }
33
+
34
+ function afterMs(ms) {
35
+ return new Date(Date.now() + ms).toISOString();
36
+ }
37
+
38
+ function processExists(pid) {
39
+ if (!pid || typeof pid !== 'number') return false;
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ export function readPollLock(rootDir) {
49
+ const p = lockPathFor(rootDir);
50
+ if (!existsSync(p)) return null;
51
+ try {
52
+ const raw = JSON.parse(readFileSync(p, 'utf8'));
53
+ if (!raw || typeof raw !== 'object') return null;
54
+ return raw;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ export function isPollLockStale(lock) {
61
+ if (!lock) return false;
62
+ if (lock.pid && !processExists(lock.pid)) return true;
63
+ if (lock.timeoutAt && new Date(lock.timeoutAt) < new Date()) return true;
64
+ return false;
65
+ }
66
+
67
+ function clearStale(rootDir) {
68
+ const lock = readPollLock(rootDir);
69
+ if (lock && isPollLockStale(lock)) {
70
+ rmSync(lockPathFor(rootDir), { force: true });
71
+ return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ function writeLockFile(rootDir, lock) {
77
+ const p = lockPathFor(rootDir);
78
+ mkdirSync(dirname(p), { recursive: true });
79
+ writeFileSync(p, JSON.stringify(lock, null, 2) + '\n', 'utf8');
80
+ }
81
+
82
+ /**
83
+ * Acquire the intake poll lock. Resolves with the lock object on success,
84
+ * rejects with an Error whose `code` is POLL_LOCK_BUSY when the wait window
85
+ * elapses without acquisition. The rejection's `holder` field carries the
86
+ * blocking lock's metadata for downstream error rendering.
87
+ */
88
+ export async function acquirePollLock({
89
+ rootDir,
90
+ actor = 'unknown',
91
+ command = '',
92
+ waitMs = 0,
93
+ timeoutMs = 5 * 60 * 1000,
94
+ pollIntervalMs = 100,
95
+ } = {}) {
96
+ if (!rootDir) throw new Error('acquirePollLock: rootDir is required');
97
+ const deadline = Date.now() + Math.max(0, waitMs);
98
+
99
+ while (true) {
100
+ clearStale(rootDir);
101
+ const existing = readPollLock(rootDir);
102
+ if (!existing) {
103
+ const lock = {
104
+ pid: process.pid,
105
+ actor,
106
+ command,
107
+ startedAt: nowIso(),
108
+ timeoutAt: timeoutMs > 0 ? afterMs(timeoutMs) : undefined,
109
+ };
110
+ writeLockFile(rootDir, lock);
111
+ return lock;
112
+ }
113
+ if (Date.now() >= deadline) {
114
+ const err = new Error(
115
+ `intake poll lock held by ${existing.actor || 'unknown'} (pid ${existing.pid}) since ${existing.startedAt}`,
116
+ );
117
+ err.code = POLL_LOCK_BUSY;
118
+ err.holder = existing;
119
+ throw err;
120
+ }
121
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Release the lock iff the calling process owns it. Returns true on release,
127
+ * false when the lock was missing, expired, or owned by someone else.
128
+ */
129
+ export function releasePollLock(rootDir) {
130
+ const lock = readPollLock(rootDir);
131
+ if (lock && lock.pid === process.pid) {
132
+ rmSync(lockPathFor(rootDir), { force: true });
133
+ return true;
134
+ }
135
+ return false;
136
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * lib/intake/prepare.mjs — turn a fresh inbox ingestion into an R&D intake packet.
3
3
  *
4
- * Called by `InboxWatcher.poll()` after each successful file ingestion.
4
+ * Triggered by `InboxWatcher.poll()` after each successful file ingestion.
5
5
  * Four deterministic preparation steps, then write the result to the
6
6
  * intake queue:
7
7
  *
@@ -14,11 +14,16 @@
14
14
  * 4. Run classifyRdIntake to produce the R&D triage object (intake type,
15
15
  * owner persona, recommended chain, action, risk).
16
16
  *
17
+ * After enqueue, fires a signal.promoted lifecycle event when the triage
18
+ * confidence crosses SIGNAL_PROMOTION_THRESHOLD (matches the default
19
+ * tag-vocabulary auto_threshold), so subscribers can act on confidently
20
+ * typed signals without polling the queue.
21
+ *
17
22
  * The agent — invoked manually or via session-start hook nudge — reads
18
23
  * the resulting `.cx/intake/pending/<id>.json` and does the real
19
24
  * comparison work (overlap with existing PRD? contradicts ADR? new RFC
20
- * candidate?). The daemon never calls an LLM. This separation keeps the
21
- * daemon cheap and predictable; the model spend stays with the agent.
25
+ * candidate?). The daemon never calls an LLM; the separation keeps the
26
+ * daemon cheap and predictable while model spend stays with the agent.
22
27
  */
23
28
 
24
29
  import { existsSync, readFileSync } from 'node:fs';
@@ -30,11 +35,19 @@ import { createIntakeQueue } from './queue.mjs';
30
35
  import { classifyRdIntake } from './classify.mjs';
31
36
  import { detectCustomerMentions, linkSignalToCustomer, updateCustomerProfile } from '../embed/customer-profiles.mjs';
32
37
  import { resolveActiveProfile } from '../profiles/loader.mjs';
38
+ import { emitBestEffort as emitRoleEvent } from '../roles/event-bus.mjs';
39
+ import { gatherAttribution, stampAttribution } from './attribution.mjs';
40
+ import { MANIFEST_REL_PATH } from './manifest.mjs';
33
41
 
34
42
  const DEFAULT_RELATED_LIMIT = 5;
35
43
  const EXCERPT_CHARS = 800;
36
44
  const QUERY_CHARS = 500;
37
45
 
46
+ // Confidence at which a triage signal is considered "promoted" from
47
+ // uncertain to confidently classified. Mirrors the default tag-vocabulary
48
+ // auto_threshold.
49
+ const SIGNAL_PROMOTION_THRESHOLD = 0.70;
50
+
38
51
  export async function prepareIntakeForIngestedFile({
39
52
  rootDir,
40
53
  ingestedFile,
@@ -85,7 +98,7 @@ export async function prepareIntakeForIngestedFile({
85
98
 
86
99
  const droppedInfo = ingestedFile.droppedInfo ?? [];
87
100
 
88
- const entry = {
101
+ const baseEntry = {
89
102
  intake: {
90
103
  sourcePath: ingestedFile.sourcePath,
91
104
  outputPath: ingestedFile.outputPath,
@@ -101,10 +114,35 @@ export async function prepareIntakeForIngestedFile({
101
114
  customers: customers.length ? customers : undefined,
102
115
  };
103
116
 
117
+ // Capability detection mirrors inbox.mjs: stamp provenance onto the packet
118
+ // when the project's intake manifest is present (init scaffolds it for
119
+ // archetype-enabled profiles). Keeps non-archetype packets shape-identical
120
+ // to the pre-attribution behavior so downstream consumers see no diff.
121
+
122
+ const archetypeOn = existsSync(`${rootDir}/${MANIFEST_REL_PATH}`);
123
+ const entry = archetypeOn ? stampAttribution(baseEntry, gatherAttribution({ cwd: rootDir })) : baseEntry;
124
+
104
125
  const intakeQueue = queue || createIntakeQueue(rootDir, env);
105
126
  const result = intakeQueue.enqueue(entry);
106
127
  const intakeId = result?.id || '';
107
128
 
129
+ // Promotion fires once a triage signal crosses the auto-classification
130
+ // threshold, giving subscribers a chance to act on a confidently typed
131
+ // signal without polling the intake queue.
132
+
133
+ if ((triage?.confidence ?? 0) >= SIGNAL_PROMOTION_THRESHOLD && triage?.intakeType && triage.intakeType !== 'unknown') {
134
+ emitRoleEvent('signal.promoted', {
135
+ summary: `${triage.intakeType} (${triage.confidence.toFixed(2)})`,
136
+ context: {
137
+ intakeId,
138
+ intakeType: triage.intakeType,
139
+ confidence: triage.confidence,
140
+ sourcePath: ingestedFile.sourcePath,
141
+ primaryOwner: triage.primaryOwner,
142
+ },
143
+ });
144
+ }
145
+
108
146
  // Update customer profiles with new evidence
109
147
  for (const mention of customerMentions) {
110
148
  try {
@@ -80,6 +80,18 @@ function buildSourceList(repoRoot = REPO_ROOT) {
80
80
  }
81
81
  }
82
82
 
83
+ // User-ingested documents (PDF/Office/audio routed through docling + whisper)
84
+ const ingestRoot = join(repoRoot, '.cx', 'ingest');
85
+ if (existsSync(ingestRoot)) {
86
+ for (const entry of readdirSync(ingestRoot)) {
87
+ const dir = join(ingestRoot, entry);
88
+ const markdown = join(dir, 'markdown.md');
89
+ if (!/^[a-f0-9]{64}$/.test(entry)) continue;
90
+ if (!existsSync(markdown)) continue;
91
+ sources.push({ path: markdown, rel: relative(repoRoot, markdown), priority: 4, source: 'ingest' });
92
+ }
93
+ }
94
+
83
95
  return sources;
84
96
  }
85
97