@geraldmaron/construct 1.2.0 → 1.2.2

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 (214) hide show
  1. package/README.md +8 -7
  2. package/bin/construct +206 -62
  3. package/commands/plan/decide.md +1 -1
  4. package/commands/remember/runbook.md +1 -1
  5. package/examples/README.md +1 -1
  6. package/examples/distribution/README.md +1 -1
  7. package/examples/distribution/sources/adr.md +2 -2
  8. package/examples/distribution/sources/deck-one-pager.md +1 -1
  9. package/examples/distribution/sources/prd-platform.md +1 -1
  10. package/examples/distribution/sources/research-brief.md +2 -2
  11. package/examples/distribution/sources/rfc-platform.md +1 -1
  12. package/examples/distribution/sources/runbook.md +2 -2
  13. package/examples/distribution/sources/strategy.md +1 -1
  14. package/lib/agent-instructions/inject.mjs +1 -1
  15. package/lib/artifact-manifest.mjs +150 -4
  16. package/lib/artifact-type-from-path.mjs +9 -5
  17. package/lib/artifact-workflow.mjs +252 -0
  18. package/lib/audit-specialists.mjs +1 -1
  19. package/lib/audit-trail.mjs +3 -3
  20. package/lib/auto-docs.mjs +13 -11
  21. package/lib/bootstrap/built-ins.mjs +3 -3
  22. package/lib/boundary.mjs +3 -2
  23. package/lib/brand-prose.mjs +1 -1
  24. package/lib/cache-governor.js +4 -4
  25. package/lib/certification/document-io-fixtures.mjs +1 -1
  26. package/lib/chat/config.mjs +2 -2
  27. package/lib/chat/desktop-binary.mjs +5 -3
  28. package/lib/chat/present.mjs +1 -0
  29. package/lib/cli-commands.mjs +23 -6
  30. package/lib/cli-service-inventory.mjs +42 -0
  31. package/lib/comment-lint.mjs +9 -5
  32. package/lib/config/alias.mjs +3 -2
  33. package/lib/config/intake-policy.mjs +139 -0
  34. package/lib/config/project-config.mjs +106 -7
  35. package/lib/config/schema.mjs +45 -6
  36. package/lib/config/source-targets.mjs +311 -0
  37. package/lib/config/xdg.mjs +63 -0
  38. package/lib/contracts/violation-log.mjs +2 -2
  39. package/lib/cost-ledger.mjs +4 -3
  40. package/lib/cost.mjs +3 -2
  41. package/lib/decisions/registry.mjs +3 -3
  42. package/lib/distill.mjs +2 -2
  43. package/lib/docs-verify.mjs +2 -0
  44. package/lib/doctor/audit.mjs +4 -3
  45. package/lib/doctor/cli.mjs +0 -1
  46. package/lib/doctor/index.mjs +4 -4
  47. package/lib/doctor/report.mjs +3 -3
  48. package/lib/doctor/source-checkout.mjs +16 -0
  49. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  50. package/lib/doctor/watchers/cost.mjs +4 -4
  51. package/lib/doctor/watchers/disk.mjs +4 -3
  52. package/lib/doctor/watchers/service-health.mjs +5 -5
  53. package/lib/document-export.mjs +39 -17
  54. package/lib/document-extract.mjs +3 -3
  55. package/lib/document-ingest.mjs +13 -3
  56. package/lib/efficiency.mjs +2 -1
  57. package/lib/embed/approval-queue.mjs +3 -2
  58. package/lib/embed/auto-sources.mjs +44 -0
  59. package/lib/embed/cli.mjs +9 -6
  60. package/lib/embed/config.mjs +6 -5
  61. package/lib/embed/daemon.mjs +6 -5
  62. package/lib/embed/demand-fetch.mjs +50 -19
  63. package/lib/embed/inbox-live-watcher.mjs +1 -1
  64. package/lib/embed/inbox.mjs +52 -41
  65. package/lib/embed/intake-metrics.mjs +3 -2
  66. package/lib/embed/reconcile.mjs +1 -1
  67. package/lib/embed/semantic.mjs +3 -2
  68. package/lib/embed/supervision.mjs +4 -2
  69. package/lib/embed/worker.mjs +2 -1
  70. package/lib/embedded-contract/index.mjs +11 -0
  71. package/lib/engine/registry.mjs +3 -3
  72. package/lib/env-config.mjs +34 -12
  73. package/lib/export-branding.mjs +32 -0
  74. package/lib/features.mjs +4 -3
  75. package/lib/health-check.mjs +7 -6
  76. package/lib/hook-health.mjs +5 -4
  77. package/lib/hooks/_lib/log.mjs +4 -3
  78. package/lib/hooks/_lib/output-mode.mjs +2 -1
  79. package/lib/hooks/agent-tracker.mjs +3 -4
  80. package/lib/hooks/audit-reads.mjs +2 -2
  81. package/lib/hooks/audit-trail.mjs +2 -2
  82. package/lib/hooks/bash-output-logger.mjs +3 -3
  83. package/lib/hooks/brand-prose-lint.mjs +1 -1
  84. package/lib/hooks/ci-status-check.mjs +3 -2
  85. package/lib/hooks/context-watch.mjs +2 -2
  86. package/lib/hooks/context-window-recovery.mjs +5 -5
  87. package/lib/hooks/dep-audit.mjs +2 -2
  88. package/lib/hooks/doc-coupling-check.mjs +3 -2
  89. package/lib/hooks/edit-accumulator.mjs +3 -2
  90. package/lib/hooks/edit-guard.mjs +3 -3
  91. package/lib/hooks/guard-bash.mjs +2 -2
  92. package/lib/hooks/model-fallback.mjs +2 -1
  93. package/lib/hooks/policy-engine.mjs +4 -3
  94. package/lib/hooks/pre-compact.mjs +7 -7
  95. package/lib/hooks/readme-age-check.mjs +2 -2
  96. package/lib/hooks/session-optimize.mjs +4 -3
  97. package/lib/hooks/session-start.mjs +20 -16
  98. package/lib/hooks/stop-notify.mjs +9 -8
  99. package/lib/hooks/stop-typecheck.mjs +4 -3
  100. package/lib/hooks/test-watch.mjs +2 -2
  101. package/lib/host-disposition.mjs +1 -1
  102. package/lib/ingest/provider-extract.mjs +2 -1
  103. package/lib/init/detect-existing-structure.mjs +2 -2
  104. package/lib/init/doc-lanes.mjs +3 -10
  105. package/lib/init-docs.mjs +12 -54
  106. package/lib/init-unified.mjs +47 -92
  107. package/lib/init.mjs +7 -5
  108. package/lib/install/desktop-binary-download.mjs +5 -2
  109. package/lib/intake/constants.mjs +29 -0
  110. package/lib/intake/daemon.mjs +31 -8
  111. package/lib/intake/intake-config.mjs +32 -112
  112. package/lib/integrations/intake-integrations.mjs +8 -7
  113. package/lib/knowledge/rag.mjs +2 -2
  114. package/lib/knowledge/search.mjs +15 -15
  115. package/lib/maintenance/cleanup.mjs +14 -10
  116. package/lib/mcp/server.mjs +20 -2
  117. package/lib/mcp/tools/embedded-contract.mjs +14 -0
  118. package/lib/mcp/tools/project.mjs +2 -1
  119. package/lib/model-cheapest-provider.mjs +4 -3
  120. package/lib/model-pricing.mjs +3 -2
  121. package/lib/model-router.mjs +4 -3
  122. package/lib/models/catalog.mjs +2 -1
  123. package/lib/models/execution-capability-profile.mjs +2 -1
  124. package/lib/models/provider-poll.mjs +2 -1
  125. package/lib/ollama-manager.mjs +0 -1
  126. package/lib/op-log.mjs +2 -1
  127. package/lib/opencode-runtime-plugin.mjs +3 -2
  128. package/lib/oracle/artifact-gate.mjs +3 -0
  129. package/lib/oracle/cli.mjs +2 -1
  130. package/lib/oracle/execute.mjs +3 -2
  131. package/lib/oracle/index.mjs +2 -1
  132. package/lib/oracle/read-model.mjs +3 -2
  133. package/lib/performance/generate.mjs +4 -3
  134. package/lib/platforms/capabilities.mjs +1 -1
  135. package/lib/playwright-demo.mjs +2 -0
  136. package/lib/plugin-registry.mjs +2 -1
  137. package/lib/profiles/lifecycle.mjs +3 -3
  138. package/lib/project-profile.mjs +2 -1
  139. package/lib/project-root.mjs +9 -7
  140. package/lib/provider-capabilities.js +4 -3
  141. package/lib/providers/auth-manager.mjs +2 -1
  142. package/lib/providers/copilot-auth.mjs +3 -2
  143. package/lib/providers/creds.mjs +3 -2
  144. package/lib/providers/registry.mjs +3 -3
  145. package/lib/providers/secret-resolver.mjs +3 -2
  146. package/lib/read-tracker-store.mjs +2 -1
  147. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  148. package/lib/reflect.mjs +2 -1
  149. package/lib/registry/generate-docs.mjs +2 -2
  150. package/lib/roles/approval-surface.mjs +5 -4
  151. package/lib/roles/event-bus.mjs +0 -1
  152. package/lib/roles/gateway.mjs +6 -2
  153. package/lib/rules-delivery.mjs +1 -1
  154. package/lib/runtime-pressure.mjs +5 -3
  155. package/lib/sandbox.mjs +3 -2
  156. package/lib/scheduler/solo.mjs +6 -4
  157. package/lib/server/auth.mjs +4 -3
  158. package/lib/server/index.mjs +53 -26
  159. package/lib/server/insights.mjs +5 -4
  160. package/lib/server/rate-limit.mjs +5 -3
  161. package/lib/server/webhook.mjs +5 -3
  162. package/lib/service-manager.mjs +5 -3
  163. package/lib/setup.mjs +9 -8
  164. package/lib/status.mjs +6 -5
  165. package/lib/storage/embeddings-local.mjs +3 -2
  166. package/lib/storage/sync.mjs +2 -2
  167. package/lib/telemetry/client.mjs +0 -1
  168. package/lib/telemetry/intent-verifications.mjs +6 -6
  169. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  170. package/lib/telemetry/rule-calls.mjs +3 -3
  171. package/lib/telemetry/skill-calls.mjs +3 -3
  172. package/lib/template-registry.mjs +1 -0
  173. package/lib/templates/visual-requirements.mjs +1 -1
  174. package/lib/test-corpus-inventory.mjs +1 -1
  175. package/lib/uninstall/uninstall.mjs +12 -10
  176. package/package.json +3 -3
  177. package/platforms/claude/settings.template.json +43 -43
  178. package/rules/common/no-fabrication.md +1 -1
  179. package/rules/common/release-gates.md +1 -1
  180. package/rules/common/research.md +1 -1
  181. package/rules/common/review-before-change.md +1 -1
  182. package/scripts/sync-specialists.mjs +11 -8
  183. package/skills/docs/adr-workflow.md +3 -3
  184. package/skills/docs/init-docs.md +9 -9
  185. package/skills/docs/prd-workflow.md +5 -5
  186. package/skills/docs/product-intelligence-review.md +1 -1
  187. package/skills/docs/product-intelligence-workflow.md +2 -2
  188. package/skills/docs/product-signal-workflow.md +1 -1
  189. package/skills/docs/runbook-workflow.md +4 -4
  190. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  191. package/skills/operating/orchestration-reference.md +1 -1
  192. package/skills/routing.md +3 -3
  193. package/specialists/artifact-manifest.json +11 -2
  194. package/specialists/artifact-manifest.schema.json +31 -1
  195. package/specialists/prompts/cx-architect.md +1 -1
  196. package/specialists/prompts/cx-docs-keeper.md +1 -1
  197. package/specialists/prompts/cx-researcher.md +1 -1
  198. package/specialists/prompts/cx-sre.md +1 -1
  199. package/specialists/role-manifests.json +6 -6
  200. package/templates/distribution/construct-reference.docx +0 -0
  201. package/templates/docs/README.md +125 -0
  202. package/templates/docs/construct_guide.md +3 -4
  203. package/templates/docs/persona-artifact.md +1 -1
  204. package/templates/docs/prds/README.md +25 -0
  205. package/templates/docs/prds/templates/_template.md +206 -0
  206. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  207. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  208. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  209. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  210. package/templates/docs/rfcs/README.md +22 -0
  211. package/templates/docs/rfcs/templates/_template.md +58 -0
  212. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  213. package/templates/homebrew/construct.rb +1 -1
  214. package/templates/workflows/new-feature.yml +8 -8
@@ -17,11 +17,11 @@
17
17
 
18
18
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
19
19
  import { execSync } from 'node:child_process';
20
- import { homedir } from 'node:os';
21
20
  import { join } from 'node:path';
22
21
  import { emitRoleEvent } from '../roles/hook-emit.mjs';
22
+ import { doctorRoot } from '../config/xdg.mjs';
23
23
 
24
- const STATE_PATH = join(homedir(), '.cx', 'readme-age-state.json');
24
+ const STATE_PATH = join(doctorRoot(), 'readme-age-state.json');
25
25
  const STALE_DAYS = 90;
26
26
  const SUPPRESS_DAYS = 7;
27
27
 
@@ -14,18 +14,19 @@
14
14
  */
15
15
  import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from 'fs';
16
16
  import { join, dirname } from 'path';
17
- import { homedir } from 'os';
18
17
  import { spawn } from 'child_process';
19
18
  import { fileURLToPath } from 'url';
20
19
 
20
+ import { doctorRoot } from '../config/xdg.mjs';
21
+
21
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
23
  const CONSTRUCT_BIN = join(__dirname, '..', '..', 'bin', 'construct');
23
24
 
24
25
  const OPTIMIZATION_THRESHOLD = 0.7;
25
26
  const MIN_TRACES_PER_AGENT = 3;
26
27
  const OPTIMIZATION_DAYS = 7;
27
- const OPTIMIZATION_LOG_DIR = join(homedir(), '.cx', 'optimization-logs');
28
- const REVIEWS_DIR = join(homedir(), '.cx', 'performance-reviews');
28
+ const OPTIMIZATION_LOG_DIR = join(doctorRoot(), 'optimization-logs');
29
+ const REVIEWS_DIR = join(doctorRoot(), 'performance-reviews');
29
30
 
30
31
  mkdirSync(OPTIMIZATION_LOG_DIR, { recursive: true });
31
32
 
@@ -24,6 +24,7 @@ import { listObservations, searchObservations } from '../observation-store.mjs';
24
24
  import { countEntities } from '../entity-store.mjs';
25
25
  import { logHookFailure } from './_lib/log.mjs';
26
26
  import { resolveHookOutputMode, writeHookContext } from './_lib/output-mode.mjs';
27
+ import { doctorRoot } from '../config/xdg.mjs';
27
28
 
28
29
  const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
29
30
  const CONSTRUCT_BIN = resolve(MODULE_DIR, '..', '..', 'bin', 'construct');
@@ -64,11 +65,11 @@ function fileIsFresh(path) {
64
65
  // stale context files inject only the digest line as fallback (Tier 2 → Tier 1
65
66
  // degradation), full body only when modified within the freshness window.
66
67
  const localCtx = join(cwd, '.cx', 'context.md');
67
- const globalCtx = join(homedir(), '.cx', 'context.md');
68
+ const globalCtx = join(doctorRoot(), '.cx', 'context.md');
68
69
  let context = '';
69
70
  let contextStale = false;
70
71
  const localState = readContextState(cwd);
71
- const globalState = localState ? null : readContextState(homedir());
72
+ const globalState = localState ? null : readContextState(doctorRoot());
72
73
  const stateContext = localState || globalState;
73
74
  const stateContextPath = localState ? localCtx : globalCtx;
74
75
  if (stateContext?.markdown) {
@@ -151,7 +152,7 @@ try {
151
152
 
152
153
  // Record memory injection stats for the Stop hook to persist into .cx/memory-stats.jsonl.
153
154
  try {
154
- const memStatsPath = join(homedir(), '.cx', 'session-memory-stats.json');
155
+ const memStatsPath = join(doctorRoot(), 'session-memory-stats.json');
155
156
  const injected = observationsNote ? (observationsNote.match(/^- /gm) || []).length : 0;
156
157
  writeFileSync(memStatsPath, JSON.stringify({
157
158
  project: projectName,
@@ -162,7 +163,7 @@ try {
162
163
  } catch { /* best effort */ }
163
164
 
164
165
  // Pending typecheck warning
165
- const tcPath = join(homedir(), '.cx', 'pending-typecheck.txt');
166
+ const tcPath = join(doctorRoot(), 'pending-typecheck.txt');
166
167
  let pendingNote = '';
167
168
  try {
168
169
  const pending = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
@@ -265,28 +266,31 @@ const selfKnowledgeNote = '\nSelf-knowledge: call `knowledge_search` for any Con
265
266
  let sourcesNote = '';
266
267
  let embedStatusNote = '';
267
268
  try {
268
- const repos = (process.env.GITHUB_REPOS ?? '').split(',').map(r => r.trim()).filter(Boolean);
269
- const jiraConfigured = !!(process.env.JIRA_BASE_URL);
270
- const linearConfigured = !!(process.env.LINEAR_API_KEY);
271
- const hasSources = repos.length > 0 || jiraConfigured || linearConfigured;
269
+ const { loadProjectConfig } = await import('../config/project-config.mjs');
270
+ const { resolveEffectiveSourceTargetsFromConfig } = await import('../config/source-targets.mjs');
271
+ const { config } = loadProjectConfig(process.cwd(), process.env);
272
+ const targets = resolveEffectiveSourceTargetsFromConfig(config, process.env);
273
+ const repos = targets.filter((t) => t.provider === 'github').map((t) => t.selector.repo);
274
+ const jiraTargets = targets.filter((t) => t.provider === 'jira');
275
+ const linearTargets = targets.filter((t) => t.provider === 'linear');
276
+ const slackTargets = targets.filter((t) => t.provider === 'slack');
277
+ const hasSources = targets.length > 0;
272
278
  if (hasSources) {
273
279
  const parts = [];
274
280
  if (repos.length > 0) parts.push(`GitHub repos: ${repos.join(', ')}`);
275
- if (jiraConfigured) parts.push('Jira: configured');
276
- if (linearConfigured) parts.push('Linear: configured');
277
- // Surface which sources are wired so `provider_fetch` is the obvious
278
- // tool for questions about them. The "fetch first, don't answer from
279
- // memory" policy lives in the persona.
281
+ if (jiraTargets.length > 0) parts.push(`Jira projects: ${jiraTargets.map((t) => t.selector.project).join(', ')}`);
282
+ if (linearTargets.length > 0) parts.push(`Linear teams: ${linearTargets.map((t) => t.selector.team).join(', ')}`);
283
+ if (slackTargets.length > 0) parts.push(`Slack channels: ${slackTargets.map((t) => t.selector.channel).join(', ')}`);
280
284
  sourcesNote = '\nProvider sources wired: ' + parts.join(' · ') +
281
285
  '. Use `provider_fetch` for any question about them.\n';
282
286
  }
287
+ } catch { /* best effort */ }
283
288
 
284
- // Embed daemon status — surface as a one-liner so operator always knows state
289
+ try {
285
290
  const { resolveEmbedStatus, autoStartEmbedIfNeeded } = await import('../embed/cli.mjs');
286
291
  const embedStatus = resolveEmbedStatus(process.env);
287
292
  if (embedStatus.level !== 'none') {
288
293
  embedStatusNote = `\n## Embed daemon\n${embedStatus.label} · ${embedStatus.detail}\n`;
289
- // Auto-start if CX_AUTO_EMBED=1 and daemon is stopped
290
294
  if (embedStatus.level === 'stopped' && process.env.CX_AUTO_EMBED === '1') {
291
295
  const result = await autoStartEmbedIfNeeded(process.env);
292
296
  if (result.started) {
@@ -397,7 +401,7 @@ try {
397
401
  try {
398
402
  const sessionId = input?.session_id || input?.sessionId;
399
403
  if (sessionId) {
400
- const bootstrapPath = join(homedir(), ".cx", "bootstrap-state.json");
404
+ const bootstrapPath = join(doctorRoot(), "bootstrap-state.json");
401
405
  let state = {};
402
406
  try { state = JSON.parse(readFileSync(bootstrapPath, "utf8")) || {}; } catch {}
403
407
  const now = Date.now();
@@ -27,6 +27,7 @@ import { captureSessionArtifacts } from '../artifact-capture.mjs';
27
27
  import { appendSessionStats } from '../memory-stats.mjs';
28
28
  import { estimateUsageCost } from '../telemetry/model-pricing-catalog.mjs';
29
29
  import { flushReadTrackerDeltas } from '../read-tracker-store.mjs';
30
+ import { doctorRoot } from '../config/xdg.mjs';
30
31
 
31
32
  function loadTranscriptCheckpoints(checkpointPath) {
32
33
  try {
@@ -148,12 +149,12 @@ if (raw) process.stdout.write(raw);
148
149
  try { flushReadTrackerDeltas({ env: process.env }); } catch { /* non-critical */ }
149
150
 
150
151
  const home = homedir();
151
- const tsResultPath = join(home, '.cx', 'ts-result.txt');
152
- const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
153
- const countPath = join(home, '.cx', 'files-changed-count.txt');
154
- const costLogPath = join(home, '.cx', 'session-cost.jsonl');
155
- const lastAgentPath = join(home, '.cx', 'last-agent.json');
156
- const checkpointPath = join(home, '.cx', 'transcript-checkpoints.json');
152
+ const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
153
+ const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
154
+ const countPath = join(doctorRoot(home), 'files-changed-count.txt');
155
+ const costLogPath = join(doctorRoot(home), 'session-cost.jsonl');
156
+ const lastAgentPath = join(doctorRoot(home), 'last-agent.json');
157
+ const checkpointPath = join(doctorRoot(home), 'transcript-checkpoints.json');
157
158
 
158
159
  // Consume-clear like warn-flags/files-changed below: a TS result is reported
159
160
  // once, not re-fired on every subsequent no-op Stop. stop-typecheck rewrites
@@ -304,7 +305,7 @@ try {
304
305
  }
305
306
  } catch { /* non-critical */ }
306
307
 
307
- const memStatsPath = join(home, '.cx', 'session-memory-stats.json');
308
+ const memStatsPath = join(doctorRoot(home), 'session-memory-stats.json');
308
309
  try {
309
310
  if (existsSync(memStatsPath)) {
310
311
  const memStats = JSON.parse(readFileSync(memStatsPath, 'utf8'));
@@ -323,7 +324,7 @@ try {
323
324
  // repeat Stop events within the window to avoid review-spam.
324
325
  try {
325
326
  const homeDir = process.env.HOME || homedir();
326
- const stampPath = join(homeDir, '.cx', 'performance-reviews', '.last-run');
327
+ const stampPath = join(doctorRoot(homeDir), 'performance-reviews', '.last-run');
327
328
  let lastRun = 0;
328
329
  try { lastRun = Number(readFileSync(stampPath, 'utf8')) || 0; } catch { /* fresh */ }
329
330
  if (Date.now() - lastRun > 24 * 60 * 60 * 1000) {
@@ -15,6 +15,7 @@ import { readFileSync, writeFileSync, existsSync } from 'fs';
15
15
  import { execSync } from 'child_process';
16
16
  import { homedir } from 'os';
17
17
  import { join, dirname } from 'path';
18
+ import { doctorRoot } from '../config/xdg.mjs';
18
19
 
19
20
  // Stop hooks must echo stdin to stdout
20
21
  let raw = '';
@@ -22,9 +23,9 @@ try { raw = readFileSync(0, 'utf8'); } catch { /* no stdin */ }
22
23
  if (raw) process.stdout.write(raw);
23
24
 
24
25
  const home = homedir();
25
- const tcPath = join(home, '.cx', 'pending-typecheck.txt');
26
- const tsResultPath = join(home, '.cx', 'ts-result.txt');
27
- const warnFlagsPath = join(home, '.cx', 'warn-flags.txt');
26
+ const tcPath = join(doctorRoot(home), 'pending-typecheck.txt');
27
+ const tsResultPath = join(doctorRoot(home), 'ts-result.txt');
28
+ const warnFlagsPath = join(doctorRoot(home), 'warn-flags.txt');
28
29
 
29
30
  // Read pending files
30
31
  const pending = existsSync(tcPath)
@@ -15,10 +15,10 @@
15
15
  */
16
16
 
17
17
  import { readFileSync, existsSync, writeFileSync } from 'node:fs';
18
- import { homedir } from 'node:os';
19
18
  import { join } from 'node:path';
20
19
  import { logHookFailure } from './_lib/log.mjs';
21
20
  import { emitRoleEvent } from '../roles/hook-emit.mjs';
21
+ import { doctorRoot } from '../config/xdg.mjs';
22
22
 
23
23
  let input = {};
24
24
  try { input = JSON.parse(readFileSync(0, 'utf8')); }
@@ -44,7 +44,7 @@ emitRoleEvent({
44
44
  context: { command, exitCode, failedNames },
45
45
  });
46
46
 
47
- const flakeTrackerPath = join(homedir(), '.cx', 'test-watch-state.json');
47
+ const flakeTrackerPath = join(doctorRoot(), 'test-watch-state.json');
48
48
  try {
49
49
  const state = existsSync(flakeTrackerPath) ? JSON.parse(readFileSync(flakeTrackerPath, 'utf8')) : {};
50
50
  const now = Date.now();
@@ -2,7 +2,7 @@
2
2
  * lib/host-disposition.mjs — single source of truth for the disposition of
3
3
  * every artifact Construct creates in a host project (ADR-0027 §1).
4
4
  *
5
- * Scope model (see docs/concepts/project-scopes.md):
5
+ * Scope model (see docs/guides/concepts/project-scopes.md):
6
6
  * templates/, docs/ — package/repo durable sources (committed)
7
7
  * .construct/ — per-project launcher (ignored)
8
8
  * .cx/ — per-project runtime: intake, oracle, observations, demos (ignored)
@@ -24,6 +24,7 @@
24
24
  import { readFileSync, existsSync } from 'node:fs';
25
25
  import { extname, join } from 'node:path';
26
26
  import { homedir } from 'node:os';
27
+ import { configDir } from '../config/xdg.mjs';
27
28
 
28
29
  import {
29
30
  AUDIO_VIDEO_EXTS,
@@ -75,7 +76,7 @@ function isAnthropic(provider, model) {
75
76
  function resolveKey(varName, env, allowAmbient) {
76
77
  if (env[varName] && typeof env[varName] === 'string' && env[varName].length > 0) return env[varName];
77
78
  if (!allowAmbient) return null;
78
- for (const file of [join(homedir(), '.construct', 'config.env'), join(homedir(), '.env')]) {
79
+ for (const file of [join(configDir(), 'config.env'), join(homedir(), '.env')]) {
79
80
  try {
80
81
  if (!existsSync(file)) continue;
81
82
  const m = readFileSync(file, 'utf8').match(new RegExp(`^${varName}=["']?(.+?)["']?$`, 'm'));
@@ -12,13 +12,13 @@
12
12
  *
13
13
  * Heuristics intentionally err on the side of "skip" — a directory is treated
14
14
  * as a real lane only when it carries at least one markdown file. An empty
15
- * docs/meetings/ that an earlier init scaffolded does NOT register; that lets
15
+ * docs/notes/meetings/ that an earlier init scaffolded does NOT register; that lets
16
16
  * re-running init be a no-op rather than a contradictory "skip what we just
17
17
  * created" message.
18
18
  *
19
19
  * Issue #97 motivates this: `construct init` on an existing project with
20
20
  * `internal/meetings/`, a custom `ingest` script, and root-level `templates/`
21
- * created `docs/meetings/`, `.cx/inbox/`, and per-lane `templates/` folders
21
+ * created `docs/notes/meetings/`, `inbox/`, and per-lane `templates/` folders
22
22
  * that conflicted with the existing workflow.
23
23
  */
24
24
 
@@ -32,12 +32,6 @@ export const DOC_LANES = {
32
32
  description: 'User-facing release notes and version history entries.',
33
33
  templates: ['changelog-entry.md'],
34
34
  },
35
- intake: {
36
- title: 'Intake',
37
- dir: 'intake',
38
- description: 'Intake batch records that explain what arrived, why it matters, and how it should be ingested.',
39
- templates: ['__intake-template__'],
40
- },
41
35
  memos: {
42
36
  title: 'Memos',
43
37
  dir: 'memos',
@@ -88,11 +82,11 @@ export const DOC_LANES = {
88
82
  },
89
83
  };
90
84
 
91
- export const LANE_ORDER = ['adrs', 'briefs', 'changelogs', 'intake', 'memos', 'meetings', 'notes', 'onboarding', 'postmortems', 'prds', 'rfcs', 'runbooks'];
85
+ export const LANE_ORDER = ['adrs', 'briefs', 'changelogs', 'memos', 'meetings', 'notes', 'onboarding', 'postmortems', 'prds', 'rfcs', 'runbooks'];
92
86
 
93
87
  export const DOC_PRESETS = {
94
- lean: ['adrs', 'intake', 'memos', 'meetings', 'notes', 'prds'],
95
- product: ['adrs', 'intake', 'memos', 'meetings', 'notes', 'prds', 'rfcs'],
88
+ lean: ['adrs', 'memos', 'meetings', 'notes', 'prds'],
89
+ product: ['adrs', 'memos', 'meetings', 'notes', 'prds', 'rfcs'],
96
90
  full: LANE_ORDER,
97
91
  };
98
92
 
@@ -107,7 +101,6 @@ export const LANE_ALIASES = {
107
101
  changelogs: 'changelogs',
108
102
  releases: 'changelogs',
109
103
  release: 'changelogs',
110
- intake: 'intake',
111
104
  memo: 'memos',
112
105
  memos: 'memos',
113
106
  meeting: 'meetings',
package/lib/init-docs.mjs CHANGED
@@ -3,8 +3,8 @@
3
3
  * lib/init-docs.mjs — stand up a documentation system for a project.
4
4
  *
5
5
  * Intentionally separate from `construct init`. Creates the docs surface only:
6
- * docs/README.md plus selected lane directories such as docs/adr/, docs/intake/,
7
- * docs/memos/, docs/notes/, docs/prds/, and docs/rfcs/ with starter templates
6
+ * docs/README.md plus selected lane directories such as docs/decisions/adr/, docs/intake/,
7
+ * docs/notes/memos/, docs/notes/, templates/docs/prds/, and templates/docs/rfcs/ with starter templates
8
8
  * copied into per-lane templates/ directories from Construct's template library.
9
9
  *
10
10
  * Usage:
@@ -290,15 +290,6 @@ function buildLaneReadme(laneKey) {
290
290
  const filename = index === 0 ? "_template.md" : templateName.replace(/\.md$/, ".template.md");
291
291
  return `- [${filename}](./templates/${filename})`;
292
292
  });
293
- const usageSection = laneKey === "intake"
294
- ? `
295
- ## Intake flow
296
-
297
- - Drop source files into either [\`.cx/inbox/\`](../../.cx/inbox/) or this lane's directory when you want Construct to ingest them.
298
- - Run \`construct ingest ./docs/intake --sync\` or \`construct ingest ./.cx/inbox --sync\` to convert supported files into retrieval-ready markdown, or let the embed daemon watch those drop zones automatically.
299
- - Durable ingested knowledge lands under \`.cx/knowledge/internal/\` by default, which is where Construct's learning and search paths already operate.
300
- `
301
- : "";
302
293
 
303
294
  return `<!--
304
295
  docs/${dirName}/README.md — lane guide for ${title}.
@@ -314,7 +305,6 @@ ${description}
314
305
  ## Starter templates
315
306
 
316
307
  ${templateLines.join("\n")}
317
- ${usageSection}
318
308
  `;
319
309
  }
320
310
 
@@ -432,39 +422,6 @@ Promote durable decisions into ADRs, memos, or PRDs when needed.
432
422
  `;
433
423
  }
434
424
 
435
- function buildIntakeTemplate() {
436
- return `<!--
437
- docs/intake/templates/_template.md — starter template for logging an intake batch.
438
-
439
- Use this when you want a durable record of what was dropped into intake and why.
440
- Raw source files belong in .cx/inbox/ until they are ingested.
441
- -->
442
-
443
- # Intake Batch: {title}
444
-
445
- - **Date**: {YYYY-MM-DD}
446
- - **Owner**: {name}
447
- - **Source**: {vendor, teammate, export, upload}
448
-
449
- ## What arrived
450
-
451
- <!-- Files, folders, or links dropped into .cx/inbox/. -->
452
-
453
- ## Why it matters
454
-
455
- <!-- Why Construct should ingest this material. -->
456
-
457
- ## Ingest plan
458
-
459
- - Run: \`construct ingest ./.cx/inbox --sync\`
460
- - Target: \`.cx/knowledge/internal/\` unless a different knowledge target is more appropriate
461
-
462
- ## Notes
463
-
464
- <!-- Caveats, access concerns, or cleanup notes. -->
465
- `;
466
- }
467
-
468
425
  function titleCase(value) {
469
426
  return value
470
427
  .split(/[-_\s]+/)
@@ -532,7 +489,6 @@ function copyLaneTemplates(laneKey) {
532
489
  const content =
533
490
  templateName === "__notes-template__" ? buildNotesTemplate()
534
491
  : templateName === '__meeting-notes-template__' ? buildMeetingNotesTemplate()
535
- : templateName === "__intake-template__" ? buildIntakeTemplate()
536
492
  : fs.readFileSync(path.join(TEMPLATE_DIR, templateName), "utf8");
537
493
  writeIfMissing(path.join(laneRoot, "templates", outputName), content);
538
494
  }
@@ -632,14 +588,16 @@ async function main() {
632
588
  if (withArchitecture) {
633
589
  writeIfMissing(path.join(docsDir, "architecture.md"), buildArchitectureDoc(projectName, allScaffoldedLaneKeys));
634
590
  }
635
- if (lanesToScaffold.includes('intake')) {
636
- const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
637
- if (inboxDecision.skip) {
638
- process.stdout.write(`[init:docs] skipping .cx/inbox/ ${inboxDecision.reason}. Run with --force to scaffold anyway.\n`);
639
- skipped.push('.cx/inbox/ (deferred to existing intake)');
640
- } else {
641
- fs.mkdirSync(path.join(target, '.cx', 'inbox'), { recursive: true });
642
- }
591
+ const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
592
+ if (inboxDecision.skip) {
593
+ process.stdout.write(`[init:docs] skipping inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.\n`);
594
+ skipped.push('inbox/ (deferred to existing intake)');
595
+ } else {
596
+ // Single canonical drop zone (ADR-0045 §C): the project-root inbox/, with
597
+ // a gitignored .staging/ for atomic rename-in handoff. Independent of any
598
+ // docs lane.
599
+
600
+ fs.mkdirSync(path.join(target, 'inbox', '.staging'), { recursive: true });
643
601
  }
644
602
 
645
603
  for (const { laneKey, reason } of deferredLanes) {
@@ -175,10 +175,7 @@ function detectProjectType(targetPath) {
175
175
  function suggestLanes(targetPath, projectType) {
176
176
  const suggestions = [];
177
177
  const files = fs.readdirSync(targetPath);
178
-
179
- // Always suggest intake for collecting feedback
180
- suggestions.push({ lane: 'intake', reason: 'Collect incoming requests and feedback', recommended: true });
181
-
178
+
182
179
  // ADRs for any non-trivial project
183
180
  if (projectType.type !== 'docs') {
184
181
  suggestions.push({ lane: 'adrs', reason: 'Track architectural decisions', recommended: true });
@@ -365,33 +362,6 @@ Attendees: {{attendees}}
365
362
  - [ ] @owner: Task description (due: {{date}})`;
366
363
  }
367
364
 
368
- function buildIntakeTemplate() {
369
- return `# Intake: {{source}}
370
-
371
- Received: {{date}}
372
- From: {{from}}
373
- Priority: {{priority}}
374
-
375
- ## What arrived
376
-
377
- Brief description of what was received.
378
-
379
- ## Why it matters
380
-
381
- Why this intake item is important to process.
382
-
383
- ## How it should be ingested
384
-
385
- Processing instructions:
386
- - [ ] Step 1
387
- - [ ] Step 2
388
- - [ ] Step 3
389
-
390
- ## Notes
391
-
392
- <!-- Caveats, access concerns, or cleanup notes. -->`;
393
- }
394
-
395
365
  function writeIfMissing(filePath, content) {
396
366
  if (fs.existsSync(filePath)) {
397
367
  skipped.push(path.relative(target, filePath));
@@ -418,8 +388,6 @@ function copyLaneTemplates(laneKey) {
418
388
  content = buildNotesTemplate();
419
389
  } else if (templateName === '__meeting-notes-template__') {
420
390
  content = buildMeetingNotesTemplate();
421
- } else if (templateName === "__intake-template__") {
422
- content = buildIntakeTemplate();
423
391
  } else {
424
392
  const templatePath = path.join(TEMPLATE_DIR, templateName);
425
393
  if (fs.existsSync(templatePath)) {
@@ -543,8 +511,8 @@ function discoverProjectDirs(targetPath) {
543
511
  // Extra inbox-style directories the user can opt into. These are NOT
544
512
  // source-code, doc, or test directories: they're places where unsorted
545
513
  // signals tend to land (downloads, scratch notes, exported research). The
546
- // built-in `.cx/inbox/` and `docs/intake/` are always watched and not
547
- // listed here.
514
+ // canonical `inbox/` at the project root is always watched and not listed
515
+ // here.
548
516
  //
549
517
  // Historically this list included `src/`, `docs/`, `tests/`, etc., and
550
518
  // `init --yes` auto-enabled every one that existed in the project. That
@@ -555,10 +523,9 @@ function discoverProjectDirs(targetPath) {
555
523
  const INTAKE_DIR_PRESETS = [];
556
524
 
557
525
  async function askIntakeCollection(targetPath, skipInteractive) {
558
- // Non-interactive default: empty parentDirs. The built-in zones
559
- // (.cx/inbox/ and docs/intake/ when present) handle every well-defined
560
- // signal path. Users who want to watch additional directories opt in
561
- // explicitly via `construct intake config set --add-dir=<path>`.
526
+ // Non-interactive default: empty parentDirs. The canonical `inbox/` zone
527
+ // handles every well-defined signal path. Users who want to watch additional
528
+ // directories opt in explicitly via `construct intake config set --add-dir=<path>`.
562
529
  if (skipInteractive) {
563
530
  return null;
564
531
  }
@@ -583,7 +550,7 @@ async function askIntakeCollection(targetPath, skipInteractive) {
583
550
  console.log(' INTAKE COLLECTION');
584
551
  console.log('═══════════════════════════════════════════════════════════');
585
552
  console.log('');
586
- console.log('Intake watches `.cx/inbox/` and `docs/intake/` by default.');
553
+ console.log('Intake watches `inbox/` at the project root.');
587
554
  console.log('Add extra directories ONLY if signals (customer notes, exports,');
588
555
  console.log('PDFs, etc.) regularly land there. Do not select code or finished');
589
556
  console.log('artifact directories like src/, docs/, or tests/.');
@@ -599,7 +566,7 @@ async function askIntakeCollection(targetPath, skipInteractive) {
599
566
  if (selected.length === 0) {
600
567
  console.log('');
601
568
  console.log('No extra directories selected. The inbox watcher will scan');
602
- console.log('.cx/inbox/ and docs/intake/ only. Add more later with:');
569
+ console.log('`inbox/` only. Add more later with:');
603
570
  console.log(' construct intake config set --add-dir=<path>');
604
571
  console.log('');
605
572
  return null;
@@ -866,41 +833,36 @@ async function main() {
866
833
 
867
834
 
868
835
  // Detect existing project content once; the result feeds three decisions
869
- // below: skip .cx/inbox/ on custom intake, skip lane scaffolding for lanes
870
- // already covered elsewhere, and skip per-lane templates/ when root
871
- // templates/ already has them. --force bypasses every check.
836
+ // below: skip the inbox/ drop zone on custom intake, skip lane scaffolding
837
+ // for lanes already covered elsewhere, and skip per-lane templates/ when
838
+ // root templates/ already has them. --force bypasses every check.
872
839
 
873
840
  const { detectExistingContent, shouldSkipProjectInbox, shouldScaffoldLane, formatDeferralSummary } =
874
841
  await import('./init/detect-existing-structure.mjs');
875
842
  const detection = detectExistingContent(target);
876
843
  const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
877
844
 
878
- if (inboxDecision.skip) {
879
- console.log(`[init:intake] skipping .cx/inbox/ ${inboxDecision.reason}. Run with --force to scaffold anyway.`);
880
- skipped.push('.cx/inbox/ (deferred to existing intake)');
881
- } else {
882
- // `.cx/inbox/` is the local intake drop zone the watcher polls; under the gitignored
883
- // `.cx/` tree the directory must exist on disk but needs no keep file.
884
-
885
- fs.mkdirSync(path.join(target, ".cx", "inbox"), { recursive: true });
886
- created.push(".cx/inbox/");
887
- }
888
-
889
- // Intake-archetype scaffolding. When the active profile declares
890
- // capabilities.intake.inbox, scaffold a project-root inbox/ drop zone and seed
891
- // the dedup manifest. The whole inbox/ is ignored via host-disposition
892
- // IGNORED_PATTERNS (ADR-0027 §1), so raw drops never enter source and no local
893
- // keep-file is needed. Existing-structure detection already opted out of
894
- // clobbering a user's pipeline above.
895
-
896
- if (intakeCap?.inbox && !inboxDecision.skip) {
897
- const projectInbox = path.join(target, 'inbox');
898
- if (!fs.existsSync(projectInbox)) {
899
- fs.mkdirSync(projectInbox, { recursive: true });
845
+ // Single canonical drop zone (ADR-0045 §C): the visible project-root `inbox/`.
846
+ // Raw drops stay out of source via host-disposition IGNORED_PATTERNS
847
+ // (ADR-0027 §1). Machine/runtime intake state lives under the gitignored
848
+ // `.cx/intake/` tree, created lazily by the queue on first use — init no
849
+ // longer scaffolds the deprecated `.cx/inbox/` drop zone. Skipped when a
850
+ // custom intake pipeline already owns the project root.
851
+
852
+ if (!inboxDecision.skip) {
853
+ const rootInbox = path.join(target, 'inbox');
854
+ if (!fs.existsSync(rootInbox)) {
855
+ fs.mkdirSync(rootInbox, { recursive: true });
900
856
  created.push('inbox/');
901
857
  }
902
858
 
903
- if (intakeCap.dedup === 'sha256') {
859
+ // Writers assemble files under `inbox/.staging/` and atomically rename them
860
+ // into `inbox/`; the gitignored staging dir keeps a half-written drop
861
+ // invisible to the watcher until it lands.
862
+
863
+ fs.mkdirSync(path.join(rootInbox, '.staging'), { recursive: true });
864
+
865
+ if (intakeCap?.dedup === 'sha256') {
904
866
  const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
905
867
  const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
906
868
  if (!manifestExists) {
@@ -908,6 +870,9 @@ async function main() {
908
870
  created.push(MANIFEST_REL_PATH);
909
871
  }
910
872
  }
873
+ } else {
874
+ console.log(`[init:intake] skipping inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.`);
875
+ skipped.push('inbox/ (deferred to existing intake)');
911
876
  }
912
877
 
913
878
  // Gitignore every Construct-generated artifact whose disposition is `ignored`
@@ -1074,32 +1039,22 @@ async function main() {
1074
1039
  console.warn(`⚠️ Could not inject agent-instruction block: ${err.message}`);
1075
1040
  }
1076
1041
 
1077
- // Intake collectionalways write intake-config.json with safe defaults
1078
- // so the user can inspect what's watched and edit the file directly. Empty
1079
- // parentDirs is the correct default: the inbox watcher always scans
1080
- // .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
1042
+ // Intake policy — write the single-zone model (ADR-0045 §C) so the user can
1043
+ // inspect what's watched and edit it directly. The canonical `inbox/` at the
1044
+ // project root is the only drop zone; empty parentDirs is correct — extra
1045
+ // dirs are opt-in.
1081
1046
 
1082
1047
  const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
1083
- // When a custom intake surface was detected, default includeProjectInbox
1084
- // to false so the watcher does not double-claim with the user's existing
1085
- // pipeline. User can opt in later via `construct config intake.includeProjectInbox=true`.
1086
-
1087
- if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
1088
1048
 
1089
- // Archetype: flip includeArchetypeInbox on so resolveInboxDirs picks up
1090
- // the project-root inbox/ drop zone alongside .cx/inbox/. Mirrors the
1091
- // existing includeProjectInbox / includeDocsIntake toggles rather than
1092
- // polluting parentDirs (which is reserved for user-explicit dirs).
1093
-
1094
- if (intakeCap?.inbox) {
1095
- intakeConfig.includeArchetypeInbox = true;
1096
- }
1097
- const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
1049
+ const { saveIntakePolicy } = await import('./config/intake-policy.mjs');
1098
1050
  try {
1099
- saveIntakeConfig(target, intakeConfig);
1100
- created.push('.cx/intake-config.json');
1051
+ saveIntakePolicy(target, {
1052
+ maxDepth: intakeConfig.maxDepth,
1053
+ additionalDirs: intakeConfig.parentDirs ?? [],
1054
+ });
1055
+ created.push('construct.config.json (intakePolicy)');
1101
1056
  } catch (err) {
1102
- console.warn(`⚠️ Could not write intake config: ${err.message}`);
1057
+ console.warn(`⚠️ Could not write intake policy: ${err.message}`);
1103
1058
  }
1104
1059
 
1105
1060
  // Ask about documentation system
@@ -1114,7 +1069,7 @@ async function main() {
1114
1069
  // Create documentation system if lanes specified
1115
1070
  if (lanes.length > 0) {
1116
1071
  // Filter out lanes that the project already covers elsewhere
1117
- // (issue #97: don't create docs/meetings/ when internal/meetings/
1072
+ // (issue #97: don't create docs/notes/meetings/ when internal/meetings/
1118
1073
  // has 12 markdown files). --force bypasses the filter.
1119
1074
 
1120
1075
  const deferredLanes = [];
@@ -1209,7 +1164,7 @@ async function main() {
1209
1164
  const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
1210
1165
  if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
1211
1166
  console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
1212
- console.log(` The agent can now compare new intake (.cx/inbox/, docs/intake/) against your existing PRDs, ADRs, and notes.`);
1167
+ console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
1213
1168
  }
1214
1169
  } catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
1215
1170
 
@@ -1240,9 +1195,9 @@ async function main() {
1240
1195
  console.log(`${++step}. Edit plan.md`);
1241
1196
  console.log(' Add your current work and tasks');
1242
1197
  console.log('');
1243
- if (lanes.includes('intake')) {
1198
+ if (created.includes('inbox/')) {
1244
1199
  console.log(`${++step}. Use Intake`);
1245
- console.log(' Drop files in .cx/inbox/ for processing');
1200
+ console.log(' Drop files in inbox/ for processing');
1246
1201
  console.log(' Run: construct intake');
1247
1202
  console.log('');
1248
1203
  }