@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
package/lib/init-unified.mjs
CHANGED
|
@@ -14,7 +14,6 @@ import path from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
buildAgentsGuide,
|
|
18
17
|
buildContextJson,
|
|
19
18
|
buildContextMarkdown,
|
|
20
19
|
buildPlanTemplate,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
import { multiSelect } from './tty-prompts.mjs';
|
|
24
23
|
import { execSync, spawnSync } from 'node:child_process';
|
|
25
24
|
import { stageProjectAdapters } from './install/stage-project.mjs';
|
|
25
|
+
import { missingIgnorePatterns, isConstructPackageRepo } from './host-disposition.mjs';
|
|
26
26
|
|
|
27
27
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
28
28
|
const ROOT_DIR = path.join(__dirname, "..");
|
|
@@ -53,6 +53,20 @@ const skipInteractive = !interactive;
|
|
|
53
53
|
// templates/ already exist (issue #97).
|
|
54
54
|
const forceScaffold = args.includes("--force");
|
|
55
55
|
|
|
56
|
+
// `bd init` is git-native and unconditionally commits its bootstrap; ADR-0027
|
|
57
|
+
// §3 requires init to leave the host repo's commit history untouched unless the
|
|
58
|
+
// user opts in. --commit-bootstrap keeps that commit; the default withholds it.
|
|
59
|
+
const commitBootstrap = args.includes("--commit-bootstrap");
|
|
60
|
+
|
|
61
|
+
// Adapter selection (construct-4xy6 / ADR-0027 §1). By default init writes
|
|
62
|
+
// adapters only for hosts detected on the machine; --with-<host> force-includes
|
|
63
|
+
// one, --all-hosts writes every adapter set. Copilot (`.github/`) is opt-in only
|
|
64
|
+
// — never written by detection — so init never touches a repo's CI directory
|
|
65
|
+
// without --with-copilot.
|
|
66
|
+
const HOST_FLAG_KEYS = ["claude", "codex", "opencode", "vscode", "cursor", "copilot"];
|
|
67
|
+
const withHostFlags = new Set(HOST_FLAG_KEYS.filter((k) => args.includes(`--with-${k}`)));
|
|
68
|
+
const allHosts = args.includes("--all-hosts");
|
|
69
|
+
|
|
56
70
|
// Active profile selector. `--profile=<id>` writes the field into the
|
|
57
71
|
// project's construct.config.json so resolveActiveProfile picks it up
|
|
58
72
|
// on first run. Unknown ids are rejected with the available catalog.
|
|
@@ -322,61 +336,35 @@ function normalizeAnswer(value) {
|
|
|
322
336
|
|
|
323
337
|
|
|
324
338
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
This directory contains the structured documentation system for ${projectName}.
|
|
329
|
-
|
|
330
|
-
## Lanes
|
|
331
|
-
|
|
332
|
-
Each subdirectory represents a documentation lane with a specific purpose:
|
|
333
|
-
|
|
334
|
-
- **adr/** – Architecture decision records
|
|
335
|
-
- **briefs/** – Research, evidence, and one-pager documents
|
|
336
|
-
- **changelogs/** – User-facing release notes
|
|
337
|
-
- **intake/** – Raw source material awaiting processing
|
|
338
|
-
- **memos/** – Decision memos and internal arguments
|
|
339
|
-
- **meetings/** – Meeting notes and minutes
|
|
340
|
-
- **notes/** – Working notes and lightweight context
|
|
341
|
-
- **onboarding/** – Setup guides and first-day workflows
|
|
342
|
-
- **postmortems/** – Blameless incident reports
|
|
343
|
-
- **prds/** – Product and capability requirement documents
|
|
344
|
-
- **rfcs/** – Architecture and implementation proposals
|
|
345
|
-
- **runbooks/** – Operational procedures and diagnostics
|
|
346
|
-
|
|
347
|
-
## Usage
|
|
348
|
-
|
|
349
|
-
### Adding Documents
|
|
350
|
-
|
|
351
|
-
Place new documents in the appropriate lane directory. Each lane has templates in its \`templates/\` subdirectory.
|
|
339
|
+
// Index only the lanes actually scaffolded — advertising lanes that have no
|
|
340
|
+
// directory sends readers to dead links. Project-framed: this is the team's doc
|
|
341
|
+
// surface, not Construct's tooling, so the body carries no `construct` commands.
|
|
352
342
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
343
|
+
function buildDocsReadme(projectName, selectedLanes = []) {
|
|
344
|
+
const laneLines = [...selectedLanes]
|
|
345
|
+
.sort()
|
|
346
|
+
.map((lane) => {
|
|
347
|
+
const meta = DOC_LANES[lane];
|
|
348
|
+
const label = meta?.title ?? lane;
|
|
349
|
+
const laneDir = meta?.dir ?? lane;
|
|
350
|
+
const desc = meta?.description ?? "Custom documentation lane.";
|
|
351
|
+
return `- [${label}](./${laneDir}/) — ${desc}`;
|
|
352
|
+
});
|
|
361
353
|
|
|
362
|
-
|
|
354
|
+
return `# ${projectName} Documentation
|
|
363
355
|
|
|
364
|
-
|
|
365
|
-
# Validate all documentation
|
|
366
|
-
construct docs:verify
|
|
356
|
+
> The canonical home for this project's long-lived documents — decision records, briefs, notes, and runbooks.
|
|
367
357
|
|
|
368
|
-
|
|
369
|
-
construct docs:verify --quick
|
|
358
|
+
## Operating model
|
|
370
359
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
360
|
+
- Keep each document focused on one lane's purpose; start from the per-lane \`templates/\`.
|
|
361
|
+
- Link durable work to the project's tracker and \`plan.md\`, and update related documents in the same change.
|
|
362
|
+
- Prune a lane when it stops serving a real purpose rather than letting it collect stale templates.
|
|
374
363
|
|
|
375
|
-
##
|
|
364
|
+
## Lanes
|
|
376
365
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
- Update README.md when architecture changes`;
|
|
366
|
+
${laneLines.join("\n")}
|
|
367
|
+
`;
|
|
380
368
|
}
|
|
381
369
|
|
|
382
370
|
function buildLaneReadme(laneKey) {
|
|
@@ -698,52 +686,28 @@ async function askIntakeCollection(targetPath, skipInteractive) {
|
|
|
698
686
|
return { parentDirs: selected, maxDepth: 4 };
|
|
699
687
|
}
|
|
700
688
|
|
|
689
|
+
// A greenfield README belongs to the project, not to Construct: a neutral
|
|
690
|
+
// skeleton the owner fills in. Construct's presence is one delineated pointer to
|
|
691
|
+
// AGENTS.md, never project-identity content or a tooling command reference —
|
|
692
|
+
// matching how a native agent stays out of the host's README (ADR-0027 §2).
|
|
693
|
+
|
|
701
694
|
function buildProjectReadme(projectName) {
|
|
702
695
|
return `# ${projectName}
|
|
703
696
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
This project uses [Construct](https://github.com/geraldmaron/construct) for agentic software development.
|
|
707
|
-
|
|
708
|
-
## Usage
|
|
709
|
-
|
|
710
|
-
From inside OpenCode, Claude Code, or similar agent surfaces:
|
|
711
|
-
|
|
712
|
-
\`\`\`text
|
|
713
|
-
@construct build the feature and ship it when it's verified
|
|
714
|
-
@construct fix the bug
|
|
715
|
-
@construct review the changes before release
|
|
716
|
-
\`\`\`
|
|
697
|
+
> Briefly describe what this project does and who it is for.
|
|
717
698
|
|
|
718
|
-
|
|
699
|
+
## Getting started
|
|
719
700
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
- \`AGENTS.md\` — Operating contract for AI agents
|
|
723
|
-
- \`plan.md\` — Current implementation plan
|
|
724
|
-
- \`.cx/context.md\` — Session context and handoff state
|
|
725
|
-
- \`docs/\` — Documentation system (if initialized with \`construct init --docs-preset\`)
|
|
701
|
+
Document how to install dependencies and run the project.
|
|
726
702
|
|
|
727
703
|
## Development
|
|
728
704
|
|
|
729
|
-
|
|
730
|
-
# Initialize project with Construct
|
|
731
|
-
construct init --docs-preset=lean
|
|
732
|
-
|
|
733
|
-
# Check documentation quality
|
|
734
|
-
construct docs:verify
|
|
705
|
+
Document how to build, test, and contribute.
|
|
735
706
|
|
|
736
|
-
|
|
737
|
-
npm test
|
|
738
|
-
\`\`\`
|
|
707
|
+
---
|
|
739
708
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
This project enforces documentation quality:
|
|
743
|
-
- README.md must exist and be current
|
|
744
|
-
- AGENTS.md must have required sections
|
|
745
|
-
- plan.md must be updated weekly
|
|
746
|
-
- .cx/context.md must track active work`;
|
|
709
|
+
<sub>Agent workflows for this repository are configured in [\`AGENTS.md\`](AGENTS.md), managed with [Construct](https://github.com/geraldmaron/construct).</sub>
|
|
710
|
+
`;
|
|
747
711
|
}
|
|
748
712
|
|
|
749
713
|
function preflight(target) {
|
|
@@ -776,11 +740,45 @@ function preflight(target) {
|
|
|
776
740
|
return { clean };
|
|
777
741
|
}
|
|
778
742
|
|
|
779
|
-
function
|
|
743
|
+
function gitHeadSha(cwd) {
|
|
744
|
+
const r = spawnSync("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
745
|
+
return r.status === 0 ? r.stdout.trim() : null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Restore the host repo's commit pointer to where init found it, leaving every
|
|
749
|
+
// bd-written file in the working tree (uncommitted, unstaged) for the user to
|
|
750
|
+
// commit on their own terms. When the bootstrap commit was the repo's first,
|
|
751
|
+
// drop the branch ref so the repo returns to "no commits yet". bd's issue data
|
|
752
|
+
// lives in Dolt, not this git commit, so the tracker stays fully functional.
|
|
753
|
+
|
|
754
|
+
function undoBootstrapCommit(cwd, headBefore) {
|
|
755
|
+
const headAfter = gitHeadSha(cwd);
|
|
756
|
+
if (!headAfter || headAfter === headBefore) return false;
|
|
757
|
+
if (headBefore) {
|
|
758
|
+
spawnSync("git", ["reset", "--mixed", headBefore], { cwd, stdio: "ignore" });
|
|
759
|
+
} else {
|
|
760
|
+
spawnSync("git", ["update-ref", "-d", "HEAD"], { cwd, stdio: "ignore" });
|
|
761
|
+
spawnSync("git", ["reset"], { cwd, stdio: "ignore" });
|
|
762
|
+
}
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function initializeBeadsTracker(target, { commitBootstrap = false } = {}) {
|
|
767
|
+
// bd init prints verbose, host-specific output (a doubled "Claude Code
|
|
768
|
+
// integration installed", a "Restart Claude Code" line that ignores the other
|
|
769
|
+
// synced hosts) that interleaves with Construct's own flow. Capture it and let
|
|
770
|
+
// the caller print one clean summary; the captured prefix names the issue ids.
|
|
771
|
+
|
|
772
|
+
// bd init is git-native and unconditionally commits its bootstrap (.beads/*,
|
|
773
|
+
// .gitignore, agent files) with no flag to suppress it. Capture HEAD first so
|
|
774
|
+
// the commit can be withheld unless --commit-bootstrap (ADR-0027 §3).
|
|
775
|
+
|
|
776
|
+
const headBefore = gitHeadSha(target);
|
|
777
|
+
|
|
780
778
|
const result = spawnSync("bd", ["init"], {
|
|
781
779
|
cwd: target,
|
|
782
780
|
encoding: "utf8",
|
|
783
|
-
stdio: "
|
|
781
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
784
782
|
killSignal: "SIGTERM",
|
|
785
783
|
timeout: 30_000,
|
|
786
784
|
});
|
|
@@ -792,8 +790,42 @@ function initializeBeadsTracker(target) {
|
|
|
792
790
|
throw result.error;
|
|
793
791
|
}
|
|
794
792
|
if (result.status !== 0) {
|
|
795
|
-
|
|
793
|
+
const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-2).join(" ");
|
|
794
|
+
throw new Error(`bd init exited with code ${result.status ?? "unknown"}${tail ? `: ${tail}` : ""}`);
|
|
796
795
|
}
|
|
796
|
+
const withheldCommit = commitBootstrap ? false : undoBootstrapCommit(target, headBefore);
|
|
797
|
+
const prefixMatch = (result.stdout || "").match(/Issue prefix:\s*(\S+)/);
|
|
798
|
+
return { prefix: prefixMatch ? prefixMatch[1] : null, withheldCommit };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Resolve the host adapter sets init should write (construct-4xy6). Returns null
|
|
802
|
+
// for "all hosts" (--all-hosts), otherwise the union of detected hosts and
|
|
803
|
+
// --with-<host> flags. Copilot is excluded from detection so `.github/` is only
|
|
804
|
+
// written on explicit --with-copilot; selecting VS Code pulls in claude because
|
|
805
|
+
// VS Code reads `.claude/agents` natively; an empty result falls back to claude.
|
|
806
|
+
|
|
807
|
+
async function resolveAdapterHosts() {
|
|
808
|
+
if (allHosts) return null;
|
|
809
|
+
const detected = new Set();
|
|
810
|
+
try {
|
|
811
|
+
const { detectHostCapabilities } = await import('./host-capabilities.mjs');
|
|
812
|
+
const nameToKey = {
|
|
813
|
+
'Claude Code': 'claude',
|
|
814
|
+
'OpenCode': 'opencode',
|
|
815
|
+
'Codex': 'codex',
|
|
816
|
+
'VS Code': 'vscode',
|
|
817
|
+
'Cursor': 'cursor',
|
|
818
|
+
'Copilot': 'copilot',
|
|
819
|
+
};
|
|
820
|
+
for (const cap of detectHostCapabilities()) {
|
|
821
|
+
if (cap.availability === 'installed' && nameToKey[cap.host]) detected.add(nameToKey[cap.host]);
|
|
822
|
+
}
|
|
823
|
+
} catch { /* detection is advisory; fall back to flags + baseline */ }
|
|
824
|
+
detected.delete('copilot');
|
|
825
|
+
const selected = new Set([...detected, ...withHostFlags]);
|
|
826
|
+
if (selected.has('vscode')) selected.add('claude');
|
|
827
|
+
if (selected.size === 0) selected.add('claude');
|
|
828
|
+
return HOST_FLAG_KEYS.filter((k) => selected.has(k));
|
|
797
829
|
}
|
|
798
830
|
|
|
799
831
|
async function main() {
|
|
@@ -813,27 +845,49 @@ async function main() {
|
|
|
813
845
|
console.log(`Initializing Construct in ${path.relative(process.cwd(), target) || "."}`);
|
|
814
846
|
}
|
|
815
847
|
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
848
|
+
// Scaffold the central project config so the configuration surface is
|
|
849
|
+
// discoverable (ADR-0027 §1 / construct-e13x). Organized by design —
|
|
850
|
+
// surfaces all major configurable blocks (deployment, telemetry,
|
|
851
|
+
// orchestration, etc.) so the user can discover and edit options easily.
|
|
852
|
+
// A fresh file is written on every init; skip-if-exists preserves user edits.
|
|
853
|
+
|
|
854
|
+
{
|
|
824
855
|
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('./config/project-config.mjs');
|
|
856
|
+
const { DEFAULT_PROJECT_CONFIG } = await import('./config/schema.mjs');
|
|
857
|
+
|
|
858
|
+
if (profileId) {
|
|
859
|
+
const { loadProfile, listProfiles } = await import('./profiles/loader.mjs');
|
|
860
|
+
if (!loadProfile(profileId)) {
|
|
861
|
+
console.error(`Unknown profile: ${profileId}. Available: ${listProfiles().join(', ')}`);
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
825
865
|
const found = findProjectConfigPath(target);
|
|
826
866
|
const cfgPath = found || path.join(target, PROJECT_CONFIG_FILENAME);
|
|
827
867
|
|
|
828
|
-
// loadProjectConfig returns
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
868
|
+
// loadProjectConfig returns { path, raw, config, source }. Persist .raw (the
|
|
869
|
+
// on-disk JSON) so user customizations survive and defaults are not silently
|
|
870
|
+
// materialized into the file.
|
|
871
|
+
|
|
872
|
+
if (!found) {
|
|
873
|
+
const cfg = {
|
|
874
|
+
$schema: "./node_modules/@geraldmaron/construct/schemas/project-config.schema.json",
|
|
875
|
+
...DEFAULT_PROJECT_CONFIG,
|
|
876
|
+
profile: profileId || 'rnd',
|
|
877
|
+
};
|
|
878
|
+
// Explicitly write the full default config object so all options are
|
|
879
|
+
// visible and discoverable to the user in the file. The $schema property
|
|
880
|
+
// provides inline documentation and validation in supported editors.
|
|
881
|
+
writeProjectConfig(cfgPath, cfg, { validate: true, silent: true });
|
|
882
|
+
created.push(PROJECT_CONFIG_FILENAME);
|
|
883
|
+
if (!quiet && profileId) console.log(`Profile set to ${profileId}.`);
|
|
884
|
+
} else if (profileId) {
|
|
885
|
+
const loaded = loadProjectConfig(target);
|
|
886
|
+
const cfg = loaded?.raw ? { ...loaded.raw } : { version: 1 };
|
|
887
|
+
cfg.profile = profileId;
|
|
888
|
+
writeProjectConfig(cfgPath, cfg, { validate: true, silent: true });
|
|
889
|
+
if (!quiet) console.log(`Profile set to ${profileId}.`);
|
|
890
|
+
}
|
|
837
891
|
}
|
|
838
892
|
|
|
839
893
|
// Resolve active profile to drive capability-gated scaffolding (intake
|
|
@@ -846,16 +900,11 @@ async function main() {
|
|
|
846
900
|
const { gatherAttribution } = await import('./intake/attribution.mjs');
|
|
847
901
|
const attribution = intakeCap?.attribution ? gatherAttribution() : null;
|
|
848
902
|
|
|
849
|
-
//
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
filePath: path.join(target, "AGENTS.md"),
|
|
855
|
-
content: buildAgentsGuide(projectName),
|
|
856
|
-
generator: "construct/init",
|
|
857
|
-
attribution,
|
|
858
|
-
});
|
|
903
|
+
// AGENTS.md and CLAUDE.md are user-owned files (ADR-0027 §2). Construct does
|
|
904
|
+
// not author their bodies — bd init creates them with the project skeleton and
|
|
905
|
+
// the Beads block, and the injectIntoAgentFile pass below adds Construct's
|
|
906
|
+
// guidance as a fenced marker block only. Pre-writing a doctrine body here
|
|
907
|
+
// would land un-fenced Construct content that sync/doctor cannot reconcile.
|
|
859
908
|
|
|
860
909
|
writeStampedIfMissing({
|
|
861
910
|
targetRoot: target,
|
|
@@ -886,15 +935,10 @@ async function main() {
|
|
|
886
935
|
attribution,
|
|
887
936
|
});
|
|
888
937
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
filePath: path.join(target, ".cx", ".gitkeep"),
|
|
894
|
-
content: "",
|
|
895
|
-
generator: "construct/init",
|
|
896
|
-
});
|
|
897
|
-
|
|
938
|
+
// `.cx/` already exists from the context writes above and is gitignored in full
|
|
939
|
+
// (ADR-0027), so a keep file there could never be tracked. No dead keep file is written.
|
|
940
|
+
|
|
941
|
+
|
|
898
942
|
// Detect existing project content once; the result feeds three decisions
|
|
899
943
|
// below: skip .cx/inbox/ on custom intake, skip lane scaffolding for lanes
|
|
900
944
|
// already covered elsewhere, and skip per-lane templates/ when root
|
|
@@ -909,21 +953,19 @@ async function main() {
|
|
|
909
953
|
console.log(`[init:intake] skipping .cx/inbox/ — ${inboxDecision.reason}. Run with --force to scaffold anyway.`);
|
|
910
954
|
skipped.push('.cx/inbox/ (deferred to existing intake)');
|
|
911
955
|
} else {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
content: "",
|
|
918
|
-
generator: "construct/init",
|
|
919
|
-
});
|
|
956
|
+
// `.cx/inbox/` is the local intake drop zone the watcher polls; under the gitignored
|
|
957
|
+
// `.cx/` tree the directory must exist on disk but needs no keep file.
|
|
958
|
+
|
|
959
|
+
fs.mkdirSync(path.join(target, ".cx", "inbox"), { recursive: true });
|
|
960
|
+
created.push(".cx/inbox/");
|
|
920
961
|
}
|
|
921
962
|
|
|
922
963
|
// Intake-archetype scaffolding. When the active profile declares
|
|
923
|
-
// capabilities.intake.inbox, scaffold a project-root inbox/ drop zone
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
964
|
+
// capabilities.intake.inbox, scaffold a project-root inbox/ drop zone and seed
|
|
965
|
+
// the dedup manifest. The whole inbox/ is ignored via host-disposition
|
|
966
|
+
// IGNORED_PATTERNS (ADR-0027 §1), so raw drops never enter source and no local
|
|
967
|
+
// keep-file is needed. Existing-structure detection already opted out of
|
|
968
|
+
// clobbering a user's pipeline above.
|
|
927
969
|
|
|
928
970
|
if (intakeCap?.inbox && !inboxDecision.skip) {
|
|
929
971
|
const projectInbox = path.join(target, 'inbox');
|
|
@@ -931,11 +973,6 @@ async function main() {
|
|
|
931
973
|
fs.mkdirSync(projectInbox, { recursive: true });
|
|
932
974
|
created.push('inbox/');
|
|
933
975
|
}
|
|
934
|
-
const inboxGitignore = path.join(projectInbox, '.gitignore');
|
|
935
|
-
if (!fs.existsSync(inboxGitignore)) {
|
|
936
|
-
fs.writeFileSync(inboxGitignore, '*\n!.gitignore\n', 'utf8');
|
|
937
|
-
created.push('inbox/.gitignore');
|
|
938
|
-
}
|
|
939
976
|
|
|
940
977
|
if (intakeCap.dedup === 'sha256') {
|
|
941
978
|
const { saveManifest, loadManifest, MANIFEST_REL_PATH } = await import('./intake/manifest.mjs');
|
|
@@ -947,104 +984,170 @@ async function main() {
|
|
|
947
984
|
}
|
|
948
985
|
}
|
|
949
986
|
|
|
950
|
-
//
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
987
|
+
// Gitignore every Construct-generated artifact whose disposition is `ignored`
|
|
988
|
+
// (ADR-0027 §1): the six adapter dirs, the `.construct/` launcher, `.cx/`
|
|
989
|
+
// runtime state, and the generated config files all carry machine-specific
|
|
990
|
+
// absolute paths (MCP server paths, env-resolved tokens) and are recreated by
|
|
991
|
+
// `construct sync`, so they are never committed. Mirrors Construct's own repo
|
|
992
|
+
// .gitignore 1:1. Idempotent per-pattern: a pattern already matched exactly,
|
|
993
|
+
// by its bare/slashed form, or by a broader `*` / `**`, is left untouched.
|
|
954
994
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
995
|
+
const isToolRepo = isConstructPackageRepo(target);
|
|
996
|
+
|
|
997
|
+
if (!isToolRepo) {
|
|
998
|
+
try {
|
|
999
|
+
const gitignorePath = path.join(target, '.gitignore');
|
|
1000
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
|
|
1001
|
+
const missing = missingIgnorePatterns(existing);
|
|
1002
|
+
if (missing.length > 0) {
|
|
1003
|
+
const prefix = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
1004
|
+
const block = `${prefix}\n# Construct — generated adapters, launcher, and runtime state.\n# Machine-specific, recreated by \`construct sync\`; never source (ADR-0027).\n${missing.join('\n')}\n`;
|
|
1005
|
+
fs.writeFileSync(gitignorePath, existing + block, 'utf8');
|
|
1006
|
+
created.push(existing.length === 0 ? '.gitignore (Construct ignores)' : `.gitignore (+${missing.length} Construct ignore${missing.length === 1 ? '' : 's'})`);
|
|
1007
|
+
}
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.warn(`⚠️ Could not update .gitignore: ${err.message}`);
|
|
967
1010
|
}
|
|
968
|
-
}
|
|
969
|
-
console.
|
|
1011
|
+
} else if (verbose) {
|
|
1012
|
+
console.log('[init:conflation] inside Construct repository; skipping .gitignore mutation.');
|
|
970
1013
|
}
|
|
971
1014
|
|
|
972
|
-
//
|
|
973
|
-
//
|
|
1015
|
+
// Resolve which host adapter sets to write (construct-4xy6). Default: the
|
|
1016
|
+
// hosts detected on this machine, plus any --with-<host>. VS Code reads
|
|
1017
|
+
// .claude/agents natively, so selecting it pulls in claude. Copilot is opt-in
|
|
1018
|
+
// only — never written by detection. --all-hosts writes every set; an empty
|
|
1019
|
+
// selection falls back to the .claude/ baseline so a project is never bare.
|
|
974
1020
|
|
|
975
|
-
|
|
976
|
-
|
|
1021
|
+
const adapterHosts = await resolveAdapterHosts();
|
|
1022
|
+
|
|
1023
|
+
// Stage .construct/ launcher + sync the selected adapter sets so init produces
|
|
1024
|
+
// the same project shape as a fresh `npm install` of the package as a dep.
|
|
1025
|
+
|
|
1026
|
+
if (!isToolRepo) {
|
|
977
1027
|
try {
|
|
978
|
-
pkgVersion =
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1028
|
+
let pkgVersion = '';
|
|
1029
|
+
try {
|
|
1030
|
+
pkgVersion = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8')).version || '';
|
|
1031
|
+
} catch { /* fall through with empty version */ }
|
|
1032
|
+
const stagingPre = fs.existsSync(path.join(target, '.construct', 'run.mjs'));
|
|
1033
|
+
const claudePre = fs.existsSync(path.join(target, '.claude', 'settings.json'));
|
|
1034
|
+
stageProjectAdapters({
|
|
1035
|
+
projectRoot: target,
|
|
1036
|
+
packageRoot: ROOT_DIR,
|
|
1037
|
+
pkgVersion,
|
|
1038
|
+
hosts: adapterHosts,
|
|
1039
|
+
log: (msg) => console.log(`[init:stage] ${msg}`),
|
|
1040
|
+
});
|
|
1041
|
+
if (!quiet && adapterHosts) {
|
|
1042
|
+
console.log(` Adapters: ${adapterHosts.join(', ')} (add more with --with-<host>, or --all-hosts).`);
|
|
1043
|
+
}
|
|
1044
|
+
if (!stagingPre && fs.existsSync(path.join(target, '.construct', 'run.mjs'))) {
|
|
1045
|
+
created.push('.construct/ (launcher staged)');
|
|
1046
|
+
}
|
|
1047
|
+
if (!claudePre && fs.existsSync(path.join(target, '.claude', 'settings.json'))) {
|
|
1048
|
+
created.push('.claude/ (agents + settings)');
|
|
1049
|
+
}
|
|
994
1050
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1051
|
+
// construct init writes project scope only. Global wiring (the `construct`
|
|
1052
|
+
// front-door agent in `~/.claude/agents/`, `~/.codex/agents/`, etc.) is
|
|
1053
|
+
// installed once by `construct sync --global` or the npm postinstall.
|
|
1054
|
+
// Specialists, slash commands, and skills live with the repo per each
|
|
1055
|
+
// host's documented best-practice scope.
|
|
1000
1056
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
console.warn(`⚠️ Adapter staging failed: ${err.message}`);
|
|
1059
|
+
}
|
|
1060
|
+
} else if (verbose) {
|
|
1061
|
+
console.log('[init:conflation] inside Construct repository; skipping .construct/ launcher staging.');
|
|
1003
1062
|
}
|
|
1004
1063
|
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1064
|
+
// Cache a host-agnostic project profile at .cx/project-profile.json (gitignored)
|
|
1065
|
+
// so per-host skill filtering has a signal to scope against. Owned by init —
|
|
1066
|
+
// install must not read the cwd (ADR-0027 §3).
|
|
1067
|
+
|
|
1068
|
+
try {
|
|
1069
|
+
const { detectProjectProfile, writeProfile } = await import('./project-profile.mjs');
|
|
1070
|
+
const profile = detectProjectProfile(target);
|
|
1071
|
+
if (profile.tags.length > 0) {
|
|
1072
|
+
writeProfile(profile, target);
|
|
1073
|
+
created.push('.cx/project-profile.json');
|
|
1074
|
+
}
|
|
1075
|
+
} catch { /* project profiling is advisory; init proceeds */ }
|
|
1076
|
+
|
|
1077
|
+
// Tool orientation describes Construct, not the project, so it belongs in the
|
|
1078
|
+
// ignored .cx/ tree rather than the host repo root — never read as project
|
|
1079
|
+
// content, never committed (ADR-0027 §1). Written once; skip-when-exists keeps
|
|
1080
|
+
// user edits across subsequent inits and `npm install` rebuilds.
|
|
1007
1081
|
|
|
1008
1082
|
const guideSrc = path.join(ROOT_DIR, 'templates', 'docs', 'construct_guide.md');
|
|
1009
|
-
const guideDst = path.join(target, 'construct_guide.md');
|
|
1083
|
+
const guideDst = path.join(target, '.cx', 'construct_guide.md');
|
|
1010
1084
|
if (fs.existsSync(guideSrc) && !fs.existsSync(guideDst)) {
|
|
1011
1085
|
try {
|
|
1086
|
+
fs.mkdirSync(path.dirname(guideDst), { recursive: true });
|
|
1012
1087
|
fs.copyFileSync(guideSrc, guideDst);
|
|
1013
|
-
created.push('construct_guide.md');
|
|
1088
|
+
created.push('.cx/construct_guide.md');
|
|
1014
1089
|
} catch (err) {
|
|
1015
|
-
console.warn(`⚠️ Could not write construct_guide.md: ${err.message}`);
|
|
1090
|
+
console.warn(`⚠️ Could not write .cx/construct_guide.md: ${err.message}`);
|
|
1016
1091
|
}
|
|
1017
1092
|
} else if (fs.existsSync(guideDst)) {
|
|
1018
|
-
skipped.push('construct_guide.md');
|
|
1093
|
+
skipped.push('.cx/construct_guide.md');
|
|
1019
1094
|
}
|
|
1020
1095
|
|
|
1021
1096
|
// Auto-init beads
|
|
1022
1097
|
const beadsMeta = path.join(target, ".beads", "metadata.json");
|
|
1023
|
-
if (!fs.existsSync(beadsMeta)) {
|
|
1024
|
-
console.log('');
|
|
1025
|
-
console.log('═══════════════════════════════════════════════════════════');
|
|
1026
|
-
console.log(' TASK TRACKING (beads)');
|
|
1027
|
-
console.log('═══════════════════════════════════════════════════════════');
|
|
1028
|
-
console.log('');
|
|
1029
|
-
console.log('Beads is a local-first issue tracker that keeps your work');
|
|
1030
|
-
console.log('organized without depending on external services like Jira.');
|
|
1031
|
-
console.log('');
|
|
1032
|
-
console.log('Features:');
|
|
1033
|
-
console.log(' • Local SQLite database (no cloud required)');
|
|
1034
|
-
console.log(' • Issues sync to git (optional)');
|
|
1035
|
-
console.log(' • Works offline');
|
|
1098
|
+
if (!fs.existsSync(beadsMeta) && !isToolRepo) {
|
|
1036
1099
|
console.log('');
|
|
1037
|
-
console.log(
|
|
1100
|
+
console.log('Initializing the task tracker (beads — local-first, git-synced, works offline)…');
|
|
1038
1101
|
fs.mkdirSync(path.join(target, ".beads"), { recursive: true });
|
|
1039
1102
|
try {
|
|
1040
|
-
initializeBeadsTracker(target);
|
|
1103
|
+
const beads = initializeBeadsTracker(target, { commitBootstrap });
|
|
1041
1104
|
created.push(".beads/ (initialized)");
|
|
1105
|
+
const prefixNote = beads?.prefix ? ` — issues prefixed \`${beads.prefix}-\`` : "";
|
|
1106
|
+
console.log(` ✓ Task tracker ready${prefixNote}. Run \`bd quickstart\` to start.`);
|
|
1107
|
+
if (beads?.withheldCommit) {
|
|
1108
|
+
console.log(" Bootstrap files left uncommitted (commit them yourself, or re-run with --commit-bootstrap).");
|
|
1109
|
+
}
|
|
1042
1110
|
} catch (e) {
|
|
1043
1111
|
console.warn("⚠️ Beads init failed:", e.message);
|
|
1044
1112
|
}
|
|
1045
1113
|
console.log('');
|
|
1046
1114
|
}
|
|
1047
|
-
|
|
1115
|
+
|
|
1116
|
+
// Wire the project's git hooks to .beads/hooks so Construct's pre-commit
|
|
1117
|
+
// secret-scan and policy gates activate. Owned by init, not install (ADR-0027
|
|
1118
|
+
// §3): install must never mutate the cwd repo. Idempotent — no-op when
|
|
1119
|
+
// .beads/hooks/pre-commit is absent or a non-default custom path is set.
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
const { ensureGitHooksPath } = await import('./git-hooks-path.mjs');
|
|
1123
|
+
const hooks = ensureGitHooksPath({ cwd: target });
|
|
1124
|
+
if (hooks.status === 'set') {
|
|
1125
|
+
created.push('git core.hooksPath → .beads/hooks');
|
|
1126
|
+
if (!quiet) console.log(` ✓ ${hooks.message}`);
|
|
1127
|
+
} else if (hooks.status === 'warning' && !quiet) {
|
|
1128
|
+
console.log(` ⚠️ ${hooks.message}`);
|
|
1129
|
+
}
|
|
1130
|
+
} catch { /* git-hooks wiring is advisory; init proceeds */ }
|
|
1131
|
+
|
|
1132
|
+
// Inject Construct's integration guidance into AGENTS.md and CLAUDE.md as a
|
|
1133
|
+
// versioned, hash-stamped marker block (ADR-0027 §2). Runs after bd init so
|
|
1134
|
+
// the block survives whatever bd wrote, preserves all content outside the
|
|
1135
|
+
// markers, and dedups against a sibling Beads Integration block. Idempotent:
|
|
1136
|
+
// same hash is a no-op, a version/hash bump replaces the block content only.
|
|
1137
|
+
|
|
1138
|
+
try {
|
|
1139
|
+
const { injectIntoAgentFile, CONSTRUCT_INTEGRATION_VERSION } = await import('./agent-instructions/inject.mjs');
|
|
1140
|
+
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
1141
|
+
const res = injectIntoAgentFile(path.join(target, name), {
|
|
1142
|
+
version: CONSTRUCT_INTEGRATION_VERSION,
|
|
1143
|
+
header: `# ${projectName}\n`,
|
|
1144
|
+
});
|
|
1145
|
+
if (!res.existed) created.push(`${name} (Construct integration)`);
|
|
1146
|
+
}
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
console.warn(`⚠️ Could not inject agent-instruction block: ${err.message}`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1048
1151
|
// Intake collection — always write intake-config.json with safe defaults
|
|
1049
1152
|
// so the user can inspect what's watched and edit the file directly. Empty
|
|
1050
1153
|
// parentDirs is the correct default: the inbox watcher always scans
|
|
@@ -1106,7 +1209,7 @@ async function main() {
|
|
|
1106
1209
|
if (lanesToScaffold.length > 0) {
|
|
1107
1210
|
writeIfMissing(
|
|
1108
1211
|
path.join(target, "docs", "README.md"),
|
|
1109
|
-
buildDocsReadme(projectName)
|
|
1212
|
+
buildDocsReadme(projectName, lanesToScaffold)
|
|
1110
1213
|
);
|
|
1111
1214
|
|
|
1112
1215
|
for (const laneKey of lanesToScaffold) {
|
|
@@ -1244,22 +1347,20 @@ async function main() {
|
|
|
1244
1347
|
|
|
1245
1348
|
if (!quiet) {
|
|
1246
1349
|
console.log('\nServices:');
|
|
1350
|
+
let dashboardUrl = null;
|
|
1247
1351
|
for (const svc of results) {
|
|
1248
1352
|
if (svc.status === 'started' || svc.status === 'reused') {
|
|
1249
1353
|
const status = svc.status === 'reused' ? ' (running)' : '';
|
|
1250
|
-
|
|
1251
|
-
|
|
1354
|
+
console.log(` ${svc.name}${status}${svc.url ? ` ${svc.url}` : ''}`);
|
|
1355
|
+
if (svc.name === 'Dashboard' && svc.url) dashboardUrl = svc.url;
|
|
1356
|
+
} else if (svc.status === 'failed') {
|
|
1357
|
+
console.log(` ${svc.name} — failed${svc.note ? ` (${svc.note})` : ''}`);
|
|
1252
1358
|
}
|
|
1253
1359
|
}
|
|
1254
|
-
|
|
1255
|
-
// Show Telemetry credentials if running locally
|
|
1360
|
+
|
|
1256
1361
|
const telemetrySvc = results.find(r => r.name === 'Telemetry' && r.url?.includes('localhost'));
|
|
1257
|
-
if (telemetrySvc) {
|
|
1258
|
-
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
const dashboardPort = process.env.DASHBOARD_PORT || '4242';
|
|
1262
|
-
console.log(`\nDashboard: http://127.0.0.1:${dashboardPort}`);
|
|
1362
|
+
if (telemetrySvc) console.log(`\nTelemetry: ${telemetrySvc.url}`);
|
|
1363
|
+
if (dashboardUrl) console.log(`\nDashboard: ${dashboardUrl} · stop services with \`construct stop\``);
|
|
1263
1364
|
}
|
|
1264
1365
|
} catch (error) {
|
|
1265
1366
|
console.error(`Services could not be started: ${error.message}`);
|