@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.
Files changed (337) hide show
  1. package/README.md +14 -2
  2. package/bin/construct +262 -14
  3. package/bin/construct-postinstall.mjs +23 -2
  4. package/commands/build/feature.md +0 -6
  5. package/commands/build/fix.md +0 -6
  6. package/commands/design/access.md +0 -6
  7. package/commands/design/flow.md +0 -6
  8. package/commands/design/ui.md +0 -6
  9. package/commands/measure/experiment.md +0 -6
  10. package/commands/measure/metrics.md +0 -6
  11. package/commands/measure/results.md +0 -6
  12. package/commands/plan/api.md +0 -6
  13. package/commands/plan/challenge.md +0 -6
  14. package/commands/plan/decide.md +0 -6
  15. package/commands/plan/feature.md +0 -7
  16. package/commands/plan/requirements.md +0 -6
  17. package/commands/remember/context.md +0 -6
  18. package/commands/remember/handoff.md +0 -6
  19. package/commands/remember/runbook.md +0 -6
  20. package/commands/review/code.md +0 -6
  21. package/commands/review/quality.md +0 -6
  22. package/commands/review/security.md +0 -6
  23. package/commands/ship/ready.md +0 -6
  24. package/commands/ship/release.md +0 -6
  25. package/commands/ship/status.md +0 -6
  26. package/commands/understand/docs.md +0 -6
  27. package/commands/understand/research.md +0 -6
  28. package/commands/understand/this.md +0 -6
  29. package/commands/understand/why.md +0 -6
  30. package/commands/work/clean.md +0 -6
  31. package/commands/work/drive.md +0 -6
  32. package/commands/work/optimize-prompts.md +0 -6
  33. package/commands/work/parallel-review.md +0 -6
  34. package/lib/auto-docs.mjs +11 -7
  35. package/lib/beads-client.mjs +19 -1
  36. package/lib/comment-lint.mjs +5 -1
  37. package/lib/context-state.mjs +15 -3
  38. package/lib/contracts/validate.mjs +54 -10
  39. package/lib/contracts/violation-log.mjs +154 -0
  40. package/lib/dashboard-static.mjs +7 -4
  41. package/lib/doc-stamp.mjs +16 -0
  42. package/lib/docs-verify.mjs +1 -8
  43. package/lib/doctor/watchers/bd-watch.mjs +6 -2
  44. package/lib/document-extract/docling-client.mjs +114 -0
  45. package/lib/document-extract/docling-sidecar.py +122 -0
  46. package/lib/document-extract/whisper-client.mjs +79 -0
  47. package/lib/document-extract.mjs +73 -8
  48. package/lib/document-ingest.mjs +26 -4
  49. package/lib/embed/daemon.mjs +26 -1
  50. package/lib/embed/docs-lifecycle.mjs +19 -0
  51. package/lib/embed/inbox.mjs +85 -2
  52. package/lib/embed/recommendation-store.mjs +29 -0
  53. package/lib/flavors/loader.mjs +1 -1
  54. package/lib/gates-audit.mjs +18 -12
  55. package/lib/handoffs/contract.mjs +2 -2
  56. package/lib/hooks/_lib/input.mjs +52 -0
  57. package/lib/hooks/adaptive-lint.mjs +4 -0
  58. package/lib/hooks/agent-tracker.mjs +59 -15
  59. package/lib/hooks/audit-reads.mjs +83 -42
  60. package/lib/hooks/audit-trail.mjs +28 -18
  61. package/lib/hooks/bash-output-logger.mjs +8 -2
  62. package/lib/hooks/block-no-verify.mjs +4 -0
  63. package/lib/hooks/ci-status-check.mjs +4 -0
  64. package/lib/hooks/comment-lint.mjs +6 -4
  65. package/lib/hooks/config-protection.mjs +4 -0
  66. package/lib/hooks/context-watch.mjs +4 -0
  67. package/lib/hooks/context-window-recovery.mjs +4 -0
  68. package/lib/hooks/dep-audit.mjs +12 -5
  69. package/lib/hooks/doc-coupling-check.mjs +5 -1
  70. package/lib/hooks/edit-accumulator.mjs +25 -10
  71. package/lib/hooks/edit-error-recovery.mjs +4 -0
  72. package/lib/hooks/edit-guard.mjs +4 -0
  73. package/lib/hooks/guard-bash.mjs +4 -0
  74. package/lib/hooks/mcp-audit.mjs +4 -0
  75. package/lib/hooks/mcp-health-check.mjs +4 -0
  76. package/lib/hooks/model-fallback.mjs +7 -11
  77. package/lib/hooks/policy-engine.mjs +4 -0
  78. package/lib/hooks/post-merge-docs-check.mjs +4 -0
  79. package/lib/hooks/post-merge-tracking.mjs +82 -0
  80. package/lib/hooks/pre-compact.mjs +4 -0
  81. package/lib/hooks/pre-push-gate.mjs +84 -231
  82. package/lib/hooks/proactive-activation.mjs +5 -2
  83. package/lib/hooks/readme-age-check.mjs +4 -0
  84. package/lib/hooks/registry-sync.mjs +35 -20
  85. package/lib/hooks/rule-verifier.mjs +3 -0
  86. package/lib/hooks/scan-secrets.mjs +4 -0
  87. package/lib/hooks/session-optimize.mjs +4 -0
  88. package/lib/hooks/session-reflect.mjs +4 -0
  89. package/lib/hooks/session-start.mjs +49 -2
  90. package/lib/hooks/session-tracking-refresh.mjs +70 -0
  91. package/lib/hooks/stop-notify.mjs +13 -2
  92. package/lib/hooks/stop-typecheck.mjs +4 -0
  93. package/lib/hooks/test-watch.mjs +8 -4
  94. package/lib/ingest/chunker.mjs +94 -0
  95. package/lib/ingest/pipeline.mjs +53 -0
  96. package/lib/ingest/store.mjs +82 -0
  97. package/lib/init-unified.mjs +90 -23
  98. package/lib/intake/attribution.mjs +77 -0
  99. package/lib/intake/intake-config.mjs +3 -0
  100. package/lib/intake/manifest.mjs +107 -0
  101. package/lib/intake/poll-lock.mjs +136 -0
  102. package/lib/intake/prepare.mjs +42 -4
  103. package/lib/knowledge/search.mjs +12 -0
  104. package/lib/logging/rotate.mjs +394 -0
  105. package/lib/mcp/server.mjs +142 -1
  106. package/lib/mcp/tools/project.mjs +2 -2
  107. package/lib/mcp/tools/skills.mjs +6 -3
  108. package/lib/mcp/tools/telemetry.mjs +1 -1
  109. package/lib/mcp/tools/workflow.mjs +6 -2
  110. package/lib/observation-store.mjs +14 -5
  111. package/lib/ollama-manager.mjs +32 -28
  112. package/lib/opencode-config.mjs +10 -3
  113. package/lib/orchestration/routing-tables.mjs +176 -0
  114. package/lib/orchestration-policy.mjs +18 -106
  115. package/lib/parity.mjs +48 -7
  116. package/lib/profiles/lifecycle.mjs +19 -1
  117. package/lib/project-init-shared.mjs +11 -5
  118. package/lib/project-root.mjs +104 -0
  119. package/lib/prompt-composer.js +11 -1
  120. package/lib/roles/catalog.mjs +1 -1
  121. package/lib/roles/event-bus.mjs +29 -9
  122. package/lib/roles/router.mjs +8 -7
  123. package/lib/runtime/uv-bootstrap.mjs +129 -0
  124. package/lib/runtime/whisper-bootstrap.mjs +102 -0
  125. package/lib/server/index.mjs +53 -10
  126. package/lib/server/static/index.html +1 -15
  127. package/lib/specialists/postconditions.mjs +1 -1
  128. package/lib/status.mjs +1 -1
  129. package/lib/storage/backup.mjs +2 -2
  130. package/lib/sync/skill-frontmatter.mjs +113 -21
  131. package/lib/telemetry/intent-verifications.mjs +16 -3
  132. package/lib/telemetry/skill-calls.mjs +12 -3
  133. package/lib/tracking-surfaces.mjs +377 -0
  134. package/lib/validators/skills.mjs +86 -54
  135. package/lib/worker/trace.mjs +19 -2
  136. package/package.json +10 -6
  137. package/personas/construct.md +4 -7
  138. package/platforms/claude/settings.template.json +29 -28
  139. package/rules/common/beads-hygiene.md +3 -9
  140. package/rules/common/code-review.md +3 -6
  141. package/rules/common/coding-style.md +3 -6
  142. package/rules/common/comments.md +3 -7
  143. package/rules/common/commit-approval.md +3 -6
  144. package/rules/common/cx-agent-routing.md +3 -7
  145. package/rules/common/cx-skill-routing.md +3 -3
  146. package/rules/common/doc-ownership.md +3 -8
  147. package/rules/common/efficiency.md +3 -8
  148. package/rules/common/framing.md +3 -8
  149. package/rules/common/git-workflow.md +3 -5
  150. package/rules/common/no-fabrication.md +4 -12
  151. package/rules/common/patterns.md +3 -5
  152. package/rules/common/release-gates.md +3 -8
  153. package/rules/common/research.md +3 -8
  154. package/rules/common/review-before-change.md +3 -9
  155. package/rules/common/security.md +3 -6
  156. package/rules/common/skill-composition.md +3 -7
  157. package/rules/common/testing.md +3 -6
  158. package/rules/golang/coding-style.md +1 -5
  159. package/rules/golang/hooks.md +1 -5
  160. package/rules/golang/patterns.md +1 -5
  161. package/rules/golang/security.md +1 -5
  162. package/rules/golang/testing.md +1 -5
  163. package/rules/python/coding-style.md +1 -5
  164. package/rules/python/hooks.md +1 -5
  165. package/rules/python/patterns.md +1 -5
  166. package/rules/python/security.md +1 -5
  167. package/rules/python/testing.md +1 -5
  168. package/rules/swift/coding-style.md +1 -5
  169. package/rules/swift/hooks.md +1 -5
  170. package/rules/swift/patterns.md +1 -5
  171. package/rules/swift/security.md +1 -5
  172. package/rules/swift/testing.md +1 -5
  173. package/rules/typescript/coding-style.md +1 -5
  174. package/rules/typescript/hooks.md +1 -5
  175. package/rules/typescript/patterns.md +1 -5
  176. package/rules/typescript/security.md +1 -5
  177. package/rules/typescript/testing.md +1 -5
  178. package/rules/web/coding-style.md +3 -5
  179. package/rules/web/design-quality.md +3 -5
  180. package/rules/web/hooks.md +3 -5
  181. package/rules/web/patterns.md +3 -5
  182. package/rules/web/performance.md +3 -5
  183. package/rules/web/security.md +3 -5
  184. package/rules/web/testing.md +3 -5
  185. package/scripts/sync-specialists.mjs +269 -75
  186. package/skills/ai/agent-dev.md +4 -5
  187. package/skills/ai/llm-security.md +4 -5
  188. package/skills/ai/ml-ops.md +4 -5
  189. package/skills/ai/orchestration-workflow.md +4 -5
  190. package/skills/ai/prompt-and-eval.md +4 -5
  191. package/skills/ai/prompt-optimizer.md +4 -6
  192. package/skills/ai/rag-system.md +4 -5
  193. package/skills/architecture/api-design.md +4 -5
  194. package/skills/architecture/caching.md +4 -5
  195. package/skills/architecture/cloud-native.md +4 -5
  196. package/skills/architecture/message-queue.md +4 -5
  197. package/skills/architecture/security-arch.md +4 -5
  198. package/skills/compliance/ai-disclosure.md +4 -5
  199. package/skills/compliance/data-privacy.md +4 -5
  200. package/skills/compliance/license-audit.md +4 -5
  201. package/skills/compliance/regulatory-review.md +4 -5
  202. package/skills/development/cpp.md +4 -5
  203. package/skills/development/go.md +4 -5
  204. package/skills/development/java.md +4 -5
  205. package/skills/development/kotlin.md +4 -5
  206. package/skills/development/mobile-crossplatform.md +4 -5
  207. package/skills/development/python.md +4 -5
  208. package/skills/development/rust.md +4 -5
  209. package/skills/development/shell.md +4 -5
  210. package/skills/development/swift.md +4 -5
  211. package/skills/development/typescript.md +4 -5
  212. package/skills/devops/ci-cd.md +4 -5
  213. package/skills/devops/containerization.md +4 -5
  214. package/skills/devops/cost-optimization.md +4 -5
  215. package/skills/devops/data-engineering.md +4 -5
  216. package/skills/devops/database.md +4 -5
  217. package/skills/devops/dependency-management.md +4 -5
  218. package/skills/devops/devsecops.md +4 -5
  219. package/skills/devops/git-workflow.md +4 -5
  220. package/skills/devops/incident-response.md +4 -5
  221. package/skills/devops/monorepo.md +4 -5
  222. package/skills/devops/observability.md +4 -5
  223. package/skills/devops/performance.md +4 -5
  224. package/skills/devops/testing.md +4 -5
  225. package/skills/docs/adr-workflow.md +4 -7
  226. package/skills/docs/backlog-proposal-workflow.md +4 -3
  227. package/skills/docs/customer-profile-workflow.md +4 -3
  228. package/skills/docs/document-ingest-workflow.md +4 -3
  229. package/skills/docs/evidence-ingest-workflow.md +4 -3
  230. package/skills/docs/init-docs.md +4 -5
  231. package/skills/docs/init-project.md +4 -5
  232. package/skills/docs/prd-workflow.md +4 -7
  233. package/skills/docs/prfaq-workflow.md +4 -3
  234. package/skills/docs/product-intelligence-review.md +4 -3
  235. package/skills/docs/product-intelligence-workflow.md +4 -6
  236. package/skills/docs/product-signal-workflow.md +4 -5
  237. package/skills/docs/research-workflow.md +4 -5
  238. package/skills/docs/runbook-workflow.md +4 -5
  239. package/skills/docs/strategy-workflow.md +4 -5
  240. package/skills/exploration/dependency-graph-reading.md +4 -7
  241. package/skills/exploration/repo-map.md +4 -5
  242. package/skills/exploration/tracer-bullet-method.md +4 -7
  243. package/skills/exploration/unknown-codebase-onboarding.md +4 -7
  244. package/skills/frameworks/django.md +4 -5
  245. package/skills/frameworks/nextjs.md +4 -5
  246. package/skills/frameworks/react.md +4 -5
  247. package/skills/frameworks/spring-boot.md +4 -5
  248. package/skills/frontend-design/accessibility.md +4 -5
  249. package/skills/frontend-design/component-patterns.md +4 -5
  250. package/skills/frontend-design/engineering.md +4 -5
  251. package/skills/frontend-design/state-management.md +4 -5
  252. package/skills/frontend-design/ui-aesthetics.md +4 -5
  253. package/skills/frontend-design/ux-principles.md +4 -5
  254. package/skills/operating/change-management.md +4 -8
  255. package/skills/operating/incident-response.md +4 -8
  256. package/skills/operating/oncall-rotation.md +4 -7
  257. package/skills/operating/orchestration-reference.md +4 -10
  258. package/skills/quality-gates/review-work.md +4 -5
  259. package/skills/quality-gates/verify-change.md +4 -5
  260. package/skills/quality-gates/verify-module.md +4 -5
  261. package/skills/quality-gates/verify-quality.md +4 -5
  262. package/skills/quality-gates/verify-security.md +4 -5
  263. package/skills/roles/architect.ai-systems.md +6 -9
  264. package/skills/roles/architect.data.md +6 -9
  265. package/skills/roles/architect.enterprise.md +6 -9
  266. package/skills/roles/architect.integration.md +6 -9
  267. package/skills/roles/architect.md +7 -14
  268. package/skills/roles/architect.platform.md +6 -9
  269. package/skills/roles/data-analyst.experiment.md +6 -9
  270. package/skills/roles/data-analyst.md +6 -9
  271. package/skills/roles/data-analyst.product-intelligence.md +7 -9
  272. package/skills/roles/data-analyst.product.md +6 -9
  273. package/skills/roles/data-analyst.telemetry.md +7 -9
  274. package/skills/roles/data-engineer.pipeline.md +6 -9
  275. package/skills/roles/data-engineer.vector-retrieval.md +7 -9
  276. package/skills/roles/data-engineer.warehouse.md +6 -9
  277. package/skills/roles/debugger.md +6 -9
  278. package/skills/roles/designer.accessibility.md +6 -9
  279. package/skills/roles/designer.md +7 -9
  280. package/skills/roles/engineer.ai.md +6 -9
  281. package/skills/roles/engineer.data.md +6 -9
  282. package/skills/roles/engineer.md +9 -9
  283. package/skills/roles/engineer.platform.md +6 -9
  284. package/skills/roles/operator.docs.md +6 -9
  285. package/skills/roles/operator.md +9 -9
  286. package/skills/roles/operator.release.md +6 -9
  287. package/skills/roles/operator.sre.md +6 -9
  288. package/skills/roles/orchestrator.md +6 -9
  289. package/skills/roles/product-manager.ai-product.md +6 -9
  290. package/skills/roles/product-manager.business-strategy.md +6 -9
  291. package/skills/roles/product-manager.enterprise.md +6 -9
  292. package/skills/roles/product-manager.growth.md +7 -9
  293. package/skills/roles/product-manager.md +7 -9
  294. package/skills/roles/product-manager.platform.md +6 -9
  295. package/skills/roles/product-manager.product.md +6 -9
  296. package/skills/roles/qa.ai-eval.md +8 -9
  297. package/skills/roles/qa.api-contract.md +7 -9
  298. package/skills/roles/qa.data-pipeline.md +7 -9
  299. package/skills/roles/qa.md +7 -9
  300. package/skills/roles/qa.test-automation.md +6 -9
  301. package/skills/roles/qa.web-ui.md +7 -9
  302. package/skills/roles/researcher.explorer.md +6 -9
  303. package/skills/roles/researcher.md +8 -9
  304. package/skills/roles/researcher.ux.md +6 -9
  305. package/skills/roles/reviewer.devil-advocate.md +6 -9
  306. package/skills/roles/reviewer.evaluator.md +6 -9
  307. package/skills/roles/reviewer.md +9 -9
  308. package/skills/roles/reviewer.trace.md +6 -9
  309. package/skills/roles/security.ai.md +7 -9
  310. package/skills/roles/security.appsec.md +6 -9
  311. package/skills/roles/security.cloud.md +6 -9
  312. package/skills/roles/security.legal-compliance.md +6 -9
  313. package/skills/roles/security.md +7 -9
  314. package/skills/roles/security.privacy.md +7 -9
  315. package/skills/roles/security.supply-chain.md +7 -9
  316. package/skills/security/blue-team.md +4 -5
  317. package/skills/security/code-audit.md +4 -5
  318. package/skills/security/pentest.md +4 -5
  319. package/skills/security/red-team.md +4 -5
  320. package/skills/security/threat-intel.md +4 -5
  321. package/skills/security/vuln-research.md +4 -5
  322. package/skills/strategy/competitive-landscape.md +4 -8
  323. package/skills/strategy/market-research-methods.md +4 -8
  324. package/skills/strategy/narrative-arc.md +4 -8
  325. package/skills/strategy/pricing-positioning.md +4 -8
  326. package/skills/utility/clean-code.md +4 -5
  327. package/specialists/policy-inventory.json +14 -0
  328. package/specialists/registry.json +158 -13
  329. package/lib/hooks/env-check.mjs +0 -83
  330. package/lib/hooks/read-tracker.mjs +0 -61
  331. package/lib/policy/unified-gates.mjs +0 -96
  332. package/lib/server/static/assets/index-ab25c707.js +0 -70
  333. package/lib/server/static/assets/index-f0c80a2b.css +0 -1
  334. package/lib/specialist-contracts-enforce.mjs +0 -158
  335. package/rules/common/agents.md +0 -28
  336. package/rules/common/development-workflow.md +0 -32
  337. 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
- * Skills are markdown files under `skills/` (and any merged plugin or project
5
- * skill paths) that follow this convention:
4
+ * After the YAML-frontmatter migration, every skill file under `skills/` has:
6
5
  *
7
- * <!--
8
- * skills/<domain>/<name>.md <Title> — <one-line description>
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
- * Use this skill when <triggering condition>.
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 H1 title
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 is missing or doesn't match a recognised "Use this skill
27
- * when/to/for ..." form (skills with non-trigger openers still load,
28
- * but they're harder for routing to surface)
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 DESCRIPTION_MAX_CHARS = 240;
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 entries = readdirSync(root, { withFileTypes: true });
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
- yield* walk(full);
52
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
53
- yield full;
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(text) {
59
- const lines = text.split(/\r?\n/);
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
- * Validate every skill file under one or more directories.
82
- *
83
- * @param {string[]|string} roots directories to walk recursively
84
- * @returns {{
85
- * valid: boolean,
86
- * errors: string[],
87
- * warnings: string[],
88
- * skills: Array<{ path: string, title: string|null, opener: string|null }>,
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
- errors.push(`${rel}: cannot read (${err.message})`);
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(raw);
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 title routing will have nothing to match`);
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 > DESCRIPTION_MAX_CHARS) {
134
- warnings.push(`${rel}: opener exceeds ${DESCRIPTION_MAX_CHARS} chars (got ${opener.length})`);
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
 
@@ -26,15 +26,29 @@
26
26
  * memory.written — durable memory record persisted
27
27
  */
28
28
 
29
- import { existsSync, mkdirSync, appendFileSync } from 'node:fs';
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
- appendFileSync(path.join(dir, `${todayShard()}.jsonl`), `${JSON.stringify(event)}\n`, 'utf8');
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);