@geraldmaron/construct 1.0.5 → 1.0.7
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 +24 -18
- package/agents/contracts.json +617 -2
- package/agents/contracts.schema.json +83 -0
- package/agents/prompts/cx-accessibility.md +5 -3
- package/agents/prompts/cx-ai-engineer.md +9 -7
- package/agents/prompts/cx-architect.md +8 -6
- package/agents/prompts/cx-business-strategist.md +8 -6
- package/agents/prompts/cx-data-analyst.md +9 -7
- package/agents/prompts/cx-data-engineer.md +5 -3
- package/agents/prompts/cx-debugger.md +8 -6
- package/agents/prompts/cx-designer.md +7 -5
- package/agents/prompts/cx-devil-advocate.md +6 -4
- package/agents/prompts/cx-docs-keeper.md +7 -5
- package/agents/prompts/cx-engineer.md +7 -5
- package/agents/prompts/cx-evaluator.md +4 -2
- package/agents/prompts/cx-explorer.md +10 -8
- package/agents/prompts/cx-legal-compliance.md +4 -2
- package/agents/prompts/cx-operations.md +7 -5
- package/agents/prompts/cx-orchestrator.md +12 -10
- package/agents/prompts/cx-platform-engineer.md +7 -5
- package/agents/prompts/cx-product-manager.md +6 -4
- package/agents/prompts/cx-qa.md +10 -8
- package/agents/prompts/cx-rd-lead.md +11 -9
- package/agents/prompts/cx-release-manager.md +7 -5
- package/agents/prompts/cx-researcher.md +24 -22
- package/agents/prompts/cx-reviewer.md +9 -7
- package/agents/prompts/cx-security.md +12 -10
- package/agents/prompts/cx-sre.md +9 -7
- package/agents/prompts/cx-test-automation.md +5 -3
- package/agents/prompts/cx-trace-reviewer.md +10 -8
- package/agents/prompts/cx-ux-researcher.md +5 -3
- package/agents/registry.json +8 -3
- package/bin/construct +444 -53
- package/commands/build/feature.md +4 -4
- package/commands/build/fix.md +8 -8
- package/commands/design/access.md +3 -3
- package/commands/design/flow.md +3 -3
- package/commands/design/ui.md +4 -4
- package/commands/measure/experiment.md +5 -5
- package/commands/measure/metrics.md +3 -3
- package/commands/measure/results.md +4 -4
- package/commands/plan/api.md +3 -3
- package/commands/plan/challenge.md +3 -3
- package/commands/plan/decide.md +3 -3
- package/commands/plan/feature.md +7 -7
- package/commands/plan/requirements.md +3 -3
- package/commands/remember/context.md +5 -5
- package/commands/remember/handoff.md +3 -3
- package/commands/remember/runbook.md +3 -3
- package/commands/review/code.md +8 -8
- package/commands/review/quality.md +4 -4
- package/commands/review/security.md +3 -3
- package/commands/ship/ready.md +3 -3
- package/commands/ship/release.md +3 -3
- package/commands/ship/status.md +4 -4
- package/commands/understand/docs.md +3 -3
- package/commands/understand/this.md +4 -4
- package/commands/understand/why.md +8 -8
- package/commands/work/clean.md +14 -14
- package/commands/work/drive.md +10 -10
- package/commands/work/optimize-prompts.md +9 -9
- package/commands/work/parallel-review.md +8 -8
- package/db/schema/006_graph.sql +24 -0
- package/examples/provider-plugin/README.md +7 -7
- package/examples/seed-observations/README.md +6 -6
- package/examples/seed-observations/anti-patterns.md +14 -14
- package/examples/seed-observations/decisions.md +4 -4
- package/examples/seed-observations/patterns.md +14 -14
- package/lib/auto-docs.mjs +13 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +2 -2
- package/lib/comment-lint.mjs +134 -0
- package/lib/contracts/validate.mjs +323 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/docs-verify.mjs +59 -6
- package/lib/doctor/cli.mjs +16 -1
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/watchers/consistency.mjs +310 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +211 -1
- package/lib/embed/cli.mjs +124 -3
- package/lib/embed/daemon.mjs +43 -4
- package/lib/embed/docs-lifecycle.mjs +1 -1
- package/lib/embed/inbox.mjs +2 -0
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/flavors/loader.mjs +1 -1
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/mcp-audit.mjs +2 -1
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/init-unified.mjs +55 -65
- package/lib/intake/classify.mjs +108 -24
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +6 -1
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/prepare.mjs +0 -1
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/graph.mjs +213 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +189 -1
- package/lib/mcp/tools/profile.mjs +270 -0
- package/lib/mcp/tools/workflow.mjs +25 -0
- package/lib/mcp-catalog.json +12 -8
- package/lib/mcp-platform-config.mjs +16 -8
- package/lib/migrations/index.mjs +106 -0
- package/lib/migrations/v1-baseline.mjs +33 -0
- package/lib/observation-store.mjs +9 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/reflect.mjs +49 -12
- package/lib/server/index.mjs +22 -28
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/telemetry/client.mjs +5 -1
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/package.json +4 -1
- package/personas/construct.md +3 -1
- package/platforms/claude/CLAUDE.md +6 -6
- package/rules/common/agents.md +2 -2
- package/rules/common/beads-hygiene.md +11 -11
- package/rules/common/code-review.md +1 -1
- package/rules/common/coding-style.md +1 -1
- package/rules/common/comments.md +8 -8
- package/rules/common/commit-approval.md +4 -4
- package/rules/common/cx-agent-routing.md +2 -2
- package/rules/common/cx-skill-routing.md +2 -2
- package/rules/common/development-workflow.md +3 -2
- package/rules/common/doc-ownership.md +2 -2
- package/rules/common/efficiency.md +3 -3
- package/rules/common/framing.md +1 -1
- package/rules/common/git-workflow.md +1 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +1 -1
- package/rules/common/release-gates.md +7 -7
- package/rules/common/research.md +4 -4
- package/rules/common/review-before-change.md +58 -0
- package/rules/common/security.md +1 -1
- package/rules/common/skill-composition.md +8 -8
- package/rules/common/testing.md +1 -1
- package/rules/golang/coding-style.md +2 -2
- package/rules/golang/hooks.md +1 -1
- package/rules/golang/patterns.md +1 -1
- package/rules/golang/security.md +1 -1
- package/rules/golang/testing.md +1 -1
- package/rules/python/coding-style.md +1 -1
- package/rules/python/hooks.md +1 -1
- package/rules/python/patterns.md +1 -1
- package/rules/python/security.md +1 -1
- package/rules/python/testing.md +1 -1
- package/rules/swift/coding-style.md +3 -3
- package/rules/swift/hooks.md +2 -2
- package/rules/swift/patterns.md +2 -2
- package/rules/swift/security.md +4 -4
- package/rules/swift/testing.md +2 -2
- package/rules/typescript/coding-style.md +1 -1
- package/rules/typescript/hooks.md +1 -1
- package/rules/typescript/patterns.md +1 -1
- package/rules/typescript/security.md +1 -1
- package/rules/typescript/testing.md +1 -1
- package/rules/web/coding-style.md +1 -1
- package/rules/web/design-quality.md +1 -1
- package/rules/web/hooks.md +1 -1
- package/rules/web/patterns.md +1 -1
- package/rules/web/performance.md +1 -1
- package/rules/web/security.md +1 -1
- package/rules/web/testing.md +1 -1
- package/scripts/sync-agents.mjs +45 -14
- package/skills/ai/agent-dev.md +1 -1
- package/skills/ai/llm-security.md +1 -1
- package/skills/ai/ml-ops.md +6 -6
- package/skills/ai/orchestration-workflow.md +1 -1
- package/skills/ai/prompt-and-eval.md +1 -1
- package/skills/ai/prompt-optimizer.md +13 -13
- package/skills/ai/rag-system.md +1 -1
- package/skills/architecture/api-design.md +1 -1
- package/skills/architecture/caching.md +1 -1
- package/skills/architecture/cloud-native.md +1 -1
- package/skills/architecture/message-queue.md +1 -1
- package/skills/architecture/security-arch.md +1 -1
- package/skills/compliance/ai-disclosure.md +1 -1
- package/skills/compliance/data-privacy.md +1 -1
- package/skills/compliance/license-audit.md +2 -2
- package/skills/compliance/regulatory-review.md +1 -1
- package/skills/development/cpp.md +1 -1
- package/skills/development/go.md +1 -1
- package/skills/development/java.md +1 -1
- package/skills/development/kotlin.md +9 -9
- package/skills/development/mobile-crossplatform.md +13 -13
- package/skills/development/python.md +1 -1
- package/skills/development/rust.md +1 -1
- package/skills/development/shell.md +1 -1
- package/skills/development/swift.md +6 -6
- package/skills/development/typescript.md +1 -1
- package/skills/devops/ci-cd.md +5 -5
- package/skills/devops/containerization.md +9 -9
- package/skills/devops/cost-optimization.md +1 -1
- package/skills/devops/data-engineering.md +2 -2
- package/skills/devops/database.md +1 -1
- package/skills/devops/dependency-management.md +3 -3
- package/skills/devops/devsecops.md +1 -1
- package/skills/devops/git-workflow.md +1 -1
- package/skills/devops/incident-response.md +18 -18
- package/skills/devops/monorepo.md +5 -5
- package/skills/devops/observability.md +1 -1
- package/skills/devops/performance.md +1 -1
- package/skills/devops/testing.md +1 -1
- package/skills/docs/adr-workflow.md +2 -2
- package/skills/docs/backlog-proposal-workflow.md +1 -1
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/document-ingest-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +1 -1
- package/skills/docs/init-docs.md +15 -15
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +3 -3
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +1 -1
- package/skills/docs/product-signal-workflow.md +9 -9
- package/skills/docs/research-workflow.md +10 -10
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/strategy-workflow.md +3 -3
- package/skills/exploration/repo-map.md +11 -11
- package/skills/frameworks/django.md +15 -15
- package/skills/frameworks/nextjs.md +16 -16
- package/skills/frameworks/react.md +12 -12
- package/skills/frameworks/spring-boot.md +12 -12
- package/skills/frontend-design/accessibility.md +6 -6
- package/skills/frontend-design/component-patterns.md +1 -1
- package/skills/frontend-design/engineering.md +1 -1
- package/skills/frontend-design/state-management.md +1 -1
- package/skills/frontend-design/ui-aesthetics.md +1 -1
- package/skills/frontend-design/ux-principles.md +1 -1
- package/skills/operating/orchestration-reference.md +27 -27
- package/skills/quality-gates/review-work.md +3 -3
- package/skills/quality-gates/verify-change.md +1 -1
- package/skills/quality-gates/verify-module.md +1 -1
- package/skills/quality-gates/verify-quality.md +1 -1
- package/skills/quality-gates/verify-security.md +1 -1
- package/skills/routing.md +14 -14
- package/skills/security/blue-team.md +1 -1
- package/skills/security/code-audit.md +1 -1
- package/skills/security/pentest.md +1 -1
- package/skills/security/red-team.md +1 -1
- package/skills/security/threat-intel.md +1 -1
- package/skills/security/vuln-research.md +1 -1
- package/skills/utility/clean-code.md +2 -2
- package/templates/docs/changelog-entry.md +1 -1
- package/templates/docs/construct_guide.md +18 -18
- package/templates/docs/meta-prd.md +16 -16
- package/templates/docs/one-pager.md +1 -1
- package/templates/docs/prd-business.md +1 -1
- package/templates/docs/prd-platform.md +1 -1
- package/templates/docs/prd.md +17 -17
- package/templates/docs/research-brief.md +8 -8
- package/templates/docs/rfc.md +1 -1
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/docs/strategy.md +1 -1
package/lib/embed/daemon.mjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { existsSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
28
28
|
import { homedir } from 'node:os';
|
|
29
|
-
import { join } from 'node:path';
|
|
29
|
+
import { dirname, join } from 'node:path';
|
|
30
30
|
|
|
31
31
|
import { loadEmbedConfig } from './config.mjs';
|
|
32
32
|
import { Scheduler } from './scheduler.mjs';
|
|
@@ -109,8 +109,44 @@ async function resolveLLMKey(varName, env) {
|
|
|
109
109
|
|
|
110
110
|
// ─── Root dir resolution ─────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Cap upward search so a daemon launched from deep in the filesystem cannot
|
|
113
|
+
// fan out to thousands of stat() calls in pathological layouts.
|
|
114
|
+
|
|
115
|
+
const PROJECT_WALKUP_MAX = 10;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Walk up from `startDir` looking for a directory that contains `.cx/context.md`.
|
|
119
|
+
* Returns the project root path, or null if nothing is found within the cap.
|
|
120
|
+
* Exported for unit tests.
|
|
121
|
+
*/
|
|
122
|
+
export function findProjectRoot(startDir, { maxLevels = PROJECT_WALKUP_MAX } = {}) {
|
|
123
|
+
if (!startDir || typeof startDir !== 'string') return null;
|
|
124
|
+
let current = startDir;
|
|
125
|
+
for (let i = 0; i <= maxLevels; i++) {
|
|
126
|
+
try {
|
|
127
|
+
if (existsSync(join(current, '.cx', 'context.md'))) return current;
|
|
128
|
+
} catch { /* skip unreadable dir */ }
|
|
129
|
+
const parent = dirname(current);
|
|
130
|
+
if (!parent || parent === current) return null;
|
|
131
|
+
current = parent;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolve the daemon's rootDir.
|
|
138
|
+
*
|
|
139
|
+
* Precedence:
|
|
140
|
+
* 1. `CX_DATA_DIR` env override (operator authority)
|
|
141
|
+
* 2. Project root discovered by walking up from `cwd` looking for `.cx/context.md`
|
|
142
|
+
* 3. `homedir()` fallback
|
|
143
|
+
*/
|
|
144
|
+
export function resolveRootDir(env = process.env, cwd = process.cwd()) {
|
|
145
|
+
const override = (env.CX_DATA_DIR ?? '').trim();
|
|
146
|
+
if (override) return override;
|
|
147
|
+
const projectRoot = findProjectRoot(cwd);
|
|
148
|
+
if (projectRoot) return projectRoot;
|
|
149
|
+
return homedir();
|
|
114
150
|
}
|
|
115
151
|
|
|
116
152
|
function resolveWorkspaceDir(env = process.env) {
|
|
@@ -708,7 +744,10 @@ this.#scheduler.register(
|
|
|
708
744
|
async () => {
|
|
709
745
|
const result = await this.#inboxWatcher.poll();
|
|
710
746
|
if (result.processed.length) {
|
|
711
|
-
|
|
747
|
+
const { getRebrand } = await import('../profiles/rebrand.mjs');
|
|
748
|
+
const { signalNoun } = getRebrand(this.#rootDir);
|
|
749
|
+
const noun = result.processed.length === 1 ? signalNoun : `${signalNoun}s`;
|
|
750
|
+
process.stderr.write(`[embed] Classified ${result.processed.length} ${noun} from [${result.dirs.join(', ')}]\n`);
|
|
712
751
|
addObservation(this.#rootDir, {
|
|
713
752
|
role: 'construct',
|
|
714
753
|
category: 'insight',
|
|
@@ -64,7 +64,7 @@ export function detectDocGaps(target, opts = {}) {
|
|
|
64
64
|
const gaps = [];
|
|
65
65
|
|
|
66
66
|
if (target.access === 'remote') {
|
|
67
|
-
// Remote-only targets
|
|
67
|
+
// Remote-only targets have no local filesystem to inspect; gap detection requires the local docs tree.
|
|
68
68
|
return gaps;
|
|
69
69
|
}
|
|
70
70
|
|
package/lib/embed/inbox.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { addObservation } from '../observation-store.mjs';
|
|
|
29
29
|
import { inferKnowledgeTarget, knowledgeDirForCategory } from '../knowledge/layout.mjs';
|
|
30
30
|
import { docLaneDir, suggestDocsLaneForFile } from '../docs-routing.mjs';
|
|
31
31
|
import { shouldCreateCx } from '../project-detection.mjs';
|
|
32
|
+
import { ensureCxDir } from '../project-init-shared.mjs';
|
|
32
33
|
import { prepareIntakeForIngestedFile } from '../intake/prepare.mjs';
|
|
33
34
|
import { loadIntakeConfig, INTAKE_DEFAULT_MAX_DEPTH } from '../intake/intake-config.mjs';
|
|
34
35
|
|
|
@@ -90,6 +91,7 @@ function writeState(rootDir, state) {
|
|
|
90
91
|
|
|
91
92
|
// Only create .cx directories if this is an initialized project
|
|
92
93
|
if (shouldCreateCx(rootDir)) {
|
|
94
|
+
ensureCxDir(rootDir);
|
|
93
95
|
mkdirSync(join(rootDir, '.cx', 'runtime'), { recursive: true });
|
|
94
96
|
writeFileSync(p, JSON.stringify(state, null, 2) + '\n');
|
|
95
97
|
}
|
package/lib/embed/scheduler.mjs
CHANGED
|
@@ -4,24 +4,30 @@
|
|
|
4
4
|
* Lightweight alternative to a full cron library — runs tasks at fixed
|
|
5
5
|
* intervals, tracks last-run times, and supports graceful shutdown.
|
|
6
6
|
* Zero external deps.
|
|
7
|
+
*
|
|
8
|
+
* One-shot startup jobs MUST set `repeat: false`. With `repeat: true`
|
|
9
|
+
* (the default) and `intervalMs: 0`, the timer fires on every event-loop
|
|
10
|
+
* tick — the bug that filled embed-daemon.log to 34 GB before this option
|
|
11
|
+
* was added.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
export class Scheduler {
|
|
10
|
-
#tasks = new Map(); // id → { fn, intervalMs, lastRun, timer, label }
|
|
15
|
+
#tasks = new Map(); // id → { fn, intervalMs, lastRun, timer, label, repeat }
|
|
11
16
|
#running = false;
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Register a task. Returns the task id.
|
|
15
20
|
* @param {string} label - Human-readable name for logging
|
|
16
|
-
* @param {number} intervalMs - How often to run (ms)
|
|
21
|
+
* @param {number} intervalMs - How often to run (ms); ignored when repeat is false
|
|
17
22
|
* @param {Function} fn - Async function to call; receives { label, lastRun }
|
|
18
23
|
* @param {object} [opts]
|
|
19
24
|
* @param {boolean} [opts.runImmediately=false] - Run once before first interval fires
|
|
20
25
|
* @param {boolean} [opts.unref=false] - Call timer.unref() so this task does not keep the event loop alive (use in tests)
|
|
26
|
+
* @param {boolean} [opts.repeat=true] - When false, the task is one-shot: it runs once (immediately if runImmediately, otherwise after intervalMs) and never re-arms. Required for startup jobs that pass intervalMs: 0.
|
|
21
27
|
*/
|
|
22
|
-
register(label, intervalMs, fn, { runImmediately = false, unref = false } = {}) {
|
|
28
|
+
register(label, intervalMs, fn, { runImmediately = false, unref = false, repeat = true } = {}) {
|
|
23
29
|
const id = `${label}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
24
|
-
const task = { label, intervalMs, fn, lastRun: null, timer: null, runImmediately, unref };
|
|
30
|
+
const task = { label, intervalMs, fn, lastRun: null, timer: null, runImmediately, unref, repeat };
|
|
25
31
|
this.#tasks.set(id, task);
|
|
26
32
|
|
|
27
33
|
if (this.#running) {
|
|
@@ -50,6 +56,7 @@ export class Scheduler {
|
|
|
50
56
|
for (const task of this.#tasks.values()) {
|
|
51
57
|
if (task.timer) {
|
|
52
58
|
clearInterval(task.timer);
|
|
59
|
+
clearTimeout(task.timer);
|
|
53
60
|
task.timer = null;
|
|
54
61
|
}
|
|
55
62
|
}
|
|
@@ -60,7 +67,10 @@ export class Scheduler {
|
|
|
60
67
|
*/
|
|
61
68
|
unregister(id) {
|
|
62
69
|
const task = this.#tasks.get(id);
|
|
63
|
-
if (task?.timer)
|
|
70
|
+
if (task?.timer) {
|
|
71
|
+
clearInterval(task.timer);
|
|
72
|
+
clearTimeout(task.timer);
|
|
73
|
+
}
|
|
64
74
|
this.#tasks.delete(id);
|
|
65
75
|
}
|
|
66
76
|
|
|
@@ -74,6 +84,7 @@ export class Scheduler {
|
|
|
74
84
|
intervalMs: t.intervalMs,
|
|
75
85
|
lastRun: t.lastRun,
|
|
76
86
|
active: Boolean(t.timer),
|
|
87
|
+
repeat: t.repeat,
|
|
77
88
|
}));
|
|
78
89
|
}
|
|
79
90
|
|
|
@@ -83,6 +94,23 @@ export class Scheduler {
|
|
|
83
94
|
process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
|
|
84
95
|
});
|
|
85
96
|
}
|
|
97
|
+
|
|
98
|
+
// One-shot jobs do not re-arm. When runImmediately is true, the task has
|
|
99
|
+
// already executed and no further scheduling is needed. When false, a
|
|
100
|
+
// single setTimeout fires once at intervalMs.
|
|
101
|
+
if (!task.repeat) {
|
|
102
|
+
if (!runImmediately) {
|
|
103
|
+
task.timer = setTimeout(() => {
|
|
104
|
+
task.timer = null;
|
|
105
|
+
this.#runTask(task).catch((err) => {
|
|
106
|
+
process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
|
|
107
|
+
});
|
|
108
|
+
}, task.intervalMs);
|
|
109
|
+
if (task.unref && task.timer.unref) task.timer.unref();
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
task.timer = setInterval(() => {
|
|
87
115
|
this.#runTask(task).catch((err) => {
|
|
88
116
|
process.stderr.write(`[scheduler] ${task.label} error: ${err.message}\n`);
|
|
@@ -95,9 +95,8 @@ export async function evaluateDocument({ content, docType, evaluatorFn = null })
|
|
|
95
95
|
return await evaluatorFn({ content, docType, rubric });
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Default: deterministic rubric
|
|
99
|
-
//
|
|
100
|
-
// For now, we use structural checks as a proxy
|
|
98
|
+
// Default path: deterministic structural-rubric evaluation.
|
|
99
|
+
// Consumers wanting LLM-graded evaluation supply `evaluatorFn` (handled above).
|
|
101
100
|
|
|
102
101
|
const scores = [];
|
|
103
102
|
const feedback = [];
|
package/lib/flavors/loader.mjs
CHANGED
|
@@ -130,7 +130,7 @@ export function validateFlavor(filePath) {
|
|
|
130
130
|
errors.push(`${filePath}: applies_to must be a non-empty array`);
|
|
131
131
|
}
|
|
132
132
|
if (!Array.isArray(fm.profiles) || fm.profiles.length === 0) {
|
|
133
|
-
errors.push(`${filePath}: profiles must be a non-empty array
|
|
133
|
+
errors.push(`${filePath}: profiles must be a non-empty array; add a frontmatter line like \`profiles: [rnd]\``);
|
|
134
134
|
}
|
|
135
135
|
return errors;
|
|
136
136
|
}
|
|
@@ -40,8 +40,24 @@ const rootDir = path.resolve(fileURLToPath(import.meta.url), '..', '..', '..');
|
|
|
40
40
|
const result = lintFile(filePath, { rootDir });
|
|
41
41
|
if (!result.errors.length && !result.warnings.length) process.exit(0);
|
|
42
42
|
|
|
43
|
+
// Artifact-prose violations are advisory at write-time (no-fabrication policy
|
|
44
|
+
// enforces them at CI/release-gate time via CONSTRUCT_ARTIFACT_LINT_MODE=block).
|
|
45
|
+
// Surface them on stderr but do not block the edit.
|
|
46
|
+
const artifactOnly = (
|
|
47
|
+
result.errors.length === 0 &&
|
|
48
|
+
result.warnings.every((w) => w.kind === 'artifact')
|
|
49
|
+
);
|
|
50
|
+
|
|
43
51
|
const { output } = formatResults([result]);
|
|
44
52
|
process.stderr.write(output);
|
|
53
|
+
|
|
54
|
+
if (artifactOnly) {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
'\nArtifact-lint advisory (no-fabrication). The edit is not blocked, but these will fail the release gate. Fix before push.\n',
|
|
57
|
+
);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
process.stderr.write(
|
|
46
62
|
'\nComment policy blocked this edit. Remove the violations or set CONSTRUCT_SKIP_COMMENT_LINT=1.\n',
|
|
47
63
|
);
|
package/lib/hooks/mcp-audit.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
11
11
|
import { join } from 'path';
|
|
12
12
|
import { shouldCreateCx } from '../project-detection.mjs';
|
|
13
|
+
import { ensureCxDir } from '../project-init-shared.mjs';
|
|
13
14
|
|
|
14
15
|
let input = {};
|
|
15
16
|
try { input = JSON.parse(readFileSync(0, 'utf8')); } catch { process.exit(0); }
|
|
@@ -47,7 +48,7 @@ if (audit[mcpServer].length > 200) audit[mcpServer] = audit[mcpServer].slice(-20
|
|
|
47
48
|
|
|
48
49
|
try {
|
|
49
50
|
if (shouldCreateCxDir) {
|
|
50
|
-
|
|
51
|
+
ensureCxDir(cwd);
|
|
51
52
|
writeFileSync(auditPath, JSON.stringify(audit, null, 2));
|
|
52
53
|
}
|
|
53
54
|
} catch { /* best effort */ }
|
|
@@ -239,20 +239,6 @@ export function onScheduleCheck() {
|
|
|
239
239
|
return events;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
/**
|
|
243
|
-
* Get all pending activations for a specialist.
|
|
244
|
-
*/
|
|
245
|
-
export function getPendingActivations(specialist) {
|
|
246
|
-
// In a full implementation, this would query a pending queue
|
|
247
|
-
// For now, return recent activation history
|
|
248
|
-
const state = getActivationState(specialist);
|
|
249
|
-
return {
|
|
250
|
-
pending: 0,
|
|
251
|
-
recent: state.activations.length,
|
|
252
|
-
total: state.totalActivations,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
242
|
/**
|
|
257
243
|
* Clear activation state (for testing or manual reset).
|
|
258
244
|
*/
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/hooks/rule-verifier.mjs — Stop hook that audits the session for
|
|
4
|
+
* compliance with the construct rules the agent claimed to follow.
|
|
5
|
+
*
|
|
6
|
+
* Checks:
|
|
7
|
+
* - Each commit / push was preceded by an approval signal scoped to that
|
|
8
|
+
* branch + change.
|
|
9
|
+
* - Edits to protected files (CLAUDE.md table) had user confirmation.
|
|
10
|
+
* - Beads claims exist for any work that touched durable state.
|
|
11
|
+
*
|
|
12
|
+
* Approval detection is intent-based, not keyword-based. The verifier reads
|
|
13
|
+
* the conversational window before each consequential action and classifies
|
|
14
|
+
* the user's stance: APPROVED, REFUSED, INCONCLUSIVE. Direct affirmation,
|
|
15
|
+
* accepting a proposed plan that named the action, or "go ahead" all count
|
|
16
|
+
* as approval — no specific word is required.
|
|
17
|
+
*
|
|
18
|
+
* Output: { ok: 'pass' | 'fail' | 'inconclusive', findings[] } written to
|
|
19
|
+
* .cx/audit.jsonl. Inconclusive results surface as escalation questions to
|
|
20
|
+
* the user rather than silent passes or false-positive blocks.
|
|
21
|
+
*
|
|
22
|
+
* @p95ms 200
|
|
23
|
+
* @maxBlockingScope Stop
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
import { logHookFailure } from './_lib/log.mjs';
|
|
29
|
+
|
|
30
|
+
const AUDIT_REL = '.cx/audit.jsonl';
|
|
31
|
+
|
|
32
|
+
function safeReadJsonLines(transcriptPath) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(transcriptPath, 'utf8');
|
|
35
|
+
return raw.split('\n').map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);
|
|
36
|
+
} catch { return []; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function lastN(entries, count) {
|
|
40
|
+
return entries.slice(Math.max(0, entries.length - count));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Identify consequential actions in the transcript tail. Returns a list of
|
|
45
|
+
* { kind, branch, target, atIndex } items. Each one needs an approval signal
|
|
46
|
+
* within the window of conversation immediately preceding it.
|
|
47
|
+
*
|
|
48
|
+
* Detected actions:
|
|
49
|
+
* - 'commit' — git commit invocation
|
|
50
|
+
* - 'push' — git push invocation
|
|
51
|
+
* - 'protected' — edit to a CLAUDE.md-protected file
|
|
52
|
+
* - 'durable-state' — bd close / claim against durable state
|
|
53
|
+
*/
|
|
54
|
+
export function findConsequentialActions(entries) {
|
|
55
|
+
const actions = [];
|
|
56
|
+
for (let i = 0; i < entries.length; i++) {
|
|
57
|
+
const e = entries[i];
|
|
58
|
+
const cmd = extractBashCommand(e);
|
|
59
|
+
if (cmd) {
|
|
60
|
+
if (/^git\s+commit\b/.test(cmd) && !/--dry-run/.test(cmd)) {
|
|
61
|
+
actions.push({ kind: 'commit', target: cmd.slice(0, 200), atIndex: i });
|
|
62
|
+
} else if (/^git\s+push\b/.test(cmd) && !/--dry-run/.test(cmd)) {
|
|
63
|
+
actions.push({ kind: 'push', target: cmd.slice(0, 200), atIndex: i });
|
|
64
|
+
} else if (/^gh\s+pr\s+(create|merge)\b/.test(cmd)) {
|
|
65
|
+
actions.push({ kind: 'pr-mutation', target: cmd.slice(0, 200), atIndex: i });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const editPath = extractEditPath(e);
|
|
69
|
+
if (editPath && /CLAUDE\.md|install\.sh|claude\/settings\.template\.json|agents\/registry\.json/.test(editPath)) {
|
|
70
|
+
actions.push({ kind: 'protected', target: editPath, atIndex: i });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return actions;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractBashCommand(entry) {
|
|
77
|
+
const toolUse = entry?.message?.content?.find?.((c) => c?.type === 'tool_use' && c?.name === 'Bash');
|
|
78
|
+
return toolUse?.input?.command || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function extractEditPath(entry) {
|
|
82
|
+
const toolUse = entry?.message?.content?.find?.((c) => c?.type === 'tool_use' && (c?.name === 'Edit' || c?.name === 'Write'));
|
|
83
|
+
return toolUse?.input?.file_path || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Classify a conversational window for approval intent. This is intentionally
|
|
88
|
+
* conservative — it does not require any keyword. If the window contains a
|
|
89
|
+
* user turn that directly addresses the action being taken (mentioning the
|
|
90
|
+
* branch, the file, or accepting a proposed plan), that counts as approval.
|
|
91
|
+
*
|
|
92
|
+
* The default classifier is deterministic: it scans for plausible approval
|
|
93
|
+
* tokens AND for the action target appearing earlier in the conversation.
|
|
94
|
+
* A pluggable classifier can replace this with an LLM-graded inference at
|
|
95
|
+
* runtime — register via setApprovalClassifier(fn).
|
|
96
|
+
*
|
|
97
|
+
* Returns: 'APPROVED' | 'REFUSED' | 'INCONCLUSIVE'.
|
|
98
|
+
*/
|
|
99
|
+
let _approvalClassifier = defaultApprovalClassifier;
|
|
100
|
+
|
|
101
|
+
export function setApprovalClassifier(fn) {
|
|
102
|
+
_approvalClassifier = typeof fn === 'function' ? fn : defaultApprovalClassifier;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function classifyApproval(window, action) {
|
|
106
|
+
return _approvalClassifier(window, action);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defaultApprovalClassifier(window, action) {
|
|
110
|
+
if (!Array.isArray(window) || window.length === 0) return 'INCONCLUSIVE';
|
|
111
|
+
|
|
112
|
+
let assistantProposedAction = false;
|
|
113
|
+
let userResponded = false;
|
|
114
|
+
let userRefused = false;
|
|
115
|
+
let userAffirmed = false;
|
|
116
|
+
|
|
117
|
+
for (const entry of window) {
|
|
118
|
+
const role = entry?.message?.role || entry?.role;
|
|
119
|
+
const text = extractText(entry).toLowerCase();
|
|
120
|
+
if (!text) continue;
|
|
121
|
+
|
|
122
|
+
if (role === 'assistant') {
|
|
123
|
+
if (actionMentioned(text, action)) assistantProposedAction = true;
|
|
124
|
+
}
|
|
125
|
+
if (role === 'user') {
|
|
126
|
+
userResponded = true;
|
|
127
|
+
if (/\b(no|stop|don'?t|cancel|wait|abort|hold off|never mind)\b/.test(text)) userRefused = true;
|
|
128
|
+
if (/\b(yes|yep|yeah|go ahead|do it|proceed|ship it|approve|confirmed|sounds good|that works|lgtm|sgtm|ok|sure|please do|merge it|continue)\b/.test(text)) userAffirmed = true;
|
|
129
|
+
// Even without explicit affirmation words, accepting a named plan is approval.
|
|
130
|
+
if (assistantProposedAction && !userRefused && text.length > 0 && !/\?$/.test(text.trim())) {
|
|
131
|
+
userAffirmed = userAffirmed || /\b(thanks|great|perfect|sounds|works|good)\b/.test(text);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (userRefused) return 'REFUSED';
|
|
137
|
+
if (userAffirmed && assistantProposedAction) return 'APPROVED';
|
|
138
|
+
if (userAffirmed) return 'APPROVED';
|
|
139
|
+
if (!userResponded) return 'INCONCLUSIVE';
|
|
140
|
+
return 'INCONCLUSIVE';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractText(entry) {
|
|
144
|
+
const msg = entry?.message;
|
|
145
|
+
if (!msg) return '';
|
|
146
|
+
if (typeof msg.content === 'string') return msg.content;
|
|
147
|
+
if (Array.isArray(msg.content)) {
|
|
148
|
+
return msg.content.map((c) => (typeof c === 'string' ? c : c?.text || '')).join(' ');
|
|
149
|
+
}
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function actionMentioned(text, action) {
|
|
154
|
+
if (!text) return false;
|
|
155
|
+
if (action.kind === 'commit' || action.kind === 'push' || action.kind === 'pr-mutation') {
|
|
156
|
+
return /\b(commit|push|merge|ship|pr|pull request)\b/.test(text);
|
|
157
|
+
}
|
|
158
|
+
if (action.kind === 'protected') {
|
|
159
|
+
const fname = action.target.split('/').pop().toLowerCase();
|
|
160
|
+
return text.includes(fname.toLowerCase()) || text.includes(action.target.toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Verify a transcript against the rule set. Returns a structured audit
|
|
167
|
+
* result. Side-effect free; the runtime entry point below writes the result
|
|
168
|
+
* to .cx/audit.jsonl.
|
|
169
|
+
*/
|
|
170
|
+
export function verifyTranscript(entries, { windowSize = 20 } = {}) {
|
|
171
|
+
const findings = [];
|
|
172
|
+
const actions = findConsequentialActions(entries);
|
|
173
|
+
|
|
174
|
+
for (const action of actions) {
|
|
175
|
+
const window = lastN(entries.slice(0, action.atIndex), windowSize);
|
|
176
|
+
const verdict = classifyApproval(window, action);
|
|
177
|
+
findings.push({
|
|
178
|
+
action: action.kind,
|
|
179
|
+
target: action.target,
|
|
180
|
+
verdict,
|
|
181
|
+
atIndex: action.atIndex,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const failed = findings.some((f) => f.verdict === 'REFUSED');
|
|
186
|
+
const inconclusive = findings.some((f) => f.verdict === 'INCONCLUSIVE');
|
|
187
|
+
let ok;
|
|
188
|
+
if (failed) ok = 'fail';
|
|
189
|
+
else if (inconclusive) ok = 'inconclusive';
|
|
190
|
+
else ok = 'pass';
|
|
191
|
+
|
|
192
|
+
return { ok, findings, actionCount: actions.length };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function writeAudit(cwd, payload) {
|
|
196
|
+
const path = join(cwd, AUDIT_REL);
|
|
197
|
+
try { mkdirSync(dirname(path), { recursive: true }); } catch { /* ignore */ }
|
|
198
|
+
try { appendFileSync(path, JSON.stringify(payload) + '\n'); }
|
|
199
|
+
catch (err) { logHookFailure({ hook: 'rule-verifier', err, phase: 'append-audit' }); }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
203
|
+
let input = {};
|
|
204
|
+
try { input = JSON.parse(readFileSync(0, 'utf8')); }
|
|
205
|
+
catch (err) { logHookFailure({ hook: 'rule-verifier', err, phase: 'parse-stdin' }); process.exit(0); }
|
|
206
|
+
|
|
207
|
+
const cwd = input?.cwd || process.cwd();
|
|
208
|
+
const transcriptPath = input?.transcript_path;
|
|
209
|
+
if (!transcriptPath || !existsSync(transcriptPath)) process.exit(0);
|
|
210
|
+
|
|
211
|
+
const entries = safeReadJsonLines(transcriptPath);
|
|
212
|
+
const result = verifyTranscript(entries);
|
|
213
|
+
writeAudit(cwd, { ts: new Date().toISOString(), hook: 'rule-verifier', ...result });
|
|
214
|
+
|
|
215
|
+
if (result.ok === 'fail' && process.env.CONSTRUCT_RULE_VERIFIER === 'block') process.exit(2);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
@@ -237,7 +237,8 @@ function main() {
|
|
|
237
237
|
console.log(` • ${agent.name}: avgScore=${agent.avgScore}, invocations=${agent.invocations}`);
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
//
|
|
240
|
+
// Sequential optimization keeps prompt-tuning deterministic and avoids
|
|
241
|
+
// colliding writes to per-agent prompt files.
|
|
241
242
|
let optimizedCount = 0;
|
|
242
243
|
let failedCount = 0;
|
|
243
244
|
|
package/lib/init-unified.mjs
CHANGED
|
@@ -615,87 +615,76 @@ function discoverProjectDirs(targetPath) {
|
|
|
615
615
|
return dirs.sort();
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
];
|
|
618
|
+
// Extra inbox-style directories the user can opt into. These are NOT
|
|
619
|
+
// source-code, doc, or test directories: they're places where unsorted
|
|
620
|
+
// signals tend to land (downloads, scratch notes, exported research). The
|
|
621
|
+
// built-in `.cx/inbox/` and `docs/intake/` are always watched and not
|
|
622
|
+
// listed here.
|
|
623
|
+
//
|
|
624
|
+
// Historically this list included `src/`, `docs/`, `tests/`, etc., and
|
|
625
|
+
// `init --yes` auto-enabled every one that existed in the project. That
|
|
626
|
+
// turned every code change, doc edit, and test file into a synthetic
|
|
627
|
+
// "intake signal" and polluted the queue with hundreds of false positives.
|
|
628
|
+
// Watching project artifact directories is opt-in only; an empty list is
|
|
629
|
+
// the correct default.
|
|
630
|
+
const INTAKE_DIR_PRESETS = [];
|
|
632
631
|
|
|
633
632
|
async function askIntakeCollection(targetPath, skipInteractive) {
|
|
633
|
+
// Non-interactive default: empty parentDirs. The built-in zones
|
|
634
|
+
// (.cx/inbox/ and docs/intake/ when present) handle every well-defined
|
|
635
|
+
// signal path. Users who want to watch additional directories opt in
|
|
636
|
+
// explicitly via `construct intake config set --add-dir=<path>`.
|
|
634
637
|
if (skipInteractive) {
|
|
635
|
-
|
|
636
|
-
const existingDirs = discoverProjectDirs(targetPath);
|
|
637
|
-
const selected = INTAKE_DIR_PRESETS
|
|
638
|
-
.filter(preset => existingDirs.includes(preset.value))
|
|
639
|
-
.map(p => p.value);
|
|
640
|
-
|
|
641
|
-
if (selected.length === 0) return null;
|
|
642
|
-
|
|
643
|
-
console.log('');
|
|
644
|
-
console.log('═══════════════════════════════════════════════════════════');
|
|
645
|
-
console.log(' INTAKE COLLECTION');
|
|
646
|
-
console.log('═══════════════════════════════════════════════════════════');
|
|
647
|
-
console.log('');
|
|
648
|
-
console.log(`Auto-detected ${selected.length} directory(ies) to watch for context:`);
|
|
649
|
-
for (const d of selected) console.log(` • ${d}/`);
|
|
650
|
-
console.log('');
|
|
651
|
-
console.log('The inbox watcher will scan these directories for new files');
|
|
652
|
-
console.log('and route them through R&D triage automatically.');
|
|
653
|
-
console.log('');
|
|
654
|
-
|
|
655
|
-
return { parentDirs: selected, maxDepth: 4 };
|
|
638
|
+
return null;
|
|
656
639
|
}
|
|
657
|
-
|
|
658
|
-
// Interactive mode
|
|
640
|
+
|
|
641
|
+
// Interactive mode: no presets to surface — the user must name the
|
|
642
|
+
// directory they want watched. Skipped entirely when the preset list
|
|
643
|
+
// is empty (the current default after the auto-include regression).
|
|
644
|
+
if (INTAKE_DIR_PRESETS.length === 0) {
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
659
648
|
const existingDirs = discoverProjectDirs(targetPath);
|
|
660
649
|
const presetOptions = INTAKE_DIR_PRESETS.map(p => ({
|
|
661
650
|
label: p.label,
|
|
662
651
|
value: p.value,
|
|
663
|
-
checked:
|
|
652
|
+
checked: false,
|
|
664
653
|
description: p.reason,
|
|
665
654
|
}));
|
|
666
|
-
|
|
655
|
+
|
|
667
656
|
console.log('');
|
|
668
657
|
console.log('═══════════════════════════════════════════════════════════');
|
|
669
658
|
console.log(' INTAKE COLLECTION');
|
|
670
659
|
console.log('═══════════════════════════════════════════════════════════');
|
|
671
660
|
console.log('');
|
|
672
|
-
console.log('Intake watches
|
|
673
|
-
console.log('
|
|
674
|
-
console.log('
|
|
661
|
+
console.log('Intake watches `.cx/inbox/` and `docs/intake/` by default.');
|
|
662
|
+
console.log('Add extra directories ONLY if signals (customer notes, exports,');
|
|
663
|
+
console.log('PDFs, etc.) regularly land there. Do not select code or finished');
|
|
664
|
+
console.log('artifact directories like src/, docs/, or tests/.');
|
|
675
665
|
console.log('');
|
|
676
|
-
|
|
666
|
+
|
|
667
|
+
void existingDirs;
|
|
677
668
|
const selected = await multiSelect({
|
|
678
|
-
title: '
|
|
679
|
-
instructions: 'Press Enter to confirm your selection',
|
|
669
|
+
title: 'Additional Inbox Directories',
|
|
670
|
+
instructions: 'Press Enter to confirm your selection (or pick none)',
|
|
680
671
|
options: presetOptions,
|
|
681
672
|
});
|
|
682
|
-
|
|
673
|
+
|
|
683
674
|
if (selected.length === 0) {
|
|
684
675
|
console.log('');
|
|
685
|
-
console.log('No directories selected. The inbox watcher will
|
|
686
|
-
console.log('.cx/inbox/ and docs/intake/
|
|
687
|
-
console.log('');
|
|
688
|
-
console.log('You can always add more later with:');
|
|
689
|
-
console.log(' construct intake config set --add-dir=src');
|
|
676
|
+
console.log('No extra directories selected. The inbox watcher will scan');
|
|
677
|
+
console.log('.cx/inbox/ and docs/intake/ only. Add more later with:');
|
|
678
|
+
console.log(' construct intake config set --add-dir=<path>');
|
|
690
679
|
console.log('');
|
|
691
680
|
return null;
|
|
692
681
|
}
|
|
693
|
-
|
|
682
|
+
|
|
694
683
|
console.log('');
|
|
695
|
-
console.log(`Watching ${selected.length} directory(ies):`);
|
|
684
|
+
console.log(`Watching ${selected.length} extra directory(ies):`);
|
|
696
685
|
for (const d of selected) console.log(` • ${d}/`);
|
|
697
686
|
console.log('');
|
|
698
|
-
|
|
687
|
+
|
|
699
688
|
return { parentDirs: selected, maxDepth: 4 };
|
|
700
689
|
}
|
|
701
690
|
|
|
@@ -949,18 +938,19 @@ async function main() {
|
|
|
949
938
|
console.log('');
|
|
950
939
|
}
|
|
951
940
|
|
|
952
|
-
// Intake collection —
|
|
941
|
+
// Intake collection — always write intake-config.json with safe defaults
|
|
942
|
+
// so the user can inspect what's watched and edit the file directly. Empty
|
|
943
|
+
// parentDirs is the correct default: the inbox watcher always scans
|
|
944
|
+
// .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
|
|
953
945
|
console.log('[TRACE init:intake-ask]');
|
|
954
|
-
|
|
955
|
-
const intakeConfig = await askIntakeCollection(target, skipInteractive);
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
console.warn(`⚠️ Could not write intake config: ${err.message}`);
|
|
963
|
-
}
|
|
946
|
+
|
|
947
|
+
const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
|
|
948
|
+
const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
|
|
949
|
+
try {
|
|
950
|
+
saveIntakeConfig(target, intakeConfig);
|
|
951
|
+
created.push('.cx/intake-config.json');
|
|
952
|
+
} catch (err) {
|
|
953
|
+
console.warn(`⚠️ Could not write intake config: ${err.message}`);
|
|
964
954
|
}
|
|
965
955
|
|
|
966
956
|
// Ask about documentation system
|