@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.
- package/README.md +51 -42
- package/bin/construct +256 -4
- package/commands/understand/research.md +5 -3
- package/lib/auto-docs.mjs +2 -2
- package/lib/cli-commands.mjs +44 -0
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- package/lib/flavors/loader.mjs +136 -0
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/init-unified.mjs +25 -2
- package/lib/intake/classify.mjs +61 -183
- package/lib/intake/prepare.mjs +7 -0
- package/lib/intake/tables/creative.mjs +94 -0
- package/lib/intake/tables/operations.mjs +85 -0
- package/lib/intake/tables/research.mjs +85 -0
- package/lib/intake/tables/rnd.mjs +175 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/observation-store.mjs +19 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +6 -9
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -0
- package/package.json +6 -1
- package/personas/construct.md +20 -20
- package/platforms/claude/settings.template.json +13 -0
- package/scripts/sync-agents.mjs +11 -0
- package/skills/roles/architect.ai-systems.md +4 -2
- package/skills/roles/architect.data.md +4 -2
- package/skills/roles/architect.enterprise.md +4 -2
- package/skills/roles/architect.integration.md +4 -2
- package/skills/roles/architect.md +7 -5
- package/skills/roles/architect.platform.md +4 -2
- package/skills/roles/data-analyst.experiment.md +4 -2
- package/skills/roles/data-analyst.md +9 -7
- package/skills/roles/data-analyst.product-intelligence.md +4 -2
- package/skills/roles/data-analyst.product.md +4 -2
- package/skills/roles/data-analyst.telemetry.md +4 -2
- package/skills/roles/data-engineer.pipeline.md +4 -2
- package/skills/roles/data-engineer.vector-retrieval.md +4 -2
- package/skills/roles/data-engineer.warehouse.md +4 -2
- package/skills/roles/debugger.md +7 -5
- package/skills/roles/designer.accessibility.md +4 -2
- package/skills/roles/designer.md +10 -8
- package/skills/roles/engineer.ai.md +4 -2
- package/skills/roles/engineer.data.md +5 -3
- package/skills/roles/engineer.md +14 -12
- package/skills/roles/engineer.platform.md +5 -3
- package/skills/roles/operator.docs.md +6 -4
- package/skills/roles/operator.md +6 -4
- package/skills/roles/operator.release.md +4 -2
- package/skills/roles/operator.sre.md +5 -3
- package/skills/roles/orchestrator.md +5 -3
- package/skills/roles/product-manager.ai-product.md +4 -2
- package/skills/roles/product-manager.business-strategy.md +4 -2
- package/skills/roles/product-manager.enterprise.md +4 -2
- package/skills/roles/product-manager.growth.md +4 -2
- package/skills/roles/product-manager.md +6 -4
- package/skills/roles/product-manager.platform.md +4 -2
- package/skills/roles/product-manager.product.md +4 -2
- package/skills/roles/qa.ai-eval.md +4 -2
- package/skills/roles/qa.api-contract.md +4 -2
- package/skills/roles/qa.data-pipeline.md +4 -2
- package/skills/roles/qa.md +7 -5
- package/skills/roles/qa.test-automation.md +5 -3
- package/skills/roles/qa.web-ui.md +4 -2
- package/skills/roles/researcher.explorer.md +4 -2
- package/skills/roles/researcher.md +11 -9
- package/skills/roles/researcher.ux.md +4 -2
- package/skills/roles/reviewer.devil-advocate.md +4 -2
- package/skills/roles/reviewer.evaluator.md +4 -2
- package/skills/roles/reviewer.md +14 -12
- package/skills/roles/reviewer.trace.md +4 -2
- package/skills/roles/security.ai.md +4 -2
- package/skills/roles/security.appsec.md +4 -2
- package/skills/roles/security.cloud.md +4 -2
- package/skills/roles/security.legal-compliance.md +4 -2
- package/skills/roles/security.md +7 -5
- package/skills/roles/security.privacy.md +4 -2
- package/skills/roles/security.supply-chain.md +4 -2
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/skill-artifact.md +27 -0
package/README.md
CHANGED
|
@@ -1,73 +1,78 @@
|
|
|
1
1
|
# Construct
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
-
|
|
5
|
+
One AI interface. A team of specialists behind it. Hard gates. Runs locally, or deploys for teams.
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
+
Open your editor and talk to `@construct`. A walkthrough lives in `construct_guide.md` at your project root.
|
|
36
35
|
|
|
37
|
-
|
|
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
|
-
|
|
38
|
+
[Five minute walkthrough](https://geraldmaron.github.io/construct/v2/docs/start).
|
|
40
39
|
|
|
41
|
-
|
|
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
|
|
48
|
-
| Understand how it works | [
|
|
49
|
-
| Pick a deployment mode | [
|
|
50
|
-
| Drop a signal and triage it | [
|
|
51
|
-
| Add a custom specialist | [
|
|
52
|
-
| Fix a blocked commit or red CI | [
|
|
53
|
-
| Plug in your own LLM | [
|
|
54
|
-
|
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
162
|
-
- [`CHANGELOG.md`](./CHANGELOG.md)
|
|
163
|
-
- [`docs/concepts/architecture.md`](./docs/concepts/architecture.md)
|
|
164
|
-
- [`AGENTS.md`](./AGENTS.md)
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
214
|
-
construct uninstall --keep-state # only .construct/
|
|
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
|
-
|
|
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
|
|
2
|
+
commands/understand/research.md. Research a topic. Verify facts from primary sources, separate evidence from inference.
|
|
3
3
|
|
|
4
|
-
Research a topic
|
|
4
|
+
Research a topic. Verify facts from primary sources, separate evidence from inference.
|
|
5
5
|
-->
|
|
6
6
|
---
|
|
7
|
-
description: Research a topic
|
|
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
|
|
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
|
|
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',
|
package/lib/cli-commands.mjs
CHANGED
|
@@ -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: '🏢',
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/lib/config/schema.mjs
CHANGED
|
@@ -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',
|