@geraldmaron/construct 1.0.11 → 1.0.14

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 (90) hide show
  1. package/README.md +55 -18
  2. package/bin/construct +259 -11
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/lib/auto-docs.mjs +23 -22
  5. package/lib/cli-commands.mjs +2 -0
  6. package/lib/dashboard-static.mjs +7 -4
  7. package/lib/doc-stamp.mjs +16 -0
  8. package/lib/docs-verify.mjs +1 -8
  9. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  10. package/lib/embed/daemon.mjs +20 -0
  11. package/lib/embed/docs-lifecycle.mjs +19 -0
  12. package/lib/embed/inbox.mjs +85 -2
  13. package/lib/embed/recommendation-store.mjs +29 -0
  14. package/lib/gates-audit.mjs +18 -12
  15. package/lib/hooks/_lib/input.mjs +52 -0
  16. package/lib/hooks/adaptive-lint.mjs +4 -0
  17. package/lib/hooks/agent-tracker.mjs +59 -15
  18. package/lib/hooks/audit-reads.mjs +83 -42
  19. package/lib/hooks/audit-trail.mjs +28 -18
  20. package/lib/hooks/bash-output-logger.mjs +8 -2
  21. package/lib/hooks/block-no-verify.mjs +4 -0
  22. package/lib/hooks/ci-status-check.mjs +4 -0
  23. package/lib/hooks/comment-lint.mjs +6 -4
  24. package/lib/hooks/config-protection.mjs +4 -0
  25. package/lib/hooks/context-watch.mjs +4 -0
  26. package/lib/hooks/context-window-recovery.mjs +4 -0
  27. package/lib/hooks/dep-audit.mjs +12 -5
  28. package/lib/hooks/doc-coupling-check.mjs +5 -1
  29. package/lib/hooks/edit-accumulator.mjs +25 -10
  30. package/lib/hooks/edit-error-recovery.mjs +4 -0
  31. package/lib/hooks/edit-guard.mjs +4 -0
  32. package/lib/hooks/guard-bash.mjs +4 -0
  33. package/lib/hooks/mcp-audit.mjs +4 -0
  34. package/lib/hooks/mcp-health-check.mjs +4 -0
  35. package/lib/hooks/model-fallback.mjs +7 -11
  36. package/lib/hooks/policy-engine.mjs +4 -0
  37. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  38. package/lib/hooks/post-merge-tracking.mjs +82 -0
  39. package/lib/hooks/pre-compact.mjs +4 -0
  40. package/lib/hooks/pre-push-gate.mjs +84 -231
  41. package/lib/hooks/proactive-activation.mjs +5 -2
  42. package/lib/hooks/readme-age-check.mjs +4 -0
  43. package/lib/hooks/registry-sync.mjs +35 -20
  44. package/lib/hooks/rule-verifier.mjs +3 -0
  45. package/lib/hooks/scan-secrets.mjs +4 -0
  46. package/lib/hooks/session-optimize.mjs +4 -0
  47. package/lib/hooks/session-reflect.mjs +4 -0
  48. package/lib/hooks/session-start.mjs +48 -1
  49. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  50. package/lib/hooks/stop-notify.mjs +13 -2
  51. package/lib/hooks/stop-typecheck.mjs +4 -0
  52. package/lib/hooks/test-watch.mjs +8 -4
  53. package/lib/init-unified.mjs +90 -23
  54. package/lib/intake/attribution.mjs +77 -0
  55. package/lib/intake/intake-config.mjs +3 -0
  56. package/lib/intake/manifest.mjs +107 -0
  57. package/lib/intake/poll-lock.mjs +136 -0
  58. package/lib/intake/prepare.mjs +42 -4
  59. package/lib/logging/rotate.mjs +394 -0
  60. package/lib/mcp/tools/project.mjs +2 -2
  61. package/lib/mcp/tools/telemetry.mjs +1 -1
  62. package/lib/opencode-config.mjs +10 -3
  63. package/lib/orchestration/routing-tables.mjs +176 -0
  64. package/lib/orchestration-policy.mjs +18 -106
  65. package/lib/parity.mjs +48 -7
  66. package/lib/profiles/lifecycle.mjs +19 -1
  67. package/lib/project-init-shared.mjs +11 -5
  68. package/lib/project-root.mjs +104 -0
  69. package/lib/roles/catalog.mjs +1 -1
  70. package/lib/roles/event-bus.mjs +29 -9
  71. package/lib/roles/router.mjs +8 -7
  72. package/lib/server/index.mjs +31 -9
  73. package/lib/server/static/index.html +1 -15
  74. package/lib/specialist-contracts-enforce.mjs +24 -10
  75. package/lib/status.mjs +1 -1
  76. package/lib/storage/backup.mjs +2 -2
  77. package/lib/sync/skill-frontmatter.mjs +71 -0
  78. package/lib/telemetry/intent-verifications.mjs +16 -3
  79. package/lib/telemetry/skill-calls.mjs +12 -3
  80. package/lib/tracking-surfaces.mjs +375 -0
  81. package/lib/worker/trace.mjs +19 -2
  82. package/package.json +6 -2
  83. package/platforms/claude/settings.template.json +28 -27
  84. package/scripts/sync-specialists.mjs +296 -94
  85. package/specialists/registry.json +158 -13
  86. package/lib/hooks/env-check.mjs +0 -83
  87. package/lib/hooks/read-tracker.mjs +0 -61
  88. package/lib/policy/unified-gates.mjs +0 -96
  89. package/lib/server/static/assets/index-ab25c707.js +0 -70
  90. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **One AI interface. A team of specialists behind it. Hard gates. Runs locally, or deploys for teams.**
4
4
 
5
- 📖 **[Read the docs →](https://geraldmaron.github.io/construct/)** · 🚀 **[5-minute quickstart →](https://geraldmaron.github.io/construct/docs/start)** · 📦 `npm install -g @geraldmaron/construct`
5
+ 📖 **[Read the docs →](https://geraldmaron.github.io/construct/)** · 🚀 **[5-minute quickstart →](https://geraldmaron.github.io/construct/start)** · 📦 `npm install -g @geraldmaron/construct`
6
6
 
7
7
  ---
8
8
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  Construct sits on top of Claude Code, OpenCode, Codex, Cursor, and Copilot. You talk to one persona called `construct`. Behind it is a team of specialists shaped by your **org profile**: software R&D by default, with curated profiles for operations, creative, and research orgs, plus a schema-validated escape hatch for custom profiles. Each profile organizes its specialists by department (Product, Engineering, Operations, etc.) and carries its own intake taxonomy, doc templates, and role set. Sessions survive boundary changes via durable state in `.cx/`, beads, and a local vector index. Solo by default. Can deploy centrally for teams that want shared memory, telemetry, queues, and policy.
12
12
 
13
- `construct profile show|list|set <id>` to switch. See [Profile lifecycle](https://geraldmaron.github.io/construct/docs/concepts/profile-lifecycle) for how new profiles are built (it's a research process, not a JSON exercise).
13
+ `construct profile show|list|set <id>` to switch. See [Profile lifecycle](https://geraldmaron.github.io/construct/concepts/profile-lifecycle) for how new profiles are built (it's a research process, not a JSON exercise).
14
14
 
15
15
  The team and enterprise modes exist because I wanted to learn what shipping a real multi-tenant tool would look like. The project is still open source, the code is still public, and the bar is still "does this help me learn." Run it solo if that's all you need.
16
16
 
@@ -39,20 +39,33 @@ Open your editor and talk to `@construct`. A walkthrough lives in `construct_gui
39
39
 
40
40
  No Node? Try `brew install geraldmaron/construct/construct`. Cloning a project that already uses Construct? `npx -y @geraldmaron/construct init` wires it up.
41
41
 
42
- [Five minute walkthrough](https://geraldmaron.github.io/construct/docs/start).
42
+ [Five minute walkthrough](https://geraldmaron.github.io/construct/start).
43
+
44
+ ## Usage
45
+
46
+ Most days, the loop is:
47
+
48
+ ```bash
49
+ construct status # confirm services and editor adapters are healthy
50
+ construct sync # refresh host adapters after registry, prompt, or config changes
51
+ construct intake list # review new signals, if your project uses the inbox
52
+ construct doctor # diagnose install, service, MCP, and adapter drift
53
+ ```
54
+
55
+ In your editor, start with `@construct`. Ask for the outcome, not the specialist. Construct routes to the right specialist chain, keeps durable state in `.cx/` and Beads, and blocks risky mutations until the configured gates pass.
43
56
 
44
57
  ## What you can do
45
58
 
46
59
  | If you want to... | Read |
47
60
  |---|---|
48
- | Install and run a first task | [Start](https://geraldmaron.github.io/construct/docs/start) |
49
- | Understand how it works | [Architecture](https://geraldmaron.github.io/construct/docs/concepts/architecture) |
50
- | Pick a deployment mode | [Deployment model](https://geraldmaron.github.io/construct/docs/concepts/deployment-model) |
51
- | Drop a signal and triage it | [Intake and triage](https://geraldmaron.github.io/construct/docs/concepts/intake-and-triage) |
52
- | Add a custom specialist | [Add a custom agent](https://geraldmaron.github.io/construct/docs/cookbook/add-a-custom-agent) |
53
- | Fix a blocked commit or red CI | [Fix a policy violation](https://geraldmaron.github.io/construct/docs/cookbook/fix-a-policy-violation) |
54
- | Plug in your own LLM | [Plug in your own LLM](https://geraldmaron.github.io/construct/docs/cookbook/plug-in-your-own-llm) |
55
- | Look up a CLI command | [CLI reference](https://geraldmaron.github.io/construct/docs/reference/cli) |
61
+ | Install and run a first task | [Start](https://geraldmaron.github.io/construct/start) |
62
+ | Understand how it works | [Architecture](https://geraldmaron.github.io/construct/concepts/architecture) |
63
+ | Pick a deployment mode | [Deployment model](https://geraldmaron.github.io/construct/concepts/deployment-model) |
64
+ | Drop a signal and triage it | [Intake and triage](https://geraldmaron.github.io/construct/concepts/intake-and-triage) |
65
+ | Add a custom specialist | [Add a custom specialist](https://geraldmaron.github.io/construct/cookbook/add-a-custom-agent) |
66
+ | Fix a blocked commit or red CI | [Fix a policy violation](https://geraldmaron.github.io/construct/cookbook/fix-a-policy-violation) |
67
+ | Plug in your own LLM | [Plug in your own LLM](https://geraldmaron.github.io/construct/cookbook/plug-in-your-own-llm) |
68
+ | Look up a CLI command | [CLI reference](https://geraldmaron.github.io/construct/reference/cli) |
56
69
 
57
70
  Works with Anthropic, OpenRouter, Ollama, and other OpenAI-compatible providers.
58
71
 
@@ -64,21 +77,27 @@ Three modes. `solo` is the default and runs everything locally. Filesystem queue
64
77
 
65
78
  `enterprise` adds tenant isolation, RBAC and ABAC scaffolding, isolated worker containers, signed MCP allowlists, and mandatory audit.
66
79
 
67
- Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deployment model](https://geraldmaron.github.io/construct/docs/concepts/deployment-model).
80
+ Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deployment model](https://geraldmaron.github.io/construct/concepts/deployment-model).
68
81
 
69
82
  ## Intake
70
83
 
71
84
  Anything dropped into `.cx/inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified by the active profile's intake taxonomy. The default `rnd` profile uses bug, user-signal, experiment, architecture, incident, security, requirement, research, ops, eval-finding, launch-asset, legal-compliance. The `operations` profile uses request, incident, ops, security, docs. The `creative` profile uses brief, content-request, asset, experiment, report. The `research` profile uses question, study, synthesis, report.
72
85
 
73
- Each signal gets a primary owner and a recommended handoff chain. Inspect with `construct intake list` and `construct intake show <id>`. Generate a task graph with `construct graph from-intake <id>`. The classifier runs in the daemon and is deterministic. The agent in your editor does the actual analysis. [Intake and triage](https://geraldmaron.github.io/construct/docs/concepts/intake-and-triage).
86
+ Each signal gets a primary owner and a recommended handoff chain. Inspect with `construct intake list` and `construct intake show <id>`. Generate a task graph with `construct graph from-intake <id>`. The classifier runs in the daemon and is deterministic. The agent in your editor does the actual analysis. [Intake and triage](https://geraldmaron.github.io/construct/concepts/intake-and-triage).
74
87
 
75
88
  ## Hard gates
76
89
 
77
- Every code mutation runs through enforcement. No secrets committed, tests green, docs current, comments lint-clean, CI passes. Gates live in three places: write-time, commit-time, CI safety net. They can only be bypassed with explicit env vars so every exception leaves an audit trail. [Gates and enforcement](https://geraldmaron.github.io/construct/docs/concepts/gates-and-enforcement).
90
+ Every code mutation runs through enforcement. No secrets committed, tests green, docs current, comments lint-clean, CI passes. Gates live in three places: write-time, commit-time, CI safety net. They can only be bypassed with explicit env vars so every exception leaves an audit trail. [Gates and enforcement](https://geraldmaron.github.io/construct/concepts/gates-and-enforcement).
78
91
 
79
92
  ## Learning loops
80
93
 
81
- Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/concepts/learning-loops.md`](./docs/concepts/learning-loops.md) for what's wired, what's coming, and how to turn pieces off.
94
+ Construct gets smarter on its own. Every session ends with an automatic capture: tools used, files touched, what the final reply said. That goes into `.cx/observations/` and is searchable from the next session. See [`docs/concepts/learning-loops.mdx`](./docs/concepts/learning-loops.mdx) for what's wired, what's coming, and how to turn pieces off.
95
+
96
+ ## `.cx/` is local-only runtime state
97
+
98
+ `construct init` writes a runtime state tree at `.cx/` inside the project root: observations, sessions, vector index, intake packets, task graphs, and traces. **It's local-only and must never be committed.** `construct init` adds `.cx/` to your project's `.gitignore` automatically (idempotent: it won't double-add if you already have it). Daily trace shards (`.cx/traces/<date>.jsonl`) cap at 100 MB and rotate to `<date>.<n>.jsonl` so a stray commit never crosses GitHub's single-file limit. Override the cap with `CONSTRUCT_TRACE_MAX_MB`.
99
+
100
+ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon.log`. That log rotates every minute at 50 MB and keeps 5 gzipped segments by default; override via `CONSTRUCT_EMBED_LOG_MAX_MB` and `CONSTRUCT_EMBED_LOG_MAX_SEGMENTS`.
82
101
 
83
102
  ## Core commands
84
103
 
@@ -124,6 +143,16 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
124
143
  | `construct workflow` | Instantiate workflow templates (PRD-to-review chains, onboarding, handoffs) |
125
144
  | `construct workspace` | Manage PM workspaces for multi-PM signal routing |
126
145
 
146
+ ### Models & Integrations
147
+
148
+ | Command | What it does |
149
+ |---|---|
150
+ | `construct claude:allow` | Manage Claude Code `permissions.allow` from the outside (auto-classifier blocks the agent from editing it) |
151
+ | `construct hosts` | Show host support for Construct orchestration |
152
+ | `construct mcp` | Manage MCP integrations |
153
+ | `construct models` | Show or update model tier assignments |
154
+ | `construct plugin` | Manage external Construct plugin manifests |
155
+
127
156
  ### Integrations
128
157
 
129
158
  | Command | What it does |
@@ -145,6 +174,14 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
145
174
  | `construct telemetry` | Query telemetry traces and latency data |
146
175
  | `construct telemetry-backfill` | Backfill sparse traces with observations (trace backend) |
147
176
 
177
+ ### Diagnostics
178
+
179
+ | Command | What it does |
180
+ |---|---|
181
+ | `construct audit` | Audit Construct internals and review the mutation trail |
182
+ | `construct cleanup` | Release dev-agent memory pressure by cleaning stale helper and bridge processes |
183
+ | `construct doc` | Verify or inspect auditability stamps on Construct-generated markdown files |
184
+
148
185
  ### Advanced
149
186
 
150
187
  | Command | What it does |
@@ -176,7 +213,7 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
176
213
 
177
214
  - [`CONTRIBUTING.md`](./CONTRIBUTING.md). Branch workflow, gates, review expectations.
178
215
  - [`CHANGELOG.md`](./CHANGELOG.md). Release history.
179
- - [`docs/concepts/architecture.md`](./docs/concepts/architecture.md). Canonical architecture.
216
+ - [`docs/concepts/architecture.mdx`](./docs/concepts/architecture.mdx). Canonical architecture.
180
217
  - [`AGENTS.md`](./AGENTS.md). Agent operating contract.
181
218
 
182
219
  ## Project structure
@@ -184,16 +221,16 @@ Construct gets smarter on its own. Every session ends with an automatic capture:
184
221
  <!-- AUTO:structure -->
185
222
  ```text
186
223
  construct/
187
- ├── apps User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)
224
+ ├── apps User-facing apps shipped from this repo (e.g. apps/docs/, the Next.js docs site)
188
225
  ├── bin CLI entrypoint (`construct`)
189
226
  ├── commands Command prompt assets
190
227
  ├── config
191
- ├── dashboard
192
228
  ├── db
193
229
  ├── deploy
194
230
  ├── docs Architecture notes, runbooks, and documentation contract
195
231
  ├── examples
196
232
  ├── lib Core runtime: CLI, hooks, MCP, status, sync, workflow
233
+ ├── packages
197
234
  ├── personas Persona prompt definitions
198
235
  ├── platforms
199
236
  ├── profiles
package/bin/construct CHANGED
@@ -416,11 +416,190 @@ async function cmdDoctor() {
416
416
  return;
417
417
  }
418
418
 
419
+ // --fix-* flags drive auto-remediation for legacy state left behind by
420
+ // pre-2.0 installs. Layer-2 update triggers (npm i -g postinstall, brew
421
+ // upgrade) normally handle this; --fix-* is the manual recovery path.
422
+ // --fix-all runs every available fix.
423
+
424
+ const wantsFixAll = args.includes('--fix-all') || args.includes('--fix');
425
+ const wantsFixGitignore = wantsFixAll || args.includes('--fix-gitignore');
426
+ const wantsFixLegacyAgents = wantsFixAll || args.includes('--fix-legacy-agents');
427
+ const wantsFixEmbedLog = wantsFixAll || args.includes('--fix-embed-log');
428
+ const wantsFixMigrateState = wantsFixAll || args.includes('--fix-migrate-state');
429
+
419
430
  const checks = [];
420
431
  const add = (label, pass, optional = false) => checks.push({ label, pass, optional });
421
432
  add('registry.json exists', fs.existsSync(path.join(ROOT_DIR, 'specialists', 'registry.json')));
422
433
  add('sync-specialists.mjs exists', fs.existsSync(path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs')));
423
434
 
435
+ // Dashboard static build output. The Next.js app under apps/dashboard
436
+ // emits a static export into lib/server/static/ via its postbuild script;
437
+ // an empty static dir means lib/server/index.mjs will serve 404s.
438
+
439
+ const dashboardIndex = path.join(ROOT_DIR, 'lib', 'server', 'static', 'index.html');
440
+ add('Dashboard built (lib/server/static/index.html)', fs.existsSync(dashboardIndex));
441
+
442
+ // Project .gitignore must cover .cx/. Auto-fix via --fix-gitignore.
443
+ // Only checks when cwd looks like a Construct project (has .cx/ or
444
+ // .construct/); non-project cwds get no warning.
445
+
446
+ const cwd = process.cwd();
447
+ const isProject = fs.existsSync(path.join(cwd, '.cx')) || fs.existsSync(path.join(cwd, '.construct'));
448
+ if (isProject) {
449
+ const giPath = path.join(cwd, '.gitignore');
450
+ const giContent = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
451
+ const lines = giContent.split('\n').map((l) => l.trim());
452
+ const cxIgnored = lines.some((l) => l === '.cx' || l === '.cx/' || l === '.cx/**' || l === '*' || l === '**');
453
+ if (!cxIgnored && wantsFixGitignore) {
454
+ const prefix = giContent.length === 0 || giContent.endsWith('\n') ? '' : '\n';
455
+ fs.writeFileSync(giPath, giContent + `${prefix}\n# Construct runtime state — local-only, never source\n.cx/\n`, 'utf8');
456
+ println(` fix: appended .cx/ to ${path.relative(cwd, giPath)}`);
457
+ add('Project .gitignore covers .cx/', true);
458
+ } else {
459
+ add('Project .gitignore covers .cx/' + (cxIgnored ? '' : ' (run `construct doctor --fix-gitignore` to add)'), cxIgnored);
460
+ }
461
+ }
462
+
463
+ // Legacy cx-* files at user scope. Layer-2 (npm postinstall, brew
464
+ // upgrade) sweeps these via `construct sync --global`; this check is
465
+ // the manual safety net. Counts files matching a registered specialist
466
+ // name across Claude / Codex / Copilot user-scope dirs.
467
+
468
+ const userHome = HOME;
469
+ const registry = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'specialists', 'registry.json'), 'utf8'));
470
+ const specialistSet = new Set((registry.specialists ?? []).map((s) => `cx-${s.name}`));
471
+ const legacyTargets = [
472
+ { dir: path.join(userHome, '.claude', 'agents'), ext: '.md' },
473
+ { dir: path.join(userHome, '.codex', 'agents'), ext: '.toml' },
474
+ { dir: path.join(userHome, '.github', 'prompts'), ext: '.prompt.md' },
475
+ ];
476
+ let legacyCount = 0;
477
+ for (const t of legacyTargets) {
478
+ if (!fs.existsSync(t.dir)) continue;
479
+ for (const f of fs.readdirSync(t.dir)) {
480
+ if (!f.endsWith(t.ext)) continue;
481
+ const base = f.slice(0, -t.ext.length);
482
+ if (specialistSet.has(base)) legacyCount++;
483
+ }
484
+ }
485
+ if (legacyCount > 0 && wantsFixLegacyAgents) {
486
+ const syncScript = path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs');
487
+ const result = spawnSync(process.execPath, [syncScript, '--global'], { stdio: 'pipe', encoding: 'utf8' });
488
+ if (result.status === 0) {
489
+ println(` fix: swept ${legacyCount} legacy cx-* file(s) from user scope via construct sync --global`);
490
+ add('No legacy cx-* files at user scope', true);
491
+ } else {
492
+ add(`Legacy cx-* sweep failed: ${result.stderr?.substring(0, 200)}`, false);
493
+ }
494
+ } else {
495
+ // Soft-warn instead of hard-fail: legacy v1.0.10 cx-* files are an
496
+ // expected upgrade-state, not real drift. The hint is what matters;
497
+ // the fix is one --fix-legacy-agents run away and CI never sees this.
498
+ // Mirrors the parity check's legacy-install reclassification.
499
+
500
+ add(
501
+ legacyCount === 0
502
+ ? 'No legacy cx-* files at user scope'
503
+ : `Legacy cx-* files at user scope: ${legacyCount} (run \`construct doctor --fix-legacy-agents\` to sweep)`,
504
+ true,
505
+ legacyCount !== 0,
506
+ );
507
+ }
508
+
509
+ // Pre-push bypass frequency. Every honored CONSTRUCT_SKIP_PREPUSH /
510
+ // CONSTRUCT_ALLOW_CLAUDE_PUSH / CONSTRUCT_SKIP_PR_LINT writes a JSONL
511
+ // entry to ~/.construct/audit/prepush-bypass.log. Surfacing frequent
512
+ // usage here is the early warning that the gate is the wrong shape —
513
+ // the alternative is letting the bypass quietly become the default.
514
+
515
+ const bypassLogPath = path.join(userHome, '.construct', 'audit', 'prepush-bypass.log');
516
+ if (fs.existsSync(bypassLogPath)) {
517
+ try {
518
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
519
+ const recent = fs.readFileSync(bypassLogPath, 'utf8')
520
+ .split('\n')
521
+ .filter(Boolean)
522
+ .map((line) => { try { return JSON.parse(line); } catch { return null; } })
523
+ .filter((e) => e && Date.parse(e.ts) >= cutoff);
524
+ const BYPASS_WARN_THRESHOLD = Number(process.env.CONSTRUCT_DOCTOR_BYPASS_WARN) || 3;
525
+ if (recent.length >= BYPASS_WARN_THRESHOLD) {
526
+ add(
527
+ `Pre-push bypasses in last 7 days: ${recent.length} (gate may be wrong-sized — see ${bypassLogPath})`,
528
+ false,
529
+ true,
530
+ );
531
+ } else {
532
+ add(`Pre-push bypasses in last 7 days: ${recent.length}`, true);
533
+ }
534
+ } catch { /* audit log read is best-effort */ }
535
+ } else {
536
+ add('Pre-push bypass log not present (no bypasses honored yet)', true);
537
+ }
538
+
539
+ // Embed daemon log size. The daemon's own scheduler rotates every
540
+ // minute at 50MB, but a freshly-upgraded install with a 34GB legacy
541
+ // log shouldn't wait 60s. --fix-embed-log forces rotation now.
542
+
543
+ const embedLogPath = path.join(userHome, '.cx', 'runtime', 'embed-daemon.log');
544
+ if (fs.existsSync(embedLogPath)) {
545
+ const sizeMb = fs.statSync(embedLogPath).size / 1024 / 1024;
546
+ const FORCE_THRESHOLD_MB = Number(process.env.CONSTRUCT_DOCTOR_EMBED_LOG_FORCE_MB) || 500;
547
+ if (sizeMb > FORCE_THRESHOLD_MB && wantsFixEmbedLog) {
548
+ const { rotateIfOversized } = await import('../lib/logging/rotate.mjs');
549
+ println(` fix: rotating ${sizeMb.toFixed(0)}MB embed-daemon.log (gzipping may take a moment)…`);
550
+ const start = Date.now();
551
+ const seg = await rotateIfOversized(embedLogPath, { maxBytes: 1, maxSegments: 5, gzip: true });
552
+ const elapsed = ((Date.now() - start) / 1000).toFixed(0);
553
+ println(` fix: rotated to ${path.basename(seg)} in ${elapsed}s`);
554
+ add('Embed daemon log size sane', true);
555
+ } else {
556
+ const label = sizeMb > FORCE_THRESHOLD_MB
557
+ ? `Embed daemon log oversized: ${sizeMb.toFixed(0)}MB (run \`construct doctor --fix-embed-log\` to rotate now)`
558
+ : `Embed daemon log size: ${sizeMb.toFixed(1)}MB`;
559
+ add(label, sizeMb <= FORCE_THRESHOLD_MB);
560
+ }
561
+ }
562
+
563
+ // Legacy ~/.cx/<project-scoped>.jsonl files left over from before the
564
+ // cross-project state isolation refactor. Four files moved from user
565
+ // scope to <project>/.cx/: contract-violations, audit-reads, agent-log,
566
+ // intent-verifications. Pre-refactor data still sits at ~/.cx/ where it
567
+ // mixed every project together. --fix-migrate-state archives them to
568
+ // ~/.cx/legacy/<name>.<ts>.jsonl so the new writers start fresh; the
569
+ // legacy data stays readable for forensics.
570
+
571
+ const projectScopedNames = [
572
+ 'contract-violations.jsonl',
573
+ 'audit-reads.jsonl',
574
+ 'agent-log.jsonl',
575
+ 'intent-verifications.jsonl',
576
+ ];
577
+ const legacyState = projectScopedNames
578
+ .map((name) => ({ name, path: path.join(userHome, '.cx', name) }))
579
+ .filter((p) => fs.existsSync(p.path) && fs.statSync(p.path).size > 0);
580
+
581
+ if (legacyState.length > 0 && wantsFixMigrateState) {
582
+ const archiveDir = path.join(userHome, '.cx', 'legacy');
583
+ fs.mkdirSync(archiveDir, { recursive: true });
584
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
585
+ const moved = [];
586
+ for (const { name, path: src } of legacyState) {
587
+ const dest = path.join(archiveDir, `${name.replace('.jsonl', '')}.${ts}.jsonl`);
588
+ fs.renameSync(src, dest);
589
+ moved.push(path.basename(dest));
590
+ }
591
+ println(` fix: archived ${moved.length} legacy user-scope file(s) to ~/.cx/legacy/`);
592
+ for (const m of moved) println(` → ${m}`);
593
+ add('No legacy user-scope project-state files', true);
594
+ } else {
595
+ add(
596
+ legacyState.length === 0
597
+ ? 'No legacy user-scope project-state files'
598
+ : `Legacy user-scope project-state files: ${legacyState.length} (run \`construct doctor --fix-migrate-state\` to archive)`,
599
+ legacyState.length === 0,
600
+ );
601
+ }
602
+
424
603
  // Tier model selection. Construct ships with no default — at least
425
604
  // one tier must be configured (registry.json primary OR CX_MODEL_*
426
605
  // env override) before any LLM-backed workflow can run.
@@ -704,7 +883,11 @@ async function cmdDoctor() {
704
883
  try {
705
884
  const { checkParity } = await import('../lib/parity.mjs');
706
885
  const parity = checkParity({ rootDir: ROOT_DIR, homeDir: HOME });
707
- add(`Cross-surface adapter parity (${parity.summary.join(' · ')})`, parity.ok);
886
+ // Legacy-install rolls up to ok (so the gate doesn't hard-fail during
887
+ // a v1.0.10 → v1.0.13+ upgrade) but renders as a warning so the
888
+ // --fix-legacy-agents hint stays visible.
889
+ const hasLegacy = parity.surfaces.some((s) => s.status === 'legacy-install');
890
+ add(`Cross-surface adapter parity (${parity.summary.join(' · ')})`, parity.ok, hasLegacy);
708
891
  } catch {
709
892
  add('Cross-surface adapter parity', false);
710
893
  }
@@ -1643,6 +1826,10 @@ async function printIntakeHelp() {
1643
1826
  println(' construct intake done <id> [--notes=…] [--output=<path>] [--no-output=<rejected|deferred|merged>]');
1644
1827
  println(' Mark a pending packet processed. --output stamps intake_id + confidence into the artifact frontmatter (provenance).');
1645
1828
  println(' construct intake skip <id> [--reason=…] Mark a pending packet skipped (preserves audit trail).');
1829
+ println(' construct intake process [--dry-run] [--wait[=<sec>]]');
1830
+ println(' One-shot inbox sweep — same logic the daemon runs, exits when the queue is empty.');
1831
+ println(' Concurrency is controlled by a poll lock — fail-fast by default, --wait[=<sec>] blocks for up to N seconds.');
1832
+ println(' --dry-run lists candidate inboxes without ingesting.');
1646
1833
  println(' construct intake reopen <id> Move a processed or skipped packet back to pending.');
1647
1834
  println(' construct intake quarantine list List packets held for human review (low confidence or close margin).');
1648
1835
  println(' construct intake quarantine show <id> Show a quarantined packet with top-3 candidate types and margins.');
@@ -1910,6 +2097,10 @@ async function cmdIntake(args) {
1910
2097
  return await cmdIntakeIntegrate(id, args.slice(2), cwd);
1911
2098
  }
1912
2099
 
2100
+ if (sub === 'process') {
2101
+ return await cmdIntakeProcess(args.slice(1), cwd);
2102
+ }
2103
+
1913
2104
  if (sub === 'config') {
1914
2105
  return await cmdIntakeConfig(args.slice(1));
1915
2106
  }
@@ -1922,10 +2113,75 @@ async function cmdIntake(args) {
1922
2113
  return await cmdIntakeNeedsAsr(args.slice(1));
1923
2114
  }
1924
2115
 
1925
- errorln(`Unknown intake subcommand: ${sub}. Available: list, show, done, skip, reopen, quarantine, reroute, integrate, config, extraction-warnings, needs-asr`);
2116
+ errorln(`Unknown intake subcommand: ${sub}. Available: list, show, done, skip, reopen, process, quarantine, reroute, integrate, config, extraction-warnings, needs-asr`);
1926
2117
  process.exit(1);
1927
2118
  }
1928
2119
 
2120
+ async function cmdIntakeProcess(args, cwd) {
2121
+ const dryRun = args.includes('--dry-run');
2122
+ const waitFlag = args.find((a) => a === '--wait' || a.startsWith('--wait='));
2123
+ const { InboxWatcher, resolveInboxDirs } = await import('../lib/embed/inbox.mjs');
2124
+
2125
+ if (dryRun) {
2126
+ const dirs = resolveInboxDirs(cwd, process.env);
2127
+ if (dirs.length === 0) {
2128
+ println('No inbox directories configured. Run `construct intake config` to add one.');
2129
+ return;
2130
+ }
2131
+ println('Would scan:');
2132
+ for (const d of dirs) println(` ${d}`);
2133
+ println('');
2134
+ println('(dry-run — no files processed; re-run without --dry-run to actually ingest.)');
2135
+ return;
2136
+ }
2137
+
2138
+ // Concurrency is controlled by the intake poll lock inside
2139
+ // InboxWatcher.poll. Default behavior is fail-fast: if the daemon (or
2140
+ // another CLI invocation) is mid-poll, raise an actionable error rather
2141
+ // than serializing silently. --wait[=<seconds>] opts into blocking for
2142
+ // up to N seconds (default 30s when --wait is bare).
2143
+
2144
+ let waitMs = 0;
2145
+ if (waitFlag) {
2146
+ const m = /^--wait(?:=(\d+))?$/.exec(waitFlag);
2147
+ const seconds = m && m[1] ? Number(m[1]) : 30;
2148
+ waitMs = Math.max(0, seconds) * 1000;
2149
+ }
2150
+
2151
+ const watcher = new InboxWatcher({ rootDir: cwd, env: process.env, cwd });
2152
+ let result;
2153
+ try {
2154
+ result = await watcher.poll({ waitMs, actor: 'construct intake process' });
2155
+ } catch (err) {
2156
+ const { POLL_LOCK_BUSY } = await import('../lib/intake/poll-lock.mjs');
2157
+ if (err && err.code === POLL_LOCK_BUSY) {
2158
+ const holder = err.holder || {};
2159
+ errorln('Another intake poll is in progress.');
2160
+ errorln(` holder: ${holder.actor || 'unknown'} (pid ${holder.pid})`);
2161
+ errorln(` started: ${holder.startedAt || 'unknown'}`);
2162
+ errorln('');
2163
+ errorln('Choose one:');
2164
+ errorln(' construct intake process --wait Block until the holder finishes (default 30s).');
2165
+ errorln(' construct intake process --wait=<sec> Block for a custom window.');
2166
+ errorln(' construct down Stop background services, then retry.');
2167
+ process.exit(2);
2168
+ }
2169
+ throw err;
2170
+ }
2171
+
2172
+ println(`Processed: ${result.processed.length}`);
2173
+ println(`Skipped (unchanged): ${result.skipped}`);
2174
+ if (result.errors.length) {
2175
+ println(`Errors: ${result.errors.length}`);
2176
+ for (const e of result.errors) println(` ${e.path}: ${e.error}`);
2177
+ }
2178
+ for (const item of result.processed) {
2179
+ const note = item.needsAsr ? ' (queued for ASR)' : '';
2180
+ println(` + ${item.path}${note}`);
2181
+ }
2182
+ if (result.errors.length) process.exitCode = 1;
2183
+ }
2184
+
1929
2185
  async function cmdIntakeQuarantine(args, cwd) {
1930
2186
  const action = args[0];
1931
2187
  const { listQuarantine, readQuarantine } = await import('../lib/intake/quarantine.mjs');
@@ -4584,14 +4840,6 @@ const handlers = new Map([
4584
4840
  process.exit(1);
4585
4841
  }
4586
4842
  }],
4587
- ['policy:list', async (args) => {
4588
- const { listPolicies, formatPolicyList } = await import('../lib/policy/unified-gates.mjs');
4589
- if (args.includes('--json')) {
4590
- println(JSON.stringify(listPolicies(), null, 2));
4591
- } else {
4592
- process.stdout.write(formatPolicyList());
4593
- }
4594
- }],
4595
4843
  ['policy', async (args) => {
4596
4844
  const sub = args[0];
4597
4845
  if (!sub || sub === '--help' || sub === '-h') {
@@ -4602,7 +4850,7 @@ const handlers = new Map([
4602
4850
  if (sub === 'show') {
4603
4851
  const { readFileSync, existsSync } = await import('node:fs');
4604
4852
  const { join } = await import('node:path');
4605
- const inventoryPath = join(ROOT_DIR, 'agents', 'policy-inventory.json');
4853
+ const inventoryPath = join(ROOT_DIR, 'specialists', 'policy-inventory.json');
4606
4854
  if (!existsSync(inventoryPath)) {
4607
4855
  errorln('specialists/policy-inventory.json not found');
4608
4856
  process.exit(1);
@@ -58,8 +58,29 @@ try {
58
58
  }
59
59
  } catch { /* fall through */ }
60
60
 
61
- // Require a package.json at the install target so monorepo nested installs
62
- // don't accidentally trigger staging in the wrong directory.
61
+ // `npm i -g @geraldmaron/construct` runs the postinstall with
62
+ // npm_config_global=true. Wire the `construct` front-door agent into the
63
+ // user's home directories so it's reachable from every host (Claude Code,
64
+ // Codex, Copilot, OpenCode) immediately after a global install. Specialists
65
+ // stay project-only and land when the user runs `construct init` in a repo.
66
+
67
+ if (process.env.npm_config_global === 'true' || process.env.npm_config_global === true) {
68
+ const syncScript = path.join(PKG_ROOT, 'scripts', 'sync-specialists.mjs');
69
+ if (existsSync(syncScript)) {
70
+ log('global install detected; syncing front-door agent into ~/');
71
+ const result = spawnSync(process.execPath, [syncScript, '--global'], {
72
+ stdio: 'inherit',
73
+ });
74
+ if (result.status !== 0) {
75
+ log(`global sync failed (exit ${result.status}); run \`construct sync --global\` manually`);
76
+ }
77
+ }
78
+ process.exit(0);
79
+ }
80
+
81
+ // Project install path: require a package.json at the install target so
82
+ // monorepo nested installs don't accidentally trigger staging in the wrong
83
+ // directory.
63
84
 
64
85
  const consumerPkgPath = path.join(initCwd, 'package.json');
65
86
  if (!existsSync(consumerPkgPath)) {
package/lib/auto-docs.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * lib/auto-docs.mjs — regenerate managed regions in markdown docs and build the MkDocs site tree.
2
+ * lib/auto-docs.mjs — regenerate managed regions in markdown docs and generated docs-site references.
3
3
  *
4
4
  * Managed regions are HTML comment markers in the form:
5
5
  * <!-- AUTO:region-name -->
@@ -9,8 +9,9 @@
9
9
  * Running regenerateDocs() is idempotent. With check:true it returns whether
10
10
  * anything would change without writing files — the mode CI uses to detect drift.
11
11
  *
12
- * buildSite() writes site/docs/ from the same sources feeding the AUTO regions
13
- * so the GitHub Pages site never drifts from the in-repo markdown.
12
+ * buildSite() is retained for compatibility; the public site now renders docs/
13
+ * directly through Next.js (apps/docs/) and buildFumadocsReference() emits
14
+ * generated reference Markdown into docs/reference/.
14
15
  */
15
16
 
16
17
  import fs from 'node:fs';
@@ -82,7 +83,7 @@ function buildCoreDocsContract() {
82
83
 
83
84
  const DIR_DESCRIPTIONS = {
84
85
  agents: 'Registry and generated platform adapter chains',
85
- apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)',
86
+ apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Next.js docs site)',
86
87
  bin: 'CLI entrypoint (`construct`)',
87
88
  claude: 'Claude Code integration (agents, settings template)',
88
89
  commands: 'Command prompt assets',
@@ -93,7 +94,7 @@ const DIR_DESCRIPTIONS = {
93
94
  opencode: 'OpenCode integration config',
94
95
  personas: 'Persona prompt definitions',
95
96
  rules: 'Coding and quality standards',
96
- site: 'MkDocs source for the GitHub Pages documentation site',
97
+ site: 'Legacy docs-site output',
97
98
  skills: 'Reusable domain knowledge files',
98
99
  tests: 'Test suite',
99
100
  };
@@ -151,7 +152,7 @@ function buildHooksTable(rootDir) {
151
152
  }
152
153
 
153
154
  function buildAgentsTable(rootDir) {
154
- const registryPath = path.join(rootDir, 'agents', 'registry.json');
155
+ const registryPath = path.join(rootDir, 'specialists', 'registry.json');
155
156
  let registry;
156
157
  try { registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')); } catch { return ''; }
157
158
 
@@ -237,12 +238,12 @@ export async function regenerateDocs({ rootDir, check = false } = {}) {
237
238
  * Check CLI command coverage against how-to links in docs/README.md.
238
239
  *
239
240
  * Returns an object with:
240
- * - covered: string[] commands that have a linked how-to
241
- * - uncovered: string[] commands with no how-to link in docs/README.md
241
+ * - covered: string[] commands that have a linked guide
242
+ * - uncovered: string[] commands with no linked guide in docs/README.md
242
243
  * - total: number
243
244
  *
244
- * A command is considered covered if its name appears in any markdown link
245
- * inside the how-to guides section of docs/README.md.
245
+ * A command is considered covered if its name appears in docs/README.md or in
246
+ * a linked cookbook/how-to guide.
246
247
  */
247
248
  export function checkDocsCoverage({ rootDir } = {}) {
248
249
  rootDir = rootDir ?? process.cwd();
@@ -251,11 +252,10 @@ export function checkDocsCoverage({ rootDir } = {}) {
251
252
  // Extract all href targets from markdown links in docs/README.md
252
253
  const linkTargets = [...docsReadme.matchAll(/\[.*?\]\((.*?)\)/g)].map(m => m[1]);
253
254
 
254
- // Build a combined corpus: docs/README.md + every linked how-to file
255
- const howToDir = path.join(rootDir, 'docs', 'how-to');
255
+ // Build a combined corpus: docs/README.md + every linked guide file
256
256
  let corpus = docsReadme;
257
257
  for (const target of linkTargets) {
258
- if (!target.startsWith('./how-to/')) continue;
258
+ if (!target.startsWith('./how-to/') && !target.startsWith('./cookbook/')) continue;
259
259
  const filePath = path.join(rootDir, 'docs', target.replace(/^\.\//, ''));
260
260
  const content = readFile(filePath);
261
261
  if (content) corpus += '\n' + content;
@@ -304,18 +304,16 @@ export function checkDocsCoverage({ rootDir } = {}) {
304
304
  return { covered, uncovered, total: covered.length + uncovered.length };
305
305
  }
306
306
 
307
- // Categories surfaced in the Fumadocs reference. Internal commands (e.g.,
307
+ // Categories surfaced in the generated CLI reference. Internal commands (e.g.,
308
308
  // `construct hook <name>`) are not user-facing reference material.
309
309
  const REFERENCE_CATEGORIES = [
310
- 'Services',
311
- 'Agents & Sync',
310
+ 'Core',
312
311
  'Work',
313
- 'Embed',
314
312
  'Models & Integrations',
315
- 'Providers',
313
+ 'Integrations',
316
314
  'Observability',
317
- 'Docs',
318
315
  'Diagnostics',
316
+ 'Advanced',
319
317
  ];
320
318
 
321
319
  function slugify(category) {
@@ -410,9 +408,12 @@ function renderAgentsPage(rootDir) {
410
408
  }
411
409
 
412
410
  /**
413
- * Emit MDX reference pages into docs/reference/ for the Fumadocs site.
414
- * Mirrors the data sources behind AUTO regions and buildSite() but writes
415
- * Markdown files Fumadocs can render directly (no MkDocs intermediate).
411
+ * Emit MDX reference pages into docs/reference/ for the Next.js docs site
412
+ * in apps/docs/. Mirrors the data sources behind AUTO regions and buildSite()
413
+ * but writes Markdown files the docs app can pull in directly (no MkDocs
414
+ * intermediate). Name kept for backwards compatibility with callers that
415
+ * still import buildFumadocsReference; the rendering layer is now plain
416
+ * Next.js + @next/mdx.
416
417
  */
417
418
  export function buildFumadocsReference({ rootDir } = {}) {
418
419
  rootDir = rootDir ?? process.cwd();
@@ -749,8 +749,10 @@ for (const cat of Object.keys(CLI_COMMANDS_BY_CATEGORY)) {
749
749
  export const CATEGORY_ORDER = [
750
750
  'Core',
751
751
  'Work',
752
+ 'Models & Integrations',
752
753
  'Integrations',
753
754
  'Observability',
755
+ 'Diagnostics',
754
756
  'Advanced',
755
757
  ];
756
758