@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
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @p95ms 300
|
|
8
8
|
* @maxBlockingScope SessionStart
|
|
9
|
+
*
|
|
10
|
+
* @lifecycle SessionStart
|
|
11
|
+
* @matcher *
|
|
12
|
+
* @exits 0 = pass
|
|
9
13
|
*/
|
|
10
14
|
import { readFileSync, existsSync, statSync, writeFileSync } from 'fs';
|
|
11
15
|
import { execSync } from 'child_process';
|
|
@@ -267,7 +271,7 @@ try {
|
|
|
267
271
|
if (hasSources) {
|
|
268
272
|
const parts = [];
|
|
269
273
|
if (repos.length > 0) parts.push(`GitHub repos: ${repos.join(', ')}`);
|
|
270
|
-
if (jiraConfigured) parts.push(
|
|
274
|
+
if (jiraConfigured) parts.push('Jira: configured');
|
|
271
275
|
if (linearConfigured) parts.push('Linear: configured');
|
|
272
276
|
// Surface which sources are wired so `provider_fetch` is the obvious
|
|
273
277
|
// tool for questions about them. The "fetch first, don't answer from
|
|
@@ -321,6 +325,49 @@ try {
|
|
|
321
325
|
brokerStatusNote = `\n${buildBrokerStatusLine({ env: process.env })}\n`;
|
|
322
326
|
} catch { /* best effort */ }
|
|
323
327
|
|
|
328
|
+
// Missing-env-vars notice. Reads .env.example from the project root,
|
|
329
|
+
// compares against .env and process.env, lists keys whose example value
|
|
330
|
+
// is a placeholder and whose live value is unset. Empty string when the
|
|
331
|
+
// project has no .env.example or all required keys are populated.
|
|
332
|
+
|
|
333
|
+
const envCheckNote = buildEnvCheckNote(cwd);
|
|
334
|
+
|
|
335
|
+
function buildEnvCheckNote(rootDir) {
|
|
336
|
+
const examplePath = join(rootDir, '.env.example');
|
|
337
|
+
if (!existsSync(examplePath)) return '';
|
|
338
|
+
const example = parseEnvFile(examplePath);
|
|
339
|
+
if (example.size === 0) return '';
|
|
340
|
+
const envFile = parseEnvFile(join(rootDir, '.env'));
|
|
341
|
+
const PLACEHOLDER = /^(YOUR_|<|__|\$\{|REPLACE|ADD_|INSERT_|xxx|TODO)/i;
|
|
342
|
+
const missing = [];
|
|
343
|
+
for (const [key, exampleVal] of example) {
|
|
344
|
+
const required = !exampleVal || PLACEHOLDER.test(exampleVal);
|
|
345
|
+
if (!required) continue;
|
|
346
|
+
const liveFromEnvFile = envFile.has(key) && envFile.get(key) !== '' && !PLACEHOLDER.test(envFile.get(key));
|
|
347
|
+
const liveFromProcessEnv = process.env[key] && !PLACEHOLDER.test(process.env[key]);
|
|
348
|
+
if (!liveFromEnvFile && !liveFromProcessEnv) missing.push(key);
|
|
349
|
+
}
|
|
350
|
+
if (missing.length === 0) return '';
|
|
351
|
+
const noun = missing.length === 1 ? 'variable' : 'variables';
|
|
352
|
+
const list = missing.map((k) => ` - ${k}`).join('\n');
|
|
353
|
+
return `\n## Environment check — ${missing.length} required ${noun} not set\n${list}\nAdd these to .env before running the app.\n`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parseEnvFile(p) {
|
|
357
|
+
const map = new Map();
|
|
358
|
+
if (!existsSync(p)) return map;
|
|
359
|
+
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
360
|
+
const stripped = line.trim();
|
|
361
|
+
if (!stripped || stripped.startsWith('#')) continue;
|
|
362
|
+
const eq = stripped.indexOf('=');
|
|
363
|
+
if (eq === -1) continue;
|
|
364
|
+
const key = stripped.slice(0, eq).trim();
|
|
365
|
+
const val = stripped.slice(eq + 1).trim();
|
|
366
|
+
if (key) map.set(key, val);
|
|
367
|
+
}
|
|
368
|
+
return map;
|
|
369
|
+
}
|
|
370
|
+
|
|
324
371
|
// Permission posture: surface gaps in ~/.claude/settings.json permissions.allow
|
|
325
372
|
// so the agent can ask the user to close them instead of stumbling into the
|
|
326
373
|
// classifier mid-task. Empty string when no gaps — keeps the banner small.
|
|
@@ -331,7 +378,7 @@ try {
|
|
|
331
378
|
permissionPostureNote = buildPermissionPostureLine({ cwd });
|
|
332
379
|
} catch { /* best effort */ }
|
|
333
380
|
|
|
334
|
-
process.stdout.write(`${header}\n${body}${stateNote}${efficiencyNote}${observationsNote}${concurrencyNote}${skillScopeNote}${dropNote}${embedStatusNote}${sourcesNote}${selfKnowledgeNote}${rolesNote}${intakeReviewNote}${brokerStatusNote}${permissionPostureNote}\n${footer}${pendingNote}\n`);
|
|
381
|
+
process.stdout.write(`${header}\n${body}${stateNote}${efficiencyNote}${observationsNote}${concurrencyNote}${skillScopeNote}${dropNote}${embedStatusNote}${sourcesNote}${selfKnowledgeNote}${rolesNote}${intakeReviewNote}${brokerStatusNote}${permissionPostureNote}${envCheckNote}\n${footer}${pendingNote}\n`);
|
|
335
382
|
// Auto-bootstrap the policy-engine gate. session-start has just emitted
|
|
336
383
|
// branch + recent commits + prior observations + context-state — that is
|
|
337
384
|
// already grounding; mark the session bootstrapped so the PreToolUse rule
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/session-tracking-refresh.mjs — keep the project's tracking
|
|
4
|
+
* surfaces current at session end.
|
|
5
|
+
*
|
|
6
|
+
* Runs as a Stop hook in parallel with the other Stop-time hooks. Touches
|
|
7
|
+
* three surfaces:
|
|
8
|
+
*
|
|
9
|
+
* - `.cx/context.md` + `.cx/context.json` — refresh Active Work, Recent
|
|
10
|
+
* Decisions, and Architecture Notes from the session's observations,
|
|
11
|
+
* commits, and bead state changes.
|
|
12
|
+
* - `plan.md` — sync the bead-status table with current `bd show <id>`
|
|
13
|
+
* truth; if every referenced bead has closed and the plan has been
|
|
14
|
+
* idle for >1h, archive a copy to `.cx/handoffs/<date>-plan-landed.md`
|
|
15
|
+
* and reset `plan.md` to the standard template.
|
|
16
|
+
*
|
|
17
|
+
* Best-effort. Failures degrade silently. Wall-clock budgeted at 2000 ms
|
|
18
|
+
* total — bd shells out, and a plan with several bead refs needs one
|
|
19
|
+
* `bd show` per ref. Exits 0 on any error so the session close path is
|
|
20
|
+
* never blocked.
|
|
21
|
+
*
|
|
22
|
+
* Only runs inside Construct projects (presence of `.cx/`) — same gate as
|
|
23
|
+
* the other Stop hooks.
|
|
24
|
+
*
|
|
25
|
+
* @lifecycle Stop
|
|
26
|
+
* @matcher *
|
|
27
|
+
* @p95ms 2000
|
|
28
|
+
* @maxBlockingScope none (Stop, non-blocking)
|
|
29
|
+
* @exits 0 = pass
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { existsSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { readHookInput } from './_lib/input.mjs';
|
|
35
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
36
|
+
|
|
37
|
+
const HARD_BUDGET_MS = 2000;
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
|
|
40
|
+
const input = readHookInput();
|
|
41
|
+
const cwd = input?.cwd || process.cwd();
|
|
42
|
+
|
|
43
|
+
// Only run inside Construct projects.
|
|
44
|
+
if (!existsSync(join(cwd, '.cx'))) process.exit(0);
|
|
45
|
+
|
|
46
|
+
const deadline = setTimeout(() => process.exit(0), HARD_BUDGET_MS);
|
|
47
|
+
deadline.unref();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Order matters. archivePlanIfLanded reads plan.md's mtime to decide
|
|
51
|
+
// whether the plan has been idle long enough to retire. syncPlanFile
|
|
52
|
+
// mutates plan.md when bead statuses drift — which bumps mtime and
|
|
53
|
+
// makes the plan look "recently touched" to the archive check. Run
|
|
54
|
+
// archive first (true idle mtime), then sync the survivor.
|
|
55
|
+
const { archivePlanIfLanded, syncPlanFile, refreshContextMd } = await import('../tracking-surfaces.mjs');
|
|
56
|
+
await archivePlanIfLanded({ rootDir: cwd });
|
|
57
|
+
await syncPlanFile({ rootDir: cwd });
|
|
58
|
+
await refreshContextMd({ rootDir: cwd });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logHookFailure({ hook: 'session-tracking-refresh', err, phase: 'refresh' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const elapsed = Date.now() - startedAt;
|
|
64
|
+
if (elapsed > HARD_BUDGET_MS) {
|
|
65
|
+
try {
|
|
66
|
+
process.stderr.write(`[session-tracking-refresh] over budget: ${elapsed}ms\n`);
|
|
67
|
+
} catch { /* stderr closed */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.exit(0);
|
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @p95ms 500
|
|
13
13
|
* @maxBlockingScope Stop
|
|
14
|
+
*
|
|
15
|
+
* @lifecycle Stop
|
|
16
|
+
* @matcher *
|
|
17
|
+
* @exits 0 = pass
|
|
14
18
|
*/
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync,
|
|
19
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
20
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
21
|
+
import { resolveProjectScope } from '../project-root.mjs';
|
|
16
22
|
import { execSync } from 'child_process';
|
|
17
23
|
import { homedir } from 'os';
|
|
18
24
|
import { join, dirname } from 'path';
|
|
@@ -228,7 +234,12 @@ try {
|
|
|
228
234
|
if (agentName) entry.agent = agentName;
|
|
229
235
|
if (taskKey) entry.task_key = taskKey;
|
|
230
236
|
|
|
231
|
-
|
|
237
|
+
// Cross-project ledger — keep at user scope but tag with projectId so a
|
|
238
|
+
// reader can attribute spend to a specific project.
|
|
239
|
+
|
|
240
|
+
const scope = resolveProjectScope();
|
|
241
|
+
if (scope?.projectId) entry.projectId = scope.projectId;
|
|
242
|
+
try { appendBounded('session-cost', costLogPath, JSON.stringify(entry) + '\n'); } catch { /* non-critical */ }
|
|
232
243
|
|
|
233
244
|
totalCostUsd += priced.costUsd;
|
|
234
245
|
turnsLogged += 1;
|
package/lib/hooks/test-watch.mjs
CHANGED
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @p95ms 30
|
|
10
10
|
* @maxBlockingScope none (async, non-blocking)
|
|
11
|
+
*
|
|
12
|
+
* @lifecycle PostToolUse
|
|
13
|
+
* @matcher Bash
|
|
14
|
+
* @exits 0 = pass
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
|
-
import { readFileSync, existsSync,
|
|
17
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
14
18
|
import { homedir } from 'node:os';
|
|
15
19
|
import { join } from 'node:path';
|
|
16
20
|
import { logHookFailure } from './_lib/log.mjs';
|
|
@@ -56,13 +60,13 @@ try {
|
|
|
56
60
|
}
|
|
57
61
|
state[name] = { failedAt: now };
|
|
58
62
|
}
|
|
59
|
-
appendFileSync(flakeTrackerPath, '');
|
|
60
63
|
const trimmed = {};
|
|
61
64
|
for (const [k, v] of Object.entries(state)) {
|
|
62
65
|
if ((v.failedAt || v.passedAt || 0) > now - 24 * 60 * 60 * 1000) trimmed[k] = v;
|
|
63
66
|
}
|
|
64
|
-
const writeFileSync = (await import('node:fs')).writeFileSync;
|
|
65
67
|
writeFileSync(flakeTrackerPath, JSON.stringify(trimmed));
|
|
66
|
-
} catch {
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logHookFailure({ hook: 'test-watch', err, phase: 'flake-track' });
|
|
70
|
+
}
|
|
67
71
|
|
|
68
72
|
process.exit(0);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/chunker.mjs — paragraph-boundary chunker with sentence overlap.
|
|
3
|
+
*
|
|
4
|
+
* 2026-06 best practice notes:
|
|
5
|
+
* - Recursive/sentence chunking remains the high-recall default (~85-90%
|
|
6
|
+
* on 2026 RAG benchmarks). Semantic chunking gains only ~2-3% recall at
|
|
7
|
+
* ~14× the embedding cost and only matters above ~5k token docs.
|
|
8
|
+
* - Parent-document retrieval is the meaningful 2026 upgrade beyond
|
|
9
|
+
* paragraph chunking — track separately if recall plateaus.
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* 1. Split markdown by blank-line paragraph boundaries.
|
|
13
|
+
* 2. Pack paragraphs into chunks under maxChars (default 1500 ≈ 375 tokens).
|
|
14
|
+
* 3. Add 1-2 sentence overlap between consecutive chunks to preserve
|
|
15
|
+
* cross-chunk anchors.
|
|
16
|
+
* 4. Preserve markdown structure: never split inside a fenced code block.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_CHARS = 1500;
|
|
20
|
+
const DEFAULT_OVERLAP_SENTENCES = 2;
|
|
21
|
+
const SENTENCE_RE = /[^.!?]+[.!?]+["')\]]*\s*/g;
|
|
22
|
+
|
|
23
|
+
export function splitSentences(text) {
|
|
24
|
+
const matches = text.match(SENTENCE_RE);
|
|
25
|
+
if (matches && matches.length) return matches.map((s) => s.trim()).filter(Boolean);
|
|
26
|
+
return text.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tailSentences(text, count) {
|
|
30
|
+
const sentences = splitSentences(text);
|
|
31
|
+
return sentences.slice(-count).join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitParagraphs(markdown) {
|
|
35
|
+
const blocks = [];
|
|
36
|
+
let buffer = '';
|
|
37
|
+
let inFence = false;
|
|
38
|
+
for (const line of markdown.split('\n')) {
|
|
39
|
+
if (/^```/.test(line.trim())) inFence = !inFence;
|
|
40
|
+
if (!inFence && line.trim() === '' && buffer.trim()) {
|
|
41
|
+
blocks.push(buffer.trim());
|
|
42
|
+
buffer = '';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
buffer += line + '\n';
|
|
46
|
+
}
|
|
47
|
+
if (buffer.trim()) blocks.push(buffer.trim());
|
|
48
|
+
return blocks;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function chunkMarkdown(markdown, { maxChars = DEFAULT_MAX_CHARS, overlapSentences = DEFAULT_OVERLAP_SENTENCES } = {}) {
|
|
52
|
+
const paragraphs = splitParagraphs(markdown || '');
|
|
53
|
+
if (paragraphs.length === 0) return [];
|
|
54
|
+
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let current = '';
|
|
57
|
+
let chunkIndex = 0;
|
|
58
|
+
const flushChunk = () => {
|
|
59
|
+
if (!current.trim()) return;
|
|
60
|
+
const text = current.trim();
|
|
61
|
+
chunks.push({
|
|
62
|
+
index: chunkIndex++,
|
|
63
|
+
text,
|
|
64
|
+
chars: text.length,
|
|
65
|
+
});
|
|
66
|
+
const overlap = overlapSentences > 0 ? tailSentences(text, overlapSentences) : '';
|
|
67
|
+
current = overlap ? overlap + '\n\n' : '';
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
for (const para of paragraphs) {
|
|
71
|
+
if (current.length + para.length + 2 > maxChars && current.trim()) {
|
|
72
|
+
flushChunk();
|
|
73
|
+
}
|
|
74
|
+
if (para.length > maxChars) {
|
|
75
|
+
flushChunk();
|
|
76
|
+
const sentences = splitSentences(para);
|
|
77
|
+
let buf = '';
|
|
78
|
+
for (const s of sentences) {
|
|
79
|
+
if (buf.length + s.length + 1 > maxChars && buf.trim()) {
|
|
80
|
+
chunks.push({ index: chunkIndex++, text: buf.trim(), chars: buf.trim().length });
|
|
81
|
+
buf = (overlapSentences > 0 ? tailSentences(buf, overlapSentences) : '') + (overlapSentences > 0 ? ' ' : '');
|
|
82
|
+
}
|
|
83
|
+
buf += s + ' ';
|
|
84
|
+
}
|
|
85
|
+
if (buf.trim()) {
|
|
86
|
+
current = buf.trim() + '\n\n';
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
current += (current ? '\n\n' : '') + para;
|
|
91
|
+
}
|
|
92
|
+
flushChunk();
|
|
93
|
+
return chunks;
|
|
94
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/pipeline.mjs — orchestrate the content-hashed ingest pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Flow: hash → de-dup → extract via docling/whisper → chunk → write to
|
|
5
|
+
* .cx/ingest/<sha>/. Returns a record describing what was stored along
|
|
6
|
+
* with any droppedInfo surfaced by the extractor.
|
|
7
|
+
*
|
|
8
|
+
* Idempotent: re-ingesting the same content (by sha256) returns the
|
|
9
|
+
* existing record without re-extracting.
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { extractDocumentTextAsync } from '../document-extract.mjs';
|
|
14
|
+
import { hashFile, defaultIngestRoot, readRecord, writeRecord } from './store.mjs';
|
|
15
|
+
import { chunkMarkdown } from './chunker.mjs';
|
|
16
|
+
|
|
17
|
+
export async function ingestFile(filePath, { cwd = process.cwd(), force = false } = {}) {
|
|
18
|
+
const absPath = path.resolve(filePath);
|
|
19
|
+
const stat = await fs.stat(absPath);
|
|
20
|
+
if (!stat.isFile()) throw new Error(`not a regular file: ${absPath}`);
|
|
21
|
+
|
|
22
|
+
const root = defaultIngestRoot(cwd);
|
|
23
|
+
const sha256 = await hashFile(absPath);
|
|
24
|
+
|
|
25
|
+
if (!force) {
|
|
26
|
+
const existing = await readRecord(root, sha256);
|
|
27
|
+
if (existing) return { ...existing, status: 'cached' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extracted = await extractDocumentTextAsync(absPath);
|
|
31
|
+
const markdown = extracted.markdown ?? extracted.text ?? '';
|
|
32
|
+
const chunks = chunkMarkdown(markdown);
|
|
33
|
+
|
|
34
|
+
const source = {
|
|
35
|
+
sourcePath: absPath,
|
|
36
|
+
fileName: path.basename(absPath),
|
|
37
|
+
extension: extracted.extension,
|
|
38
|
+
bytes: stat.size,
|
|
39
|
+
sha256,
|
|
40
|
+
ingestedAt: new Date().toISOString(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const meta = {
|
|
44
|
+
extractionMethod: extracted.extractionMethod,
|
|
45
|
+
extractorMetadata: extracted.metadata ?? null,
|
|
46
|
+
droppedInfo: extracted.droppedInfo ?? [],
|
|
47
|
+
chunkCount: chunks.length,
|
|
48
|
+
chunkChars: chunks.reduce((a, c) => a + c.chars, 0),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const record = await writeRecord(root, { sha256, source, meta, markdown });
|
|
52
|
+
return { ...record, status: 'ingested', droppedInfo: meta.droppedInfo, chunks };
|
|
53
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/ingest/store.mjs — content-addressed ingest store at .cx/ingest/<hash>/.
|
|
3
|
+
*
|
|
4
|
+
* Each ingested document is stored under its SHA-256 content hash:
|
|
5
|
+
* .cx/ingest/<sha256>/source.json — original path, size, sha256, ingestedAt
|
|
6
|
+
* .cx/ingest/<sha256>/markdown.md — extracted markdown body
|
|
7
|
+
* .cx/ingest/<sha256>/meta.json — extractor metadata, droppedInfo, chunks
|
|
8
|
+
*
|
|
9
|
+
* Idempotent: re-ingesting a file with the same content is a no-op (returns
|
|
10
|
+
* the existing record). Re-running ingestion is the safe default.
|
|
11
|
+
*
|
|
12
|
+
* 2026-06 best practice notes:
|
|
13
|
+
* - SHA-256 is still the universal content-addressing primitive. BLAKE3 is
|
|
14
|
+
* ~2.3× faster on M-series and worth considering at multi-GB scale,
|
|
15
|
+
* but is not the industry default and adds an unfamiliar dependency.
|
|
16
|
+
* Revisit if ingest throughput becomes a measured bottleneck.
|
|
17
|
+
* - Storing markdown body separately from metadata keeps the embedding
|
|
18
|
+
* and search paths simple: they only read markdown.md and look up
|
|
19
|
+
* provenance from meta.json on demand.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import { createReadStream } from 'node:fs';
|
|
23
|
+
import fs from 'node:fs/promises';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
|
|
26
|
+
export function defaultIngestRoot(cwd = process.cwd()) {
|
|
27
|
+
return path.join(cwd, '.cx', 'ingest');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function hashFile(filePath) {
|
|
31
|
+
return new Promise((resolveHash, reject) => {
|
|
32
|
+
const hash = createHash('sha256');
|
|
33
|
+
const stream = createReadStream(filePath);
|
|
34
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
35
|
+
stream.on('error', reject);
|
|
36
|
+
stream.on('end', () => resolveHash(hash.digest('hex')));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recordDirFor(root, sha256) {
|
|
41
|
+
return path.join(root, sha256);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function readRecord(root, sha256) {
|
|
45
|
+
const dir = recordDirFor(root, sha256);
|
|
46
|
+
try {
|
|
47
|
+
const [source, meta, markdown] = await Promise.all([
|
|
48
|
+
fs.readFile(path.join(dir, 'source.json'), 'utf8').then(JSON.parse),
|
|
49
|
+
fs.readFile(path.join(dir, 'meta.json'), 'utf8').then(JSON.parse),
|
|
50
|
+
fs.readFile(path.join(dir, 'markdown.md'), 'utf8'),
|
|
51
|
+
]);
|
|
52
|
+
return { sha256, source, meta, markdown, dir };
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function writeRecord(root, { sha256, source, meta, markdown }) {
|
|
59
|
+
const dir = recordDirFor(root, sha256);
|
|
60
|
+
await fs.mkdir(dir, { recursive: true });
|
|
61
|
+
await Promise.all([
|
|
62
|
+
fs.writeFile(path.join(dir, 'source.json'), JSON.stringify(source, null, 2) + '\n', 'utf8'),
|
|
63
|
+
fs.writeFile(path.join(dir, 'meta.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8'),
|
|
64
|
+
fs.writeFile(path.join(dir, 'markdown.md'), markdown, 'utf8'),
|
|
65
|
+
]);
|
|
66
|
+
return { sha256, dir, source, meta, markdown };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function listRecords(root) {
|
|
70
|
+
let entries;
|
|
71
|
+
try { entries = await fs.readdir(root, { withFileTypes: true }); }
|
|
72
|
+
catch { return []; }
|
|
73
|
+
const records = [];
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
const sha = entry.name;
|
|
77
|
+
if (!/^[a-f0-9]{64}$/.test(sha)) continue;
|
|
78
|
+
const record = await readRecord(root, sha);
|
|
79
|
+
if (record) records.push(record);
|
|
80
|
+
}
|
|
81
|
+
return records;
|
|
82
|
+
}
|
package/lib/init-unified.mjs
CHANGED
|
@@ -812,12 +812,28 @@ async function main() {
|
|
|
812
812
|
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('./config/project-config.mjs');
|
|
813
813
|
const found = findProjectConfigPath(target);
|
|
814
814
|
const cfgPath = found || path.join(target, PROJECT_CONFIG_FILENAME);
|
|
815
|
-
|
|
815
|
+
|
|
816
|
+
// loadProjectConfig returns a wrapper { path, raw, config, source, errors }.
|
|
817
|
+
// Persist .raw (the on-disk JSON) so user customizations survive and
|
|
818
|
+
// defaults are not silently materialized into the file.
|
|
819
|
+
|
|
820
|
+
const loaded = found ? loadProjectConfig(target) : null;
|
|
821
|
+
const cfg = loaded?.raw ? { ...loaded.raw } : { version: 1 };
|
|
816
822
|
cfg.profile = profileId;
|
|
817
823
|
writeProjectConfig(cfgPath, cfg, { validate: true });
|
|
818
824
|
if (!quiet) console.log(`Profile set to ${profileId}.`);
|
|
819
825
|
}
|
|
820
|
-
|
|
826
|
+
|
|
827
|
+
// Resolve active profile to drive capability-gated scaffolding (intake
|
|
828
|
+
// archetype, attribution stamping). Falls back to rnd when no profile is
|
|
829
|
+
// configured — matches resolveActiveProfile semantics elsewhere.
|
|
830
|
+
|
|
831
|
+
const { resolveActiveProfile } = await import('./profiles/loader.mjs');
|
|
832
|
+
const activeProfile = resolveActiveProfile(target, profileId) ?? null;
|
|
833
|
+
const intakeCap = activeProfile?.capabilities?.intake ?? null;
|
|
834
|
+
const { gatherAttribution } = await import('./intake/attribution.mjs');
|
|
835
|
+
const attribution = intakeCap?.attribution ? gatherAttribution() : null;
|
|
836
|
+
|
|
821
837
|
// Always create core Construct files
|
|
822
838
|
writeStampedIfMissing({
|
|
823
839
|
targetRoot: target,
|
|
@@ -826,8 +842,9 @@ async function main() {
|
|
|
826
842
|
filePath: path.join(target, "AGENTS.md"),
|
|
827
843
|
content: buildAgentsGuide(projectName),
|
|
828
844
|
generator: "construct/init",
|
|
845
|
+
attribution,
|
|
829
846
|
});
|
|
830
|
-
|
|
847
|
+
|
|
831
848
|
writeStampedIfMissing({
|
|
832
849
|
targetRoot: target,
|
|
833
850
|
created,
|
|
@@ -835,17 +852,18 @@ async function main() {
|
|
|
835
852
|
filePath: path.join(target, "plan.md"),
|
|
836
853
|
content: buildPlanTemplate(),
|
|
837
854
|
generator: "construct/init",
|
|
855
|
+
attribution,
|
|
838
856
|
});
|
|
839
|
-
|
|
857
|
+
|
|
840
858
|
writeStampedIfMissing({
|
|
841
859
|
targetRoot: target,
|
|
842
860
|
created,
|
|
843
861
|
skipped,
|
|
844
862
|
filePath: path.join(target, ".cx", "context.json"),
|
|
845
|
-
content: buildContextJson(projectName),
|
|
863
|
+
content: buildContextJson(projectName, { attribution }),
|
|
846
864
|
generator: "construct/init",
|
|
847
865
|
});
|
|
848
|
-
|
|
866
|
+
|
|
849
867
|
writeStampedIfMissing({
|
|
850
868
|
targetRoot: target,
|
|
851
869
|
created,
|
|
@@ -853,6 +871,7 @@ async function main() {
|
|
|
853
871
|
filePath: path.join(target, ".cx", "context.md"),
|
|
854
872
|
content: buildContextMarkdown(),
|
|
855
873
|
generator: "construct/init",
|
|
874
|
+
attribution,
|
|
856
875
|
});
|
|
857
876
|
|
|
858
877
|
writeStampedIfMissing({
|
|
@@ -888,6 +907,56 @@ async function main() {
|
|
|
888
907
|
});
|
|
889
908
|
}
|
|
890
909
|
|
|
910
|
+
// Intake-archetype scaffolding. When the active profile declares
|
|
911
|
+
// capabilities.intake.inbox, scaffold a project-root inbox/ drop zone
|
|
912
|
+
// (with a .gitignore so raw drops never enter source) and seed the
|
|
913
|
+
// dedup manifest. The existing-structure detection already opted us
|
|
914
|
+
// out of clobbering a user's pipeline above.
|
|
915
|
+
|
|
916
|
+
if (intakeCap?.inbox && !inboxDecision.skip) {
|
|
917
|
+
const projectInbox = path.join(target, 'inbox');
|
|
918
|
+
if (!fs.existsSync(projectInbox)) {
|
|
919
|
+
fs.mkdirSync(projectInbox, { recursive: true });
|
|
920
|
+
created.push('inbox/');
|
|
921
|
+
}
|
|
922
|
+
const inboxGitignore = path.join(projectInbox, '.gitignore');
|
|
923
|
+
if (!fs.existsSync(inboxGitignore)) {
|
|
924
|
+
fs.writeFileSync(inboxGitignore, '*\n!.gitignore\n', 'utf8');
|
|
925
|
+
created.push('inbox/.gitignore');
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (intakeCap.dedup === 'sha256') {
|
|
929
|
+
const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
|
|
930
|
+
const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
|
|
931
|
+
if (!manifestExists) {
|
|
932
|
+
saveManifest(target, loadManifest(target));
|
|
933
|
+
created.push(MANIFEST_REL_PATH);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Ensure `.cx/` is in project .gitignore. .cx/ is runtime state — observations,
|
|
939
|
+
// sessions, vector index, and traces (with daily JSONL files that can exceed
|
|
940
|
+
// GitHub's 100 MB file-size limit). Idempotent: if `.cx/` (or a broader
|
|
941
|
+
// pattern like `.cx` / `*` / `**`) already matches, leave the file alone.
|
|
942
|
+
|
|
943
|
+
try {
|
|
944
|
+
const gitignorePath = path.join(target, '.gitignore');
|
|
945
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
946
|
+
const lines = existing.split('\n').map((l) => l.trim());
|
|
947
|
+
const alreadyIgnored = lines.some((l) =>
|
|
948
|
+
l === '.cx' || l === '.cx/' || l === '.cx/**' || l === '*' || l === '**'
|
|
949
|
+
);
|
|
950
|
+
if (!alreadyIgnored) {
|
|
951
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
952
|
+
const block = `${prefix}\n# Construct runtime state — local-only, never source\n.cx/\n`;
|
|
953
|
+
fs.writeFileSync(gitignorePath, existing + block, 'utf8');
|
|
954
|
+
created.push(existing.length === 0 ? '.gitignore (added .cx/)' : '.gitignore (appended .cx/)');
|
|
955
|
+
}
|
|
956
|
+
} catch (err) {
|
|
957
|
+
console.warn(`⚠️ Could not update .gitignore: ${err.message}`);
|
|
958
|
+
}
|
|
959
|
+
|
|
891
960
|
// Stage .construct/ launcher + sync .claude/ adapters so init produces the
|
|
892
961
|
// same project shape as a fresh `npm install` of the package as a dep.
|
|
893
962
|
|
|
@@ -911,23 +980,12 @@ async function main() {
|
|
|
911
980
|
created.push('.claude/ (agents + settings)');
|
|
912
981
|
}
|
|
913
982
|
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const homeResult = spawnSync(process.execPath, [syncScript], {
|
|
921
|
-
cwd: target,
|
|
922
|
-
stdio: 'pipe',
|
|
923
|
-
env: { ...process.env, CONSTRUCT_PROJECT_ROOT: target },
|
|
924
|
-
timeout: 120000,
|
|
925
|
-
});
|
|
926
|
-
if (homeResult.status !== 0) {
|
|
927
|
-
const errMsg = homeResult.stderr.toString().substring(0, 200);
|
|
928
|
-
console.warn(`⚠️ Home adapter sync had issues (exit ${homeResult.status}): ${errMsg}`);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
983
|
+
// construct init writes project scope only. Global wiring (the `construct`
|
|
984
|
+
// front-door agent in `~/.claude/agents/`, `~/.codex/agents/`, etc.) is
|
|
985
|
+
// installed once by `construct sync --global` or the npm postinstall.
|
|
986
|
+
// Specialists, slash commands, and skills live with the repo per each
|
|
987
|
+
// host's documented best-practice scope.
|
|
988
|
+
|
|
931
989
|
} catch (err) {
|
|
932
990
|
console.warn(`⚠️ Adapter staging failed: ${err.message}`);
|
|
933
991
|
}
|
|
@@ -987,6 +1045,15 @@ async function main() {
|
|
|
987
1045
|
// pipeline. User can opt in later via `construct config intake.includeProjectInbox=true`.
|
|
988
1046
|
|
|
989
1047
|
if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
|
|
1048
|
+
|
|
1049
|
+
// Archetype: flip includeArchetypeInbox on so resolveInboxDirs picks up
|
|
1050
|
+
// the project-root inbox/ drop zone alongside .cx/inbox/. Mirrors the
|
|
1051
|
+
// existing includeProjectInbox / includeDocsIntake toggles rather than
|
|
1052
|
+
// polluting parentDirs (which is reserved for user-explicit dirs).
|
|
1053
|
+
|
|
1054
|
+
if (intakeCap?.inbox) {
|
|
1055
|
+
intakeConfig.includeArchetypeInbox = true;
|
|
1056
|
+
}
|
|
990
1057
|
const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
|
|
991
1058
|
try {
|
|
992
1059
|
saveIntakeConfig(target, intakeConfig);
|