@geraldmaron/construct 1.0.4 → 1.0.5

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 +51 -42
  2. package/bin/construct +256 -4
  3. package/commands/understand/research.md +5 -3
  4. package/lib/auto-docs.mjs +2 -2
  5. package/lib/cli-commands.mjs +44 -0
  6. package/lib/comment-lint.mjs +7 -1
  7. package/lib/config/schema.mjs +3 -0
  8. package/lib/flavors/loader.mjs +136 -0
  9. package/lib/hooks/agent-tracker.mjs +22 -3
  10. package/lib/hooks/pre-push-gate.mjs +14 -1
  11. package/lib/hooks/session-optimize.mjs +3 -2
  12. package/lib/hooks/session-reflect.mjs +68 -0
  13. package/lib/init-unified.mjs +25 -2
  14. package/lib/intake/classify.mjs +61 -183
  15. package/lib/intake/prepare.mjs +7 -0
  16. package/lib/intake/tables/creative.mjs +94 -0
  17. package/lib/intake/tables/operations.mjs +85 -0
  18. package/lib/intake/tables/research.mjs +85 -0
  19. package/lib/intake/tables/rnd.mjs +175 -0
  20. package/lib/knowledge/research-store.mjs +109 -0
  21. package/lib/observation-store.mjs +19 -0
  22. package/lib/outcomes/aggregate.mjs +104 -0
  23. package/lib/outcomes/record.mjs +115 -0
  24. package/lib/parity.mjs +6 -9
  25. package/lib/profiles/lifecycle.mjs +388 -0
  26. package/lib/profiles/loader.mjs +123 -0
  27. package/lib/profiles/validate-custom.mjs +114 -0
  28. package/lib/reflect/extractor.mjs +193 -0
  29. package/lib/reflect.mjs +89 -2
  30. package/lib/sandbox.mjs +102 -0
  31. package/package.json +6 -1
  32. package/personas/construct.md +20 -20
  33. package/platforms/claude/settings.template.json +13 -0
  34. package/scripts/sync-agents.mjs +11 -0
  35. package/skills/roles/architect.ai-systems.md +4 -2
  36. package/skills/roles/architect.data.md +4 -2
  37. package/skills/roles/architect.enterprise.md +4 -2
  38. package/skills/roles/architect.integration.md +4 -2
  39. package/skills/roles/architect.md +7 -5
  40. package/skills/roles/architect.platform.md +4 -2
  41. package/skills/roles/data-analyst.experiment.md +4 -2
  42. package/skills/roles/data-analyst.md +9 -7
  43. package/skills/roles/data-analyst.product-intelligence.md +4 -2
  44. package/skills/roles/data-analyst.product.md +4 -2
  45. package/skills/roles/data-analyst.telemetry.md +4 -2
  46. package/skills/roles/data-engineer.pipeline.md +4 -2
  47. package/skills/roles/data-engineer.vector-retrieval.md +4 -2
  48. package/skills/roles/data-engineer.warehouse.md +4 -2
  49. package/skills/roles/debugger.md +7 -5
  50. package/skills/roles/designer.accessibility.md +4 -2
  51. package/skills/roles/designer.md +10 -8
  52. package/skills/roles/engineer.ai.md +4 -2
  53. package/skills/roles/engineer.data.md +5 -3
  54. package/skills/roles/engineer.md +14 -12
  55. package/skills/roles/engineer.platform.md +5 -3
  56. package/skills/roles/operator.docs.md +6 -4
  57. package/skills/roles/operator.md +6 -4
  58. package/skills/roles/operator.release.md +4 -2
  59. package/skills/roles/operator.sre.md +5 -3
  60. package/skills/roles/orchestrator.md +5 -3
  61. package/skills/roles/product-manager.ai-product.md +4 -2
  62. package/skills/roles/product-manager.business-strategy.md +4 -2
  63. package/skills/roles/product-manager.enterprise.md +4 -2
  64. package/skills/roles/product-manager.growth.md +4 -2
  65. package/skills/roles/product-manager.md +6 -4
  66. package/skills/roles/product-manager.platform.md +4 -2
  67. package/skills/roles/product-manager.product.md +4 -2
  68. package/skills/roles/qa.ai-eval.md +4 -2
  69. package/skills/roles/qa.api-contract.md +4 -2
  70. package/skills/roles/qa.data-pipeline.md +4 -2
  71. package/skills/roles/qa.md +7 -5
  72. package/skills/roles/qa.test-automation.md +5 -3
  73. package/skills/roles/qa.web-ui.md +4 -2
  74. package/skills/roles/researcher.explorer.md +4 -2
  75. package/skills/roles/researcher.md +11 -9
  76. package/skills/roles/researcher.ux.md +4 -2
  77. package/skills/roles/reviewer.devil-advocate.md +4 -2
  78. package/skills/roles/reviewer.evaluator.md +4 -2
  79. package/skills/roles/reviewer.md +14 -12
  80. package/skills/roles/reviewer.trace.md +4 -2
  81. package/skills/roles/security.ai.md +4 -2
  82. package/skills/roles/security.appsec.md +4 -2
  83. package/skills/roles/security.cloud.md +4 -2
  84. package/skills/roles/security.legal-compliance.md +4 -2
  85. package/skills/roles/security.md +7 -5
  86. package/skills/roles/security.privacy.md +4 -2
  87. package/skills/roles/security.supply-chain.md +4 -2
  88. package/templates/docs/persona-artifact.md +36 -0
  89. package/templates/docs/research-finding.md +26 -0
  90. package/templates/docs/skill-artifact.md +27 -0
package/README.md CHANGED
@@ -1,73 +1,78 @@
1
1
  # Construct
2
2
 
3
- > One AI interface. 28 specialists. Hard gates. Runs locally or deploys for teams.
3
+ > Heads up. Construct is an open source project I started. I am not a developer. This is a side project. There may be bugs, there may be defects, but I'm building it to learn in public. If you'd like to contribute, please do.
4
4
 
5
- Construct is a deployable AI R&D operating system. You address one persona — `construct` — in Claude Code, OpenCode, Codex, Cursor, or Copilot. Behind that persona is a team of 28 specialists (architect, engineer, reviewer, QA, security, designer, …) under typed contracts and enforced gates. Sessions survive boundary changes via durable state in `.cx/`, beads, and the vector index. Construct runs locally for individual users (the default) and can be deployed centrally for shared team usage with shared memory, telemetry, queues, and policy.
5
+ One AI interface. A team of specialists behind it. Hard gates. Runs locally, or deploys for teams.
6
6
 
7
- **Full docs:** [`geraldmaron.github.io/construct/v2/`](https://geraldmaron.github.io/construct/v2/) (Phase 1) · while the new docs site is rolling out, the legacy MkDocs site still serves the root URL.
7
+ Construct sits on top of Claude Code, OpenCode, Codex, Cursor, and Copilot. You talk to one persona called `construct`. Behind it are 28 specialists (architect, engineer, reviewer, QA, security, designer, and more) under typed contracts and enforced gates. 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.
8
8
 
9
- ## Getting Started
9
+ 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.
10
10
 
11
- ### Step 1: Install CLI (one-time, per machine)
11
+ Full docs: [`geraldmaron.github.io/construct/v2/`](https://geraldmaron.github.io/construct/v2/).
12
+
13
+ ## Getting started
14
+
15
+ Install the CLI (once per machine):
12
16
 
13
17
  ```bash
14
18
  npm install -g @geraldmaron/construct
15
19
  ```
16
20
 
17
- ### Step 2: Machine Setup (one-time, per machine)
18
-
19
- First time on a new machine, bootstrap local services. `construct install` can start local Postgres/pgvector via Docker; traces are written locally by default and remote telemetry export is optional:
21
+ Bootstrap local services (once per machine):
20
22
 
21
23
  ```bash
22
24
  construct install --yes
23
- # Local services:
24
- # Traces: .cx/traces/*.jsonl
25
- # Postgres: postgresql://construct:construct@127.0.0.1:54329/construct
26
25
  ```
27
26
 
28
- ### Step 3: Initialize Project (per project)
27
+ Initialize a project:
29
28
 
30
29
  ```bash
31
30
  cd ~/your-project
32
31
  construct init --auto-start
33
32
  ```
34
33
 
35
- The postinstall hook stages `.construct/` and `.claude/` into your project, so hooks, agents, and slash commands are wired up automatically. Peers who clone your repo and run `npm install` get the same setup with no extra steps.
34
+ Open your editor and talk to `@construct`. A walkthrough lives in `construct_guide.md` at your project root.
36
35
 
37
- Open your editor and address `@construct`. A friendly orientation lives in `construct_guide.md` at your project root.
36
+ No Node? Try `brew install geraldmaron/construct/construct`. Cloning a project that already uses Construct? `npx -y @geraldmaron/construct init` wires it up.
38
37
 
39
- If you cloned a project that does not yet pin Construct, run `npx -y @geraldmaron/construct init` once to wire it up. No Node at all? `brew install geraldmaron/construct/construct`.
38
+ [Five minute walkthrough](https://geraldmaron.github.io/construct/v2/docs/start).
40
39
 
41
- [Full 5-minute walkthrough →](https://geraldmaron.github.io/construct/v2/docs/start)
42
-
43
- ## What you can do with it
40
+ ## What you can do
44
41
 
45
42
  | If you want to... | Read |
46
43
  |---|---|
47
- | Install + first task | [Start](https://geraldmaron.github.io/construct/v2/docs/start) |
48
- | Understand how it works | [Concepts → Architecture](https://geraldmaron.github.io/construct/v2/docs/concepts/architecture) |
49
- | Pick a deployment mode | [Concepts → Deployment model](https://geraldmaron.github.io/construct/v2/docs/concepts/deployment-model) |
50
- | Drop a signal and triage it | [Concepts → Intake and triage](https://geraldmaron.github.io/construct/v2/docs/concepts/intake-and-triage) |
51
- | Add a custom specialist | [Cookbook → Add a custom agent](https://geraldmaron.github.io/construct/v2/docs/cookbook/add-a-custom-agent) |
52
- | Fix a blocked commit or red CI | [Cookbook → Fix a policy violation](https://geraldmaron.github.io/construct/v2/docs/cookbook/fix-a-policy-violation) |
53
- | Plug in your own LLM | [Cookbook → Plug in your own LLM](https://geraldmaron.github.io/construct/v2/docs/cookbook/plug-in-your-own-llm) |
54
- | Deploy to AWS | [Cookbook → Deploy to AWS](https://geraldmaron.github.io/construct/v2/docs/cookbook/deploy-to-aws) |
55
- | Look up a CLI command | [Reference → CLI](https://geraldmaron.github.io/construct/v2/docs/reference/cli) |
56
- | Recover from an outage | [Operations → Troubleshooting](https://geraldmaron.github.io/construct/v2/docs/operations/troubleshooting) |
44
+ | Install and run a first task | [Start](https://geraldmaron.github.io/construct/v2/docs/start) |
45
+ | Understand how it works | [Architecture](https://geraldmaron.github.io/construct/v2/docs/concepts/architecture) |
46
+ | Pick a deployment mode | [Deployment model](https://geraldmaron.github.io/construct/v2/docs/concepts/deployment-model) |
47
+ | Drop a signal and triage it | [Intake and triage](https://geraldmaron.github.io/construct/v2/docs/concepts/intake-and-triage) |
48
+ | Add a custom specialist | [Add a custom agent](https://geraldmaron.github.io/construct/v2/docs/cookbook/add-a-custom-agent) |
49
+ | Fix a blocked commit or red CI | [Fix a policy violation](https://geraldmaron.github.io/construct/v2/docs/cookbook/fix-a-policy-violation) |
50
+ | Plug in your own LLM | [Plug in your own LLM](https://geraldmaron.github.io/construct/v2/docs/cookbook/plug-in-your-own-llm) |
51
+ | Look up a CLI command | [CLI reference](https://geraldmaron.github.io/construct/v2/docs/reference/cli) |
57
52
 
58
53
  Works with Anthropic, OpenRouter, Ollama, and other OpenAI-compatible providers.
59
54
 
60
- ## Deployable: solo, team, or enterprise
55
+ ## Deployment modes
56
+
57
+ Three modes. `solo` is the default and runs everything locally. Filesystem queue, local repo state, optional Postgres via Docker, local JSONL traces. If every cloud service goes down, you still work from `plan.md`, `.cx/context.md`, beads, git, and the local vector index.
61
58
 
62
- Construct has three deployment modes. `solo` (the default) runs everything locally — filesystem queue, local repo state, optional Postgres/Docker, and local JSONL traces — so if every cloud service goes down you still work from `plan.md`, `.cx/context.md`, beads, git, and the local vector index. `team` promotes the intake queue to Postgres with row-locked worker claims, shares memory across the team, runs workers in a Docker pool, centralizes telemetry through Langfuse-compatible, HTTP, or OTLP export, and routes MCP through a broker. `enterprise` adds tenant isolation, RBAC/ABAC scaffolding, isolated worker containers, signed MCP allowlists, and mandatory audit. Pick or change modes with `construct config mode [solo|team|enterprise]`. [Concepts → Deployment model](https://geraldmaron.github.io/construct/v2/docs/concepts/deployment-model).
59
+ `team` promotes the intake queue to Postgres with row-locked worker claims. Shared memory, Docker worker pool, centralized telemetry, MCP through a broker.
60
+
61
+ `enterprise` adds tenant isolation, RBAC and ABAC scaffolding, isolated worker containers, signed MCP allowlists, and mandatory audit.
62
+
63
+ Pick or change modes with `construct config mode [solo|team|enterprise]`. [Deployment model](https://geraldmaron.github.io/construct/v2/docs/concepts/deployment-model).
63
64
 
64
65
  ## Signals to R&D
65
66
 
66
- Anything dropped into `.cx/inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified into an R&D intake type — bug, user-signal, experiment, eval-finding, architecture, incident, security, requirement, research, ops, or legal-compliance and assigned a primary owner persona with a recommended handoff chain. Inspect with `construct intake list / show <id>`, generate a task graph with `construct graph from-intake <id>`, and update node status with verifiable evidence as work progresses. The classifier is deterministic and runs in the daemon (no LLM); the agent in your editor handles the actual analysis. [Concepts → Intake and triage](https://geraldmaron.github.io/construct/v2/docs/concepts/intake-and-triage).
67
+ Anything dropped into `.cx/inbox/` (a bug report, a customer comment, a competitor PDF, a postmortem draft) is classified into one of: bug, user-signal, experiment, eval-finding, architecture, incident, security, requirement, research, ops, legal-compliance. 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/v2/docs/concepts/intake-and-triage).
67
68
 
68
69
  ## Hard gates
69
70
 
70
- 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) and can only be bypassed with explicit env vars so every exception leaves an audit trail. [Concepts → Gates and enforcement](https://geraldmaron.github.io/construct/v2/docs/concepts/gates-and-enforcement).
71
+ 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/v2/docs/concepts/gates-and-enforcement).
72
+
73
+ ## Learning loops
74
+
75
+ 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.
71
76
 
72
77
  ## Core commands
73
78
 
@@ -83,7 +88,9 @@ Every code mutation runs through enforcement: no secrets committed, tests green,
83
88
  | `construct install` | Machine setup: install Docker, cm, and bootstrap config |
84
89
  | `construct intake` | View and process R&D intake queue |
85
90
  | `construct models` | Manage AI model assignments |
91
+ | `construct profile` | Manage the active org profile and its lifecycle (draft, promote, archive, health) |
86
92
  | `construct recommendations` | View and manage artifact recommendations |
93
+ | `construct sandbox` | Isolated tmpdir-based environment for QA / specialist dry-runs |
87
94
  | `construct status` | Show system health and credentials |
88
95
  | `construct stop` | Stop all running services |
89
96
  | `construct sync` | Sync agent adapters to AI tools |
@@ -101,6 +108,7 @@ Every code mutation runs through enforcement: no secrets committed, tests green,
101
108
  | `construct infer` | Infer schema from documents |
102
109
  | `construct ingest` | Convert documents to indexed markdown |
103
110
  | `construct integrations` | Check and manage external system connections |
111
+ | `construct knowledge` | Query, index, or add to the project knowledge base |
104
112
  | `construct memory` | Inspect memory layer |
105
113
  | `construct reflect` | Capture improvement feedback |
106
114
  | `construct search` | Hybrid search across project state |
@@ -158,10 +166,10 @@ Every code mutation runs through enforcement: no secrets committed, tests green,
158
166
 
159
167
  ## For contributors
160
168
 
161
- - [`CONTRIBUTING.md`](./CONTRIBUTING.md) branch workflow, gates, review expectations.
162
- - [`CHANGELOG.md`](./CHANGELOG.md) release history.
163
- - [`docs/concepts/architecture.md`](./docs/concepts/architecture.md) canonical architecture (rendered on the docs site at [Concepts → Architecture](https://geraldmaron.github.io/construct/docs/concepts/architecture)).
164
- - [`AGENTS.md`](./AGENTS.md) agent operating contract.
169
+ - [`CONTRIBUTING.md`](./CONTRIBUTING.md). Branch workflow, gates, review expectations.
170
+ - [`CHANGELOG.md`](./CHANGELOG.md). Release history.
171
+ - [`docs/concepts/architecture.md`](./docs/concepts/architecture.md). Canonical architecture.
172
+ - [`AGENTS.md`](./AGENTS.md). Agent operating contract.
165
173
 
166
174
  ## Project structure
167
175
 
@@ -169,7 +177,7 @@ Every code mutation runs through enforcement: no secrets committed, tests green,
169
177
  ```text
170
178
  construct/
171
179
  ├── agents Registry and generated platform adapter chains
172
- ├── apps User-facing apps shipped from this repo (e.g., apps/docs/ Fumadocs docs site)
180
+ ├── apps User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)
173
181
  ├── bin CLI entrypoint (`construct`)
174
182
  ├── commands Command prompt assets
175
183
  ├── dashboard
@@ -180,6 +188,7 @@ construct/
180
188
  ├── lib Core runtime: CLI, hooks, MCP, status, sync, workflow
181
189
  ├── personas Persona prompt definitions
182
190
  ├── platforms
191
+ ├── profiles
183
192
  ├── providers
184
193
  ├── rules Coding and quality standards
185
194
  ├── schemas
@@ -193,16 +202,16 @@ construct/
193
202
 
194
203
  ## Uninstall
195
204
 
196
- Run the uninstaller first, then drop the package:
205
+ Run the uninstaller first, then remove the package:
197
206
 
198
207
  ```bash
199
208
  construct uninstall # interactive; pick what to remove
200
209
  npm uninstall @geraldmaron/construct
201
210
  ```
202
211
 
203
- `construct uninstall` probes both project-scope (`.construct/`, the Construct-owned `.claude/agents/` + `.claude/commands/` files, hooks/mcpServers Construct added to `.claude/settings.json`) and machine-scope state (`~/.cx/`, `~/.construct/workspace/`, the embedding model cache, the local Postgres container). Auto-risk items are removed by default; ask-risk items (Postgres data, API keys, AGENTS.md/plan.md you may have edited) are skipped unless you opt in.
212
+ `construct uninstall` finds both project state (`.construct/`, the Construct-owned files under `.claude/agents/` and `.claude/commands/`, hooks and mcpServers Construct added to `.claude/settings.json`) and machine state (`~/.cx/`, `~/.construct/workspace/`, the embedding model cache, the local Postgres container). Auto-risk items go by default. Ask-risk items (Postgres data, API keys, files you may have edited) are skipped unless you opt in.
204
213
 
205
- It never touches Docker itself, Homebrew CLIs like `cm`/`cass`, the pgvector image, or anything you've added to `.claude/settings.json` by hand. Those appear in the final summary as follow-ups you can run if you want.
214
+ It will not touch Docker itself, Homebrew CLIs like `cm` and `cass`, the pgvector image, or anything you added to `.claude/settings.json` by hand. Those appear in the final summary as follow-ups.
206
215
 
207
216
  Useful flags:
208
217
 
@@ -210,8 +219,8 @@ Useful flags:
210
219
  construct uninstall --dry-run # show the plan, change nothing
211
220
  construct uninstall --yes # non-interactive, auto-risk only
212
221
  construct uninstall --yes --all # non-interactive, everything
213
- construct uninstall --scope=project # only this project; leave ~/.construct alone
214
- construct uninstall --keep-state # only .construct/ + .claude/; keep .cx/, ~/.construct, Postgres
222
+ construct uninstall --scope=project # only this project, leave ~/.construct alone
223
+ construct uninstall --keep-state # only .construct/ and .claude/, keep .cx/, ~/.construct, Postgres
215
224
  ```
216
225
 
217
226
  ## License
package/bin/construct CHANGED
@@ -186,14 +186,49 @@ function runNodeScript(scriptPath, args = [], extraEnv = {}) {
186
186
  async function cmdStatus() {
187
187
  const jsonOutput = restArgsCache.includes('--json');
188
188
  const status = await buildStatus({ rootDir: ROOT_DIR, cwd: process.cwd(), homeDir: HOME, env: process.env });
189
-
189
+
190
190
  if (jsonOutput) {
191
191
  println(JSON.stringify(status, null, 2));
192
192
  return;
193
193
  }
194
-
194
+
195
195
  println(formatStatusReport(status));
196
-
196
+
197
+ // Append the learning-loops summary so the operator gets a single answer
198
+ // to "is this thing actually learning?" without remembering a second
199
+ // command. Cheap: just reads .cx/observations + .cx/outcomes summaries.
200
+ try {
201
+ const { resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
202
+ const { readSummary } = await import('../lib/outcomes/aggregate.mjs');
203
+ const fs = await import('node:fs');
204
+ const path = await import('node:path');
205
+ const cwd = process.cwd();
206
+ const active = resolveActiveProfile(cwd);
207
+ const obsIdxPath = path.join(cwd, '.cx', 'observations', 'index.json');
208
+ let obsTotal = 0;
209
+ let obs24h = 0;
210
+ if (fs.existsSync(obsIdxPath)) {
211
+ const idx = JSON.parse(fs.readFileSync(obsIdxPath, 'utf8'));
212
+ obsTotal = idx.length;
213
+ const since = Date.now() - 24 * 60 * 60 * 1000;
214
+ obs24h = idx.filter((e) => Date.parse(e.createdAt) >= since).length;
215
+ }
216
+ const researchDir = path.join(cwd, '.cx', 'knowledge', 'external', 'research');
217
+ const researchCount = fs.existsSync(researchDir)
218
+ ? fs.readdirSync(researchDir).filter((f) => f.endsWith('.md')).length
219
+ : 0;
220
+ const summary = readSummary(cwd);
221
+ const outcomeRoles = summary?.roles ? Object.keys(summary.roles).length : 0;
222
+
223
+ println('');
224
+ println('Learning loops');
225
+ println('──────────────');
226
+ println(` Profile ${active.id} (${active.displayName || ''})`);
227
+ println(` Observations ${obsTotal} total, ${obs24h} in last 24h`);
228
+ println(` Research files ${researchCount}`);
229
+ println(` Outcome roles ${outcomeRoles}`);
230
+ println(` Detail: npm run learning:status`);
231
+ } catch { /* best-effort surface; never block status */ }
197
232
  }
198
233
 
199
234
  async function cmdShow() {
@@ -3233,6 +3268,27 @@ async function cmdHook(args) {
3233
3268
  const [command, ...rest] = process.argv.slice(2);
3234
3269
  const restArgsCache = rest;
3235
3270
 
3271
+ // Parse `--key=value` and repeatable `--source-url=<url>` for `construct knowledge add`.
3272
+ function parseKnowledgeAddArgs(args) {
3273
+ const opts = { slug: null, topic: null, confidence: 'inferred', sources: [] };
3274
+ for (const arg of args) {
3275
+ if (arg.startsWith('--slug=')) opts.slug = arg.slice('--slug='.length);
3276
+ else if (arg.startsWith('--topic=')) opts.topic = arg.slice('--topic='.length);
3277
+ else if (arg.startsWith('--confidence=')) opts.confidence = arg.slice('--confidence='.length);
3278
+ else if (arg.startsWith('--source-url=')) opts.sources.push({ url: arg.slice('--source-url='.length) });
3279
+ else if (arg.startsWith('--source=')) { /* tag, ignored for now */ }
3280
+ }
3281
+ return opts;
3282
+ }
3283
+
3284
+ // Read all of stdin as a UTF-8 string. Returns '' if nothing was piped.
3285
+ async function readStdin() {
3286
+ if (process.stdin.isTTY) return '';
3287
+ let raw = '';
3288
+ for await (const chunk of process.stdin) raw += chunk;
3289
+ return raw;
3290
+ }
3291
+
3236
3292
  const handlers = new Map([
3237
3293
  // Core
3238
3294
  ['dev', cmdUp],
@@ -3268,6 +3324,24 @@ const handlers = new Map([
3268
3324
  ['handoffs', cmdHandoffs],
3269
3325
  ['headhunt', cmdHeadhunt],
3270
3326
  ['init', cmdInit],
3327
+ ['drop', async (args) => {
3328
+ const { runDropCli } = await import('../lib/drop.mjs');
3329
+ return runDropCli(args, { cwd: process.cwd(), env: process.env });
3330
+ }],
3331
+ ['docs', async (args) => {
3332
+ const sub = args[0];
3333
+ if (!sub || sub === '--help' || sub === '-h') {
3334
+ println('Usage: construct docs <check|verify|update|site>');
3335
+ return;
3336
+ }
3337
+ const rest = args.slice(1);
3338
+ if (sub === 'verify') return cmdDocsVerify(rest);
3339
+ if (sub === 'update') return cmdDocsUpdate(rest);
3340
+ if (sub === 'check') return cmdDocsCheck(rest);
3341
+ if (sub === 'site') return cmdDocsSite();
3342
+ errorln(`Unknown docs subcommand: ${sub}. Available: check, verify, update, site`);
3343
+ process.exit(1);
3344
+ }],
3271
3345
  ['docs:verify', cmdDocsVerify],
3272
3346
  ['init:update', cmdInitUpdate],
3273
3347
  ['models', cmdModels],
@@ -3833,7 +3907,185 @@ const handlers = new Map([
3833
3907
  }
3834
3908
  return;
3835
3909
  }
3836
- errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index`);
3910
+ if (sub === 'add') {
3911
+ const opts = parseKnowledgeAddArgs(args.slice(1));
3912
+ if (!opts.slug || !opts.topic) {
3913
+ errorln('Usage: construct knowledge add --source=research --slug=<id> --topic="..." [--confidence=confirmed|inferred|weak] [--source-url=<url>] (body on stdin)');
3914
+ process.exit(1);
3915
+ }
3916
+ const body = await readStdin();
3917
+ if (!body || body.trim().length < 10) {
3918
+ errorln('construct knowledge add: stdin body is empty or too short');
3919
+ process.exit(1);
3920
+ }
3921
+ const { addResearchFinding } = await import('../lib/knowledge/research-store.mjs');
3922
+ try {
3923
+ const { path: outPath, bytes } = await addResearchFinding({
3924
+ cwd: process.cwd(),
3925
+ slug: opts.slug,
3926
+ topic: opts.topic,
3927
+ body,
3928
+ confidence: opts.confidence,
3929
+ sources: opts.sources,
3930
+ });
3931
+ process.stdout.write(`✓ wrote ${outPath} (${bytes} bytes)\n`);
3932
+ return;
3933
+ } catch (err) {
3934
+ errorln(`construct knowledge add: ${err.message}`);
3935
+ process.exit(1);
3936
+ }
3937
+ }
3938
+ errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add`);
3939
+ process.exit(1);
3940
+ }],
3941
+ ['sandbox', async (args) => {
3942
+ const sub = args[0] || 'create';
3943
+ const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
3944
+ if (sub === 'create') {
3945
+ const profileArg = args.find((a) => a.startsWith('--profile='));
3946
+ const profile = profileArg ? profileArg.split('=')[1] : null;
3947
+ const { id, path: p } = createSandbox({ profile });
3948
+ process.stdout.write(`✓ created sandbox ${id}\n path: ${p}\n`);
3949
+ if (profile) process.stdout.write(` profile: ${profile}\n`);
3950
+ return;
3951
+ }
3952
+ if (sub === 'list') {
3953
+ const all = listSandboxes();
3954
+ if (all.length === 0) { process.stdout.write(' (no sandboxes)\n'); return; }
3955
+ for (const s of all) process.stdout.write(` ${s.id.padEnd(30)} ${s.path}\n`);
3956
+ return;
3957
+ }
3958
+ if (sub === 'delete') {
3959
+ const id = args[1];
3960
+ if (!id) { errorln('Usage: construct sandbox delete <id>'); process.exit(1); }
3961
+ process.stdout.write(deleteSandbox(id) ? `✓ removed ${id}\n` : `not found: ${id}\n`);
3962
+ return;
3963
+ }
3964
+ if (sub === 'prune') {
3965
+ const daysArg = args.find((a) => a.startsWith('--days='));
3966
+ const days = daysArg ? Number(daysArg.split('=')[1]) || 7 : 7;
3967
+ const removed = pruneSandboxes({ olderThanDays: days });
3968
+ process.stdout.write(`✓ pruned ${removed} sandbox${removed === 1 ? '' : 'es'} older than ${days} day${days === 1 ? '' : 's'}\n`);
3969
+ return;
3970
+ }
3971
+ errorln(`Unknown sandbox subcommand: ${sub}. Available: create, list, delete, prune`);
3972
+ process.exit(1);
3973
+ }],
3974
+ ['profile', async (args) => {
3975
+ const sub = args[0] || 'show';
3976
+ const { listProfiles, loadProfile, resolveActiveProfile } = await import('../lib/profiles/loader.mjs');
3977
+ if (sub === 'list') {
3978
+ for (const id of listProfiles()) {
3979
+ const p = loadProfile(id);
3980
+ process.stdout.write(` ${id.padEnd(14)} ${p?.displayName || ''}\n`);
3981
+ }
3982
+ return;
3983
+ }
3984
+ if (sub === 'show') {
3985
+ const active = resolveActiveProfile(process.cwd());
3986
+ process.stdout.write(JSON.stringify({
3987
+ id: active.id,
3988
+ displayName: active.displayName,
3989
+ tagline: active.tagline,
3990
+ roles: active.roles?.length || 0,
3991
+ intakeTypes: active.intake?.types?.length || 0,
3992
+ }, null, 2) + '\n');
3993
+ return;
3994
+ }
3995
+ if (sub === 'set') {
3996
+ const id = args[1];
3997
+ if (!id) { errorln('Usage: construct profile set <id>'); process.exit(1); }
3998
+ const target = loadProfile(id);
3999
+ if (!target) {
4000
+ errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
4001
+ process.exit(1);
4002
+ }
4003
+ const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
4004
+ const fsMod = await import('node:fs');
4005
+ const pathMod = await import('node:path');
4006
+ const found = findProjectConfigPath(process.cwd());
4007
+ const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
4008
+ const cfg = found ? (loadProjectConfig(process.cwd()) || { version: 1 }) : { version: 1 };
4009
+ cfg.profile = id;
4010
+ writeProjectConfig(cfgPath, cfg, { validate: true });
4011
+ process.stdout.write(`✓ profile set to ${id} (${target.displayName})\n`);
4012
+ return;
4013
+ }
4014
+ if (sub === 'create') {
4015
+ const id = args[1];
4016
+ if (!id) {
4017
+ errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...]');
4018
+ process.exit(1);
4019
+ }
4020
+ const displayArg = args.find((a) => a.startsWith('--display='));
4021
+ const displayName = displayArg ? displayArg.split('=')[1] : id;
4022
+ const seedRoles = args
4023
+ .filter((a) => a.startsWith('--role='))
4024
+ .map((a) => a.slice('--role='.length))
4025
+ .filter(Boolean);
4026
+ const seedDepartments = args
4027
+ .filter((a) => a.startsWith('--department='))
4028
+ .map((a) => a.slice('--department='.length))
4029
+ .map((spec) => {
4030
+ const [deptId, deptDisplay] = spec.split(':');
4031
+ return { id: deptId, displayName: deptDisplay || deptId };
4032
+ });
4033
+ const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
4034
+ try {
4035
+ const result = createDraftProfile({ cwd: process.cwd(), id, displayName, seedRoles, seedDepartments });
4036
+ const pathMod = await import('node:path');
4037
+ process.stdout.write(`✓ draft created for ${id}\n`);
4038
+ process.stdout.write(` brief: ${pathMod.relative(process.cwd(), result.briefPath)}\n`);
4039
+ process.stdout.write(` draft: ${pathMod.relative(process.cwd(), result.draftPath)}\n`);
4040
+ if (result.personaPaths.length) process.stdout.write(` personas: ${result.personaPaths.length} scaffold(s) under personas/\n`);
4041
+ if (result.departmentPaths.length) process.stdout.write(` departments: ${result.departmentPaths.length} charter(s) under departments/\n`);
4042
+ process.stdout.write(`\nNext steps (in order, per docs/concepts/profile-lifecycle.md):\n`);
4043
+ process.stdout.write(` 1. Discover: dispatch cx-ux-researcher to fill personas/<role>.md from interviews + primary sources.\n`);
4044
+ process.stdout.write(` 2. Frame: dispatch cx-product-manager to fill departments/<dept>.md charters and the intake taxonomy in profile.json.\n`);
4045
+ process.stdout.write(` 3. Architect: dispatch cx-architect to reconcile role reuse vs new, and to populate departments[].\n`);
4046
+ process.stdout.write(` 4. Validate: dispatch cx-evaluator to run persona-eval and classifier-eval against representative signals.\n`);
4047
+ return;
4048
+ } catch (err) {
4049
+ errorln(err.message);
4050
+ process.exit(1);
4051
+ }
4052
+ }
4053
+ if (sub === 'drafts') {
4054
+ const { listDrafts } = await import('../lib/profiles/lifecycle.mjs');
4055
+ const drafts = listDrafts(process.cwd());
4056
+ if (drafts.length === 0) { process.stdout.write(' (no drafts)\n'); return; }
4057
+ for (const d of drafts) {
4058
+ const flags = [d.hasBrief ? 'brief' : null, d.hasProfile ? 'profile' : null].filter(Boolean).join(', ');
4059
+ process.stdout.write(` ${d.id.padEnd(20)} ${flags}\n`);
4060
+ }
4061
+ return;
4062
+ }
4063
+ if (sub === 'archive') {
4064
+ const id = args[1];
4065
+ const reasonArg = args.find((a) => a.startsWith('--reason='));
4066
+ const reason = reasonArg ? reasonArg.split('=')[1] : '';
4067
+ if (!id) { errorln('Usage: construct profile archive <id> --reason="..."'); process.exit(1); }
4068
+ const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
4069
+ try {
4070
+ const { archived } = archiveProfile({ id, reason });
4071
+ process.stdout.write(`✓ archived ${id} to ${archived}\n`);
4072
+ return;
4073
+ } catch (err) {
4074
+ errorln(err.message);
4075
+ process.exit(1);
4076
+ }
4077
+ }
4078
+ if (sub === 'health') {
4079
+ const id = args[1];
4080
+ if (!id) { errorln('Usage: construct profile health <id> [--days=N]'); process.exit(1); }
4081
+ const daysArg = args.find((a) => a.startsWith('--days='));
4082
+ const windowDays = daysArg ? Number(daysArg.split('=')[1]) || 30 : 30;
4083
+ const { profileHealth } = await import('../lib/profiles/lifecycle.mjs');
4084
+ const report = profileHealth(process.cwd(), id, { windowDays });
4085
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
4086
+ return;
4087
+ }
4088
+ errorln(`Unknown profile subcommand: ${sub}. Available: list, show, set, create, drafts, archive, health`);
3837
4089
  process.exit(1);
3838
4090
  }],
3839
4091
  ]);
@@ -1,10 +1,10 @@
1
1
  <!--
2
- commands/understand/research.md Research a topic verify facts from primary sources, separate evidence from inference
2
+ commands/understand/research.md. Research a topic. Verify facts from primary sources, separate evidence from inference.
3
3
 
4
- Research a topic verify facts from primary sources, separate evidence from inference
4
+ Research a topic. Verify facts from primary sources, separate evidence from inference.
5
5
  -->
6
6
  ---
7
- description: Research a topic verify facts from primary sources, separate evidence from inference
7
+ description: Research a topic. Verify facts from primary sources, separate evidence from inference.
8
8
  ---
9
9
 
10
10
  You are Construct. Research: $ARGUMENTS
@@ -20,3 +20,5 @@ Method:
20
20
  For each finding: source, date, confidence (confirmed / inferred / weak signal).
21
21
 
22
22
  Output: FINDINGS (with citations) | INFERENCES (labeled) | GAPS | RECOMMENDATION
23
+
24
+ Persistence: after producing the output, run `construct knowledge add --source=research --slug=<topic-slug> --topic="<one-line topic>" --confidence=<confirmed|inferred|weak>` and pass the FINDINGS+INFERENCES+GAPS+RECOMMENDATION block on stdin. The store writes a frontmatter-stamped markdown file under `.cx/knowledge/external/research/` and indexes it for future hybrid search. Pass `--source-url=<url>` repeatedly for each cited source (required when confidence=confirmed).
package/lib/auto-docs.mjs CHANGED
@@ -71,7 +71,7 @@ function buildCoreDocsContract() {
71
71
  '| `docs/README.md` | Docs index and maintenance contract | Core docs set or maintenance expectations change |',
72
72
  '| `docs/concepts/architecture.md` | Canonical architecture and invariants | Runtime shape, contracts, boundaries, or major dependencies change |',
73
73
  '',
74
- '`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed durable work belongs in the tracker (Beads or external).',
74
+ '`plan.md` is a local working document. `construct init` creates it for the active session, but it is gitignored and not committed; durable work belongs in the tracker (Beads or external).',
75
75
  '',
76
76
  'Tracker hierarchy: external tracker (prefer Beads) for durable work, `plan.md` for the local working plan, and cass-memory via MCP `memory` for cross-session recall.',
77
77
  '',
@@ -82,7 +82,7 @@ function buildCoreDocsContract() {
82
82
 
83
83
  const DIR_DESCRIPTIONS = {
84
84
  agents: 'Registry and generated platform adapter chains',
85
- apps: 'User-facing apps shipped from this repo (e.g., apps/docs/ Fumadocs docs site)',
85
+ apps: 'User-facing apps shipped from this repo (e.g. apps/docs/, the Fumadocs docs site)',
86
86
  bin: 'CLI entrypoint (`construct`)',
87
87
  claude: 'Claude Code integration (agents, settings template)',
88
88
  commands: 'Command prompt assets',
@@ -128,6 +128,50 @@ export const CLI_COMMANDS = [
128
128
  { name: 'search <query>', desc: 'Search customer profiles by name/alias' },
129
129
  ],
130
130
  },
131
+ {
132
+ name: 'knowledge',
133
+ emoji: '🧠',
134
+ category: 'Work',
135
+ core: false,
136
+ description: 'Query, index, or add to the project knowledge base',
137
+ usage: 'construct knowledge trends|index|add',
138
+ subcommands: [
139
+ { name: 'trends', desc: 'Show trend report across observations and artifacts' },
140
+ { name: 'index', desc: 'Rebuild the local RAG corpus over .cx/ artifacts' },
141
+ { name: 'add --source=research --slug=<id> --topic="..." [--source-url=<url>]', desc: 'Persist a research finding into .cx/knowledge/external/research/' },
142
+ ],
143
+ },
144
+ {
145
+ name: 'sandbox',
146
+ emoji: '🧪',
147
+ category: 'Core',
148
+ core: true,
149
+ description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
150
+ usage: 'construct sandbox create|list|delete|prune [--profile=<id>]',
151
+ subcommands: [
152
+ { name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
153
+ { name: 'list', desc: 'List existing sandboxes, newest first' },
154
+ { name: 'delete <id>', desc: 'Remove one sandbox by id' },
155
+ { name: 'prune [--days=N]', desc: 'Remove sandboxes older than N days (default 7)' },
156
+ ],
157
+ },
158
+ {
159
+ name: 'profile',
160
+ emoji: '🧭',
161
+ category: 'Core',
162
+ core: true,
163
+ description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
164
+ usage: 'construct profile show|list|set|create|drafts|archive|health',
165
+ subcommands: [
166
+ { name: 'show', desc: 'Show the active profile' },
167
+ { name: 'list', desc: 'List curated profiles' },
168
+ { name: 'set <id>', desc: 'Switch the active profile (writes construct.config.json)' },
169
+ { name: 'create <id> [--display="..."]', desc: 'Scaffold a draft profile + requirements brief under .cx/profiles/draft-<id>/' },
170
+ { name: 'drafts', desc: 'List in-progress draft profiles' },
171
+ { name: 'archive <id> --reason="..."', desc: 'Move a curated profile into archive/profiles/<id>/' },
172
+ { name: 'health <id> [--days=N]', desc: 'Per-profile observation + outcome rollup' },
173
+ ],
174
+ },
131
175
  {
132
176
  name: 'workspace',
133
177
  emoji: '🏢',
@@ -42,7 +42,13 @@ function requiresHeader(rel) {
42
42
  if (rel.startsWith('lib/server/static/')) {
43
43
  return { required: false, type: null };
44
44
  }
45
- const type = (jsMatch && ext !== '.html') ? 'js' : (jsMatch || mdMatch) ? 'md' : null;
45
+ // Markdown files always get markdown-style headers, even if the directory
46
+ // glob (e.g. ^tests/) primarily targets JS sources. Without this, .md docs
47
+ // co-located with .mjs tests were forced into /** */ format.
48
+ let type;
49
+ if (ext === '.md') type = mdMatch || jsMatch ? 'md' : null;
50
+ else if (jsMatch && ext !== '.html') type = 'js';
51
+ else type = (jsMatch || mdMatch) ? 'md' : null;
46
52
  return { required: jsMatch || mdMatch, type };
47
53
  }
48
54
 
@@ -20,6 +20,7 @@ export const CONFIG_SCHEMA_VERSION = 1;
20
20
 
21
21
  export const DEPLOYMENT_MODES = ['solo', 'team', 'enterprise'];
22
22
  export const MCP_BROKER_VALUES = ['auto', 'on', 'off'];
23
+ export const DEFAULT_PROFILE_ID = 'rnd';
23
24
 
24
25
  export const DEFAULT_PROJECT_CONFIG = Object.freeze({
25
26
  version: CONFIG_SCHEMA_VERSION,
@@ -31,6 +32,7 @@ export const DEFAULT_PROJECT_CONFIG = Object.freeze({
31
32
  tenantId: null,
32
33
  }),
33
34
  providers: Object.freeze({}),
35
+ profile: DEFAULT_PROFILE_ID,
34
36
  autoEmbed: false,
35
37
  telemetry: Object.freeze({
36
38
  enabled: true,
@@ -77,6 +79,7 @@ export const FIELD_RULES = {
77
79
  },
78
80
  },
79
81
  providers: { type: 'object', required: false },
82
+ profile: { type: 'string', required: false, maxLength: 40 },
80
83
  autoEmbed: { type: 'boolean', required: false },
81
84
  telemetry: {
82
85
  type: 'object',