@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/attribution.mjs — provenance metadata for intake-archetype artifacts.
|
|
3
|
+
*
|
|
4
|
+
* Two-field attribution. Human identity comes from `git config user.name` +
|
|
5
|
+
* `user.email`; agent identity comes from CONSTRUCT_AGENT_ID (set by hosts
|
|
6
|
+
* like Claude Code, Codex, Copilot before they invoke the binary). Both are
|
|
7
|
+
* captured because either can be the proximate cause of a write and the
|
|
8
|
+
* audit reader needs to know which.
|
|
9
|
+
*
|
|
10
|
+
* Pure, side-effect free. No git/cli call when the env var
|
|
11
|
+
* CONSTRUCT_ATTRIBUTION_DISABLE=1 — useful for deterministic tests and for
|
|
12
|
+
* privacy-sensitive setups that prefer no identity capture.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
function gitConfigValue(key, cwd) {
|
|
18
|
+
const result = spawnSync('git', ['config', '--get', key], {
|
|
19
|
+
cwd,
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
timeout: 2000,
|
|
22
|
+
});
|
|
23
|
+
if (result.status !== 0) return null;
|
|
24
|
+
const value = (result.stdout || '').trim();
|
|
25
|
+
return value || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function humanIdentity(cwd) {
|
|
29
|
+
const name = gitConfigValue('user.name', cwd);
|
|
30
|
+
const email = gitConfigValue('user.email', cwd);
|
|
31
|
+
if (!name && !email) return null;
|
|
32
|
+
if (name && email) return `${name} <${email}>`;
|
|
33
|
+
return name || email;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function agentIdentity() {
|
|
37
|
+
return (
|
|
38
|
+
process.env.CONSTRUCT_AGENT_ID
|
|
39
|
+
|| process.env.CLAUDE_AGENT_ID
|
|
40
|
+
|| null
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function gatherAttribution({ now = new Date(), cwd } = {}) {
|
|
45
|
+
if (process.env.CONSTRUCT_ATTRIBUTION_DISABLE === '1') {
|
|
46
|
+
return {
|
|
47
|
+
createdBy: null,
|
|
48
|
+
createdByAgent: null,
|
|
49
|
+
createdAt: now.toISOString(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
createdBy: humanIdentity(cwd),
|
|
54
|
+
createdByAgent: agentIdentity(),
|
|
55
|
+
createdAt: now.toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function stampAttribution(record, attribution = gatherAttribution()) {
|
|
60
|
+
if (!record || typeof record !== 'object') return record;
|
|
61
|
+
return {
|
|
62
|
+
...record,
|
|
63
|
+
createdBy: attribution.createdBy,
|
|
64
|
+
createdByAgent: attribution.createdByAgent,
|
|
65
|
+
createdAt: attribution.createdAt,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function touchAttribution(record, attribution = gatherAttribution()) {
|
|
70
|
+
if (!record || typeof record !== 'object') return record;
|
|
71
|
+
return {
|
|
72
|
+
...record,
|
|
73
|
+
lastModifiedBy: attribution.createdBy,
|
|
74
|
+
lastModifiedByAgent: attribution.createdByAgent,
|
|
75
|
+
lastModifiedAt: attribution.createdAt,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -44,6 +44,7 @@ export const DEFAULT_INTAKE_CONFIG = Object.freeze({
|
|
|
44
44
|
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
45
45
|
includeProjectInbox: true,
|
|
46
46
|
includeDocsIntake: true,
|
|
47
|
+
includeArchetypeInbox: false,
|
|
47
48
|
});
|
|
48
49
|
|
|
49
50
|
export function intakeConfigPath(rootDir) {
|
|
@@ -98,6 +99,7 @@ export function loadIntakeConfig(rootDir, env = process.env) {
|
|
|
98
99
|
maxDepth,
|
|
99
100
|
includeProjectInbox: stored.includeProjectInbox !== false,
|
|
100
101
|
includeDocsIntake: stored.includeDocsIntake !== false,
|
|
102
|
+
includeArchetypeInbox: stored.includeArchetypeInbox === true,
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -113,6 +115,7 @@ export function saveIntakeConfig(rootDir, patch = {}) {
|
|
|
113
115
|
maxDepth: clampDepth(patch.maxDepth ?? current.maxDepth),
|
|
114
116
|
includeProjectInbox: patch.includeProjectInbox !== undefined ? Boolean(patch.includeProjectInbox) : current.includeProjectInbox,
|
|
115
117
|
includeDocsIntake: patch.includeDocsIntake !== undefined ? Boolean(patch.includeDocsIntake) : current.includeDocsIntake,
|
|
118
|
+
includeArchetypeInbox: patch.includeArchetypeInbox !== undefined ? Boolean(patch.includeArchetypeInbox) : current.includeArchetypeInbox,
|
|
116
119
|
};
|
|
117
120
|
|
|
118
121
|
const file = intakeConfigPath(rootDir);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/manifest.mjs — SHA-256 dedup manifest for intake-archetype projects.
|
|
3
|
+
*
|
|
4
|
+
* Projects that scaffold the intake archetype (profile.capabilities.intake)
|
|
5
|
+
* drop raw source files into `inbox/`. The manifest records which source
|
|
6
|
+
* files have already been processed (by content hash, not path) so the
|
|
7
|
+
* intake runtime can refuse to reprocess an unchanged file and so a moved
|
|
8
|
+
* or renamed file still dedups correctly.
|
|
9
|
+
*
|
|
10
|
+
* Storage: <projectRoot>/.cx/intake/manifest.json
|
|
11
|
+
*
|
|
12
|
+
* Shape:
|
|
13
|
+
* {
|
|
14
|
+
* "version": 1,
|
|
15
|
+
* "files": {
|
|
16
|
+
* "<sha256>": {
|
|
17
|
+
* "sourcePath": "inbox/notes-2026-05-29.md",
|
|
18
|
+
* "processedAt": "2026-05-29T19:00:00.000Z",
|
|
19
|
+
* "intakeId": "intake-1779164832122-onxe",
|
|
20
|
+
* "createdBy": "Gerald Dagher <gerald@example.com>",
|
|
21
|
+
* "createdByAgent": "claude-opus-4-7"
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Pure functions over the JSON. Callers own the rootDir; no implicit cwd.
|
|
27
|
+
* Atomic writes go through write-rename so a crash mid-write cannot leave
|
|
28
|
+
* the manifest half-written.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createHash } from 'node:crypto';
|
|
32
|
+
import {
|
|
33
|
+
existsSync,
|
|
34
|
+
mkdirSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
renameSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
} from 'node:fs';
|
|
39
|
+
import { join } from 'node:path';
|
|
40
|
+
|
|
41
|
+
export const MANIFEST_REL_PATH = '.cx/intake/manifest.json';
|
|
42
|
+
export const MANIFEST_VERSION = 1;
|
|
43
|
+
|
|
44
|
+
function manifestPath(rootDir) {
|
|
45
|
+
return join(rootDir, MANIFEST_REL_PATH);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function emptyManifest() {
|
|
49
|
+
return { version: MANIFEST_VERSION, files: {} };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function loadManifest(rootDir) {
|
|
53
|
+
const p = manifestPath(rootDir);
|
|
54
|
+
if (!existsSync(p)) return emptyManifest();
|
|
55
|
+
try {
|
|
56
|
+
const raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
57
|
+
if (!raw || typeof raw !== 'object' || typeof raw.files !== 'object') {
|
|
58
|
+
return emptyManifest();
|
|
59
|
+
}
|
|
60
|
+
return raw;
|
|
61
|
+
} catch {
|
|
62
|
+
return emptyManifest();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function saveManifest(rootDir, manifest) {
|
|
67
|
+
const p = manifestPath(rootDir);
|
|
68
|
+
const dir = join(rootDir, '.cx', 'intake');
|
|
69
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
70
|
+
const payload = JSON.stringify(manifest, null, 2) + '\n';
|
|
71
|
+
const tmp = `${p}.tmp`;
|
|
72
|
+
writeFileSync(tmp, payload, 'utf8');
|
|
73
|
+
renameSync(tmp, p);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function sha256Of(buffer) {
|
|
77
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function hasFile(rootDir, sha) {
|
|
81
|
+
if (!sha) return false;
|
|
82
|
+
const manifest = loadManifest(rootDir);
|
|
83
|
+
return Boolean(manifest.files[sha]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function recordFile(rootDir, sha, entry) {
|
|
87
|
+
if (!sha) throw new Error('recordFile: sha is required');
|
|
88
|
+
const manifest = loadManifest(rootDir);
|
|
89
|
+
manifest.files[sha] = {
|
|
90
|
+
sourcePath: entry?.sourcePath ?? null,
|
|
91
|
+
processedAt: entry?.processedAt ?? new Date().toISOString(),
|
|
92
|
+
intakeId: entry?.intakeId ?? null,
|
|
93
|
+
createdBy: entry?.createdBy ?? null,
|
|
94
|
+
createdByAgent: entry?.createdByAgent ?? null,
|
|
95
|
+
};
|
|
96
|
+
saveManifest(rootDir, manifest);
|
|
97
|
+
return manifest.files[sha];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function manifestStats(rootDir) {
|
|
101
|
+
const manifest = loadManifest(rootDir);
|
|
102
|
+
const entries = Object.values(manifest.files);
|
|
103
|
+
return {
|
|
104
|
+
total: entries.length,
|
|
105
|
+
version: manifest.version,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/intake/poll-lock.mjs — pid-stamped file lock around InboxWatcher.poll().
|
|
3
|
+
*
|
|
4
|
+
* Two pollers (the embed daemon and a manual `construct intake process`)
|
|
5
|
+
* share the same state file + manifest. Without serialization, both can
|
|
6
|
+
* decide a freshly-dropped file is unprocessed in the window between
|
|
7
|
+
* "read state" and "write state," producing two intake packets for one
|
|
8
|
+
* source. The lock makes one poll iteration atomic from the system's
|
|
9
|
+
* perspective: the second caller waits, retries until the timeout, or
|
|
10
|
+
* raises POLL_LOCK_BUSY for the CLI to render a clear refusal.
|
|
11
|
+
*
|
|
12
|
+
* Lockfile shape (JSON at <rootDir>/.cx/runtime/inbox-poll.lock):
|
|
13
|
+
* { pid, actor, startedAt, timeoutAt? }
|
|
14
|
+
*
|
|
15
|
+
* Stale detection: a lock whose pid is gone, or whose timeoutAt has
|
|
16
|
+
* passed, is cleared on the next acquire attempt. Mirrors the
|
|
17
|
+
* lib/beads-lock.mjs API shape so callers familiar with one know the other.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
export const POLL_LOCK_REL_PATH = '.cx/runtime/inbox-poll.lock';
|
|
24
|
+
export const POLL_LOCK_BUSY = 'POLL_LOCK_BUSY';
|
|
25
|
+
|
|
26
|
+
function lockPathFor(rootDir) {
|
|
27
|
+
return join(rootDir, POLL_LOCK_REL_PATH);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nowIso() {
|
|
31
|
+
return new Date().toISOString();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function afterMs(ms) {
|
|
35
|
+
return new Date(Date.now() + ms).toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function processExists(pid) {
|
|
39
|
+
if (!pid || typeof pid !== 'number') return false;
|
|
40
|
+
try {
|
|
41
|
+
process.kill(pid, 0);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function readPollLock(rootDir) {
|
|
49
|
+
const p = lockPathFor(rootDir);
|
|
50
|
+
if (!existsSync(p)) return null;
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(readFileSync(p, 'utf8'));
|
|
53
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
54
|
+
return raw;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isPollLockStale(lock) {
|
|
61
|
+
if (!lock) return false;
|
|
62
|
+
if (lock.pid && !processExists(lock.pid)) return true;
|
|
63
|
+
if (lock.timeoutAt && new Date(lock.timeoutAt) < new Date()) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearStale(rootDir) {
|
|
68
|
+
const lock = readPollLock(rootDir);
|
|
69
|
+
if (lock && isPollLockStale(lock)) {
|
|
70
|
+
rmSync(lockPathFor(rootDir), { force: true });
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function writeLockFile(rootDir, lock) {
|
|
77
|
+
const p = lockPathFor(rootDir);
|
|
78
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
79
|
+
writeFileSync(p, JSON.stringify(lock, null, 2) + '\n', 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Acquire the intake poll lock. Resolves with the lock object on success,
|
|
84
|
+
* rejects with an Error whose `code` is POLL_LOCK_BUSY when the wait window
|
|
85
|
+
* elapses without acquisition. The rejection's `holder` field carries the
|
|
86
|
+
* blocking lock's metadata for downstream error rendering.
|
|
87
|
+
*/
|
|
88
|
+
export async function acquirePollLock({
|
|
89
|
+
rootDir,
|
|
90
|
+
actor = 'unknown',
|
|
91
|
+
command = '',
|
|
92
|
+
waitMs = 0,
|
|
93
|
+
timeoutMs = 5 * 60 * 1000,
|
|
94
|
+
pollIntervalMs = 100,
|
|
95
|
+
} = {}) {
|
|
96
|
+
if (!rootDir) throw new Error('acquirePollLock: rootDir is required');
|
|
97
|
+
const deadline = Date.now() + Math.max(0, waitMs);
|
|
98
|
+
|
|
99
|
+
while (true) {
|
|
100
|
+
clearStale(rootDir);
|
|
101
|
+
const existing = readPollLock(rootDir);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
const lock = {
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
actor,
|
|
106
|
+
command,
|
|
107
|
+
startedAt: nowIso(),
|
|
108
|
+
timeoutAt: timeoutMs > 0 ? afterMs(timeoutMs) : undefined,
|
|
109
|
+
};
|
|
110
|
+
writeLockFile(rootDir, lock);
|
|
111
|
+
return lock;
|
|
112
|
+
}
|
|
113
|
+
if (Date.now() >= deadline) {
|
|
114
|
+
const err = new Error(
|
|
115
|
+
`intake poll lock held by ${existing.actor || 'unknown'} (pid ${existing.pid}) since ${existing.startedAt}`,
|
|
116
|
+
);
|
|
117
|
+
err.code = POLL_LOCK_BUSY;
|
|
118
|
+
err.holder = existing;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Release the lock iff the calling process owns it. Returns true on release,
|
|
127
|
+
* false when the lock was missing, expired, or owned by someone else.
|
|
128
|
+
*/
|
|
129
|
+
export function releasePollLock(rootDir) {
|
|
130
|
+
const lock = readPollLock(rootDir);
|
|
131
|
+
if (lock && lock.pid === process.pid) {
|
|
132
|
+
rmSync(lockPathFor(rootDir), { force: true });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
package/lib/intake/prepare.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/intake/prepare.mjs — turn a fresh inbox ingestion into an R&D intake packet.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Triggered by `InboxWatcher.poll()` after each successful file ingestion.
|
|
5
5
|
* Four deterministic preparation steps, then write the result to the
|
|
6
6
|
* intake queue:
|
|
7
7
|
*
|
|
@@ -14,11 +14,16 @@
|
|
|
14
14
|
* 4. Run classifyRdIntake to produce the R&D triage object (intake type,
|
|
15
15
|
* owner persona, recommended chain, action, risk).
|
|
16
16
|
*
|
|
17
|
+
* After enqueue, fires a signal.promoted lifecycle event when the triage
|
|
18
|
+
* confidence crosses SIGNAL_PROMOTION_THRESHOLD (matches the default
|
|
19
|
+
* tag-vocabulary auto_threshold), so subscribers can act on confidently
|
|
20
|
+
* typed signals without polling the queue.
|
|
21
|
+
*
|
|
17
22
|
* The agent — invoked manually or via session-start hook nudge — reads
|
|
18
23
|
* the resulting `.cx/intake/pending/<id>.json` and does the real
|
|
19
24
|
* comparison work (overlap with existing PRD? contradicts ADR? new RFC
|
|
20
|
-
* candidate?). The daemon never calls an LLM
|
|
21
|
-
* daemon cheap and predictable
|
|
25
|
+
* candidate?). The daemon never calls an LLM; the separation keeps the
|
|
26
|
+
* daemon cheap and predictable while model spend stays with the agent.
|
|
22
27
|
*/
|
|
23
28
|
|
|
24
29
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -30,11 +35,19 @@ import { createIntakeQueue } from './queue.mjs';
|
|
|
30
35
|
import { classifyRdIntake } from './classify.mjs';
|
|
31
36
|
import { detectCustomerMentions, linkSignalToCustomer, updateCustomerProfile } from '../embed/customer-profiles.mjs';
|
|
32
37
|
import { resolveActiveProfile } from '../profiles/loader.mjs';
|
|
38
|
+
import { emitBestEffort as emitRoleEvent } from '../roles/event-bus.mjs';
|
|
39
|
+
import { gatherAttribution, stampAttribution } from './attribution.mjs';
|
|
40
|
+
import { MANIFEST_REL_PATH } from './manifest.mjs';
|
|
33
41
|
|
|
34
42
|
const DEFAULT_RELATED_LIMIT = 5;
|
|
35
43
|
const EXCERPT_CHARS = 800;
|
|
36
44
|
const QUERY_CHARS = 500;
|
|
37
45
|
|
|
46
|
+
// Confidence at which a triage signal is considered "promoted" from
|
|
47
|
+
// uncertain to confidently classified. Mirrors the default tag-vocabulary
|
|
48
|
+
// auto_threshold.
|
|
49
|
+
const SIGNAL_PROMOTION_THRESHOLD = 0.70;
|
|
50
|
+
|
|
38
51
|
export async function prepareIntakeForIngestedFile({
|
|
39
52
|
rootDir,
|
|
40
53
|
ingestedFile,
|
|
@@ -85,7 +98,7 @@ export async function prepareIntakeForIngestedFile({
|
|
|
85
98
|
|
|
86
99
|
const droppedInfo = ingestedFile.droppedInfo ?? [];
|
|
87
100
|
|
|
88
|
-
const
|
|
101
|
+
const baseEntry = {
|
|
89
102
|
intake: {
|
|
90
103
|
sourcePath: ingestedFile.sourcePath,
|
|
91
104
|
outputPath: ingestedFile.outputPath,
|
|
@@ -101,10 +114,35 @@ export async function prepareIntakeForIngestedFile({
|
|
|
101
114
|
customers: customers.length ? customers : undefined,
|
|
102
115
|
};
|
|
103
116
|
|
|
117
|
+
// Capability detection mirrors inbox.mjs: stamp provenance onto the packet
|
|
118
|
+
// when the project's intake manifest is present (init scaffolds it for
|
|
119
|
+
// archetype-enabled profiles). Keeps non-archetype packets shape-identical
|
|
120
|
+
// to the pre-attribution behavior so downstream consumers see no diff.
|
|
121
|
+
|
|
122
|
+
const archetypeOn = existsSync(`${rootDir}/${MANIFEST_REL_PATH}`);
|
|
123
|
+
const entry = archetypeOn ? stampAttribution(baseEntry, gatherAttribution({ cwd: rootDir })) : baseEntry;
|
|
124
|
+
|
|
104
125
|
const intakeQueue = queue || createIntakeQueue(rootDir, env);
|
|
105
126
|
const result = intakeQueue.enqueue(entry);
|
|
106
127
|
const intakeId = result?.id || '';
|
|
107
128
|
|
|
129
|
+
// Promotion fires once a triage signal crosses the auto-classification
|
|
130
|
+
// threshold, giving subscribers a chance to act on a confidently typed
|
|
131
|
+
// signal without polling the intake queue.
|
|
132
|
+
|
|
133
|
+
if ((triage?.confidence ?? 0) >= SIGNAL_PROMOTION_THRESHOLD && triage?.intakeType && triage.intakeType !== 'unknown') {
|
|
134
|
+
emitRoleEvent('signal.promoted', {
|
|
135
|
+
summary: `${triage.intakeType} (${triage.confidence.toFixed(2)})`,
|
|
136
|
+
context: {
|
|
137
|
+
intakeId,
|
|
138
|
+
intakeType: triage.intakeType,
|
|
139
|
+
confidence: triage.confidence,
|
|
140
|
+
sourcePath: ingestedFile.sourcePath,
|
|
141
|
+
primaryOwner: triage.primaryOwner,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
108
146
|
// Update customer profiles with new evidence
|
|
109
147
|
for (const mention of customerMentions) {
|
|
110
148
|
try {
|
package/lib/knowledge/search.mjs
CHANGED
|
@@ -80,6 +80,18 @@ function buildSourceList(repoRoot = REPO_ROOT) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// User-ingested documents (PDF/Office/audio routed through docling + whisper)
|
|
84
|
+
const ingestRoot = join(repoRoot, '.cx', 'ingest');
|
|
85
|
+
if (existsSync(ingestRoot)) {
|
|
86
|
+
for (const entry of readdirSync(ingestRoot)) {
|
|
87
|
+
const dir = join(ingestRoot, entry);
|
|
88
|
+
const markdown = join(dir, 'markdown.md');
|
|
89
|
+
if (!/^[a-f0-9]{64}$/.test(entry)) continue;
|
|
90
|
+
if (!existsSync(markdown)) continue;
|
|
91
|
+
sources.push({ path: markdown, rel: relative(repoRoot, markdown), priority: 4, source: 'ingest' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
return sources;
|
|
84
96
|
}
|
|
85
97
|
|