@geraldmaron/construct 1.0.14 → 1.0.15
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 +6 -0
- package/bin/construct +3 -3
- package/commands/build/feature.md +0 -6
- package/commands/build/fix.md +0 -6
- package/commands/design/access.md +0 -6
- package/commands/design/flow.md +0 -6
- package/commands/design/ui.md +0 -6
- package/commands/measure/experiment.md +0 -6
- package/commands/measure/metrics.md +0 -6
- package/commands/measure/results.md +0 -6
- package/commands/plan/api.md +0 -6
- package/commands/plan/challenge.md +0 -6
- package/commands/plan/decide.md +0 -6
- package/commands/plan/feature.md +0 -7
- package/commands/plan/requirements.md +0 -6
- package/commands/remember/context.md +0 -6
- package/commands/remember/handoff.md +0 -6
- package/commands/remember/runbook.md +0 -6
- package/commands/review/code.md +0 -6
- package/commands/review/quality.md +0 -6
- package/commands/review/security.md +0 -6
- package/commands/ship/ready.md +0 -6
- package/commands/ship/release.md +0 -6
- package/commands/ship/status.md +0 -6
- package/commands/understand/docs.md +0 -6
- package/commands/understand/research.md +0 -6
- package/commands/understand/this.md +0 -6
- package/commands/understand/why.md +0 -6
- package/commands/work/clean.md +0 -6
- package/commands/work/drive.md +0 -6
- package/commands/work/optimize-prompts.md +0 -6
- package/commands/work/parallel-review.md +0 -6
- package/lib/beads-client.mjs +19 -1
- package/lib/comment-lint.mjs +5 -1
- package/lib/context-state.mjs +15 -3
- package/lib/contracts/validate.mjs +54 -10
- package/lib/contracts/violation-log.mjs +154 -0
- package/lib/document-extract/docling-client.mjs +114 -0
- package/lib/document-extract/docling-sidecar.py +122 -0
- package/lib/document-extract/whisper-client.mjs +79 -0
- package/lib/document-extract.mjs +73 -8
- package/lib/document-ingest.mjs +26 -4
- package/lib/embed/daemon.mjs +6 -1
- package/lib/flavors/loader.mjs +1 -1
- package/lib/handoffs/contract.mjs +2 -2
- package/lib/hooks/session-start.mjs +1 -1
- package/lib/ingest/chunker.mjs +94 -0
- package/lib/ingest/pipeline.mjs +53 -0
- package/lib/ingest/store.mjs +82 -0
- package/lib/knowledge/search.mjs +12 -0
- package/lib/mcp/server.mjs +142 -1
- package/lib/mcp/tools/skills.mjs +6 -3
- package/lib/mcp/tools/workflow.mjs +6 -2
- package/lib/observation-store.mjs +14 -5
- package/lib/ollama-manager.mjs +32 -28
- package/lib/prompt-composer.js +11 -1
- package/lib/runtime/uv-bootstrap.mjs +129 -0
- package/lib/runtime/whisper-bootstrap.mjs +102 -0
- package/lib/server/index.mjs +22 -1
- package/lib/server/static/index.html +1 -1
- package/lib/specialists/postconditions.mjs +1 -1
- package/lib/sync/skill-frontmatter.mjs +113 -21
- package/lib/tracking-surfaces.mjs +2 -0
- package/lib/validators/skills.mjs +86 -54
- package/package.json +5 -5
- package/personas/construct.md +4 -7
- package/platforms/claude/settings.template.json +1 -1
- package/rules/common/beads-hygiene.md +3 -9
- package/rules/common/code-review.md +3 -6
- package/rules/common/coding-style.md +3 -6
- package/rules/common/comments.md +3 -7
- package/rules/common/commit-approval.md +3 -6
- package/rules/common/cx-agent-routing.md +3 -7
- package/rules/common/cx-skill-routing.md +3 -3
- package/rules/common/doc-ownership.md +3 -8
- package/rules/common/efficiency.md +3 -8
- package/rules/common/framing.md +3 -8
- package/rules/common/git-workflow.md +3 -5
- package/rules/common/no-fabrication.md +4 -12
- package/rules/common/patterns.md +3 -5
- package/rules/common/release-gates.md +3 -8
- package/rules/common/research.md +3 -8
- package/rules/common/review-before-change.md +3 -9
- package/rules/common/security.md +3 -6
- package/rules/common/skill-composition.md +3 -7
- package/rules/common/testing.md +3 -6
- package/rules/golang/coding-style.md +1 -5
- package/rules/golang/hooks.md +1 -5
- package/rules/golang/patterns.md +1 -5
- package/rules/golang/security.md +1 -5
- package/rules/golang/testing.md +1 -5
- package/rules/python/coding-style.md +1 -5
- package/rules/python/hooks.md +1 -5
- package/rules/python/patterns.md +1 -5
- package/rules/python/security.md +1 -5
- package/rules/python/testing.md +1 -5
- package/rules/swift/coding-style.md +1 -5
- package/rules/swift/hooks.md +1 -5
- package/rules/swift/patterns.md +1 -5
- package/rules/swift/security.md +1 -5
- package/rules/swift/testing.md +1 -5
- package/rules/typescript/coding-style.md +1 -5
- package/rules/typescript/hooks.md +1 -5
- package/rules/typescript/patterns.md +1 -5
- package/rules/typescript/security.md +1 -5
- package/rules/typescript/testing.md +1 -5
- package/rules/web/coding-style.md +3 -5
- package/rules/web/design-quality.md +3 -5
- package/rules/web/hooks.md +3 -5
- package/rules/web/patterns.md +3 -5
- package/rules/web/performance.md +3 -5
- package/rules/web/security.md +3 -5
- package/rules/web/testing.md +3 -5
- package/skills/ai/agent-dev.md +4 -5
- package/skills/ai/llm-security.md +4 -5
- package/skills/ai/ml-ops.md +4 -5
- package/skills/ai/orchestration-workflow.md +4 -5
- package/skills/ai/prompt-and-eval.md +4 -5
- package/skills/ai/prompt-optimizer.md +4 -6
- package/skills/ai/rag-system.md +4 -5
- package/skills/architecture/api-design.md +4 -5
- package/skills/architecture/caching.md +4 -5
- package/skills/architecture/cloud-native.md +4 -5
- package/skills/architecture/message-queue.md +4 -5
- package/skills/architecture/security-arch.md +4 -5
- package/skills/compliance/ai-disclosure.md +4 -5
- package/skills/compliance/data-privacy.md +4 -5
- package/skills/compliance/license-audit.md +4 -5
- package/skills/compliance/regulatory-review.md +4 -5
- package/skills/development/cpp.md +4 -5
- package/skills/development/go.md +4 -5
- package/skills/development/java.md +4 -5
- package/skills/development/kotlin.md +4 -5
- package/skills/development/mobile-crossplatform.md +4 -5
- package/skills/development/python.md +4 -5
- package/skills/development/rust.md +4 -5
- package/skills/development/shell.md +4 -5
- package/skills/development/swift.md +4 -5
- package/skills/development/typescript.md +4 -5
- package/skills/devops/ci-cd.md +4 -5
- package/skills/devops/containerization.md +4 -5
- package/skills/devops/cost-optimization.md +4 -5
- package/skills/devops/data-engineering.md +4 -5
- package/skills/devops/database.md +4 -5
- package/skills/devops/dependency-management.md +4 -5
- package/skills/devops/devsecops.md +4 -5
- package/skills/devops/git-workflow.md +4 -5
- package/skills/devops/incident-response.md +4 -5
- package/skills/devops/monorepo.md +4 -5
- package/skills/devops/observability.md +4 -5
- package/skills/devops/performance.md +4 -5
- package/skills/devops/testing.md +4 -5
- package/skills/docs/adr-workflow.md +4 -7
- package/skills/docs/backlog-proposal-workflow.md +4 -3
- package/skills/docs/customer-profile-workflow.md +4 -3
- package/skills/docs/document-ingest-workflow.md +4 -3
- package/skills/docs/evidence-ingest-workflow.md +4 -3
- package/skills/docs/init-docs.md +4 -5
- package/skills/docs/init-project.md +4 -5
- package/skills/docs/prd-workflow.md +4 -7
- package/skills/docs/prfaq-workflow.md +4 -3
- package/skills/docs/product-intelligence-review.md +4 -3
- package/skills/docs/product-intelligence-workflow.md +4 -6
- package/skills/docs/product-signal-workflow.md +4 -5
- package/skills/docs/research-workflow.md +4 -5
- package/skills/docs/runbook-workflow.md +4 -5
- package/skills/docs/strategy-workflow.md +4 -5
- package/skills/exploration/dependency-graph-reading.md +4 -7
- package/skills/exploration/repo-map.md +4 -5
- package/skills/exploration/tracer-bullet-method.md +4 -7
- package/skills/exploration/unknown-codebase-onboarding.md +4 -7
- package/skills/frameworks/django.md +4 -5
- package/skills/frameworks/nextjs.md +4 -5
- package/skills/frameworks/react.md +4 -5
- package/skills/frameworks/spring-boot.md +4 -5
- package/skills/frontend-design/accessibility.md +4 -5
- package/skills/frontend-design/component-patterns.md +4 -5
- package/skills/frontend-design/engineering.md +4 -5
- package/skills/frontend-design/state-management.md +4 -5
- package/skills/frontend-design/ui-aesthetics.md +4 -5
- package/skills/frontend-design/ux-principles.md +4 -5
- package/skills/operating/change-management.md +4 -8
- package/skills/operating/incident-response.md +4 -8
- package/skills/operating/oncall-rotation.md +4 -7
- package/skills/operating/orchestration-reference.md +4 -10
- package/skills/quality-gates/review-work.md +4 -5
- package/skills/quality-gates/verify-change.md +4 -5
- package/skills/quality-gates/verify-module.md +4 -5
- package/skills/quality-gates/verify-quality.md +4 -5
- package/skills/quality-gates/verify-security.md +4 -5
- package/skills/roles/architect.ai-systems.md +6 -9
- package/skills/roles/architect.data.md +6 -9
- package/skills/roles/architect.enterprise.md +6 -9
- package/skills/roles/architect.integration.md +6 -9
- package/skills/roles/architect.md +7 -14
- package/skills/roles/architect.platform.md +6 -9
- package/skills/roles/data-analyst.experiment.md +6 -9
- package/skills/roles/data-analyst.md +6 -9
- package/skills/roles/data-analyst.product-intelligence.md +7 -9
- package/skills/roles/data-analyst.product.md +6 -9
- package/skills/roles/data-analyst.telemetry.md +7 -9
- package/skills/roles/data-engineer.pipeline.md +6 -9
- package/skills/roles/data-engineer.vector-retrieval.md +7 -9
- package/skills/roles/data-engineer.warehouse.md +6 -9
- package/skills/roles/debugger.md +6 -9
- package/skills/roles/designer.accessibility.md +6 -9
- package/skills/roles/designer.md +7 -9
- package/skills/roles/engineer.ai.md +6 -9
- package/skills/roles/engineer.data.md +6 -9
- package/skills/roles/engineer.md +9 -9
- package/skills/roles/engineer.platform.md +6 -9
- package/skills/roles/operator.docs.md +6 -9
- package/skills/roles/operator.md +9 -9
- package/skills/roles/operator.release.md +6 -9
- package/skills/roles/operator.sre.md +6 -9
- package/skills/roles/orchestrator.md +6 -9
- package/skills/roles/product-manager.ai-product.md +6 -9
- package/skills/roles/product-manager.business-strategy.md +6 -9
- package/skills/roles/product-manager.enterprise.md +6 -9
- package/skills/roles/product-manager.growth.md +7 -9
- package/skills/roles/product-manager.md +7 -9
- package/skills/roles/product-manager.platform.md +6 -9
- package/skills/roles/product-manager.product.md +6 -9
- package/skills/roles/qa.ai-eval.md +8 -9
- package/skills/roles/qa.api-contract.md +7 -9
- package/skills/roles/qa.data-pipeline.md +7 -9
- package/skills/roles/qa.md +7 -9
- package/skills/roles/qa.test-automation.md +6 -9
- package/skills/roles/qa.web-ui.md +7 -9
- package/skills/roles/researcher.explorer.md +6 -9
- package/skills/roles/researcher.md +8 -9
- package/skills/roles/researcher.ux.md +6 -9
- package/skills/roles/reviewer.devil-advocate.md +6 -9
- package/skills/roles/reviewer.evaluator.md +6 -9
- package/skills/roles/reviewer.md +9 -9
- package/skills/roles/reviewer.trace.md +6 -9
- package/skills/roles/security.ai.md +7 -9
- package/skills/roles/security.appsec.md +6 -9
- package/skills/roles/security.cloud.md +6 -9
- package/skills/roles/security.legal-compliance.md +6 -9
- package/skills/roles/security.md +7 -9
- package/skills/roles/security.privacy.md +7 -9
- package/skills/roles/security.supply-chain.md +7 -9
- package/skills/security/blue-team.md +4 -5
- package/skills/security/code-audit.md +4 -5
- package/skills/security/pentest.md +4 -5
- package/skills/security/red-team.md +4 -5
- package/skills/security/threat-intel.md +4 -5
- package/skills/security/vuln-research.md +4 -5
- package/skills/strategy/competitive-landscape.md +4 -8
- package/skills/strategy/market-research-methods.md +4 -8
- package/skills/strategy/narrative-arc.md +4 -8
- package/skills/strategy/pricing-positioning.md +4 -8
- package/skills/utility/clean-code.md +4 -5
- package/specialists/policy-inventory.json +14 -0
- package/lib/specialist-contracts-enforce.mjs +0 -172
- package/rules/common/agents.md +0 -28
- package/rules/common/development-workflow.md +0 -32
- package/rules/common/performance.md +0 -55
package/lib/document-ingest.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
10
|
-
import { extractDocumentText, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
|
|
10
|
+
import { extractDocumentText, extractDocumentTextAsync, extractDocumentMetadata, isExtractableDocumentPath } from './document-extract.mjs';
|
|
11
11
|
import { syncFileStateToSql } from './storage/sync.mjs';
|
|
12
12
|
import { stampFrontmatter } from './doc-stamp.mjs';
|
|
13
13
|
import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS } from './knowledge/layout.mjs';
|
|
@@ -127,6 +127,8 @@ export async function ingestDocuments(inputPaths, {
|
|
|
127
127
|
outputDir = null,
|
|
128
128
|
target = 'knowledge/internal',
|
|
129
129
|
sync = false,
|
|
130
|
+
strict = false,
|
|
131
|
+
highFidelity = true,
|
|
130
132
|
env = process.env,
|
|
131
133
|
} = {}) {
|
|
132
134
|
if (!Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
@@ -142,8 +144,12 @@ export async function ingestDocuments(inputPaths, {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
const results = [];
|
|
147
|
+
let totalDrops = 0;
|
|
145
148
|
for (const sourcePath of files) {
|
|
146
|
-
const extracted =
|
|
149
|
+
const extracted = highFidelity
|
|
150
|
+
? await extractDocumentTextAsync(sourcePath, { maxChars: 200_000 })
|
|
151
|
+
: extractDocumentText(sourcePath, { maxChars: 200_000 });
|
|
152
|
+
totalDrops += (extracted.droppedInfo ?? []).reduce((a, d) => a + (Number(d.count) || 1), 0);
|
|
147
153
|
const metadata = extractDocumentMetadata(sourcePath);
|
|
148
154
|
const targetPath = resolveOutputPath(sourcePath, { cwd, outputPath, outputDir, target });
|
|
149
155
|
ensureDir(dirname(targetPath));
|
|
@@ -184,6 +190,13 @@ export async function ingestDocuments(inputPaths, {
|
|
|
184
190
|
});
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
if (strict && totalDrops > 0) {
|
|
194
|
+
const err = new Error(`--strict: ${totalDrops} dropped items across ${results.filter(r => r.droppedInfo.length).length} files`);
|
|
195
|
+
err.code = 'INGEST_DROPS';
|
|
196
|
+
err.droppedInfo = results.flatMap((r) => r.droppedInfo.map((d) => ({ ...d, sourcePath: r.sourcePath })));
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
|
|
187
200
|
return {
|
|
188
201
|
status: 'ok',
|
|
189
202
|
target: outputPath ? 'custom' : target,
|
|
@@ -197,6 +210,9 @@ export async function ingestDocuments(inputPaths, {
|
|
|
197
210
|
indexedLocally: true,
|
|
198
211
|
storageSync: syncResult,
|
|
199
212
|
files: results,
|
|
213
|
+
droppedInfo: results.flatMap((r) => r.droppedInfo.map((d) => ({ ...d, sourcePath: r.sourcePath }))),
|
|
214
|
+
droppedCount: totalDrops,
|
|
215
|
+
highFidelity,
|
|
200
216
|
};
|
|
201
217
|
}
|
|
202
218
|
|
|
@@ -206,19 +222,25 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
206
222
|
let outputDir = null;
|
|
207
223
|
let target = 'knowledge/internal';
|
|
208
224
|
let sync = false;
|
|
225
|
+
let strict = false;
|
|
226
|
+
let highFidelity = true;
|
|
209
227
|
|
|
210
228
|
for (const arg of argv) {
|
|
211
229
|
if (arg.startsWith('--out=')) outputPath = arg.split('=').slice(1).join('=');
|
|
212
230
|
else if (arg.startsWith('--out-dir=')) outputDir = arg.split('=').slice(1).join('=');
|
|
213
231
|
else if (arg.startsWith('--target=')) target = arg.split('=').slice(1).join('=');
|
|
214
232
|
else if (arg === '--sync') sync = true;
|
|
233
|
+
else if (arg === '--strict') strict = true;
|
|
234
|
+
else if (arg === '--legacy-extractor') highFidelity = false;
|
|
215
235
|
else inputs.push(arg);
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
if (inputs.length === 0) {
|
|
219
239
|
throw new Error(
|
|
220
240
|
`Usage: construct ingest <file-or-dir> [more paths] [--out=FILE] [--out-dir=DIR] ` +
|
|
221
|
-
`[--target=sibling|knowledge/<subdir>] [--sync]\n` +
|
|
241
|
+
`[--target=sibling|knowledge/<subdir>] [--sync] [--strict] [--legacy-extractor]\n` +
|
|
242
|
+
` --strict: exit non-zero if any extraction drops occur\n` +
|
|
243
|
+
` --legacy-extractor: use the pre-docling regex extractor (lower fidelity)\n` +
|
|
222
244
|
` knowledge subdirs: ${KNOWLEDGE_SUBDIRS.join(', ')}`,
|
|
223
245
|
);
|
|
224
246
|
}
|
|
@@ -229,5 +251,5 @@ export async function runIngestCli(argv = process.argv.slice(2), { cwd = process
|
|
|
229
251
|
);
|
|
230
252
|
}
|
|
231
253
|
|
|
232
|
-
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, env });
|
|
254
|
+
return ingestDocuments(inputs, { cwd, outputPath, outputDir, target, sync, strict, highFidelity, env });
|
|
233
255
|
}
|
package/lib/embed/daemon.mjs
CHANGED
|
@@ -1095,7 +1095,12 @@ this.#scheduler.register(
|
|
|
1095
1095
|
outputTokens = data.usage?.completion_tokens || 0;
|
|
1096
1096
|
}
|
|
1097
1097
|
}
|
|
1098
|
-
} catch {
|
|
1098
|
+
} catch (probeErr) {
|
|
1099
|
+
// Probe call failed; record the timing entry but surface the
|
|
1100
|
+
// failure to the daemon log so operators can distinguish a 0ms
|
|
1101
|
+
// success from a hard error. Don't crash the daemon.
|
|
1102
|
+
process.stderr.write(`[embed] Probe call failed: ${probeErr.message}\n`);
|
|
1103
|
+
}
|
|
1099
1104
|
|
|
1100
1105
|
const endTime = Date.now();
|
|
1101
1106
|
|
package/lib/flavors/loader.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - validateFlavor(path) — frontmatter conformance check against the schema
|
|
7
7
|
* - perRoleFlavorCount(profile) — flavor counts per role for cap enforcement
|
|
8
8
|
*
|
|
9
|
-
* The cap is enforced at sync time and in lint-
|
|
9
|
+
* The cap is enforced at sync time and in lint-profiles.
|
|
10
10
|
* Six flavors per role per profile is the current ceiling; bumping requires
|
|
11
11
|
* an ADR. Existing roles like qa already have 6, so the cap is set tight.
|
|
12
12
|
*/
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
*
|
|
39
39
|
* `parseHandoff` is intentionally permissive — pre-contract handoffs with
|
|
40
40
|
* no frontmatter still parse with status='legacy' so they aren't lost.
|
|
41
|
-
* `
|
|
41
|
+
* `validateHandoffFile` is the strict version, used when writing.
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { readFileSync } from 'node:fs';
|
|
@@ -109,7 +109,7 @@ export function parseHandoffFile(filePath) {
|
|
|
109
109
|
return parseHandoff(text);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export function
|
|
112
|
+
export function validateHandoffFile(parsed) {
|
|
113
113
|
const errors = [];
|
|
114
114
|
const fm = parsed?.frontmatter || {};
|
|
115
115
|
if (fm.schema !== HANDOFF_SCHEMA_VERSION) errors.push(`schema must be '${HANDOFF_SCHEMA_VERSION}'`);
|
|
@@ -271,7 +271,7 @@ try {
|
|
|
271
271
|
if (hasSources) {
|
|
272
272
|
const parts = [];
|
|
273
273
|
if (repos.length > 0) parts.push(`GitHub repos: ${repos.join(', ')}`);
|
|
274
|
-
if (jiraConfigured) parts.push(
|
|
274
|
+
if (jiraConfigured) parts.push('Jira: configured');
|
|
275
275
|
if (linearConfigured) parts.push('Linear: configured');
|
|
276
276
|
// Surface which sources are wired so `provider_fetch` is the obvious
|
|
277
277
|
// tool for questions about them. The "fetch first, don't answer from
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/chunker.mjs — paragraph-boundary chunker with sentence overlap.
|
|
3
|
+
*
|
|
4
|
+
* 2026-06 best practice notes:
|
|
5
|
+
* - Recursive/sentence chunking remains the high-recall default (~85-90%
|
|
6
|
+
* on 2026 RAG benchmarks). Semantic chunking gains only ~2-3% recall at
|
|
7
|
+
* ~14× the embedding cost and only matters above ~5k token docs.
|
|
8
|
+
* - Parent-document retrieval is the meaningful 2026 upgrade beyond
|
|
9
|
+
* paragraph chunking — track separately if recall plateaus.
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* 1. Split markdown by blank-line paragraph boundaries.
|
|
13
|
+
* 2. Pack paragraphs into chunks under maxChars (default 1500 ≈ 375 tokens).
|
|
14
|
+
* 3. Add 1-2 sentence overlap between consecutive chunks to preserve
|
|
15
|
+
* cross-chunk anchors.
|
|
16
|
+
* 4. Preserve markdown structure: never split inside a fenced code block.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_CHARS = 1500;
|
|
20
|
+
const DEFAULT_OVERLAP_SENTENCES = 2;
|
|
21
|
+
const SENTENCE_RE = /[^.!?]+[.!?]+["')\]]*\s*/g;
|
|
22
|
+
|
|
23
|
+
export function splitSentences(text) {
|
|
24
|
+
const matches = text.match(SENTENCE_RE);
|
|
25
|
+
if (matches && matches.length) return matches.map((s) => s.trim()).filter(Boolean);
|
|
26
|
+
return text.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tailSentences(text, count) {
|
|
30
|
+
const sentences = splitSentences(text);
|
|
31
|
+
return sentences.slice(-count).join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitParagraphs(markdown) {
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let buffer = '';
|
|
37
|
+
let inFence = false;
|
|
38
|
+
for (const line of markdown.split('\n')) {
|
|
39
|
+
if (/^```/.test(line.trim())) inFence = !inFence;
|
|
40
|
+
if (!inFence && line.trim() === '' && buffer.trim()) {
|
|
41
|
+
blocks.push(buffer.trim());
|
|
42
|
+
buffer = '';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
buffer += line + '\n';
|
|
46
|
+
}
|
|
47
|
+
if (buffer.trim()) blocks.push(buffer.trim());
|
|
48
|
+
return blocks;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function chunkMarkdown(markdown, { maxChars = DEFAULT_MAX_CHARS, overlapSentences = DEFAULT_OVERLAP_SENTENCES } = {}) {
|
|
52
|
+
const paragraphs = splitParagraphs(markdown || '');
|
|
53
|
+
if (paragraphs.length === 0) return [];
|
|
54
|
+
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let current = '';
|
|
57
|
+
let chunkIndex = 0;
|
|
58
|
+
const flushChunk = () => {
|
|
59
|
+
if (!current.trim()) return;
|
|
60
|
+
const text = current.trim();
|
|
61
|
+
chunks.push({
|
|
62
|
+
index: chunkIndex++,
|
|
63
|
+
text,
|
|
64
|
+
chars: text.length,
|
|
65
|
+
});
|
|
66
|
+
const overlap = overlapSentences > 0 ? tailSentences(text, overlapSentences) : '';
|
|
67
|
+
current = overlap ? overlap + '\n\n' : '';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const para of paragraphs) {
|
|
71
|
+
if (current.length + para.length + 2 > maxChars && current.trim()) {
|
|
72
|
+
flushChunk();
|
|
73
|
+
}
|
|
74
|
+
if (para.length > maxChars) {
|
|
75
|
+
flushChunk();
|
|
76
|
+
const sentences = splitSentences(para);
|
|
77
|
+
let buf = '';
|
|
78
|
+
for (const s of sentences) {
|
|
79
|
+
if (buf.length + s.length + 1 > maxChars && buf.trim()) {
|
|
80
|
+
chunks.push({ index: chunkIndex++, text: buf.trim(), chars: buf.trim().length });
|
|
81
|
+
buf = (overlapSentences > 0 ? tailSentences(buf, overlapSentences) : '') + (overlapSentences > 0 ? ' ' : '');
|
|
82
|
+
}
|
|
83
|
+
buf += s + ' ';
|
|
84
|
+
}
|
|
85
|
+
if (buf.trim()) {
|
|
86
|
+
current = buf.trim() + '\n\n';
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
current += (current ? '\n\n' : '') + para;
|
|
91
|
+
}
|
|
92
|
+
flushChunk();
|
|
93
|
+
return chunks;
|
|
94
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/pipeline.mjs — orchestrate the content-hashed ingest pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Flow: hash → de-dup → extract via docling/whisper → chunk → write to
|
|
5
|
+
* .cx/ingest/<sha>/. Returns a record describing what was stored along
|
|
6
|
+
* with any droppedInfo surfaced by the extractor.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: re-ingesting the same content (by sha256) returns the
|
|
9
|
+
* existing record without re-extracting.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { extractDocumentTextAsync } from '../document-extract.mjs';
|
|
14
|
+
import { hashFile, defaultIngestRoot, readRecord, writeRecord } from './store.mjs';
|
|
15
|
+
import { chunkMarkdown } from './chunker.mjs';
|
|
16
|
+
|
|
17
|
+
export async function ingestFile(filePath, { cwd = process.cwd(), force = false } = {}) {
|
|
18
|
+
const absPath = path.resolve(filePath);
|
|
19
|
+
const stat = await fs.stat(absPath);
|
|
20
|
+
if (!stat.isFile()) throw new Error(`not a regular file: ${absPath}`);
|
|
21
|
+
|
|
22
|
+
const root = defaultIngestRoot(cwd);
|
|
23
|
+
const sha256 = await hashFile(absPath);
|
|
24
|
+
|
|
25
|
+
if (!force) {
|
|
26
|
+
const existing = await readRecord(root, sha256);
|
|
27
|
+
if (existing) return { ...existing, status: 'cached' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extracted = await extractDocumentTextAsync(absPath);
|
|
31
|
+
const markdown = extracted.markdown ?? extracted.text ?? '';
|
|
32
|
+
const chunks = chunkMarkdown(markdown);
|
|
33
|
+
|
|
34
|
+
const source = {
|
|
35
|
+
sourcePath: absPath,
|
|
36
|
+
fileName: path.basename(absPath),
|
|
37
|
+
extension: extracted.extension,
|
|
38
|
+
bytes: stat.size,
|
|
39
|
+
sha256,
|
|
40
|
+
ingestedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const meta = {
|
|
44
|
+
extractionMethod: extracted.extractionMethod,
|
|
45
|
+
extractorMetadata: extracted.metadata ?? null,
|
|
46
|
+
droppedInfo: extracted.droppedInfo ?? [],
|
|
47
|
+
chunkCount: chunks.length,
|
|
48
|
+
chunkChars: chunks.reduce((a, c) => a + c.chars, 0),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const record = await writeRecord(root, { sha256, source, meta, markdown });
|
|
52
|
+
return { ...record, status: 'ingested', droppedInfo: meta.droppedInfo, chunks };
|
|
53
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/store.mjs — content-addressed ingest store at .cx/ingest/<hash>/.
|
|
3
|
+
*
|
|
4
|
+
* Each ingested document is stored under its SHA-256 content hash:
|
|
5
|
+
* .cx/ingest/<sha256>/source.json — original path, size, sha256, ingestedAt
|
|
6
|
+
* .cx/ingest/<sha256>/markdown.md — extracted markdown body
|
|
7
|
+
* .cx/ingest/<sha256>/meta.json — extractor metadata, droppedInfo, chunks
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-ingesting a file with the same content is a no-op (returns
|
|
10
|
+
* the existing record). Re-running ingestion is the safe default.
|
|
11
|
+
*
|
|
12
|
+
* 2026-06 best practice notes:
|
|
13
|
+
* - SHA-256 is still the universal content-addressing primitive. BLAKE3 is
|
|
14
|
+
* ~2.3× faster on M-series and worth considering at multi-GB scale,
|
|
15
|
+
* but is not the industry default and adds an unfamiliar dependency.
|
|
16
|
+
* Revisit if ingest throughput becomes a measured bottleneck.
|
|
17
|
+
* - Storing markdown body separately from metadata keeps the embedding
|
|
18
|
+
* and search paths simple: they only read markdown.md and look up
|
|
19
|
+
* provenance from meta.json on demand.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { createReadStream } from 'node:fs';
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
export function defaultIngestRoot(cwd = process.cwd()) {
|
|
27
|
+
return path.join(cwd, '.cx', 'ingest');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function hashFile(filePath) {
|
|
31
|
+
return new Promise((resolveHash, reject) => {
|
|
32
|
+
const hash = createHash('sha256');
|
|
33
|
+
const stream = createReadStream(filePath);
|
|
34
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
35
|
+
stream.on('error', reject);
|
|
36
|
+
stream.on('end', () => resolveHash(hash.digest('hex')));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recordDirFor(root, sha256) {
|
|
41
|
+
return path.join(root, sha256);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readRecord(root, sha256) {
|
|
45
|
+
const dir = recordDirFor(root, sha256);
|
|
46
|
+
try {
|
|
47
|
+
const [source, meta, markdown] = await Promise.all([
|
|
48
|
+
fs.readFile(path.join(dir, 'source.json'), 'utf8').then(JSON.parse),
|
|
49
|
+
fs.readFile(path.join(dir, 'meta.json'), 'utf8').then(JSON.parse),
|
|
50
|
+
fs.readFile(path.join(dir, 'markdown.md'), 'utf8'),
|
|
51
|
+
]);
|
|
52
|
+
return { sha256, source, meta, markdown, dir };
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function writeRecord(root, { sha256, source, meta, markdown }) {
|
|
59
|
+
const dir = recordDirFor(root, sha256);
|
|
60
|
+
await fs.mkdir(dir, { recursive: true });
|
|
61
|
+
await Promise.all([
|
|
62
|
+
fs.writeFile(path.join(dir, 'source.json'), JSON.stringify(source, null, 2) + '\n', 'utf8'),
|
|
63
|
+
fs.writeFile(path.join(dir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8'),
|
|
64
|
+
fs.writeFile(path.join(dir, 'markdown.md'), markdown, 'utf8'),
|
|
65
|
+
]);
|
|
66
|
+
return { sha256, dir, source, meta, markdown };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function listRecords(root) {
|
|
70
|
+
let entries;
|
|
71
|
+
try { entries = await fs.readdir(root, { withFileTypes: true }); }
|
|
72
|
+
catch { return []; }
|
|
73
|
+
const records = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
const sha = entry.name;
|
|
77
|
+
if (!/^[a-f0-9]{64}$/.test(sha)) continue;
|
|
78
|
+
const record = await readRecord(root, sha);
|
|
79
|
+
if (record) records.push(record);
|
|
80
|
+
}
|
|
81
|
+
return records;
|
|
82
|
+
}
|
package/lib/knowledge/search.mjs
CHANGED
|
@@ -80,6 +80,18 @@ function buildSourceList(repoRoot = REPO_ROOT) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// User-ingested documents (PDF/Office/audio routed through docling + whisper)
|
|
84
|
+
const ingestRoot = join(repoRoot, '.cx', 'ingest');
|
|
85
|
+
if (existsSync(ingestRoot)) {
|
|
86
|
+
for (const entry of readdirSync(ingestRoot)) {
|
|
87
|
+
const dir = join(ingestRoot, entry);
|
|
88
|
+
const markdown = join(dir, 'markdown.md');
|
|
89
|
+
if (!/^[a-f0-9]{64}$/.test(entry)) continue;
|
|
90
|
+
if (!existsSync(markdown)) continue;
|
|
91
|
+
sources.push({ path: markdown, rel: relative(repoRoot, markdown), priority: 4, source: 'ingest' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
return sources;
|
|
84
96
|
}
|
|
85
97
|
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -116,7 +116,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
name: 'extract_document_text',
|
|
119
|
-
description: 'Extracts readable text from a local document path. Supports PDF
|
|
119
|
+
description: 'Extracts readable text from a local document path. High-fidelity backend (docling Python sidecar via uv, whisper.cpp for audio/video) is wired into `construct ingest`; this MCP path returns the legacy regex extraction for backwards compatibility. Supports PDF, DOCX, XLSX, PPTX, HTML, plain text, email, and transcripts.',
|
|
120
120
|
inputSchema: {
|
|
121
121
|
type: 'object',
|
|
122
122
|
properties: {
|
|
@@ -903,6 +903,147 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
903
903
|
},
|
|
904
904
|
},
|
|
905
905
|
},
|
|
906
|
+
{
|
|
907
|
+
name: 'workflow_init',
|
|
908
|
+
description: 'Initialize a new workflow for the current project. Creates plan.md state if not already present and returns the initial workflow envelope.',
|
|
909
|
+
inputSchema: {
|
|
910
|
+
type: 'object',
|
|
911
|
+
properties: {
|
|
912
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
913
|
+
title: { type: 'string', description: 'Workflow title shown in the plan header (default: "Untitled workflow").' },
|
|
914
|
+
spec_ref: { type: 'string', description: 'Optional reference to a spec/PRD/ADR id this workflow implements.' },
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
name: 'workflow_add_task',
|
|
920
|
+
description: 'Add a task to the current workflow. Pass `request` for intent-based routing (the classifier picks track + specialist) or pass explicit task fields (`key`, `title`, etc.) for manual entry.',
|
|
921
|
+
inputSchema: {
|
|
922
|
+
type: 'object',
|
|
923
|
+
properties: {
|
|
924
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
925
|
+
request: { type: 'string', description: 'Natural-language task request; when present, intent-based routing is used and the explicit fields below act as overrides.' },
|
|
926
|
+
key: { type: 'string', description: 'Stable task key (e.g. T-001). Generated when omitted.' },
|
|
927
|
+
title: { type: 'string', description: 'Short task title.' },
|
|
928
|
+
phase: { type: 'string', description: 'Phase bucket (plan, build, validate, ship, etc.).' },
|
|
929
|
+
owner: { type: 'string', description: 'Specialist or persona that owns the task.' },
|
|
930
|
+
files: { type: 'array', items: { type: 'string' }, description: 'File paths this task touches.' },
|
|
931
|
+
readFirst: { type: 'array', items: { type: 'string' }, description: 'Files the owner should read before editing.' },
|
|
932
|
+
doNotChange: { type: 'array', items: { type: 'string' }, description: 'Files/regions the owner must not modify.' },
|
|
933
|
+
acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Acceptance criteria as a checklist.' },
|
|
934
|
+
verification: { type: 'string', description: 'Command(s) or description of how to verify the task is done.' },
|
|
935
|
+
dependsOn: { type: 'array', items: { type: 'string' }, description: 'Task keys this task depends on.' },
|
|
936
|
+
overlays: { type: 'array', items: { type: 'string' }, description: 'Role flavors that augment the owner persona for this task.' },
|
|
937
|
+
challengeRequired: { type: 'boolean', description: 'Force a cx-devil-advocate challenge before the task can complete.' },
|
|
938
|
+
challengeStatus: { type: 'string', description: 'Initial challenge status when seeded.' },
|
|
939
|
+
tokenBudget: { type: 'number', description: 'Per-task token budget for cost tracking.' },
|
|
940
|
+
status: { type: 'string', description: 'Initial status override.' },
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: 'workflow_update_task',
|
|
946
|
+
description: 'Update fields on an existing workflow task. Requires the task `key`. Only fields supplied are changed.',
|
|
947
|
+
inputSchema: {
|
|
948
|
+
type: 'object',
|
|
949
|
+
required: ['key'],
|
|
950
|
+
properties: {
|
|
951
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
952
|
+
key: { type: 'string', description: 'Task key to update.' },
|
|
953
|
+
status: { type: 'string', description: 'New status (pending, in_progress, blocked_needs_user, blocked_by_dep, done, etc.).' },
|
|
954
|
+
owner: { type: 'string', description: 'New owner persona.' },
|
|
955
|
+
phase: { type: 'string', description: 'New phase bucket.' },
|
|
956
|
+
note: { type: 'string', description: 'Append-only progress note.' },
|
|
957
|
+
verification: { type: 'string', description: 'Updated verification description.' },
|
|
958
|
+
overlays: { type: 'array', items: { type: 'string' }, description: 'Replace the overlay list.' },
|
|
959
|
+
challengeRequired: { type: 'boolean', description: 'Toggle whether a challenge is required.' },
|
|
960
|
+
challengeStatus: { type: 'string', description: 'Update the challenge status (proposed, accepted, refused, etc.).' },
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
name: 'workflow_needs_main_input',
|
|
966
|
+
description: 'Mark a workflow task as blocked pending user input. Sets status to blocked_needs_user and writes a packet describing the blocker for the orchestrator to surface.',
|
|
967
|
+
inputSchema: {
|
|
968
|
+
type: 'object',
|
|
969
|
+
required: ['taskKey', 'blocker', 'question'],
|
|
970
|
+
properties: {
|
|
971
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
972
|
+
taskKey: { type: 'string', description: 'Task key to mark blocked.' },
|
|
973
|
+
worker: { type: 'string', description: 'Specialist that needs input (default: current owner).' },
|
|
974
|
+
blocker: { type: 'string', description: 'One-line description of what is blocking progress.' },
|
|
975
|
+
question: { type: 'string', description: 'The specific question to put to the user.' },
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
name: 'workflow_validate',
|
|
981
|
+
description: 'Validate the current workflow state against the schema and run consistency checks (no orphan tasks, no circular dependencies, every owner resolves to a known persona).',
|
|
982
|
+
inputSchema: {
|
|
983
|
+
type: 'object',
|
|
984
|
+
properties: {
|
|
985
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: 'workflow_status',
|
|
991
|
+
description: 'Return the full workflow snapshot for the current project: tasks, summary, alignment health, and the public-health surface used by the dashboard.',
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: 'object',
|
|
994
|
+
properties: {
|
|
995
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
name: 'workflow_contract_validate',
|
|
1001
|
+
description: 'Validate a producer→consumer handoff against specialists/contracts.json. Required when a specialist hands off to another role: enforces input.mustContain, output schema, disk-artifact postconditions, and binary postconditions per producer (rubber-stamp prevention, post-hoc threat-model prevention, etc.). Self-enforcing: a producer with binary rules MUST pass `packet`, or the call itself is a contract violation.',
|
|
1002
|
+
inputSchema: {
|
|
1003
|
+
type: 'object',
|
|
1004
|
+
required: ['producer', 'consumer'],
|
|
1005
|
+
properties: {
|
|
1006
|
+
producer: { type: 'string', description: 'Producer agent or persona name (e.g. cx-reviewer, cx-security).' },
|
|
1007
|
+
consumer: { type: 'string', description: 'Consumer agent or persona name receiving the handoff.' },
|
|
1008
|
+
id: { type: 'string', description: 'Optional contract id; overrides producer/consumer lookup.' },
|
|
1009
|
+
artifact: { type: 'object', description: 'The handoff payload to validate against the contract schema and disk-artifact postconditions.' },
|
|
1010
|
+
packet: { type: 'object', description: 'The producer\'s in-memory output packet. REQUIRED when the producer has binary postconditions; omitting it is itself a contract violation.' },
|
|
1011
|
+
enforcement: { type: 'string', enum: ['block', 'warn'], description: 'Enforcement mode (default: block). Use warn only when explicitly advisory.' },
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: 'workflow_import_plan',
|
|
1017
|
+
description: 'Bulk-add tasks from a markdown plan to the current workflow. Parses headings and bullet structure to extract task titles, owners, and acceptance criteria.',
|
|
1018
|
+
inputSchema: {
|
|
1019
|
+
type: 'object',
|
|
1020
|
+
required: ['markdown'],
|
|
1021
|
+
properties: {
|
|
1022
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
1023
|
+
markdown: { type: 'string', description: 'Plan markdown to parse.' },
|
|
1024
|
+
phase: { type: 'string', description: 'Phase bucket applied to all imported tasks.' },
|
|
1025
|
+
owner: { type: 'string', description: 'Default owner for tasks that do not specify one.' },
|
|
1026
|
+
readFirst: { type: 'array', items: { type: 'string' }, description: 'Default readFirst files applied to all imported tasks.' },
|
|
1027
|
+
doNotChange: { type: 'array', items: { type: 'string' }, description: 'Default doNotChange files applied to all imported tasks.' },
|
|
1028
|
+
acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Default acceptance criteria applied to all imported tasks.' },
|
|
1029
|
+
title: { type: 'string', description: 'Workflow title to set if the workflow is newly created.' },
|
|
1030
|
+
spec_ref: { type: 'string', description: 'Spec reference to associate with the workflow.' },
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
name: 'cx_trace_telemetry',
|
|
1036
|
+
description: 'Record a single CX telemetry trace for an agent invocation. Use to log start/end, model used, token cost, and outcome verdict for performance review.',
|
|
1037
|
+
inputSchema: {
|
|
1038
|
+
type: 'object',
|
|
1039
|
+
required: ['agent', 'trace'],
|
|
1040
|
+
properties: {
|
|
1041
|
+
agent: { type: 'string', description: 'Agent or persona name being traced.' },
|
|
1042
|
+
trace: { type: 'object', description: 'Trace record: start_ts, end_ts, model, tokens, verdict, notes, etc.' },
|
|
1043
|
+
cwd: { type: 'string', description: 'Project root (default: server cwd).' },
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
906
1047
|
],
|
|
907
1048
|
}));
|
|
908
1049
|
|
package/lib/mcp/tools/skills.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { routeRequest, routeRequestVerified, requiresExecutiveApproval, TERMINAL_STATES } from '../../orchestration-policy.mjs';
|
|
12
12
|
import { buildTaskPacketFromIntent } from '../../workflow-state.mjs';
|
|
13
13
|
import { logSkillCall } from '../../telemetry/skill-calls.mjs';
|
|
@@ -73,12 +73,15 @@ export function searchSkills(args, { ROOT_DIR }) {
|
|
|
73
73
|
if (!pattern) return { error: 'Missing pattern argument' };
|
|
74
74
|
const skillsDir = join(ROOT_DIR, 'skills');
|
|
75
75
|
|
|
76
|
+
// argv-array form: pattern is a discrete argument, never interpreted by a
|
|
77
|
+
// shell. MCP tool inputs are model-controlled and must not reach a shell.
|
|
78
|
+
|
|
76
79
|
try {
|
|
77
|
-
const output =
|
|
80
|
+
const output = execFileSync('rg', ['-i', pattern, skillsDir], { encoding: 'utf8' });
|
|
78
81
|
return { results: output.split('\n').filter(Boolean) };
|
|
79
82
|
} catch {
|
|
80
83
|
try {
|
|
81
|
-
const output =
|
|
84
|
+
const output = execFileSync('grep', ['-ri', pattern, skillsDir], { encoding: 'utf8' });
|
|
82
85
|
return { results: output.split('\n').filter(Boolean) };
|
|
83
86
|
} catch {
|
|
84
87
|
return { results: [], note: 'No matches found or grep error' };
|
|
@@ -104,7 +104,10 @@ export function workflowValidate(args) {
|
|
|
104
104
|
* consumer — agent or persona name receiving it
|
|
105
105
|
* id — optional contract id (overrides producer/consumer lookup)
|
|
106
106
|
* artifact — the handoff payload to validate
|
|
107
|
-
*
|
|
107
|
+
* packet — the producer's in-memory output packet. Required when the
|
|
108
|
+
* producer has binary postconditions; omitting it is itself
|
|
109
|
+
* a contract violation.
|
|
110
|
+
* enforcement — 'block' (default) or 'warn'; 'block' returns ok:false on
|
|
108
111
|
* violation so the workflow can refuse to advance
|
|
109
112
|
*
|
|
110
113
|
* Returns { ok, status?, errors?, warnings?, contract } where status is set
|
|
@@ -117,7 +120,8 @@ export async function workflowContractValidate(args) {
|
|
|
117
120
|
consumer: args.consumer,
|
|
118
121
|
id: args.id,
|
|
119
122
|
artifact: args.artifact,
|
|
120
|
-
|
|
123
|
+
packet: args.packet,
|
|
124
|
+
enforcement: args.enforcement || 'block',
|
|
121
125
|
});
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -112,10 +112,12 @@ function logCapDrop(rootDir, kind, dropped, total) {
|
|
|
112
112
|
function writeIndex(rootDir, entries) {
|
|
113
113
|
ensureDir(obsDir(rootDir), rootDir);
|
|
114
114
|
const trimmed = entries.slice(0, MAX_INDEX);
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const dropped = Math.max(0, entries.length - MAX_INDEX);
|
|
116
|
+
if (dropped > 0) {
|
|
117
|
+
logCapDrop(rootDir, 'observation-index', dropped, entries.length);
|
|
117
118
|
}
|
|
118
119
|
fs.writeFileSync(indexPath(rootDir), JSON.stringify(trimmed, null, 2) + '\n');
|
|
120
|
+
return { dropped };
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
function readVectors(rootDir) {
|
|
@@ -130,10 +132,12 @@ function readVectors(rootDir) {
|
|
|
130
132
|
|
|
131
133
|
function writeVectors(rootDir, records) {
|
|
132
134
|
ensureDir(obsDir(rootDir), rootDir);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
const dropped = Math.max(0, records.length - MAX_INDEX);
|
|
136
|
+
if (dropped > 0) {
|
|
137
|
+
logCapDrop(rootDir, 'observation-vectors', dropped, records.length);
|
|
135
138
|
}
|
|
136
139
|
fs.writeFileSync(vectorsPath(rootDir), JSON.stringify(records, null, 2) + '\n');
|
|
140
|
+
return { dropped };
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
/**
|
|
@@ -188,6 +192,7 @@ export async function addObservation(rootDir, {
|
|
|
188
192
|
JSON.stringify(record, null, 2) + '\n',
|
|
189
193
|
);
|
|
190
194
|
|
|
195
|
+
let indexDropped = 0;
|
|
191
196
|
withFileLockSync(indexPath(rootDir), () => {
|
|
192
197
|
const index = readIndex(rootDir);
|
|
193
198
|
index.unshift({
|
|
@@ -198,8 +203,12 @@ export async function addObservation(rootDir, {
|
|
|
198
203
|
project: record.project,
|
|
199
204
|
createdAt: now,
|
|
200
205
|
});
|
|
201
|
-
writeIndex(rootDir, index);
|
|
206
|
+
indexDropped = writeIndex(rootDir, index).dropped;
|
|
202
207
|
});
|
|
208
|
+
if (indexDropped > 0) {
|
|
209
|
+
record.capDropped = indexDropped;
|
|
210
|
+
process.stderr.write(`[observation-store] observation cap reached: ${indexDropped} oldest entries evicted (cap=${MAX_INDEX})\n`);
|
|
211
|
+
}
|
|
203
212
|
|
|
204
213
|
// Try SQL storage with neural embeddings
|
|
205
214
|
try {
|