@geraldmaron/construct 1.2.1 → 1.2.2
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 +6 -6
- package/bin/construct +41 -67
- package/commands/plan/decide.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/examples/README.md +1 -1
- package/examples/distribution/README.md +1 -1
- package/examples/distribution/sources/adr.md +2 -2
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/examples/distribution/sources/prd-platform.md +1 -1
- package/examples/distribution/sources/research-brief.md +2 -2
- package/examples/distribution/sources/rfc-platform.md +1 -1
- package/examples/distribution/sources/runbook.md +2 -2
- package/examples/distribution/sources/strategy.md +1 -1
- package/lib/agent-instructions/inject.mjs +1 -1
- package/lib/artifact-type-from-path.mjs +9 -5
- package/lib/audit-specialists.mjs +1 -1
- package/lib/audit-trail.mjs +3 -3
- package/lib/auto-docs.mjs +10 -10
- package/lib/bootstrap/built-ins.mjs +3 -3
- package/lib/boundary.mjs +3 -2
- package/lib/brand-prose.mjs +1 -1
- package/lib/cache-governor.js +4 -4
- package/lib/certification/document-io-fixtures.mjs +1 -1
- package/lib/chat/config.mjs +2 -2
- package/lib/chat/desktop-binary.mjs +5 -3
- package/lib/cli-commands.mjs +3 -3
- package/lib/cli-service-inventory.mjs +1 -1
- package/lib/comment-lint.mjs +9 -5
- package/lib/config/alias.mjs +3 -2
- package/lib/config/intake-policy.mjs +8 -78
- package/lib/config/project-config.mjs +95 -5
- package/lib/config/schema.mjs +9 -19
- package/lib/config/xdg.mjs +63 -0
- package/lib/contracts/violation-log.mjs +2 -2
- package/lib/cost-ledger.mjs +4 -3
- package/lib/cost.mjs +3 -2
- package/lib/decisions/registry.mjs +3 -3
- package/lib/distill.mjs +2 -2
- package/lib/docs-verify.mjs +2 -0
- package/lib/doctor/audit.mjs +4 -3
- package/lib/doctor/cli.mjs +0 -1
- package/lib/doctor/index.mjs +4 -4
- package/lib/doctor/report.mjs +3 -3
- package/lib/doctor/watchers/bd-watch.mjs +5 -5
- package/lib/doctor/watchers/cost.mjs +4 -4
- package/lib/doctor/watchers/disk.mjs +4 -3
- package/lib/doctor/watchers/service-health.mjs +5 -5
- package/lib/document-export.mjs +1 -1
- package/lib/document-extract.mjs +3 -3
- package/lib/document-ingest.mjs +13 -3
- package/lib/efficiency.mjs +2 -1
- package/lib/embed/approval-queue.mjs +3 -2
- package/lib/embed/cli.mjs +7 -5
- package/lib/embed/config.mjs +6 -5
- package/lib/embed/daemon.mjs +6 -5
- package/lib/embed/inbox-live-watcher.mjs +1 -1
- package/lib/embed/inbox.mjs +52 -37
- package/lib/embed/intake-metrics.mjs +3 -2
- package/lib/embed/reconcile.mjs +1 -1
- package/lib/embed/semantic.mjs +3 -2
- package/lib/embed/supervision.mjs +4 -2
- package/lib/engine/registry.mjs +3 -3
- package/lib/env-config.mjs +34 -12
- package/lib/features.mjs +4 -3
- package/lib/health-check.mjs +7 -6
- package/lib/hook-health.mjs +5 -4
- package/lib/hooks/_lib/log.mjs +4 -3
- package/lib/hooks/_lib/output-mode.mjs +2 -1
- package/lib/hooks/agent-tracker.mjs +3 -4
- package/lib/hooks/audit-reads.mjs +2 -2
- package/lib/hooks/audit-trail.mjs +2 -2
- package/lib/hooks/bash-output-logger.mjs +3 -3
- package/lib/hooks/brand-prose-lint.mjs +1 -1
- package/lib/hooks/ci-status-check.mjs +3 -2
- package/lib/hooks/context-watch.mjs +2 -2
- package/lib/hooks/context-window-recovery.mjs +5 -5
- package/lib/hooks/dep-audit.mjs +2 -2
- package/lib/hooks/doc-coupling-check.mjs +3 -2
- package/lib/hooks/edit-accumulator.mjs +3 -2
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/guard-bash.mjs +2 -2
- package/lib/hooks/model-fallback.mjs +2 -1
- package/lib/hooks/policy-engine.mjs +4 -3
- package/lib/hooks/pre-compact.mjs +7 -7
- package/lib/hooks/readme-age-check.mjs +2 -2
- package/lib/hooks/session-optimize.mjs +4 -3
- package/lib/hooks/session-start.mjs +6 -5
- package/lib/hooks/stop-notify.mjs +9 -8
- package/lib/hooks/stop-typecheck.mjs +4 -3
- package/lib/hooks/test-watch.mjs +2 -2
- package/lib/host-disposition.mjs +1 -1
- package/lib/ingest/provider-extract.mjs +2 -1
- package/lib/init/detect-existing-structure.mjs +2 -2
- package/lib/init/doc-lanes.mjs +3 -10
- package/lib/init-docs.mjs +12 -54
- package/lib/init-unified.mjs +35 -81
- package/lib/init.mjs +7 -5
- package/lib/install/desktop-binary-download.mjs +5 -2
- package/lib/intake/daemon.mjs +31 -8
- package/lib/intake/intake-config.mjs +5 -32
- package/lib/integrations/intake-integrations.mjs +8 -7
- package/lib/knowledge/rag.mjs +2 -2
- package/lib/knowledge/search.mjs +15 -15
- package/lib/maintenance/cleanup.mjs +14 -10
- package/lib/mcp/server.mjs +1 -1
- package/lib/mcp/tools/project.mjs +2 -1
- package/lib/model-cheapest-provider.mjs +4 -3
- package/lib/model-pricing.mjs +3 -2
- package/lib/model-router.mjs +4 -3
- package/lib/models/catalog.mjs +2 -1
- package/lib/models/execution-capability-profile.mjs +2 -1
- package/lib/models/provider-poll.mjs +2 -1
- package/lib/ollama-manager.mjs +0 -1
- package/lib/op-log.mjs +2 -1
- package/lib/opencode-runtime-plugin.mjs +3 -2
- package/lib/oracle/artifact-gate.mjs +3 -0
- package/lib/oracle/cli.mjs +2 -1
- package/lib/oracle/execute.mjs +3 -2
- package/lib/oracle/index.mjs +2 -1
- package/lib/oracle/read-model.mjs +3 -2
- package/lib/performance/generate.mjs +4 -3
- package/lib/platforms/capabilities.mjs +1 -1
- package/lib/plugin-registry.mjs +2 -1
- package/lib/profiles/lifecycle.mjs +3 -3
- package/lib/project-profile.mjs +2 -1
- package/lib/project-root.mjs +9 -7
- package/lib/provider-capabilities.js +4 -3
- package/lib/providers/auth-manager.mjs +2 -1
- package/lib/providers/copilot-auth.mjs +3 -2
- package/lib/providers/creds.mjs +3 -2
- package/lib/providers/registry.mjs +3 -3
- package/lib/providers/secret-resolver.mjs +3 -2
- package/lib/read-tracker-store.mjs +2 -1
- package/lib/reconcile/mcp-entry-reconcile.mjs +2 -2
- package/lib/reflect.mjs +2 -1
- package/lib/registry/generate-docs.mjs +2 -2
- package/lib/roles/approval-surface.mjs +5 -4
- package/lib/roles/event-bus.mjs +0 -1
- package/lib/roles/gateway.mjs +6 -2
- package/lib/rules-delivery.mjs +1 -1
- package/lib/runtime-pressure.mjs +5 -3
- package/lib/sandbox.mjs +3 -2
- package/lib/scheduler/solo.mjs +6 -4
- package/lib/server/auth.mjs +4 -3
- package/lib/server/index.mjs +24 -26
- package/lib/server/insights.mjs +5 -4
- package/lib/server/webhook.mjs +2 -1
- package/lib/service-manager.mjs +5 -3
- package/lib/setup.mjs +9 -8
- package/lib/status.mjs +6 -5
- package/lib/storage/embeddings-local.mjs +3 -2
- package/lib/storage/sync.mjs +2 -2
- package/lib/telemetry/client.mjs +0 -1
- package/lib/telemetry/intent-verifications.mjs +6 -6
- package/lib/telemetry/model-pricing-catalog.mjs +4 -3
- package/lib/telemetry/rule-calls.mjs +3 -3
- package/lib/telemetry/skill-calls.mjs +3 -3
- package/lib/template-registry.mjs +1 -0
- package/lib/templates/visual-requirements.mjs +1 -1
- package/lib/test-corpus-inventory.mjs +1 -1
- package/lib/uninstall/uninstall.mjs +12 -10
- package/package.json +3 -3
- package/platforms/claude/settings.template.json +43 -43
- package/rules/common/no-fabrication.md +1 -1
- package/rules/common/release-gates.md +1 -1
- package/rules/common/research.md +1 -1
- package/rules/common/review-before-change.md +1 -1
- package/scripts/sync-specialists.mjs +11 -8
- package/skills/docs/adr-workflow.md +3 -3
- package/skills/docs/init-docs.md +9 -9
- package/skills/docs/prd-workflow.md +5 -5
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +2 -2
- package/skills/docs/product-signal-workflow.md +1 -1
- package/skills/docs/runbook-workflow.md +4 -4
- package/skills/exploration/unknown-codebase-onboarding.md +1 -1
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/routing.md +3 -3
- package/specialists/prompts/cx-architect.md +1 -1
- package/specialists/prompts/cx-docs-keeper.md +1 -1
- package/specialists/prompts/cx-researcher.md +1 -1
- package/specialists/prompts/cx-sre.md +1 -1
- package/specialists/role-manifests.json +6 -6
- package/templates/docs/README.md +125 -0
- package/templates/docs/construct_guide.md +3 -4
- package/templates/docs/persona-artifact.md +1 -1
- package/templates/docs/prds/README.md +25 -0
- package/templates/docs/prds/templates/_template.md +206 -0
- package/templates/docs/prds/templates/meta-prd.template.md +177 -0
- package/templates/docs/prds/templates/prd-business.template.md +61 -0
- package/templates/docs/prds/templates/prd-platform.template.md +81 -0
- package/templates/docs/prds/templates/prfaq.template.md +38 -0
- package/templates/docs/rfcs/README.md +22 -0
- package/templates/docs/rfcs/templates/_template.md +58 -0
- package/templates/docs/rfcs/templates/rfc-platform.template.md +72 -0
- package/templates/homebrew/construct.rb +1 -1
- package/templates/workflows/new-feature.yml +8 -8
- package/lib/intake/legacy-paths.mjs +0 -5
package/lib/init-unified.mjs
CHANGED
|
@@ -175,10 +175,7 @@ function detectProjectType(targetPath) {
|
|
|
175
175
|
function suggestLanes(targetPath, projectType) {
|
|
176
176
|
const suggestions = [];
|
|
177
177
|
const files = fs.readdirSync(targetPath);
|
|
178
|
-
|
|
179
|
-
// Always suggest intake for collecting feedback
|
|
180
|
-
suggestions.push({ lane: 'intake', reason: 'Collect incoming requests and feedback', recommended: true });
|
|
181
|
-
|
|
178
|
+
|
|
182
179
|
// ADRs for any non-trivial project
|
|
183
180
|
if (projectType.type !== 'docs') {
|
|
184
181
|
suggestions.push({ lane: 'adrs', reason: 'Track architectural decisions', recommended: true });
|
|
@@ -365,33 +362,6 @@ Attendees: {{attendees}}
|
|
|
365
362
|
- [ ] @owner: Task description (due: {{date}})`;
|
|
366
363
|
}
|
|
367
364
|
|
|
368
|
-
function buildIntakeTemplate() {
|
|
369
|
-
return `# Intake: {{source}}
|
|
370
|
-
|
|
371
|
-
Received: {{date}}
|
|
372
|
-
From: {{from}}
|
|
373
|
-
Priority: {{priority}}
|
|
374
|
-
|
|
375
|
-
## What arrived
|
|
376
|
-
|
|
377
|
-
Brief description of what was received.
|
|
378
|
-
|
|
379
|
-
## Why it matters
|
|
380
|
-
|
|
381
|
-
Why this intake item is important to process.
|
|
382
|
-
|
|
383
|
-
## How it should be ingested
|
|
384
|
-
|
|
385
|
-
Processing instructions:
|
|
386
|
-
- [ ] Step 1
|
|
387
|
-
- [ ] Step 2
|
|
388
|
-
- [ ] Step 3
|
|
389
|
-
|
|
390
|
-
## Notes
|
|
391
|
-
|
|
392
|
-
<!-- Caveats, access concerns, or cleanup notes. -->`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
365
|
function writeIfMissing(filePath, content) {
|
|
396
366
|
if (fs.existsSync(filePath)) {
|
|
397
367
|
skipped.push(path.relative(target, filePath));
|
|
@@ -418,8 +388,6 @@ function copyLaneTemplates(laneKey) {
|
|
|
418
388
|
content = buildNotesTemplate();
|
|
419
389
|
} else if (templateName === '__meeting-notes-template__') {
|
|
420
390
|
content = buildMeetingNotesTemplate();
|
|
421
|
-
} else if (templateName === "__intake-template__") {
|
|
422
|
-
content = buildIntakeTemplate();
|
|
423
391
|
} else {
|
|
424
392
|
const templatePath = path.join(TEMPLATE_DIR, templateName);
|
|
425
393
|
if (fs.existsSync(templatePath)) {
|
|
@@ -543,8 +511,8 @@ function discoverProjectDirs(targetPath) {
|
|
|
543
511
|
// Extra inbox-style directories the user can opt into. These are NOT
|
|
544
512
|
// source-code, doc, or test directories: they're places where unsorted
|
|
545
513
|
// signals tend to land (downloads, scratch notes, exported research). The
|
|
546
|
-
//
|
|
547
|
-
//
|
|
514
|
+
// canonical `inbox/` at the project root is always watched and not listed
|
|
515
|
+
// here.
|
|
548
516
|
//
|
|
549
517
|
// Historically this list included `src/`, `docs/`, `tests/`, etc., and
|
|
550
518
|
// `init --yes` auto-enabled every one that existed in the project. That
|
|
@@ -555,10 +523,9 @@ function discoverProjectDirs(targetPath) {
|
|
|
555
523
|
const INTAKE_DIR_PRESETS = [];
|
|
556
524
|
|
|
557
525
|
async function askIntakeCollection(targetPath, skipInteractive) {
|
|
558
|
-
// Non-interactive default: empty parentDirs. The
|
|
559
|
-
//
|
|
560
|
-
//
|
|
561
|
-
// explicitly via `construct intake config set --add-dir=<path>`.
|
|
526
|
+
// Non-interactive default: empty parentDirs. The canonical `inbox/` zone
|
|
527
|
+
// handles every well-defined signal path. Users who want to watch additional
|
|
528
|
+
// directories opt in explicitly via `construct intake config set --add-dir=<path>`.
|
|
562
529
|
if (skipInteractive) {
|
|
563
530
|
return null;
|
|
564
531
|
}
|
|
@@ -583,7 +550,7 @@ async function askIntakeCollection(targetPath, skipInteractive) {
|
|
|
583
550
|
console.log(' INTAKE COLLECTION');
|
|
584
551
|
console.log('═══════════════════════════════════════════════════════════');
|
|
585
552
|
console.log('');
|
|
586
|
-
console.log('Intake watches
|
|
553
|
+
console.log('Intake watches `inbox/` at the project root.');
|
|
587
554
|
console.log('Add extra directories ONLY if signals (customer notes, exports,');
|
|
588
555
|
console.log('PDFs, etc.) regularly land there. Do not select code or finished');
|
|
589
556
|
console.log('artifact directories like src/, docs/, or tests/.');
|
|
@@ -599,7 +566,7 @@ async function askIntakeCollection(targetPath, skipInteractive) {
|
|
|
599
566
|
if (selected.length === 0) {
|
|
600
567
|
console.log('');
|
|
601
568
|
console.log('No extra directories selected. The inbox watcher will scan');
|
|
602
|
-
console.log('
|
|
569
|
+
console.log('`inbox/` only. Add more later with:');
|
|
603
570
|
console.log(' construct intake config set --add-dir=<path>');
|
|
604
571
|
console.log('');
|
|
605
572
|
return null;
|
|
@@ -866,37 +833,35 @@ async function main() {
|
|
|
866
833
|
|
|
867
834
|
|
|
868
835
|
// Detect existing project content once; the result feeds three decisions
|
|
869
|
-
// below: skip
|
|
870
|
-
// already covered elsewhere, and skip per-lane templates/ when
|
|
871
|
-
// templates/ already has them. --force bypasses every check.
|
|
836
|
+
// below: skip the inbox/ drop zone on custom intake, skip lane scaffolding
|
|
837
|
+
// for lanes already covered elsewhere, and skip per-lane templates/ when
|
|
838
|
+
// root templates/ already has them. --force bypasses every check.
|
|
872
839
|
|
|
873
840
|
const { detectExistingContent, shouldSkipProjectInbox, shouldScaffoldLane, formatDeferralSummary } =
|
|
874
841
|
await import('./init/detect-existing-structure.mjs');
|
|
875
842
|
const detection = detectExistingContent(target);
|
|
876
843
|
const inboxDecision = shouldSkipProjectInbox(detection, { force: forceScaffold });
|
|
877
844
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
fs.mkdirSync(path.join(target, ".cx", "inbox"), { recursive: true });
|
|
886
|
-
created.push(".cx/inbox/");
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Universal project-root inbox/ drop zone. Raw drops stay out of source via
|
|
890
|
-
// host-disposition IGNORED_PATTERNS (ADR-0027 §1). Skipped when custom intake
|
|
891
|
-
// pipeline already owns the project root.
|
|
845
|
+
// Single canonical drop zone (ADR-0045 §C): the visible project-root `inbox/`.
|
|
846
|
+
// Raw drops stay out of source via host-disposition IGNORED_PATTERNS
|
|
847
|
+
// (ADR-0027 §1). Machine/runtime intake state lives under the gitignored
|
|
848
|
+
// `.cx/intake/` tree, created lazily by the queue on first use — init no
|
|
849
|
+
// longer scaffolds the deprecated `.cx/inbox/` drop zone. Skipped when a
|
|
850
|
+
// custom intake pipeline already owns the project root.
|
|
892
851
|
|
|
893
852
|
if (!inboxDecision.skip) {
|
|
894
|
-
const
|
|
895
|
-
if (!fs.existsSync(
|
|
896
|
-
fs.mkdirSync(
|
|
853
|
+
const rootInbox = path.join(target, 'inbox');
|
|
854
|
+
if (!fs.existsSync(rootInbox)) {
|
|
855
|
+
fs.mkdirSync(rootInbox, { recursive: true });
|
|
897
856
|
created.push('inbox/');
|
|
898
857
|
}
|
|
899
858
|
|
|
859
|
+
// Writers assemble files under `inbox/.staging/` and atomically rename them
|
|
860
|
+
// into `inbox/`; the gitignored staging dir keeps a half-written drop
|
|
861
|
+
// invisible to the watcher until it lands.
|
|
862
|
+
|
|
863
|
+
fs.mkdirSync(path.join(rootInbox, '.staging'), { recursive: true });
|
|
864
|
+
|
|
900
865
|
if (intakeCap?.dedup === 'sha256') {
|
|
901
866
|
const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
|
|
902
867
|
const manifestExists = fs.existsSync(path.join(target, MANIFEST_REL_PATH));
|
|
@@ -906,11 +871,10 @@ async function main() {
|
|
|
906
871
|
}
|
|
907
872
|
}
|
|
908
873
|
} else {
|
|
874
|
+
console.log(`[init:intake] skipping inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.`);
|
|
909
875
|
skipped.push('inbox/ (deferred to existing intake)');
|
|
910
876
|
}
|
|
911
877
|
|
|
912
|
-
// Legacy archetype block removed — root inbox/ is universal (construct-1arm).
|
|
913
|
-
|
|
914
878
|
// Gitignore every Construct-generated artifact whose disposition is `ignored`
|
|
915
879
|
// (ADR-0027 §1): the six adapter dirs, the `.construct/` launcher, `.cx/`
|
|
916
880
|
// runtime state, and the generated config files all carry machine-specific
|
|
@@ -1075,28 +1039,18 @@ async function main() {
|
|
|
1075
1039
|
console.warn(`⚠️ Could not inject agent-instruction block: ${err.message}`);
|
|
1076
1040
|
}
|
|
1077
1041
|
|
|
1078
|
-
// Intake
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1042
|
+
// Intake policy — write the single-zone model (ADR-0045 §C) so the user can
|
|
1043
|
+
// inspect what's watched and edit it directly. The canonical `inbox/` at the
|
|
1044
|
+
// project root is the only drop zone; empty parentDirs is correct — extra
|
|
1045
|
+
// dirs are opt-in.
|
|
1082
1046
|
|
|
1083
1047
|
const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
|
|
1084
1048
|
|
|
1085
|
-
if (inboxDecision.skip) intakeConfig.includeProjectInbox = false;
|
|
1086
|
-
|
|
1087
|
-
intakeConfig.includeRootInbox = !inboxDecision.skip;
|
|
1088
|
-
intakeConfig.includeArchetypeInbox = intakeConfig.includeRootInbox;
|
|
1089
|
-
|
|
1090
1049
|
const { saveIntakePolicy } = await import('./config/intake-policy.mjs');
|
|
1091
1050
|
try {
|
|
1092
1051
|
saveIntakePolicy(target, {
|
|
1093
1052
|
maxDepth: intakeConfig.maxDepth,
|
|
1094
1053
|
additionalDirs: intakeConfig.parentDirs ?? [],
|
|
1095
|
-
zones: {
|
|
1096
|
-
rootInbox: intakeConfig.includeRootInbox !== false,
|
|
1097
|
-
projectInbox: intakeConfig.includeProjectInbox !== false,
|
|
1098
|
-
docsIntake: intakeConfig.includeDocsIntake !== false,
|
|
1099
|
-
},
|
|
1100
1054
|
});
|
|
1101
1055
|
created.push('construct.config.json (intakePolicy)');
|
|
1102
1056
|
} catch (err) {
|
|
@@ -1115,7 +1069,7 @@ async function main() {
|
|
|
1115
1069
|
// Create documentation system if lanes specified
|
|
1116
1070
|
if (lanes.length > 0) {
|
|
1117
1071
|
// Filter out lanes that the project already covers elsewhere
|
|
1118
|
-
// (issue #97: don't create docs/meetings/ when internal/meetings/
|
|
1072
|
+
// (issue #97: don't create docs/notes/meetings/ when internal/meetings/
|
|
1119
1073
|
// has 12 markdown files). --force bypasses the filter.
|
|
1120
1074
|
|
|
1121
1075
|
const deferredLanes = [];
|
|
@@ -1210,7 +1164,7 @@ async function main() {
|
|
|
1210
1164
|
const seed = await syncFileStateToSql(target, { env: process.env, project: projectName });
|
|
1211
1165
|
if (seed?.status === 'ok' && (seed.documentsSynced || 0) > 0) {
|
|
1212
1166
|
console.log(`\n🔍 Indexed existing project material: ${seed.documentsSynced} doc(s), ${seed.embeddingsSynced || 0} embeddings.`);
|
|
1213
|
-
console.log(` The agent can now compare new intake (
|
|
1167
|
+
console.log(` The agent can now compare new intake (inbox/) against your existing PRDs, ADRs, and notes.`);
|
|
1214
1168
|
}
|
|
1215
1169
|
} catch { /* silent — corpus seeding is best-effort, not a setup blocker */ }
|
|
1216
1170
|
|
|
@@ -1241,9 +1195,9 @@ async function main() {
|
|
|
1241
1195
|
console.log(`${++step}. Edit plan.md`);
|
|
1242
1196
|
console.log(' Add your current work and tasks');
|
|
1243
1197
|
console.log('');
|
|
1244
|
-
if (
|
|
1198
|
+
if (created.includes('inbox/')) {
|
|
1245
1199
|
console.log(`${++step}. Use Intake`);
|
|
1246
|
-
console.log(' Drop files in
|
|
1200
|
+
console.log(' Drop files in inbox/ for processing');
|
|
1247
1201
|
console.log(' Run: construct intake');
|
|
1248
1202
|
console.log('');
|
|
1249
1203
|
}
|
package/lib/init.mjs
CHANGED
|
@@ -77,12 +77,14 @@ function main() {
|
|
|
77
77
|
content: `${JSON.stringify({ version: 1, formality: 3, toneOverride: {}, audienceLexicon: { prefer: [], avoid: [] } }, null, 2)}\n`,
|
|
78
78
|
generator: "construct/init",
|
|
79
79
|
});
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// Single canonical drop zone (ADR-0045 §C): the visible project-root `inbox/`,
|
|
81
|
+
// covered by the host .gitignore (ADR-0027 §1). Writers stage under the
|
|
82
|
+
// gitignored `inbox/.staging/` and atomically rename in; the watcher consumes
|
|
83
|
+
// only complete top-level files. Machine/runtime intake state lives under the
|
|
84
|
+
// gitignored `.cx/intake/`, created lazily by the queue.
|
|
83
85
|
|
|
84
|
-
fs.mkdirSync(path.join(target, "
|
|
85
|
-
created.push("
|
|
86
|
+
fs.mkdirSync(path.join(target, "inbox", ".staging"), { recursive: true });
|
|
87
|
+
created.push("inbox/");
|
|
86
88
|
|
|
87
89
|
process.stdout.write(`\nConstruct init → ${target}\n\n`);
|
|
88
90
|
if (created.length) {
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* lib/install/desktop-binary-download.mjs — downloads the Construct chat desktop binary.
|
|
3
3
|
*
|
|
4
4
|
* Fetches the platform-matching construct-chat binary from the latest GitHub
|
|
5
|
-
* release and writes it to
|
|
5
|
+
* release and writes it to the bin/ subdir of the XDG state dir. Called by
|
|
6
|
+
* `construct install --scope=user`.
|
|
6
7
|
* Network failures are non-fatal: returns { status: 'skipped', reason } so the
|
|
7
8
|
* rest of setup can proceed without the desktop binary.
|
|
8
9
|
*/
|
|
@@ -10,6 +11,8 @@
|
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
|
|
14
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
15
|
+
|
|
13
16
|
const GITHUB_REPO = 'geraldmaron/construct';
|
|
14
17
|
|
|
15
18
|
function assetName() {
|
|
@@ -46,7 +49,7 @@ async function downloadAsset(url, dest, env) {
|
|
|
46
49
|
|
|
47
50
|
// Returns { status: 'downloaded'|'skipped'|'failed' }; 'skipped' and 'failed' are both non-fatal.
|
|
48
51
|
export async function downloadDesktopBinary({ homeDir, env = process.env } = {}) {
|
|
49
|
-
const binDir = path.join(homeDir, '
|
|
52
|
+
const binDir = path.join(stateDir(homeDir), 'bin');
|
|
50
53
|
const dest = path.join(binDir, destBinaryName());
|
|
51
54
|
const name = assetName();
|
|
52
55
|
|
package/lib/intake/daemon.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/intake/daemon.mjs — Continuous intake daemon with safeguards.
|
|
3
3
|
*
|
|
4
|
-
* Polls
|
|
5
|
-
* classifier, and writes packets to `.cx/intake/pending/`.
|
|
6
|
-
* TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
|
-
* the budget then dead-letters with the
|
|
4
|
+
* Polls the canonical project-root `inbox/` for new files, classifies them via
|
|
5
|
+
* the existing intake classifier, and writes packets to `.cx/intake/pending/`.
|
|
6
|
+
* Packets past their TTL move to `.cx/intake/dead-letter/`; failed
|
|
7
|
+
* classification retries up to the budget then dead-letters with the reason.
|
|
8
|
+
*
|
|
9
|
+
* Atomic handoff (ADR-0045 §C): only complete top-level files are enqueued.
|
|
10
|
+
* Dotfiles and the `inbox/.staging/` assembly directory are skipped, so a file
|
|
11
|
+
* a writer is still staging is invisible until it is renamed into `inbox/`.
|
|
8
12
|
*
|
|
9
13
|
* Built on lib/daemons/contract.mjs — every safeguard (bounded lifetime,
|
|
10
14
|
* idle shutdown, heartbeat, killswitch, single-writer lock) applies.
|
|
@@ -12,30 +16,49 @@
|
|
|
12
16
|
|
|
13
17
|
import { readdirSync, statSync, readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
|
14
18
|
import { join, basename } from 'node:path';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
19
|
|
|
17
20
|
import { createDaemon, classifyPacket } from '../daemons/contract.mjs';
|
|
18
21
|
import { memoryCapMbFor } from '../resources/process-budget.mjs';
|
|
22
|
+
import { stateDir } from '../config/xdg.mjs';
|
|
19
23
|
|
|
20
24
|
const KILLSWITCH_ENV = 'CONSTRUCT_INTAKE_DAEMON';
|
|
21
25
|
|
|
22
|
-
function inboxDir(cwd) { return join(cwd, '
|
|
26
|
+
function inboxDir(cwd) { return join(cwd, 'inbox'); }
|
|
23
27
|
function pendingDir(cwd) { return join(cwd, '.cx', 'intake', 'pending'); }
|
|
24
28
|
function deadLetterDir(cwd) { return join(cwd, '.cx', 'intake', 'dead-letter'); }
|
|
25
|
-
function heartbeatPath() { return join(
|
|
29
|
+
function heartbeatPath() { return join(stateDir(), 'intake-daemon.heartbeat'); }
|
|
26
30
|
|
|
27
31
|
function ensureDir(dir) {
|
|
28
32
|
try { mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
29
33
|
}
|
|
30
34
|
|
|
35
|
+
// A writer staging into `inbox/.staging/` and renaming into `inbox/` is the
|
|
36
|
+
// atomic handoff. As a backstop for writers that drop in place, a file whose
|
|
37
|
+
// size is still moving between two stats is mid-write — skip it this tick.
|
|
38
|
+
|
|
39
|
+
function isSizeStable(full) {
|
|
40
|
+
try {
|
|
41
|
+
const a = statSync(full);
|
|
42
|
+
const b = statSync(full);
|
|
43
|
+
return a.size === b.size;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
function listInboxFiles(cwd) {
|
|
32
50
|
const dir = inboxDir(cwd);
|
|
33
51
|
if (!existsSync(dir)) return [];
|
|
34
52
|
let entries;
|
|
35
53
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return []; }
|
|
54
|
+
|
|
55
|
+
// Only complete top-level files: dotfiles (and the `.staging/` assembly dir)
|
|
56
|
+
// are skipped, so a file a writer is still staging is invisible until rename.
|
|
57
|
+
|
|
36
58
|
return entries
|
|
37
59
|
.filter((e) => e.isFile() && !e.name.startsWith('.'))
|
|
38
|
-
.map((e) => join(dir, e.name))
|
|
60
|
+
.map((e) => join(dir, e.name))
|
|
61
|
+
.filter((full) => isSizeStable(full));
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
/**
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/intake/intake-config.mjs — intake watcher config facade.
|
|
3
3
|
*
|
|
4
|
-
* Reads and writes intake policy via construct.config.json intakePolicy
|
|
5
|
-
*
|
|
4
|
+
* Reads and writes intake policy via construct.config.json intakePolicy, the
|
|
5
|
+
* only config source (single-zone model, ADR-0045 §C).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { shouldCreateCx } from '../project-detection.mjs';
|
|
9
9
|
import {
|
|
10
|
-
loadIntakePolicy,
|
|
11
10
|
resolvedIntakeConfig,
|
|
12
11
|
saveIntakePolicy,
|
|
13
|
-
migrateLegacyIntakeConfig,
|
|
14
12
|
DEFAULT_INTAKE_POLICY,
|
|
15
13
|
} from '../config/intake-policy.mjs';
|
|
16
14
|
import {
|
|
@@ -19,38 +17,22 @@ import {
|
|
|
19
17
|
INTAKE_DEPTH_GUIDANCE,
|
|
20
18
|
describeIntakeDepth,
|
|
21
19
|
} from './constants.mjs';
|
|
22
|
-
import { INTAKE_CONFIG_FILE } from './legacy-paths.mjs';
|
|
23
|
-
import { join } from 'node:path';
|
|
24
20
|
|
|
25
21
|
export {
|
|
26
22
|
INTAKE_DEFAULT_MAX_DEPTH,
|
|
27
23
|
INTAKE_HARD_MAX_DEPTH,
|
|
28
24
|
INTAKE_DEPTH_GUIDANCE,
|
|
29
25
|
describeIntakeDepth,
|
|
30
|
-
INTAKE_CONFIG_FILE,
|
|
31
|
-
migrateLegacyIntakeConfig,
|
|
32
26
|
DEFAULT_INTAKE_POLICY,
|
|
33
27
|
};
|
|
34
28
|
|
|
35
29
|
export const DEFAULT_INTAKE_CONFIG = Object.freeze({
|
|
36
30
|
parentDirs: [],
|
|
37
31
|
maxDepth: INTAKE_DEFAULT_MAX_DEPTH,
|
|
38
|
-
includeProjectInbox: true,
|
|
39
|
-
includeDocsIntake: true,
|
|
40
|
-
includeArchetypeInbox: true,
|
|
41
|
-
includeRootInbox: true,
|
|
42
32
|
});
|
|
43
33
|
|
|
44
|
-
export function intakeConfigPath(rootDir) {
|
|
45
|
-
return join(rootDir, INTAKE_CONFIG_FILE);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
34
|
export function loadIntakeConfig(rootDir, env = process.env) {
|
|
49
|
-
|
|
50
|
-
if (cfg.legacyWarning && process.stderr.isTTY) {
|
|
51
|
-
process.stderr.write(`[intake-config] ${cfg.legacyWarning}\n`);
|
|
52
|
-
}
|
|
53
|
-
return cfg;
|
|
35
|
+
return resolvedIntakeConfig(rootDir, env);
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
export function saveIntakeConfig(rootDir, patch = {}) {
|
|
@@ -60,24 +42,15 @@ export function saveIntakeConfig(rootDir, patch = {}) {
|
|
|
60
42
|
|
|
61
43
|
const policyPatch = {};
|
|
62
44
|
if (patch.maxDepth !== undefined) policyPatch.maxDepth = patch.maxDepth;
|
|
63
|
-
if (patch.includeProjectInbox !== undefined) policyPatch.projectInbox = patch.includeProjectInbox;
|
|
64
|
-
if (patch.includeDocsIntake !== undefined) policyPatch.docsIntake = patch.includeDocsIntake;
|
|
65
|
-
if (patch.includeArchetypeInbox !== undefined || patch.includeRootInbox !== undefined) {
|
|
66
|
-
policyPatch.rootInbox = patch.includeRootInbox ?? patch.includeArchetypeInbox;
|
|
67
|
-
}
|
|
68
45
|
if (Array.isArray(patch.parentDirs)) policyPatch.additionalDirs = patch.parentDirs;
|
|
69
46
|
|
|
70
47
|
const { policy } = saveIntakePolicy(rootDir, policyPatch);
|
|
71
|
-
return
|
|
48
|
+
return policyToConfigShape(policy);
|
|
72
49
|
}
|
|
73
50
|
|
|
74
|
-
function
|
|
51
|
+
function policyToConfigShape(policy) {
|
|
75
52
|
return {
|
|
76
53
|
parentDirs: policy.additionalDirs,
|
|
77
54
|
maxDepth: policy.maxDepth,
|
|
78
|
-
includeProjectInbox: policy.zones.projectInbox,
|
|
79
|
-
includeDocsIntake: policy.zones.docsIntake,
|
|
80
|
-
includeArchetypeInbox: policy.zones.rootInbox,
|
|
81
|
-
includeRootInbox: policy.zones.rootInbox,
|
|
82
55
|
};
|
|
83
56
|
}
|
|
@@ -18,6 +18,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
|
18
18
|
import { join } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { configDir } from '../config/xdg.mjs';
|
|
21
22
|
|
|
22
23
|
// ── Multi-method auth helpers ────────────────────────────────────────────
|
|
23
24
|
|
|
@@ -268,8 +269,8 @@ export async function createJiraTicket(packet, { host, email, token, project, is
|
|
|
268
269
|
const resolvedToken = token || auth.token || process.env.JIRA_API_TOKEN;
|
|
269
270
|
const resolvedProject = project || auth.project || process.env.JIRA_PROJECT;
|
|
270
271
|
|
|
271
|
-
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.construct/config.env or shell rc.' };
|
|
272
|
-
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
272
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_HOST not set (e.g. https://your-domain.atlassian.net). Check ~/.config/construct/config.env or shell rc.' };
|
|
273
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_USER and JIRA_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
|
|
273
274
|
if (!resolvedProject) return { ok: false, externalUrl: null, externalId: null, error: 'JIRA_PROJECT not set (e.g. PROJ)' };
|
|
274
275
|
|
|
275
276
|
const t = packet?.triage || {};
|
|
@@ -385,8 +386,8 @@ export async function publishArtifactToConfluence(artifact, { host, email, token
|
|
|
385
386
|
const resolvedToken = token || auth.token || process.env.CONFLUENCE_API_TOKEN;
|
|
386
387
|
const resolvedSpace = space || auth.space || process.env.CONFLUENCE_SPACE;
|
|
387
388
|
|
|
388
|
-
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.construct/config.env or shell rc.' };
|
|
389
|
-
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.construct/config.env, ~/.env, or 1Password.' };
|
|
389
|
+
if (!resolvedHost) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_HOST not set (e.g. https://your-domain.atlassian.net/wiki). Check ~/.config/construct/config.env or shell rc.' };
|
|
390
|
+
if (!resolvedEmail || !resolvedToken) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_USER and CONFLUENCE_API_TOKEN required. Check ~/.config/construct/config.env, ~/.env, or 1Password.' };
|
|
390
391
|
if (!resolvedSpace) return { ok: false, externalUrl: null, externalId: null, error: 'CONFLUENCE_SPACE not set (e.g. PROD)' };
|
|
391
392
|
|
|
392
393
|
const prefix = { prd: 'PRD', adr: 'ADR', rfc: 'RFC' }[artifact.type] || artifact.type.toUpperCase();
|
|
@@ -608,7 +609,7 @@ function resolveCredentialSync(varName, homeDir) {
|
|
|
608
609
|
}
|
|
609
610
|
} catch { /* skip */ }
|
|
610
611
|
try {
|
|
611
|
-
const cfgPath = join(homeDir, '
|
|
612
|
+
const cfgPath = join(configDir(homeDir), 'config.env');
|
|
612
613
|
if (existsSync(cfgPath)) {
|
|
613
614
|
const content = readFileSync(cfgPath, 'utf8');
|
|
614
615
|
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
|
@@ -703,9 +704,9 @@ function resolveCredential(varName, homeDir) {
|
|
|
703
704
|
}
|
|
704
705
|
} catch { /* skip */ }
|
|
705
706
|
|
|
706
|
-
//
|
|
707
|
+
// config.env in the XDG config dir
|
|
707
708
|
try {
|
|
708
|
-
const cfgPath = join(homeDir, '
|
|
709
|
+
const cfgPath = join(configDir(homeDir), 'config.env');
|
|
709
710
|
if (existsSync(cfgPath)) {
|
|
710
711
|
const content = readFileSync(cfgPath, 'utf8');
|
|
711
712
|
const m = content.match(new RegExp(`^${varName}=(.+)$`, 'm'));
|
package/lib/knowledge/rag.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Sources indexed:
|
|
9
9
|
* - Observations (.cx/observations/)
|
|
10
|
-
* - Artifacts (docs/adr/, docs/prd/, docs/rfc/)
|
|
10
|
+
* - Artifacts (docs/decisions/adr/, docs/specs/prd/, docs/decisions/rfc/)
|
|
11
11
|
* - Snapshots (.cx/snapshot.md + any configured output paths)
|
|
12
12
|
* - Approval queue (.cx/approval-queue.jsonl)
|
|
13
13
|
*
|
|
@@ -102,7 +102,7 @@ function loadMarkdownChunks(dir, source) {
|
|
|
102
102
|
*/
|
|
103
103
|
function loadArtifactChunks(rootDir) {
|
|
104
104
|
const chunks = [];
|
|
105
|
-
for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/concepts/architecture.md']) {
|
|
105
|
+
for (const subdir of ['docs/adr', 'docs/prd', 'docs/rfc', 'docs/guides/concepts/architecture.md']) {
|
|
106
106
|
const full = path.resolve(rootDir, subdir);
|
|
107
107
|
if (subdir.endsWith('.md')) {
|
|
108
108
|
if (!fs.existsSync(full)) continue;
|
package/lib/knowledge/search.mjs
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* `knowledge_search` MCP tool — no daemon, no network, no external deps.
|
|
7
7
|
*
|
|
8
8
|
* Sources searched (in priority order):
|
|
9
|
-
* 1. docs/concepts/*.md — architecture, agents, gates, durable state
|
|
10
|
-
* 2. docs/start/*.mdx — install + first task
|
|
9
|
+
* 1. docs/guides/concepts/*.md — architecture, agents, gates, durable state
|
|
10
|
+
* 2. docs/guides/start/*.mdx — install + first task
|
|
11
11
|
* 3. docs/README.md — docs index + contract
|
|
12
12
|
* 4. .cx/knowledge/ — operator-written internal docs
|
|
13
|
-
* 5. Any *.md in docs/cookbook/ — task-oriented recipes
|
|
13
|
+
* 5. Any *.md in docs/guides/cookbook/ — task-oriented recipes
|
|
14
14
|
* 6. <projectRoot>/.cx/knowledge/** — the cwd project's knowledge tree,
|
|
15
15
|
* including `external/research/` written by `construct knowledge add`,
|
|
16
16
|
* so foreign-project queries surface project content alongside bundled docs.
|
|
@@ -47,17 +47,17 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
|
|
|
47
47
|
const sources = [];
|
|
48
48
|
|
|
49
49
|
const priority = [
|
|
50
|
-
'docs/concepts/architecture.md',
|
|
51
|
-
'docs/concepts/agents-and-personas.mdx',
|
|
52
|
-
'docs/concepts/gates-and-enforcement.md',
|
|
53
|
-
'docs/concepts/beads-and-state.md',
|
|
54
|
-
'docs/concepts/prompt-surfaces.md',
|
|
55
|
-
'docs/concepts/knowledge-layout.md',
|
|
56
|
-
'docs/concepts/deployment-model.md',
|
|
57
|
-
'docs/concepts/embedding-boundary.md',
|
|
50
|
+
'docs/guides/concepts/architecture.md',
|
|
51
|
+
'docs/guides/concepts/agents-and-personas.mdx',
|
|
52
|
+
'docs/guides/concepts/gates-and-enforcement.md',
|
|
53
|
+
'docs/guides/concepts/beads-and-state.md',
|
|
54
|
+
'docs/guides/concepts/prompt-surfaces.md',
|
|
55
|
+
'docs/guides/concepts/knowledge-layout.md',
|
|
56
|
+
'docs/guides/concepts/deployment-model.md',
|
|
57
|
+
'docs/guides/concepts/embedding-boundary.md',
|
|
58
58
|
'docs/README.md',
|
|
59
|
-
'docs/start/install.mdx',
|
|
60
|
-
'docs/start/first-task.mdx',
|
|
59
|
+
'docs/guides/start/install.mdx',
|
|
60
|
+
'docs/guides/start/first-task.mdx',
|
|
61
61
|
];
|
|
62
62
|
|
|
63
63
|
for (const rel of priority) {
|
|
@@ -66,12 +66,12 @@ function buildSourceList(repoRoot = REPO_ROOT, projectRoot = null) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// Cookbook recipes (task-oriented how-tos)
|
|
69
|
-
const cookbookDir = join(repoRoot, 'docs', 'cookbook');
|
|
69
|
+
const cookbookDir = join(repoRoot, 'docs', 'guides', 'cookbook');
|
|
70
70
|
if (existsSync(cookbookDir)) {
|
|
71
71
|
for (const file of readdirSync(cookbookDir)) {
|
|
72
72
|
if (file.endsWith('.md') || file.endsWith('.mdx')) {
|
|
73
73
|
const full = join(cookbookDir, file);
|
|
74
|
-
sources.push({ path: full, rel: `docs/cookbook/${file}`, priority: 2 });
|
|
74
|
+
sources.push({ path: full, rel: `docs/guides/cookbook/${file}`, priority: 2 });
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* Triggered from two entry points:
|
|
11
11
|
* - `construct cleanup` — manual sweep, optionally --dry-run
|
|
12
12
|
* - bin/construct startup check — automatic when the installed version
|
|
13
|
-
* differs from
|
|
14
|
-
* so an upgrade reclaims
|
|
13
|
+
* differs from the XDG state-dir
|
|
14
|
+
* .cleanup-stamp, so an upgrade reclaims
|
|
15
|
+
* disk without
|
|
15
16
|
* requiring a manual command.
|
|
16
17
|
*
|
|
17
18
|
* Scope is deliberately narrow: only artifacts whose retention is owned by
|
|
@@ -24,6 +25,9 @@ import fs from 'node:fs';
|
|
|
24
25
|
import os from 'node:os';
|
|
25
26
|
import path from 'node:path';
|
|
26
27
|
|
|
28
|
+
import { stateDir, cacheDir } from '../config/xdg.mjs';
|
|
29
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
30
|
+
|
|
27
31
|
const HARD_BUDGET_MS = 5000;
|
|
28
32
|
|
|
29
33
|
// Size caps in bytes. Each entry: [filename relative to ~/.cx/, maxBytes].
|
|
@@ -92,7 +96,7 @@ function rotationConfig(env = process.env) {
|
|
|
92
96
|
* were oversized before the cap existed.
|
|
93
97
|
*/
|
|
94
98
|
export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dryRun = false } = {}) {
|
|
95
|
-
const dir = path.join(homeDir, '
|
|
99
|
+
const dir = path.join(doctorRoot(homeDir), 'runtime');
|
|
96
100
|
const baseLog = path.join(dir, 'embed-daemon.log');
|
|
97
101
|
const { maxBytes, keep } = rotationConfig(env);
|
|
98
102
|
|
|
@@ -149,7 +153,7 @@ export function cleanupEmbedLog({ homeDir = os.homedir(), env = process.env, dry
|
|
|
149
153
|
* recent records survive.
|
|
150
154
|
*/
|
|
151
155
|
export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}) {
|
|
152
|
-
const dir =
|
|
156
|
+
const dir = doctorRoot(homeDir);
|
|
153
157
|
const summary = { truncated: [], freedBytes: 0 };
|
|
154
158
|
if (!fs.existsSync(dir)) return summary;
|
|
155
159
|
|
|
@@ -168,11 +172,11 @@ export function cleanupJsonlLogs({ homeDir = os.homedir(), dryRun = false } = {}
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
|
-
* Age out
|
|
175
|
+
* Age out XDG cache-dir entries older than `maxAgeDays`. Cache hits are
|
|
172
176
|
* idempotent and re-fetched on demand, so pruning is safe.
|
|
173
177
|
*/
|
|
174
178
|
export function cleanupCacheDir({ homeDir = os.homedir(), maxAgeDays = 30, dryRun = false } = {}) {
|
|
175
|
-
const dir =
|
|
179
|
+
const dir = cacheDir(homeDir);
|
|
176
180
|
const summary = { removed: [], freedBytes: 0 };
|
|
177
181
|
if (!fs.existsSync(dir)) return summary;
|
|
178
182
|
|
|
@@ -249,18 +253,18 @@ export function formatBytes(bytes) {
|
|
|
249
253
|
// ---------------------------------------------------------------------------
|
|
250
254
|
// Version stamp — drives automatic cleanup on upgrade.
|
|
251
255
|
//
|
|
252
|
-
//
|
|
253
|
-
// On every CLI invocation, bin/construct compares the current package
|
|
256
|
+
// The .cleanup-stamp in the XDG state dir records the last version that ran
|
|
257
|
+
// cleanup. On every CLI invocation, bin/construct compares the current package
|
|
254
258
|
// version to the stamp. A mismatch triggers a single cleanup pass and
|
|
255
259
|
// writes the new version into the stamp. The first ever invocation also
|
|
256
260
|
// triggers cleanup (no stamp present) — this is what unblocks existing
|
|
257
261
|
// installs that already have 34 GB logs sitting around.
|
|
258
262
|
// ---------------------------------------------------------------------------
|
|
259
263
|
|
|
260
|
-
const
|
|
264
|
+
const STAMP_FILENAME = '.cleanup-stamp';
|
|
261
265
|
|
|
262
266
|
export function stampPath(homeDir = os.homedir()) {
|
|
263
|
-
return path.join(homeDir,
|
|
267
|
+
return path.join(stateDir(homeDir), STAMP_FILENAME);
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
export function readVersionStamp({ homeDir = os.homedir() } = {}) {
|
package/lib/mcp/server.mjs
CHANGED
|
@@ -859,7 +859,7 @@ const ALL_TOOL_DEFS = [
|
|
|
859
859
|
},
|
|
860
860
|
{
|
|
861
861
|
name: 'profile_create',
|
|
862
|
-
description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/concepts/profile-lifecycle.md` after creation.',
|
|
862
|
+
description: 'Scaffold a draft org profile under `.cx/profiles/draft-<id>/` (requirements.md + profile.json + persona stubs + department charters). Writes durable state — requires `confirm=true`. For curated catalog work, follow `docs/guides/concepts/profile-lifecycle.md` after creation.',
|
|
863
863
|
inputSchema: {
|
|
864
864
|
type: 'object',
|
|
865
865
|
required: ['confirm', 'id'],
|