@geraldmaron/construct 1.0.12 → 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 +14 -2
- package/bin/construct +262 -14
- package/bin/construct-postinstall.mjs +23 -2
- 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/auto-docs.mjs +11 -7
- 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/dashboard-static.mjs +7 -4
- package/lib/doc-stamp.mjs +16 -0
- package/lib/docs-verify.mjs +1 -8
- package/lib/doctor/watchers/bd-watch.mjs +6 -2
- 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 +26 -1
- package/lib/embed/docs-lifecycle.mjs +19 -0
- package/lib/embed/inbox.mjs +85 -2
- package/lib/embed/recommendation-store.mjs +29 -0
- package/lib/flavors/loader.mjs +1 -1
- package/lib/gates-audit.mjs +18 -12
- package/lib/handoffs/contract.mjs +2 -2
- package/lib/hooks/_lib/input.mjs +52 -0
- package/lib/hooks/adaptive-lint.mjs +4 -0
- package/lib/hooks/agent-tracker.mjs +59 -15
- package/lib/hooks/audit-reads.mjs +83 -42
- package/lib/hooks/audit-trail.mjs +28 -18
- package/lib/hooks/bash-output-logger.mjs +8 -2
- package/lib/hooks/block-no-verify.mjs +4 -0
- package/lib/hooks/ci-status-check.mjs +4 -0
- package/lib/hooks/comment-lint.mjs +6 -4
- package/lib/hooks/config-protection.mjs +4 -0
- package/lib/hooks/context-watch.mjs +4 -0
- package/lib/hooks/context-window-recovery.mjs +4 -0
- package/lib/hooks/dep-audit.mjs +12 -5
- package/lib/hooks/doc-coupling-check.mjs +5 -1
- package/lib/hooks/edit-accumulator.mjs +25 -10
- package/lib/hooks/edit-error-recovery.mjs +4 -0
- package/lib/hooks/edit-guard.mjs +4 -0
- package/lib/hooks/guard-bash.mjs +4 -0
- package/lib/hooks/mcp-audit.mjs +4 -0
- package/lib/hooks/mcp-health-check.mjs +4 -0
- package/lib/hooks/model-fallback.mjs +7 -11
- package/lib/hooks/policy-engine.mjs +4 -0
- package/lib/hooks/post-merge-docs-check.mjs +4 -0
- package/lib/hooks/post-merge-tracking.mjs +82 -0
- package/lib/hooks/pre-compact.mjs +4 -0
- package/lib/hooks/pre-push-gate.mjs +84 -231
- package/lib/hooks/proactive-activation.mjs +5 -2
- package/lib/hooks/readme-age-check.mjs +4 -0
- package/lib/hooks/registry-sync.mjs +35 -20
- package/lib/hooks/rule-verifier.mjs +3 -0
- package/lib/hooks/scan-secrets.mjs +4 -0
- package/lib/hooks/session-optimize.mjs +4 -0
- package/lib/hooks/session-reflect.mjs +4 -0
- package/lib/hooks/session-start.mjs +49 -2
- package/lib/hooks/session-tracking-refresh.mjs +70 -0
- package/lib/hooks/stop-notify.mjs +13 -2
- package/lib/hooks/stop-typecheck.mjs +4 -0
- package/lib/hooks/test-watch.mjs +8 -4
- package/lib/ingest/chunker.mjs +94 -0
- package/lib/ingest/pipeline.mjs +53 -0
- package/lib/ingest/store.mjs +82 -0
- package/lib/init-unified.mjs +90 -23
- package/lib/intake/attribution.mjs +77 -0
- package/lib/intake/intake-config.mjs +3 -0
- package/lib/intake/manifest.mjs +107 -0
- package/lib/intake/poll-lock.mjs +136 -0
- package/lib/intake/prepare.mjs +42 -4
- package/lib/knowledge/search.mjs +12 -0
- package/lib/logging/rotate.mjs +394 -0
- package/lib/mcp/server.mjs +142 -1
- package/lib/mcp/tools/project.mjs +2 -2
- package/lib/mcp/tools/skills.mjs +6 -3
- package/lib/mcp/tools/telemetry.mjs +1 -1
- package/lib/mcp/tools/workflow.mjs +6 -2
- package/lib/observation-store.mjs +14 -5
- package/lib/ollama-manager.mjs +32 -28
- package/lib/opencode-config.mjs +10 -3
- package/lib/orchestration/routing-tables.mjs +176 -0
- package/lib/orchestration-policy.mjs +18 -106
- package/lib/parity.mjs +48 -7
- package/lib/profiles/lifecycle.mjs +19 -1
- package/lib/project-init-shared.mjs +11 -5
- package/lib/project-root.mjs +104 -0
- package/lib/prompt-composer.js +11 -1
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/event-bus.mjs +29 -9
- package/lib/roles/router.mjs +8 -7
- package/lib/runtime/uv-bootstrap.mjs +129 -0
- package/lib/runtime/whisper-bootstrap.mjs +102 -0
- package/lib/server/index.mjs +53 -10
- package/lib/server/static/index.html +1 -15
- package/lib/specialists/postconditions.mjs +1 -1
- package/lib/status.mjs +1 -1
- package/lib/storage/backup.mjs +2 -2
- package/lib/sync/skill-frontmatter.mjs +113 -21
- package/lib/telemetry/intent-verifications.mjs +16 -3
- package/lib/telemetry/skill-calls.mjs +12 -3
- package/lib/tracking-surfaces.mjs +377 -0
- package/lib/validators/skills.mjs +86 -54
- package/lib/worker/trace.mjs +19 -2
- package/package.json +10 -6
- package/personas/construct.md +4 -7
- package/platforms/claude/settings.template.json +29 -28
- 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/scripts/sync-specialists.mjs +269 -75
- 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/specialists/registry.json +158 -13
- package/lib/hooks/env-check.mjs +0 -83
- package/lib/hooks/read-tracker.mjs +0 -61
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/server/static/assets/index-ab25c707.js +0 -70
- package/lib/server/static/assets/index-f0c80a2b.css +0 -1
- package/lib/specialist-contracts-enforce.mjs +0 -158
- package/rules/common/agents.md +0 -28
- package/rules/common/development-workflow.md +0 -32
- package/rules/common/performance.md +0 -55
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/logging/rotate.mjs — shared file-rotation primitive.
|
|
3
|
+
*
|
|
4
|
+
* Two consumers need bounded log files: the trace writer
|
|
5
|
+
* (`.cx/traces/<date>.jsonl`, capped to avoid >100MB single-file commits
|
|
6
|
+
* that GitHub rejects) and the embed daemon stdout log
|
|
7
|
+
* (`~/.cx/runtime/embed-daemon.log`, capped so a stuck "Telemetry skipped"
|
|
8
|
+
* message can't fill the disk).
|
|
9
|
+
*
|
|
10
|
+
* `appendWithRotation` is for code paths that own the writes (the trace
|
|
11
|
+
* writer). `rotateIfOversized` is for externally-written files where Construct
|
|
12
|
+
* code only polls (the OS-supervisor stdout redirect — Construct never writes
|
|
13
|
+
* the embed log directly).
|
|
14
|
+
*
|
|
15
|
+
* Rotated segments are named `<base>.<n><ext>` where `n` starts at 1 and
|
|
16
|
+
* counts upward; with `gzip: true` segments get a `.gz` suffix and are
|
|
17
|
+
* compressed lazily on rotation. `maxSegments` (when set) drops the oldest
|
|
18
|
+
* `n>=maxSegments` segments on every rotation so the directory stays bounded.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, statSync, renameSync, readdirSync, readFileSync, unlinkSync, appendFileSync, createReadStream, createWriteStream, mkdirSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { createGzip, gunzipSync } from 'node:zlib';
|
|
24
|
+
import { pipeline } from 'node:stream/promises';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse `<base>.<ext>` from a full path. `app.log` → base=app, ext=.log.
|
|
28
|
+
* `2026-05-28.jsonl` → base=2026-05-28, ext=.jsonl.
|
|
29
|
+
*/
|
|
30
|
+
function splitBase(filePath) {
|
|
31
|
+
const dir = path.dirname(filePath);
|
|
32
|
+
const ext = path.extname(filePath);
|
|
33
|
+
const base = path.basename(filePath, ext);
|
|
34
|
+
return { dir, base, ext };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Enumerate existing rotated segments for a given file, sorted ascending by
|
|
39
|
+
* segment index. Includes both compressed and uncompressed forms.
|
|
40
|
+
*/
|
|
41
|
+
function listSegments(filePath) {
|
|
42
|
+
const { dir, base, ext } = splitBase(filePath);
|
|
43
|
+
if (!existsSync(dir)) return [];
|
|
44
|
+
|
|
45
|
+
// Pattern: <base>.<n>[<ext>][.gz]. Match any segment that starts with base
|
|
46
|
+
// and a digit suffix; tolerate both `.1.jsonl` and `.1.jsonl.gz`.
|
|
47
|
+
|
|
48
|
+
const pattern = new RegExp(`^${escapeRegex(base)}\\.(\\d+)${escapeRegex(ext)}(\\.gz)?$`);
|
|
49
|
+
const entries = readdirSync(dir)
|
|
50
|
+
.map((name) => {
|
|
51
|
+
const m = name.match(pattern);
|
|
52
|
+
return m ? { name, index: Number(m[1]), gzipped: !!m[2] } : null;
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.sort((a, b) => a.index - b.index);
|
|
56
|
+
return entries.map((e) => ({ ...e, fullPath: path.join(dir, e.name) }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeRegex(s) {
|
|
60
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rename current → next-segment, optionally gzipping the previous tail. Old
|
|
65
|
+
* segments past `maxSegments` are deleted. Returns the new segment path or
|
|
66
|
+
* null when no rotation happened.
|
|
67
|
+
*/
|
|
68
|
+
export async function rotateIfOversized(filePath, { maxBytes, maxSegments = 0, gzip = false } = {}) {
|
|
69
|
+
if (!maxBytes || maxBytes <= 0) return null;
|
|
70
|
+
if (!existsSync(filePath)) return null;
|
|
71
|
+
|
|
72
|
+
let stat;
|
|
73
|
+
try { stat = statSync(filePath); }
|
|
74
|
+
catch { return null; }
|
|
75
|
+
if (!stat.isFile() || stat.size < maxBytes) return null;
|
|
76
|
+
|
|
77
|
+
const { dir, base, ext } = splitBase(filePath);
|
|
78
|
+
const existing = listSegments(filePath);
|
|
79
|
+
const nextIndex = (existing[existing.length - 1]?.index ?? 0) + 1;
|
|
80
|
+
|
|
81
|
+
// Move current → <base>.<nextIndex><ext>. Then optionally gzip the rotated
|
|
82
|
+
// file. Doing the rename first means a concurrent writer that reopens the
|
|
83
|
+
// path lands on a fresh file with no data loss.
|
|
84
|
+
|
|
85
|
+
const rotatedPlain = path.join(dir, `${base}.${nextIndex}${ext}`);
|
|
86
|
+
renameSync(filePath, rotatedPlain);
|
|
87
|
+
|
|
88
|
+
let finalPath = rotatedPlain;
|
|
89
|
+
if (gzip) {
|
|
90
|
+
finalPath = `${rotatedPlain}.gz`;
|
|
91
|
+
try {
|
|
92
|
+
await pipeline(createReadStream(rotatedPlain), createGzip(), createWriteStream(finalPath));
|
|
93
|
+
try { unlinkSync(rotatedPlain); } catch { /* already cleaned up */ }
|
|
94
|
+
} catch {
|
|
95
|
+
// Compression failed; leave the plain rotated segment in place rather
|
|
96
|
+
// than losing data.
|
|
97
|
+
|
|
98
|
+
finalPath = rotatedPlain;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pruneSegments(filePath, maxSegments);
|
|
103
|
+
return finalPath;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Drop oldest segments past `maxSegments`. With maxSegments=0, keep every
|
|
108
|
+
* segment (no pruning). Always preserves the active file (which has no
|
|
109
|
+
* numeric suffix and is therefore not in the segment list).
|
|
110
|
+
*/
|
|
111
|
+
export function pruneSegments(filePath, maxSegments = 0) {
|
|
112
|
+
if (!maxSegments || maxSegments <= 0) return [];
|
|
113
|
+
const segments = listSegments(filePath);
|
|
114
|
+
if (segments.length <= maxSegments) return [];
|
|
115
|
+
const drop = segments.slice(0, segments.length - maxSegments);
|
|
116
|
+
for (const seg of drop) {
|
|
117
|
+
try { unlinkSync(seg.fullPath); } catch { /* already gone */ }
|
|
118
|
+
}
|
|
119
|
+
return drop.map((s) => s.fullPath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Append `line` to `filePath`, rotating beforehand if the file would exceed
|
|
124
|
+
* `maxBytes`. Creates the parent directory on first append.
|
|
125
|
+
*
|
|
126
|
+
* Synchronous + best-effort gzip. The cost of compressing on rotation is
|
|
127
|
+
* amortized over `maxBytes` worth of writes, so the rotation tail rarely
|
|
128
|
+
* blocks the calling code path. When gzip:false rotation is just a rename.
|
|
129
|
+
*/
|
|
130
|
+
export async function appendWithRotation(filePath, line, { maxBytes, maxSegments = 0, gzip = false } = {}) {
|
|
131
|
+
const dir = path.dirname(filePath);
|
|
132
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
133
|
+
|
|
134
|
+
if (maxBytes && maxBytes > 0 && existsSync(filePath)) {
|
|
135
|
+
try {
|
|
136
|
+
const stat = statSync(filePath);
|
|
137
|
+
// Rotate when this append would cross the cap, not after — keeps every
|
|
138
|
+
// single segment strictly under `maxBytes`.
|
|
139
|
+
|
|
140
|
+
if (stat.isFile() && stat.size + Buffer.byteLength(line) >= maxBytes) {
|
|
141
|
+
await rotateIfOversized(filePath, { maxBytes: 0, maxSegments, gzip });
|
|
142
|
+
}
|
|
143
|
+
} catch { /* fall through to plain append */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
appendFileSync(filePath, line);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sync variant for hot paths that can't `await` rotation. Skips gzip (which
|
|
151
|
+
* requires streams) but still renames + prunes. Use this in trace writers and
|
|
152
|
+
* other latency-sensitive append loops.
|
|
153
|
+
*/
|
|
154
|
+
export function appendWithRotationSync(filePath, line, { maxBytes, maxSegments = 0 } = {}) {
|
|
155
|
+
const dir = path.dirname(filePath);
|
|
156
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
157
|
+
|
|
158
|
+
if (maxBytes && maxBytes > 0 && existsSync(filePath)) {
|
|
159
|
+
try {
|
|
160
|
+
const stat = statSync(filePath);
|
|
161
|
+
if (stat.isFile() && stat.size + Buffer.byteLength(line) >= maxBytes) {
|
|
162
|
+
const { dir: d, base, ext } = splitBase(filePath);
|
|
163
|
+
const existing = listSegments(filePath);
|
|
164
|
+
const nextIndex = (existing[existing.length - 1]?.index ?? 0) + 1;
|
|
165
|
+
renameSync(filePath, path.join(d, `${base}.${nextIndex}${ext}`));
|
|
166
|
+
pruneSegments(filePath, maxSegments);
|
|
167
|
+
}
|
|
168
|
+
} catch { /* fall through */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
appendFileSync(filePath, line);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Per-channel resource limits. Every Construct logger has a documented
|
|
176
|
+
* cap here so disk usage is bounded by design, not by hope. New loggers
|
|
177
|
+
* MUST be registered; the `appendBounded` helper refuses to write to an
|
|
178
|
+
* unregistered channel.
|
|
179
|
+
*
|
|
180
|
+
* Sizes are conservative defaults chosen for a single-developer machine.
|
|
181
|
+
* Override via env var on a per-channel basis when the workload genuinely
|
|
182
|
+
* exceeds these (CI runners, multi-tenant team installs, long-lived
|
|
183
|
+
* background daemons).
|
|
184
|
+
*/
|
|
185
|
+
export const LIMITS = {
|
|
186
|
+
// Trace shards under .cx/traces/<date>.jsonl — capped below GitHub's
|
|
187
|
+
// 100 MB single-file ceiling. Bead construct-1vv5.
|
|
188
|
+
|
|
189
|
+
trace: {
|
|
190
|
+
maxBytes: 100 * 1024 * 1024,
|
|
191
|
+
maxSegments: 0, // keep all history; rotation is for the size cap, not retention
|
|
192
|
+
gzip: false,
|
|
193
|
+
envOverride: 'CONSTRUCT_TRACE_MAX_MB',
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// OS-supervised stdout log at ~/.cx/runtime/embed-daemon.log. Bead
|
|
197
|
+
// construct-88i. Rotation is poll-style via the daemon scheduler.
|
|
198
|
+
|
|
199
|
+
'embed-daemon-log': {
|
|
200
|
+
maxBytes: 50 * 1024 * 1024,
|
|
201
|
+
maxSegments: 5,
|
|
202
|
+
gzip: true,
|
|
203
|
+
envOverride: 'CONSTRUCT_EMBED_LOG_MAX_MB',
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Per-edit audit of file reads. High traffic in active sessions.
|
|
207
|
+
// ~/.cx/audit-reads.jsonl.
|
|
208
|
+
|
|
209
|
+
'audit-reads': {
|
|
210
|
+
maxBytes: 25 * 1024 * 1024,
|
|
211
|
+
maxSegments: 4,
|
|
212
|
+
gzip: true,
|
|
213
|
+
envOverride: 'CONSTRUCT_AUDIT_READS_MAX_MB',
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
// Per-skill-call telemetry. ~/.cx/skill-calls.jsonl.
|
|
217
|
+
|
|
218
|
+
'skill-calls': {
|
|
219
|
+
maxBytes: 25 * 1024 * 1024,
|
|
220
|
+
maxSegments: 4,
|
|
221
|
+
gzip: true,
|
|
222
|
+
envOverride: 'CONSTRUCT_SKILL_CALLS_MAX_MB',
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// Agent-dispatch log written by `lib/hooks/agent-tracker.mjs`. Path: ~/.cx/agent-log.jsonl.
|
|
226
|
+
|
|
227
|
+
'agent-log': {
|
|
228
|
+
maxBytes: 25 * 1024 * 1024,
|
|
229
|
+
maxSegments: 4,
|
|
230
|
+
gzip: true,
|
|
231
|
+
envOverride: 'CONSTRUCT_AGENT_LOG_MAX_MB',
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// Pending role invocations across all projects. ~/.cx/role-pending.jsonl.
|
|
235
|
+
|
|
236
|
+
'role-pending': {
|
|
237
|
+
maxBytes: 10 * 1024 * 1024,
|
|
238
|
+
maxSegments: 2,
|
|
239
|
+
gzip: true,
|
|
240
|
+
envOverride: 'CONSTRUCT_ROLE_PENDING_MAX_MB',
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
// Intent verifications. ~/.cx/intent-verifications.jsonl.
|
|
244
|
+
|
|
245
|
+
'intent-verifications': {
|
|
246
|
+
maxBytes: 10 * 1024 * 1024,
|
|
247
|
+
maxSegments: 2,
|
|
248
|
+
gzip: true,
|
|
249
|
+
envOverride: 'CONSTRUCT_INTENT_VERIFICATIONS_MAX_MB',
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
// Contract postcondition violations. ~/.cx/contract-violations.jsonl.
|
|
253
|
+
|
|
254
|
+
'contract-violations': {
|
|
255
|
+
maxBytes: 10 * 1024 * 1024,
|
|
256
|
+
maxSegments: 2,
|
|
257
|
+
gzip: true,
|
|
258
|
+
envOverride: 'CONSTRUCT_CONTRACT_VIOLATIONS_MAX_MB',
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
// Bash-output warning flags appended by the bash-output-logger hook on
|
|
262
|
+
// every Bash tool use over the size threshold. ~/.cx/warn-flags.txt.
|
|
263
|
+
|
|
264
|
+
'bash-warn-flags': {
|
|
265
|
+
maxBytes: 5 * 1024 * 1024,
|
|
266
|
+
maxSegments: 2,
|
|
267
|
+
gzip: false,
|
|
268
|
+
envOverride: 'CONSTRUCT_BASH_WARN_FLAGS_MAX_MB',
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// Per-turn cost ledger written by the Stop hook. Cross-project (so the
|
|
272
|
+
// user has one place to see spend across every project) — each entry
|
|
273
|
+
// carries a projectId tag so readers can split by project. Path:
|
|
274
|
+
// ~/.cx/session-cost.jsonl.
|
|
275
|
+
|
|
276
|
+
'session-cost': {
|
|
277
|
+
maxBytes: 25 * 1024 * 1024,
|
|
278
|
+
maxSegments: 4,
|
|
279
|
+
gzip: true,
|
|
280
|
+
envOverride: 'CONSTRUCT_SESSION_COST_MAX_MB',
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// Tamper-evident audit trail of every mutation Construct (or a dispatched
|
|
284
|
+
// subagent) makes. Project-scoped. Path: <project>/.cx/audit-trail.jsonl.
|
|
285
|
+
|
|
286
|
+
'audit-trail': {
|
|
287
|
+
maxBytes: 50 * 1024 * 1024,
|
|
288
|
+
maxSegments: 4,
|
|
289
|
+
gzip: true,
|
|
290
|
+
envOverride: 'CONSTRUCT_AUDIT_TRAIL_MAX_MB',
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
// Pending typecheck queue written by the edit-accumulator hook on every
|
|
294
|
+
// Edit/Write of a TS/JS file. Path: ~/.cx/pending-typecheck.txt.
|
|
295
|
+
|
|
296
|
+
'edit-accumulator': {
|
|
297
|
+
maxBytes: 5 * 1024 * 1024,
|
|
298
|
+
maxSegments: 2,
|
|
299
|
+
gzip: false,
|
|
300
|
+
envOverride: 'CONSTRUCT_EDIT_ACCUMULATOR_MAX_MB',
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Resolve the effective cap for a channel, honoring env-var overrides
|
|
306
|
+
* (interpreted as megabytes). Returns the byte budget; 0 disables rotation.
|
|
307
|
+
*/
|
|
308
|
+
function resolveCap(channel, env = process.env) {
|
|
309
|
+
const def = LIMITS[channel];
|
|
310
|
+
if (!def) throw new Error(`appendBounded: unknown channel "${channel}". Register it in lib/logging/rotate.mjs#LIMITS.`);
|
|
311
|
+
const raw = def.envOverride ? env[def.envOverride] : undefined;
|
|
312
|
+
if (raw === undefined) return def.maxBytes;
|
|
313
|
+
const mb = Number(raw);
|
|
314
|
+
if (!Number.isFinite(mb) || mb < 0) return def.maxBytes;
|
|
315
|
+
return Math.floor(mb * 1024 * 1024);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Append a line to `filePath` with rotation governed by the named channel's
|
|
320
|
+
* registered limit. Synchronous; renames the active file on overflow and
|
|
321
|
+
* prunes oldest segments past the channel's maxSegments. The registry
|
|
322
|
+
* guarantees nothing grows unbounded — every logger appending to a known
|
|
323
|
+
* channel inherits its documented disk budget.
|
|
324
|
+
*/
|
|
325
|
+
export function appendBounded(channel, filePath, line, env = process.env) {
|
|
326
|
+
const def = LIMITS[channel];
|
|
327
|
+
if (!def) throw new Error(`appendBounded: unknown channel "${channel}". Register it in lib/logging/rotate.mjs#LIMITS.`);
|
|
328
|
+
const maxBytes = resolveCap(channel, env);
|
|
329
|
+
return appendWithRotationSync(filePath, line, {
|
|
330
|
+
maxBytes,
|
|
331
|
+
maxSegments: def.maxSegments,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Last non-empty line written under the channel, looking across the active
|
|
337
|
+
* file and the rotated segments. Resolves the right value for chain-hash
|
|
338
|
+
* fields (`prev_line_hash`) on the first write after a rotation, where the
|
|
339
|
+
* active file is empty but the chain head lives at the tail of the most
|
|
340
|
+
* recent rotated segment (`<base>.<n><ext>` with the highest `n`, possibly
|
|
341
|
+
* `.gz`-suffixed).
|
|
342
|
+
*
|
|
343
|
+
* Sync. Reads the WHOLE most-recent gzipped segment in memory when it has
|
|
344
|
+
* to fall back to one; segment caps in LIMITS keep the worst case bounded
|
|
345
|
+
* (audit-trail tops out at 50 MB raw, ~5–10 MB gzipped).
|
|
346
|
+
*
|
|
347
|
+
* Returns the line WITHOUT its trailing newline, or null when no data has
|
|
348
|
+
* ever been written for the channel.
|
|
349
|
+
*/
|
|
350
|
+
export function readLastLineAcrossSegments(filePath) {
|
|
351
|
+
if (existsSync(filePath)) {
|
|
352
|
+
const lastFromActive = readLastLineOfFile(filePath);
|
|
353
|
+
if (lastFromActive !== null) return lastFromActive;
|
|
354
|
+
}
|
|
355
|
+
const segments = listSegments(filePath);
|
|
356
|
+
if (segments.length === 0) return null;
|
|
357
|
+
const newest = segments[segments.length - 1];
|
|
358
|
+
try {
|
|
359
|
+
const raw = readFileSync(newest.fullPath);
|
|
360
|
+
const decoded = newest.gzipped ? gunzipSync(raw) : raw;
|
|
361
|
+
return lastLineOfBuffer(decoded);
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function lastLineOfBuffer(buf) {
|
|
368
|
+
let end = buf.length;
|
|
369
|
+
while (end > 0 && buf[end - 1] === 0x0a) end -= 1;
|
|
370
|
+
if (end === 0) return null;
|
|
371
|
+
let start = end - 1;
|
|
372
|
+
while (start > 0 && buf[start - 1] !== 0x0a) start -= 1;
|
|
373
|
+
return buf.slice(start, end).toString('utf8');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function readLastLineOfFile(filePath) {
|
|
377
|
+
try {
|
|
378
|
+
if (statSync(filePath).size === 0) return null;
|
|
379
|
+
|
|
380
|
+
// Read the whole file and walk back to the last newline boundary. A
|
|
381
|
+
// bounded tail read (e.g. last 8 KB) would truncate JSONL records that
|
|
382
|
+
// exceed the window — the audit-trail's `content_hash` field can put a
|
|
383
|
+
// record over 50 KB — and a truncated string would break the
|
|
384
|
+
// prev_line_hash chain it feeds. Per-channel size caps in LIMITS bound
|
|
385
|
+
// the worst-case allocation here.
|
|
386
|
+
|
|
387
|
+
return lastLineOfBuffer(readFileSync(filePath));
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Re-export for convenient testing. */
|
|
394
|
+
export const __test = { listSegments, splitBase, resolveCap, readLastLineOfFile };
|
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
|
|
|
@@ -281,7 +281,7 @@ export function projectContext(args, { ROOT_DIR }) {
|
|
|
281
281
|
const has_context_file = contextInspection.hasFile;
|
|
282
282
|
const workflow = loadWorkflow(cwd);
|
|
283
283
|
const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
|
|
284
|
-
const registry = readJSON(join(ROOT_DIR, '
|
|
284
|
+
const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
|
|
285
285
|
const executionContractModel = resolveExecutionContractModelMetadata({
|
|
286
286
|
envValues: mergedEnv,
|
|
287
287
|
registryModels: registry.models ?? {},
|
|
@@ -323,7 +323,7 @@ export function workflowStatus(args, { ROOT_DIR }) {
|
|
|
323
323
|
const workflow = loadWorkflow(cwd);
|
|
324
324
|
const workflowHealth = inspectWorkflowHealth(workflow, { cwd });
|
|
325
325
|
const mergedEnv = loadConstructEnv({ rootDir: ROOT_DIR, homeDir: homedir(), env: process.env });
|
|
326
|
-
const registry = readJSON(join(ROOT_DIR, '
|
|
326
|
+
const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
|
|
327
327
|
const executionContractModel = resolveExecutionContractModelMetadata({
|
|
328
328
|
envValues: mergedEnv,
|
|
329
329
|
registryModels: registry.models ?? {},
|
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' };
|
|
@@ -70,7 +70,7 @@ function resolveSessionContext() {
|
|
|
70
70
|
|
|
71
71
|
export async function cxTrace(args, { ROOT_DIR }) {
|
|
72
72
|
const ctx = resolveSessionContext();
|
|
73
|
-
const registry = readJSON(join(ROOT_DIR, '
|
|
73
|
+
const registry = readJSON(join(ROOT_DIR, 'specialists', 'registry.json')) ?? {};
|
|
74
74
|
const registryModels = registry.models ?? {};
|
|
75
75
|
const currentModels = readCurrentModels(join(ROOT_DIR, '.env'), registryModels, process.env);
|
|
76
76
|
const route = typeof args.input === 'string' ? routeRequest({ request: args.input }) : null;
|
|
@@ -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
|
|