@geraldmaron/construct 1.2.1 → 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 (198) hide show
  1. package/README.md +6 -6
  2. package/bin/construct +41 -67
  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-type-from-path.mjs +9 -5
  16. package/lib/audit-specialists.mjs +1 -1
  17. package/lib/audit-trail.mjs +3 -3
  18. package/lib/auto-docs.mjs +10 -10
  19. package/lib/bootstrap/built-ins.mjs +3 -3
  20. package/lib/boundary.mjs +3 -2
  21. package/lib/brand-prose.mjs +1 -1
  22. package/lib/cache-governor.js +4 -4
  23. package/lib/certification/document-io-fixtures.mjs +1 -1
  24. package/lib/chat/config.mjs +2 -2
  25. package/lib/chat/desktop-binary.mjs +5 -3
  26. package/lib/cli-commands.mjs +3 -3
  27. package/lib/cli-service-inventory.mjs +1 -1
  28. package/lib/comment-lint.mjs +9 -5
  29. package/lib/config/alias.mjs +3 -2
  30. package/lib/config/intake-policy.mjs +8 -78
  31. package/lib/config/project-config.mjs +95 -5
  32. package/lib/config/schema.mjs +9 -19
  33. package/lib/config/xdg.mjs +63 -0
  34. package/lib/contracts/violation-log.mjs +2 -2
  35. package/lib/cost-ledger.mjs +4 -3
  36. package/lib/cost.mjs +3 -2
  37. package/lib/decisions/registry.mjs +3 -3
  38. package/lib/distill.mjs +2 -2
  39. package/lib/docs-verify.mjs +2 -0
  40. package/lib/doctor/audit.mjs +4 -3
  41. package/lib/doctor/cli.mjs +0 -1
  42. package/lib/doctor/index.mjs +4 -4
  43. package/lib/doctor/report.mjs +3 -3
  44. package/lib/doctor/watchers/bd-watch.mjs +5 -5
  45. package/lib/doctor/watchers/cost.mjs +4 -4
  46. package/lib/doctor/watchers/disk.mjs +4 -3
  47. package/lib/doctor/watchers/service-health.mjs +5 -5
  48. package/lib/document-export.mjs +1 -1
  49. package/lib/document-extract.mjs +3 -3
  50. package/lib/document-ingest.mjs +13 -3
  51. package/lib/efficiency.mjs +2 -1
  52. package/lib/embed/approval-queue.mjs +3 -2
  53. package/lib/embed/cli.mjs +7 -5
  54. package/lib/embed/config.mjs +6 -5
  55. package/lib/embed/daemon.mjs +6 -5
  56. package/lib/embed/inbox-live-watcher.mjs +1 -1
  57. package/lib/embed/inbox.mjs +52 -37
  58. package/lib/embed/intake-metrics.mjs +3 -2
  59. package/lib/embed/reconcile.mjs +1 -1
  60. package/lib/embed/semantic.mjs +3 -2
  61. package/lib/embed/supervision.mjs +4 -2
  62. package/lib/engine/registry.mjs +3 -3
  63. package/lib/env-config.mjs +34 -12
  64. package/lib/features.mjs +4 -3
  65. package/lib/health-check.mjs +7 -6
  66. package/lib/hook-health.mjs +5 -4
  67. package/lib/hooks/_lib/log.mjs +4 -3
  68. package/lib/hooks/_lib/output-mode.mjs +2 -1
  69. package/lib/hooks/agent-tracker.mjs +3 -4
  70. package/lib/hooks/audit-reads.mjs +2 -2
  71. package/lib/hooks/audit-trail.mjs +2 -2
  72. package/lib/hooks/bash-output-logger.mjs +3 -3
  73. package/lib/hooks/brand-prose-lint.mjs +1 -1
  74. package/lib/hooks/ci-status-check.mjs +3 -2
  75. package/lib/hooks/context-watch.mjs +2 -2
  76. package/lib/hooks/context-window-recovery.mjs +5 -5
  77. package/lib/hooks/dep-audit.mjs +2 -2
  78. package/lib/hooks/doc-coupling-check.mjs +3 -2
  79. package/lib/hooks/edit-accumulator.mjs +3 -2
  80. package/lib/hooks/edit-guard.mjs +3 -3
  81. package/lib/hooks/guard-bash.mjs +2 -2
  82. package/lib/hooks/model-fallback.mjs +2 -1
  83. package/lib/hooks/policy-engine.mjs +4 -3
  84. package/lib/hooks/pre-compact.mjs +7 -7
  85. package/lib/hooks/readme-age-check.mjs +2 -2
  86. package/lib/hooks/session-optimize.mjs +4 -3
  87. package/lib/hooks/session-start.mjs +6 -5
  88. package/lib/hooks/stop-notify.mjs +9 -8
  89. package/lib/hooks/stop-typecheck.mjs +4 -3
  90. package/lib/hooks/test-watch.mjs +2 -2
  91. package/lib/host-disposition.mjs +1 -1
  92. package/lib/ingest/provider-extract.mjs +2 -1
  93. package/lib/init/detect-existing-structure.mjs +2 -2
  94. package/lib/init/doc-lanes.mjs +3 -10
  95. package/lib/init-docs.mjs +12 -54
  96. package/lib/init-unified.mjs +35 -81
  97. package/lib/init.mjs +7 -5
  98. package/lib/install/desktop-binary-download.mjs +5 -2
  99. package/lib/intake/daemon.mjs +31 -8
  100. package/lib/intake/intake-config.mjs +5 -32
  101. package/lib/integrations/intake-integrations.mjs +8 -7
  102. package/lib/knowledge/rag.mjs +2 -2
  103. package/lib/knowledge/search.mjs +15 -15
  104. package/lib/maintenance/cleanup.mjs +14 -10
  105. package/lib/mcp/server.mjs +1 -1
  106. package/lib/mcp/tools/project.mjs +2 -1
  107. package/lib/model-cheapest-provider.mjs +4 -3
  108. package/lib/model-pricing.mjs +3 -2
  109. package/lib/model-router.mjs +4 -3
  110. package/lib/models/catalog.mjs +2 -1
  111. package/lib/models/execution-capability-profile.mjs +2 -1
  112. package/lib/models/provider-poll.mjs +2 -1
  113. package/lib/ollama-manager.mjs +0 -1
  114. package/lib/op-log.mjs +2 -1
  115. package/lib/opencode-runtime-plugin.mjs +3 -2
  116. package/lib/oracle/artifact-gate.mjs +3 -0
  117. package/lib/oracle/cli.mjs +2 -1
  118. package/lib/oracle/execute.mjs +3 -2
  119. package/lib/oracle/index.mjs +2 -1
  120. package/lib/oracle/read-model.mjs +3 -2
  121. package/lib/performance/generate.mjs +4 -3
  122. package/lib/platforms/capabilities.mjs +1 -1
  123. package/lib/plugin-registry.mjs +2 -1
  124. package/lib/profiles/lifecycle.mjs +3 -3
  125. package/lib/project-profile.mjs +2 -1
  126. package/lib/project-root.mjs +9 -7
  127. package/lib/provider-capabilities.js +4 -3
  128. package/lib/providers/auth-manager.mjs +2 -1
  129. package/lib/providers/copilot-auth.mjs +3 -2
  130. package/lib/providers/creds.mjs +3 -2
  131. package/lib/providers/registry.mjs +3 -3
  132. package/lib/providers/secret-resolver.mjs +3 -2
  133. package/lib/read-tracker-store.mjs +2 -1
  134. package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
  135. package/lib/reflect.mjs +2 -1
  136. package/lib/registry/generate-docs.mjs +2 -2
  137. package/lib/roles/approval-surface.mjs +5 -4
  138. package/lib/roles/event-bus.mjs +0 -1
  139. package/lib/roles/gateway.mjs +6 -2
  140. package/lib/rules-delivery.mjs +1 -1
  141. package/lib/runtime-pressure.mjs +5 -3
  142. package/lib/sandbox.mjs +3 -2
  143. package/lib/scheduler/solo.mjs +6 -4
  144. package/lib/server/auth.mjs +4 -3
  145. package/lib/server/index.mjs +24 -26
  146. package/lib/server/insights.mjs +5 -4
  147. package/lib/server/webhook.mjs +2 -1
  148. package/lib/service-manager.mjs +5 -3
  149. package/lib/setup.mjs +9 -8
  150. package/lib/status.mjs +6 -5
  151. package/lib/storage/embeddings-local.mjs +3 -2
  152. package/lib/storage/sync.mjs +2 -2
  153. package/lib/telemetry/client.mjs +0 -1
  154. package/lib/telemetry/intent-verifications.mjs +6 -6
  155. package/lib/telemetry/model-pricing-catalog.mjs +4 -3
  156. package/lib/telemetry/rule-calls.mjs +3 -3
  157. package/lib/telemetry/skill-calls.mjs +3 -3
  158. package/lib/template-registry.mjs +1 -0
  159. package/lib/templates/visual-requirements.mjs +1 -1
  160. package/lib/test-corpus-inventory.mjs +1 -1
  161. package/lib/uninstall/uninstall.mjs +12 -10
  162. package/package.json +3 -3
  163. package/platforms/claude/settings.template.json +43 -43
  164. package/rules/common/no-fabrication.md +1 -1
  165. package/rules/common/release-gates.md +1 -1
  166. package/rules/common/research.md +1 -1
  167. package/rules/common/review-before-change.md +1 -1
  168. package/scripts/sync-specialists.mjs +11 -8
  169. package/skills/docs/adr-workflow.md +3 -3
  170. package/skills/docs/init-docs.md +9 -9
  171. package/skills/docs/prd-workflow.md +5 -5
  172. package/skills/docs/product-intelligence-review.md +1 -1
  173. package/skills/docs/product-intelligence-workflow.md +2 -2
  174. package/skills/docs/product-signal-workflow.md +1 -1
  175. package/skills/docs/runbook-workflow.md +4 -4
  176. package/skills/exploration/unknown-codebase-onboarding.md +1 -1
  177. package/skills/operating/orchestration-reference.md +1 -1
  178. package/skills/routing.md +3 -3
  179. package/specialists/prompts/cx-architect.md +1 -1
  180. package/specialists/prompts/cx-docs-keeper.md +1 -1
  181. package/specialists/prompts/cx-researcher.md +1 -1
  182. package/specialists/prompts/cx-sre.md +1 -1
  183. package/specialists/role-manifests.json +6 -6
  184. package/templates/docs/README.md +125 -0
  185. package/templates/docs/construct_guide.md +3 -4
  186. package/templates/docs/persona-artifact.md +1 -1
  187. package/templates/docs/prds/README.md +25 -0
  188. package/templates/docs/prds/templates/_template.md +206 -0
  189. package/templates/docs/prds/templates/meta-prd.template.md +177 -0
  190. package/templates/docs/prds/templates/prd-business.template.md +61 -0
  191. package/templates/docs/prds/templates/prd-platform.template.md +81 -0
  192. package/templates/docs/prds/templates/prfaq.template.md +38 -0
  193. package/templates/docs/rfcs/README.md +22 -0
  194. package/templates/docs/rfcs/templates/_template.md +58 -0
  195. package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
  196. package/templates/homebrew/construct.rb +1 -1
  197. package/templates/workflows/new-feature.yml +8 -8
  198. package/lib/intake/legacy-paths.mjs +0 -5
@@ -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,37 +833,35 @@ 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
- // Universal project-root inbox/ drop zone. Raw drops stay out of source via
890
- // host-disposition IGNORED_PATTERNS (ADR-0027 §1). Skipped when custom intake
891
- // pipeline already owns the project root.
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.
892
851
 
893
852
  if (!inboxDecision.skip) {
894
- const projectInbox = path.join(target, 'inbox');
895
- if (!fs.existsSync(projectInbox)) {
896
- fs.mkdirSync(projectInbox, { recursive: true });
853
+ const rootInbox = path.join(target, 'inbox');
854
+ if (!fs.existsSync(rootInbox)) {
855
+ fs.mkdirSync(rootInbox, { recursive: true });
897
856
  created.push('inbox/');
898
857
  }
899
858
 
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
+
900
865
  if (intakeCap?.dedup === 'sha256') {
901
866
  const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
902
867
  const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
@@ -906,11 +871,10 @@ async function main() {
906
871
  }
907
872
  }
908
873
  } else {
874
+ console.log(`[init:intake] skipping inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.`);
909
875
  skipped.push('inbox/ (deferred to existing intake)');
910
876
  }
911
877
 
912
- // Legacy archetype block removed — root inbox/ is universal (construct-1arm).
913
-
914
878
  // Gitignore every Construct-generated artifact whose disposition is `ignored`
915
879
  // (ADR-0027 §1): the six adapter dirs, the `.construct/` launcher, `.cx/`
916
880
  // runtime state, and the generated config files all carry machine-specific
@@ -1075,28 +1039,18 @@ async function main() {
1075
1039
  console.warn(`⚠️ Could not inject agent-instruction block: ${err.message}`);
1076
1040
  }
1077
1041
 
1078
- // Intake collectionalways write intake-config.json with safe defaults
1079
- // so the user can inspect what's watched and edit the file directly. Empty
1080
- // parentDirs is the correct default: the inbox watcher always scans
1081
- // .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.
1082
1046
 
1083
1047
  const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
1084
1048
 
1085
- if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
1086
-
1087
- intakeConfig.includeRootInbox = !inboxDecision.skip;
1088
- intakeConfig.includeArchetypeInbox = intakeConfig.includeRootInbox;
1089
-
1090
1049
  const { saveIntakePolicy } = await import('./config/intake-policy.mjs');
1091
1050
  try {
1092
1051
  saveIntakePolicy(target, {
1093
1052
  maxDepth: intakeConfig.maxDepth,
1094
1053
  additionalDirs: intakeConfig.parentDirs ?? [],
1095
- zones: {
1096
- rootInbox: intakeConfig.includeRootInbox !== false,
1097
- projectInbox: intakeConfig.includeProjectInbox !== false,
1098
- docsIntake: intakeConfig.includeDocsIntake !== false,
1099
- },
1100
1054
  });
1101
1055
  created.push('construct.config.json (intakePolicy)');
1102
1056
  } catch (err) {
@@ -1115,7 +1069,7 @@ async function main() {
1115
1069
  // Create documentation system if lanes specified
1116
1070
  if (lanes.length > 0) {
1117
1071
  // Filter out lanes that the project already covers elsewhere
1118
- // (issue #97: don't create docs/meetings/ when internal/meetings/
1072
+ // (issue #97: don't create docs/notes/meetings/ when internal/meetings/
1119
1073
  // has 12 markdown files). --force bypasses the filter.
1120
1074
 
1121
1075
  const deferredLanes = [];
@@ -1210,7 +1164,7 @@ async function main() {
1210
1164
  const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
1211
1165
  if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
1212
1166
  console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
1213
- 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.`);
1214
1168
  }
1215
1169
  } catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
1216
1170
 
@@ -1241,9 +1195,9 @@ async function main() {
1241
1195
  console.log(`${++step}. Edit plan.md`);
1242
1196
  console.log(' Add your current work and tasks');
1243
1197
  console.log('');
1244
- if (lanes.includes('intake')) {
1198
+ if (created.includes('inbox/')) {
1245
1199
  console.log(`${++step}. Use Intake`);
1246
- console.log(' Drop files in .cx/inbox/ for processing');
1200
+ console.log(' Drop files in inbox/ for processing');
1247
1201
  console.log(' Run: construct intake');
1248
1202
  console.log('');
1249
1203
  }
package/lib/init.mjs CHANGED
@@ -77,12 +77,14 @@ function main() {
77
77
  content: `${JSON.stringify({ version: 1, formality: 3, toneOverride: {}, audienceLexicon: { prefer: [], avoid: [] } }, null, 2)}\n`,
78
78
  generator: "construct/init",
79
79
  });
80
- // `.cx/` already exists from the context writes above and is gitignored in full
81
- // (ADR-0027), so a keep file there could never be tracked; `.cx/inbox/` is the local
82
- // intake drop zone the watcher polls. Create the directories; write no dead keep file.
80
+ // Single canonical drop zone (ADR-0045 §C): the visible project-root `inbox/`,
81
+ // covered by the host .gitignore (ADR-0027 §1). Writers stage under the
82
+ // gitignored `inbox/.staging/` and atomically rename in; the watcher consumes
83
+ // only complete top-level files. Machine/runtime intake state lives under the
84
+ // gitignored `.cx/intake/`, created lazily by the queue.
83
85
 
84
- fs.mkdirSync(path.join(target, ".cx", "inbox"), { recursive: true });
85
- created.push(".cx/inbox/");
86
+ fs.mkdirSync(path.join(target, "inbox", ".staging"), { recursive: true });
87
+ created.push("inbox/");
86
88
 
87
89
  process.stdout.write(`\nConstruct init → ${target}\n\n`);
88
90
  if (created.length) {
@@ -2,7 +2,8 @@
2
2
  * lib/install/desktop-binary-download.mjs — downloads the Construct chat desktop binary.
3
3
  *
4
4
  * Fetches the platform-matching construct-chat binary from the latest GitHub
5
- * release and writes it to ~/.construct/bin/. Called by `construct install --scope=user`.
5
+ * release and writes it to the bin/ subdir of the XDG state dir. Called by
6
+ * `construct install --scope=user`.
6
7
  * Network failures are non-fatal: returns { status: 'skipped', reason } so the
7
8
  * rest of setup can proceed without the desktop binary.
8
9
  */
@@ -10,6 +11,8 @@
10
11
  import fs from 'node:fs';
11
12
  import path from 'node:path';
12
13
 
14
+ import { stateDir } from '../config/xdg.mjs';
15
+
13
16
  const GITHUB_REPO = 'geraldmaron/construct';
14
17
 
15
18
  function assetName() {
@@ -46,7 +49,7 @@ async function downloadAsset(url, dest, env) {
46
49
 
47
50
  // Returns { status: 'downloaded'|'skipped'|'failed' }; 'skipped' and 'failed' are both non-fatal.
48
51
  export async function downloadDesktopBinary({ homeDir, env = process.env } = {}) {
49
- const binDir = path.join(homeDir, '.construct', 'bin');
52
+ const binDir = path.join(stateDir(homeDir), 'bin');
50
53
  const dest = path.join(binDir, destBinaryName());
51
54
  const name = assetName();
52
55
 
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * lib/intake/daemon.mjs — Continuous intake daemon with safeguards.
3
3
  *
4
- * Polls `.cx/inbox/` for new files, classifies them via the existing intake
5
- * classifier, and writes packets to `.cx/intake/pending/`. Packets past their
6
- * TTL move to `.cx/intake/dead-letter/`; failed classification retries up to
7
- * the budget then dead-letters with the failure reason.
4
+ * Polls the canonical project-root `inbox/` for new files, classifies them via
5
+ * the existing intake classifier, and writes packets to `.cx/intake/pending/`.
6
+ * Packets past their TTL move to `.cx/intake/dead-letter/`; failed
7
+ * classification retries up to the budget then dead-letters with the reason.
8
+ *
9
+ * Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
10
+ * Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
11
+ * a writer is still staging is invisible until it is renamed into `inbox/`.
8
12
  *
9
13
  * Built on lib/daemons/contract.mjs — every safeguard (bounded lifetime,
10
14
  * idle shutdown, heartbeat, killswitch, single-writer lock) applies.
@@ -12,30 +16,49 @@
12
16
 
13
17
  import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
14
18
  import { join, basename } from 'node:path';
15
- import { homedir } from 'node:os';
16
19
 
17
20
  import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
18
21
  import { memoryCapMbFor } from '../resources/process-budget.mjs';
22
+ import { stateDir } from '../config/xdg.mjs';
19
23
 
20
24
  const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
21
25
 
22
- function inboxDir(cwd) { return join(cwd, '.cx', 'inbox'); }
26
+ function inboxDir(cwd) { return join(cwd, 'inbox'); }
23
27
  function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
24
28
  function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
25
- function heartbeatPath() { return join(homedir(), '.construct', 'intake-daemon.heartbeat'); }
29
+ function heartbeatPath() { return join(stateDir(), 'intake-daemon.heartbeat'); }
26
30
 
27
31
  function ensureDir(dir) {
28
32
  try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
29
33
  }
30
34
 
35
+ // A writer staging into `inbox/.staging/` and renaming into `inbox/` is the
36
+ // atomic handoff. As a backstop for writers that drop in place, a file whose
37
+ // size is still moving between two stats is mid-write — skip it this tick.
38
+
39
+ function isSizeStable(full) {
40
+ try {
41
+ const a = statSync(full);
42
+ const b = statSync(full);
43
+ return a.size === b.size;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
31
49
  function listInboxFiles(cwd) {
32
50
  const dir = inboxDir(cwd);
33
51
  if (!existsSync(dir)) return [];
34
52
  let entries;
35
53
  try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
54
+
55
+ // Only complete top-level files: dotfiles (and the `.staging/` assembly dir)
56
+ // are skipped, so a file a writer is still staging is invisible until rename.
57
+
36
58
  return entries
37
59
  .filter((e) => e.isFile() && !e.name.startsWith('.'))
38
- .map((e) => join(dir, e.name));
60
+ .map((e) => join(dir, e.name))
61
+ .filter((full) => isSizeStable(full));
39
62
  }
40
63
 
41
64
  /**
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * lib/intake/intake-config.mjs — intake watcher config facade.
3
3
  *
4
- * Reads and writes intake policy via construct.config.json intakePolicy.
5
- * Legacy .cx/intake-config.json remains a warned compatibility fallback.
4
+ * Reads and writes intake policy via construct.config.json intakePolicy, the
5
+ * only config source (single-zone model, ADR-0045 §C).
6
6
  */
7
7
 
8
8
  import { shouldCreateCx } from '../project-detection.mjs';
9
9
  import {
10
- loadIntakePolicy,
11
10
  resolvedIntakeConfig,
12
11
  saveIntakePolicy,
13
- migrateLegacyIntakeConfig,
14
12
  DEFAULT_INTAKE_POLICY,
15
13
  } from '../config/intake-policy.mjs';
16
14
  import {
@@ -19,38 +17,22 @@ import {
19
17
  INTAKE_DEPTH_GUIDANCE,
20
18
  describeIntakeDepth,
21
19
  } from './constants.mjs';
22
- import { INTAKE_CONFIG_FILE } from './legacy-paths.mjs';
23
- import { join } from 'node:path';
24
20
 
25
21
  export {
26
22
  INTAKE_DEFAULT_MAX_DEPTH,
27
23
  INTAKE_HARD_MAX_DEPTH,
28
24
  INTAKE_DEPTH_GUIDANCE,
29
25
  describeIntakeDepth,
30
- INTAKE_CONFIG_FILE,
31
- migrateLegacyIntakeConfig,
32
26
  DEFAULT_INTAKE_POLICY,
33
27
  };
34
28
 
35
29
  export const DEFAULT_INTAKE_CONFIG = Object.freeze({
36
30
  parentDirs: [],
37
31
  maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
38
- includeProjectInbox: true,
39
- includeDocsIntake: true,
40
- includeArchetypeInbox: true,
41
- includeRootInbox: true,
42
32
  });
43
33
 
44
- export function intakeConfigPath(rootDir) {
45
- return join(rootDir, INTAKE_CONFIG_FILE);
46
- }
47
-
48
34
  export function loadIntakeConfig(rootDir, env = process.env) {
49
- const cfg = resolvedIntakeConfig(rootDir, env);
50
- if (cfg.legacyWarning && process.stderr.isTTY) {
51
- process.stderr.write(`[intake-config] ${cfg.legacyWarning}\n`);
52
- }
53
- return cfg;
35
+ return resolvedIntakeConfig(rootDir, env);
54
36
  }
55
37
 
56
38
  export function saveIntakeConfig(rootDir, patch = {}) {
@@ -60,24 +42,15 @@ export function saveIntakeConfig(rootDir, patch = {}) {
60
42
 
61
43
  const policyPatch = {};
62
44
  if (patch.maxDepth !== undefined) policyPatch.maxDepth = patch.maxDepth;
63
- if (patch.includeProjectInbox !== undefined) policyPatch.projectInbox = patch.includeProjectInbox;
64
- if (patch.includeDocsIntake !== undefined) policyPatch.docsIntake = patch.includeDocsIntake;
65
- if (patch.includeArchetypeInbox !== undefined || patch.includeRootInbox !== undefined) {
66
- policyPatch.rootInbox = patch.includeRootInbox ?? patch.includeArchetypeInbox;
67
- }
68
45
  if (Array.isArray(patch.parentDirs)) policyPatch.additionalDirs = patch.parentDirs;
69
46
 
70
47
  const { policy } = saveIntakePolicy(rootDir, policyPatch);
71
- return policyToLegacyShape(policy);
48
+ return policyToConfigShape(policy);
72
49
  }
73
50
 
74
- function policyToLegacyShape(policy) {
51
+ function policyToConfigShape(policy) {
75
52
  return {
76
53
  parentDirs: policy.additionalDirs,
77
54
  maxDepth: policy.maxDepth,
78
- includeProjectInbox: policy.zones.projectInbox,
79
- includeDocsIntake: policy.zones.docsIntake,
80
- includeArchetypeInbox: policy.zones.rootInbox,
81
- includeRootInbox: policy.zones.rootInbox,
82
55
  };
83
56
  }
@@ -18,6 +18,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
18
18
  import { join } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import { spawnSync } from 'node:child_process';
21
+ import { configDir } from '../config/xdg.mjs';
21
22
 
22
23
  // ── Multi-method auth helpers ────────────────────────────────────────────
23
24
 
@@ -268,8 +269,8 @@ export async function createJiraTicket(packet, { host, email, token, project, is
268
269
  const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
269
270
  const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
270
271
 
271
- if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
272
- if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
272
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.config/construct/config.env or shell rc.' };
273
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
273
274
  if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
274
275
 
275
276
  const t = packet?.triage || {};
@@ -385,8 +386,8 @@ export async function publishArtifactToConfluence(artifact, { host, email, token
385
386
  const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
386
387
  const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
387
388
 
388
- if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
389
- if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
389
+ if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.config/construct/config.env or shell rc.' };
390
+ if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
390
391
  if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
391
392
 
392
393
  const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
@@ -608,7 +609,7 @@ function resolveCredentialSync(varName, homeDir) {
608
609
  }
609
610
  } catch { /* skip */ }
610
611
  try {
611
- const cfgPath = join(homeDir, '.construct', 'config.env');
612
+ const cfgPath = join(configDir(homeDir), 'config.env');
612
613
  if (existsSync(cfgPath)) {
613
614
  const content = readFileSync(cfgPath, 'utf8');
614
615
  const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
@@ -703,9 +704,9 @@ function resolveCredential(varName, homeDir) {
703
704
  }
704
705
  } catch { /* skip */ }
705
706
 
706
- // ~/.construct/config.env
707
+ // config.env in the XDG config dir
707
708
  try {
708
- const cfgPath = join(homeDir, '.construct', 'config.env');
709
+ const cfgPath = join(configDir(homeDir), 'config.env');
709
710
  if (existsSync(cfgPath)) {
710
711
  const content = readFileSync(cfgPath, 'utf8');
711
712
  const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Sources indexed:
9
9
  * - Observations (.cx/observations/)
10
- * - Artifacts (docs/adr/, docs/prd/, docs/rfc/)
10
+ * - Artifacts (docs/decisions/adr/, docs/specs/prd/, docs/decisions/rfc/)
11
11
  * - Snapshots (.cx/snapshot.md + any configured output paths)
12
12
  * - Approval queue (.cx/approval-queue.jsonl)
13
13
  *
@@ -102,7 +102,7 @@ function loadMarkdownChunks(dir, source) {
102
102
  */
103
103
  function loadArtifactChunks(rootDir) {
104
104
  const chunks = [];
105
- for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/concepts/architecture.md']) {
105
+ for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/guides/concepts/architecture.md']) {
106
106
  const full = path.resolve(rootDir, subdir);
107
107
  if (subdir.endsWith('.md')) {
108
108
  if (!fs.existsSync(full)) continue;
@@ -6,11 +6,11 @@
6
6
  * `knowledge_search` MCP tool — no daemon, no network, no external deps.
7
7
  *
8
8
  * Sources searched (in priority order):
9
- * 1. docs/concepts/*.md — architecture, agents, gates, durable state
10
- * 2. docs/start/*.mdx — install + first task
9
+ * 1. docs/guides/concepts/*.md — architecture, agents, gates, durable state
10
+ * 2. docs/guides/start/*.mdx — install + first task
11
11
  * 3. docs/README.md — docs index + contract
12
12
  * 4. .cx/knowledge/ — operator-written internal docs
13
- * 5. Any *.md in docs/cookbook/ — task-oriented recipes
13
+ * 5. Any *.md in docs/guides/cookbook/ — task-oriented recipes
14
14
  * 6. <projectRoot>/.cx/knowledge/** — the cwd project's knowledge tree,
15
15
  * including `external/research/` written by `construct knowledge add`,
16
16
  * so foreign-project queries surface project content alongside bundled docs.
@@ -47,17 +47,17 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
47
47
  const sources = [];
48
48
 
49
49
  const priority = [
50
- 'docs/concepts/architecture.md',
51
- 'docs/concepts/agents-and-personas.mdx',
52
- 'docs/concepts/gates-and-enforcement.md',
53
- 'docs/concepts/beads-and-state.md',
54
- 'docs/concepts/prompt-surfaces.md',
55
- 'docs/concepts/knowledge-layout.md',
56
- 'docs/concepts/deployment-model.md',
57
- 'docs/concepts/embedding-boundary.md',
50
+ 'docs/guides/concepts/architecture.md',
51
+ 'docs/guides/concepts/agents-and-personas.mdx',
52
+ 'docs/guides/concepts/gates-and-enforcement.md',
53
+ 'docs/guides/concepts/beads-and-state.md',
54
+ 'docs/guides/concepts/prompt-surfaces.md',
55
+ 'docs/guides/concepts/knowledge-layout.md',
56
+ 'docs/guides/concepts/deployment-model.md',
57
+ 'docs/guides/concepts/embedding-boundary.md',
58
58
  'docs/README.md',
59
- 'docs/start/install.mdx',
60
- 'docs/start/first-task.mdx',
59
+ 'docs/guides/start/install.mdx',
60
+ 'docs/guides/start/first-task.mdx',
61
61
  ];
62
62
 
63
63
  for (const rel of priority) {
@@ -66,12 +66,12 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
66
66
  }
67
67
 
68
68
  // Cookbook recipes (task-oriented how-tos)
69
- const cookbookDir = join(repoRoot, 'docs', 'cookbook');
69
+ const cookbookDir = join(repoRoot, 'docs', 'guides', 'cookbook');
70
70
  if (existsSync(cookbookDir)) {
71
71
  for (const file of readdirSync(cookbookDir)) {
72
72
  if (file.endsWith('.md') || file.endsWith('.mdx')) {
73
73
  const full = join(cookbookDir, file);
74
- sources.push({ path: full, rel: `docs/cookbook/${file}`, priority: 2 });
74
+ sources.push({ path: full, rel: `docs/guides/cookbook/${file}`, priority: 2 });
75
75
  }
76
76
  }
77
77
  }
@@ -10,8 +10,9 @@
10
10
  * Triggered from two entry points:
11
11
  * - `construct cleanup` — manual sweep, optionally --dry-run
12
12
  * - bin/construct startup check — automatic when the installed version
13
- * differs from ~/.construct/.cleanup-stamp,
14
- * so an upgrade reclaims disk without
13
+ * differs from the XDG state-dir
14
+ * .cleanup-stamp, so an upgrade reclaims
15
+ * disk without
15
16
  * requiring a manual command.
16
17
  *
17
18
  * Scope is deliberately narrow: only artifacts whose retention is owned by
@@ -24,6 +25,9 @@ import fs from 'node:fs';
24
25
  import os from 'node:os';
25
26
  import path from 'node:path';
26
27
 
28
+ import { stateDir, cacheDir } from '../config/xdg.mjs';
29
+ import { doctorRoot } from '../config/xdg.mjs';
30
+
27
31
  const HARD_BUDGET_MS = 5000;
28
32
 
29
33
  // Size caps in bytes. Each entry: [filename relative to ~/.cx/, maxBytes].
@@ -92,7 +96,7 @@ function rotationConfig(env = process.env) {
92
96
  * were oversized before the cap existed.
93
97
  */
94
98
  export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
95
- const dir = path.join(homeDir, '.cx', 'runtime');
99
+ const dir = path.join(doctorRoot(homeDir), 'runtime');
96
100
  const baseLog = path.join(dir, 'embed-daemon.log');
97
101
  const { maxBytes, keep } = rotationConfig(env);
98
102
 
@@ -149,7 +153,7 @@ export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dry
149
153
  * recent records survive.
150
154
  */
151
155
  export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}) {
152
- const dir = path.join(homeDir, '.cx');
156
+ const dir = doctorRoot(homeDir);
153
157
  const summary = { truncated: [], freedBytes: 0 };
154
158
  if (!fs.existsSync(dir)) return summary;
155
159
 
@@ -168,11 +172,11 @@ export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}
168
172
  }
169
173
 
170
174
  /**
171
- * Age out ~/.construct/cache/ entries older than `maxAgeDays`. Cache hits are
175
+ * Age out XDG cache-dir entries older than `maxAgeDays`. Cache hits are
172
176
  * idempotent and re-fetched on demand, so pruning is safe.
173
177
  */
174
178
  export function cleanupCacheDir({ homeDir = os.homedir(), maxAgeDays = 30, dryRun = false } = {}) {
175
- const dir = path.join(homeDir, '.construct', 'cache');
179
+ const dir = cacheDir(homeDir);
176
180
  const summary = { removed: [], freedBytes: 0 };
177
181
  if (!fs.existsSync(dir)) return summary;
178
182
 
@@ -249,18 +253,18 @@ export function formatBytes(bytes) {
249
253
  // ---------------------------------------------------------------------------
250
254
  // Version stamp — drives automatic cleanup on upgrade.
251
255
  //
252
- // ~/.construct/.cleanup-stamp records the last version that ran cleanup.
253
- // On every CLI invocation, bin/construct compares the current package
256
+ // The .cleanup-stamp in the XDG state dir records the last version that ran
257
+ // cleanup. On every CLI invocation, bin/construct compares the current package
254
258
  // version to the stamp. A mismatch triggers a single cleanup pass and
255
259
  // writes the new version into the stamp. The first ever invocation also
256
260
  // triggers cleanup (no stamp present) — this is what unblocks existing
257
261
  // installs that already have 34 GB logs sitting around.
258
262
  // ---------------------------------------------------------------------------
259
263
 
260
- const STAMP_RELATIVE = path.join('.construct', '.cleanup-stamp');
264
+ const STAMP_FILENAME = '.cleanup-stamp';
261
265
 
262
266
  export function stampPath(homeDir = os.homedir()) {
263
- return path.join(homeDir, STAMP_RELATIVE);
267
+ return path.join(stateDir(homeDir), STAMP_FILENAME);
264
268
  }
265
269
 
266
270
  export function readVersionStamp({ homeDir = os.homedir() } = {}) {
@@ -859,7 +859,7 @@ const ALL_TOOL_DEFS = [
859
859
  },
860
860
  {
861
861
  name: 'profile_create',
862
- description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/concepts/profile-lifecycle.md` after creation.',
862
+ description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/guides/concepts/profile-lifecycle.md` after creation.',
863
863
  inputSchema: {
864
864
  type: 'object',
865
865
  required: ['confirm', 'id'],