@geraldmaron/construct 1.0.6 → 1.0.8
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 +39 -34
- package/bin/construct +1403 -96
- package/commands/build/feature.md +1 -1
- package/commands/build/fix.md +1 -1
- package/commands/design/access.md +1 -1
- package/commands/design/flow.md +1 -1
- package/commands/design/ui.md +1 -1
- package/commands/measure/experiment.md +1 -1
- package/commands/measure/metrics.md +1 -1
- package/commands/measure/results.md +1 -1
- package/commands/plan/api.md +1 -1
- package/commands/plan/challenge.md +1 -1
- package/commands/plan/decide.md +1 -1
- package/commands/plan/feature.md +1 -1
- package/commands/plan/requirements.md +1 -1
- package/commands/remember/context.md +1 -1
- package/commands/remember/handoff.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/commands/review/code.md +1 -1
- package/commands/review/quality.md +1 -1
- package/commands/review/security.md +1 -1
- package/commands/ship/ready.md +1 -1
- package/commands/ship/release.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/understand/docs.md +1 -1
- package/commands/understand/this.md +1 -1
- package/commands/understand/why.md +1 -1
- package/commands/work/clean.md +1 -1
- package/commands/work/drive.md +1 -1
- package/commands/work/optimize-prompts.md +2 -2
- package/commands/work/parallel-review.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +8 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +139 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +371 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -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 +360 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +420 -17
- package/lib/document-ingest.mjs +2 -0
- 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 +50 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/env-config.mjs +9 -3
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +14 -1
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +73 -65
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +309 -58
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +31 -6
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -1
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +11 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- 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/model-registry.mjs +1 -1
- package/lib/observation-store.mjs +9 -4
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/reflect.mjs +49 -12
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +29 -29
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/client.mjs +5 -1
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +16 -3
- package/personas/construct.md +5 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/development-workflow.md +2 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/review-before-change.md +58 -0
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +171 -68
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/specialists/contracts.json +1002 -0
- package/specialists/contracts.schema.json +83 -0
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +3 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-architect.md +3 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +3 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +3 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-debugger.md +3 -1
- package/{agents → specialists}/prompts/cx-designer.md +3 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +3 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +4 -2
- package/{agents → specialists}/prompts/cx-engineer.md +4 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +3 -1
- package/{agents → specialists}/prompts/cx-explorer.md +3 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +3 -1
- package/{agents → specialists}/prompts/cx-operations.md +3 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +4 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-qa.md +3 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +3 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-researcher.md +3 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +4 -2
- package/{agents → specialists}/prompts/cx-security.md +3 -1
- package/{agents → specialists}/prompts/cx-sre.md +4 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +3 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +5 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +3 -1
- package/{agents → specialists}/registry.json +651 -418
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/docs/construct_guide.md +6 -6
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- package/agents/contracts.json +0 -387
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -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
|
|
|
@@ -161,7 +161,11 @@ try {
|
|
|
161
161
|
}
|
|
162
162
|
} catch { /* non-critical */ }
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// Pricing readout is intentionally suppressed in user-facing notifications.
|
|
165
|
+
// The session-cost ledger is still written (lines below) so OTel + dashboard
|
|
166
|
+
// consumers can pick the data up when the observability surfaces are wired.
|
|
167
|
+
// Do not reintroduce a costNote variable here without a deliberate decision
|
|
168
|
+
// about whether to surface pricing to the user.
|
|
165
169
|
let totalCostUsd = 0;
|
|
166
170
|
let turnsLogged = 0;
|
|
167
171
|
try {
|
|
@@ -232,17 +236,14 @@ try {
|
|
|
232
236
|
|
|
233
237
|
saveTranscriptCheckpoints(checkpointPath, checkpoints);
|
|
234
238
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
costNote = `~$${totalCostUsd.toFixed(2)} ${label}`;
|
|
238
|
-
}
|
|
239
|
+
// totalCostUsd and turnsLogged stay as ledger-write accumulators only.
|
|
240
|
+
// Downstream telemetry consumers read the session-cost.jsonl ledger.
|
|
239
241
|
} catch { /* non-critical */ }
|
|
240
242
|
|
|
241
243
|
const parts = [];
|
|
242
244
|
if (fileCount > 0) parts.push(`${fileCount} file${fileCount !== 1 ? 's' : ''} updated`);
|
|
243
245
|
if (tsResult === 'pass') parts.push('TS OK');
|
|
244
246
|
else if (tsResult !== 'unchecked') parts.push(`TS: ${tsResult}`);
|
|
245
|
-
if (costNote) parts.push(costNote);
|
|
246
247
|
|
|
247
248
|
const hasWarnings = warnings.length > 0;
|
|
248
249
|
const title = hasWarnings ? 'Construct — Review needed' : 'Construct';
|
|
@@ -215,6 +215,10 @@ export function detectHostCapabilities() {
|
|
|
215
215
|
];
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
export function formatHostCapabilitiesJson(hosts = detectHostCapabilities()) {
|
|
219
|
+
return JSON.stringify(hosts, null, 2) + '\n';
|
|
220
|
+
}
|
|
221
|
+
|
|
218
222
|
export function printHostCapabilities(hosts = detectHostCapabilities()) {
|
|
219
223
|
console.log("Construct orchestration host capabilities:");
|
|
220
224
|
for (const host of hosts) {
|
|
@@ -229,5 +233,10 @@ export function printHostCapabilities(hosts = detectHostCapabilities()) {
|
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
232
|
-
|
|
236
|
+
const args = new Set(process.argv.slice(2));
|
|
237
|
+
if (args.has('--json')) {
|
|
238
|
+
process.stdout.write(formatHostCapabilitiesJson());
|
|
239
|
+
} else {
|
|
240
|
+
printHostCapabilities();
|
|
241
|
+
}
|
|
233
242
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/hygiene/scan.mjs — Lightweight doc hygiene candidate scanner.
|
|
3
|
+
*
|
|
4
|
+
* Selects up to N (default 25) documents for hygiene review each run using:
|
|
5
|
+
* 1. lifecycle/approved tag + last_verified_at older than 30 days
|
|
6
|
+
* 2. lifecycle/draft tag + last_verified_at older than 7 days
|
|
7
|
+
* 3. Any document without a last_verified_at field
|
|
8
|
+
*
|
|
9
|
+
* Sorted oldest-first. Results are beads-item candidates — actual creation
|
|
10
|
+
* is handled by the scheduler job handler.
|
|
11
|
+
*
|
|
12
|
+
* Wired into the `doc-hygiene-scan` scheduler job (lib/scheduler/index.mjs).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const APPROVED_STALE_DAYS = 30;
|
|
19
|
+
const DRAFT_STALE_DAYS = 7;
|
|
20
|
+
const DEFAULT_BATCH = 25;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read YAML frontmatter from a markdown file. Returns {} on failure.
|
|
24
|
+
*/
|
|
25
|
+
function readFrontmatter(filePath) {
|
|
26
|
+
let text = '';
|
|
27
|
+
try { text = fs.readFileSync(filePath, 'utf8'); } catch { return {}; }
|
|
28
|
+
const m = text.match(/^---\n([\s\S]*?)\n---/);
|
|
29
|
+
if (!m) return {};
|
|
30
|
+
const fm = {};
|
|
31
|
+
for (const line of m[1].split('\n')) {
|
|
32
|
+
const kv = line.match(/^(\w[\w_-]*):\s*(.+)$/);
|
|
33
|
+
if (kv) fm[kv[1].replace(/-/g, '_')] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
34
|
+
}
|
|
35
|
+
return fm;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Walk a directory and return all .md / .mdx files.
|
|
40
|
+
*/
|
|
41
|
+
function walkMarkdown(dir, results = []) {
|
|
42
|
+
let entries;
|
|
43
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
|
|
44
|
+
for (const e of entries) {
|
|
45
|
+
const p = path.join(dir, e.name);
|
|
46
|
+
if (e.isDirectory()) walkMarkdown(p, results);
|
|
47
|
+
else if (e.name.endsWith('.md') || e.name.endsWith('.mdx')) results.push(p);
|
|
48
|
+
}
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the N oldest hygiene candidates from the given scopes.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} opts
|
|
56
|
+
* @param {string} opts.cwd — project root
|
|
57
|
+
* @param {string[]} [opts.scopes] — directories to scan relative to cwd
|
|
58
|
+
* @param {number} [opts.limit] — max candidates (default 25)
|
|
59
|
+
* @returns {Array<{path, rel, verdict, lastVerifiedAt, ageDays, tags}>}
|
|
60
|
+
*/
|
|
61
|
+
export function findHygieneCandidates({ cwd = process.cwd(), scopes = ['docs/prd', 'docs/adr', 'docs/rfc', '.cx/knowledge'], limit = DEFAULT_BATCH } = {}) {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const candidates = [];
|
|
64
|
+
|
|
65
|
+
for (const scope of scopes) {
|
|
66
|
+
const dir = path.join(cwd, scope);
|
|
67
|
+
if (!fs.existsSync(dir)) continue;
|
|
68
|
+
for (const filePath of walkMarkdown(dir)) {
|
|
69
|
+
const fm = readFrontmatter(filePath);
|
|
70
|
+
// Handle both inline YAML arrays `[a, b]` and comma-separated strings.
|
|
71
|
+
const rawTags = fm.tags ? String(fm.tags).replace(/^\s*\[|\]\s*$/g, '') : '';
|
|
72
|
+
const tags = rawTags ? rawTags.split(',').map((t) => t.trim().replace(/^["']|["']$/g, '')) : [];
|
|
73
|
+
const lva = fm.last_verified_at || null;
|
|
74
|
+
const ageDays = lva ? Math.floor((now - new Date(lva).getTime()) / 86400000) : null;
|
|
75
|
+
|
|
76
|
+
const lifecycle = tags.find((t) => t.startsWith('lifecycle/')) || null;
|
|
77
|
+
|
|
78
|
+
let include = false;
|
|
79
|
+
let reason = '';
|
|
80
|
+
|
|
81
|
+
if (!lva) {
|
|
82
|
+
include = true;
|
|
83
|
+
reason = 'no last_verified_at';
|
|
84
|
+
} else if (lifecycle === 'lifecycle/approved' && ageDays > APPROVED_STALE_DAYS) {
|
|
85
|
+
include = true;
|
|
86
|
+
reason = `approved and stale (${ageDays}d > ${APPROVED_STALE_DAYS}d)`;
|
|
87
|
+
} else if (lifecycle === 'lifecycle/draft' && ageDays > DRAFT_STALE_DAYS) {
|
|
88
|
+
include = true;
|
|
89
|
+
reason = `draft and stale (${ageDays}d > ${DRAFT_STALE_DAYS}d)`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (include) {
|
|
93
|
+
candidates.push({
|
|
94
|
+
path: filePath,
|
|
95
|
+
rel: path.relative(cwd, filePath),
|
|
96
|
+
reason,
|
|
97
|
+
lastVerifiedAt: lva,
|
|
98
|
+
ageDays,
|
|
99
|
+
tags,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Sort: no last_verified_at first (oldest-first by path), then by age descending.
|
|
106
|
+
candidates.sort((a, b) => {
|
|
107
|
+
if (!a.lastVerifiedAt && !b.lastVerifiedAt) return a.rel.localeCompare(b.rel);
|
|
108
|
+
if (!a.lastVerifiedAt) return -1;
|
|
109
|
+
if (!b.lastVerifiedAt) return 1;
|
|
110
|
+
return (b.ageDays || 0) - (a.ageDays || 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return candidates.slice(0, limit);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stamp last_verified_at into a document's frontmatter.
|
|
118
|
+
* If no frontmatter exists, prepends one.
|
|
119
|
+
*/
|
|
120
|
+
export function stampVerified(filePath, { date = new Date().toISOString().slice(0, 10) } = {}) {
|
|
121
|
+
let text = '';
|
|
122
|
+
try { text = fs.readFileSync(filePath, 'utf8'); } catch {
|
|
123
|
+
throw new Error(`stampVerified: cannot read ${filePath}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const fmMatch = text.match(/^(---\n)([\s\S]*?)(\n---)/);
|
|
127
|
+
if (fmMatch) {
|
|
128
|
+
const fmBody = fmMatch[2];
|
|
129
|
+
if (fmBody.includes('last_verified_at:')) {
|
|
130
|
+
// Update existing field.
|
|
131
|
+
const updated = fmBody.replace(/^last_verified_at:.+$/m, `last_verified_at: ${date}`);
|
|
132
|
+
fs.writeFileSync(filePath, `${fmMatch[1]}${updated}${fmMatch[3]}${text.slice(fmMatch[0].length)}`);
|
|
133
|
+
} else {
|
|
134
|
+
// Append field inside frontmatter.
|
|
135
|
+
fs.writeFileSync(filePath, `${fmMatch[1]}${fmBody}\nlast_verified_at: ${date}${fmMatch[3]}${text.slice(fmMatch[0].length)}`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// No frontmatter — prepend one.
|
|
139
|
+
fs.writeFileSync(filePath, `---\nlast_verified_at: ${date}\n---\n${text}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
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
|
|
|
@@ -902,6 +891,24 @@ async function main() {
|
|
|
902
891
|
if (!claudePre && fs.existsSync(path.join(target, '.claude', 'settings.json'))) {
|
|
903
892
|
created.push('.claude/ (agents + settings)');
|
|
904
893
|
}
|
|
894
|
+
|
|
895
|
+
// Also sync home-level adapters for all 6 host surfaces (Copilot, OpenCode,
|
|
896
|
+
// Codex, VS Code, Cursor, plus Claude home) so the user's environment is
|
|
897
|
+
// fully wired after init. Project-local Claude adapters were written above.
|
|
898
|
+
const syncScript = path.join(ROOT_DIR, 'scripts', 'sync-specialists.mjs');
|
|
899
|
+
if (fs.existsSync(syncScript)) {
|
|
900
|
+
const { spawnSync } = await import('node:child_process');
|
|
901
|
+
const homeResult = spawnSync(process.execPath, [syncScript], {
|
|
902
|
+
cwd: target,
|
|
903
|
+
stdio: 'pipe',
|
|
904
|
+
env: { ...process.env, CONSTRUCT_PROJECT_ROOT: target },
|
|
905
|
+
timeout: 120000,
|
|
906
|
+
});
|
|
907
|
+
if (homeResult.status !== 0) {
|
|
908
|
+
const errMsg = homeResult.stderr.toString().substring(0, 200);
|
|
909
|
+
console.warn(`⚠️ Home adapter sync had issues (exit ${homeResult.status}): ${errMsg}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
905
912
|
} catch (err) {
|
|
906
913
|
console.warn(`⚠️ Adapter staging failed: ${err.message}`);
|
|
907
914
|
}
|
|
@@ -949,18 +956,19 @@ async function main() {
|
|
|
949
956
|
console.log('');
|
|
950
957
|
}
|
|
951
958
|
|
|
952
|
-
// Intake collection —
|
|
959
|
+
// Intake collection — always write intake-config.json with safe defaults
|
|
960
|
+
// so the user can inspect what's watched and edit the file directly. Empty
|
|
961
|
+
// parentDirs is the correct default: the inbox watcher always scans
|
|
962
|
+
// .cx/inbox/ and docs/intake/ (when present); extra dirs are opt-in.
|
|
953
963
|
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
|
-
}
|
|
964
|
+
|
|
965
|
+
const intakeConfig = (await askIntakeCollection(target, skipInteractive)) ?? { parentDirs: [], maxDepth: 4 };
|
|
966
|
+
const { saveIntakeConfig } = await import('./intake/intake-config.mjs');
|
|
967
|
+
try {
|
|
968
|
+
saveIntakeConfig(target, intakeConfig);
|
|
969
|
+
created.push('.cx/intake-config.json');
|
|
970
|
+
} catch (err) {
|
|
971
|
+
console.warn(`⚠️ Could not write intake config: ${err.message}`);
|
|
964
972
|
}
|
|
965
973
|
|
|
966
974
|
// Ask about documentation system
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Same work as `bin/construct-postinstall.mjs` but callable from both the
|
|
5
5
|
* npm postinstall hook and `construct init`. Stages `.construct/{run.mjs,
|
|
6
|
-
* bootstrap.sh, bootstrap.ps1, version}` and runs `sync-
|
|
6
|
+
* bootstrap.sh, bootstrap.ps1, version}` and runs `sync-specialists.mjs --project`
|
|
7
7
|
* to materialise `.claude/agents/` and `.claude/settings.json` from the
|
|
8
8
|
* bundled registry. Idempotent — re-runs overwrite the launcher files but
|
|
9
9
|
* leave existing `.construct/version` and project state untouched beyond
|
|
10
|
-
* what `sync-
|
|
10
|
+
* what `sync-specialists.mjs --project` writes itself.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
@@ -20,13 +20,13 @@ export function stageProjectAdapters({ projectRoot, packageRoot, pkgVersion, log
|
|
|
20
20
|
const emit = typeof log === 'function' ? log : () => {};
|
|
21
21
|
|
|
22
22
|
const templateDir = path.join(packageRoot, 'templates', 'distribution');
|
|
23
|
-
const syncScript = path.join(packageRoot, 'scripts', 'sync-
|
|
23
|
+
const syncScript = path.join(packageRoot, 'scripts', 'sync-specialists.mjs');
|
|
24
24
|
|
|
25
25
|
ensureProjectLauncher({ projectRoot, templateDir, pkgVersion });
|
|
26
26
|
emit(`staged .construct/ launcher in ${projectRoot}`);
|
|
27
27
|
|
|
28
28
|
if (!existsSync(syncScript)) {
|
|
29
|
-
emit(`sync-
|
|
29
|
+
emit(`sync-specialists.mjs not found at ${syncScript}; skipping adapter sync`);
|
|
30
30
|
return { staged: true, synced: false };
|
|
31
31
|
}
|
|
32
32
|
|