@geraldmaron/construct 1.0.20 → 1.0.23
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 +23 -5
- package/bin/construct +194 -16
- package/bin/construct-postinstall.mjs +25 -15
- package/lib/acp/server.mjs +113 -0
- package/lib/agent-instructions/inject.mjs +94 -0
- package/lib/auto-docs.mjs +10 -2
- package/lib/cli-commands.mjs +44 -16
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +22 -0
- package/lib/decisions/enforced-baseline.json +3 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/document-ingest.mjs +13 -4
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- package/lib/embedded-contract/triage.mjs +24 -2
- package/lib/embedded-contract/workflow-invoke.mjs +21 -0
- package/lib/features.mjs +11 -11
- package/lib/git-hooks-path.mjs +61 -0
- package/lib/home-namespace.mjs +60 -0
- package/lib/hooks/ci-status-check.mjs +62 -40
- package/lib/hooks/orchestration-dispatch-guard.mjs +153 -0
- package/lib/hooks/pre-push-gate.mjs +15 -6
- package/lib/hooks/stop-notify.mjs +32 -17
- package/lib/hooks/stop-typecheck.mjs +7 -2
- package/lib/host-capabilities.mjs +24 -8
- package/lib/host-disposition.mjs +76 -0
- package/lib/ingest/provider-extract.mjs +1 -1
- package/lib/ingest/strategy.mjs +35 -3
- package/lib/init-docs.mjs +1 -1
- package/lib/init-unified.mjs +320 -219
- package/lib/init-update.mjs +4 -84
- package/lib/init.mjs +9 -25
- package/lib/install/stage-project.mjs +8 -2
- package/lib/intent-classifier.mjs +1 -1
- package/lib/knowledge/search.mjs +52 -3
- package/lib/mcp/server.mjs +57 -14
- package/lib/mcp/tools/memory.mjs +2 -2
- package/lib/mcp/tools/orchestration-run.mjs +125 -0
- package/lib/model-registry.mjs +40 -33
- package/lib/observation-store.mjs +6 -2
- package/lib/opencode-config.mjs +1 -1
- package/lib/orchestration/events.mjs +66 -0
- package/lib/orchestration/run-store-postgres.mjs +85 -0
- package/lib/orchestration/run-store-sqlite.mjs +122 -0
- package/lib/orchestration/runtime.mjs +188 -50
- package/lib/orchestration/store.mjs +102 -0
- package/lib/orchestration/worker.mjs +215 -0
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +80 -26
- package/lib/policy/unified-gates.mjs +96 -0
- package/lib/project-init-shared.mjs +0 -173
- package/lib/reconcile/adapter-prune.mjs +105 -0
- package/lib/reconcile/agent-instructions-rewrap.mjs +98 -0
- package/lib/reconcile/gitignore-coverage.mjs +88 -0
- package/lib/reconcile/index.mjs +171 -0
- package/lib/reconcile/legacy-doctrine-strip.mjs +139 -0
- package/lib/reconcile/legacy-guide-decommit.mjs +67 -0
- package/lib/reconcile/legacy-skills-cleanup.mjs +200 -0
- package/lib/reconcile/mcp-entry-reconcile.mjs +142 -0
- package/lib/reconcile/postgres-namespace.mjs +102 -0
- package/lib/runtime/uv-bootstrap.mjs +27 -3
- package/lib/schema-infer.mjs +16 -2
- package/lib/server/csrf.mjs +14 -2
- package/lib/server/index.mjs +95 -0
- package/lib/service-manager.mjs +39 -14
- package/lib/setup-prompts.mjs +2 -1
- package/lib/setup.mjs +165 -141
- package/lib/storage/backend.mjs +14 -0
- package/lib/storage/unified-storage.mjs +550 -0
- package/lib/template-registry.mjs +73 -0
- package/lib/term-format.mjs +75 -0
- package/lib/uninstall/uninstall.mjs +180 -7
- package/package.json +2 -2
- package/personas/construct.md +7 -8
- package/platforms/claude/settings.template.json +30 -4
- package/platforms/opencode/config.template.json +2 -2
- package/rules/common/neurodivergent-output.md +66 -0
- package/rules/common/tool-invisibility.md +37 -0
- package/scripts/sync-specialists.mjs +381 -95
- package/skills/operating/orchestration-reference.md +2 -16
- package/specialists/policy-inventory.json +14 -0
- package/specialists/prompts/cx-accessibility.md +2 -6
- package/specialists/prompts/cx-ai-engineer.md +0 -4
- package/specialists/prompts/cx-architect.md +3 -5
- package/specialists/prompts/cx-business-strategist.md +0 -5
- package/specialists/prompts/cx-data-analyst.md +0 -4
- package/specialists/prompts/cx-data-engineer.md +0 -4
- package/specialists/prompts/cx-debugger.md +2 -6
- package/specialists/prompts/cx-designer.md +0 -8
- package/specialists/prompts/cx-devil-advocate.md +2 -2
- package/specialists/prompts/cx-docs-keeper.md +0 -13
- package/specialists/prompts/cx-engineer.md +0 -13
- package/specialists/prompts/cx-evaluator.md +2 -2
- package/specialists/prompts/cx-explorer.md +4 -5
- package/specialists/prompts/cx-legal-compliance.md +4 -5
- package/specialists/prompts/cx-operations.md +0 -5
- package/specialists/prompts/cx-orchestrator.md +0 -4
- package/specialists/prompts/cx-platform-engineer.md +0 -8
- package/specialists/prompts/cx-product-manager.md +0 -8
- package/specialists/prompts/cx-qa.md +2 -11
- package/specialists/prompts/cx-rd-lead.md +0 -5
- package/specialists/prompts/cx-release-manager.md +0 -8
- package/specialists/prompts/cx-researcher.md +5 -29
- package/specialists/prompts/cx-reviewer.md +2 -6
- package/specialists/prompts/cx-security.md +2 -11
- package/specialists/prompts/cx-sre.md +0 -15
- package/specialists/prompts/cx-test-automation.md +0 -4
- package/specialists/prompts/cx-trace-reviewer.md +2 -2
- package/specialists/prompts/cx-ux-researcher.md +0 -4
- package/specialists/registry.json +29 -29
- package/templates/distribution/run.mjs +36 -7
- package/templates/docs/accessibility-audit.md +56 -0
- package/templates/docs/architecture-review.md +59 -0
- package/templates/docs/code-review-report.md +46 -0
- package/templates/docs/construct_guide.md +14 -14
- package/templates/docs/debug-investigation.md +53 -0
- package/templates/docs/qa-report.md +48 -0
- package/templates/docs/security-audit-report.md +48 -0
- package/templates/docs/task-packet.md +49 -0
- package/templates/docs/verdict.md +40 -0
- package/lib/server/static/index.html +0 -1
|
@@ -25,179 +25,6 @@ export function writeStampedIfMissing({ targetRoot, created, skipped, filePath,
|
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export function buildAgentsGuide(projectName) {
|
|
29
|
-
return `<!--
|
|
30
|
-
AGENTS.md — canonical operating contract for AI agents working in this repository.
|
|
31
|
-
|
|
32
|
-
Generated by \`construct init\`. Keep this file concise, current, and pruned:
|
|
33
|
-
- update it when workflow rules, tracker hierarchy, or required tools change
|
|
34
|
-
- remove stale sections instead of preserving historical guidance here
|
|
35
|
-
- avoid duplicating deeper docs when a canonical source already exists elsewhere
|
|
36
|
-
-->
|
|
37
|
-
|
|
38
|
-
# ${projectName} Agent Guide
|
|
39
|
-
|
|
40
|
-
## Operating hierarchy
|
|
41
|
-
|
|
42
|
-
- Use Beads (\`bd\`) as the canonical durable tracker. All non-trivial work must have a Beads issue before implementation begins.
|
|
43
|
-
- Use \`plan.md\` as the human-readable implementation plan tied to the active Beads issue set.
|
|
44
|
-
- Use cass-memory through the MCP \`memory\` server for cross-session and cross-tool recall, not for task tracking.
|
|
45
|
-
|
|
46
|
-
## Start-of-session rules
|
|
47
|
-
|
|
48
|
-
- Read \`AGENTS.md\`, \`plan.md\`, \`.cx/context.md\`, and any canonical docs that exist before planning or coding.
|
|
49
|
-
- Treat \`construct up\` as runtime hydration only. If services are unavailable, resume from durable state: \`plan.md\`, \`.cx/context.md\`, the latest \`.cx/handoffs/\` file, Beads, docs, and git.
|
|
50
|
-
- Run \`bd ready\` to see unblocked work available to claim.
|
|
51
|
-
- Run \`bd show <id>\` for the active issue to see its full context, dependencies, and notes.
|
|
52
|
-
- Check \`.cx/handoffs/\` for the most recent handoff — if another session was active, read the latest handoff to understand what was in progress.
|
|
53
|
-
- Carry tracker ids into plans, workflow entries, handoffs, and verification notes.
|
|
54
|
-
- Use the single-writer rule: if two agent or harness sessions would touch the same file, one session owns the edit and the other reviews, researches, or waits for handoff.
|
|
55
|
-
|
|
56
|
-
## Maintenance rules
|
|
57
|
-
|
|
58
|
-
- Update \`plan.md\`, \`.cx/context.md\`, and \`.cx/context.json\` when work changes project reality.
|
|
59
|
-
- Do not rely on dashboard chat history, running services, or an agent session as the source of truth. Persist recovery context in repo files, Beads, docs, and git before ending work.
|
|
60
|
-
- Prune stale sections from managed docs as soon as they stop matching the repo.
|
|
61
|
-
- Prefer updating an existing canonical file over creating a new top-level markdown file.
|
|
62
|
-
|
|
63
|
-
## End-of-session rules
|
|
64
|
-
|
|
65
|
-
- Before calling work done, update \`plan.md\` to reflect completed tasks: mark items \`done\`, add new phases or rows for work that was not previously tracked, and remove or correct rows that no longer match reality.
|
|
66
|
-
- Session-level todo lists (OpenCode, Claude Code, etc.) are ephemeral. Any task that represents a durable change to the project must be reflected in \`plan.md\` before the session closes — not just in the todo tracker.
|
|
67
|
-
- Write a handoff to \`.cx/handoffs/{date}-{slug}.md\` covering: current state, recent decisions, next actions, risks, do-not-touch files, and which Beads issue id(s) were in scope. Use \`bd note <id>\` to attach a summary to the tracker issue.
|
|
68
|
-
- Do not close a session with a working-tree change that has no corresponding \`plan.md\` entry or Beads issue.
|
|
69
|
-
- Check CI status after every push. Do not call work complete if CI is failing — either fix the failure or explicitly flag it as a known infrastructure issue (e.g. billing, secrets) that is outside the scope of the change.
|
|
70
|
-
- Wait for CI to reach a terminal state (success or failure) before declaring done. "Queued" or "in progress" is not done. Run \`gh run list --limit 3\` and confirm the relevant workflow shows \`completed success\`.
|
|
71
|
-
|
|
72
|
-
## Parallel agent coordination
|
|
73
|
-
|
|
74
|
-
- **Embedded dolt lock contention**: Parallel \`bd\` commands fail because beads uses an embedded dolt database with a single writer lock. Always serialize beads operations.
|
|
75
|
-
- **Use \`construct beads\`**: Instead of direct \`bd\` commands, use \`construct beads <command>\` for lock‑aware serialized execution.
|
|
76
|
-
- **Merge‑slot coordination**: For batch operations like \`bd dolt push\` that need exclusive database access, check \`construct‑6uo\` (Merge slot coordination bead) using \`bd merge-slot check/acquire/release\`.
|
|
77
|
-
- **Lock visibility**: Run \`construct beads status\` to see who holds the lock, pending queue, and stale lock cleanup.
|
|
78
|
-
- **Queueing**: If lock is held, requests are queued in \`.beads/queue.jsonl\` and processed FIFO.
|
|
79
|
-
- **Stale lock cleanup**: Run \`construct beads cleanup\` to remove locks from dead processes.
|
|
80
|
-
|
|
81
|
-
### New workflow for beads operations
|
|
82
|
-
|
|
83
|
-
1. **Check status**: \`construct beads status\` (or \`construct beads status --json\`)
|
|
84
|
-
2. **Run commands**: \`construct beads ready\`, \`construct beads show construct-xxx\`, \`construct beads update construct-xxx --claim\`
|
|
85
|
-
3. **Clean up**: \`construct beads cleanup\` if operations are stuck
|
|
86
|
-
4. **View queue**: \`construct beads queue\` to see pending requests
|
|
87
|
-
5. **Batch operations**: Before \`bd dolt push\`, check merge‑slot availability
|
|
88
|
-
|
|
89
|
-
## Documentation System
|
|
90
|
-
|
|
91
|
-
### README.md Maintenance
|
|
92
|
-
- Update README.md when architecture changes
|
|
93
|
-
- README.md must reflect current project capabilities
|
|
94
|
-
- Documentation updates belong in same PR as code changes
|
|
95
|
-
- README.md should link to AGENTS.md, plan.md, and docs/
|
|
96
|
-
|
|
97
|
-
### Documentation Quality Gates
|
|
98
|
-
- Run \`construct docs:verify\` before calling work complete
|
|
99
|
-
- CI will fail if critical docs are missing or stale
|
|
100
|
-
- Documentation drift must be addressed within one sprint
|
|
101
|
-
|
|
102
|
-
### Documentation Lifecycle
|
|
103
|
-
- Use \`construct init --docs-preset=product\` to establish documentation lanes
|
|
104
|
-
- ADRs for architectural decisions, RFCs for proposals, PRDs for products
|
|
105
|
-
- Documentation should be linked to beads issues in plan.md
|
|
106
|
-
|
|
107
|
-
### Update Mechanism
|
|
108
|
-
- Run \`construct init:update\` quarterly to adopt new standards
|
|
109
|
-
- Check \`.cx/context.json\` version matches current template
|
|
110
|
-
- Review and apply migration scripts when prompted
|
|
111
|
-
|
|
112
|
-
## CI Enforcement
|
|
113
|
-
|
|
114
|
-
### Documentation Validation
|
|
115
|
-
- \`construct docs:verify\` runs on every PR
|
|
116
|
-
- Missing README.md, AGENTS.md, or plan.md fails CI
|
|
117
|
-
- Stale plan.md (>7 days) or .cx/context.md (>7 days) fails CI
|
|
118
|
-
- README.md missing basic sections warns but doesn't fail
|
|
119
|
-
|
|
120
|
-
### Quality Gates
|
|
121
|
-
- All local tests must pass before pushing
|
|
122
|
-
- CI must be green after pushing
|
|
123
|
-
- Documentation validation must pass
|
|
124
|
-
- Comment policy must be enforced
|
|
125
|
-
|
|
126
|
-
## Verification rules
|
|
127
|
-
|
|
128
|
-
- Record verification evidence in the Beads issue (\`bd note <id>\`) or \`plan.md\`, or the relevant durable doc before calling work done.
|
|
129
|
-
- If architecture, contracts, or operating rules change, update the relevant canonical docs in the same change.
|
|
130
|
-
- All local tests must pass (\`npm test\`) before pushing. CI must be green after pushing before the task is considered done.
|
|
131
|
-
- Push Beads changes alongside code: \`bd dolt push\` must succeed before the session closes.
|
|
132
|
-
|
|
133
|
-
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:placeholder -->
|
|
134
|
-
## Beads Issue Tracker
|
|
135
|
-
|
|
136
|
-
This project uses **bd (beads)** for issue tracking. Run \`bd prime\` to see full workflow context and commands.
|
|
137
|
-
|
|
138
|
-
### Quick Reference
|
|
139
|
-
|
|
140
|
-
\`\`\`bash
|
|
141
|
-
bd ready # Find available work
|
|
142
|
-
bd show <id> # View issue details
|
|
143
|
-
bd update <id> --claim # Claim work
|
|
144
|
-
bd close <id> # Complete work
|
|
145
|
-
|
|
146
|
-
# New: lock‑aware execution
|
|
147
|
-
construct beads ready # Run bd ready with lock management
|
|
148
|
-
construct beads status # Show lock and queue status
|
|
149
|
-
construct beads cleanup # Remove stale locks
|
|
150
|
-
construct beads queue # Show pending requests
|
|
151
|
-
\`\`\`
|
|
152
|
-
|
|
153
|
-
### Rules
|
|
154
|
-
|
|
155
|
-
- Use \`bd\` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists
|
|
156
|
-
- **IMPORTANT**: For parallel agent safety, prefer \`construct beads <command>\` over direct \`bd\` calls
|
|
157
|
-
- Run \`bd prime\` for detailed command reference and session close protocol
|
|
158
|
-
- Use \`bd remember\` for persistent knowledge — do NOT use MEMORY.md files
|
|
159
|
-
- Acquire the merge slot (\`bd merge-slot acquire\`) before batch operations like \`bd dolt push\`
|
|
160
|
-
- **Beads hygiene is a project contract.** Read \`rules/common/beads-hygiene.md\` once per session — it defines when to claim, close, supersede, and prune beads. Stale tracker state is treated as broken work.
|
|
161
|
-
|
|
162
|
-
## Session Completion
|
|
163
|
-
|
|
164
|
-
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until \`git push\` succeeds.
|
|
165
|
-
|
|
166
|
-
**MANDATORY WORKFLOW:**
|
|
167
|
-
|
|
168
|
-
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
|
169
|
-
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
|
170
|
-
3. **Update issue status** - Close finished work, update in-progress items
|
|
171
|
-
4. **PUSH TO REMOTE** - This is MANDATORY:
|
|
172
|
-
\`\`\`bash
|
|
173
|
-
git pull --rebase
|
|
174
|
-
# Check merge‑slot before dolt push
|
|
175
|
-
construct beads dolt push
|
|
176
|
-
git push
|
|
177
|
-
git status # MUST show "up to date with origin"
|
|
178
|
-
\`\`\`
|
|
179
|
-
5. **Run end‑of‑session automation**:
|
|
180
|
-
\`\`\`bash
|
|
181
|
-
# Sync plan.md with bead statuses
|
|
182
|
-
node -e "import('./lib/beads-automation.mjs').then(m => m.syncPlanWithBeads({ cwd: process.cwd() }))"
|
|
183
|
-
|
|
184
|
-
# Create handoff
|
|
185
|
-
node -e "import('./lib/beads-automation.mjs').then(m => m.endOfSessionHook({ actor: process.env.USER, cwd: process.cwd() }))"
|
|
186
|
-
\`\`\`
|
|
187
|
-
6. **Clean up** - Clear stashes, prune remote branches
|
|
188
|
-
7. **Verify** - All changes committed AND pushed
|
|
189
|
-
8. **Hand off** - Provide context for next session
|
|
190
|
-
|
|
191
|
-
**CRITICAL RULES:**
|
|
192
|
-
- Work is NOT complete until \`git push\` succeeds
|
|
193
|
-
- NEVER stop before pushing - that leaves work stranded locally
|
|
194
|
-
- NEVER say "ready to push when you are" - YOU must push
|
|
195
|
-
- If push fails, resolve and retry until it succeeds
|
|
196
|
-
- ALWAYS use \`construct beads\` for beads operations in multi‑agent sessions
|
|
197
|
-
- Clean up stale locks with \`construct beads cleanup\` if operations hang
|
|
198
|
-
<!-- END BEADS INTEGRATION -->`;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
28
|
export function buildPlanTemplate() {
|
|
202
29
|
return `<!--
|
|
203
30
|
plan.md — current implementation plan for this repository.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/reconcile/adapter-prune.mjs — report and remove adapter directories for
|
|
3
|
+
* hosts not installed on this machine (ADR-0027 §1).
|
|
4
|
+
*
|
|
5
|
+
* `construct sync` writes adapter sets for the hosts it detects. A project
|
|
6
|
+
* synced where more hosts were installed, then opened on a leaner machine,
|
|
7
|
+
* carries adapter dirs for hosts absent from the current machine. Each adapter
|
|
8
|
+
* dir is sync-regenerated machine-specific state, so pruning the dirs for
|
|
9
|
+
* undetected hosts is safe — a later sync recreates whatever the current
|
|
10
|
+
* machine actually has.
|
|
11
|
+
*
|
|
12
|
+
* `.claude` stays untouched: it is the baseline set that VS Code reads
|
|
13
|
+
* natively, so its presence carries no implication about Claude Code. `.github`
|
|
14
|
+
* (Copilot prompts / instructions) is out of scope entirely.
|
|
15
|
+
*
|
|
16
|
+
* Safety: `ask`. Deleting generated directories runs only on explicit consent
|
|
17
|
+
* (`construct sync --reconcile=adapter-prune`), and the auto runner skips ask
|
|
18
|
+
* tasks. detect() reads only; apply() is idempotent because a removed dir drops
|
|
19
|
+
* out of the present set.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { ADAPTER_DIRS } from '../host-disposition.mjs';
|
|
26
|
+
import { detectHostCapabilities } from '../host-capabilities.mjs';
|
|
27
|
+
|
|
28
|
+
const HOST_TO_DIR = {
|
|
29
|
+
'Claude Code': '.claude',
|
|
30
|
+
OpenCode: '.opencode',
|
|
31
|
+
Codex: '.codex',
|
|
32
|
+
'VS Code': '.vscode',
|
|
33
|
+
Cursor: '.cursor',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const PROTECTED_DIRS = new Set(['.claude']);
|
|
37
|
+
|
|
38
|
+
function isConstructProject(dir) {
|
|
39
|
+
return fs.existsSync(path.join(dir, '.cx')) || fs.existsSync(path.join(dir, '.construct'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectedDirSet() {
|
|
43
|
+
const dirs = new Set();
|
|
44
|
+
for (const host of detectHostCapabilities()) {
|
|
45
|
+
if (host.availability !== 'installed') continue;
|
|
46
|
+
const dir = HOST_TO_DIR[host.host];
|
|
47
|
+
if (dir) dirs.add(dir);
|
|
48
|
+
}
|
|
49
|
+
return dirs;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A candidate is a present adapter dir whose host is not installed, excluding
|
|
53
|
+
// the protected baseline `.claude` that VS Code consumes natively.
|
|
54
|
+
|
|
55
|
+
function prunableDirs(projectDir) {
|
|
56
|
+
const detected = detectedDirSet();
|
|
57
|
+
const out = [];
|
|
58
|
+
for (const dir of ADAPTER_DIRS) {
|
|
59
|
+
if (PROTECTED_DIRS.has(dir)) continue;
|
|
60
|
+
if (detected.has(dir)) continue;
|
|
61
|
+
if (fs.existsSync(path.join(projectDir, dir))) out.push(dir);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function detect() {
|
|
67
|
+
const dir = process.cwd();
|
|
68
|
+
if (!isConstructProject(dir)) {
|
|
69
|
+
return { needsRepair: false, summary: 'Not a Construct project directory.' };
|
|
70
|
+
}
|
|
71
|
+
const prunable = prunableDirs(dir);
|
|
72
|
+
if (prunable.length === 0) {
|
|
73
|
+
return { needsRepair: false, summary: 'No adapter directories for uninstalled hosts.' };
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
needsRepair: true,
|
|
77
|
+
summary: `${prunable.length} adapter director${prunable.length === 1 ? 'y' : 'ies'} for uninstalled hosts: ${prunable.join(', ')}.`,
|
|
78
|
+
details: { prunable },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function apply() {
|
|
83
|
+
const dir = process.cwd();
|
|
84
|
+
const prunable = prunableDirs(dir);
|
|
85
|
+
const removed = [];
|
|
86
|
+
for (const adapter of prunable) {
|
|
87
|
+
const full = path.join(dir, adapter);
|
|
88
|
+
try {
|
|
89
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
90
|
+
removed.push(adapter);
|
|
91
|
+
} catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (removed.length === 0) return { summary: 'Nothing to prune.' };
|
|
96
|
+
return { summary: `Removed ${removed.length} adapter director${removed.length === 1 ? 'y' : 'ies'} for uninstalled hosts: ${removed.join(', ')}.` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default {
|
|
100
|
+
id: 'adapter-prune',
|
|
101
|
+
description: 'Remove adapter directories for hosts not installed on this machine (never .claude or .github).',
|
|
102
|
+
safety: 'ask',
|
|
103
|
+
detect,
|
|
104
|
+
apply,
|
|
105
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/reconcile/agent-instructions-rewrap.mjs — keep the CONSTRUCT INTEGRATION
|
|
3
|
+
* block current inside existing AGENTS.md / CLAUDE.md files (ADR-0027 §2).
|
|
4
|
+
*
|
|
5
|
+
* The integration block is versioned and hash-stamped. A changed managed body
|
|
6
|
+
* or version leaves files written by an earlier init carrying a stale block.
|
|
7
|
+
* Re-injection rewrites the current block into files that already contain one,
|
|
8
|
+
* replacing only the marker region and preserving every byte outside it.
|
|
9
|
+
*
|
|
10
|
+
* Scope is deliberately narrow: a file is touched only when it EXISTS in cwd
|
|
11
|
+
* and already carries a CONSTRUCT INTEGRATION marker. Files without a block are
|
|
12
|
+
* left for init to create — backward-repair never authors a block into a file
|
|
13
|
+
* the user never opted into. Safety: `auto`. detect() reads only; apply() is
|
|
14
|
+
* idempotent because injectIntoAgentFile returns `unchanged` once the stamp
|
|
15
|
+
* matches.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
injectIntoAgentFile,
|
|
23
|
+
injectConstructBlock,
|
|
24
|
+
buildConstructIntegrationBody,
|
|
25
|
+
CONSTRUCT_INTEGRATION_VERSION,
|
|
26
|
+
} from '../agent-instructions/inject.mjs';
|
|
27
|
+
|
|
28
|
+
const AGENT_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
29
|
+
const BEGIN_MARKER = '<!-- BEGIN CONSTRUCT INTEGRATION';
|
|
30
|
+
const BEADS_BLOCK_RE = /<!-- BEGIN BEADS INTEGRATION/;
|
|
31
|
+
|
|
32
|
+
function hasIntegrationBlock(content) {
|
|
33
|
+
return content.includes(BEGIN_MARKER);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Re-injection that would change the file maps to a stale block. The body
|
|
37
|
+
// dedups against a sibling Beads block, so the comparison rebuilds the body
|
|
38
|
+
// the same way injectIntoAgentFile does before hashing.
|
|
39
|
+
|
|
40
|
+
function wouldChange(content) {
|
|
41
|
+
const body = buildConstructIntegrationBody({ hasBeadsBlock: BEADS_BLOCK_RE.test(content) });
|
|
42
|
+
const { action } = injectConstructBlock(content, body, CONSTRUCT_INTEGRATION_VERSION);
|
|
43
|
+
return action !== 'unchanged';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function candidateFiles() {
|
|
47
|
+
const dir = process.cwd();
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const name of AGENT_FILES) {
|
|
50
|
+
const full = path.join(dir, name);
|
|
51
|
+
if (!fs.existsSync(full)) continue;
|
|
52
|
+
let content = '';
|
|
53
|
+
try {
|
|
54
|
+
content = fs.readFileSync(full, 'utf8');
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!hasIntegrationBlock(content)) continue;
|
|
59
|
+
out.push({ name, full, content });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function detect() {
|
|
65
|
+
const candidates = candidateFiles();
|
|
66
|
+
if (candidates.length === 0) {
|
|
67
|
+
return { needsRepair: false, summary: 'No agent file carries a CONSTRUCT INTEGRATION block.' };
|
|
68
|
+
}
|
|
69
|
+
const stale = candidates.filter((c) => wouldChange(c.content)).map((c) => c.name);
|
|
70
|
+
if (stale.length === 0) {
|
|
71
|
+
return { needsRepair: false, summary: 'CONSTRUCT INTEGRATION blocks are current.' };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
needsRepair: true,
|
|
75
|
+
summary: `Stale CONSTRUCT INTEGRATION block in ${stale.join(', ')}.`,
|
|
76
|
+
details: { stale },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function apply() {
|
|
81
|
+
const candidates = candidateFiles();
|
|
82
|
+
const rewrapped = [];
|
|
83
|
+
for (const c of candidates) {
|
|
84
|
+
if (!wouldChange(c.content)) continue;
|
|
85
|
+
const result = injectIntoAgentFile(c.full, { version: CONSTRUCT_INTEGRATION_VERSION });
|
|
86
|
+
if (result.changed) rewrapped.push(c.name);
|
|
87
|
+
}
|
|
88
|
+
if (rewrapped.length === 0) return { summary: 'Already current.' };
|
|
89
|
+
return { summary: `Rewrapped CONSTRUCT INTEGRATION block in ${rewrapped.join(', ')}.` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export default {
|
|
93
|
+
id: 'agent-instructions-rewrap',
|
|
94
|
+
description: 'Refresh stale CONSTRUCT INTEGRATION blocks in existing AGENTS.md / CLAUDE.md files.',
|
|
95
|
+
safety: 'auto',
|
|
96
|
+
detect,
|
|
97
|
+
apply,
|
|
98
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/reconcile/gitignore-coverage.mjs — repair a project `.gitignore` that
|
|
3
|
+
* predates the full Construct ignore set (ADR-0027 §1).
|
|
4
|
+
*
|
|
5
|
+
* The init writer seeds every machine-specific, sync-regenerated artifact into
|
|
6
|
+
* `.gitignore` so adapter dirs, the launcher, and runtime state never enter
|
|
7
|
+
* source control. A project initialized before a pattern was added carries a
|
|
8
|
+
* partial set; this task appends the missing patterns under the same comment
|
|
9
|
+
* header the init writer uses, leaving all user-authored lines untouched.
|
|
10
|
+
*
|
|
11
|
+
* Scope: a Construct project directory (cwd has `.cx/` or `.construct/`) that
|
|
12
|
+
* already has a `.gitignore`. Absent either, there is nothing to repair — init
|
|
13
|
+
* owns creation. Safety: `auto`. detect() reads only; apply() appends and is
|
|
14
|
+
* idempotent because missingIgnorePatterns() drains to empty once the patterns
|
|
15
|
+
* are present.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { missingIgnorePatterns, isConstructPackageRepo } from '../host-disposition.mjs';
|
|
22
|
+
|
|
23
|
+
const HEADER = '# Construct — generated adapters, launcher, and runtime state.';
|
|
24
|
+
const SUBHEADER = '# Machine-specific, recreated by `construct sync`; never source (ADR-0027).';
|
|
25
|
+
|
|
26
|
+
function isConstructProject(dir) {
|
|
27
|
+
return fs.existsSync(path.join(dir, '.cx')) || fs.existsSync(path.join(dir, '.construct'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function gitignorePath(dir) {
|
|
31
|
+
return path.join(dir, '.gitignore');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function detect() {
|
|
35
|
+
const dir = process.cwd();
|
|
36
|
+
if (!isConstructProject(dir)) {
|
|
37
|
+
return { needsRepair: false, summary: 'Not a Construct project directory.' };
|
|
38
|
+
}
|
|
39
|
+
if (isConstructPackageRepo(dir)) {
|
|
40
|
+
return { needsRepair: false, summary: 'Construct package repo — .construct/ holds shipped templates, not a staged launcher.' };
|
|
41
|
+
}
|
|
42
|
+
const giPath = gitignorePath(dir);
|
|
43
|
+
if (!fs.existsSync(giPath)) {
|
|
44
|
+
return { needsRepair: false, summary: 'No .gitignore present (init owns creation).' };
|
|
45
|
+
}
|
|
46
|
+
let content = '';
|
|
47
|
+
try {
|
|
48
|
+
content = fs.readFileSync(giPath, 'utf8');
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return { needsRepair: false, summary: `Could not read .gitignore: ${err.message}` };
|
|
51
|
+
}
|
|
52
|
+
const missing = missingIgnorePatterns(content);
|
|
53
|
+
if (missing.length === 0) {
|
|
54
|
+
return { needsRepair: false, summary: '.gitignore covers the full Construct ignore set.' };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
needsRepair: true,
|
|
58
|
+
summary: `${missing.length} Construct ignore pattern${missing.length === 1 ? '' : 's'} missing from .gitignore.`,
|
|
59
|
+
details: { missing },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function apply() {
|
|
64
|
+
const dir = process.cwd();
|
|
65
|
+
const giPath = gitignorePath(dir);
|
|
66
|
+
if (!fs.existsSync(giPath)) return { summary: 'No .gitignore to repair.' };
|
|
67
|
+
const existing = fs.readFileSync(giPath, 'utf8');
|
|
68
|
+
const missing = missingIgnorePatterns(existing);
|
|
69
|
+
if (missing.length === 0) return { summary: 'Already covered.' };
|
|
70
|
+
|
|
71
|
+
// Mirror the init writer's separator and header so forward-fix and
|
|
72
|
+
// backward-repair produce byte-identical blocks.
|
|
73
|
+
|
|
74
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
75
|
+
const block = `${prefix}\n${HEADER}\n${SUBHEADER}\n${missing.join('\n')}\n`;
|
|
76
|
+
fs.writeFileSync(giPath, existing + block, 'utf8');
|
|
77
|
+
return {
|
|
78
|
+
summary: `Appended ${missing.length} Construct ignore pattern${missing.length === 1 ? '' : 's'} to .gitignore.`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default {
|
|
83
|
+
id: 'gitignore-coverage',
|
|
84
|
+
description: 'Append missing Construct ignore patterns to a project .gitignore that predates the full set.',
|
|
85
|
+
safety: 'auto',
|
|
86
|
+
detect,
|
|
87
|
+
apply,
|
|
88
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/reconcile/index.mjs — registry of state-reconciliation tasks that
|
|
3
|
+
* bring on-disk state in line with the expected disposition contract
|
|
4
|
+
* (ADR-0027). Separate from lib/migrations/ (artifact-JSON schema
|
|
5
|
+
* versioning) and lib/storage/migrations.mjs (Postgres schema).
|
|
6
|
+
*
|
|
7
|
+
* Each task declares: id (kebab), description (one line), safety
|
|
8
|
+
* (`auto` = run silently from `construct sync` when detect() reports
|
|
9
|
+
* needsRepair; `ask` = surfaced by `construct doctor` and runs only on
|
|
10
|
+
* explicit `construct sync --reconcile=<id>`), detect() (async, cheap,
|
|
11
|
+
* side-effect-free, returns { needsRepair, summary, ... }), and apply()
|
|
12
|
+
* (async, idempotent, returns { summary }).
|
|
13
|
+
*
|
|
14
|
+
* Two invariants every task must hold:
|
|
15
|
+
* 1. Never touch a user-authored file without consent — `safety: 'ask'`
|
|
16
|
+
* gates anything that could remove user edits, and detect() must
|
|
17
|
+
* exclude paths whose mtime suggests user interaction.
|
|
18
|
+
* 2. Calling detect() after apply() must return needsRepair: false on
|
|
19
|
+
* the same input. A task that would loop is a detection bug — the
|
|
20
|
+
* stamp file is a hint and a record, not a correctness guard.
|
|
21
|
+
*
|
|
22
|
+
* Stamps land at ~/.construct/reconcile.json so the dashboard and doctor
|
|
23
|
+
* can show what's been applied; loss of the file does not break apply()
|
|
24
|
+
* (apply() stays idempotent on its own).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
|
|
30
|
+
import { constructDir } from '../paths.mjs';
|
|
31
|
+
import legacySkillsCleanup from './legacy-skills-cleanup.mjs';
|
|
32
|
+
import gitignoreCoverage from './gitignore-coverage.mjs';
|
|
33
|
+
import agentInstructionsRewrap from './agent-instructions-rewrap.mjs';
|
|
34
|
+
import legacyDoctrineStrip from './legacy-doctrine-strip.mjs';
|
|
35
|
+
import legacyGuideDecommit from './legacy-guide-decommit.mjs';
|
|
36
|
+
import mcpEntryReconcile from './mcp-entry-reconcile.mjs';
|
|
37
|
+
import adapterPrune from './adapter-prune.mjs';
|
|
38
|
+
import postgresNamespace from './postgres-namespace.mjs';
|
|
39
|
+
|
|
40
|
+
export const RECONCILIATIONS = [
|
|
41
|
+
legacySkillsCleanup,
|
|
42
|
+
gitignoreCoverage,
|
|
43
|
+
agentInstructionsRewrap,
|
|
44
|
+
legacyDoctrineStrip,
|
|
45
|
+
legacyGuideDecommit,
|
|
46
|
+
mcpEntryReconcile,
|
|
47
|
+
adapterPrune,
|
|
48
|
+
postgresNamespace,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const STAMP_FILE = 'reconcile.json';
|
|
52
|
+
|
|
53
|
+
function stampPath() {
|
|
54
|
+
return path.join(constructDir(), STAMP_FILE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readStamps() {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(stampPath(), 'utf8');
|
|
60
|
+
const parsed = JSON.parse(raw);
|
|
61
|
+
return parsed && typeof parsed === 'object' && parsed.applied
|
|
62
|
+
? parsed
|
|
63
|
+
: { applied: {} };
|
|
64
|
+
} catch {
|
|
65
|
+
return { applied: {} };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeStamp(id, summary) {
|
|
70
|
+
const stamps = readStamps();
|
|
71
|
+
stamps.applied[id] = { at: new Date().toISOString(), summary };
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(path.dirname(stampPath()), { recursive: true });
|
|
74
|
+
fs.writeFileSync(stampPath(), JSON.stringify(stamps, null, 2));
|
|
75
|
+
} catch {
|
|
76
|
+
// Stamp failure is non-fatal — apply() must be idempotent so a missing
|
|
77
|
+
// stamp just means the next detect() catches the same state again.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function safeDetect(task) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await task.detect();
|
|
84
|
+
return result && typeof result === 'object'
|
|
85
|
+
? { needsRepair: !!result.needsRepair, summary: result.summary || null, details: result.details || null }
|
|
86
|
+
: { needsRepair: false, summary: null, details: null };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return { needsRepair: false, summary: null, details: null, error: err.message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function listReconciliations() {
|
|
93
|
+
const stamps = readStamps();
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const task of RECONCILIATIONS) {
|
|
96
|
+
const detected = await safeDetect(task);
|
|
97
|
+
out.push({
|
|
98
|
+
id: task.id,
|
|
99
|
+
description: task.description,
|
|
100
|
+
safety: task.safety,
|
|
101
|
+
needsRepair: detected.needsRepair,
|
|
102
|
+
summary: detected.summary,
|
|
103
|
+
details: detected.details,
|
|
104
|
+
error: detected.error || null,
|
|
105
|
+
previouslyApplied: !!stamps.applied[task.id],
|
|
106
|
+
appliedAt: stamps.applied[task.id]?.at || null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function runAutoReconciliations({ dryRun = false, verbose = false, logger = console } = {}) {
|
|
113
|
+
const applied = [];
|
|
114
|
+
const skipped = [];
|
|
115
|
+
for (const task of RECONCILIATIONS) {
|
|
116
|
+
if (task.safety !== 'auto') {
|
|
117
|
+
skipped.push({ id: task.id, reason: 'safety:ask' });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const detected = await safeDetect(task);
|
|
121
|
+
if (detected.error) {
|
|
122
|
+
skipped.push({ id: task.id, reason: `detect-error:${detected.error}` });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!detected.needsRepair) {
|
|
126
|
+
skipped.push({ id: task.id, reason: 'no-op' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
applied.push({ id: task.id, summary: detected.summary, dryRun: true });
|
|
131
|
+
if (verbose && logger?.log) logger.log(`[reconcile:${task.id}] would apply: ${detected.summary}`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const result = await task.apply();
|
|
136
|
+
const summary = result?.summary || detected.summary || '';
|
|
137
|
+
writeStamp(task.id, summary);
|
|
138
|
+
applied.push({ id: task.id, summary });
|
|
139
|
+
if (verbose && logger?.log) logger.log(`[reconcile:${task.id}] ${summary}`);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
skipped.push({ id: task.id, reason: `apply-error:${err.message}` });
|
|
142
|
+
if (verbose && logger?.error) logger.error(`[reconcile:${task.id}] failed: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { applied, skipped };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function runReconciliation(id, { dryRun = false } = {}) {
|
|
149
|
+
const task = RECONCILIATIONS.find((x) => x.id === id);
|
|
150
|
+
if (!task) {
|
|
151
|
+
return { ok: false, reason: `Unknown reconciliation: ${id}`, available: RECONCILIATIONS.map((x) => x.id) };
|
|
152
|
+
}
|
|
153
|
+
const detected = await safeDetect(task);
|
|
154
|
+
if (detected.error) return { ok: false, reason: `detect-error:${detected.error}` };
|
|
155
|
+
if (!detected.needsRepair) {
|
|
156
|
+
return { ok: true, ran: false, reason: 'no-op', summary: detected.summary || 'Nothing to reconcile.' };
|
|
157
|
+
}
|
|
158
|
+
if (dryRun) return { ok: true, ran: false, dryRun: true, summary: detected.summary };
|
|
159
|
+
try {
|
|
160
|
+
const result = await task.apply();
|
|
161
|
+
const summary = result?.summary || detected.summary || '';
|
|
162
|
+
writeStamp(id, summary);
|
|
163
|
+
return { ok: true, ran: true, summary };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
return { ok: false, ran: false, reason: `apply-error:${err.message}` };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function reconcileStampPath() {
|
|
170
|
+
return stampPath();
|
|
171
|
+
}
|