@geraldmaron/construct 1.0.7 → 1.0.9
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 +20 -17
- package/bin/construct +1199 -69
- package/commands/work/optimize-prompts.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +2 -2
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +5 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +65 -17
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -0
- package/lib/doctor/watchers/consistency.mjs +62 -12
- package/lib/document-extract.mjs +221 -28
- package/lib/document-ingest.mjs +2 -0
- package/lib/embed/inbox.mjs +48 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/env-config.mjs +9 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +12 -0
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +18 -0
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +222 -55
- package/lib/intake/filesystem-queue.mjs +25 -5
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -0
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/mcp/server.mjs +9 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/migrations/index.mjs +1 -1
- package/lib/model-registry.mjs +1 -1
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +7 -1
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +13 -3
- package/personas/construct.md +2 -2
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/no-fabrication.md +1 -1
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +130 -58
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/{agents → specialists}/contracts.schema.json +1 -1
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +1 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-architect.md +1 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +1 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +1 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-debugger.md +1 -1
- package/{agents → specialists}/prompts/cx-designer.md +1 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +1 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +2 -2
- package/{agents → specialists}/prompts/cx-engineer.md +2 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +1 -1
- package/{agents → specialists}/prompts/cx-explorer.md +1 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +1 -1
- package/{agents → specialists}/prompts/cx-operations.md +1 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +2 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-qa.md +1 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +1 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-researcher.md +1 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +2 -2
- package/{agents → specialists}/prompts/cx-security.md +1 -1
- package/{agents → specialists}/prompts/cx-sre.md +2 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +1 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +3 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +1 -1
- package/{agents → specialists}/registry.json +651 -423
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- /package/{agents → specialists}/contracts.json +0 -0
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/contracts/validate.mjs — Validate
|
|
2
|
+
* lib/contracts/validate.mjs — Validate specialists/contracts.json at sync time + at handoff.
|
|
3
3
|
*
|
|
4
4
|
* Three validation tiers:
|
|
5
5
|
* 1. Schema shape: contracts.json conforms to contracts.schema.json (minimal
|
|
6
6
|
* validator — top-level required fields, contract entry required fields).
|
|
7
7
|
* 2. Cross-file refs: every output.schema points to a real file in lib/schemas/,
|
|
8
8
|
* every producer/consumer name resolves to an agent or persona in
|
|
9
|
-
*
|
|
9
|
+
* specialists/registry.json, every well-known event/intake string is reachable.
|
|
10
10
|
* 3. Runtime handoff: a single artifact validated against the schema referenced
|
|
11
11
|
* by a producer→consumer contract, with mustContain post-conditions.
|
|
12
12
|
*
|
|
13
13
|
* Surfaces:
|
|
14
|
-
* - scripts/sync-
|
|
14
|
+
* - scripts/sync-specialists.mjs invokes validateContractsFile at sync time.
|
|
15
15
|
* - bin/construct lint:contracts invokes the same path in CI.
|
|
16
16
|
* - workflowContractValidate (runtime) invokes validateHandoff per handoff.
|
|
17
17
|
*/
|
|
@@ -21,9 +21,9 @@ import { join, dirname, resolve } from 'node:path';
|
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
|
|
23
23
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
24
|
-
const CONTRACTS_PATH = join(REPO_ROOT, '
|
|
25
|
-
const CONTRACTS_SCHEMA_PATH = join(REPO_ROOT, '
|
|
26
|
-
const REGISTRY_PATH = join(REPO_ROOT, '
|
|
24
|
+
const CONTRACTS_PATH = join(REPO_ROOT, 'specialists', 'contracts.json');
|
|
25
|
+
const CONTRACTS_SCHEMA_PATH = join(REPO_ROOT, 'specialists', 'contracts.schema.json');
|
|
26
|
+
const REGISTRY_PATH = join(REPO_ROOT, 'specialists', 'registry.json');
|
|
27
27
|
|
|
28
28
|
const WELL_KNOWN_PRODUCERS = new Set(['user', 'oncall', 'incident-system', '*']);
|
|
29
29
|
const WELL_KNOWN_CONSUMERS = new Set(['user', 'construct']);
|
|
@@ -33,7 +33,7 @@ function readJson(path) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Validate
|
|
36
|
+
* Validate specialists/contracts.json shape and cross-file references.
|
|
37
37
|
* Returns { ok, errors[] } — errors is empty on success.
|
|
38
38
|
*/
|
|
39
39
|
export function validateContractsFile({ contractsPath, registryPath, repoRoot = REPO_ROOT } = {}) {
|
|
@@ -42,9 +42,9 @@ export function validateContractsFile({ contractsPath, registryPath, repoRoot =
|
|
|
42
42
|
// When repoRoot is supplied but explicit paths are not, derive both from it
|
|
43
43
|
// so test fixtures and the consistency watcher can validate slices in a
|
|
44
44
|
// tmpdir without pointing at the live repo.
|
|
45
|
-
const cPath = contractsPath || join(repoRoot, '
|
|
46
|
-
const rPath = registryPath || join(repoRoot, '
|
|
47
|
-
const schemaPath = join(repoRoot, '
|
|
45
|
+
const cPath = contractsPath || join(repoRoot, 'specialists', 'contracts.json');
|
|
46
|
+
const rPath = registryPath || join(repoRoot, 'specialists', 'registry.json');
|
|
47
|
+
const schemaPath = join(repoRoot, 'specialists', 'contracts.schema.json');
|
|
48
48
|
|
|
49
49
|
if (!existsSync(cPath)) {
|
|
50
50
|
return { ok: false, errors: [`contracts file not found: ${cPath}`] };
|
|
@@ -116,13 +116,11 @@ function collectAgentNames(registryPath) {
|
|
|
116
116
|
if (!existsSync(registryPath)) return names;
|
|
117
117
|
try {
|
|
118
118
|
const registry = readJson(registryPath);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (p?.name) names.add(p.name);
|
|
125
|
-
if (p?.displayName) names.add(p.displayName);
|
|
119
|
+
if (registry.orchestrator?.name) names.add(registry.orchestrator.name);
|
|
120
|
+
if (registry.orchestrator?.displayName) names.add(registry.orchestrator.displayName);
|
|
121
|
+
for (const s of registry.specialists || []) {
|
|
122
|
+
if (s?.name) names.add(s.name);
|
|
123
|
+
if (s?.displayName) names.add(s.displayName);
|
|
126
124
|
}
|
|
127
125
|
} catch { /* fall through with whatever names we collected */ }
|
|
128
126
|
return names;
|
|
@@ -321,3 +319,53 @@ export function validatePostconditions({ contract, artifactPath }) {
|
|
|
321
319
|
}
|
|
322
320
|
return errors;
|
|
323
321
|
}
|
|
322
|
+
|
|
323
|
+
// Minimum evidence required: either a PR reference (#N or PR #N) or a file
|
|
324
|
+
// reference of the form `path:<line>` or `file:<line>` where line is a number.
|
|
325
|
+
const CLOSE_REASON_PR_PATTERN = /#\d+/;
|
|
326
|
+
const CLOSE_REASON_FILE_PATTERN = /[\w./\\-]+(?:\.(?:mjs|ts|js|tsx|jsx|py|go|rs|java|md|json|yml|yaml)):[0-9]+/i;
|
|
327
|
+
const CLOSE_REASON_MIN_LENGTH = 20;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Validate a beads close reason for evidence quality.
|
|
331
|
+
*
|
|
332
|
+
* Returns { ok: boolean, message: string|null }. On failure, `message` describes
|
|
333
|
+
* what is missing. The caller decides whether to warn or block based on
|
|
334
|
+
* CONSTRUCT_BEADS_HYGIENE (warn | block). Default behavior is warn.
|
|
335
|
+
*
|
|
336
|
+
* A passing reason must:
|
|
337
|
+
* - Be at least 20 characters.
|
|
338
|
+
* - Contain a PR reference (#N) OR a file:line reference (path.mjs:42).
|
|
339
|
+
*/
|
|
340
|
+
export function validateBeadsCloseReason(reason) {
|
|
341
|
+
if (!reason || typeof reason !== 'string') {
|
|
342
|
+
return { ok: false, message: 'Close reason is empty. Add evidence: PR reference (#N) or file:line reference.' };
|
|
343
|
+
}
|
|
344
|
+
const trimmed = reason.trim();
|
|
345
|
+
if (trimmed.length < CLOSE_REASON_MIN_LENGTH) {
|
|
346
|
+
return { ok: false, message: `Close reason too short (${trimmed.length} chars). Minimum: ${CLOSE_REASON_MIN_LENGTH}. Add evidence.` };
|
|
347
|
+
}
|
|
348
|
+
const hasPr = CLOSE_REASON_PR_PATTERN.test(trimmed);
|
|
349
|
+
const hasFile = CLOSE_REASON_FILE_PATTERN.test(trimmed);
|
|
350
|
+
if (!hasPr && !hasFile) {
|
|
351
|
+
return {
|
|
352
|
+
ok: false,
|
|
353
|
+
message: `Close reason lacks evidence. Include a PR reference (#N) or a file:line reference (e.g. lib/foo.mjs:42). Got: "${trimmed.slice(0, 80)}"`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return { ok: true, message: null };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Apply the beads close-reason policy: warn or block based on
|
|
361
|
+
* CONSTRUCT_BEADS_HYGIENE env var. Returns exit code (0 = ok, 1 = block).
|
|
362
|
+
*/
|
|
363
|
+
export function applyBeadsHygienePolicy(reason, { stderr = process.stderr } = {}) {
|
|
364
|
+
const { ok, message } = validateBeadsCloseReason(reason);
|
|
365
|
+
if (ok) return 0;
|
|
366
|
+
|
|
367
|
+
const mode = (process.env.CONSTRUCT_BEADS_HYGIENE || 'warn').toLowerCase();
|
|
368
|
+
const prefix = mode === 'block' ? '[beads-hygiene] BLOCK' : '[beads-hygiene] WARN';
|
|
369
|
+
stderr.write(`${prefix}: ${message}\n`);
|
|
370
|
+
return mode === 'block' ? 1 : 0;
|
|
371
|
+
}
|
package/lib/distill.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { join, extname, relative, dirname, basename } from 'path';
|
|
|
11
11
|
import { execSync, spawnSync } from 'child_process';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import { getActiveOverlays } from './headhunt.mjs';
|
|
14
|
+
import { withGenAiSpan, GenAiAttrs } from './telemetry/otel-tracer.mjs';
|
|
14
15
|
import {
|
|
15
16
|
EXTRACTABLE_DOCUMENT_EXTS,
|
|
16
17
|
UTF8_TEXT_EXTS,
|
|
@@ -355,10 +356,27 @@ export async function distill(dir, { format = 'summary', query = '', mode = 'aut
|
|
|
355
356
|
}
|
|
356
357
|
|
|
357
358
|
process.stderr.write(' Invoking claude --print …\n\n');
|
|
358
|
-
const result =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
const result = await withGenAiSpan('chat', {
|
|
360
|
+
[GenAiAttrs.SYSTEM]: 'anthropic',
|
|
361
|
+
[GenAiAttrs.OPERATION_NAME]: 'chat',
|
|
362
|
+
[GenAiAttrs.REQUEST_MODEL]: process.env.CONSTRUCT_MODEL || 'claude-3-5-sonnet',
|
|
363
|
+
'construct.operation': 'distill',
|
|
364
|
+
'construct.prompt_chars': prompt.length,
|
|
365
|
+
}, async (span) => {
|
|
366
|
+
const r = spawnSync('claude', ['--print', prompt], {
|
|
367
|
+
encoding: 'utf8',
|
|
368
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
369
|
+
timeout: 120_000,
|
|
370
|
+
});
|
|
371
|
+
if (r.error) throw r.error;
|
|
372
|
+
if (r.status !== 0) {
|
|
373
|
+
const errOut = r.stderr || '';
|
|
374
|
+
if (errOut) process.stderr.write(`${errOut}\n`);
|
|
375
|
+
throw new Error(`claude exited with status ${r.status}`);
|
|
376
|
+
}
|
|
377
|
+
const out = r.stdout || '';
|
|
378
|
+
span.setAttribute(GenAiAttrs.USAGE_OUTPUT_TOKENS, Math.ceil(out.length / 4));
|
|
379
|
+
return r;
|
|
362
380
|
});
|
|
363
381
|
|
|
364
382
|
if (result.error) throw result.error;
|
package/lib/doc-stamp.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* stampFrontmatter — inject/update audit block at top of a markdown string
|
|
8
8
|
* verifyStamp — verify body_hash against current content
|
|
9
9
|
* parseStamp — extract stamp fields from a document string
|
|
10
|
+
* validateTags — validate frontmatter tags against a loaded vocabulary
|
|
10
11
|
*
|
|
11
12
|
* Design:
|
|
12
13
|
* - Zero npm dependencies (Node built-ins only)
|
|
@@ -16,9 +17,12 @@
|
|
|
16
17
|
* trimmed, so whitespace-only edits do not break verification.
|
|
17
18
|
* - generator field is intentionally short (no version pinning) so the
|
|
18
19
|
* stamp survives construct upgrades without false-positive hash mismatches.
|
|
20
|
+
* - validateTags does NOT throw: it returns { errors, warnings } for callers
|
|
21
|
+
* to decide how to handle violations.
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
24
|
import { createHash, randomBytes } from 'node:crypto';
|
|
25
|
+
import { validateTags as vocabValidateTags, isTagDeprecated, isTagArchived } from './tags/vocabulary.mjs';
|
|
22
26
|
|
|
23
27
|
// ---------------------------------------------------------------------------
|
|
24
28
|
// UUIDv7 — RFC 9562 §5.7
|
|
@@ -147,6 +151,50 @@ export function parseStamp(content) {
|
|
|
147
151
|
return fields;
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Tag validation — uses vocabulary module, returns errors/warnings for callers
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Validates frontmatter tags against a loaded vocabulary object.
|
|
160
|
+
* Returns { errors: string[], warnings: string[] }.
|
|
161
|
+
*
|
|
162
|
+
* Errors (hard violations):
|
|
163
|
+
* - Two or more tags from the same exclusive facet (duplicateFacets)
|
|
164
|
+
* - Writing a tag with status 'archived'
|
|
165
|
+
*
|
|
166
|
+
* Warnings (soft violations):
|
|
167
|
+
* - Tag ID not found in vocabulary (unknown)
|
|
168
|
+
* - Writing a tag with status 'deprecated'
|
|
169
|
+
*/
|
|
170
|
+
export function validateTags(frontmatter, vocab) {
|
|
171
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
|
|
172
|
+
const errors = [];
|
|
173
|
+
const warnings = [];
|
|
174
|
+
|
|
175
|
+
if (tags.length === 0) return { errors, warnings };
|
|
176
|
+
|
|
177
|
+
const { unknown, duplicateFacets } = vocabValidateTags(tags, vocab);
|
|
178
|
+
|
|
179
|
+
for (const id of duplicateFacets) {
|
|
180
|
+
errors.push(`exclusive-facet violation: tag "${id}" shares a facet with an earlier tag`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const id of unknown) {
|
|
184
|
+
warnings.push(`unknown tag: "${id}" is not in the controlled vocabulary`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const id of tags) {
|
|
188
|
+
if (isTagArchived(id, vocab)) {
|
|
189
|
+
errors.push(`archived-tag write: tag "${id}" has status archived and must not be applied`);
|
|
190
|
+
} else if (isTagDeprecated(id, vocab)) {
|
|
191
|
+
warnings.push(`deprecated-tag write: tag "${id}" has status deprecated`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { errors, warnings };
|
|
196
|
+
}
|
|
197
|
+
|
|
150
198
|
/**
|
|
151
199
|
* Verify that the stored body_hash matches the current body.
|
|
152
200
|
* Returns { valid: true } or { valid: false, reason, stored, computed }.
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* 2. MCP registration ↔ impl: every `name === '<tool>'` branch in the MCP
|
|
8
8
|
* dispatch resolves to an imported function, and every exported tool
|
|
9
9
|
* function in lib/mcp/tools/*.mjs is wired into the dispatch.
|
|
10
|
-
* 3. Roles ↔ registry: every persona key in
|
|
11
|
-
* resolves to a persona or agent name in
|
|
10
|
+
* 3. Roles ↔ registry: every persona key in specialists/role-manifests.json
|
|
11
|
+
* resolves to a persona or agent name in specialists/registry.json.
|
|
12
12
|
* 4. Persona promptFiles: every registry.personas[].promptFile and
|
|
13
13
|
* registry.agents[].promptFile exists on disk.
|
|
14
|
-
* 5. Contracts ↔ schemas:
|
|
14
|
+
* 5. Contracts ↔ schemas: specialists/contracts.json passes validateContractsFile
|
|
15
15
|
* (cross-file refs, producer/consumer names, schema paths).
|
|
16
16
|
*
|
|
17
17
|
* Each violation produces an audit record. Blocking severity escalates to a
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
23
23
|
import { join, dirname, resolve } from 'node:path';
|
|
24
|
+
import os from 'node:os';
|
|
24
25
|
import { fileURLToPath } from 'node:url';
|
|
25
26
|
|
|
26
27
|
import { record } from '../audit.mjs';
|
|
@@ -61,7 +62,7 @@ export async function tick() {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
|
|
65
|
+
* Run all six consistency checks. Returns { findings: [], passed: [] }.
|
|
65
66
|
* Pure function — no audit/escalate side effects. Surfaced separately so the
|
|
66
67
|
* `construct doctor consistency` CLI and the release-gate functional test can
|
|
67
68
|
* call it without spinning up the daemon.
|
|
@@ -85,6 +86,9 @@ export async function runAllChecks({ repoRoot = REPO_ROOT } = {}) {
|
|
|
85
86
|
const contracts = await checkContractsDrift({ repoRoot });
|
|
86
87
|
collect(contracts, findings, passed, 'contracts-drift');
|
|
87
88
|
|
|
89
|
+
const hosts = checkHostSurfaces({ repoRoot });
|
|
90
|
+
collect(hosts, findings, passed, 'host-surfaces');
|
|
91
|
+
|
|
88
92
|
return { findings, passed };
|
|
89
93
|
}
|
|
90
94
|
|
|
@@ -219,8 +223,8 @@ function camelToSnake(s) {
|
|
|
219
223
|
|
|
220
224
|
function checkRolesDrift({ repoRoot }) {
|
|
221
225
|
const violations = [];
|
|
222
|
-
const manifestsPath = join(repoRoot, '
|
|
223
|
-
const registryPath = join(repoRoot, '
|
|
226
|
+
const manifestsPath = join(repoRoot, 'specialists', 'role-manifests.json');
|
|
227
|
+
const registryPath = join(repoRoot, 'specialists', 'registry.json');
|
|
224
228
|
|
|
225
229
|
if (!existsSync(manifestsPath) || !existsSync(registryPath)) {
|
|
226
230
|
return { summary: 'role manifests or registry missing', violations: [] };
|
|
@@ -230,11 +234,12 @@ function checkRolesDrift({ repoRoot }) {
|
|
|
230
234
|
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
231
235
|
|
|
232
236
|
const known = new Set();
|
|
233
|
-
for (const
|
|
234
|
-
if (
|
|
237
|
+
for (const s of registry.specialists || []) {
|
|
238
|
+
if (s?.name) { known.add(s.name); known.add(`cx-${s.name}`); }
|
|
235
239
|
}
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
if (registry.orchestrator?.name) {
|
|
241
|
+
known.add(registry.orchestrator.name);
|
|
242
|
+
known.add(`cx-${registry.orchestrator.name}`);
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
const personaKeys = Object.keys(manifests.personas || manifests || {});
|
|
@@ -257,13 +262,16 @@ function checkRolesDrift({ repoRoot }) {
|
|
|
257
262
|
|
|
258
263
|
function checkPersonaPrompts({ repoRoot }) {
|
|
259
264
|
const violations = [];
|
|
260
|
-
const registryPath = join(repoRoot, '
|
|
265
|
+
const registryPath = join(repoRoot, 'specialists', 'registry.json');
|
|
261
266
|
if (!existsSync(registryPath)) return { summary: 'registry missing', violations: [] };
|
|
262
267
|
|
|
263
268
|
const registry = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
264
269
|
let checked = 0;
|
|
265
270
|
|
|
266
|
-
for (const list of [
|
|
271
|
+
for (const list of [
|
|
272
|
+
registry.orchestrator ? [registry.orchestrator] : [],
|
|
273
|
+
registry.specialists || [],
|
|
274
|
+
]) {
|
|
267
275
|
for (const entry of list) {
|
|
268
276
|
const ref = entry?.promptFile;
|
|
269
277
|
if (!ref) continue;
|
|
@@ -308,3 +316,45 @@ async function checkContractsDrift({ repoRoot }) {
|
|
|
308
316
|
}
|
|
309
317
|
return { summary: `contracts: ${violations.length} drift`, violations };
|
|
310
318
|
}
|
|
319
|
+
|
|
320
|
+
// ── Check 6: host surfaces ─────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
function getVSCodeSettingsDir(homeDir) {
|
|
323
|
+
const platform = os.platform();
|
|
324
|
+
if (platform === 'darwin') return join(homeDir, 'Library', 'Application Support', 'Code', 'User');
|
|
325
|
+
if (platform === 'linux') return join(homeDir, '.config', 'Code', 'User');
|
|
326
|
+
const appData = process.env.APPDATA ?? join(homeDir, 'AppData', 'Roaming');
|
|
327
|
+
return join(appData, 'Code', 'User');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function checkHostSurfaces({ repoRoot }) {
|
|
331
|
+
const violations = [];
|
|
332
|
+
const homeDir = os.homedir();
|
|
333
|
+
const surfaces = [
|
|
334
|
+
{ id: 'claude', label: 'Claude Code', check: () => existsSync(join(homeDir, '.claude', 'agents')) },
|
|
335
|
+
{ id: 'opencode', label: 'OpenCode', check: () => existsSync(join(homeDir, '.config', 'opencode', 'opencode.json')) },
|
|
336
|
+
{ id: 'codex', label: 'Codex', check: () => existsSync(join(homeDir, '.codex', 'agents')) },
|
|
337
|
+
{ id: 'copilot', label: 'Copilot', check: () => existsSync(join(homeDir, '.github', 'prompts')) },
|
|
338
|
+
{ id: 'vscode', label: 'VS Code', check: () => existsSync(join(getVSCodeSettingsDir(homeDir), 'settings.json')) },
|
|
339
|
+
{ id: 'cursor', label: 'Cursor', check: () => existsSync(join(homeDir, '.cursor', 'mcp.json')) },
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
const absent = surfaces.filter((s) => !s.check());
|
|
343
|
+
if (absent.length > 0) {
|
|
344
|
+
for (const s of absent) {
|
|
345
|
+
violations.push({
|
|
346
|
+
category: 'host-surfaces',
|
|
347
|
+
severity: 'advisory',
|
|
348
|
+
target: s.id,
|
|
349
|
+
summary: `Host surface '${s.label}' config not found — run \`construct sync\` to set up adapters`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
violations.push({
|
|
353
|
+
category: 'host-surfaces',
|
|
354
|
+
severity: 'advisory',
|
|
355
|
+
summary: `${absent.length}/${surfaces.length} host surfaces absent: ${absent.map((s) => s.id).join(', ')}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return { summary: `host-surfaces: ${surfaces.length - absent.length}/${surfaces.length} present`, violations };
|
|
360
|
+
}
|