@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
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * @p95ms 300
8
8
  * @maxBlockingScope SessionStart
9
+ *
10
+ * @lifecycle SessionStart
11
+ * @matcher *
12
+ * @exits 0 = pass
9
13
  */
10
14
  import { readFileSync, existsSync, statSync, writeFileSync } from 'fs';
11
15
  import { execSync } from 'child_process';
@@ -267,7 +271,7 @@ try {
267
271
  if (hasSources) {
268
272
  const parts = [];
269
273
  if (repos.length > 0) parts.push(`GitHub repos: ${repos.join(', ')}`);
270
- if (jiraConfigured) parts.push(`Jira: ${process.env.JIRA_BASE_URL}`);
274
+ if (jiraConfigured) parts.push('Jira: configured');
271
275
  if (linearConfigured) parts.push('Linear: configured');
272
276
  // Surface which sources are wired so `provider_fetch` is the obvious
273
277
  // tool for questions about them. The "fetch first, don't answer from
@@ -321,6 +325,49 @@ try {
321
325
  brokerStatusNote = `\n${buildBrokerStatusLine({ env: process.env })}\n`;
322
326
  } catch { /* best effort */ }
323
327
 
328
+ // Missing-env-vars notice. Reads .env.example from the project root,
329
+ // compares against .env and process.env, lists keys whose example value
330
+ // is a placeholder and whose live value is unset. Empty string when the
331
+ // project has no .env.example or all required keys are populated.
332
+
333
+ const envCheckNote = buildEnvCheckNote(cwd);
334
+
335
+ function buildEnvCheckNote(rootDir) {
336
+ const examplePath = join(rootDir, '.env.example');
337
+ if (!existsSync(examplePath)) return '';
338
+ const example = parseEnvFile(examplePath);
339
+ if (example.size === 0) return '';
340
+ const envFile = parseEnvFile(join(rootDir, '.env'));
341
+ const PLACEHOLDER = /^(YOUR_|<|__|\$\{|REPLACE|ADD_|INSERT_|xxx|TODO)/i;
342
+ const missing = [];
343
+ for (const [key, exampleVal] of example) {
344
+ const required = !exampleVal || PLACEHOLDER.test(exampleVal);
345
+ if (!required) continue;
346
+ const liveFromEnvFile = envFile.has(key) && envFile.get(key) !== '' && !PLACEHOLDER.test(envFile.get(key));
347
+ const liveFromProcessEnv = process.env[key] && !PLACEHOLDER.test(process.env[key]);
348
+ if (!liveFromEnvFile && !liveFromProcessEnv) missing.push(key);
349
+ }
350
+ if (missing.length === 0) return '';
351
+ const noun = missing.length === 1 ? 'variable' : 'variables';
352
+ const list = missing.map((k) => ` - ${k}`).join('\n');
353
+ return `\n## Environment check — ${missing.length} required ${noun} not set\n${list}\nAdd these to .env before running the app.\n`;
354
+ }
355
+
356
+ function parseEnvFile(p) {
357
+ const map = new Map();
358
+ if (!existsSync(p)) return map;
359
+ for (const line of readFileSync(p, 'utf8').split('\n')) {
360
+ const stripped = line.trim();
361
+ if (!stripped || stripped.startsWith('#')) continue;
362
+ const eq = stripped.indexOf('=');
363
+ if (eq === -1) continue;
364
+ const key = stripped.slice(0, eq).trim();
365
+ const val = stripped.slice(eq + 1).trim();
366
+ if (key) map.set(key, val);
367
+ }
368
+ return map;
369
+ }
370
+
324
371
  // Permission posture: surface gaps in ~/.claude/settings.json permissions.allow
325
372
  // so the agent can ask the user to close them instead of stumbling into the
326
373
  // classifier mid-task. Empty string when no gaps — keeps the banner small.
@@ -331,7 +378,7 @@ try {
331
378
  permissionPostureNote = buildPermissionPostureLine({ cwd });
332
379
  } catch { /* best effort */ }
333
380
 
334
- process.stdout.write(`${header}\n${body}${stateNote}${efficiencyNote}${observationsNote}${concurrencyNote}${skillScopeNote}${dropNote}${embedStatusNote}${sourcesNote}${selfKnowledgeNote}${rolesNote}${intakeReviewNote}${brokerStatusNote}${permissionPostureNote}\n${footer}${pendingNote}\n`);
381
+ process.stdout.write(`${header}\n${body}${stateNote}${efficiencyNote}${observationsNote}${concurrencyNote}${skillScopeNote}${dropNote}${embedStatusNote}${sourcesNote}${selfKnowledgeNote}${rolesNote}${intakeReviewNote}${brokerStatusNote}${permissionPostureNote}${envCheckNote}\n${footer}${pendingNote}\n`);
335
382
  // Auto-bootstrap the policy-engine gate. session-start has just emitted
336
383
  // branch + recent commits + prior observations + context-state — that is
337
384
  // already grounding; mark the session bootstrapped so the PreToolUse rule
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lib/hooks/session-tracking-refresh.mjs — keep the project's tracking
4
+ * surfaces current at session end.
5
+ *
6
+ * Runs as a Stop hook in parallel with the other Stop-time hooks. Touches
7
+ * three surfaces:
8
+ *
9
+ * - `.cx/context.md` + `.cx/context.json` — refresh Active Work, Recent
10
+ * Decisions, and Architecture Notes from the session's observations,
11
+ * commits, and bead state changes.
12
+ * - `plan.md` — sync the bead-status table with current `bd show <id>`
13
+ * truth; if every referenced bead has closed and the plan has been
14
+ * idle for >1h, archive a copy to `.cx/handoffs/<date>-plan-landed.md`
15
+ * and reset `plan.md` to the standard template.
16
+ *
17
+ * Best-effort. Failures degrade silently. Wall-clock budgeted at 2000 ms
18
+ * total — bd shells out, and a plan with several bead refs needs one
19
+ * `bd show` per ref. Exits 0 on any error so the session close path is
20
+ * never blocked.
21
+ *
22
+ * Only runs inside Construct projects (presence of `.cx/`) — same gate as
23
+ * the other Stop hooks.
24
+ *
25
+ * @lifecycle Stop
26
+ * @matcher *
27
+ * @p95ms 2000
28
+ * @maxBlockingScope none (Stop, non-blocking)
29
+ * @exits 0 = pass
30
+ */
31
+
32
+ import { existsSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { readHookInput } from './_lib/input.mjs';
35
+ import { logHookFailure } from './_lib/log.mjs';
36
+
37
+ const HARD_BUDGET_MS = 2000;
38
+ const startedAt = Date.now();
39
+
40
+ const input = readHookInput();
41
+ const cwd = input?.cwd || process.cwd();
42
+
43
+ // Only run inside Construct projects.
44
+ if (!existsSync(join(cwd, '.cx'))) process.exit(0);
45
+
46
+ const deadline = setTimeout(() => process.exit(0), HARD_BUDGET_MS);
47
+ deadline.unref();
48
+
49
+ try {
50
+ // Order matters. archivePlanIfLanded reads plan.md's mtime to decide
51
+ // whether the plan has been idle long enough to retire. syncPlanFile
52
+ // mutates plan.md when bead statuses drift — which bumps mtime and
53
+ // makes the plan look "recently touched" to the archive check. Run
54
+ // archive first (true idle mtime), then sync the survivor.
55
+ const { archivePlanIfLanded, syncPlanFile, refreshContextMd } = await import('../tracking-surfaces.mjs');
56
+ await archivePlanIfLanded({ rootDir: cwd });
57
+ await syncPlanFile({ rootDir: cwd });
58
+ await refreshContextMd({ rootDir: cwd });
59
+ } catch (err) {
60
+ logHookFailure({ hook: 'session-tracking-refresh', err, phase: 'refresh' });
61
+ }
62
+
63
+ const elapsed = Date.now() - startedAt;
64
+ if (elapsed > HARD_BUDGET_MS) {
65
+ try {
66
+ process.stderr.write(`[session-tracking-refresh] over budget: ${elapsed}ms\n`);
67
+ } catch { /* stderr closed */ }
68
+ }
69
+
70
+ process.exit(0);
@@ -11,8 +11,14 @@
11
11
  *
12
12
  * @p95ms 500
13
13
  * @maxBlockingScope Stop
14
+ *
15
+ * @lifecycle Stop
16
+ * @matcher *
17
+ * @exits 0 = pass
14
18
  */
15
- import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
19
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
20
+ import { appendBounded } from '../logging/rotate.mjs';
21
+ import { resolveProjectScope } from '../project-root.mjs';
16
22
  import { execSync } from 'child_process';
17
23
  import { homedir } from 'os';
18
24
  import { join, dirname } from 'path';
@@ -228,7 +234,12 @@ try {
228
234
  if (agentName) entry.agent = agentName;
229
235
  if (taskKey) entry.task_key = taskKey;
230
236
 
231
- try { appendFileSync(costLogPath, JSON.stringify(entry) + '\n'); } catch { /* non-critical */ }
237
+ // Cross-project ledger keep at user scope but tag with projectId so a
238
+ // reader can attribute spend to a specific project.
239
+
240
+ const scope = resolveProjectScope();
241
+ if (scope?.projectId) entry.projectId = scope.projectId;
242
+ try { appendBounded('session-cost', costLogPath, JSON.stringify(entry) + '\n'); } catch { /* non-critical */ }
232
243
 
233
244
  totalCostUsd += priced.costUsd;
234
245
  turnsLogged += 1;
@@ -6,6 +6,10 @@
6
6
  *
7
7
  * @p95ms 2000
8
8
  * @maxBlockingScope Stop
9
+ *
10
+ * @lifecycle Stop
11
+ * @matcher *
12
+ * @exits 0 = pass
9
13
  */
10
14
  import { readFileSync, writeFileSync, existsSync } from 'fs';
11
15
  import { execSync } from 'child_process';
@@ -8,9 +8,13 @@
8
8
  *
9
9
  * @p95ms 30
10
10
  * @maxBlockingScope none (async, non-blocking)
11
+ *
12
+ * @lifecycle PostToolUse
13
+ * @matcher Bash
14
+ * @exits 0 = pass
11
15
  */
12
16
 
13
- import { readFileSync, existsSync, appendFileSync } from 'node:fs';
17
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
14
18
  import { homedir } from 'node:os';
15
19
  import { join } from 'node:path';
16
20
  import { logHookFailure } from './_lib/log.mjs';
@@ -56,13 +60,13 @@ try {
56
60
  }
57
61
  state[name] = { failedAt: now };
58
62
  }
59
- appendFileSync(flakeTrackerPath, '');
60
63
  const trimmed = {};
61
64
  for (const [k, v] of Object.entries(state)) {
62
65
  if ((v.failedAt || v.passedAt || 0) > now - 24 * 60 * 60 * 1000) trimmed[k] = v;
63
66
  }
64
- const writeFileSync = (await import('node:fs')).writeFileSync;
65
67
  writeFileSync(flakeTrackerPath, JSON.stringify(trimmed));
66
- } catch { /* best effort */ }
68
+ } catch (err) {
69
+ logHookFailure({ hook: 'test-watch', err, phase: 'flake-track' });
70
+ }
67
71
 
68
72
  process.exit(0);
@@ -0,0 +1,94 @@
1
+ /**
2
+ * lib/ingest/chunker.mjs — paragraph-boundary chunker with sentence overlap.
3
+ *
4
+ * 2026-06 best practice notes:
5
+ * - Recursive/sentence chunking remains the high-recall default (~85-90%
6
+ * on 2026 RAG benchmarks). Semantic chunking gains only ~2-3% recall at
7
+ * ~14× the embedding cost and only matters above ~5k token docs.
8
+ * - Parent-document retrieval is the meaningful 2026 upgrade beyond
9
+ * paragraph chunking — track separately if recall plateaus.
10
+ *
11
+ * Strategy:
12
+ * 1. Split markdown by blank-line paragraph boundaries.
13
+ * 2. Pack paragraphs into chunks under maxChars (default 1500 ≈ 375 tokens).
14
+ * 3. Add 1-2 sentence overlap between consecutive chunks to preserve
15
+ * cross-chunk anchors.
16
+ * 4. Preserve markdown structure: never split inside a fenced code block.
17
+ */
18
+
19
+ const DEFAULT_MAX_CHARS = 1500;
20
+ const DEFAULT_OVERLAP_SENTENCES = 2;
21
+ const SENTENCE_RE = /[^.!?]+[.!?]+["')\]]*\s*/g;
22
+
23
+ export function splitSentences(text) {
24
+ const matches = text.match(SENTENCE_RE);
25
+ if (matches && matches.length) return matches.map((s) => s.trim()).filter(Boolean);
26
+ return text.split('\n').map((s) => s.trim()).filter(Boolean);
27
+ }
28
+
29
+ function tailSentences(text, count) {
30
+ const sentences = splitSentences(text);
31
+ return sentences.slice(-count).join(' ');
32
+ }
33
+
34
+ function splitParagraphs(markdown) {
35
+ const blocks = [];
36
+ let buffer = '';
37
+ let inFence = false;
38
+ for (const line of markdown.split('\n')) {
39
+ if (/^```/.test(line.trim())) inFence = !inFence;
40
+ if (!inFence && line.trim() === '' && buffer.trim()) {
41
+ blocks.push(buffer.trim());
42
+ buffer = '';
43
+ continue;
44
+ }
45
+ buffer += line + '\n';
46
+ }
47
+ if (buffer.trim()) blocks.push(buffer.trim());
48
+ return blocks;
49
+ }
50
+
51
+ export function chunkMarkdown(markdown, { maxChars = DEFAULT_MAX_CHARS, overlapSentences = DEFAULT_OVERLAP_SENTENCES } = {}) {
52
+ const paragraphs = splitParagraphs(markdown || '');
53
+ if (paragraphs.length === 0) return [];
54
+
55
+ const chunks = [];
56
+ let current = '';
57
+ let chunkIndex = 0;
58
+ const flushChunk = () => {
59
+ if (!current.trim()) return;
60
+ const text = current.trim();
61
+ chunks.push({
62
+ index: chunkIndex++,
63
+ text,
64
+ chars: text.length,
65
+ });
66
+ const overlap = overlapSentences > 0 ? tailSentences(text, overlapSentences) : '';
67
+ current = overlap ? overlap + '\n\n' : '';
68
+ };
69
+
70
+ for (const para of paragraphs) {
71
+ if (current.length + para.length + 2 > maxChars && current.trim()) {
72
+ flushChunk();
73
+ }
74
+ if (para.length > maxChars) {
75
+ flushChunk();
76
+ const sentences = splitSentences(para);
77
+ let buf = '';
78
+ for (const s of sentences) {
79
+ if (buf.length + s.length + 1 > maxChars && buf.trim()) {
80
+ chunks.push({ index: chunkIndex++, text: buf.trim(), chars: buf.trim().length });
81
+ buf = (overlapSentences > 0 ? tailSentences(buf, overlapSentences) : '') + (overlapSentences > 0 ? ' ' : '');
82
+ }
83
+ buf += s + ' ';
84
+ }
85
+ if (buf.trim()) {
86
+ current = buf.trim() + '\n\n';
87
+ }
88
+ continue;
89
+ }
90
+ current += (current ? '\n\n' : '') + para;
91
+ }
92
+ flushChunk();
93
+ return chunks;
94
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * lib/ingest/pipeline.mjs — orchestrate the content-hashed ingest pipeline.
3
+ *
4
+ * Flow: hash → de-dup → extract via docling/whisper → chunk → write to
5
+ * .cx/ingest/<sha>/. Returns a record describing what was stored along
6
+ * with any droppedInfo surfaced by the extractor.
7
+ *
8
+ * Idempotent: re-ingesting the same content (by sha256) returns the
9
+ * existing record without re-extracting.
10
+ */
11
+ import fs from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { extractDocumentTextAsync } from '../document-extract.mjs';
14
+ import { hashFile, defaultIngestRoot, readRecord, writeRecord } from './store.mjs';
15
+ import { chunkMarkdown } from './chunker.mjs';
16
+
17
+ export async function ingestFile(filePath, { cwd = process.cwd(), force = false } = {}) {
18
+ const absPath = path.resolve(filePath);
19
+ const stat = await fs.stat(absPath);
20
+ if (!stat.isFile()) throw new Error(`not a regular file: ${absPath}`);
21
+
22
+ const root = defaultIngestRoot(cwd);
23
+ const sha256 = await hashFile(absPath);
24
+
25
+ if (!force) {
26
+ const existing = await readRecord(root, sha256);
27
+ if (existing) return { ...existing, status: 'cached' };
28
+ }
29
+
30
+ const extracted = await extractDocumentTextAsync(absPath);
31
+ const markdown = extracted.markdown ?? extracted.text ?? '';
32
+ const chunks = chunkMarkdown(markdown);
33
+
34
+ const source = {
35
+ sourcePath: absPath,
36
+ fileName: path.basename(absPath),
37
+ extension: extracted.extension,
38
+ bytes: stat.size,
39
+ sha256,
40
+ ingestedAt: new Date().toISOString(),
41
+ };
42
+
43
+ const meta = {
44
+ extractionMethod: extracted.extractionMethod,
45
+ extractorMetadata: extracted.metadata ?? null,
46
+ droppedInfo: extracted.droppedInfo ?? [],
47
+ chunkCount: chunks.length,
48
+ chunkChars: chunks.reduce((a, c) => a + c.chars, 0),
49
+ };
50
+
51
+ const record = await writeRecord(root, { sha256, source, meta, markdown });
52
+ return { ...record, status: 'ingested', droppedInfo: meta.droppedInfo, chunks };
53
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * lib/ingest/store.mjs — content-addressed ingest store at .cx/ingest/<hash>/.
3
+ *
4
+ * Each ingested document is stored under its SHA-256 content hash:
5
+ * .cx/ingest/<sha256>/source.json — original path, size, sha256, ingestedAt
6
+ * .cx/ingest/<sha256>/markdown.md — extracted markdown body
7
+ * .cx/ingest/<sha256>/meta.json — extractor metadata, droppedInfo, chunks
8
+ *
9
+ * Idempotent: re-ingesting a file with the same content is a no-op (returns
10
+ * the existing record). Re-running ingestion is the safe default.
11
+ *
12
+ * 2026-06 best practice notes:
13
+ * - SHA-256 is still the universal content-addressing primitive. BLAKE3 is
14
+ * ~2.3× faster on M-series and worth considering at multi-GB scale,
15
+ * but is not the industry default and adds an unfamiliar dependency.
16
+ * Revisit if ingest throughput becomes a measured bottleneck.
17
+ * - Storing markdown body separately from metadata keeps the embedding
18
+ * and search paths simple: they only read markdown.md and look up
19
+ * provenance from meta.json on demand.
20
+ */
21
+ import { createHash } from 'node:crypto';
22
+ import { createReadStream } from 'node:fs';
23
+ import fs from 'node:fs/promises';
24
+ import path from 'node:path';
25
+
26
+ export function defaultIngestRoot(cwd = process.cwd()) {
27
+ return path.join(cwd, '.cx', 'ingest');
28
+ }
29
+
30
+ export async function hashFile(filePath) {
31
+ return new Promise((resolveHash, reject) => {
32
+ const hash = createHash('sha256');
33
+ const stream = createReadStream(filePath);
34
+ stream.on('data', (chunk) => hash.update(chunk));
35
+ stream.on('error', reject);
36
+ stream.on('end', () => resolveHash(hash.digest('hex')));
37
+ });
38
+ }
39
+
40
+ function recordDirFor(root, sha256) {
41
+ return path.join(root, sha256);
42
+ }
43
+
44
+ export async function readRecord(root, sha256) {
45
+ const dir = recordDirFor(root, sha256);
46
+ try {
47
+ const [source, meta, markdown] = await Promise.all([
48
+ fs.readFile(path.join(dir, 'source.json'), 'utf8').then(JSON.parse),
49
+ fs.readFile(path.join(dir, 'meta.json'), 'utf8').then(JSON.parse),
50
+ fs.readFile(path.join(dir, 'markdown.md'), 'utf8'),
51
+ ]);
52
+ return { sha256, source, meta, markdown, dir };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ export async function writeRecord(root, { sha256, source, meta, markdown }) {
59
+ const dir = recordDirFor(root, sha256);
60
+ await fs.mkdir(dir, { recursive: true });
61
+ await Promise.all([
62
+ fs.writeFile(path.join(dir, 'source.json'), JSON.stringify(source, null, 2) + '\n', 'utf8'),
63
+ fs.writeFile(path.join(dir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8'),
64
+ fs.writeFile(path.join(dir, 'markdown.md'), markdown, 'utf8'),
65
+ ]);
66
+ return { sha256, dir, source, meta, markdown };
67
+ }
68
+
69
+ export async function listRecords(root) {
70
+ let entries;
71
+ try { entries = await fs.readdir(root, { withFileTypes: true }); }
72
+ catch { return []; }
73
+ const records = [];
74
+ for (const entry of entries) {
75
+ if (!entry.isDirectory()) continue;
76
+ const sha = entry.name;
77
+ if (!/^[a-f0-9]{64}$/.test(sha)) continue;
78
+ const record = await readRecord(root, sha);
79
+ if (record) records.push(record);
80
+ }
81
+ return records;
82
+ }
@@ -812,12 +812,28 @@ async function main() {
812
812
  const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('./config/project-config.mjs');
813
813
  const found = findProjectConfigPath(target);
814
814
  const cfgPath = found || path.join(target, PROJECT_CONFIG_FILENAME);
815
- const cfg = found ? (loadProjectConfig(target) || { version: 1 }) : { version: 1 };
815
+
816
+ // loadProjectConfig returns a wrapper { path, raw, config, source, errors }.
817
+ // Persist .raw (the on-disk JSON) so user customizations survive and
818
+ // defaults are not silently materialized into the file.
819
+
820
+ const loaded = found ? loadProjectConfig(target) : null;
821
+ const cfg = loaded?.raw ? { ...loaded.raw } : { version: 1 };
816
822
  cfg.profile = profileId;
817
823
  writeProjectConfig(cfgPath, cfg, { validate: true });
818
824
  if (!quiet) console.log(`Profile set to ${profileId}.`);
819
825
  }
820
-
826
+
827
+ // Resolve active profile to drive capability-gated scaffolding (intake
828
+ // archetype, attribution stamping). Falls back to rnd when no profile is
829
+ // configured — matches resolveActiveProfile semantics elsewhere.
830
+
831
+ const { resolveActiveProfile } = await import('./profiles/loader.mjs');
832
+ const activeProfile = resolveActiveProfile(target, profileId) ?? null;
833
+ const intakeCap = activeProfile?.capabilities?.intake ?? null;
834
+ const { gatherAttribution } = await import('./intake/attribution.mjs');
835
+ const attribution = intakeCap?.attribution ? gatherAttribution() : null;
836
+
821
837
  // Always create core Construct files
822
838
  writeStampedIfMissing({
823
839
  targetRoot: target,
@@ -826,8 +842,9 @@ async function main() {
826
842
  filePath: path.join(target, "AGENTS.md"),
827
843
  content: buildAgentsGuide(projectName),
828
844
  generator: "construct/init",
845
+ attribution,
829
846
  });
830
-
847
+
831
848
  writeStampedIfMissing({
832
849
  targetRoot: target,
833
850
  created,
@@ -835,17 +852,18 @@ async function main() {
835
852
  filePath: path.join(target, "plan.md"),
836
853
  content: buildPlanTemplate(),
837
854
  generator: "construct/init",
855
+ attribution,
838
856
  });
839
-
857
+
840
858
  writeStampedIfMissing({
841
859
  targetRoot: target,
842
860
  created,
843
861
  skipped,
844
862
  filePath: path.join(target, ".cx", "context.json"),
845
- content: buildContextJson(projectName),
863
+ content: buildContextJson(projectName, { attribution }),
846
864
  generator: "construct/init",
847
865
  });
848
-
866
+
849
867
  writeStampedIfMissing({
850
868
  targetRoot: target,
851
869
  created,
@@ -853,6 +871,7 @@ async function main() {
853
871
  filePath: path.join(target, ".cx", "context.md"),
854
872
  content: buildContextMarkdown(),
855
873
  generator: "construct/init",
874
+ attribution,
856
875
  });
857
876
 
858
877
  writeStampedIfMissing({
@@ -888,6 +907,56 @@ async function main() {
888
907
  });
889
908
  }
890
909
 
910
+ // Intake-archetype scaffolding. When the active profile declares
911
+ // capabilities.intake.inbox, scaffold a project-root inbox/ drop zone
912
+ // (with a .gitignore so raw drops never enter source) and seed the
913
+ // dedup manifest. The existing-structure detection already opted us
914
+ // out of clobbering a user's pipeline above.
915
+
916
+ if (intakeCap?.inbox && !inboxDecision.skip) {
917
+ const projectInbox = path.join(target, 'inbox');
918
+ if (!fs.existsSync(projectInbox)) {
919
+ fs.mkdirSync(projectInbox, { recursive: true });
920
+ created.push('inbox/');
921
+ }
922
+ const inboxGitignore = path.join(projectInbox, '.gitignore');
923
+ if (!fs.existsSync(inboxGitignore)) {
924
+ fs.writeFileSync(inboxGitignore, '*\n!.gitignore\n', 'utf8');
925
+ created.push('inbox/.gitignore');
926
+ }
927
+
928
+ if (intakeCap.dedup === 'sha256') {
929
+ const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
930
+ const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
931
+ if (!manifestExists) {
932
+ saveManifest(target, loadManifest(target));
933
+ created.push(MANIFEST_REL_PATH);
934
+ }
935
+ }
936
+ }
937
+
938
+ // Ensure `.cx/` is in project .gitignore. .cx/ is runtime state — observations,
939
+ // sessions, vector index, and traces (with daily JSONL files that can exceed
940
+ // GitHub's 100 MB file-size limit). Idempotent: if `.cx/` (or a broader
941
+ // pattern like `.cx` / `*` / `**`) already matches, leave the file alone.
942
+
943
+ try {
944
+ const gitignorePath = path.join(target, '.gitignore');
945
+ const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
946
+ const lines = existing.split('\n').map((l) => l.trim());
947
+ const alreadyIgnored = lines.some((l) =>
948
+ l === '.cx' || l === '.cx/' || l === '.cx/**' || l === '*' || l === '**'
949
+ );
950
+ if (!alreadyIgnored) {
951
+ const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
952
+ const block = `${prefix}\n# Construct runtime state — local-only, never source\n.cx/\n`;
953
+ fs.writeFileSync(gitignorePath, existing + block, 'utf8');
954
+ created.push(existing.length === 0 ? '.gitignore (added .cx/)' : '.gitignore (appended .cx/)');
955
+ }
956
+ } catch (err) {
957
+ console.warn(`⚠️ Could not update .gitignore: ${err.message}`);
958
+ }
959
+
891
960
  // Stage .construct/ launcher + sync .claude/ adapters so init produces the
892
961
  // same project shape as a fresh `npm install` of the package as a dep.
893
962
 
@@ -911,23 +980,12 @@ async function main() {
911
980
  created.push('.claude/ (agents + settings)');
912
981
  }
913
982
 
914
- // Also sync home-level adapters for all 6 host surfaces (Copilot, OpenCode,
915
- // Codex, VS Code, Cursor, plus Claude home) so the user's environment is
916
- // fully wired after init. Project-local Claude adapters were written above.
917
- const syncScript = path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs');
918
- if (fs.existsSync(syncScript)) {
919
- const { spawnSync } = await import('node:child_process');
920
- const homeResult = spawnSync(process.execPath, [syncScript], {
921
- cwd: target,
922
- stdio: 'pipe',
923
- env: { ...process.env, CONSTRUCT_PROJECT_ROOT: target },
924
- timeout: 120000,
925
- });
926
- if (homeResult.status !== 0) {
927
- const errMsg = homeResult.stderr.toString().substring(0, 200);
928
- console.warn(`⚠️ Home adapter sync had issues (exit ${homeResult.status}): ${errMsg}`);
929
- }
930
- }
983
+ // construct init writes project scope only. Global wiring (the `construct`
984
+ // front-door agent in `~/.claude/agents/`, `~/.codex/agents/`, etc.) is
985
+ // installed once by `construct sync --global` or the npm postinstall.
986
+ // Specialists, slash commands, and skills live with the repo per each
987
+ // host's documented best-practice scope.
988
+
931
989
  } catch (err) {
932
990
  console.warn(`⚠️ Adapter staging failed: ${err.message}`);
933
991
  }
@@ -987,6 +1045,15 @@ async function main() {
987
1045
  // pipeline. User can opt in later via `construct config intake.includeProjectInbox=true`.
988
1046
 
989
1047
  if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
1048
+
1049
+ // Archetype: flip includeArchetypeInbox on so resolveInboxDirs picks up
1050
+ // the project-root inbox/ drop zone alongside .cx/inbox/. Mirrors the
1051
+ // existing includeProjectInbox / includeDocsIntake toggles rather than
1052
+ // polluting parentDirs (which is reserved for user-explicit dirs).
1053
+
1054
+ if (intakeCap?.inbox) {
1055
+ intakeConfig.includeArchetypeInbox = true;
1056
+ }
990
1057
  const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
991
1058
  try {
992
1059
  saveIntakeConfig(target, intakeConfig);