@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,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tracking-surfaces.mjs — automated maintenance for project-tracking files.
|
|
3
|
+
*
|
|
4
|
+
* Owns WHAT to refresh; the hooks that call these functions own WHEN. Three
|
|
5
|
+
* surfaces are covered:
|
|
6
|
+
*
|
|
7
|
+
* - `.cx/context.md` + `.cx/context.json` — project state. Refreshed from
|
|
8
|
+
* recent observations, commits, and bead state at the end of each
|
|
9
|
+
* session via `refreshContextMd()`.
|
|
10
|
+
* - `plan.md` — local working plan. Bead-status table synced via
|
|
11
|
+
* `syncPlanFile()` (thin wrapper around `syncPlanWithBeads` in
|
|
12
|
+
* lib/beads-automation.mjs). When every referenced bead is closed,
|
|
13
|
+
* `archivePlanIfLanded()` stamps a landed footer, copies the plan into
|
|
14
|
+
* `.cx/handoffs/`, and resets `plan.md` to the standard template.
|
|
15
|
+
* - Beads — `closeBeadsFromPrRefs()` parses a merged PR's body for
|
|
16
|
+
* `Refs:` / `Closes:` / `Fixes:` lines and closes the named beads with
|
|
17
|
+
* a "Merged via PR #N (sha)" reason.
|
|
18
|
+
*
|
|
19
|
+
* Every export is best-effort: caught failures degrade to a structured
|
|
20
|
+
* result object, never throw. Caller hooks log to stderr via logHookFailure
|
|
21
|
+
* if they care about the failure mode.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
|
|
28
|
+
import { extractBeadsFromPlan, syncPlanWithBeads } from './beads-automation.mjs';
|
|
29
|
+
import { assertBeadId } from './beads-client.mjs';
|
|
30
|
+
import { buildPlanTemplate } from './project-init-shared.mjs';
|
|
31
|
+
|
|
32
|
+
const CONTEXT_RECENT_DAYS = 30;
|
|
33
|
+
const CONTEXT_MAX_ITEMS_PER_SECTION = 10;
|
|
34
|
+
|
|
35
|
+
const SECTION_HEADERS = {
|
|
36
|
+
activeWork: '## Active Work',
|
|
37
|
+
recentDecisions: '## Recent Decisions',
|
|
38
|
+
architectureNotes: '## Architecture Notes',
|
|
39
|
+
openQuestions: '## Open Questions',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// context.md / context.json refresh
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Pull recent activity from observations, git, and beads into the managed
|
|
48
|
+
* sections of `.cx/context.md`. Preserves any non-managed sections the user
|
|
49
|
+
* has authored. Stamps `.cx/context.json` with the refresh timestamp and
|
|
50
|
+
* derived counts so a session-start digest can tell when content is fresh.
|
|
51
|
+
*/
|
|
52
|
+
export async function refreshContextMd({ rootDir, now = new Date() } = {}) {
|
|
53
|
+
if (!rootDir) return { ok: false, reason: 'rootDir-required' };
|
|
54
|
+
const contextMdPath = join(rootDir, '.cx', 'context.md');
|
|
55
|
+
const contextJsonPath = join(rootDir, '.cx', 'context.json');
|
|
56
|
+
if (!existsSync(contextMdPath)) return { ok: false, reason: 'no-context-md' };
|
|
57
|
+
|
|
58
|
+
const cutoff = new Date(now.getTime() - CONTEXT_RECENT_DAYS * 24 * 60 * 60 * 1000);
|
|
59
|
+
const observations = collectRecentObservations(rootDir, cutoff);
|
|
60
|
+
const commits = collectRecentCommits(rootDir, cutoff);
|
|
61
|
+
const closedBeads = collectRecentlyClosedBeads(rootDir, cutoff);
|
|
62
|
+
const openBeads = collectInProgressBeads(rootDir);
|
|
63
|
+
|
|
64
|
+
const sections = {
|
|
65
|
+
activeWork: formatActiveWork(openBeads),
|
|
66
|
+
recentDecisions: formatRecentDecisions(observations, commits, closedBeads),
|
|
67
|
+
architectureNotes: formatArchitectureNotes(observations),
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const before = readFileSync(contextMdPath, 'utf8');
|
|
71
|
+
const after = rewriteManagedSections(before, sections);
|
|
72
|
+
if (after !== before) {
|
|
73
|
+
writeFileSync(contextMdPath, after, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const json = readJsonSafe(contextJsonPath) ?? { format: 'json', source: 'construct' };
|
|
77
|
+
json.lastRefreshAt = now.toISOString();
|
|
78
|
+
json.activeWork = openBeads.map((b) => ({ id: b.id, title: b.title, status: b.status }));
|
|
79
|
+
json.recentDecisions = closedBeads.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION).map((b) => ({
|
|
80
|
+
id: b.id,
|
|
81
|
+
title: b.title,
|
|
82
|
+
closedAt: b.closed || b.updated || b.updatedAt,
|
|
83
|
+
}));
|
|
84
|
+
json.architectureNotes = observations
|
|
85
|
+
.filter((o) => o.category === 'architecture' || o.category === 'decision')
|
|
86
|
+
.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION)
|
|
87
|
+
.map((o) => ({ summary: o.summary, ts: o.ts }));
|
|
88
|
+
writeFileSync(contextJsonPath, JSON.stringify(json, null, 2) + '\n', 'utf8');
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
ok: true,
|
|
92
|
+
changed: after !== before,
|
|
93
|
+
counts: {
|
|
94
|
+
activeWork: openBeads.length,
|
|
95
|
+
recentDecisions: closedBeads.length,
|
|
96
|
+
observations: observations.length,
|
|
97
|
+
commits: commits.length,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// plan.md sync and archive
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run the existing plan↔beads sync. Wrapper so the Stop hook calls one
|
|
108
|
+
* stable name even if the underlying implementation changes later.
|
|
109
|
+
*/
|
|
110
|
+
export async function syncPlanFile({ rootDir }) {
|
|
111
|
+
if (!rootDir) return { ok: false, reason: 'rootDir-required' };
|
|
112
|
+
try {
|
|
113
|
+
const changed = await syncPlanWithBeads({ cwd: rootDir });
|
|
114
|
+
return { ok: true, changed };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { ok: false, reason: 'sync-threw', error: err?.message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* When every bead referenced in `plan.md` is closed AND the plan has not
|
|
122
|
+
* been touched in the last hour, stamp a "Landed" footer, copy the plan to
|
|
123
|
+
* `.cx/handoffs/<date>-plan-landed.md`, and reset `plan.md` to the template.
|
|
124
|
+
* Otherwise no-op.
|
|
125
|
+
*/
|
|
126
|
+
export async function archivePlanIfLanded({ rootDir, now = new Date() } = {}) {
|
|
127
|
+
if (!rootDir) return { ok: false, reason: 'rootDir-required' };
|
|
128
|
+
const planPath = join(rootDir, 'plan.md');
|
|
129
|
+
if (!existsSync(planPath)) return { ok: false, reason: 'no-plan' };
|
|
130
|
+
|
|
131
|
+
let stat;
|
|
132
|
+
try { stat = statSync(planPath); }
|
|
133
|
+
catch { return { ok: false, reason: 'stat-failed' }; }
|
|
134
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
135
|
+
if (now.getTime() - stat.mtimeMs < ONE_HOUR_MS) {
|
|
136
|
+
return { ok: false, reason: 'plan-recently-touched' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = readFileSync(planPath, 'utf8');
|
|
140
|
+
const beadRefs = extractBeadsFromPlan(content);
|
|
141
|
+
if (beadRefs.length === 0) return { ok: false, reason: 'no-bead-refs' };
|
|
142
|
+
|
|
143
|
+
const uniqueIds = [...new Set(beadRefs.map((b) => b.beadId))];
|
|
144
|
+
const statuses = uniqueIds.map((id) => readBeadStatus(rootDir, id));
|
|
145
|
+
if (statuses.some((s) => s !== 'closed')) {
|
|
146
|
+
return { ok: false, reason: 'beads-still-open' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const handoffsDir = join(rootDir, '.cx', 'handoffs');
|
|
150
|
+
mkdirSync(handoffsDir, { recursive: true });
|
|
151
|
+
const date = now.toISOString().slice(0, 10);
|
|
152
|
+
const archivePath = join(handoffsDir, `${date}-plan-landed.md`);
|
|
153
|
+
const archiveBody = `# Landed plan — ${date}\n\nBeads closed: ${uniqueIds.join(', ')}\n\nArchived from \`plan.md\` on ${now.toISOString()} by the session-tracking-refresh hook.\n\n---\n\n${content}`;
|
|
154
|
+
writeFileSync(archivePath, archiveBody, 'utf8');
|
|
155
|
+
|
|
156
|
+
writeFileSync(planPath, buildPlanTemplate(), 'utf8');
|
|
157
|
+
|
|
158
|
+
return { ok: true, changed: true, archivePath, beadsClosed: uniqueIds };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Bead closure from merged PR
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const PR_REFS_PATTERN = /(?:Refs|Closes?|Fixes?|Resolves?)\s*:?\s*(?:#?\d+|(?:construct-[a-z0-9]+(?:\s*,\s*construct-[a-z0-9]+)*))/gi;
|
|
166
|
+
const BEAD_ID_PATTERN = /construct-[a-z0-9]+/g;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parse a merged PR's body for bead references and close each open one.
|
|
170
|
+
* Idempotent: skips beads already closed. Best-effort: each `bd close`
|
|
171
|
+
* failure is captured in the result, no throws.
|
|
172
|
+
*
|
|
173
|
+
* @returns {Promise<{ ok: boolean, closed: string[], skipped: string[], errors: object[] }>}
|
|
174
|
+
*/
|
|
175
|
+
export async function closeBeadsFromPrRefs({ prNumber, mergeCommitSha = '', cwd = process.cwd() } = {}) {
|
|
176
|
+
if (!prNumber) return { ok: false, reason: 'pr-number-required', closed: [], skipped: [], errors: [] };
|
|
177
|
+
|
|
178
|
+
const body = readPrBody(prNumber, cwd);
|
|
179
|
+
if (!body) return { ok: false, reason: 'pr-body-unavailable', closed: [], skipped: [], errors: [] };
|
|
180
|
+
|
|
181
|
+
const beadIds = new Set();
|
|
182
|
+
for (const match of body.match(PR_REFS_PATTERN) ?? []) {
|
|
183
|
+
for (const id of match.match(BEAD_ID_PATTERN) ?? []) beadIds.add(id);
|
|
184
|
+
}
|
|
185
|
+
if (beadIds.size === 0) return { ok: true, closed: [], skipped: [], errors: [] };
|
|
186
|
+
|
|
187
|
+
const closed = [];
|
|
188
|
+
const skipped = [];
|
|
189
|
+
const errors = [];
|
|
190
|
+
const reason = mergeCommitSha
|
|
191
|
+
? `Merged via PR #${prNumber} (${mergeCommitSha.slice(0, 12)})`
|
|
192
|
+
: `Merged via PR #${prNumber}`;
|
|
193
|
+
|
|
194
|
+
for (const id of beadIds) {
|
|
195
|
+
try { assertBeadId(id); } catch (err) { errors.push({ id, reason: 'invalid-bead-id', detail: err.message }); continue; }
|
|
196
|
+
const status = readBeadStatus(cwd, id);
|
|
197
|
+
if (status === 'closed') { skipped.push(id); continue; }
|
|
198
|
+
if (status === null) { errors.push({ id, reason: 'bd-show-failed' }); continue; }
|
|
199
|
+
try {
|
|
200
|
+
const result = spawnSync('bd', ['close', id, '--reason', reason], { cwd, encoding: 'utf8', timeout: 5000 });
|
|
201
|
+
if (result.status === 0) closed.push(id);
|
|
202
|
+
else errors.push({ id, reason: 'bd-close-failed', detail: (result.stderr || '').slice(0, 200) });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
errors.push({ id, reason: 'bd-close-threw', detail: err?.message });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { ok: true, closed, skipped, errors };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Internal helpers
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function collectRecentObservations(rootDir, cutoff) {
|
|
216
|
+
const dir = join(rootDir, '.cx', 'observations');
|
|
217
|
+
if (!existsSync(dir)) return [];
|
|
218
|
+
const out = [];
|
|
219
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
220
|
+
if (entry.isDirectory()) {
|
|
221
|
+
const subdir = join(dir, entry.name);
|
|
222
|
+
for (const file of safeReaddir(subdir)) {
|
|
223
|
+
const obs = readJsonSafe(join(subdir, file));
|
|
224
|
+
if (!obs) continue;
|
|
225
|
+
const ts = obs.timestamp || obs.ts;
|
|
226
|
+
if (!ts || new Date(ts) < cutoff) continue;
|
|
227
|
+
out.push({ ...obs, ts });
|
|
228
|
+
}
|
|
229
|
+
} else if (entry.name.endsWith('.json') || entry.name.endsWith('.jsonl')) {
|
|
230
|
+
const obs = readJsonSafe(join(dir, entry.name));
|
|
231
|
+
if (!obs) continue;
|
|
232
|
+
const ts = obs.timestamp || obs.ts;
|
|
233
|
+
if (!ts || new Date(ts) < cutoff) continue;
|
|
234
|
+
out.push({ ...obs, ts });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
out.sort((a, b) => new Date(b.ts) - new Date(a.ts));
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function collectRecentCommits(rootDir, cutoff) {
|
|
242
|
+
try {
|
|
243
|
+
const result = spawnSync('git', ['log', '--since', cutoff.toISOString(), '--pretty=format:%H|%s|%cI'], {
|
|
244
|
+
cwd: rootDir,
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
timeout: 3000,
|
|
247
|
+
});
|
|
248
|
+
if (result.status !== 0) return [];
|
|
249
|
+
return result.stdout.split('\n').filter(Boolean).map((line) => {
|
|
250
|
+
const [sha, subject, committerDate] = line.split('|');
|
|
251
|
+
return { sha, subject, committerDate };
|
|
252
|
+
});
|
|
253
|
+
} catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function collectRecentlyClosedBeads(rootDir, cutoff) {
|
|
259
|
+
const beads = runBdListJson(rootDir, ['list', '--status', 'closed', '--json']);
|
|
260
|
+
return beads
|
|
261
|
+
.filter((b) => {
|
|
262
|
+
const ts = b.closed || b.updated || b.updatedAt;
|
|
263
|
+
return ts && new Date(ts) >= cutoff;
|
|
264
|
+
})
|
|
265
|
+
.sort((a, b) => new Date(b.closed || b.updated || b.updatedAt) - new Date(a.closed || a.updated || a.updatedAt));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function collectInProgressBeads(rootDir) {
|
|
269
|
+
return runBdListJson(rootDir, ['list', '--status', 'in_progress', '--json']);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function runBdListJson(rootDir, args) {
|
|
273
|
+
try {
|
|
274
|
+
const result = spawnSync('bd', args, { cwd: rootDir, encoding: 'utf8', timeout: 5000 });
|
|
275
|
+
if (result.status !== 0) return [];
|
|
276
|
+
const parsed = JSON.parse(result.stdout);
|
|
277
|
+
if (Array.isArray(parsed)) return parsed;
|
|
278
|
+
if (parsed && Array.isArray(parsed.issues)) return parsed.issues;
|
|
279
|
+
return [];
|
|
280
|
+
} catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readBeadStatus(cwd, id) {
|
|
286
|
+
try {
|
|
287
|
+
const result = spawnSync('bd', ['show', id, '--json'], { cwd, encoding: 'utf8', timeout: 3000 });
|
|
288
|
+
if (result.status !== 0) return null;
|
|
289
|
+
const parsed = JSON.parse(result.stdout);
|
|
290
|
+
return parsed?.status || null;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readPrBody(prNumber, cwd) {
|
|
297
|
+
try {
|
|
298
|
+
const result = spawnSync('gh', ['pr', 'view', String(prNumber), '--json', 'body'], {
|
|
299
|
+
cwd,
|
|
300
|
+
encoding: 'utf8',
|
|
301
|
+
timeout: 10000,
|
|
302
|
+
});
|
|
303
|
+
if (result.status !== 0) return null;
|
|
304
|
+
const parsed = JSON.parse(result.stdout);
|
|
305
|
+
return parsed?.body || null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function safeReaddir(dir) {
|
|
312
|
+
try { return readdirSync(dir); }
|
|
313
|
+
catch { return []; }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function readJsonSafe(path) {
|
|
317
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
318
|
+
catch { return null; }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function formatActiveWork(openBeads) {
|
|
322
|
+
if (openBeads.length === 0) return '_None in progress._';
|
|
323
|
+
return openBeads
|
|
324
|
+
.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION)
|
|
325
|
+
.map((b) => `- **${b.id}** · ${b.title}`)
|
|
326
|
+
.join('\n');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function formatRecentDecisions(observations, commits, closedBeads) {
|
|
330
|
+
const items = [];
|
|
331
|
+
for (const bead of closedBeads.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION)) {
|
|
332
|
+
items.push(`- closed **${bead.id}** · ${bead.title}`);
|
|
333
|
+
}
|
|
334
|
+
for (const commit of commits.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION)) {
|
|
335
|
+
items.push(`- commit \`${commit.sha.slice(0, 10)}\` — ${commit.subject}`);
|
|
336
|
+
}
|
|
337
|
+
for (const obs of observations.filter((o) => o.category === 'decision').slice(0, CONTEXT_MAX_ITEMS_PER_SECTION)) {
|
|
338
|
+
items.push(`- decision — ${obs.summary}`);
|
|
339
|
+
}
|
|
340
|
+
if (items.length === 0) return '_No recent decisions captured._';
|
|
341
|
+
return items.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION * 2).join('\n');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatArchitectureNotes(observations) {
|
|
345
|
+
const arch = observations
|
|
346
|
+
.filter((o) => o.category === 'architecture' || /architect|contract|boundary|invariant/i.test(o.summary || ''))
|
|
347
|
+
.slice(0, CONTEXT_MAX_ITEMS_PER_SECTION);
|
|
348
|
+
if (arch.length === 0) return '_No new architecture notes._';
|
|
349
|
+
return arch.map((o) => `- ${o.summary}`).join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function rewriteManagedSections(content, sections) {
|
|
353
|
+
let out = content;
|
|
354
|
+
for (const [key, header] of Object.entries(SECTION_HEADERS)) {
|
|
355
|
+
if (!(key in sections)) continue;
|
|
356
|
+
const body = sections[key];
|
|
357
|
+
out = replaceSection(out, header, body);
|
|
358
|
+
}
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function replaceSection(content, header, body) {
|
|
363
|
+
const lines = content.split('\n');
|
|
364
|
+
const startIdx = lines.findIndex((l) => l.trim() === header);
|
|
365
|
+
if (startIdx === -1) return content;
|
|
366
|
+
|
|
367
|
+
let endIdx = lines.length;
|
|
368
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
369
|
+
if (lines[i].startsWith('## ') && lines[i].trim() !== header) {
|
|
370
|
+
endIdx = i;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const before = lines.slice(0, startIdx + 1);
|
|
375
|
+
const after = lines.slice(endIdx);
|
|
376
|
+
return [...before, '', body, '', ...after].join('\n');
|
|
377
|
+
}
|
|
@@ -1,62 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/validators/skills.mjs — Validate the structure of skill files.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* skill paths) that follow this convention:
|
|
4
|
+
* After the YAML-frontmatter migration, every skill file under `skills/` has:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* ---
|
|
7
|
+
* name: <kebab-case>
|
|
8
|
+
* description: "<≤1024 chars, includes a 'use when' trigger>"
|
|
9
|
+
* [role/applies_to/inherits/version/profiles/cap for role files]
|
|
10
|
+
* ---
|
|
11
11
|
* # <Title>
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* ...
|
|
12
|
+
* Use when: <trigger condition>
|
|
13
|
+
* <body>
|
|
15
14
|
*
|
|
16
15
|
* The validator splits findings into hard errors (block the build) and soft
|
|
17
16
|
* warnings (surfaced by `construct doctor` without failing it):
|
|
18
17
|
*
|
|
19
18
|
* Hard errors:
|
|
20
|
-
* - missing
|
|
19
|
+
* - frontmatter missing or not parseable as YAML
|
|
20
|
+
* - name missing, > 64 chars, fails ^[a-z0-9][a-z0-9-]*$, or contains
|
|
21
|
+
* reserved tokens (anthropic, claude)
|
|
22
|
+
* - description missing, empty, > 1024 chars, contains XML/HTML tags,
|
|
23
|
+
* or missing a "use when" trigger clause
|
|
24
|
+
* - missing H1 title (in body)
|
|
21
25
|
* - title over 80 chars
|
|
22
26
|
* - duplicate relative paths across roots
|
|
23
27
|
* - unreadable file
|
|
24
28
|
*
|
|
25
29
|
* Soft warnings:
|
|
26
|
-
* - opener
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* - opener over 240 chars
|
|
30
|
-
*
|
|
31
|
-
* Hard checks prevent broken file structure; soft checks surface drift from
|
|
32
|
-
* authoring conventions without blocking authors who legitimately diverge.
|
|
30
|
+
* - body opener missing or doesn't match a "Use this skill when/to/for ..."
|
|
31
|
+
* form (YAML description is now primary; body opener is a behavioral hint)
|
|
32
|
+
* - body opener over 240 chars
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
36
36
|
import path from 'node:path';
|
|
37
|
+
import yaml from 'js-yaml';
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const DESCRIPTION_MAX = 1024;
|
|
40
|
+
const NAME_MAX = 64;
|
|
39
41
|
const TITLE_MAX_CHARS = 80;
|
|
42
|
+
const BODY_OPENER_MAX = 240;
|
|
43
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
44
|
+
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
40
45
|
|
|
41
|
-
// Accept any of the conventional skill-trigger openers actually in use across
|
|
42
|
-
// the corpus: "Use this skill when/to/for", "Use when:", "Use this when",
|
|
43
|
-
// "Trigger:", or a leading "When ..." that signals a triggering condition.
|
|
44
46
|
const TRIGGER_PATTERN = /^(?:use\s+(?:this\s+(?:skill\s+)?)?(?:when|to|for|if)\b|when\s+to\s+use\b|trigger\s*:|use\s+when\s*:)/i;
|
|
45
47
|
|
|
46
48
|
function* walk(root) {
|
|
47
|
-
const
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
49
50
|
const full = path.join(root, entry.name);
|
|
50
|
-
if (entry.isDirectory())
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
if (entry.isDirectory()) yield* walk(full);
|
|
52
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) yield full;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseFrontmatter(content) {
|
|
57
|
+
const match = content.match(FRONTMATTER_RE);
|
|
58
|
+
if (!match) return { frontmatter: null, body: content };
|
|
59
|
+
try {
|
|
60
|
+
const parsed = yaml.load(match[1]) || {};
|
|
61
|
+
return { frontmatter: parsed, body: content.slice(match[0].length) };
|
|
62
|
+
} catch (err) {
|
|
63
|
+
return { frontmatter: null, body: content.slice(match[0].length), yamlError: err.message };
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
function extractTitleAndOpener(
|
|
59
|
-
const lines =
|
|
67
|
+
function extractTitleAndOpener(body) {
|
|
68
|
+
const lines = body.split(/\r?\n/);
|
|
60
69
|
let title = null;
|
|
61
70
|
let opener = null;
|
|
62
71
|
let inBody = false;
|
|
@@ -64,11 +73,7 @@ function extractTitleAndOpener(text) {
|
|
|
64
73
|
const line = lines[i].trim();
|
|
65
74
|
if (!title) {
|
|
66
75
|
const m = line.match(/^#\s+(.+?)\s*$/);
|
|
67
|
-
if (m) {
|
|
68
|
-
title = m[1];
|
|
69
|
-
inBody = true;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
76
|
+
if (m) { title = m[1]; inBody = true; continue; }
|
|
72
77
|
} else if (inBody && line && !line.startsWith('#') && !line.startsWith('<!--')) {
|
|
73
78
|
opener = line;
|
|
74
79
|
break;
|
|
@@ -77,23 +82,31 @@ function extractTitleAndOpener(text) {
|
|
|
77
82
|
return { title, opener };
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
function validateName(name) {
|
|
86
|
+
if (!name) return 'frontmatter.name is missing';
|
|
87
|
+
if (typeof name !== 'string') return `frontmatter.name must be a string (got ${typeof name})`;
|
|
88
|
+
if (!NAME_RE.test(name)) return `frontmatter.name "${name}" must match ${NAME_RE}`;
|
|
89
|
+
if (name.length > NAME_MAX) return `frontmatter.name "${name}" exceeds ${NAME_MAX} chars`;
|
|
90
|
+
if (/\banthropic\b|\bclaude\b/.test(name)) return `frontmatter.name "${name}" contains reserved token`;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateDescription(description) {
|
|
95
|
+
if (!description) return 'frontmatter.description is missing or empty';
|
|
96
|
+
if (typeof description !== 'string') return `frontmatter.description must be a string (got ${typeof description})`;
|
|
97
|
+
if (description.length > DESCRIPTION_MAX) return `frontmatter.description ${description.length} > ${DESCRIPTION_MAX} chars`;
|
|
98
|
+
if (/<[A-Za-z][^>]*>/.test(description)) return 'frontmatter.description contains XML/HTML tags';
|
|
99
|
+
if (!/use\s+when|use\s+this/i.test(description)) return 'frontmatter.description missing "use when" trigger clause';
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
91
103
|
export function validateSkills(roots) {
|
|
92
104
|
const dirs = Array.isArray(roots) ? roots : [roots];
|
|
93
105
|
const errors = [];
|
|
94
106
|
const warnings = [];
|
|
95
107
|
const skills = [];
|
|
96
108
|
const seenRelative = new Map();
|
|
109
|
+
const seenNames = new Map();
|
|
97
110
|
|
|
98
111
|
for (const dir of dirs) {
|
|
99
112
|
let exists = false;
|
|
@@ -113,12 +126,31 @@ export function validateSkills(roots) {
|
|
|
113
126
|
|
|
114
127
|
let raw;
|
|
115
128
|
try { raw = readFileSync(filePath, 'utf8'); }
|
|
116
|
-
catch (err) {
|
|
117
|
-
|
|
129
|
+
catch (err) { errors.push(`${rel}: cannot read (${err.message})`); continue; }
|
|
130
|
+
|
|
131
|
+
const { frontmatter, body, yamlError } = parseFrontmatter(raw);
|
|
132
|
+
|
|
133
|
+
if (yamlError) {
|
|
134
|
+
errors.push(`${rel}: frontmatter YAML parse error — ${yamlError}`);
|
|
118
135
|
continue;
|
|
119
136
|
}
|
|
137
|
+
if (!frontmatter) {
|
|
138
|
+
errors.push(`${rel}: missing YAML frontmatter block (---name/description---)`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nameErr = validateName(frontmatter.name);
|
|
143
|
+
if (nameErr) errors.push(`${rel}: ${nameErr}`);
|
|
144
|
+
else {
|
|
145
|
+
const dupName = seenNames.get(frontmatter.name);
|
|
146
|
+
if (dupName) errors.push(`${rel}: duplicate frontmatter.name "${frontmatter.name}" (also in ${dupName})`);
|
|
147
|
+
else seenNames.set(frontmatter.name, rel);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const descErr = validateDescription(frontmatter.description);
|
|
151
|
+
if (descErr) errors.push(`${rel}: ${descErr}`);
|
|
120
152
|
|
|
121
|
-
const { title, opener } = extractTitleAndOpener(
|
|
153
|
+
const { title, opener } = extractTitleAndOpener(body);
|
|
122
154
|
|
|
123
155
|
if (!title) {
|
|
124
156
|
errors.push(`${rel}: missing H1 title (e.g. "# Skill Name")`);
|
|
@@ -127,14 +159,14 @@ export function validateSkills(roots) {
|
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
if (!opener) {
|
|
130
|
-
warnings.push(`${rel}: no trigger opener after the H1
|
|
162
|
+
warnings.push(`${rel}: no trigger opener in body after the H1 — YAML description still primary, but body opener helps in-task behavior`);
|
|
131
163
|
} else if (!TRIGGER_PATTERN.test(opener)) {
|
|
132
|
-
warnings.push(`${rel}: opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
|
|
133
|
-
} else if (opener.length >
|
|
134
|
-
warnings.push(`${rel}: opener exceeds ${
|
|
164
|
+
warnings.push(`${rel}: body opener should start with "Use this skill when/to/for ..." (got "${opener.slice(0, 60)}…")`);
|
|
165
|
+
} else if (opener.length > BODY_OPENER_MAX) {
|
|
166
|
+
warnings.push(`${rel}: body opener exceeds ${BODY_OPENER_MAX} chars (got ${opener.length})`);
|
|
135
167
|
}
|
|
136
168
|
|
|
137
|
-
skills.push({ path: rel, title, opener });
|
|
169
|
+
skills.push({ path: rel, name: frontmatter.name, description: frontmatter.description, title, opener });
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
|
package/lib/worker/trace.mjs
CHANGED
|
@@ -26,15 +26,29 @@
|
|
|
26
26
|
* memory.written — durable memory record persisted
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import { existsSync, mkdirSync
|
|
29
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
30
30
|
import { randomBytes } from 'node:crypto';
|
|
31
31
|
import path from 'node:path';
|
|
32
32
|
|
|
33
33
|
import { createTelemetryClient } from '../telemetry/client.mjs';
|
|
34
34
|
import { ensureCxDir } from '../project-init-shared.mjs';
|
|
35
|
+
import { appendWithRotationSync } from '../logging/rotate.mjs';
|
|
35
36
|
|
|
36
37
|
const TRACE_SUBDIR = '.cx/traces';
|
|
37
38
|
|
|
39
|
+
// GitHub rejects single files > 100 MB. Keep every trace shard strictly
|
|
40
|
+
// under 100 MB so an accidental commit + push doesn't brick the repo. Cap
|
|
41
|
+
// is configurable via CONSTRUCT_TRACE_MAX_MB; default 100. Override of 0
|
|
42
|
+
// disables rotation entirely (not recommended).
|
|
43
|
+
|
|
44
|
+
function traceMaxBytes(env = process.env) {
|
|
45
|
+
const raw = env.CONSTRUCT_TRACE_MAX_MB;
|
|
46
|
+
if (raw === undefined) return 100 * 1024 * 1024;
|
|
47
|
+
const parsed = Number(raw);
|
|
48
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
|
49
|
+
return Math.floor(parsed * 1024 * 1024);
|
|
50
|
+
}
|
|
51
|
+
|
|
38
52
|
export const TRACE_EVENT_TYPES = [
|
|
39
53
|
'daemon.started',
|
|
40
54
|
'daemon.heartbeat',
|
|
@@ -168,7 +182,10 @@ export function emitTraceEvent({
|
|
|
168
182
|
metadata,
|
|
169
183
|
createdAt: new Date().toISOString(),
|
|
170
184
|
};
|
|
171
|
-
|
|
185
|
+
const shardPath = path.join(dir, `${todayShard()}.jsonl`);
|
|
186
|
+
appendWithRotationSync(shardPath, `${JSON.stringify(event)}\n`, {
|
|
187
|
+
maxBytes: traceMaxBytes(env),
|
|
188
|
+
});
|
|
172
189
|
|
|
173
190
|
// Fire-and-forget export to the configured remote adapter. Silent no-op when not configured.
|
|
174
191
|
exportToRemote(event, env, rootDir);
|