@geraldmaron/construct 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/bin/construct +336 -17
- package/lib/artifact-loop-core.mjs +26 -5
- package/lib/artifact-manifest-overlay.mjs +273 -0
- package/lib/artifact-manifest.mjs +13 -10
- package/lib/cli-commands.mjs +44 -4
- package/lib/config/source-target-registry.mjs +1 -0
- package/lib/config/source-targets.mjs +30 -0
- package/lib/doc-stamp.mjs +12 -0
- package/lib/doctor/index.mjs +2 -1
- package/lib/doctor/source-target-health.mjs +101 -0
- package/lib/doctor/watchers/source-targets.mjs +44 -0
- package/lib/document-ingest.mjs +25 -3
- package/lib/embed/demand-fetch.mjs +30 -36
- package/lib/embed/providers/directory.mjs +117 -0
- package/lib/embed/providers/registry.mjs +7 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +1 -1
- package/lib/extensions/manifests/directory.manifest.json +24 -1
- package/lib/extensions/manifests/github.manifest.json +10 -0
- package/lib/extensions/manifests/linear.manifest.json +1 -1
- package/lib/hooks/session-start.mjs +18 -11
- package/lib/init-unified.mjs +2 -7
- package/lib/knowledge/rag.mjs +52 -11
- package/lib/knowledge/search.mjs +69 -6
- package/lib/knowledge/synthesis.mjs +157 -0
- package/lib/mcp/server.mjs +2 -2
- package/lib/mcp/tool-definitions-workflow.mjs +35 -7
- package/lib/mcp/tools/artifact-author.mjs +126 -13
- package/lib/mcp/tools/orchestration-run.mjs +3 -0
- package/lib/mcp/tools/skills.mjs +17 -0
- package/lib/model-cheapest-provider.mjs +2 -1
- package/lib/model-policy.mjs +329 -0
- package/lib/model-router.mjs +10 -9
- package/lib/model-tiers.mjs +27 -0
- package/lib/models/catalog.mjs +5 -4
- package/lib/orchestration/classification.mjs +11 -0
- package/lib/orchestration/context-bindings.mjs +85 -0
- package/lib/orchestration/readiness.mjs +2 -1
- package/lib/orchestration/research-evidence-gate.mjs +104 -0
- package/lib/orchestration/runtime.mjs +35 -1
- package/lib/orchestration/worker.mjs +9 -0
- package/lib/providers/directory/index.mjs +19 -7
- package/lib/setup.mjs +2 -1
- package/lib/sources/content-roots.mjs +147 -0
- package/lib/sources/repo-cache.mjs +142 -0
- package/lib/template-registry.mjs +2 -0
- package/lib/test-corpus-inventory.mjs +1 -0
- package/lib/tracker/contribute.mjs +266 -0
- package/lib/validator.mjs +2 -3
- package/package.json +1 -1
- package/registry/agent-manifest.json +0 -4
- package/registry/capabilities.json +20 -1
- package/templates/docs/README.md +22 -0
- package/templates/docs/adhoc.md +12 -0
- package/templates/docs/strategy-comparison.md +19 -0
package/README.md
CHANGED
|
@@ -198,6 +198,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
198
198
|
| `construct reflect` | Capture improvement feedback and update Construct core |
|
|
199
199
|
| `construct search` | Hybrid search across project state |
|
|
200
200
|
| `construct storage` | Manage storage backend |
|
|
201
|
+
| `construct synthesize` | Cross-project synthesis: map each registered project, reduce to an origin-cited answer |
|
|
201
202
|
| `construct tags` | Manage the controlled tag vocabulary (propose, add, deprecate, audit) |
|
|
202
203
|
| `construct team` | Team review, template listing, and custom team authoring (`team:add` / `team:remove` are internal registry editors) |
|
|
203
204
|
| `construct tools` | Detect optional publish pipeline binaries (Pandoc, D2, VHS, Playwright) |
|
|
@@ -220,6 +221,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
220
221
|
| `construct models` | Show or update model tier assignments |
|
|
221
222
|
| `construct orchestrate` | Construct-owned local orchestration runtime and readiness preflight |
|
|
222
223
|
| `construct plugin` | Manage external Construct plugin manifests |
|
|
224
|
+
| `construct tracker` | Analyze registered projects and contribute governed issue proposals to an external tracker (Jira) |
|
|
223
225
|
|
|
224
226
|
### Integrations
|
|
225
227
|
|
|
@@ -288,6 +290,7 @@ The embed daemon writes its supervisor stdout log to `~/.cx/runtime/embed-daemon
|
|
|
288
290
|
| `construct scheduler` | Manage scheduled background jobs (tag-mining, doc-hygiene, skill-rollup) |
|
|
289
291
|
| `construct skills` | Skill relevance detection |
|
|
290
292
|
| `construct sources` | Manage typed integration source targets in construct.config.json |
|
|
293
|
+
| `construct templates` | List doc templates and register custom document classes (project-tier overlay; builtin manifest untouched) |
|
|
291
294
|
| `construct uninstall` | Remove Construct state |
|
|
292
295
|
| `construct update` | Reinstall this checkout |
|
|
293
296
|
| `construct upgrade` | Upgrade to latest npm version |
|
package/bin/construct
CHANGED
|
@@ -19,6 +19,7 @@ import { CLI_COMMANDS, CATEGORY_ORDER, formatCommandHelp } from '../lib/cli-comm
|
|
|
19
19
|
import { buildStatus, formatStatusReport } from '../lib/status.mjs';
|
|
20
20
|
import { validateRegistry } from '../lib/validator.mjs';
|
|
21
21
|
import { readCurrentModels, readOpenRouterApiKeyFromOpenCodeConfig, applyToEnv, resetEnv, setModelWithTierInference, listAvailableModels } from '../lib/model-router.mjs';
|
|
22
|
+
import { MODEL_TIERS } from '../lib/model-tiers.mjs';
|
|
22
23
|
import { pollFreeModels, topForTier, selectForTier } from '../lib/model-free-selector.mjs';
|
|
23
24
|
import { cmdMcpList, cmdMcpAdd, cmdMcpRemove, cmdMcpInfo } from '../lib/mcp-manager.mjs';
|
|
24
25
|
import { cmdOllama } from '../lib/ollama-manager.mjs';
|
|
@@ -1513,6 +1514,27 @@ async function cmdDoctor() {
|
|
|
1513
1514
|
add(`Reconciliation drift check failed: ${err.message}`, false, true);
|
|
1514
1515
|
}
|
|
1515
1516
|
|
|
1517
|
+
// Source-target health (B8): directory paths that vanished and corpus caches
|
|
1518
|
+
// past TTL, plus soft credential-presence notices. Filesystem + env only — no
|
|
1519
|
+
// outbound fetch — so the default doctor keeps its zero-network invariant. A
|
|
1520
|
+
// project with no registered targets stays silent.
|
|
1521
|
+
try {
|
|
1522
|
+
const { checkSourceTargetHealth } = await import('../lib/doctor/source-target-health.mjs');
|
|
1523
|
+
const { configured, findings } = checkSourceTargetHealth({ cwd: process.cwd() });
|
|
1524
|
+
if (configured > 0) {
|
|
1525
|
+
const problems = findings.filter((f) => !f.ok);
|
|
1526
|
+
if (problems.length === 0) {
|
|
1527
|
+
add(`Source targets healthy (${configured} configured)`, true);
|
|
1528
|
+
} else {
|
|
1529
|
+
for (const f of findings) {
|
|
1530
|
+
if (!f.ok) add(f.label, false, Boolean(f.optional));
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
add(`Source-target health check failed: ${err.message}`, false, true);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1516
1538
|
// Docling document-extraction runtime (Python venv via uv). Provisioned lazily
|
|
1517
1539
|
// on first document ingest, or eagerly via `construct install --with-docling`.
|
|
1518
1540
|
// Advisory: absence is fine until a document is ingested.
|
|
@@ -3292,6 +3314,11 @@ async function cmdSources(args) {
|
|
|
3292
3314
|
findProjectConfigPath,
|
|
3293
3315
|
PROJECT_CONFIG_FILENAME,
|
|
3294
3316
|
} = await import('../lib/config/project-config.mjs');
|
|
3317
|
+
const {
|
|
3318
|
+
isCorpusTarget,
|
|
3319
|
+
corpusFreshness,
|
|
3320
|
+
syncCorpusTarget,
|
|
3321
|
+
} = await import('../lib/sources/repo-cache.mjs');
|
|
3295
3322
|
|
|
3296
3323
|
const cwd = process.cwd();
|
|
3297
3324
|
const action = args[0] ?? 'list';
|
|
@@ -3305,7 +3332,15 @@ async function cmdSources(args) {
|
|
|
3305
3332
|
println('');
|
|
3306
3333
|
if (configTargets.length === 0) println(' (no targets in construct.config.json)');
|
|
3307
3334
|
for (const t of configTargets) {
|
|
3308
|
-
|
|
3335
|
+
let content = '';
|
|
3336
|
+
if (isCorpusTarget(t)) {
|
|
3337
|
+
const fresh = corpusFreshness(t, { projectRoot: cwd });
|
|
3338
|
+
const freshness = !fresh.cached
|
|
3339
|
+
? 'not synced'
|
|
3340
|
+
: `synced ${fresh.lastFetch ?? 'unknown'}${fresh.stale ? ', stale' : ''}`;
|
|
3341
|
+
content = ` · corpus(${fresh.ref}) · ${freshness}`;
|
|
3342
|
+
}
|
|
3343
|
+
println(` ${t.id} · ${t.provider} · ${JSON.stringify(t.selector)}${content}`);
|
|
3309
3344
|
}
|
|
3310
3345
|
if (envTargets.length) {
|
|
3311
3346
|
println('');
|
|
@@ -3391,7 +3426,87 @@ async function cmdSources(args) {
|
|
|
3391
3426
|
return;
|
|
3392
3427
|
}
|
|
3393
3428
|
|
|
3394
|
-
|
|
3429
|
+
if (action === 'sync') {
|
|
3430
|
+
const onlyId = args[1] ?? null;
|
|
3431
|
+
const { config } = loadProjectConfig(cwd, process.env);
|
|
3432
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, process.env)
|
|
3433
|
+
.filter((t) => isCorpusTarget(t))
|
|
3434
|
+
.filter((t) => !onlyId || t.id === onlyId);
|
|
3435
|
+
|
|
3436
|
+
if (onlyId && !targets.length) {
|
|
3437
|
+
errorln(`No corpus target with id: ${onlyId}`);
|
|
3438
|
+
process.exit(1);
|
|
3439
|
+
}
|
|
3440
|
+
if (!targets.length) {
|
|
3441
|
+
info('no corpus targets to sync');
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
let failures = 0;
|
|
3446
|
+
for (const t of targets) {
|
|
3447
|
+
try {
|
|
3448
|
+
const result = syncCorpusTarget(t, { projectRoot: cwd });
|
|
3449
|
+
info(`synced ${t.id} (${result.mode}) → ${result.head.slice(0, 7)} @ ${result.dir}`);
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
failures++;
|
|
3452
|
+
errorln(`failed to sync ${t.id}: ${err.message}`);
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
if (failures) process.exit(1);
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
errorln(`Unknown sources action: ${action}. Available: list, add, remove, validate, sync`);
|
|
3460
|
+
process.exit(1);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// Read a `--flag value` or `--flag=value` option out of an argv slice.
|
|
3464
|
+
|
|
3465
|
+
function readOption(args, flag) {
|
|
3466
|
+
const eq = args.find((a) => a.startsWith(`${flag}=`));
|
|
3467
|
+
if (eq) return eq.slice(flag.length + 1);
|
|
3468
|
+
const idx = args.indexOf(flag);
|
|
3469
|
+
if (idx >= 0 && args[idx + 1] && !args[idx + 1].startsWith('--')) return args[idx + 1];
|
|
3470
|
+
return undefined;
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
async function cmdTemplates(args) {
|
|
3474
|
+
const { registerArtifactType } = await import('../lib/artifact-manifest-overlay.mjs');
|
|
3475
|
+
const { listTemplates } = await import('../lib/mcp/tools/skills.mjs');
|
|
3476
|
+
const cwd = process.cwd();
|
|
3477
|
+
const action = args[0] ?? 'list';
|
|
3478
|
+
|
|
3479
|
+
if (action === 'list' || action === 'show') {
|
|
3480
|
+
const res = listTemplates({ ROOT_DIR });
|
|
3481
|
+
println('Templates');
|
|
3482
|
+
println(` shipped: ${res.shipped.join(', ')}`);
|
|
3483
|
+
println(` overridden: ${res.overridden.length ? res.overridden.join(', ') : '(none)'}`);
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
if (action === 'register') {
|
|
3488
|
+
const type = args.slice(1).find((a) => !a.startsWith('--'));
|
|
3489
|
+
if (!type) {
|
|
3490
|
+
errorln('Usage: construct templates register <type> [--description "..."] [--from <file>] [--force]');
|
|
3491
|
+
process.exit(1);
|
|
3492
|
+
}
|
|
3493
|
+
const description = readOption(args, '--description');
|
|
3494
|
+
const from = readOption(args, '--from');
|
|
3495
|
+
const force = args.includes('--force');
|
|
3496
|
+
try {
|
|
3497
|
+
const r = registerArtifactType({ type, description, from, cwd, force });
|
|
3498
|
+
ok(`registered document class '${r.type}'`);
|
|
3499
|
+
info(`template: ${path.relative(cwd, r.templatePath)}`);
|
|
3500
|
+
info(`overlay: ${path.relative(cwd, r.overlayPath)}`);
|
|
3501
|
+
info(`author it via MCP: author_artifact {type:"${r.type}", draft_markdown:"..."}`);
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
errorln(err.message);
|
|
3504
|
+
process.exit(1);
|
|
3505
|
+
}
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
errorln(`Unknown templates action: ${action}. Available: list, register`);
|
|
3395
3510
|
process.exit(1);
|
|
3396
3511
|
}
|
|
3397
3512
|
|
|
@@ -4183,7 +4298,7 @@ async function cmdOrchestrate(args) {
|
|
|
4183
4298
|
println(summarizeOrchestrationReadiness(readiness));
|
|
4184
4299
|
println(`Reason: ${readiness.reasonCode}`);
|
|
4185
4300
|
println(`Next: ${readiness.nextStep}`);
|
|
4186
|
-
const modelLine =
|
|
4301
|
+
const modelLine = MODEL_TIERS
|
|
4187
4302
|
.map((tier) => `${tier}=${readiness.modelResolved?.[tier] ? 'resolved' : 'unresolved'}`)
|
|
4188
4303
|
.join(' ');
|
|
4189
4304
|
println(`Execution: workerBackend=${readiness.workerBackend} webMode=${readiness.webMode} credentialMaterializable=${readiness.credentialMaterializable} (${modelLine})`);
|
|
@@ -4255,7 +4370,7 @@ async function cmdOrchestrate(args) {
|
|
|
4255
4370
|
const { shapeRun } = await import('../lib/mcp/tools/orchestration-run.mjs');
|
|
4256
4371
|
const positional = rest.filter((a, i) => !a.startsWith('--') && !(i > 0 && rest[i - 1].startsWith('--') && !['--no-construct', '--no-execute', '--json'].includes(rest[i - 1])));
|
|
4257
4372
|
const text = flag('--text') || positional.join(' ').trim();
|
|
4258
|
-
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
4373
|
+
if (!text) { errorln('Usage: construct orchestrate run "<request>" [--workflow-type T] [--strategy orchestrated|prompt-only|auto] [--context id[:role],...] [--host H] [--host-model M] [--no-construct] [--no-execute] [--json]'); process.exit(1); }
|
|
4259
4374
|
const cliWorkerBackend = flag('--worker-backend');
|
|
4260
4375
|
// The host worker backend materializes prompts for an attached MCP host
|
|
4261
4376
|
// session to execute and submit back via orchestration_task_result — a
|
|
@@ -4267,6 +4382,19 @@ async function cmdOrchestrate(args) {
|
|
|
4267
4382
|
errorln('--worker-backend=host requires an attached MCP host session to execute the materialized prompts and submit results back — the CLI has no such session. Use --worker-backend=inline (prepare only) or --worker-backend=provider (needs a provider key), or invoke orchestration_run through an MCP host instead.');
|
|
4268
4383
|
process.exit(1);
|
|
4269
4384
|
}
|
|
4385
|
+
// --context=id[:role],id[:role] binds the run to registered source targets;
|
|
4386
|
+
// an unknown id is rejected at plan time (below), never a silent skip. Accept
|
|
4387
|
+
// both `--context=…` and `--context …` since the epic UX documents the former
|
|
4388
|
+
// while sibling orchestrate flags use the latter.
|
|
4389
|
+
const contextInline = rest.find((a) => a.startsWith('--context='));
|
|
4390
|
+
const contextFlag = contextInline ? contextInline.slice('--context='.length) : flag('--context');
|
|
4391
|
+
const contextTargets = contextFlag
|
|
4392
|
+
? contextFlag.split(',').map((s) => s.trim()).filter(Boolean).map((entry) => {
|
|
4393
|
+
const colon = entry.indexOf(':');
|
|
4394
|
+
return colon === -1 ? { id: entry } : { id: entry.slice(0, colon), role: entry.slice(colon + 1) || undefined };
|
|
4395
|
+
})
|
|
4396
|
+
: undefined;
|
|
4397
|
+
|
|
4270
4398
|
const request = {
|
|
4271
4399
|
request: text,
|
|
4272
4400
|
workflowType: flag('--workflow-type'),
|
|
@@ -4278,13 +4406,20 @@ async function cmdOrchestrate(args) {
|
|
|
4278
4406
|
workerBackend: cliWorkerBackend,
|
|
4279
4407
|
fileCount: Number(flag('--file-count') || 0),
|
|
4280
4408
|
moduleCount: Number(flag('--module-count') || 0),
|
|
4409
|
+
contextTargets,
|
|
4281
4410
|
};
|
|
4282
4411
|
// runOrchestration/executeRun resolve the worker backend from opts.workerBackend,
|
|
4283
4412
|
// not from request — the flag must reach opts explicitly or an override is silently
|
|
4284
4413
|
// dropped (construct-1xlz).
|
|
4285
4414
|
|
|
4286
4415
|
const runOpts = cliWorkerBackend ? { workerBackend: cliWorkerBackend } : {};
|
|
4287
|
-
|
|
4416
|
+
let run;
|
|
4417
|
+
try {
|
|
4418
|
+
run = rest.includes('--no-execute') ? await planRun(request, runOpts) : await runOrchestration(request, runOpts);
|
|
4419
|
+
} catch (err) {
|
|
4420
|
+
if (err?.code === 'CONTEXT_TARGET_UNKNOWN') { errorln(err.message); process.exit(1); }
|
|
4421
|
+
throw err;
|
|
4422
|
+
}
|
|
4288
4423
|
if (wantsJson) { println(JSON.stringify(hostAdapterMetadata(run), null, 2)); return; }
|
|
4289
4424
|
|
|
4290
4425
|
// AP5.1/AP5.4: the CLI must never print the raw persisted status — a
|
|
@@ -4296,6 +4431,9 @@ async function cmdOrchestrate(args) {
|
|
|
4296
4431
|
if (shaped.degraded) println(` degraded: ${shaped.degradationReason}`);
|
|
4297
4432
|
if (run.tasks.length) println(` tasks: ${run.tasks.map((t) => `${t.role}(${t.status})`).join(' → ')}`);
|
|
4298
4433
|
else println(' tasks: none (no Construct-owned specialist sequence for this execution mode)');
|
|
4434
|
+
if (run.contextBindings?.length) {
|
|
4435
|
+
println(` context: ${run.contextBindings.map((b) => `${b.id}[${b.provider}${b.role ? `:${b.role}` : ''}]${b.contentRoot ? '✓' : ''}`).join(', ')}`);
|
|
4436
|
+
}
|
|
4299
4437
|
for (const t of run.tasks) {
|
|
4300
4438
|
if (t.reasoning) println(` ↳ ${t.role} reasoning: ${t.reasoning.length > 300 ? `${t.reasoning.slice(0, 300)}…` : t.reasoning}`);
|
|
4301
4439
|
}
|
|
@@ -4416,12 +4554,22 @@ async function cmdDb(args) {
|
|
|
4416
4554
|
async function cmdModels(args) {
|
|
4417
4555
|
const envPath = getUserEnvPath(HOME);
|
|
4418
4556
|
const sub = args[0] && !args[0].startsWith('-') ? args[0] : null;
|
|
4419
|
-
const KNOWN_SUBCOMMANDS = new Set(['list', 'set', 'free', 'reset', 'resolve']);
|
|
4557
|
+
const KNOWN_SUBCOMMANDS = new Set(['list', 'set', 'free', 'reset', 'resolve', 'policy', 'explain']);
|
|
4420
4558
|
if (sub && !KNOWN_SUBCOMMANDS.has(sub)) {
|
|
4421
4559
|
errorln(`Unknown models subcommand: ${sub}. Run \`construct models --help\` for the supported set.`);
|
|
4422
4560
|
process.exit(1);
|
|
4423
4561
|
}
|
|
4424
4562
|
|
|
4563
|
+
if (sub === 'policy') {
|
|
4564
|
+
await cmdModelsPolicy(args.slice(1));
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
if (sub === 'explain') {
|
|
4569
|
+
await cmdModelsExplain(args.slice(1));
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4425
4573
|
if (sub === 'resolve') {
|
|
4426
4574
|
const flag = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : undefined; };
|
|
4427
4575
|
const { resolveEmbeddedModel } = await import('../lib/embedded-contract/model-resolve.mjs');
|
|
@@ -4487,7 +4635,7 @@ async function cmdModels(args) {
|
|
|
4487
4635
|
}
|
|
4488
4636
|
const models = await pollFreeModels(apiKey);
|
|
4489
4637
|
println('Available free models on OpenRouter:\n');
|
|
4490
|
-
for (const currentTier of
|
|
4638
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4491
4639
|
const top = topForTier(models, currentTier, 3);
|
|
4492
4640
|
println(` ${currentTier} (top ${top.length}):`);
|
|
4493
4641
|
top.forEach((model, index) => println(` ${index + 1}. ${model.id}`));
|
|
@@ -4499,7 +4647,7 @@ async function cmdModels(args) {
|
|
|
4499
4647
|
const apiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig();
|
|
4500
4648
|
const models = await pollFreeModels(apiKey);
|
|
4501
4649
|
const selections = {};
|
|
4502
|
-
for (const currentTier of
|
|
4650
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4503
4651
|
const fallbacks = [];
|
|
4504
4652
|
const id = selectForTier(models, currentTier, fallbacks);
|
|
4505
4653
|
if (id) selections[currentTier] = id;
|
|
@@ -4526,7 +4674,7 @@ async function cmdModels(args) {
|
|
|
4526
4674
|
await import('../lib/model-cheapest-provider.mjs');
|
|
4527
4675
|
const allTiers = args.includes('--all-tiers') || !args.find((arg) => arg.startsWith('--tier='));
|
|
4528
4676
|
const tierArg = args.find((arg) => arg.startsWith('--tier='))?.split('=')[1];
|
|
4529
|
-
const tiers = allTiers ? [
|
|
4677
|
+
const tiers = allTiers ? [...MODEL_TIERS] : (tierArg ? [tierArg] : ['standard']);
|
|
4530
4678
|
const selections = {};
|
|
4531
4679
|
for (const currentTier of tiers) {
|
|
4532
4680
|
const { selectCheapestProvider } = await import('../lib/model-cheapest-provider.mjs');
|
|
@@ -4563,9 +4711,9 @@ async function cmdModels(args) {
|
|
|
4563
4711
|
return;
|
|
4564
4712
|
}
|
|
4565
4713
|
const result = readCurrentModels(envPath, {});
|
|
4566
|
-
const nothingSelected =
|
|
4714
|
+
const nothingSelected = MODEL_TIERS.every((t) => !result[t]);
|
|
4567
4715
|
println('Current model assignments:');
|
|
4568
|
-
for (const currentTier of
|
|
4716
|
+
for (const currentTier of MODEL_TIERS) {
|
|
4569
4717
|
const id = result[currentTier];
|
|
4570
4718
|
const source = result.sources?.[currentTier] || (id ? 'registry' : 'not configured');
|
|
4571
4719
|
println(` ${currentTier.padEnd(11)}${id ? `${id} [${source}]` : '(not configured)'}`);
|
|
@@ -4580,6 +4728,101 @@ async function cmdModels(args) {
|
|
|
4580
4728
|
}
|
|
4581
4729
|
}
|
|
4582
4730
|
|
|
4731
|
+
async function cmdModelsPolicy(args) {
|
|
4732
|
+
const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'show';
|
|
4733
|
+
const asJson = args.includes('--json');
|
|
4734
|
+
const {
|
|
4735
|
+
computePolicyPreset, writeModelRegistry, readPolicyView, POLICY_PRESETS,
|
|
4736
|
+
} = await import('../lib/model-policy.mjs');
|
|
4737
|
+
const { defaultModelRegistryPath } = await import('../lib/model-router.mjs');
|
|
4738
|
+
const registryPath = defaultModelRegistryPath(process.env);
|
|
4739
|
+
|
|
4740
|
+
if (sub === 'show') {
|
|
4741
|
+
const view = readPolicyView({ env: process.env, registryPath });
|
|
4742
|
+
if (asJson) {
|
|
4743
|
+
println(JSON.stringify(view, null, 2));
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
println('Effective model policy:');
|
|
4747
|
+
for (const t of view.tiers) {
|
|
4748
|
+
const model = t.model ?? '(not configured)';
|
|
4749
|
+
println(` ${t.tier.padEnd(11)}${model} [${t.source}]`);
|
|
4750
|
+
}
|
|
4751
|
+
println('');
|
|
4752
|
+
println('Work category → tier:');
|
|
4753
|
+
for (const [category, tier] of Object.entries(view.workCategoryMap)) {
|
|
4754
|
+
println(` ${category.padEnd(11)}→ ${tier}`);
|
|
4755
|
+
}
|
|
4756
|
+
println('');
|
|
4757
|
+
println(`Registry: ${registryPath}`);
|
|
4758
|
+
println('Precedence: CX_MODEL_<TIER> env pins always win over registry defaults.');
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
if (sub === 'set') {
|
|
4763
|
+
const preset = args[1] && !args[1].startsWith('-') ? args[1] : null;
|
|
4764
|
+
if (!preset) {
|
|
4765
|
+
errorln(`Usage: construct models policy set <${POLICY_PRESETS.join('|')}>`);
|
|
4766
|
+
process.exit(1);
|
|
4767
|
+
}
|
|
4768
|
+
const apiKey = process.env.OPENROUTER_API_KEY || readOpenRouterApiKeyFromOpenCodeConfig() || null;
|
|
4769
|
+
const result = await computePolicyPreset(preset, { env: process.env, apiKey });
|
|
4770
|
+
if (!result.ok) {
|
|
4771
|
+
errorln(result.refusal);
|
|
4772
|
+
for (const w of result.warnings ?? []) errorln(` ${w}`);
|
|
4773
|
+
process.exit(1);
|
|
4774
|
+
}
|
|
4775
|
+
writeModelRegistry(registryPath, result.models, { preset });
|
|
4776
|
+
if (asJson) {
|
|
4777
|
+
println(JSON.stringify({ preset, registryPath, models: result.models, warnings: result.warnings }, null, 2));
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
println(`Applied \`${preset}\` policy to ${registryPath}:`);
|
|
4781
|
+
for (const tier of MODEL_TIERS) {
|
|
4782
|
+
const def = result.models[tier];
|
|
4783
|
+
const primary = def?.primary ?? '(unset)';
|
|
4784
|
+
const fallback = def?.fallback?.length ? ` (fallback: ${def.fallback.join(', ')})` : '';
|
|
4785
|
+
println(` ${tier.padEnd(11)}${primary}${fallback}`);
|
|
4786
|
+
}
|
|
4787
|
+
for (const w of result.warnings ?? []) errorln(`Note: ${w}`);
|
|
4788
|
+
println('');
|
|
4789
|
+
println('Env pins (CX_MODEL_<TIER>) still override these. Run `construct sync` to propagate.');
|
|
4790
|
+
if (!args.includes('--no-sync')) await cmdSync([]);
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
errorln(`Unknown models policy subcommand: ${sub}. Use \`show\` or \`set <preset>\`.`);
|
|
4795
|
+
process.exit(1);
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4798
|
+
async function cmdModelsExplain(args) {
|
|
4799
|
+
const asJson = args.includes('--json');
|
|
4800
|
+
const roleFlagIdx = args.indexOf('--role');
|
|
4801
|
+
const role = roleFlagIdx !== -1 ? args[roleFlagIdx + 1] : (args[0] && !args[0].startsWith('-') ? args[0] : null);
|
|
4802
|
+
if (!role) {
|
|
4803
|
+
errorln('Usage: construct models explain --role <specialist>');
|
|
4804
|
+
process.exit(1);
|
|
4805
|
+
}
|
|
4806
|
+
const { explainRole } = await import('../lib/model-policy.mjs');
|
|
4807
|
+
const trace = explainRole(role, { env: process.env, rootDir: ROOT_DIR });
|
|
4808
|
+
if (!trace.ok) {
|
|
4809
|
+
errorln(trace.error);
|
|
4810
|
+
process.exit(1);
|
|
4811
|
+
}
|
|
4812
|
+
if (asJson) {
|
|
4813
|
+
println(JSON.stringify(trace, null, 2));
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
println(`Resolution trace for ${trace.role}:`);
|
|
4817
|
+
println(` declared tier: ${trace.declaredTier ?? '(none)'}`);
|
|
4818
|
+
if (trace.declaredModel) println(` declared model: ${trace.declaredModel}`);
|
|
4819
|
+
println(` resolved tier: ${trace.tier}`);
|
|
4820
|
+
println(` resolved model: ${trace.resolvedModel ?? '(not configured)'}`);
|
|
4821
|
+
println(` winning source: ${trace.source}`);
|
|
4822
|
+
println('');
|
|
4823
|
+
println(`Matches: construct models resolve --json --tier ${trace.tier}`);
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4583
4826
|
async function cmdMcp(args) {
|
|
4584
4827
|
const subcmd = args[0] ?? 'list';
|
|
4585
4828
|
const id = args[1];
|
|
@@ -6545,6 +6788,7 @@ const handlers = new Map([
|
|
|
6545
6788
|
['config', cmdConfig],
|
|
6546
6789
|
['approvals', cmdApprovals],
|
|
6547
6790
|
['sources', cmdSources],
|
|
6791
|
+
['templates', cmdTemplates],
|
|
6548
6792
|
['intake', cmdIntake],
|
|
6549
6793
|
['recommendations', cmdRecommendations],
|
|
6550
6794
|
['integrations', cmdIntegrations],
|
|
@@ -7257,9 +7501,13 @@ const handlers = new Map([
|
|
|
7257
7501
|
process.exit(1);
|
|
7258
7502
|
}
|
|
7259
7503
|
const { ask, buildCorpus } = await import('../lib/knowledge/rag.mjs');
|
|
7504
|
+
const { resolveContentRootsFromConfig } = await import('../lib/sources/content-roots.mjs');
|
|
7505
|
+
const { loadProjectConfig } = await import('../lib/config/project-config.mjs');
|
|
7260
7506
|
const rootDir = process.cwd();
|
|
7261
|
-
|
|
7262
|
-
const
|
|
7507
|
+
const { config } = loadProjectConfig(rootDir);
|
|
7508
|
+
const roots = resolveContentRootsFromConfig(config, { projectRoot: rootDir });
|
|
7509
|
+
process.stdout.write(`Building knowledge index${roots.length ? ` (+${roots.length} registered project${roots.length === 1 ? '' : 's'})` : ''}…\n`);
|
|
7510
|
+
const corpus = buildCorpus(rootDir, { roots });
|
|
7263
7511
|
process.stdout.write(`Indexed ${corpus.length} chunks. Retrieving…\n\n`);
|
|
7264
7512
|
const result = await ask(question, { rootDir, corpus });
|
|
7265
7513
|
if (result.cliMissing) {
|
|
@@ -7269,7 +7517,8 @@ const handlers = new Map([
|
|
|
7269
7517
|
if (result.sources?.length) {
|
|
7270
7518
|
process.stdout.write('\n--- Sources ---\n');
|
|
7271
7519
|
for (const s of result.sources.slice(0, 5)) {
|
|
7272
|
-
|
|
7520
|
+
const proj = s.origin?.projectKey && s.origin.projectKey !== 'self' ? ` «${s.origin.projectKey}»` : '';
|
|
7521
|
+
process.stdout.write(` • [${s.source}]${proj} ${s.title} (score: ${s.score?.toFixed(3)})\n`);
|
|
7273
7522
|
}
|
|
7274
7523
|
}
|
|
7275
7524
|
}],
|
|
@@ -7384,9 +7633,10 @@ const handlers = new Map([
|
|
|
7384
7633
|
const tagFlag = restArgs.find((a) => a.startsWith('--tag='))?.replace('--tag=', '');
|
|
7385
7634
|
const tagMatchFlag = restArgs.find((a) => a.startsWith('--tag-match='))?.replace('--tag-match=', '');
|
|
7386
7635
|
const topKFlag = restArgs.find((a) => a.startsWith('--top='))?.replace('--top=', '');
|
|
7636
|
+
const projectsFlag = restArgs.find((a) => a.startsWith('--projects='))?.replace('--projects=', '');
|
|
7387
7637
|
const queryParts = restArgs.filter((a) => !a.startsWith('--'));
|
|
7388
7638
|
const query = queryParts.join(' ').trim();
|
|
7389
|
-
if (!query) { errorln('Usage: construct knowledge search "<query>" [--tag=<tag>] [--tag-match=any|all]'); process.exit(1); }
|
|
7639
|
+
if (!query) { errorln('Usage: construct knowledge search "<query>" [--tag=<tag>] [--tag-match=any|all] [--projects=<id,...>|all|self]'); process.exit(1); }
|
|
7390
7640
|
const { knowledgeSearch } = await import('../lib/knowledge/search.mjs');
|
|
7391
7641
|
const tags = tagFlag ? tagFlag.split(',') : undefined;
|
|
7392
7642
|
const result = knowledgeSearch({
|
|
@@ -7396,11 +7646,13 @@ const handlers = new Map([
|
|
|
7396
7646
|
rootDir: process.cwd(),
|
|
7397
7647
|
tags,
|
|
7398
7648
|
tagMatch: tagMatchFlag || 'any',
|
|
7649
|
+
projects: projectsFlag,
|
|
7399
7650
|
});
|
|
7400
7651
|
if (!result.ok) { errorln(result.message); process.exit(1); }
|
|
7401
|
-
println(`${result.hits.length} result(s) for "${query}"${tags ? ` [tag: ${tags.join(',')}]` : ''}\n`);
|
|
7652
|
+
println(`${result.hits.length} result(s) for "${query}"${tags ? ` [tag: ${tags.join(',')}]` : ''}${projectsFlag ? ` [projects: ${projectsFlag}]` : ''}\n`);
|
|
7402
7653
|
for (const h of result.hits) {
|
|
7403
|
-
|
|
7654
|
+
const proj = h.origin?.projectKey && h.origin.projectKey !== 'self' ? ` «${h.origin.projectKey}»` : '';
|
|
7655
|
+
println(` ${h.file}${h.heading ? ` § ${h.heading}` : ''}${proj} (score: ${h.score})`);
|
|
7404
7656
|
println(` ${h.text.slice(0, 120).replace(/\n/g, ' ')}`);
|
|
7405
7657
|
println('');
|
|
7406
7658
|
}
|
|
@@ -7409,6 +7661,73 @@ const handlers = new Map([
|
|
|
7409
7661
|
errorln(`Unknown knowledge subcommand: ${sub}. Available: trends, index, add, search`);
|
|
7410
7662
|
process.exit(1);
|
|
7411
7663
|
}],
|
|
7664
|
+
['synthesize', async (args) => {
|
|
7665
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
7666
|
+
const projects = flag('projects') || 'all';
|
|
7667
|
+
const ask = flag('ask') || args.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
7668
|
+
const template = flag('template') || null;
|
|
7669
|
+
const dryRun = args.includes('--dry-run');
|
|
7670
|
+
const wantsJson = args.includes('--json');
|
|
7671
|
+
if (!ask) {
|
|
7672
|
+
errorln('Usage: construct synthesize --ask "<question>" [--projects=all|self|id,...] [--template <name>] [--dry-run] [--json]');
|
|
7673
|
+
process.exit(1);
|
|
7674
|
+
}
|
|
7675
|
+
const { synthesize } = await import('../lib/knowledge/synthesis.mjs');
|
|
7676
|
+
const result = await synthesize({ projects, ask, cwd: process.cwd(), template, dryRun, rootDir: ROOT_DIR });
|
|
7677
|
+
if (!result.ok) { errorln(result.message); process.exit(1); }
|
|
7678
|
+
if (wantsJson) { println(JSON.stringify(result, null, 2)); return; }
|
|
7679
|
+
if (dryRun) {
|
|
7680
|
+
println(result.context);
|
|
7681
|
+
println('\n--- Reduce prompt (not sent in --dry-run) ---\n');
|
|
7682
|
+
println(result.prompt);
|
|
7683
|
+
return;
|
|
7684
|
+
}
|
|
7685
|
+
if (result.cliMissing) println('[claude CLI not available — showing assembled context]\n');
|
|
7686
|
+
println(result.answer);
|
|
7687
|
+
return;
|
|
7688
|
+
}],
|
|
7689
|
+
['tracker', async (args) => {
|
|
7690
|
+
const sub = args[0] || 'contribute';
|
|
7691
|
+
const flag = (name) => args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3)
|
|
7692
|
+
?? (args.includes(`--${name}`) ? args[args.indexOf(`--${name}`) + 1] : undefined);
|
|
7693
|
+
if (sub !== 'contribute') { errorln(`Unknown tracker subcommand: ${sub}. Available: contribute`); process.exit(1); }
|
|
7694
|
+
const wantsJson = args.includes('--json');
|
|
7695
|
+
const applyId = flag('apply');
|
|
7696
|
+
const { analyzeAndPropose, applyProposal } = await import('../lib/tracker/contribute.mjs');
|
|
7697
|
+
|
|
7698
|
+
if (applyId) {
|
|
7699
|
+
const approve = flag('approve') || null;
|
|
7700
|
+
const res = await applyProposal({ proposalId: applyId, approveToken: approve, cwd: process.cwd() });
|
|
7701
|
+
if (!res.ok) { errorln(res.message); process.exit(1); }
|
|
7702
|
+
if (wantsJson) { println(JSON.stringify(res, null, 2)); return; }
|
|
7703
|
+
println(`Applied proposal ${res.proposalId} — ${res.dryRun ? 'DRY-RUN (nothing written)' : 'EXECUTED'}`);
|
|
7704
|
+
for (const r of res.results) println(` ${r.key} — ${r.status}`);
|
|
7705
|
+
if (res.dryRun) println('\nRe-run with --approve <token> to write the governed batch.');
|
|
7706
|
+
return;
|
|
7707
|
+
}
|
|
7708
|
+
|
|
7709
|
+
const target = flag('target');
|
|
7710
|
+
const against = flag('against') || 'all';
|
|
7711
|
+
if (!target) {
|
|
7712
|
+
errorln('Usage: construct tracker contribute --target <id> [--against <ids|all>] [--json]\n or: construct tracker contribute --apply <proposal-id> [--approve <token>] [--json]');
|
|
7713
|
+
process.exit(1);
|
|
7714
|
+
}
|
|
7715
|
+
let res;
|
|
7716
|
+
try {
|
|
7717
|
+
res = await analyzeAndPropose({ target, against, cwd: process.cwd() });
|
|
7718
|
+
} catch (err) {
|
|
7719
|
+
errorln(err.message); process.exit(1);
|
|
7720
|
+
}
|
|
7721
|
+
if (!res.ok) { errorln(res.message); process.exit(1); }
|
|
7722
|
+
if (wantsJson) { println(JSON.stringify(res.proposal, null, 2)); return; }
|
|
7723
|
+
const p = res.proposal;
|
|
7724
|
+
println(`Proposal ${p.id} for ${p.projectKey} (${p.targetId}) — ${p.proposals.length} proposed, ${p.suppressed.length} suppressed (${p.existingIssueCount} existing issues)`);
|
|
7725
|
+
println(` artifact: ${res.paths.md}`);
|
|
7726
|
+
for (const it of p.proposals) println(` + ${it.summary}`);
|
|
7727
|
+
for (const s of p.suppressed) println(` ~ suppressed: ${s.summary} → ${s.matchedIssueKey} (${s.similarity})`);
|
|
7728
|
+
println(`\nReview the artifact, then: construct tracker contribute --apply ${p.id} [--approve <token>]`);
|
|
7729
|
+
return;
|
|
7730
|
+
}],
|
|
7412
7731
|
['sandbox', async (args) => {
|
|
7413
7732
|
const sub = args[0] || 'create';
|
|
7414
7733
|
const { createSandbox, listSandboxes, deleteSandbox, pruneSandboxes } = await import('../lib/sandbox.mjs');
|
|
@@ -234,8 +234,8 @@ function todayStamp() {
|
|
|
234
234
|
return new Date().toISOString().slice(0, 10);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
function outputPathForType(artifactType, slug, cwd) {
|
|
238
|
-
const relDir = OUTPUT_DIR_BY_TYPE[artifactType] || 'docs/specs/prd';
|
|
237
|
+
function outputPathForType(artifactType, slug, cwd, entry = null) {
|
|
238
|
+
const relDir = OUTPUT_DIR_BY_TYPE[artifactType] || entry?.outputDir || 'docs/specs/prd';
|
|
239
239
|
const fileName = artifactType === 'adr'
|
|
240
240
|
? `ADR-draft-${slug}.md`
|
|
241
241
|
: `${todayStamp()}-${slug}.md`;
|
|
@@ -278,12 +278,29 @@ export function extractArtifactMarkdown(text) {
|
|
|
278
278
|
return doc;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
281
|
+
// An adhoc artifact has no fixed template: the supplied instructions become the
|
|
282
|
+
// scaffold body, marked [unverified] so it clears the citation gate as an
|
|
283
|
+
// unresearched draft. Free-form structure, still through the release gate.
|
|
284
|
+
|
|
285
|
+
function buildAdhocScaffold({ title, instructions }) {
|
|
286
|
+
const instr = String(instructions || '').trim();
|
|
287
|
+
const bodyText = instr
|
|
288
|
+
|| 'Free-form artifact. Expand this from the supplied instructions and evidence.';
|
|
289
|
+
return ensureFrontmatter(
|
|
290
|
+
`# ${title}\n\n${bodyText}\n\n> Scope: adhoc artifact — structure follows the instructions above.\n\n[unverified]\n`,
|
|
291
|
+
'adhoc',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildDraftBody({ artifactType, title, draftFromHost, draftMarkdown, rootDir, allowScaffold = true, instructions }) {
|
|
282
296
|
const source = draftMarkdown || (draftFromHost ? normalizeDraftMarkdown(draftFromHost) : null);
|
|
283
297
|
if (source && source.length >= 200 && /^#\s+/m.test(source)) {
|
|
284
298
|
return ensureFrontmatter(source, artifactType);
|
|
285
299
|
}
|
|
286
300
|
if (!allowScaffold) return null;
|
|
301
|
+
if (artifactType === 'adhoc') {
|
|
302
|
+
return buildAdhocScaffold({ title, instructions });
|
|
303
|
+
}
|
|
287
304
|
const tmpl = getTemplate({ name: artifactType }, { ROOT_DIR: rootDir });
|
|
288
305
|
if (tmpl?.content) {
|
|
289
306
|
return fillTemplate(tmpl.content, { title, date: todayStamp() });
|
|
@@ -300,17 +317,20 @@ export async function runConstructArtifactLoop({
|
|
|
300
317
|
artifactType: artifactTypeOverride,
|
|
301
318
|
draftMarkdown,
|
|
302
319
|
allowScaffold = true,
|
|
320
|
+
instructions,
|
|
321
|
+
titleOverride,
|
|
303
322
|
} = {}) {
|
|
304
323
|
const root = rootDir || findConstructRoot(cwd);
|
|
305
324
|
const draftFromHost = draftMarkdown ? null : lastAssistantBody(turnBlocks);
|
|
306
325
|
const resolvedDraft = draftMarkdown || extractArtifactMarkdown(draftFromHost);
|
|
307
326
|
const artifactType = artifactTypeOverride || resolveLoopArtifactType(text, turnBlocks);
|
|
308
|
-
const
|
|
327
|
+
const entry = getArtifactEntry(artifactType, { rootDir: root, cwd });
|
|
328
|
+
const title = titleOverride?.trim() || extractTitle(
|
|
309
329
|
resolvedDraft || draftFromHost,
|
|
310
330
|
explicit ? (extractLoopSubject(text) || 'draft-artifact') : 'draft-artifact',
|
|
311
331
|
);
|
|
312
332
|
const slug = slugify(title);
|
|
313
|
-
const outPath = outputPathForType(artifactType, slug, cwd);
|
|
333
|
+
const outPath = outputPathForType(artifactType, slug, cwd, entry);
|
|
314
334
|
const workflowType = WORKFLOW_BY_TYPE[artifactType] || 'prd-draft';
|
|
315
335
|
const input = [
|
|
316
336
|
`Artifact loop: ${artifactType}`,
|
|
@@ -335,6 +355,7 @@ export async function runConstructArtifactLoop({
|
|
|
335
355
|
draftMarkdown: resolvedDraft,
|
|
336
356
|
rootDir: root,
|
|
337
357
|
allowScaffold,
|
|
358
|
+
instructions,
|
|
338
359
|
});
|
|
339
360
|
if (!body) {
|
|
340
361
|
const relPath = path.relative(cwd, outPath);
|