@geraldmaron/construct 1.0.20 → 1.0.23
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 +23 -5
- package/bin/construct +194 -16
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +44 -16
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +22 -0
- package/lib/decisions/enforced-baseline.json +3 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/document-ingest.mjs +13 -4
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/embedded-contract/triage.mjs +24 -2
- package/lib/embedded-contract/workflow-invoke.mjs +21 -0
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/ingest/strategy.mjs +35 -3
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/run-store-postgres.mjs +85 -0
- package/lib/orchestration/run-store-sqlite.mjs +122 -0
- package/lib/orchestration/runtime.mjs +188 -50
- package/lib/orchestration/store.mjs +102 -0
- package/lib/orchestration/worker.mjs +215 -0
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +80 -26
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +171 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/reconcile/postgres-namespace.mjs +102 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +381 -95
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +2 -11
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +29 -29
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +14 -14
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/lib/server/static/index.html +0 -1
package/README.md
CHANGED
|
@@ -22,12 +22,14 @@ Install the CLI (once per machine):
|
|
|
22
22
|
npm install -g @geraldmaron/construct
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Bootstrap local services (once per machine):
|
|
25
|
+
Bootstrap local services (once per machine, opt-in to machine-scope writes):
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
construct install --yes
|
|
28
|
+
construct install --scope=user --yes
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`construct install` defaults to `--scope=project`, which writes nothing and prints scope guidance — see the [footprint contract](#footprint-contract) below or [ADR 0029](docs/adr/0029-install-scopes-and-hook-budgets.md). Use `--scope=user` for machine setup, `--scope=both` for both.
|
|
32
|
+
|
|
31
33
|
Initialize a project:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
@@ -37,7 +39,7 @@ construct init --yes
|
|
|
37
39
|
|
|
38
40
|
`construct init` scaffolds the project (`.cx/`, `AGENTS.md`, `plan.md`, adapters) and starts the local services by default. Pass `--no-start` to skip service startup, or `--interactive` for the guided flow.
|
|
39
41
|
|
|
40
|
-
Open your editor and talk to `@construct`. A walkthrough lives in
|
|
42
|
+
Open your editor and talk to `@construct`. A walkthrough lives in `.cx/construct_guide.md` (gitignored — local reference only).
|
|
41
43
|
|
|
42
44
|
No Node? Try `brew install geraldmaron/construct/construct`. Cloning a project that already uses Construct? `npx -y @geraldmaron/construct init` wires it up.
|
|
43
45
|
|
|
@@ -97,6 +99,18 @@ First run downloads `uv` and creates `.cx/runtime/docling/.venv` (~1.5 GB includ
|
|
|
97
99
|
|
|
98
100
|
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).
|
|
99
101
|
|
|
102
|
+
## Footprint contract
|
|
103
|
+
|
|
104
|
+
Construct's writes are scoped and disclosed up front. The default `construct install` (no flag) writes nothing — it prints scope guidance. Project writes happen only under `construct init` inside a project directory; machine writes happen only under `construct install --scope=user`, with an itemized interactive consent prompt for any global Claude Code config mutation.
|
|
105
|
+
|
|
106
|
+
| Scope | Trigger | Paths |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| Project | `construct init` | `.construct/`, `.cx/`, `.claude/` adapter tree, host adapters (`.codex/`, `.opencode/`, `.cursor/`, `.vscode/`), `construct.config.json`, marker block in `CLAUDE.md` / `AGENTS.md`, `.gitignore` append, `.beads/` |
|
|
109
|
+
| Machine | `construct install --scope=user` | `~/.construct/config.env`, `~/.construct/lib` (symlink), `~/.construct/services/`, `~/Library/LaunchAgents/` (macOS), MCP entries in `~/.config/opencode/opencode.json` and `~/.codex/config.toml`, marker block in `~/.claude/CLAUDE.md`, hook injection in `~/.claude/settings.json` (last two require interactive consent or `--yes`) |
|
|
110
|
+
| Never touched | — | Shell rc files (`~/.bashrc`, `~/.zshrc`), npm global config, `git config --global` |
|
|
111
|
+
|
|
112
|
+
Full table with file:line citations and the per-hook performance budget contract: [Architecture — Footprint contract](https://geraldmaron.github.io/construct/concepts/architecture#footprint-contract) and [ADR 0029](docs/adr/0029-install-scopes-and-hook-budgets.md).
|
|
113
|
+
|
|
100
114
|
## Learning loops
|
|
101
115
|
|
|
102
116
|
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.
|
|
@@ -114,11 +128,12 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
114
128
|
|
|
115
129
|
| Command | What it does |
|
|
116
130
|
|---|---|
|
|
131
|
+
| `construct dashboard` | Start the local dashboard/orchestration daemon (or --token to mint a dashboard token) |
|
|
117
132
|
| `construct dev` | Start services for development |
|
|
118
133
|
| `construct docs` | Documentation commands |
|
|
119
134
|
| `construct doctor` | Check installation health |
|
|
120
135
|
| `construct init` | Project setup (once per repo): scaffold .cx/, AGENTS.md, plan.md, adapters |
|
|
121
|
-
| `construct install` | Machine setup (
|
|
136
|
+
| `construct install` | Machine setup (scoped per ADR-0029): --scope=project\|user\|both, default project |
|
|
122
137
|
| `construct intake` | View and process the active profile's intake queue (queue label varies by profile) |
|
|
123
138
|
| `construct profile` | Manage the active org profile and its lifecycle (draft, promote, archive, health) |
|
|
124
139
|
| `construct recommendations` | View and manage artifact recommendations |
|
|
@@ -136,6 +151,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
136
151
|
| `construct customer` | Manage customer profiles for product intelligence |
|
|
137
152
|
| `construct distill` | Distill documents with query-focused chunking |
|
|
138
153
|
| `construct drop` | Ingest file from Downloads/Desktop |
|
|
154
|
+
| `construct export` | Export markdown to PDF/DOCX/HTML via Pandoc + Typst (optional system binaries; ADR-0024) |
|
|
139
155
|
| `construct graph` | Task graph management |
|
|
140
156
|
| `construct handoffs` | List and inspect session handoff files in .cx/handoffs/ |
|
|
141
157
|
| `construct headhunt` | Create domain expertise overlays |
|
|
@@ -157,13 +173,14 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
157
173
|
|
|
158
174
|
| Command | What it does |
|
|
159
175
|
|---|---|
|
|
176
|
+
| `construct acp` | Run Construct as an Agent Client Protocol (ACP) server over stdio for Zed/JetBrains/VS Code ACP clients |
|
|
160
177
|
| `construct capability` | Describe what this Construct install can do (embedded contract; read-only, secret-free) |
|
|
161
178
|
| `construct claude:allow` | Manage Claude Code `permissions.allow` from the outside (auto-classifier blocks the agent from editing it) |
|
|
162
179
|
| `construct execution` | Resolve the execution-capability contract for an embedded workflow (orchestrated vs prompt-only; descriptive, not enforced) |
|
|
163
180
|
| `construct hosts` | Show host support for Construct orchestration |
|
|
164
181
|
| `construct mcp` | Manage MCP integrations |
|
|
165
182
|
| `construct models` | Show or update model tier assignments |
|
|
166
|
-
| `construct orchestrate` | Construct-owned local orchestration runtime
|
|
183
|
+
| `construct orchestrate` | Construct-owned local orchestration runtime, in-process or against the local daemon (--remote) |
|
|
167
184
|
| `construct plugin` | Manage external Construct plugin manifests |
|
|
168
185
|
|
|
169
186
|
### Integrations
|
|
@@ -251,6 +268,7 @@ construct/
|
|
|
251
268
|
├── bin CLI entrypoint (`construct`)
|
|
252
269
|
├── commands Command prompt assets
|
|
253
270
|
├── config
|
|
271
|
+
├── dashboard
|
|
254
272
|
├── db
|
|
255
273
|
├── deploy
|
|
256
274
|
├── docs Architecture notes, runbooks, and documentation contract
|
package/bin/construct
CHANGED
|
@@ -65,6 +65,7 @@ import { runTeamReviewCli } from '../lib/telemetry/team-rollup.mjs';
|
|
|
65
65
|
import { readDashboardState, startDashboard, startServices } from '../lib/service-manager.mjs';
|
|
66
66
|
import { readCostLog, summarizeCostData, formatCostReport, clearCostLog } from '../lib/cost.mjs';
|
|
67
67
|
import { readEfficiencyLog, summarizeEfficiencyData, formatEfficiencyReport } from '../lib/efficiency.mjs';
|
|
68
|
+
import { resolveColors } from '../lib/term-format.mjs';
|
|
68
69
|
import { buildHybridSearchResultsAsync } from '../lib/storage/hybrid-query.mjs';
|
|
69
70
|
import { createSqlClient, closeSqlClient, readVectorConfig } from '../lib/storage/backend.mjs';
|
|
70
71
|
import { describeVectorStore } from '../lib/storage/vector-store.mjs';
|
|
@@ -80,12 +81,7 @@ for (const [key, value] of Object.entries(ENV)) {
|
|
|
80
81
|
if (!(key in process.env)) process.env[key] = value;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
const COLORS = (
|
|
84
|
-
? {
|
|
85
|
-
bold: '\u001b[1m', dim: '\u001b[2m', reset: '\u001b[0m',
|
|
86
|
-
red: '\u001b[31m', green: '\u001b[32m', yellow: '\u001b[33m', cyan: '\u001b[36m',
|
|
87
|
-
}
|
|
88
|
-
: { bold: '', dim: '', reset: '', red: '', green: '', yellow: '', cyan: '' };
|
|
84
|
+
const COLORS = resolveColors();
|
|
89
85
|
|
|
90
86
|
function println(message = '') { process.stdout.write(`${message}\n`); }
|
|
91
87
|
function errorln(message = '') { process.stderr.write(`${message}\n`); }
|
|
@@ -250,6 +246,45 @@ async function cmdSync(args) {
|
|
|
250
246
|
opLog.close('failed', { stage: 'sync-specialists', status: syncStatus });
|
|
251
247
|
process.exit(syncStatus);
|
|
252
248
|
}
|
|
249
|
+
|
|
250
|
+
// State reconciliation (ADR-0027): bring on-disk state in line with the
|
|
251
|
+
// current disposition contract. Auto-safety tasks run silently; ask-safety
|
|
252
|
+
// tasks surface via construct doctor instead. --reconcile=<id> runs one task
|
|
253
|
+
// on explicit consent (the path for ask-safety tasks). --no-reconcile
|
|
254
|
+
// bypasses the auto pass entirely for diagnostic runs.
|
|
255
|
+
|
|
256
|
+
const reconcileArg = args.find((a) => a.startsWith('--reconcile='));
|
|
257
|
+
if (reconcileArg) {
|
|
258
|
+
const id = reconcileArg.slice('--reconcile='.length);
|
|
259
|
+
try {
|
|
260
|
+
const { runReconciliation } = await import('../lib/reconcile/index.mjs');
|
|
261
|
+
const result = await runReconciliation(id, {});
|
|
262
|
+
if (!result.ok) {
|
|
263
|
+
errorln(`Reconciliation failed (${id}): ${result.reason}`);
|
|
264
|
+
if (result.available) errorln(`Available: ${result.available.join(', ')}`);
|
|
265
|
+
opLog.close('failed', { stage: 'reconcile', id, reason: result.reason });
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
if (result.ran) ok(`Reconciled (${id}): ${result.summary}`);
|
|
269
|
+
else println(`Reconciliation (${id}): ${result.summary}`);
|
|
270
|
+
opLog.event('reconcile-explicit', { id, ran: !!result.ran });
|
|
271
|
+
} catch (err) {
|
|
272
|
+
errorln(`Reconciliation error (${id}): ${err.message}`);
|
|
273
|
+
opLog.close('failed', { stage: 'reconcile', id, reason: err.message });
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
} else if (!args.includes('--no-reconcile')) {
|
|
277
|
+
try {
|
|
278
|
+
const verbose = args.includes('--verbose') || args.includes('-v');
|
|
279
|
+
const { runAutoReconciliations } = await import('../lib/reconcile/index.mjs');
|
|
280
|
+
const { applied } = await runAutoReconciliations({ verbose });
|
|
281
|
+
for (const entry of applied) ok(`Reconciled (${entry.id}): ${entry.summary}`);
|
|
282
|
+
opLog.event('reconcile', { applied: applied.length });
|
|
283
|
+
} catch (err) {
|
|
284
|
+
opLog.event('reconcile-error', { message: err.message });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
253
288
|
if (args.includes('--no-docs')) {
|
|
254
289
|
opLog.close('ok', { docs: 'skipped' });
|
|
255
290
|
return;
|
|
@@ -584,23 +619,24 @@ async function cmdDoctor() {
|
|
|
584
619
|
// Tier model selection. Construct ships with no default — at least
|
|
585
620
|
// one tier must be configured (registry.json primary OR CX_MODEL_*
|
|
586
621
|
// env override) before any LLM-backed workflow can run.
|
|
587
|
-
//
|
|
588
|
-
|
|
622
|
+
// A missing, partial, or unresolvable tier is advisory, not a repo defect:
|
|
623
|
+
// model tiers are user/machine-specific config, so doctor warns and never
|
|
624
|
+
// fails the gate on them.
|
|
589
625
|
try {
|
|
590
626
|
const { resolveModelTiers } = await import('../lib/model-registry.mjs');
|
|
591
627
|
const resolved = resolveModelTiers({ env: process.env });
|
|
592
628
|
if (resolved.complete && !resolved.errors) {
|
|
593
629
|
add('Models — all tiers configured', true);
|
|
594
630
|
} else if (resolved.configured === 0) {
|
|
595
|
-
add('Models — no tier configured (pick one in the dashboard or run `construct models --apply`)',
|
|
631
|
+
add('Models — no tier configured (pick one in the dashboard or run `construct models --apply`)', false, true);
|
|
596
632
|
} else {
|
|
597
633
|
const missing = Object.entries(resolved.models)
|
|
598
634
|
.filter(([, v]) => !v)
|
|
599
635
|
.map(([k]) => k);
|
|
600
|
-
add(`Models — only ${resolved.configured}/3 tiers configured (missing: ${missing.join(', ')})`,
|
|
636
|
+
add(`Models — only ${resolved.configured}/3 tiers configured (missing: ${missing.join(', ')})`, false, true);
|
|
601
637
|
}
|
|
602
638
|
} catch (err) {
|
|
603
|
-
add(`Models — check failed: ${err.message}`,
|
|
639
|
+
add(`Models — check failed: ${err.message}`, false, true);
|
|
604
640
|
}
|
|
605
641
|
|
|
606
642
|
add('Node.js 20+ (recommended)', Number.parseInt(process.versions.node.split('.')[0], 10) >= 20);
|
|
@@ -1069,6 +1105,26 @@ async function cmdDoctor() {
|
|
|
1069
1105
|
add('Registry integrity check', false);
|
|
1070
1106
|
}
|
|
1071
1107
|
|
|
1108
|
+
// Reconciliation drift (ADR-0027). Any task reporting needsRepair is surfaced
|
|
1109
|
+
// with its summary and the exact `construct sync --reconcile=<id>` command.
|
|
1110
|
+
// Auto tasks that still show drift (e.g. a run with --no-reconcile) and all
|
|
1111
|
+
// ask tasks both land here, since ask tasks never run from the auto pass.
|
|
1112
|
+
let reconcileDrift = [];
|
|
1113
|
+
try {
|
|
1114
|
+
const { listReconciliations } = await import('../lib/reconcile/index.mjs');
|
|
1115
|
+
const tasks = await listReconciliations();
|
|
1116
|
+
reconcileDrift = tasks.filter((t) => t.needsRepair);
|
|
1117
|
+
add(
|
|
1118
|
+
reconcileDrift.length === 0
|
|
1119
|
+
? 'Reconciliation drift: none'
|
|
1120
|
+
: `Reconciliation drift: ${reconcileDrift.length} task${reconcileDrift.length === 1 ? '' : 's'} need repair (see below)`,
|
|
1121
|
+
reconcileDrift.length === 0,
|
|
1122
|
+
true,
|
|
1123
|
+
);
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
add(`Reconciliation drift check failed: ${err.message}`, false, true);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1072
1128
|
println('Construct Health Check');
|
|
1073
1129
|
println('══════════════════════');
|
|
1074
1130
|
println('');
|
|
@@ -1080,6 +1136,17 @@ async function cmdDoctor() {
|
|
|
1080
1136
|
else if (check.optional) warnCount += 1;
|
|
1081
1137
|
else failCount += 1;
|
|
1082
1138
|
}
|
|
1139
|
+
|
|
1140
|
+
if (reconcileDrift.length > 0) {
|
|
1141
|
+
println('');
|
|
1142
|
+
println('Reconciliation drift:');
|
|
1143
|
+
for (const task of reconcileDrift) {
|
|
1144
|
+
println(` ⚠ ${task.id} (${task.safety})`);
|
|
1145
|
+
if (task.summary) println(` ${task.summary}`);
|
|
1146
|
+
println(` fix: construct sync --reconcile=${task.id}`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1083
1150
|
println('');
|
|
1084
1151
|
println(`Results: ${okCount} passed, ${warnCount} warnings, ${failCount} failed`);
|
|
1085
1152
|
if (failCount > 0) process.exit(1);
|
|
@@ -1178,6 +1245,16 @@ async function cmdUp(args = []) {
|
|
|
1178
1245
|
else warn(`${svc.name}: ${svc.note}`);
|
|
1179
1246
|
}
|
|
1180
1247
|
|
|
1248
|
+
// Show Langfuse credentials guidance after first start
|
|
1249
|
+
const langfuseService = results.find(r => r.name === 'Langfuse');
|
|
1250
|
+
if (langfuseService && langfuseService.status === 'started' && langfuseService.note?.includes('auto-configured')) {
|
|
1251
|
+
println('');
|
|
1252
|
+
info('Langfuse credentials:');
|
|
1253
|
+
info(' Dashboard: http://localhost:54330 (login: admin@construct.local / construct-admin)');
|
|
1254
|
+
info(' API keys auto-saved to ~/.construct/config.env');
|
|
1255
|
+
info(' Run `construct status` anytime to view credentials');
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1181
1258
|
if (recovery) {
|
|
1182
1259
|
println('');
|
|
1183
1260
|
info(`Recovery: ${recovery.message}`);
|
|
@@ -1410,6 +1487,44 @@ async function cmdIngest(args) {
|
|
|
1410
1487
|
}
|
|
1411
1488
|
}
|
|
1412
1489
|
|
|
1490
|
+
async function cmdExport(args) {
|
|
1491
|
+
// construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>]
|
|
1492
|
+
// Optional capability per ADR-0024: requires pandoc on PATH (and typst for PDF).
|
|
1493
|
+
// Absent tooling returns a structured "install X" error, not a crash.
|
|
1494
|
+
|
|
1495
|
+
const positional = args.filter((a) => !a.startsWith('--'));
|
|
1496
|
+
const inputPath = positional[0];
|
|
1497
|
+
const toFlag = args.find((a) => a.startsWith('--to='))?.split('=')[1];
|
|
1498
|
+
const outFlag = args.find((a) => a.startsWith('--output='))?.split('=')[1];
|
|
1499
|
+
const detectOnly = args.includes('--detect');
|
|
1500
|
+
|
|
1501
|
+
const { detect, exportMarkdown, EXPORT_FORMATS } = await import('../lib/document-export.mjs');
|
|
1502
|
+
|
|
1503
|
+
if (detectOnly) {
|
|
1504
|
+
const format = toFlag || 'pdf';
|
|
1505
|
+
println(JSON.stringify(detect(format), null, 2));
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
if (!inputPath) {
|
|
1510
|
+
errorln('Usage: construct export <markdown-file> --to=<pdf|docx|html> [--output=<path>] [--detect]');
|
|
1511
|
+
process.exit(1);
|
|
1512
|
+
}
|
|
1513
|
+
if (!toFlag) {
|
|
1514
|
+
errorln(`--to is required. Supported formats: ${EXPORT_FORMATS.join(', ')}`);
|
|
1515
|
+
process.exit(1);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const resolvedIn = path.resolve(process.cwd(), inputPath);
|
|
1519
|
+
const resolvedOut = outFlag ? path.resolve(process.cwd(), outFlag) : null;
|
|
1520
|
+
const result = exportMarkdown({ inputPath: resolvedIn, outputPath: resolvedOut, format: toFlag });
|
|
1521
|
+
if (!result.ok) {
|
|
1522
|
+
errorln(result.message);
|
|
1523
|
+
process.exit(result.missing?.length ? 2 : 1);
|
|
1524
|
+
}
|
|
1525
|
+
ok(result.message);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1413
1528
|
async function cmdInfer(args) {
|
|
1414
1529
|
try {
|
|
1415
1530
|
const result = await runInferCli(args, { cwd: process.cwd() });
|
|
@@ -1976,7 +2091,17 @@ async function cmdEmbeddedTriage(args) {
|
|
|
1976
2091
|
input = fs.readFileSync(0, 'utf8');
|
|
1977
2092
|
}
|
|
1978
2093
|
if (!input.trim() && !ingestion?.error) {
|
|
1979
|
-
|
|
2094
|
+
// This is an embedded-contract verb: an embedder that pipes empty/no input
|
|
2095
|
+
// must still get a parseable, versioned envelope — not a bare exit. Emit a
|
|
2096
|
+
// typed error envelope on stdout so the contract holds, and exit non-zero so
|
|
2097
|
+
// shell callers still see failure.
|
|
2098
|
+
const { wrapResponse } = await import('../lib/embedded-contract/envelope.mjs');
|
|
2099
|
+
const envelope = wrapResponse({
|
|
2100
|
+
surface: 'cli',
|
|
2101
|
+
warnings: ['No artifact provided — pass --text "...", --file <path>, or pipe content on stdin.'],
|
|
2102
|
+
data: { error: { code: 'missing_input', message: 'intake classify requires an artifact to classify; received empty input.' } },
|
|
2103
|
+
});
|
|
2104
|
+
println(JSON.stringify(envelope, null, 2));
|
|
1980
2105
|
process.exit(1);
|
|
1981
2106
|
}
|
|
1982
2107
|
const request = {
|
|
@@ -3364,6 +3489,47 @@ async function cmdOrchestrate(args) {
|
|
|
3364
3489
|
const flag = (name) => { const i = rest.indexOf(name); return i !== -1 ? rest[i + 1] : undefined; };
|
|
3365
3490
|
const wantsJson = rest.includes('--json') || args.includes('--json');
|
|
3366
3491
|
|
|
3492
|
+
// Thin-client mode: drive the local orchestration daemon over HTTP instead of
|
|
3493
|
+
// running in-process, proving the engine-as-service contract editors/CI use.
|
|
3494
|
+
if (rest.includes('--remote')) {
|
|
3495
|
+
const remoteVal = flag('--remote');
|
|
3496
|
+
const base = (remoteVal && /^https?:\/\//.test(remoteVal)) ? remoteVal.replace(/\/$/, '') : `http://${process.env.BIND_HOST || '127.0.0.1'}:${process.env.PORT || '4242'}`;
|
|
3497
|
+
const { getDashboardToken } = await import('../lib/server/auth.mjs');
|
|
3498
|
+
const token = getDashboardToken();
|
|
3499
|
+
const headers = { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) };
|
|
3500
|
+
const positional = rest.filter((a) => !a.startsWith('--'));
|
|
3501
|
+
try {
|
|
3502
|
+
if (sub === 'run') {
|
|
3503
|
+
const text = flag('--text') || positional.join(' ').trim();
|
|
3504
|
+
if (!text) { errorln('Usage: construct orchestrate run "<request>" --remote [--strategy S] [--worker-backend provider] [--json]'); process.exit(1); }
|
|
3505
|
+
const res = await fetch(`${base}/api/orchestration/runs`, { method: 'POST', headers, body: JSON.stringify({ request: text, workflowType: flag('--workflow-type'), requestedStrategy: flag('--strategy') || 'auto', host: flag('--host'), hostModel: flag('--host-model'), hostProvider: flag('--host-provider'), workerBackend: flag('--worker-backend'), fileCount: Number(flag('--file-count') || 0), moduleCount: Number(flag('--module-count') || 0) }) });
|
|
3506
|
+
const env = await res.json();
|
|
3507
|
+
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3508
|
+
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3509
|
+
println(`Run ${env.data.runId} started on ${base} — ${env.data.status} · executionMode=${env.data.execution?.executionMode}. Poll: construct orchestrate status ${env.data.runId} --remote`);
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
if (sub === 'status') {
|
|
3513
|
+
const runId = positional[0];
|
|
3514
|
+
const path = runId ? `/api/orchestration/runs/${encodeURIComponent(runId)}` : '/api/orchestration/runs';
|
|
3515
|
+
const res = await fetch(`${base}${path}`, { headers });
|
|
3516
|
+
const env = await res.json();
|
|
3517
|
+
if (!res.ok) { errorln(`Daemon error (${res.status}): ${env.error || 'unknown'}`); process.exit(1); }
|
|
3518
|
+
if (wantsJson) { println(JSON.stringify(env.data, null, 2)); return; }
|
|
3519
|
+
if (runId) {
|
|
3520
|
+
println(`Run ${env.data.runId} — ${env.data.status} · executionMode=${env.data.execution?.executionMode}`);
|
|
3521
|
+
for (const t of env.data.tasks || []) println(` ${t.id} ${t.role} — ${t.status}${t.output ? ` (${t.output.length} chars)` : ''}`);
|
|
3522
|
+
} else {
|
|
3523
|
+
for (const r of env.data.runs || []) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
|
|
3524
|
+
}
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
} catch (err) {
|
|
3528
|
+
errorln(`Could not reach the orchestration daemon at ${base}: ${err.message}. Start it with \`construct dashboard\`.`);
|
|
3529
|
+
process.exit(1);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3367
3533
|
if (sub === 'run') {
|
|
3368
3534
|
const { runOrchestration, planRun, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
3369
3535
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
@@ -3380,12 +3546,15 @@ async function cmdOrchestrate(args) {
|
|
|
3380
3546
|
fileCount: Number(flag('--file-count') || 0),
|
|
3381
3547
|
moduleCount: Number(flag('--module-count') || 0),
|
|
3382
3548
|
};
|
|
3383
|
-
const run = rest.includes('--no-execute') ? planRun(request, {}) : runOrchestration(request, {});
|
|
3549
|
+
const run = rest.includes('--no-execute') ? await planRun(request, {}) : await runOrchestration(request, {});
|
|
3384
3550
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3385
3551
|
println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode} · backend=${run.workerBackend} · hostRole=${run.hostRole}`);
|
|
3386
3552
|
if (run.execution.degraded) println(` degraded: ${run.execution.degradationReason}`);
|
|
3387
3553
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
3388
3554
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
3555
|
+
for (const t of run.tasks) {
|
|
3556
|
+
if (t.reasoning) println(` ↳ ${t.role} reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
3557
|
+
}
|
|
3389
3558
|
return;
|
|
3390
3559
|
}
|
|
3391
3560
|
|
|
@@ -3393,14 +3562,17 @@ async function cmdOrchestrate(args) {
|
|
|
3393
3562
|
const { getRun, getRuns, hostAdapterMetadata } = await import('../lib/orchestration/runtime.mjs');
|
|
3394
3563
|
const runId = rest.find((a) => !a.startsWith('--'));
|
|
3395
3564
|
if (runId) {
|
|
3396
|
-
const run = getRun(process.cwd(), runId);
|
|
3565
|
+
const run = await getRun(process.cwd(), runId);
|
|
3397
3566
|
if (!run) { errorln(`Run not found: ${runId}`); process.exit(1); }
|
|
3398
3567
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
3399
3568
|
println(`Run ${run.runId} — ${run.status} · executionMode=${run.execution.executionMode}`);
|
|
3400
|
-
for (const t of run.tasks)
|
|
3569
|
+
for (const t of run.tasks) {
|
|
3570
|
+
println(` ${t.id} ${t.role} — ${t.status}`);
|
|
3571
|
+
if (t.reasoning) println(` reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
3572
|
+
}
|
|
3401
3573
|
return;
|
|
3402
3574
|
}
|
|
3403
|
-
const runs = getRuns(process.cwd());
|
|
3575
|
+
const runs = await getRuns(process.cwd());
|
|
3404
3576
|
if (wantsJson) { println(JSON.stringify(runs, null, 2)); return; }
|
|
3405
3577
|
if (!runs.length) { println('No orchestration runs recorded.'); return; }
|
|
3406
3578
|
for (const r of runs) println(` ${r.runId} — ${r.status} · ${r.executionMode || ''} · ${r.createdAt}`);
|
|
@@ -5059,6 +5231,7 @@ async function readStdin() {
|
|
|
5059
5231
|
const handlers = new Map([
|
|
5060
5232
|
// Core
|
|
5061
5233
|
['dev', cmdUp],
|
|
5234
|
+
['dashboard', cmdServe],
|
|
5062
5235
|
['stop', cmdDown],
|
|
5063
5236
|
['status', cmdStatus],
|
|
5064
5237
|
['install', cmdSetup],
|
|
@@ -5080,6 +5253,7 @@ const handlers = new Map([
|
|
|
5080
5253
|
['diff', cmdDiff],
|
|
5081
5254
|
['distill', cmdDistill],
|
|
5082
5255
|
['ingest', cmdIngest],
|
|
5256
|
+
['export', cmdExport],
|
|
5083
5257
|
['infer', cmdInfer],
|
|
5084
5258
|
['search', cmdSearch],
|
|
5085
5259
|
['storage', cmdStorage],
|
|
@@ -5119,6 +5293,10 @@ const handlers = new Map([
|
|
|
5119
5293
|
['models', cmdModels],
|
|
5120
5294
|
['execution', cmdExecution],
|
|
5121
5295
|
['orchestrate', cmdOrchestrate],
|
|
5296
|
+
['acp', async () => {
|
|
5297
|
+
const { runAcpServer } = await import('../lib/acp/server.mjs');
|
|
5298
|
+
runAcpServer({ input: process.stdin, output: process.stdout, env: process.env, defaultCwd: process.cwd() });
|
|
5299
|
+
}],
|
|
5122
5300
|
['beads:stats', async (args) => {
|
|
5123
5301
|
const { getContentionStats, getHumanStatus } = await import('../lib/beads-optimistic.mjs');
|
|
5124
5302
|
const jsonOutput = args.includes('--json');
|
|
@@ -24,6 +24,7 @@ import path from 'node:path';
|
|
|
24
24
|
import { fileURLToPath } from 'node:url';
|
|
25
25
|
|
|
26
26
|
import { stageProjectAdapters } from '../lib/install/stage-project.mjs';
|
|
27
|
+
import { missingIgnorePatterns } from '../lib/host-disposition.mjs';
|
|
27
28
|
|
|
28
29
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const PKG_ROOT = path.resolve(HERE, '..');
|
|
@@ -70,23 +71,17 @@ try {
|
|
|
70
71
|
}
|
|
71
72
|
} catch { /* fall through */ }
|
|
72
73
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
// stay project-only and land when the user runs `construct init` in a repo.
|
|
74
|
+
// ADR-0029: machine-scope writes are opt-in. The postinstall hook for a global
|
|
75
|
+
// install prints scope guidance and exits; `~/.claude/CLAUDE.md`,
|
|
76
|
+
// `~/.claude/settings.json`, and `~/.construct/*` land only when the user runs
|
|
77
|
+
// `construct install --scope=user`, so the consent point is visible.
|
|
78
78
|
|
|
79
79
|
if (process.env.npm_config_global === 'true' || process.env.npm_config_global === true) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
if (result.status !== 0) {
|
|
87
|
-
fail(`Global front-door sync failed (exit ${result.status}).`, 'Run `construct sync --global` to complete it.');
|
|
88
|
-
}
|
|
89
|
-
}
|
|
80
|
+
log('global install detected; machine-scope setup is opt-in (ADR-0029)');
|
|
81
|
+
log('to wire ~/.construct/* and the front-door agent, run:');
|
|
82
|
+
log(' construct install --scope=user');
|
|
83
|
+
log('to set up a project, cd into it and run:');
|
|
84
|
+
log(' construct init');
|
|
90
85
|
process.exit(0);
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -114,6 +109,21 @@ try {
|
|
|
114
109
|
pkgVersion: PKG_VERSION,
|
|
115
110
|
log,
|
|
116
111
|
});
|
|
112
|
+
|
|
113
|
+
// ADR-0027: Ensure .gitignore covers the newly staged adapters (construct-f6l6).
|
|
114
|
+
// Idempotent: missingIgnorePatterns returns only patterns not already present.
|
|
115
|
+
const giPath = path.join(initCwd, '.gitignore');
|
|
116
|
+
const existing = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
117
|
+
const missing = missingIgnorePatterns(existing);
|
|
118
|
+
if (missing.length > 0) {
|
|
119
|
+
const HEADER = '# Construct — generated adapters, launcher, and runtime state.';
|
|
120
|
+
const SUBHEADER = '# Machine-specific, recreated by `construct sync`; never source (ADR-0027).';
|
|
121
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
122
|
+
const block = `${prefix}\n${HEADER}\n${SUBHEADER}\n${missing.join('\n')}\n`;
|
|
123
|
+
const { appendFileSync } = await import('node:fs');
|
|
124
|
+
appendFileSync(giPath, block, 'utf8');
|
|
125
|
+
log(`appended ${missing.length} Construct ignore pattern(s) to .gitignore`);
|
|
126
|
+
}
|
|
117
127
|
} catch (err) {
|
|
118
128
|
fail(`Adapter staging failed: ${err.message}`, 'The package is installed; run `npx construct init` in this project to complete setup.');
|
|
119
129
|
// Intentionally exit 0: staging is best-effort completion, and a non-zero exit
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/acp/server.mjs — Construct as an Agent Client Protocol (ACP) server.
|
|
3
|
+
*
|
|
4
|
+
* ACP (agentclientprotocol.com) is the editor↔agent standard that lets Zed,
|
|
5
|
+
* JetBrains, and the VS Code ACP client drive any agent over JSON-RPC 2.0 on
|
|
6
|
+
* stdio. This makes Construct a first-class native agent in those editors: an
|
|
7
|
+
* ACP `session/prompt` runs a real multi-specialist orchestration via the same
|
|
8
|
+
* engine the daemon and the MCP tool use (ADR-0023) — the editor is a thin
|
|
9
|
+
* client, Construct owns the loop.
|
|
10
|
+
*
|
|
11
|
+
* Framing is newline-delimited JSON (one JSON-RPC message per line). Supported
|
|
12
|
+
* methods: `initialize`, `session/new`, `session/prompt`, `session/cancel`;
|
|
13
|
+
* progress streams as `session/update` notifications. The prompt bridges to
|
|
14
|
+
* planRun/executeRun and forwards run-lifecycle events as agent message chunks.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createInterface } from 'node:readline';
|
|
18
|
+
|
|
19
|
+
const PROTOCOL_VERSION = 1;
|
|
20
|
+
|
|
21
|
+
function describeEvent(event) {
|
|
22
|
+
if (event.type === 'planned') return `Planned ${event.tasks ?? ''} specialist task(s).`;
|
|
23
|
+
if (event.type === 'running') return 'Running orchestration…';
|
|
24
|
+
if (event.type === 'task') return `· ${event.role ?? 'task'} — ${event.status ?? ''}`;
|
|
25
|
+
if (event.type === 'completed') return `Completed: ${event.status ?? 'done'}.`;
|
|
26
|
+
if (event.type === 'error') return `Error: ${event.error?.message ?? 'run failed'}.`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function summarize(run) {
|
|
31
|
+
const tasks = (run.tasks || []).map((t) => `- ${t.role} (${t.status})`).join('\n');
|
|
32
|
+
return `Orchestration ${run.status} — executionMode=${run.execution?.executionMode}.\n${tasks}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function textFromPrompt(prompt) {
|
|
36
|
+
if (!Array.isArray(prompt)) return '';
|
|
37
|
+
return prompt.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('\n').trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function runAcpServer({ input, output, env = process.env, defaultCwd = process.cwd() } = {}) {
|
|
41
|
+
const sessions = new Map();
|
|
42
|
+
let sessionCounter = 0;
|
|
43
|
+
|
|
44
|
+
const write = (msg) => output.write(`${JSON.stringify(msg)}\n`);
|
|
45
|
+
const respond = (id, result) => write({ jsonrpc: '2.0', id, result });
|
|
46
|
+
const fail = (id, code, message) => write({ jsonrpc: '2.0', id, error: { code, message } });
|
|
47
|
+
const notify = (method, params) => write({ jsonrpc: '2.0', method, params });
|
|
48
|
+
const update = (sessionId, text) => notify('session/update', { sessionId, update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } } });
|
|
49
|
+
|
|
50
|
+
async function handlePrompt(id, params) {
|
|
51
|
+
const sessionId = params?.sessionId;
|
|
52
|
+
const session = sessions.get(sessionId);
|
|
53
|
+
if (!session) return fail(id, -32602, `Unknown sessionId: ${sessionId}`);
|
|
54
|
+
const text = textFromPrompt(params?.prompt);
|
|
55
|
+
if (!text) return fail(id, -32602, 'prompt has no text content');
|
|
56
|
+
|
|
57
|
+
const { planRun, executeRun } = await import('../orchestration/runtime.mjs');
|
|
58
|
+
const { onRunEvent, isCancelRequested } = await import('../orchestration/events.mjs');
|
|
59
|
+
try {
|
|
60
|
+
const planned = await planRun({ request: text, requestedStrategy: 'orchestrated' }, { env, cwd: session.cwd });
|
|
61
|
+
session.runId = planned.runId;
|
|
62
|
+
const off = onRunEvent(planned.runId, (event) => {
|
|
63
|
+
const line = describeEvent(event);
|
|
64
|
+
if (line) update(sessionId, line);
|
|
65
|
+
});
|
|
66
|
+
const run = await executeRun(session.cwd, planned.runId, { env, workerBackend: 'inline' });
|
|
67
|
+
off();
|
|
68
|
+
update(sessionId, summarize(run));
|
|
69
|
+
respond(id, { stopReason: isCancelRequested(planned.runId) ? 'cancelled' : 'end_turn' });
|
|
70
|
+
} catch (err) {
|
|
71
|
+
update(sessionId, `Error: ${err.message}`);
|
|
72
|
+
respond(id, { stopReason: 'refusal' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function dispatch(msg) {
|
|
77
|
+
const { id, method, params } = msg;
|
|
78
|
+
if (method === 'initialize') {
|
|
79
|
+
respond(id, {
|
|
80
|
+
protocolVersion: typeof params?.protocolVersion === 'number' ? params.protocolVersion : PROTOCOL_VERSION,
|
|
81
|
+
agentCapabilities: { promptCapabilities: { image: false, audio: false, embeddedContext: true } },
|
|
82
|
+
authMethods: [],
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (method === 'session/new') {
|
|
87
|
+
const sessionId = `acp-${++sessionCounter}`;
|
|
88
|
+
sessions.set(sessionId, { cwd: params?.cwd || defaultCwd, runId: null });
|
|
89
|
+
respond(id, { sessionId });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (method === 'session/prompt') { await handlePrompt(id, params); return; }
|
|
93
|
+
if (method === 'session/cancel') {
|
|
94
|
+
const session = sessions.get(params?.sessionId);
|
|
95
|
+
if (session?.runId) { const { requestCancel } = await import('../orchestration/events.mjs'); requestCancel(session.runId); }
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (id !== undefined) fail(id, -32601, `Method not found: ${method}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
102
|
+
rl.on('line', (line) => {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed) return;
|
|
105
|
+
let msg;
|
|
106
|
+
try { msg = JSON.parse(trimmed); } catch { return; }
|
|
107
|
+
Promise.resolve(dispatch(msg)).catch((err) => {
|
|
108
|
+
if (msg?.id !== undefined) fail(msg.id, -32603, err.message);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { close: () => rl.close() };
|
|
113
|
+
}
|