@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
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
5
|
* - Detect stale, missing, or outdated docs across targets
|
|
6
|
+
* - Emit document.stale lifecycle events for each stale gap surfaced
|
|
7
|
+
* (consumed by cx-docs-keeper and any project-bound subscribers)
|
|
6
8
|
* - Generate/update docs as artifacts in the appropriate docs lane
|
|
7
9
|
* - Route writes through the authority guard (direct-write for low-risk, approval-queued for high-risk)
|
|
8
10
|
* - Treat ALL doc types uniformly: adrs, prds, memos, notes, intake, roadmaps
|
|
@@ -23,6 +25,7 @@ import { WORKSPACE_DOCS_LANES } from './config.mjs';
|
|
|
23
25
|
import { recommendArtifacts } from './artifact.mjs';
|
|
24
26
|
import { createRecommendation, autoSuppressStale, recommendationStats, isRecommendationActive } from './recommendation-store.mjs';
|
|
25
27
|
import { detectConflicts } from './conflict-detection.mjs';
|
|
28
|
+
import { emitBestEffort as emitRoleEvent } from '../roles/event-bus.mjs';
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Risk classification for doc operations.
|
|
@@ -232,6 +235,22 @@ export async function runDocsLifecycle(opts) {
|
|
|
232
235
|
const gaps = detectDocGaps(target, { snapshot, roleLens });
|
|
233
236
|
allGaps.push(...gaps.map((g) => ({ ...g, target: targetLabel(target) })));
|
|
234
237
|
|
|
238
|
+
// Surface staleness as lifecycle events for any specialist subscribed
|
|
239
|
+
// to document.stale (cx-docs-keeper by default).
|
|
240
|
+
|
|
241
|
+
for (const gap of gaps) {
|
|
242
|
+
if (gap.type !== 'stale') continue;
|
|
243
|
+
emitRoleEvent('document.stale', {
|
|
244
|
+
summary: gap.summary,
|
|
245
|
+
context: {
|
|
246
|
+
lane: gap.lane,
|
|
247
|
+
file: gap.file,
|
|
248
|
+
ageMs: gap.age,
|
|
249
|
+
target: targetLabel(target),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
235
254
|
// Check intake patterns for artifact recommendations (uses recommendation store for dedup)
|
|
236
255
|
const intakeGaps = detectIntakeDocGaps(target.path);
|
|
237
256
|
for (const gap of intakeGaps) {
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -33,10 +33,14 @@ import { shouldCreateCx } from '../project-detection.mjs';
|
|
|
33
33
|
import { ensureCxDir } from '../project-init-shared.mjs';
|
|
34
34
|
import { prepareIntakeForIngestedFile } from '../intake/prepare.mjs';
|
|
35
35
|
import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-config.mjs';
|
|
36
|
+
import { loadManifest, recordFile, hasFile, sha256Of, MANIFEST_REL_PATH } from '../intake/manifest.mjs';
|
|
37
|
+
import { gatherAttribution } from '../intake/attribution.mjs';
|
|
38
|
+
import { acquirePollLock, releasePollLock } from '../intake/poll-lock.mjs';
|
|
36
39
|
|
|
37
40
|
const STATE_FILE = '.cx/runtime/inbox-state.json';
|
|
38
41
|
const DEFAULT_INBOX_SUBDIR = '.cx/inbox';
|
|
39
42
|
const DOCS_INTAKE_SUBDIR = 'docs/intake';
|
|
43
|
+
const ARCHETYPE_INBOX_SUBDIR = 'inbox';
|
|
40
44
|
const MAX_FILE_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB hard cap
|
|
41
45
|
const STALE_STATE_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
42
46
|
|
|
@@ -155,6 +159,16 @@ export function resolveInboxDirs(rootDir, env = process.env) {
|
|
|
155
159
|
if (existsSync(docsIntake)) dirs.push(docsIntake);
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// Archetype inbox is a profile-driven drop zone for raw source files
|
|
163
|
+
// (capabilities.intake.inbox). Off by default; init flips it on when the
|
|
164
|
+
// active profile declares the capability so non-archetype projects keep
|
|
165
|
+
// their pre-Piece-C behavior.
|
|
166
|
+
|
|
167
|
+
if (config.includeArchetypeInbox) {
|
|
168
|
+
const archetypeInbox = join(rootDir, ARCHETYPE_INBOX_SUBDIR);
|
|
169
|
+
if (existsSync(archetypeInbox)) dirs.push(archetypeInbox);
|
|
170
|
+
}
|
|
171
|
+
|
|
158
172
|
for (const candidate of config.parentDirs) {
|
|
159
173
|
if (!candidate) continue;
|
|
160
174
|
if (!existsSync(candidate)) continue;
|
|
@@ -298,8 +312,31 @@ export class InboxWatcher {
|
|
|
298
312
|
/**
|
|
299
313
|
* Scan all inbox dirs, ingest new files, record observations.
|
|
300
314
|
* Returns { processed: [...], skipped: number, errors: [...] }
|
|
315
|
+
*
|
|
316
|
+
* @param {object} [opts]
|
|
317
|
+
* @param {number} [opts.waitMs] - When >0, block up to this many ms for
|
|
318
|
+
* the intake poll lock. Default 0 (fail-fast). The CLI sets this when
|
|
319
|
+
* the operator opts into waiting; the daemon's tick keeps the default
|
|
320
|
+
* so a stuck poll does not stall the next scheduled iteration.
|
|
321
|
+
* @param {string} [opts.actor] - Stamped onto the lock for diagnostics.
|
|
301
322
|
*/
|
|
302
|
-
async poll() {
|
|
323
|
+
async poll({ waitMs = 0, actor = 'inbox-watcher' } = {}) {
|
|
324
|
+
let lockHeld = false;
|
|
325
|
+
try {
|
|
326
|
+
await acquirePollLock({
|
|
327
|
+
rootDir: this.#rootDir,
|
|
328
|
+
actor,
|
|
329
|
+
command: 'InboxWatcher.poll',
|
|
330
|
+
waitMs,
|
|
331
|
+
});
|
|
332
|
+
lockHeld = true;
|
|
333
|
+
return await this.#pollLocked();
|
|
334
|
+
} finally {
|
|
335
|
+
if (lockHeld) releasePollLock(this.#rootDir);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async #pollLocked() {
|
|
303
340
|
const config = loadIntakeConfig(this.#rootDir, this.#env);
|
|
304
341
|
const dirs = resolveInboxDirs(this.#rootDir, this.#env);
|
|
305
342
|
const state = readState(this.#rootDir);
|
|
@@ -307,6 +344,15 @@ export class InboxWatcher {
|
|
|
307
344
|
const errors = [];
|
|
308
345
|
let skipped = 0;
|
|
309
346
|
|
|
347
|
+
// Capability detection: SHA-256 dedup is enabled when the project's
|
|
348
|
+
// intake manifest exists (init scaffolds it for the archetype). Hashing
|
|
349
|
+
// is only paid when the manifest is in use, so non-archetype projects
|
|
350
|
+
// keep the original fast path.
|
|
351
|
+
|
|
352
|
+
const manifestPath = join(this.#rootDir, MANIFEST_REL_PATH);
|
|
353
|
+
const manifestEnabled = existsSync(manifestPath);
|
|
354
|
+
const attribution = manifestEnabled ? gatherAttribution({ cwd: this.#rootDir }) : null;
|
|
355
|
+
|
|
310
356
|
for (const dir of dirs) {
|
|
311
357
|
const candidates = scanDir(dir, config.maxDepth);
|
|
312
358
|
for (const candidate of candidates) {
|
|
@@ -319,6 +365,27 @@ export class InboxWatcher {
|
|
|
319
365
|
continue;
|
|
320
366
|
}
|
|
321
367
|
|
|
368
|
+
// Manifest dedup: a file with identical content under any path is
|
|
369
|
+
// a duplicate. Catches renames, copies, and re-drops the state
|
|
370
|
+
// check misses. Skipped here without consuming the ingest budget.
|
|
371
|
+
|
|
372
|
+
let candidateSha = null;
|
|
373
|
+
if (manifestEnabled) {
|
|
374
|
+
try {
|
|
375
|
+
const bytes = readFileSync(candidate.path);
|
|
376
|
+
candidateSha = sha256Of(bytes);
|
|
377
|
+
if (hasFile(this.#rootDir, candidateSha)) {
|
|
378
|
+
skipped += 1;
|
|
379
|
+
state[key] = { mtimeMs: candidate.mtimeMs, processedAt: new Date().toISOString(), dedupHash: candidateSha };
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (this.#env.CONSTRUCT_DEBUG_INTAKE === '1') {
|
|
384
|
+
process.stderr.write(`[inbox] sha256 hashing failed for ${candidate.path}: ${err.message}\n`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
322
389
|
try {
|
|
323
390
|
const { category, knowledgeSubdir } = inferFileClassification(candidate.path);
|
|
324
391
|
const result = await ingestDocuments([candidate.path], {
|
|
@@ -381,7 +448,23 @@ export class InboxWatcher {
|
|
|
381
448
|
}
|
|
382
449
|
|
|
383
450
|
state[key] = { mtimeMs: candidate.mtimeMs, processedAt: new Date().toISOString(), outputPath: fileResult.outputPath };
|
|
384
|
-
|
|
451
|
+
|
|
452
|
+
if (manifestEnabled && candidateSha) {
|
|
453
|
+
try {
|
|
454
|
+
recordFile(this.#rootDir, candidateSha, {
|
|
455
|
+
sourcePath: candidate.path,
|
|
456
|
+
intakeId,
|
|
457
|
+
createdBy: attribution?.createdBy ?? null,
|
|
458
|
+
createdByAgent: attribution?.createdByAgent ?? null,
|
|
459
|
+
});
|
|
460
|
+
} catch (err) {
|
|
461
|
+
if (this.#env.CONSTRUCT_DEBUG_INTAKE === '1') {
|
|
462
|
+
process.stderr.write(`[inbox] manifest record failed for ${candidate.path}: ${err.message}\n`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
processed.push({ path: candidate.path, outputPath: fileResult.outputPath, docsPath, characters: fileResult.characters, knowledgeSubdir, intakeId, dedupHash: candidateSha });
|
|
385
468
|
}
|
|
386
469
|
} catch (err) {
|
|
387
470
|
// Audio/video files cannot be text-extracted without ASR. Route to
|
|
@@ -18,11 +18,20 @@
|
|
|
18
18
|
* Prioritization formula:
|
|
19
19
|
* score = (signalCount * 2) + (customerImpact * 3) + (recencyBonus) + (strategicBonus)
|
|
20
20
|
* P0: score >= 10, P1: >= 7, P2: >= 4, P3: < 4
|
|
21
|
+
*
|
|
22
|
+
* Enrichment state machine: every new record starts with state: 'raw'. The
|
|
23
|
+
* enrichment loop (subscriber to recommendation.generated, lifecycle event
|
|
24
|
+
* emitted from createRecommendation on the new-record path only) transitions
|
|
25
|
+
* raw → enriching → enriched | enrichment_failed and writes enrichedAt /
|
|
26
|
+
* enrichedBy / framingNotes back to the index. Dedup-update path does not
|
|
27
|
+
* re-emit so a steady drip of signals against the same recommendation does
|
|
28
|
+
* not retrigger enrichment.
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
31
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
24
32
|
import { join } from 'node:path';
|
|
25
33
|
import { randomUUID } from 'node:crypto';
|
|
34
|
+
import { emitBestEffort } from '../roles/event-bus.mjs';
|
|
26
35
|
import { cxDir } from '../paths.mjs';
|
|
27
36
|
import { hasSqlStore } from '../storage/sql-store.mjs';
|
|
28
37
|
import { createSqlClient } from '../storage/backend.mjs';
|
|
@@ -161,6 +170,9 @@ export function createRecommendation({ type, title, reason, signalCount = 1, cus
|
|
|
161
170
|
supersededAt: null,
|
|
162
171
|
supersededById: null,
|
|
163
172
|
suppressedUntil: null,
|
|
173
|
+
state: 'raw',
|
|
174
|
+
enrichedAt: null,
|
|
175
|
+
enrichedBy: null,
|
|
164
176
|
};
|
|
165
177
|
|
|
166
178
|
// Append to log
|
|
@@ -171,6 +183,23 @@ export function createRecommendation({ type, title, reason, signalCount = 1, cus
|
|
|
171
183
|
writeIndex(index);
|
|
172
184
|
|
|
173
185
|
createRecommendationPgBestEffort(rec, project, env);
|
|
186
|
+
|
|
187
|
+
// Surfaces the new recommendation as a lifecycle event so the enrichment
|
|
188
|
+
// loop (and any project-bound subscribers in .cx/specialists/) can route
|
|
189
|
+
// the matched specialist's contract chain against it.
|
|
190
|
+
|
|
191
|
+
emitBestEffort('recommendation.generated', {
|
|
192
|
+
project: project || '',
|
|
193
|
+
summary: `${rec.type}: ${rec.title}`,
|
|
194
|
+
context: {
|
|
195
|
+
recommendationId: rec.id,
|
|
196
|
+
dedupKey: key,
|
|
197
|
+
priority: rec.priority,
|
|
198
|
+
score: rec.score,
|
|
199
|
+
artifactType: rec.type,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
174
203
|
return { id, dedupKey: key, priority: rec.priority, score, existing: false };
|
|
175
204
|
}
|
|
176
205
|
|
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
|
*/
|
package/lib/gates-audit.mjs
CHANGED
|
@@ -41,18 +41,24 @@ const DEFAULT_ROOT_DIR = join(MODULE_DIR, '..');
|
|
|
41
41
|
const MERGE_AGGREGATOR = 'ci-required';
|
|
42
42
|
|
|
43
43
|
const GATE_DEFINITIONS = [
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{ ciJob: '
|
|
51
|
-
{ ciJob: '
|
|
52
|
-
{ ciJob: '
|
|
53
|
-
{ ciJob: '
|
|
54
|
-
{ ciJob: '
|
|
55
|
-
{ ciJob:
|
|
44
|
+
// Test, build, audit, evals, docs-drift, comment-policy, prose, profiles —
|
|
45
|
+
// all CI-only. The local pre-push gate was shrunk to claude/* refusal +
|
|
46
|
+
// SHA-aware red-CI re-push check + PR body lint; CI is the source of
|
|
47
|
+
// truth for everything else (see CHANGELOG entry "Pre-push hooks shrunk
|
|
48
|
+
// to local-only signals; CI is the source of truth" and the inverted
|
|
49
|
+
// contract enforced by tests/ci-parity.test.mjs).
|
|
50
|
+
{ ciJob: 'test (ubuntu-latest / node 22)', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
51
|
+
{ ciJob: 'test (macos-latest / node 22)', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
52
|
+
{ ciJob: 'test (ubuntu-latest / node 20)', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
53
|
+
{ ciJob: 'test (macos-latest / node 20)', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
54
|
+
{ ciJob: 'retrieval evals', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
55
|
+
{ ciJob: 'dependency CVE audit', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
56
|
+
{ ciJob: 'secret scanning', prePushLabel: null, preCommitCheck: 'ECC secret scan', critical: true, note: 'pre-commit ECC scan covers a subset of gitleaks rules' },
|
|
57
|
+
{ ciJob: 'postgres + pgvector integration', prePushLabel: null, preCommitCheck: null, critical: false, note: 'CI-only: requires Docker Postgres + pgvector container; not practical locally', localMirror: 'ci-only', requireMergeVia: MERGE_AGGREGATOR },
|
|
58
|
+
{ ciJob: 'docs drift check', prePushLabel: null, critical: true, requireMergeVia: MERGE_AGGREGATOR, localMirror: 'ci-only' },
|
|
59
|
+
{ ciJob: 'comment policy', prePushLabel: null, preCommitCheck: 'Construct comment-lint', critical: true, note: 'pre-commit Construct policy section calls `lint:comments` across the full worktree', requireMergeVia: MERGE_AGGREGATOR },
|
|
60
|
+
{ ciJob: 'template policy', prePushLabel: null, preCommitGhPrIntercept: true, critical: true, note: 'pre-push-gate intercepts `gh pr create` / `gh pr edit` and lints the body', requireMergeVia: MERGE_AGGREGATOR },
|
|
61
|
+
{ ciJob: MERGE_AGGREGATOR, prePushLabel: null, preCommitCheck: null, critical: true, note: 'aggregator: succeeds iff every wrapped conditional job in ci.yml ended in success or skipped', localMirror: 'ci-only' },
|
|
56
62
|
];
|
|
57
63
|
|
|
58
64
|
function parseCIJobs(rootDir) {
|
|
@@ -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}'`);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hooks/_lib/input.mjs — shared hook input parsing.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code passes hook input as JSON on stdin. Codex and OpenCode (when
|
|
5
|
+
* they ship hook support) pass via TOOL_INPUT_FILE_PATH env or argv. This
|
|
6
|
+
* helper normalizes all three so a hook reads one shape regardless of the
|
|
7
|
+
* host transport. Hooks should call `readHookInput()` once at startup and
|
|
8
|
+
* branch on `input.tool_name` from there.
|
|
9
|
+
*
|
|
10
|
+
* Returns an object with at minimum `tool_name` and `tool_input` fields,
|
|
11
|
+
* plus any fields present in the source payload. Empty object on parse
|
|
12
|
+
* failure (the hook is then free to exit 0 silently if no useful input
|
|
13
|
+
* landed — matches the existing convention).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
|
|
18
|
+
export function readHookInput({ env = process.env, argv = process.argv } = {}) {
|
|
19
|
+
const stdin = tryReadStdin();
|
|
20
|
+
if (stdin) {
|
|
21
|
+
const parsed = tryParseJson(stdin);
|
|
22
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const envPath = env.TOOL_INPUT_FILE_PATH;
|
|
26
|
+
if (envPath) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = readFileSync(envPath, 'utf8');
|
|
29
|
+
const parsed = tryParseJson(raw);
|
|
30
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
31
|
+
return { tool_input: { file_path: envPath } };
|
|
32
|
+
} catch { /* fall through */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const argvJsonPos = argv.findIndex((a) => a === '--input-json');
|
|
36
|
+
if (argvJsonPos >= 0 && argv[argvJsonPos + 1]) {
|
|
37
|
+
const parsed = tryParseJson(argv[argvJsonPos + 1]);
|
|
38
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tryReadStdin() {
|
|
45
|
+
try { return readFileSync(0, 'utf8'); }
|
|
46
|
+
catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tryParseJson(s) {
|
|
50
|
+
try { return JSON.parse(s); }
|
|
51
|
+
catch { return null; }
|
|
52
|
+
}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/hooks/agent-tracker.mjs —
|
|
3
|
+
* lib/hooks/agent-tracker.mjs — Task tool lifecycle hook: records dispatch +
|
|
4
|
+
* outcome and enqueues `next:cx-<role>` handoffs.
|
|
4
5
|
*
|
|
5
|
-
* Runs as PostToolUse after
|
|
6
|
-
*
|
|
6
|
+
* Runs as PostToolUse after Task. Writes the agent-log to the project-scoped
|
|
7
|
+
* .cx/agent-log.jsonl, captures success/failure observations under
|
|
8
|
+
* .cx/observations/, and enqueues handoffs into ~/.cx/role-pending.jsonl
|
|
9
|
+
* (kept user-scope because a handoff may legitimately span projects; each
|
|
10
|
+
* entry carries a projectId tag for attribution).
|
|
11
|
+
*
|
|
12
|
+
* Handoffs also emit a `handoff.received` lifecycle event via the role
|
|
13
|
+
* event-bus for observability. The direct role-pending write stays the
|
|
14
|
+
* source of truth until the gateway becomes an event-bus consumer; the
|
|
15
|
+
* emit is the bridge that makes the dispatch observable through the same
|
|
16
|
+
* surface as every other lifecycle event.
|
|
7
17
|
*
|
|
8
18
|
* @p95ms 10
|
|
9
19
|
* @maxBlockingScope none (PostToolUse, non-blocking)
|
|
20
|
+
*
|
|
21
|
+
* @lifecycle PostToolUse
|
|
22
|
+
* @matcher Task
|
|
23
|
+
* @exits 0 = pass
|
|
10
24
|
*/
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync
|
|
12
|
-
import { join } from 'path';
|
|
13
|
-
import { homedir } from 'os';
|
|
25
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
29
|
+
import { resolveProjectScopedPath } from '../project-root.mjs';
|
|
30
|
+
import { emit as emitRoleEvent } from '../roles/event-bus.mjs';
|
|
31
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
14
32
|
|
|
15
33
|
let input = {};
|
|
16
34
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
@@ -67,8 +85,11 @@ try {
|
|
|
67
85
|
const safeName = agentName.replace(/[^a-z0-9._-]/gi, '_');
|
|
68
86
|
writeFileSync(join(cxDir, `last-agent-${safeName}.json`), JSON.stringify(agentEntry));
|
|
69
87
|
|
|
70
|
-
//
|
|
71
|
-
|
|
88
|
+
// Per-project agent log (was ~/.cx/agent-log.jsonl which mixed every
|
|
89
|
+
// project's dispatches into one stream). Falls back to ~/.cx/ when the
|
|
90
|
+
// hook fires outside a Construct project.
|
|
91
|
+
|
|
92
|
+
const agentLogFile = resolveProjectScopedPath('agent-log.jsonl', { ensureDir: false });
|
|
72
93
|
const logEntry = {
|
|
73
94
|
timestamp: new Date().toISOString(),
|
|
74
95
|
agent: agentName,
|
|
@@ -79,7 +100,7 @@ try {
|
|
|
79
100
|
toolInputKeys: Object.keys(toolInput).filter(k => !k.includes('secret') && !k.includes('password') && !k.includes('token'))
|
|
80
101
|
};
|
|
81
102
|
|
|
82
|
-
|
|
103
|
+
appendBounded('agent-log', agentLogFile, JSON.stringify(logEntry) + '\n');
|
|
83
104
|
|
|
84
105
|
// Record observation for pattern learning (only if we have meaningful outcome)
|
|
85
106
|
if (outcome !== 'unknown' && description.length > 10) {
|
|
@@ -150,9 +171,8 @@ try {
|
|
|
150
171
|
}
|
|
151
172
|
}
|
|
152
173
|
|
|
153
|
-
} catch (
|
|
154
|
-
|
|
155
|
-
console.error(`Agent tracker error: ${error.message}`);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
logHookFailure({ hook: 'agent-tracker', err, phase: 'main' });
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
// Auto-enqueue handoff when a completing persona's result references
|
|
@@ -165,12 +185,18 @@ try {
|
|
|
165
185
|
const unique = [...new Set(matches)];
|
|
166
186
|
if (unique.length > 0) {
|
|
167
187
|
const { isOnboarded, loadManifest } = await import('../roles/manifest.mjs');
|
|
188
|
+
// role-pending stays at user scope (a handoff might span projects),
|
|
189
|
+
// but each entry gets a projectId tag so a reader can attribute it.
|
|
190
|
+
|
|
168
191
|
const pendingPath = join(homedir(), '.cx', 'role-pending.jsonl');
|
|
192
|
+
const { resolveProjectScope } = await import('../project-root.mjs');
|
|
193
|
+
const scope = resolveProjectScope();
|
|
169
194
|
const bdMatch = /\b(construct-[a-z0-9]+)\b/i.exec(resultText) || [];
|
|
170
195
|
const bdIssueId = bdMatch[1] || null;
|
|
171
196
|
for (const targetId of unique) {
|
|
172
197
|
if (!isOnboarded(targetId)) continue;
|
|
173
198
|
const manifest = loadManifest(targetId);
|
|
199
|
+
const summary = `Handoff from ${agentName}${bdIssueId ? ` (re ${bdIssueId})` : ''}`;
|
|
174
200
|
const entry = {
|
|
175
201
|
ts: Date.now(),
|
|
176
202
|
personaId: targetId,
|
|
@@ -178,15 +204,33 @@ try {
|
|
|
178
204
|
bdIssueId,
|
|
179
205
|
fingerprint: `handoff-${agentName}-${targetId}-${Date.now().toString(36)}`,
|
|
180
206
|
eventType: 'handoff.received',
|
|
181
|
-
summary
|
|
207
|
+
summary,
|
|
182
208
|
killSwitchEnv: manifest?.killSwitchEnv || '',
|
|
183
209
|
handoffFrom: agentName,
|
|
210
|
+
...(scope?.projectId ? { projectId: scope.projectId } : {}),
|
|
184
211
|
};
|
|
185
|
-
|
|
212
|
+
appendBounded('role-pending', pendingPath, JSON.stringify(entry) + '\n');
|
|
213
|
+
|
|
214
|
+
// Mirror the handoff to the event bus so the dispatch is observable
|
|
215
|
+
// alongside every other lifecycle event. Best-effort: a bus failure
|
|
216
|
+
// does not affect the role-pending source of truth above.
|
|
217
|
+
try {
|
|
218
|
+
emitRoleEvent('handoff.received', {
|
|
219
|
+
project: scope?.projectId || '',
|
|
220
|
+
summary,
|
|
221
|
+
context: {
|
|
222
|
+
targetCxId: `cx-${targetId}`,
|
|
223
|
+
handoffFrom: agentName,
|
|
224
|
+
bdIssueId,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
} catch { /* swallow */ }
|
|
186
228
|
}
|
|
187
229
|
}
|
|
188
230
|
}
|
|
189
|
-
} catch {
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logHookFailure({ hook: 'agent-tracker', err, phase: 'handoff' });
|
|
233
|
+
}
|
|
190
234
|
|
|
191
235
|
// Helper functions for learning system
|
|
192
236
|
function extractKeywords(text) {
|