@geraldmaron/construct 1.2.0 → 1.2.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 +8 -7
- package/bin/construct +206 -62
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-manifest.mjs +150 -4
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/artifact-workflow.mjs +252 -0
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +13 -11
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/chat/present.mjs +1 -0
- package/lib/cli-commands.mjs +23 -6
- package/lib/cli-service-inventory.mjs +42 -0
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +139 -0
- package/lib/config/project-config.mjs +106 -7
- package/lib/config/schema.mjs +45 -6
- package/lib/config/source-targets.mjs +311 -0
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/source-checkout.mjs +16 -0
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +39 -17
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/auto-sources.mjs +44 -0
- package/lib/embed/cli.mjs +9 -6
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/demand-fetch.mjs +50 -19
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -41
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/embed/worker.mjs +2 -1
- package/lib/embedded-contract/index.mjs +11 -0
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/export-branding.mjs +32 -0
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +20 -16
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +47 -92
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/constants.mjs +29 -0
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +32 -112
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +20 -2
- package/lib/mcp/tools/embedded-contract.mjs +14 -0
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/playwright-demo.mjs +2 -0
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +53 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/rate-limit.mjs +5 -3
- package/lib/server/webhook.mjs +5 -3
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +9 -8
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +3 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/artifact-manifest.json +11 -2
- package/specialists/artifact-manifest.schema.json +31 -1
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/distribution/construct-reference.docx +0 -0
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
package/lib/init.mjs
CHANGED
|
@@ -77,12 +77,14 @@ function main() {
|
|
|
77
77
|
content: `${JSON.stringify({ version: 1, formality: 3, toneOverride: {}, audienceLexicon: { prefer: [], avoid: [] } }, null, 2)}\n`,
|
|
78
78
|
generator: "construct/init",
|
|
79
79
|
});
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// Single canonical drop zone (ADR-0045 §C): the visible project-root `inbox/`,
|
|
81
|
+
// covered by the host .gitignore (ADR-0027 §1). Writers stage under the
|
|
82
|
+
// gitignored `inbox/.staging/` and atomically rename in; the watcher consumes
|
|
83
|
+
// only complete top-level files. Machine/runtime intake state lives under the
|
|
84
|
+
// gitignored `.cx/intake/`, created lazily by the queue.
|
|
83
85
|
|
|
84
|
-
fs.mkdirSync(path.join(target, "
|
|
85
|
-
created.push("
|
|
86
|
+
fs.mkdirSync(path.join(target, "inbox", ".staging"), { recursive: true });
|
|
87
|
+
created.push("inbox/");
|
|
86
88
|
|
|
87
89
|
process.stdout.write(`\nConstruct init → ${target}\n\n`);
|
|
88
90
|
if (created.length) {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* lib/install/desktop-binary-download.mjs — downloads the Construct chat desktop binary.
|
|
3
3
|
*
|
|
4
4
|
* Fetches the platform-matching construct-chat binary from the latest GitHub
|
|
5
|
-
* release and writes it to
|
|
5
|
+
* release and writes it to the bin/ subdir of the XDG state dir. Called by
|
|
6
|
+
* `construct install --scope=user`.
|
|
6
7
|
* Network failures are non-fatal: returns { status: 'skipped', reason } so the
|
|
7
8
|
* rest of setup can proceed without the desktop binary.
|
|
8
9
|
*/
|
|
@@ -10,6 +11,8 @@
|
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
|
|
14
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
15
|
+
|
|
13
16
|
const GITHUB_REPO = 'geraldmaron/construct';
|
|
14
17
|
|
|
15
18
|
function assetName() {
|
|
@@ -46,7 +49,7 @@ async function downloadAsset(url, dest, env) {
|
|
|
46
49
|
|
|
47
50
|
// Returns { status: 'downloaded'|'skipped'|'failed' }; 'skipped' and 'failed' are both non-fatal.
|
|
48
51
|
export async function downloadDesktopBinary({ homeDir, env = process.env } = {}) {
|
|
49
|
-
const binDir = path.join(homeDir, '
|
|
52
|
+
const binDir = path.join(stateDir(homeDir), 'bin');
|
|
50
53
|
const dest = path.join(binDir, destBinaryName());
|
|
51
54
|
const name = assetName();
|
|
52
55
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/constants.mjs — shared intake watcher depth constants and guidance.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const INTAKE_DEFAULT_MAX_DEPTH = 4;
|
|
6
|
+
export const INTAKE_HARD_MAX_DEPTH = 16;
|
|
7
|
+
|
|
8
|
+
export const INTAKE_DEPTH_GUIDANCE = [
|
|
9
|
+
{ value: 0, label: 'Only this directory', detail: 'Scans files directly in the parent dir, ignores all subdirs.' },
|
|
10
|
+
{ value: 1, label: 'One level deep', detail: 'Parent dir plus its immediate subdirs (e.g. parent/intake/file.md).' },
|
|
11
|
+
{ value: 2, label: 'Two levels deep', detail: 'Parent and two subdirs. A reasonable default for organized intake roots.' },
|
|
12
|
+
{ value: 4, label: 'Four levels (default)', detail: 'Catches most nested layouts without scanning huge trees.' },
|
|
13
|
+
{ value: 8, label: 'Deep scan', detail: 'Useful for archives. Slower; skip if the parent contains build output.' },
|
|
14
|
+
{ value: INTAKE_HARD_MAX_DEPTH, label: 'Unlimited (capped)', detail: `Walks up to ${INTAKE_HARD_MAX_DEPTH} levels — effectively unlimited. May be slow.` },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function describeIntakeDepth(depth = INTAKE_DEFAULT_MAX_DEPTH) {
|
|
18
|
+
const n = Number(depth);
|
|
19
|
+
const value = !Number.isFinite(n) || n < 0
|
|
20
|
+
? INTAKE_DEFAULT_MAX_DEPTH
|
|
21
|
+
: Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
22
|
+
const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
|
|
23
|
+
if (exact) return exact;
|
|
24
|
+
return {
|
|
25
|
+
value,
|
|
26
|
+
label: `Custom depth (${value})`,
|
|
27
|
+
detail: `Walks up to ${value} levels of subdirectories below each parent.`,
|
|
28
|
+
};
|
|
29
|
+
}
|
package/lib/intake/daemon.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/intake/daemon.mjs — Continuous intake daemon with safeguards.
|
|
3
3
|
*
|
|
4
|
-
* Polls
|
|
5
|
-
* classifier, and writes packets to `.cx/intake/pending/`.
|
|
6
|
-
* TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
|
-
* the budget then dead-letters with the
|
|
4
|
+
* Polls the canonical project-root `inbox/` for new files, classifies them via
|
|
5
|
+
* the existing intake classifier, and writes packets to `.cx/intake/pending/`.
|
|
6
|
+
* Packets past their TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
|
+
* classification retries up to the budget then dead-letters with the reason.
|
|
8
|
+
*
|
|
9
|
+
* Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
|
|
10
|
+
* Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
|
|
11
|
+
* a writer is still staging is invisible until it is renamed into `inbox/`.
|
|
8
12
|
*
|
|
9
13
|
* Built on lib/daemons/contract.mjs — every safeguard (bounded lifetime,
|
|
10
14
|
* idle shutdown, heartbeat, killswitch, single-writer lock) applies.
|
|
@@ -12,30 +16,49 @@
|
|
|
12
16
|
|
|
13
17
|
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
|
14
18
|
import { join, basename } from 'node:path';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
19
|
|
|
17
20
|
import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
|
|
18
21
|
import { memoryCapMbFor } from '../resources/process-budget.mjs';
|
|
22
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
19
23
|
|
|
20
24
|
const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
|
|
21
25
|
|
|
22
|
-
function inboxDir(cwd) { return join(cwd, '
|
|
26
|
+
function inboxDir(cwd) { return join(cwd, 'inbox'); }
|
|
23
27
|
function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
|
|
24
28
|
function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
|
|
25
|
-
function heartbeatPath() { return join(
|
|
29
|
+
function heartbeatPath() { return join(stateDir(), 'intake-daemon.heartbeat'); }
|
|
26
30
|
|
|
27
31
|
function ensureDir(dir) {
|
|
28
32
|
try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// A writer staging into `inbox/.staging/` and renaming into `inbox/` is the
|
|
36
|
+
// atomic handoff. As a backstop for writers that drop in place, a file whose
|
|
37
|
+
// size is still moving between two stats is mid-write — skip it this tick.
|
|
38
|
+
|
|
39
|
+
function isSizeStable(full) {
|
|
40
|
+
try {
|
|
41
|
+
const a = statSync(full);
|
|
42
|
+
const b = statSync(full);
|
|
43
|
+
return a.size === b.size;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
function listInboxFiles(cwd) {
|
|
32
50
|
const dir = inboxDir(cwd);
|
|
33
51
|
if (!existsSync(dir)) return [];
|
|
34
52
|
let entries;
|
|
35
53
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
54
|
+
|
|
55
|
+
// Only complete top-level files: dotfiles (and the `.staging/` assembly dir)
|
|
56
|
+
// are skipped, so a file a writer is still staging is invisible until rename.
|
|
57
|
+
|
|
36
58
|
return entries
|
|
37
59
|
.filter((e) => e.isFile() && !e.name.startsWith('.'))
|
|
38
|
-
.map((e) => join(dir, e.name))
|
|
60
|
+
.map((e) => join(dir, e.name))
|
|
61
|
+
.filter((full) => isSizeStable(full));
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
/**
|
|
@@ -1,136 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/intake/intake-config.mjs —
|
|
2
|
+
* lib/intake/intake-config.mjs — intake watcher config facade.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `<rootDir>/.cx/intake-config.json` and merges with env-driven defaults so
|
|
7
|
-
* CLI, dashboard, and process env all converge on the same answer.
|
|
8
|
-
*
|
|
9
|
-
* Schema:
|
|
10
|
-
* {
|
|
11
|
-
* parentDirs: string[] // absolute or rootDir-relative
|
|
12
|
-
* maxDepth: number // 0 = only the parent dir, no subdirs
|
|
13
|
-
* includeProjectInbox: boolean // always include <rootDir>/.cx/inbox
|
|
14
|
-
* includeDocsIntake: boolean // include <rootDir>/docs/intake when present
|
|
15
|
-
* }
|
|
16
|
-
*
|
|
17
|
-
* maxDepth guidance (surfaced verbatim in the dashboard):
|
|
18
|
-
* 0 — only files directly inside the parent dir
|
|
19
|
-
* 1 — parent dir + its immediate subdirs
|
|
20
|
-
* 2 — two levels of subdirs (a common default for project intake roots)
|
|
21
|
-
* 3+ — deep scans; use with care on large trees
|
|
4
|
+
* Reads and writes intake policy via construct.config.json intakePolicy, the
|
|
5
|
+
* only config source (single-zone model, ADR-0045 §C).
|
|
22
6
|
*/
|
|
23
7
|
|
|
24
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
-
import { isAbsolute, join, resolve } from 'node:path';
|
|
26
8
|
import { shouldCreateCx } from '../project-detection.mjs';
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
9
|
+
import {
|
|
10
|
+
resolvedIntakeConfig,
|
|
11
|
+
saveIntakePolicy,
|
|
12
|
+
DEFAULT_INTAKE_POLICY,
|
|
13
|
+
} from '../config/intake-policy.mjs';
|
|
14
|
+
import {
|
|
15
|
+
INTAKE_DEFAULT_MAX_DEPTH,
|
|
16
|
+
INTAKE_HARD_MAX_DEPTH,
|
|
17
|
+
INTAKE_DEPTH_GUIDANCE,
|
|
18
|
+
describeIntakeDepth,
|
|
19
|
+
} from './constants.mjs';
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
INTAKE_DEFAULT_MAX_DEPTH,
|
|
23
|
+
INTAKE_HARD_MAX_DEPTH,
|
|
24
|
+
INTAKE_DEPTH_GUIDANCE,
|
|
25
|
+
describeIntakeDepth,
|
|
26
|
+
DEFAULT_INTAKE_POLICY,
|
|
27
|
+
};
|
|
41
28
|
|
|
42
29
|
export const DEFAULT_INTAKE_CONFIG = Object.freeze({
|
|
43
30
|
parentDirs: [],
|
|
44
31
|
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
45
|
-
includeProjectInbox: true,
|
|
46
|
-
includeDocsIntake: true,
|
|
47
|
-
includeArchetypeInbox: false,
|
|
48
32
|
});
|
|
49
33
|
|
|
50
|
-
export function intakeConfigPath(rootDir) {
|
|
51
|
-
return join(rootDir, INTAKE_CONFIG_FILE);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function clampDepth(value) {
|
|
55
|
-
const n = Number(value);
|
|
56
|
-
if (!Number.isFinite(n) || n < 0) return INTAKE_DEFAULT_MAX_DEPTH;
|
|
57
|
-
return Math.min(Math.floor(n), INTAKE_HARD_MAX_DEPTH);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function normalizeDir(dir, rootDir) {
|
|
61
|
-
if (typeof dir !== 'string') return null;
|
|
62
|
-
const trimmed = dir.trim();
|
|
63
|
-
if (!trimmed) return null;
|
|
64
|
-
return isAbsolute(trimmed) ? trimmed : resolve(rootDir, trimmed);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function parseEnvDirs(env) {
|
|
68
|
-
const raw = String(env?.CX_INBOX_DIRS ?? '').trim();
|
|
69
|
-
if (!raw) return [];
|
|
70
|
-
return raw.split(':').map((p) => p.trim()).filter(Boolean);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
34
|
export function loadIntakeConfig(rootDir, env = process.env) {
|
|
74
|
-
|
|
75
|
-
let stored = {};
|
|
76
|
-
if (existsSync(file)) {
|
|
77
|
-
try { stored = JSON.parse(readFileSync(file, 'utf8')); } catch { stored = {}; }
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const parentDirs = []
|
|
81
|
-
.concat(Array.isArray(stored.parentDirs) ? stored.parentDirs : [])
|
|
82
|
-
.concat(parseEnvDirs(env))
|
|
83
|
-
.map((dir) => normalizeDir(dir, rootDir))
|
|
84
|
-
.filter(Boolean);
|
|
85
|
-
|
|
86
|
-
const seen = new Set();
|
|
87
|
-
const uniqueDirs = [];
|
|
88
|
-
for (const dir of parentDirs) {
|
|
89
|
-
if (seen.has(dir)) continue;
|
|
90
|
-
seen.add(dir);
|
|
91
|
-
uniqueDirs.push(dir);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const envDepth = env?.CX_INTAKE_MAX_DEPTH;
|
|
95
|
-
const maxDepth = clampDepth(envDepth ?? stored.maxDepth ?? DEFAULT_INTAKE_CONFIG.maxDepth);
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
parentDirs: uniqueDirs,
|
|
99
|
-
maxDepth,
|
|
100
|
-
includeProjectInbox: stored.includeProjectInbox !== false,
|
|
101
|
-
includeDocsIntake: stored.includeDocsIntake !== false,
|
|
102
|
-
includeArchetypeInbox: stored.includeArchetypeInbox === true,
|
|
103
|
-
};
|
|
35
|
+
return resolvedIntakeConfig(rootDir, env);
|
|
104
36
|
}
|
|
105
37
|
|
|
106
38
|
export function saveIntakeConfig(rootDir, patch = {}) {
|
|
107
39
|
if (!shouldCreateCx(rootDir)) {
|
|
108
40
|
throw new Error('Refusing to write intake config: directory is not an initialized construct project. Run `construct init` first.');
|
|
109
41
|
}
|
|
110
|
-
const current = loadIntakeConfig(rootDir, {});
|
|
111
|
-
const next = {
|
|
112
|
-
parentDirs: Array.isArray(patch.parentDirs)
|
|
113
|
-
? patch.parentDirs.map((dir) => normalizeDir(dir, rootDir)).filter(Boolean)
|
|
114
|
-
: current.parentDirs,
|
|
115
|
-
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
116
|
-
includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
|
|
117
|
-
includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
|
|
118
|
-
includeArchetypeInbox: patch.includeArchetypeInbox !== undefined ? Boolean(patch.includeArchetypeInbox) : current.includeArchetypeInbox,
|
|
119
|
-
};
|
|
120
42
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
43
|
+
const policyPatch = {};
|
|
44
|
+
if (patch.maxDepth !== undefined) policyPatch.maxDepth = patch.maxDepth;
|
|
45
|
+
if (Array.isArray(patch.parentDirs)) policyPatch.additionalDirs = patch.parentDirs;
|
|
46
|
+
|
|
47
|
+
const { policy } = saveIntakePolicy(rootDir, policyPatch);
|
|
48
|
+
return policyToConfigShape(policy);
|
|
125
49
|
}
|
|
126
50
|
|
|
127
|
-
|
|
128
|
-
const value = clampDepth(depth);
|
|
129
|
-
const exact = INTAKE_DEPTH_GUIDANCE.find((g) => g.value === value);
|
|
130
|
-
if (exact) return exact;
|
|
51
|
+
function policyToConfigShape(policy) {
|
|
131
52
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
detail: `Walks up to ${value} levels of subdirectories below each parent.`,
|
|
53
|
+
parentDirs: policy.additionalDirs,
|
|
54
|
+
maxDepth: policy.maxDepth,
|
|
135
55
|
};
|
|
136
56
|
}
|
|
@@ -18,6 +18,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { configDir } from '../config/xdg.mjs';
|
|
21
22
|
|
|
22
23
|
// ── Multi-method auth helpers ────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -268,8 +269,8 @@ export async function createJiraTicket(packet, { host, email, token, project, is
|
|
|
268
269
|
const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
|
|
269
270
|
const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
|
|
270
271
|
|
|
271
|
-
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
|
|
272
|
-
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
272
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.config/construct/config.env or shell rc.' };
|
|
273
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
|
|
273
274
|
if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
|
|
274
275
|
|
|
275
276
|
const t = packet?.triage || {};
|
|
@@ -385,8 +386,8 @@ export async function publishArtifactToConfluence(artifact, { host, email, token
|
|
|
385
386
|
const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
|
|
386
387
|
const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
|
|
387
388
|
|
|
388
|
-
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
|
|
389
|
-
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
389
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.config/construct/config.env or shell rc.' };
|
|
390
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
|
|
390
391
|
if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
|
|
391
392
|
|
|
392
393
|
const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
|
|
@@ -608,7 +609,7 @@ function resolveCredentialSync(varName, homeDir) {
|
|
|
608
609
|
}
|
|
609
610
|
} catch { /* skip */ }
|
|
610
611
|
try {
|
|
611
|
-
const cfgPath = join(homeDir, '
|
|
612
|
+
const cfgPath = join(configDir(homeDir), 'config.env');
|
|
612
613
|
if (existsSync(cfgPath)) {
|
|
613
614
|
const content = readFileSync(cfgPath, 'utf8');
|
|
614
615
|
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
|
@@ -703,9 +704,9 @@ function resolveCredential(varName, homeDir) {
|
|
|
703
704
|
}
|
|
704
705
|
} catch { /* skip */ }
|
|
705
706
|
|
|
706
|
-
//
|
|
707
|
+
// config.env in the XDG config dir
|
|
707
708
|
try {
|
|
708
|
-
const cfgPath = join(homeDir, '
|
|
709
|
+
const cfgPath = join(configDir(homeDir), 'config.env');
|
|
709
710
|
if (existsSync(cfgPath)) {
|
|
710
711
|
const content = readFileSync(cfgPath, 'utf8');
|
|
711
712
|
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
package/lib/knowledge/rag.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Sources indexed:
|
|
9
9
|
* - Observations (.cx/observations/)
|
|
10
|
-
* - Artifacts (docs/adr/, docs/prd/, docs/rfc/)
|
|
10
|
+
* - Artifacts (docs/decisions/adr/, docs/specs/prd/, docs/decisions/rfc/)
|
|
11
11
|
* - Snapshots (.cx/snapshot.md + any configured output paths)
|
|
12
12
|
* - Approval queue (.cx/approval-queue.jsonl)
|
|
13
13
|
*
|
|
@@ -102,7 +102,7 @@ function loadMarkdownChunks(dir, source) {
|
|
|
102
102
|
*/
|
|
103
103
|
function loadArtifactChunks(rootDir) {
|
|
104
104
|
const chunks = [];
|
|
105
|
-
for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/concepts/architecture.md']) {
|
|
105
|
+
for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/guides/concepts/architecture.md']) {
|
|
106
106
|
const full = path.resolve(rootDir, subdir);
|
|
107
107
|
if (subdir.endsWith('.md')) {
|
|
108
108
|
if (!fs.existsSync(full)) continue;
|
package/lib/knowledge/search.mjs
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* `knowledge_search` MCP tool — no daemon, no network, no external deps.
|
|
7
7
|
*
|
|
8
8
|
* Sources searched (in priority order):
|
|
9
|
-
* 1. docs/concepts/*.md — architecture, agents, gates, durable state
|
|
10
|
-
* 2. docs/start/*.mdx — install + first task
|
|
9
|
+
* 1. docs/guides/concepts/*.md — architecture, agents, gates, durable state
|
|
10
|
+
* 2. docs/guides/start/*.mdx — install + first task
|
|
11
11
|
* 3. docs/README.md — docs index + contract
|
|
12
12
|
* 4. .cx/knowledge/ — operator-written internal docs
|
|
13
|
-
* 5. Any *.md in docs/cookbook/ — task-oriented recipes
|
|
13
|
+
* 5. Any *.md in docs/guides/cookbook/ — task-oriented recipes
|
|
14
14
|
* 6. <projectRoot>/.cx/knowledge/** — the cwd project's knowledge tree,
|
|
15
15
|
* including `external/research/` written by `construct knowledge add`,
|
|
16
16
|
* so foreign-project queries surface project content alongside bundled docs.
|
|
@@ -47,17 +47,17 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
|
|
|
47
47
|
const sources = [];
|
|
48
48
|
|
|
49
49
|
const priority = [
|
|
50
|
-
'docs/concepts/architecture.md',
|
|
51
|
-
'docs/concepts/agents-and-personas.mdx',
|
|
52
|
-
'docs/concepts/gates-and-enforcement.md',
|
|
53
|
-
'docs/concepts/beads-and-state.md',
|
|
54
|
-
'docs/concepts/prompt-surfaces.md',
|
|
55
|
-
'docs/concepts/knowledge-layout.md',
|
|
56
|
-
'docs/concepts/deployment-model.md',
|
|
57
|
-
'docs/concepts/embedding-boundary.md',
|
|
50
|
+
'docs/guides/concepts/architecture.md',
|
|
51
|
+
'docs/guides/concepts/agents-and-personas.mdx',
|
|
52
|
+
'docs/guides/concepts/gates-and-enforcement.md',
|
|
53
|
+
'docs/guides/concepts/beads-and-state.md',
|
|
54
|
+
'docs/guides/concepts/prompt-surfaces.md',
|
|
55
|
+
'docs/guides/concepts/knowledge-layout.md',
|
|
56
|
+
'docs/guides/concepts/deployment-model.md',
|
|
57
|
+
'docs/guides/concepts/embedding-boundary.md',
|
|
58
58
|
'docs/README.md',
|
|
59
|
-
'docs/start/install.mdx',
|
|
60
|
-
'docs/start/first-task.mdx',
|
|
59
|
+
'docs/guides/start/install.mdx',
|
|
60
|
+
'docs/guides/start/first-task.mdx',
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
for (const rel of priority) {
|
|
@@ -66,12 +66,12 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Cookbook recipes (task-oriented how-tos)
|
|
69
|
-
const cookbookDir = join(repoRoot, 'docs', 'cookbook');
|
|
69
|
+
const cookbookDir = join(repoRoot, 'docs', 'guides', 'cookbook');
|
|
70
70
|
if (existsSync(cookbookDir)) {
|
|
71
71
|
for (const file of readdirSync(cookbookDir)) {
|
|
72
72
|
if (file.endsWith('.md') || file.endsWith('.mdx')) {
|
|
73
73
|
const full = join(cookbookDir, file);
|
|
74
|
-
sources.push({ path: full, rel: `docs/cookbook/${file}`, priority: 2 });
|
|
74
|
+
sources.push({ path: full, rel: `docs/guides/cookbook/${file}`, priority: 2 });
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Triggered from two entry points:
|
|
11
11
|
* - `construct cleanup` — manual sweep, optionally --dry-run
|
|
12
12
|
* - bin/construct startup check — automatic when the installed version
|
|
13
|
-
* differs from
|
|
14
|
-
* so an upgrade reclaims
|
|
13
|
+
* differs from the XDG state-dir
|
|
14
|
+
* .cleanup-stamp, so an upgrade reclaims
|
|
15
|
+
* disk without
|
|
15
16
|
* requiring a manual command.
|
|
16
17
|
*
|
|
17
18
|
* Scope is deliberately narrow: only artifacts whose retention is owned by
|
|
@@ -24,6 +25,9 @@ import fs from 'node:fs';
|
|
|
24
25
|
import os from 'node:os';
|
|
25
26
|
import path from 'node:path';
|
|
26
27
|
|
|
28
|
+
import { stateDir, cacheDir } from '../config/xdg.mjs';
|
|
29
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
30
|
+
|
|
27
31
|
const HARD_BUDGET_MS = 5000;
|
|
28
32
|
|
|
29
33
|
// Size caps in bytes. Each entry: [filename relative to ~/.cx/, maxBytes].
|
|
@@ -92,7 +96,7 @@ function rotationConfig(env = process.env) {
|
|
|
92
96
|
* were oversized before the cap existed.
|
|
93
97
|
*/
|
|
94
98
|
export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
|
|
95
|
-
const dir = path.join(homeDir, '
|
|
99
|
+
const dir = path.join(doctorRoot(homeDir), 'runtime');
|
|
96
100
|
const baseLog = path.join(dir, 'embed-daemon.log');
|
|
97
101
|
const { maxBytes, keep } = rotationConfig(env);
|
|
98
102
|
|
|
@@ -149,7 +153,7 @@ export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dry
|
|
|
149
153
|
* recent records survive.
|
|
150
154
|
*/
|
|
151
155
|
export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}) {
|
|
152
|
-
const dir =
|
|
156
|
+
const dir = doctorRoot(homeDir);
|
|
153
157
|
const summary = { truncated: [], freedBytes: 0 };
|
|
154
158
|
if (!fs.existsSync(dir)) return summary;
|
|
155
159
|
|
|
@@ -168,11 +172,11 @@ export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
|
-
* Age out
|
|
175
|
+
* Age out XDG cache-dir entries older than `maxAgeDays`. Cache hits are
|
|
172
176
|
* idempotent and re-fetched on demand, so pruning is safe.
|
|
173
177
|
*/
|
|
174
178
|
export function cleanupCacheDir({ homeDir = os.homedir(), maxAgeDays = 30, dryRun = false } = {}) {
|
|
175
|
-
const dir =
|
|
179
|
+
const dir = cacheDir(homeDir);
|
|
176
180
|
const summary = { removed: [], freedBytes: 0 };
|
|
177
181
|
if (!fs.existsSync(dir)) return summary;
|
|
178
182
|
|
|
@@ -249,18 +253,18 @@ export function formatBytes(bytes) {
|
|
|
249
253
|
// ---------------------------------------------------------------------------
|
|
250
254
|
// Version stamp — drives automatic cleanup on upgrade.
|
|
251
255
|
//
|
|
252
|
-
//
|
|
253
|
-
// On every CLI invocation, bin/construct compares the current package
|
|
256
|
+
// The .cleanup-stamp in the XDG state dir records the last version that ran
|
|
257
|
+
// cleanup. On every CLI invocation, bin/construct compares the current package
|
|
254
258
|
// version to the stamp. A mismatch triggers a single cleanup pass and
|
|
255
259
|
// writes the new version into the stamp. The first ever invocation also
|
|
256
260
|
// triggers cleanup (no stamp present) — this is what unblocks existing
|
|
257
261
|
// installs that already have 34 GB logs sitting around.
|
|
258
262
|
// ---------------------------------------------------------------------------
|
|
259
263
|
|
|
260
|
-
const
|
|
264
|
+
const STAMP_FILENAME = '.cleanup-stamp';
|
|
261
265
|
|
|
262
266
|
export function stampPath(homeDir = os.homedir()) {
|
|
263
|
-
return path.join(homeDir,
|
|
267
|
+
return path.join(stateDir(homeDir), STAMP_FILENAME);
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
export function readVersionStamp({ homeDir = os.homedir() } = {}) {
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
profileCreate, profileArchive, sandboxList, learningStatus,
|
|
59
59
|
knowledgeGraphAsk,
|
|
60
60
|
} from './tools/profile.mjs';
|
|
61
|
-
import { modelResolve, triageRecommend, workflowInvoke, capabilityDescribe, executionResolve } from './tools/embedded-contract.mjs';
|
|
61
|
+
import { modelResolve, triageRecommend, workflowInvoke, capabilityDescribe, executionResolve, artifactWorkflow } from './tools/embedded-contract.mjs';
|
|
62
62
|
|
|
63
63
|
const DEFAULT_ROOT_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
64
64
|
const ROOT_DIR = resolve(process.env.CX_TOOLKIT_DIR || DEFAULT_ROOT_DIR);
|
|
@@ -859,7 +859,7 @@ const ALL_TOOL_DEFS = [
|
|
|
859
859
|
},
|
|
860
860
|
{
|
|
861
861
|
name: 'profile_create',
|
|
862
|
-
description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/concepts/profile-lifecycle.md` after creation.',
|
|
862
|
+
description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/guides/concepts/profile-lifecycle.md` after creation.',
|
|
863
863
|
inputSchema: {
|
|
864
864
|
type: 'object',
|
|
865
865
|
required: ['confirm', 'id'],
|
|
@@ -1134,6 +1134,23 @@ const ALL_TOOL_DEFS = [
|
|
|
1134
1134
|
},
|
|
1135
1135
|
},
|
|
1136
1136
|
},
|
|
1137
|
+
{
|
|
1138
|
+
name: 'artifact_workflow',
|
|
1139
|
+
description: 'Plan a manifest-backed document artifact workflow and return a truthful provenance report. It separates planned steps from locally executed validation/export and never claims a host-planned specialist review or rewrite was completed. Durable local export requires approval_mode=allow-durable-write.',
|
|
1140
|
+
inputSchema: {
|
|
1141
|
+
type: 'object',
|
|
1142
|
+
properties: {
|
|
1143
|
+
input: { type: 'string', description: 'Natural-language artifact request.' },
|
|
1144
|
+
artifact_type: { type: 'string', description: 'Registered manifest document class.' },
|
|
1145
|
+
file_path: { type: 'string', description: 'Optional existing source document for local validation/export.' },
|
|
1146
|
+
format: { type: 'string', description: 'Distribution format such as pdf, docx, html, deck, or pptx.' },
|
|
1147
|
+
output_path: { type: 'string', description: 'Optional output destination when locally exporting.' },
|
|
1148
|
+
branding: { type: 'string', enum: ['construct', 'plain'], description: 'Construct is default; plain is an explicit opt-out.' },
|
|
1149
|
+
overrides: { type: 'object', description: 'Per-invocation manifest workflow overrides.' },
|
|
1150
|
+
approval_mode: { type: 'string', enum: ['proposal-only', 'requires-human-approval', 'allow-durable-write'], description: 'Only allow-durable-write performs local validation/export.' },
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
},
|
|
1137
1154
|
{
|
|
1138
1155
|
name: 'model_resolve',
|
|
1139
1156
|
description: 'Resolve which model an embedded Construct workflow should use given the host/IDE provider context. Precedence: host model → same-provider-family fallback → Construct tier default → structured config error. Never reads or returns credential values (requiresCredential is a boolean) and never claims unverified provider health. Read-only; performs no writes.',
|
|
@@ -1421,6 +1438,7 @@ export async function dispatchToolByName(name, args = {}) {
|
|
|
1421
1438
|
else if (name === 'workflow_invoke') result = await workflowInvoke(args);
|
|
1422
1439
|
else if (name === 'capability_describe') result = capabilityDescribe(args);
|
|
1423
1440
|
else if (name === 'construct_execution_resolve') result = executionResolve(args);
|
|
1441
|
+
else if (name === 'artifact_workflow') result = artifactWorkflow(args);
|
|
1424
1442
|
else if (name === 'orchestration_run') { const m = await import('./tools/orchestration-run.mjs'); result = await m.orchestrationRun(args); }
|
|
1425
1443
|
else if (name === 'orchestration_status') { const m = await import('./tools/orchestration-run.mjs'); result = await m.orchestrationStatus(args); }
|
|
1426
1444
|
else if (name === 'construct_call') {
|
|
@@ -14,6 +14,7 @@ import { invokeWorkflow as invokeWorkflowCore } from '../../embedded-contract/wo
|
|
|
14
14
|
import { buildCapabilityContract } from '../../embedded-contract/capability.mjs';
|
|
15
15
|
import { resolveExecution as resolveExecutionCore } from '../../embedded-contract/execution.mjs';
|
|
16
16
|
import { resolveInput } from '../../embedded-contract/ingest.mjs';
|
|
17
|
+
import { runArtifactWorkflow as runArtifactWorkflowCore } from '../../artifact-workflow.mjs';
|
|
17
18
|
|
|
18
19
|
async function ingestArgs(args) {
|
|
19
20
|
if (args.input || !args.file_path) return { input: args.input, ingestion: undefined };
|
|
@@ -91,3 +92,16 @@ export function executionResolve(args = {}) {
|
|
|
91
92
|
};
|
|
92
93
|
return wrapContractResult(resolveExecutionCore(request), { surface: 'mcp' });
|
|
93
94
|
}
|
|
95
|
+
|
|
96
|
+
export function artifactWorkflow(args = {}) {
|
|
97
|
+
return wrapContractResult(runArtifactWorkflowCore({
|
|
98
|
+
input: args.input || args.request,
|
|
99
|
+
artifactType: args.artifact_type,
|
|
100
|
+
filePath: args.file_path,
|
|
101
|
+
format: args.format,
|
|
102
|
+
outputPath: args.output_path,
|
|
103
|
+
branding: args.branding,
|
|
104
|
+
overrides: args.overrides,
|
|
105
|
+
approvalMode: args.approval_mode,
|
|
106
|
+
}), { surface: 'mcp' });
|
|
107
|
+
}
|
|
@@ -13,6 +13,7 @@ import { inspectContextState } from '../../context-state.mjs';
|
|
|
13
13
|
import { loadWorkflow, summarizeWorkflow, inspectWorkflowHealth } from '../../workflow-state.mjs';
|
|
14
14
|
import { readCurrentModels, resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from '../../model-router.mjs';
|
|
15
15
|
import { buildPublicHealthSurface } from '../../status.mjs';
|
|
16
|
+
import { doctorRoot } from '../../config/xdg.mjs';
|
|
16
17
|
|
|
17
18
|
export function exec(cmd, cwd) {
|
|
18
19
|
return execSync(cmd, { stdio: 'pipe', timeout: 10000, cwd }).toString().trim();
|
|
@@ -60,7 +61,7 @@ function isBinary(buffer) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
export function agentHealth(args) {
|
|
63
|
-
const reviewDir = join(
|
|
64
|
+
const reviewDir = join(doctorRoot(), 'performance-reviews');
|
|
64
65
|
|
|
65
66
|
if (!existsSync(reviewDir)) {
|
|
66
67
|
return { error: "No performance reviews found. Run 'construct review' to generate one." };
|