@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
|
@@ -1,41 +1,56 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/audit-reads.mjs —
|
|
3
|
+
* lib/hooks/audit-reads.mjs — post-Read state tracker.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* are off by default because they happen much more often than writes; enable
|
|
7
|
-
* with `CONSTRUCT_AUDIT_READS=1`. When enabled, every Read tool call writes
|
|
8
|
-
* a single JSONL line to ~/.cx/audit-reads.jsonl with:
|
|
5
|
+
* Two responsibilities on every Read tool call:
|
|
9
6
|
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - bytes (size of the file at read time)
|
|
17
|
-
* - content_hash (sha256 of the file's first 64KiB at read time, truncated)
|
|
18
|
-
* - prev_line_hash (chained tamper-evidence — hash of previous JSONL line)
|
|
7
|
+
* 1. ALWAYS — upsert a file-hash entry in ~/.cx/file-hashes.json keyed by
|
|
8
|
+
* absolute path, with the current sha256 + size + iso timestamp.
|
|
9
|
+
* Edit-guard reads the store to detect staleness on a subsequent Edit:
|
|
10
|
+
* mismatched on-disk content versus the stored hash means the Edit
|
|
11
|
+
* target needs a fresh Read. A read-tracker delta is recorded for the
|
|
12
|
+
* efficiency snapshot in the same pass.
|
|
19
13
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* 2. OPT-IN — when `CONSTRUCT_AUDIT_READS=1`, write a single JSONL line
|
|
15
|
+
* to <project>/.cx/audit-reads.jsonl (falls back to ~/.cx/ outside a
|
|
16
|
+
* project) with timestamp, session_id, agent, cwd, target, bytes,
|
|
17
|
+
* content_hash, and prev_line_hash. The prev_line_hash chain makes
|
|
18
|
+
* after-the-fact reordering or deletion detectable by simple replay
|
|
19
|
+
* (symmetric counterpart to audit-trail.mjs for mutations). Bounded
|
|
20
|
+
* via the `audit-reads` channel.
|
|
21
|
+
*
|
|
22
|
+
* Reads happen far more often than writes, so the audit log is opt-in.
|
|
23
|
+
* The hash store is always-on because edit-guard depends on it for
|
|
24
|
+
* correctness, not telemetry.
|
|
23
25
|
*
|
|
24
26
|
* @p95ms 8
|
|
25
27
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
28
|
+
*
|
|
29
|
+
* @lifecycle PostToolUse
|
|
30
|
+
* @matcher Read
|
|
31
|
+
* @exits 0 = pass
|
|
26
32
|
*/
|
|
27
|
-
import { readFileSync,
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
28
34
|
import { createHash } from 'node:crypto';
|
|
29
|
-
import { join, resolve } from 'node:path';
|
|
35
|
+
import { join, resolve, dirname } from 'node:path';
|
|
30
36
|
import { homedir } from 'node:os';
|
|
31
37
|
import { logHookFailure } from './_lib/log.mjs';
|
|
38
|
+
import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
|
|
39
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
40
|
+
import { flushReadTrackerDeltas, recordReadDelta } from '../read-tracker-store.mjs';
|
|
32
41
|
|
|
33
42
|
const CX_DIR = join(homedir(), '.cx');
|
|
34
|
-
|
|
43
|
+
// audit-reads.jsonl is PROJECT-SCOPED — reads happen in a specific project,
|
|
44
|
+
// not across the user's whole machine. resolveProjectScopedPath returns
|
|
45
|
+
// <project>/.cx/audit-reads.jsonl when the hook fires from inside a
|
|
46
|
+
// Construct project, falling back to the legacy ~/.cx path otherwise so
|
|
47
|
+
// existing standalone invocations keep working.
|
|
48
|
+
|
|
49
|
+
const AUDIT_FILE = resolveProjectScopedPath('audit-reads.jsonl', { ensureDir: false });
|
|
50
|
+
const HASH_STORE = join(CX_DIR, 'file-hashes.json');
|
|
35
51
|
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
36
52
|
const HASH_PREFIX_BYTES = 64 * 1024;
|
|
37
|
-
|
|
38
|
-
if (process.env.CONSTRUCT_AUDIT_READS !== '1') process.exit(0);
|
|
53
|
+
const HASH_RETENTION_MS = 2 * 60 * 60 * 1000;
|
|
39
54
|
|
|
40
55
|
function sha256(input) { return createHash('sha256').update(input).digest('hex'); }
|
|
41
56
|
|
|
@@ -48,14 +63,9 @@ function readLastAgent() {
|
|
|
48
63
|
|
|
49
64
|
function readPrevLineHash() {
|
|
50
65
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const readFrom = Math.max(0, size - 2048);
|
|
55
|
-
const tail = readFileSync(AUDIT_FILE, 'utf8').slice(readFrom);
|
|
56
|
-
const lines = tail.split('\n').filter(Boolean);
|
|
57
|
-
if (lines.length === 0) return null;
|
|
58
|
-
return sha256(lines[lines.length - 1]);
|
|
66
|
+
const lastLine = readLastLineAcrossSegments(AUDIT_FILE);
|
|
67
|
+
if (lastLine === null) return null;
|
|
68
|
+
return sha256(lastLine);
|
|
59
69
|
} catch { return null; }
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -71,22 +81,53 @@ if (!filePath) process.exit(0);
|
|
|
71
81
|
|
|
72
82
|
const absPath = filePath.startsWith('/') ? filePath : resolve(cwd, filePath);
|
|
73
83
|
|
|
84
|
+
// Stage 1 — always-on hash store update for edit-guard staleness detection.
|
|
85
|
+
|
|
86
|
+
let fullContent = null;
|
|
74
87
|
let bytes = null;
|
|
75
|
-
let
|
|
88
|
+
let prefixHash = null;
|
|
76
89
|
try {
|
|
77
90
|
if (existsSync(absPath)) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
fullContent = readFileSync(absPath, 'utf8');
|
|
92
|
+
bytes = Buffer.byteLength(fullContent, 'utf8');
|
|
93
|
+
const prefix = Buffer.from(fullContent, 'utf8').slice(0, HASH_PREFIX_BYTES);
|
|
94
|
+
prefixHash = sha256(prefix).slice(0, 32);
|
|
95
|
+
|
|
96
|
+
const edgeHash = createHash('sha256').update(fullContent).digest('hex').slice(0, 16);
|
|
97
|
+
const nowIso = new Date().toISOString();
|
|
98
|
+
const requestedLimit = Number(input?.tool_input?.limit || 0);
|
|
99
|
+
const effectiveLimit = requestedLimit > 0 ? requestedLimit : 2000;
|
|
100
|
+
|
|
101
|
+
mkdirSync(CX_DIR, { recursive: true });
|
|
102
|
+
let store = {};
|
|
103
|
+
try { store = JSON.parse(readFileSync(HASH_STORE, 'utf8')); } catch { /* fresh store */ }
|
|
104
|
+
store[absPath] = { hash: edgeHash, ts: nowIso, size: fullContent.length };
|
|
105
|
+
|
|
106
|
+
const cutoff = Date.now() - HASH_RETENTION_MS;
|
|
107
|
+
for (const [k, v] of Object.entries(store)) {
|
|
108
|
+
if (new Date(v.ts).getTime() < cutoff) delete store[k];
|
|
109
|
+
}
|
|
110
|
+
writeFileSync(HASH_STORE, JSON.stringify(store, null, 2));
|
|
111
|
+
|
|
112
|
+
recordReadDelta({
|
|
113
|
+
path: absPath,
|
|
114
|
+
size: fullContent.length,
|
|
115
|
+
limit: effectiveLimit,
|
|
116
|
+
ts: nowIso,
|
|
117
|
+
}, process.env);
|
|
118
|
+
|
|
119
|
+
if (process.env.CONSTRUCT_READ_TRACKER_FLUSH === '1') {
|
|
120
|
+
flushReadTrackerDeltas({ nowIso, env: process.env });
|
|
121
|
+
}
|
|
85
122
|
}
|
|
86
123
|
} catch (err) {
|
|
87
|
-
logHookFailure({ hook: 'audit-reads', err, phase: 'hash', input: { filePath: absPath } });
|
|
124
|
+
logHookFailure({ hook: 'audit-reads', err, phase: 'hash-store', input: { filePath: absPath } });
|
|
88
125
|
}
|
|
89
126
|
|
|
127
|
+
// Stage 2 — opt-in tamper-evident audit log.
|
|
128
|
+
|
|
129
|
+
if (process.env.CONSTRUCT_AUDIT_READS !== '1') process.exit(0);
|
|
130
|
+
|
|
90
131
|
const record = {
|
|
91
132
|
ts: new Date().toISOString(),
|
|
92
133
|
session_id: input?.session_id || null,
|
|
@@ -95,13 +136,13 @@ const record = {
|
|
|
95
136
|
cwd,
|
|
96
137
|
target: filePath,
|
|
97
138
|
bytes,
|
|
98
|
-
content_hash:
|
|
139
|
+
content_hash: prefixHash,
|
|
99
140
|
prev_line_hash: readPrevLineHash(),
|
|
100
141
|
};
|
|
101
142
|
|
|
102
143
|
try {
|
|
103
|
-
mkdirSync(
|
|
104
|
-
|
|
144
|
+
mkdirSync(dirname(AUDIT_FILE), { recursive: true });
|
|
145
|
+
appendBounded('audit-reads', AUDIT_FILE, JSON.stringify(record) + '\n');
|
|
105
146
|
} catch (err) {
|
|
106
147
|
logHookFailure({ hook: 'audit-reads', err, phase: 'append' });
|
|
107
148
|
}
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* For Bash, only records commands that mutate state (git/npm/docker/cargo etc.
|
|
8
8
|
* write operations, rm, mv, chmod). Read-only commands are skipped.
|
|
9
9
|
*
|
|
10
|
-
* Each record is a single JSONL line written to
|
|
10
|
+
* Each record is a single JSONL line written to
|
|
11
|
+
* <project>/.cx/audit-trail.jsonl (falls back to ~/.cx/audit-trail.jsonl
|
|
12
|
+
* when the hook fires outside a Construct project) with:
|
|
11
13
|
* - timestamp ISO
|
|
12
14
|
* - session_id (from hook input when available)
|
|
13
15
|
* - tool_name
|
|
@@ -21,18 +23,26 @@
|
|
|
21
23
|
* - prev_line_hash (chained tamper-evidence — hash of previous JSONL line)
|
|
22
24
|
*
|
|
23
25
|
* The prev_line_hash chain means any after-the-fact reordering or deletion
|
|
24
|
-
* breaks the chain and is detectable by a simple replay.
|
|
26
|
+
* breaks the chain and is detectable by a simple replay. Bounded via the
|
|
27
|
+
* `audit-trail` channel registered in lib/logging/rotate.mjs LIMITS.
|
|
25
28
|
*
|
|
26
29
|
* @p95ms 15
|
|
27
30
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
31
|
+
*
|
|
32
|
+
* @lifecycle PostToolUse
|
|
33
|
+
* @matcher Edit|Write|MultiEdit|NotebookEdit|Bash
|
|
34
|
+
* @exits 0 = pass
|
|
28
35
|
*/
|
|
29
|
-
import { readFileSync,
|
|
36
|
+
import { readFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
30
37
|
import { createHash } from 'node:crypto';
|
|
31
|
-
import { join, resolve } from 'node:path';
|
|
38
|
+
import { join, resolve, dirname } from 'node:path';
|
|
32
39
|
import { homedir } from 'node:os';
|
|
40
|
+
import { appendBounded, readLastLineAcrossSegments } from '../logging/rotate.mjs';
|
|
41
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
42
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
33
43
|
|
|
34
44
|
const CX_DIR = join(homedir(), '.cx');
|
|
35
|
-
const AUDIT_FILE =
|
|
45
|
+
const AUDIT_FILE = resolveProjectScopedPath('audit-trail.jsonl', { ensureDir: false });
|
|
36
46
|
const LAST_AGENT = join(CX_DIR, 'last-agent.json');
|
|
37
47
|
const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);
|
|
38
48
|
|
|
@@ -69,14 +79,9 @@ function readLastAgent() {
|
|
|
69
79
|
|
|
70
80
|
function readPrevLineHash() {
|
|
71
81
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const readFrom = Math.max(0, size - 2048);
|
|
76
|
-
const fs = readFileSync(AUDIT_FILE, 'utf8').slice(readFrom);
|
|
77
|
-
const lines = fs.split('\n').filter(Boolean);
|
|
78
|
-
if (lines.length === 0) return null;
|
|
79
|
-
return sha256(lines[lines.length - 1]);
|
|
82
|
+
const lastLine = readLastLineAcrossSegments(AUDIT_FILE);
|
|
83
|
+
if (lastLine === null) return null;
|
|
84
|
+
return sha256(lastLine);
|
|
80
85
|
} catch {
|
|
81
86
|
return null;
|
|
82
87
|
}
|
|
@@ -89,7 +94,8 @@ function truncateSnippet(s, limit = 280) {
|
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
let input = {};
|
|
92
|
-
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
97
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
98
|
+
catch (err) { logHookFailure({ hook: 'audit-trail', err, phase: 'parse' }); process.exit(0); }
|
|
93
99
|
|
|
94
100
|
const toolName = input?.tool_name || '';
|
|
95
101
|
if (!MUTATING_TOOLS.has(toolName)) process.exit(0);
|
|
@@ -129,7 +135,9 @@ if (toolName !== 'Bash' && target) {
|
|
|
129
135
|
if (existsSync(absPath)) {
|
|
130
136
|
contentHash = sha256(readFileSync(absPath, 'utf8')).slice(0, 32);
|
|
131
137
|
}
|
|
132
|
-
} catch {
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logHookFailure({ hook: 'audit-trail', err, phase: 'hash', input: { target: absPath } });
|
|
140
|
+
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
const record = {
|
|
@@ -146,8 +154,10 @@ const record = {
|
|
|
146
154
|
};
|
|
147
155
|
|
|
148
156
|
try {
|
|
149
|
-
mkdirSync(
|
|
150
|
-
|
|
151
|
-
} catch {
|
|
157
|
+
mkdirSync(dirname(AUDIT_FILE), { recursive: true });
|
|
158
|
+
appendBounded('audit-trail', AUDIT_FILE, JSON.stringify(record) + '\n');
|
|
159
|
+
} catch (err) {
|
|
160
|
+
logHookFailure({ hook: 'audit-trail', err, phase: 'append' });
|
|
161
|
+
}
|
|
152
162
|
|
|
153
163
|
process.exit(0);
|
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @p95ms 20
|
|
16
16
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
17
|
+
*
|
|
18
|
+
* @lifecycle PostToolUse
|
|
19
|
+
* @matcher Bash
|
|
20
|
+
* @exits 0 = pass
|
|
17
21
|
*/
|
|
18
|
-
import { readFileSync, writeFileSync, mkdirSync
|
|
22
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
19
23
|
import { join } from 'node:path';
|
|
20
24
|
import { homedir } from 'node:os';
|
|
25
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
26
|
|
|
22
27
|
const SIZE_THRESHOLD_CHARS = 4000;
|
|
23
28
|
const LOG_DIR = join(homedir(), '.cx', 'bash-logs');
|
|
@@ -61,7 +66,8 @@ try {
|
|
|
61
66
|
);
|
|
62
67
|
|
|
63
68
|
try {
|
|
64
|
-
|
|
69
|
+
appendBounded(
|
|
70
|
+
'bash-warn-flags',
|
|
65
71
|
WARN_FLAGS,
|
|
66
72
|
`Bash output ${kb} KB saved to ${logPath} — prefer grepping the log over re-running.\n`,
|
|
67
73
|
);
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* @p95ms 4000
|
|
16
16
|
* @maxBlockingScope none (UserPromptSubmit, non-blocking)
|
|
17
|
+
*
|
|
18
|
+
* @lifecycle UserPromptSubmit
|
|
19
|
+
* @matcher *
|
|
20
|
+
* @exits 0 = pass
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
@@ -7,13 +7,17 @@
|
|
|
7
7
|
* narrative voice, point-in-time notes, noise sentinels. The agent sees the
|
|
8
8
|
* block reason and is expected to fix the comment before continuing.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* No bypass. If the policy fires on a legitimate comment, the policy is wrong
|
|
11
|
+
* — fix the policy in lib/comment-lint.mjs, not the call site.
|
|
11
12
|
*
|
|
12
13
|
* Input: reads tool_input.file_path from either the legacy TOOL_INPUT_FILE_PATH
|
|
13
14
|
* env var or modern stdin JSON ({ tool_input: { file_path } }).
|
|
14
15
|
*
|
|
16
|
+
* @lifecycle PostToolUse
|
|
17
|
+
* @matcher Write|Edit|MultiEdit
|
|
15
18
|
* @p95ms 60
|
|
16
19
|
* @maxBlockingScope PostToolUse
|
|
20
|
+
* @exits 0 = pass | 2 = block tool call
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { readFileSync } from 'node:fs';
|
|
@@ -21,8 +25,6 @@ import path from 'node:path';
|
|
|
21
25
|
import { fileURLToPath } from 'node:url';
|
|
22
26
|
import { lintFile, formatResults } from '../comment-lint.mjs';
|
|
23
27
|
|
|
24
|
-
if (process.env.CONSTRUCT_SKIP_COMMENT_LINT === '1') process.exit(0);
|
|
25
|
-
|
|
26
28
|
let filePath = process.env.TOOL_INPUT_FILE_PATH;
|
|
27
29
|
if (!filePath) {
|
|
28
30
|
try {
|
|
@@ -59,6 +61,6 @@ if (artifactOnly) {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
process.stderr.write(
|
|
62
|
-
'\nComment policy blocked this edit. Remove the violations
|
|
64
|
+
'\nComment policy blocked this edit. Remove the violations before continuing.\n',
|
|
63
65
|
);
|
|
64
66
|
process.exit(2);
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @p95ms 5
|
|
18
18
|
* @maxBlockingScope PreToolUse
|
|
19
|
+
*
|
|
20
|
+
* @lifecycle PreToolUse
|
|
21
|
+
* @matcher Write|Edit|MultiEdit
|
|
22
|
+
* @exits 0 = pass | 2 = block tool call
|
|
19
23
|
*/
|
|
20
24
|
import { readFileSync } from 'fs';
|
|
21
25
|
import { logHookFailure } from './_lib/log.mjs';
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* @p95ms 20
|
|
28
28
|
* @maxBlockingScope UserPromptSubmit
|
|
29
|
+
*
|
|
30
|
+
* @lifecycle UserPromptSubmit
|
|
31
|
+
* @matcher *
|
|
32
|
+
* @exits 0 = pass
|
|
29
33
|
*/
|
|
30
34
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
31
35
|
import { join } from 'node:path';
|
package/lib/hooks/dep-audit.mjs
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @p95ms 5000
|
|
10
10
|
* @maxBlockingScope none (PostToolUse, async, non-blocking)
|
|
11
|
+
*
|
|
12
|
+
* @lifecycle PostToolUse
|
|
13
|
+
* @matcher Write|Edit
|
|
14
|
+
* @exits 0 = pass
|
|
11
15
|
*/
|
|
12
|
-
import { readFileSync,
|
|
13
|
-
import { execSync } from 'child_process';
|
|
14
|
-
import { homedir } from 'os';
|
|
15
|
-
import { join, dirname, basename } from 'path';
|
|
16
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
17
|
+
import { execSync } from 'node:child_process';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { join, dirname, basename } from 'node:path';
|
|
20
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
16
22
|
|
|
17
23
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
18
24
|
if (!filePath) process.exit(0);
|
|
@@ -28,7 +34,8 @@ const projectDir = dirname(filePath);
|
|
|
28
34
|
const warnFlagsPath = join(homedir(), '.cx', 'warn-flags.txt');
|
|
29
35
|
|
|
30
36
|
function appendWarn(msg) {
|
|
31
|
-
try {
|
|
37
|
+
try { appendBounded('bash-warn-flags', warnFlagsPath, msg + '\n'); }
|
|
38
|
+
catch (err) { logHookFailure({ hook: 'dep-audit', err, phase: 'append' }); }
|
|
32
39
|
process.stderr.write(`[dep-audit] ${msg}\n`);
|
|
33
40
|
emitVulnerability(msg);
|
|
34
41
|
}
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @p95ms 15
|
|
14
14
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
15
|
+
*
|
|
16
|
+
* @lifecycle PostToolUse
|
|
17
|
+
* @matcher Write|Edit|MultiEdit
|
|
18
|
+
* @exits 0 = pass
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
@@ -73,7 +77,7 @@ writeState(state);
|
|
|
73
77
|
|
|
74
78
|
if (advisory != null) {
|
|
75
79
|
process.stderr.write(
|
|
76
|
-
`[doc-coupling] ${state.codeEdits} code file edit(s) this session under lib/|bin/|src/|app/; CHANGELOG.md / docs / .cx/context still not touched. Update docs before the next commit (the pre-commit gate will block otherwise
|
|
80
|
+
`[doc-coupling] ${state.codeEdits} code file edit(s) this session under lib/|bin/|src/|app/; CHANGELOG.md / docs / .cx/context still not touched. Update docs before the next commit (the pre-commit gate will block otherwise).\n`,
|
|
77
81
|
);
|
|
78
82
|
}
|
|
79
83
|
|
|
@@ -1,39 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/edit-accumulator.mjs —
|
|
3
|
+
* lib/hooks/edit-accumulator.mjs — accumulates edited paths for the next
|
|
4
|
+
* typecheck batch.
|
|
4
5
|
*
|
|
5
|
-
* Runs as PostToolUse after Edit/Write. Maintains
|
|
6
|
+
* Runs as PostToolUse after Edit/Write. Maintains two pieces of session
|
|
7
|
+
* state at ~/.cx/: a total edit counter for surface-area summaries and a
|
|
8
|
+
* deduped queue of TS/JS files awaiting typecheck. The typecheck queue is
|
|
9
|
+
* bounded via the `edit-accumulator` channel in lib/logging/rotate.mjs.
|
|
6
10
|
*
|
|
7
11
|
* @p95ms 10
|
|
8
12
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
13
|
+
*
|
|
14
|
+
* @lifecycle PostToolUse
|
|
15
|
+
* @matcher Write|Edit|MultiEdit
|
|
16
|
+
* @exits 0 = pass
|
|
9
17
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import { homedir } from 'os';
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
22
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
12
23
|
|
|
13
24
|
const TS_EXTS = new Set(['.ts','.tsx','.js','.mjs','.jsx','.cjs']);
|
|
14
25
|
const filePath = process.env.TOOL_INPUT_FILE_PATH || '';
|
|
15
26
|
|
|
16
27
|
if (!filePath) process.exit(0);
|
|
17
28
|
|
|
18
|
-
const ext =
|
|
29
|
+
const ext = path.extname(filePath);
|
|
19
30
|
const home = homedir();
|
|
20
|
-
const countPath =
|
|
31
|
+
const countPath = path.join(home, '.cx', 'files-changed-count.txt');
|
|
21
32
|
|
|
22
33
|
// Always increment the total files-changed counter for notify
|
|
23
34
|
try {
|
|
24
35
|
const current = existsSync(countPath) ? parseInt(readFileSync(countPath, 'utf8').trim() || '0', 10) : 0;
|
|
25
36
|
writeFileSync(countPath, String(current + 1));
|
|
26
|
-
} catch {
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logHookFailure({ hook: 'edit-accumulator', err, phase: 'count' });
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
// Only accumulate TS/JS files for typecheck
|
|
29
42
|
if (!TS_EXTS.has(ext)) process.exit(0);
|
|
30
43
|
|
|
31
|
-
const tcPath =
|
|
44
|
+
const tcPath = path.join(home, '.cx', 'pending-typecheck.txt');
|
|
32
45
|
try {
|
|
33
46
|
const existing = existsSync(tcPath) ? readFileSync(tcPath, 'utf8').split('\n').filter(Boolean) : [];
|
|
34
47
|
if (!existing.includes(filePath)) {
|
|
35
|
-
|
|
48
|
+
appendBounded('edit-accumulator', tcPath, filePath + '\n');
|
|
36
49
|
}
|
|
37
|
-
} catch {
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logHookFailure({ hook: 'edit-accumulator', err, phase: 'append' });
|
|
52
|
+
}
|
|
38
53
|
|
|
39
54
|
process.exit(0);
|
package/lib/hooks/edit-guard.mjs
CHANGED
package/lib/hooks/guard-bash.mjs
CHANGED
package/lib/hooks/mcp-audit.mjs
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @p95ms 10
|
|
9
9
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
10
|
+
*
|
|
11
|
+
* @lifecycle PostToolUse
|
|
12
|
+
* @matcher mcp__.*
|
|
13
|
+
* @exits 0 = pass
|
|
10
14
|
*/
|
|
11
15
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
12
16
|
import { join } from 'path';
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @p95ms 51
|
|
8
8
|
* @maxBlockingScope none (PreToolUse, warn-only)
|
|
9
|
+
*
|
|
10
|
+
* @lifecycle PreToolUse
|
|
11
|
+
* @matcher mcp__.*
|
|
12
|
+
* @exits 0 = pass | 2 = block tool call
|
|
9
13
|
*/
|
|
10
14
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
11
15
|
import { homedir } from 'os';
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @p95ms 150
|
|
17
17
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
18
|
+
*
|
|
19
|
+
* @lifecycle PostToolUseFailure
|
|
20
|
+
* @matcher *
|
|
21
|
+
* @exits 0 = pass
|
|
18
22
|
*/
|
|
19
|
-
import { existsSync, mkdirSync,
|
|
23
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
20
24
|
import { execFileSync } from 'node:child_process';
|
|
21
25
|
import { homedir } from 'node:os';
|
|
22
26
|
import { join } from 'node:path';
|
|
@@ -27,20 +31,12 @@ import {
|
|
|
27
31
|
selectFallbackModel,
|
|
28
32
|
writeProviderCooldown,
|
|
29
33
|
} from '../model-router.mjs';
|
|
34
|
+
import { readHookInput } from './_lib/input.mjs';
|
|
30
35
|
|
|
31
36
|
const cooldownPath = join(homedir(), '.cx', 'provider-cooldowns.json');
|
|
32
37
|
const envPath = join(process.cwd(), '.env');
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const raw = readFileSync(0, 'utf8');
|
|
37
|
-
return raw ? JSON.parse(raw) : {};
|
|
38
|
-
} catch {
|
|
39
|
-
return {};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const input = readInput();
|
|
39
|
+
const input = readHookInput();
|
|
44
40
|
|
|
45
41
|
const classified = classifyProviderFailure(input);
|
|
46
42
|
if (!classified || !classified.retryable) process.exit(0);
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @p95ms 80
|
|
17
17
|
* @maxBlockingScope PreToolUse, Stop, UserPromptSubmit
|
|
18
|
+
*
|
|
19
|
+
* @lifecycle PreToolUse
|
|
20
|
+
* @matcher Write|Edit|MultiEdit|NotebookEdit|TodoWrite|Read|Grep|Glob|LS|NotebookRead
|
|
21
|
+
* @exits 0 = pass | 2 = block tool call
|
|
18
22
|
*/
|
|
19
23
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
20
24
|
import { execSync } from 'node:child_process';
|