@geraldmaron/construct 1.0.3 → 1.0.5
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 +51 -42
- package/agents/prompts/cx-ai-engineer.md +6 -26
- package/agents/prompts/cx-architect.md +1 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +6 -26
- package/agents/prompts/cx-docs-keeper.md +1 -31
- package/agents/prompts/cx-explorer.md +1 -0
- package/agents/prompts/cx-orchestrator.md +40 -112
- package/agents/prompts/cx-platform-engineer.md +2 -22
- package/agents/prompts/cx-product-manager.md +2 -1
- package/agents/prompts/cx-qa.md +0 -20
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-researcher.md +77 -31
- package/agents/prompts/cx-security.md +11 -49
- package/agents/prompts/cx-sre.md +9 -43
- package/agents/prompts/cx-ux-researcher.md +1 -0
- package/agents/role-manifests.json +4 -4
- package/bin/construct +279 -4
- package/commands/understand/research.md +5 -3
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +3 -4
- package/lib/beads-automation.mjs +16 -7
- package/lib/cli-commands.mjs +52 -2
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +37 -17
- package/lib/embed/daemon.mjs +10 -8
- package/lib/embed/recommendation-store.mjs +213 -14
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/flavors/loader.mjs +136 -0
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-unified.mjs +25 -2
- package/lib/init-update.mjs +246 -131
- package/lib/intake/classify.mjs +61 -183
- package/lib/intake/prepare.mjs +7 -0
- package/lib/intake/tables/creative.mjs +94 -0
- package/lib/intake/tables/operations.mjs +85 -0
- package/lib/intake/tables/research.mjs +85 -0
- package/lib/intake/tables/rnd.mjs +175 -0
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-router.mjs +61 -1
- package/lib/observation-store.mjs +19 -0
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +121 -21
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -0
- package/lib/server/index.mjs +76 -0
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +30 -22
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +1 -2
- package/lib/setup.mjs +8 -43
- package/lib/status.mjs +51 -5
- package/lib/storage/backend.mjs +12 -2
- package/lib/strategy-store.mjs +371 -0
- package/lib/telemetry/backends/local.mjs +6 -4
- package/lib/telemetry/client.mjs +185 -0
- package/lib/telemetry/ingest.mjs +13 -5
- package/lib/telemetry/team-rollup.mjs +9 -2
- package/lib/worker/trace.mjs +17 -27
- package/package.json +10 -2
- package/personas/construct.md +20 -20
- package/platforms/claude/settings.template.json +13 -0
- package/rules/common/research.md +44 -12
- package/scripts/sync-agents.mjs +11 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -2
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +5 -5
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +3 -3
- package/skills/docs/product-signal-workflow.md +48 -18
- package/skills/docs/research-workflow.md +26 -14
- package/skills/docs/strategy-workflow.md +36 -0
- package/skills/roles/architect.ai-systems.md +4 -2
- package/skills/roles/architect.data.md +4 -2
- package/skills/roles/architect.enterprise.md +4 -2
- package/skills/roles/architect.integration.md +4 -2
- package/skills/roles/architect.md +7 -5
- package/skills/roles/architect.platform.md +4 -2
- package/skills/roles/data-analyst.experiment.md +4 -2
- package/skills/roles/data-analyst.md +9 -7
- package/skills/roles/data-analyst.product-intelligence.md +5 -3
- package/skills/roles/data-analyst.product.md +4 -2
- package/skills/roles/data-analyst.telemetry.md +4 -2
- package/skills/roles/data-engineer.pipeline.md +4 -2
- package/skills/roles/data-engineer.vector-retrieval.md +4 -2
- package/skills/roles/data-engineer.warehouse.md +4 -2
- package/skills/roles/debugger.md +7 -5
- package/skills/roles/designer.accessibility.md +4 -2
- package/skills/roles/designer.md +10 -8
- package/skills/roles/engineer.ai.md +4 -2
- package/skills/roles/engineer.data.md +5 -3
- package/skills/roles/engineer.md +14 -12
- package/skills/roles/engineer.platform.md +5 -3
- package/skills/roles/operator.docs.md +6 -4
- package/skills/roles/operator.md +6 -4
- package/skills/roles/operator.release.md +4 -2
- package/skills/roles/operator.sre.md +5 -3
- package/skills/roles/orchestrator.md +5 -3
- package/skills/roles/product-manager.ai-product.md +4 -2
- package/skills/roles/product-manager.business-strategy.md +4 -2
- package/skills/roles/product-manager.enterprise.md +4 -2
- package/skills/roles/product-manager.growth.md +4 -2
- package/skills/roles/product-manager.md +6 -4
- package/skills/roles/product-manager.platform.md +4 -2
- package/skills/roles/product-manager.product.md +4 -2
- package/skills/roles/qa.ai-eval.md +4 -2
- package/skills/roles/qa.api-contract.md +4 -2
- package/skills/roles/qa.data-pipeline.md +4 -2
- package/skills/roles/qa.md +7 -5
- package/skills/roles/qa.test-automation.md +5 -3
- package/skills/roles/qa.web-ui.md +4 -2
- package/skills/roles/researcher.explorer.md +4 -2
- package/skills/roles/researcher.md +35 -20
- package/skills/roles/researcher.ux.md +4 -2
- package/skills/roles/reviewer.devil-advocate.md +4 -2
- package/skills/roles/reviewer.evaluator.md +4 -2
- package/skills/roles/reviewer.md +14 -12
- package/skills/roles/reviewer.trace.md +4 -2
- package/skills/roles/security.ai.md +4 -2
- package/skills/roles/security.appsec.md +4 -2
- package/skills/roles/security.cloud.md +4 -2
- package/skills/roles/security.legal-compliance.md +4 -2
- package/skills/roles/security.md +7 -5
- package/skills/roles/security.privacy.md +4 -2
- package/skills/roles/security.supply-chain.md +4 -2
- package/skills/routing.md +8 -1
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/skill-artifact.md +27 -0
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +6 -6
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync, execSync } from "node:child_process";
|
|
11
11
|
import { readOpenCodeConfig } from "./opencode-config.mjs";
|
|
12
|
+
import fs from "node:fs";
|
|
12
13
|
import net from "node:net";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
13
16
|
|
|
14
17
|
const LOWEST_PORT = 0;
|
|
15
18
|
const HIGHEST_PORT = 65535;
|
|
@@ -46,7 +49,8 @@ function commandVersion(command, args = ["--version"]) {
|
|
|
46
49
|
try {
|
|
47
50
|
return execFileSync(command, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 5000 })
|
|
48
51
|
.toString()
|
|
49
|
-
.trim()
|
|
52
|
+
.trim()
|
|
53
|
+
.split(/\r?\n/, 1)[0];
|
|
50
54
|
} catch {
|
|
51
55
|
return null;
|
|
52
56
|
}
|
|
@@ -71,6 +75,49 @@ function versionAtLeast(version, minimum) {
|
|
|
71
75
|
return true;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function detectVsCodeAvailability(homeDir = os.homedir()) {
|
|
79
|
+
const settingsCandidates = (() => {
|
|
80
|
+
const platform = os.platform();
|
|
81
|
+
if (platform === "darwin") {
|
|
82
|
+
return [
|
|
83
|
+
path.join(homeDir, "Library", "Application Support", "Code", "User", "settings.json"),
|
|
84
|
+
path.join(homeDir, "Library", "Application Support", "Code - Insiders", "User", "settings.json"),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
if (platform === "linux") {
|
|
88
|
+
return [
|
|
89
|
+
path.join(homeDir, ".config", "Code", "User", "settings.json"),
|
|
90
|
+
path.join(homeDir, ".config", "Code - Insiders", "User", "settings.json"),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
if (platform === "win32") {
|
|
94
|
+
const appData = process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming");
|
|
95
|
+
return [
|
|
96
|
+
path.join(appData, "Code", "User", "settings.json"),
|
|
97
|
+
path.join(appData, "Code - Insiders", "User", "settings.json"),
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
const version = commandVersion("code") || commandVersion("code-insiders");
|
|
104
|
+
const hasSettings = settingsCandidates.some((candidate) => fs.existsSync(candidate));
|
|
105
|
+
return { version, hasSettings };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function detectCursorAvailability(homeDir = os.homedir()) {
|
|
109
|
+
const version = commandVersion("cursor") || commandVersion("cursor-agent");
|
|
110
|
+
const hasConfig = fs.existsSync(path.join(homeDir, ".cursor", "mcp.json"));
|
|
111
|
+
return { version, hasConfig };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function detectCopilotAvailability(homeDir = os.homedir()) {
|
|
115
|
+
const promptsDir = path.join(homeDir, ".github", "prompts");
|
|
116
|
+
const instructionsPath = path.join(homeDir, ".github", "copilot-instructions.md");
|
|
117
|
+
const hasFiles = fs.existsSync(promptsDir) || fs.existsSync(instructionsPath);
|
|
118
|
+
return { hasFiles };
|
|
119
|
+
}
|
|
120
|
+
|
|
74
121
|
export function detectHostCapabilities() {
|
|
75
122
|
const claudeRaw = commandVersion("claude");
|
|
76
123
|
const claudeVersion = parseClaudeVersion(claudeRaw);
|
|
@@ -78,6 +125,9 @@ export function detectHostCapabilities() {
|
|
|
78
125
|
const tmuxRaw = commandVersion("tmux", ["-V"]);
|
|
79
126
|
const opencodeRaw = commandVersion("opencode");
|
|
80
127
|
const codexRaw = commandVersion("codex");
|
|
128
|
+
const vscode = detectVsCodeAvailability();
|
|
129
|
+
const cursor = detectCursorAvailability();
|
|
130
|
+
const copilot = detectCopilotAvailability();
|
|
81
131
|
|
|
82
132
|
return [
|
|
83
133
|
{
|
|
@@ -123,6 +173,45 @@ export function detectHostCapabilities() {
|
|
|
123
173
|
"Native profile switching is not automatic; continue in-session when dispatch is unavailable.",
|
|
124
174
|
],
|
|
125
175
|
},
|
|
176
|
+
{
|
|
177
|
+
host: "VS Code",
|
|
178
|
+
availability: vscode.version || vscode.hasSettings ? "installed" : "missing",
|
|
179
|
+
version: vscode.version,
|
|
180
|
+
orchestration: "copilot-mcp",
|
|
181
|
+
promptableWorkers: false,
|
|
182
|
+
sharedTaskRuntime: "editor-session",
|
|
183
|
+
lifecycleHooks: [],
|
|
184
|
+
notes: [
|
|
185
|
+
"Construct manages GitHub Copilot MCP registrations in user settings when a VS Code settings.json already exists.",
|
|
186
|
+
"VS Code is an editor surface, not Construct's primary multi-agent runtime.",
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
host: "Cursor",
|
|
191
|
+
availability: cursor.version || cursor.hasConfig ? "installed" : "missing",
|
|
192
|
+
version: cursor.version,
|
|
193
|
+
orchestration: "mcp-only",
|
|
194
|
+
promptableWorkers: false,
|
|
195
|
+
sharedTaskRuntime: "editor-session",
|
|
196
|
+
lifecycleHooks: [],
|
|
197
|
+
notes: [
|
|
198
|
+
"Construct manages Cursor MCP registrations in ~/.cursor/mcp.json when that file exists.",
|
|
199
|
+
"Cursor agent profiles are not generated today; use Construct through MCP-backed tools and shared project state.",
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
host: "Copilot",
|
|
204
|
+
availability: copilot.hasFiles ? "installed" : "missing",
|
|
205
|
+
version: null,
|
|
206
|
+
orchestration: "prompt-profiles",
|
|
207
|
+
promptableWorkers: false,
|
|
208
|
+
sharedTaskRuntime: "editor-session",
|
|
209
|
+
lifecycleHooks: [],
|
|
210
|
+
notes: [
|
|
211
|
+
"Construct writes reusable prompt profiles under ~/.github/prompts/ plus a managed block in ~/.github/copilot-instructions.md.",
|
|
212
|
+
"Copilot does not expose Construct-style subagent dispatch; use the generated profiles for focused passes.",
|
|
213
|
+
],
|
|
214
|
+
},
|
|
126
215
|
];
|
|
127
216
|
}
|
|
128
217
|
|
package/lib/init-unified.mjs
CHANGED
|
@@ -49,6 +49,12 @@ const interactive = args.includes("--interactive") || args.includes("-i");
|
|
|
49
49
|
const quiet = args.includes("--quiet") || args.includes("-q");
|
|
50
50
|
const skipInteractive = !interactive;
|
|
51
51
|
|
|
52
|
+
// Active profile selector. `--profile=<id>` writes the field into the
|
|
53
|
+
// project's construct.config.json so resolveActiveProfile picks it up
|
|
54
|
+
// on first run. Unknown ids are rejected with the available catalog.
|
|
55
|
+
const profileArg = args.find((arg) => arg.startsWith("--profile="));
|
|
56
|
+
const profileId = profileArg ? profileArg.split("=")[1] : null;
|
|
57
|
+
|
|
52
58
|
const created = [];
|
|
53
59
|
const skipped = [];
|
|
54
60
|
|
|
@@ -795,12 +801,29 @@ function initializeBeadsTracker(target) {
|
|
|
795
801
|
|
|
796
802
|
async function main() {
|
|
797
803
|
const projectName = inferProjectName(target);
|
|
798
|
-
|
|
804
|
+
|
|
799
805
|
const { clean } = preflight(target);
|
|
800
|
-
|
|
806
|
+
|
|
801
807
|
if (!quiet) {
|
|
802
808
|
console.log(`Initializing Construct in ${path.relative(process.cwd(), target) || "."}`);
|
|
803
809
|
}
|
|
810
|
+
|
|
811
|
+
// Persist --profile=<id> into construct.config.json. Validate against the
|
|
812
|
+
// curated catalog so a typo never silently falls back to rnd.
|
|
813
|
+
if (profileId) {
|
|
814
|
+
const { loadProfile, listProfiles } = await import('./profiles/loader.mjs');
|
|
815
|
+
if (!loadProfile(profileId)) {
|
|
816
|
+
console.error(`Unknown profile: ${profileId}. Available: ${listProfiles().join(', ')}`);
|
|
817
|
+
process.exit(1);
|
|
818
|
+
}
|
|
819
|
+
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('./config/project-config.mjs');
|
|
820
|
+
const found = findProjectConfigPath(target);
|
|
821
|
+
const cfgPath = found || path.join(target, PROJECT_CONFIG_FILENAME);
|
|
822
|
+
const cfg = found ? (loadProjectConfig(target) || { version: 1 }) : { version: 1 };
|
|
823
|
+
cfg.profile = profileId;
|
|
824
|
+
writeProjectConfig(cfgPath, cfg, { validate: true });
|
|
825
|
+
if (!quiet) console.log(`Profile set to ${profileId}.`);
|
|
826
|
+
}
|
|
804
827
|
|
|
805
828
|
// Always create core Construct files
|
|
806
829
|
writeStampedIfMissing({
|
package/lib/init-update.mjs
CHANGED
|
@@ -1,168 +1,283 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* lib/init-update.mjs —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* lib/init-update.mjs — non-destructive project-standard updates and template conflict resolution.
|
|
4
|
+
*
|
|
5
|
+
* `construct init:update` helps projects adopt current Construct standards without replacing
|
|
6
|
+
* user-managed instruction files. Proposed updates are written under .cx/proposals/ for
|
|
7
|
+
* manual review and merge.
|
|
8
|
+
*
|
|
9
|
+
* checkTemplateConflicts(targetDir) compares project templates against the construct install's
|
|
10
|
+
* templates/docs/ directory. resolveTemplateConflict(conflict, resolution, targetDir) applies
|
|
11
|
+
* one of three resolutions: keep-project, use-construct, or move-to-cx-override.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import fs from "node:fs";
|
|
14
15
|
import path from "node:path";
|
|
15
|
-
import
|
|
16
|
+
import crypto from "node:crypto";
|
|
17
|
+
import { constructDir } from "./paths.mjs";
|
|
16
18
|
|
|
17
19
|
const args = process.argv.slice(2);
|
|
18
20
|
const dryRun = args.includes("--dry-run");
|
|
19
|
-
const cwdArg = args.find(arg => arg.startsWith("--cwd="));
|
|
21
|
+
const cwdArg = args.find((arg) => arg.startsWith("--cwd="));
|
|
20
22
|
const cwd = cwdArg ? path.resolve(cwdArg.split("=")[1]) : process.cwd();
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
const REQUIRED_AGENT_SECTIONS = [
|
|
25
|
+
"Operating hierarchy",
|
|
26
|
+
"Start-of-session rules",
|
|
27
|
+
"Maintenance rules",
|
|
28
|
+
"End-of-session rules",
|
|
29
|
+
"Verification rules",
|
|
30
|
+
];
|
|
23
31
|
|
|
24
|
-
// Load the AGENTS.md template from project-init-shared
|
|
25
32
|
async function loadAgentsTemplate(projectName) {
|
|
26
|
-
const modulePath = path.join(import.meta.dirname,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (module.buildAgentsGuide) {
|
|
31
|
-
return module.buildAgentsGuide(projectName);
|
|
32
|
-
}
|
|
33
|
-
throw new Error('Could not find buildAgentsGuide function in project-init-shared.mjs');
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error(`❌ Failed to load AGENTS.md template: ${error.message}`);
|
|
36
|
-
process.exit(1);
|
|
33
|
+
const modulePath = path.join(import.meta.dirname, "project-init-shared.mjs");
|
|
34
|
+
const module = await import(modulePath);
|
|
35
|
+
if (!module.buildAgentsGuide) {
|
|
36
|
+
throw new Error("buildAgentsGuide not available from project-init-shared.mjs");
|
|
37
37
|
}
|
|
38
|
+
return module.buildAgentsGuide(projectName);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(`📋 Dry run - would update AGENTS.md`);
|
|
48
|
-
console.log(`--- Current AGENTS.md (first 200 chars) ---`);
|
|
49
|
-
console.log(existingContent.substring(0, 200) + '...');
|
|
50
|
-
console.log(`--- New AGENTS.md (first 200 chars) ---`);
|
|
51
|
-
console.log(newTemplate.substring(0, 200) + '...');
|
|
52
|
-
return null;
|
|
41
|
+
function inferProjectName(targetDir) {
|
|
42
|
+
const packagePath = path.join(targetDir, "package.json");
|
|
43
|
+
if (fs.existsSync(packagePath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
46
|
+
if (pkg.name) return pkg.name;
|
|
47
|
+
} catch { /* fall through */ }
|
|
53
48
|
}
|
|
54
|
-
|
|
55
|
-
// Backup existing file
|
|
56
|
-
const backupPath = path.join(cwd, 'AGENTS.md.backup');
|
|
57
|
-
writeFileSync(backupPath, existingContent, 'utf8');
|
|
58
|
-
console.log(`📦 Created backup at ${backupPath}`);
|
|
59
|
-
|
|
60
|
-
// Write new template
|
|
61
|
-
const agentsPath = path.join(cwd, 'AGENTS.md');
|
|
62
|
-
writeFileSync(agentsPath, newTemplate, 'utf8');
|
|
63
|
-
console.log(`✅ Updated AGENTS.md`);
|
|
64
|
-
|
|
65
|
-
return newTemplate;
|
|
49
|
+
return path.basename(targetDir);
|
|
66
50
|
}
|
|
67
51
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
function findMissingAgentsSections(content) {
|
|
53
|
+
return REQUIRED_AGENT_SECTIONS.filter((section) => !content.toLowerCase().includes(section.toLowerCase()));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ensureProposalDir(targetDir) {
|
|
57
|
+
const dir = path.join(targetDir, ".cx", "proposals");
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeProposal(targetDir, fileName, content) {
|
|
63
|
+
const proposalDir = ensureProposalDir(targetDir);
|
|
64
|
+
const proposalPath = path.join(proposalDir, fileName);
|
|
65
|
+
fs.writeFileSync(proposalPath, content, "utf8");
|
|
66
|
+
return proposalPath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildAgentsProposal(existingContent, template, missingSections) {
|
|
70
|
+
const missingList = missingSections.map((section) => `- ${section}`).join("\n");
|
|
71
|
+
return [
|
|
72
|
+
"<!--",
|
|
73
|
+
"Construct init:update proposal",
|
|
74
|
+
"Review this file and merge the needed sections into AGENTS.md manually.",
|
|
75
|
+
"The original AGENTS.md was not modified.",
|
|
76
|
+
"-->",
|
|
77
|
+
"",
|
|
78
|
+
"# Proposed AGENTS.md Update",
|
|
79
|
+
"",
|
|
80
|
+
"## Why this proposal exists",
|
|
81
|
+
"",
|
|
82
|
+
"Your current `AGENTS.md` is missing these required sections:",
|
|
83
|
+
missingList,
|
|
84
|
+
"",
|
|
85
|
+
"## Current AGENTS.md",
|
|
86
|
+
"",
|
|
87
|
+
"```md",
|
|
88
|
+
existingContent.trimEnd(),
|
|
89
|
+
"```",
|
|
90
|
+
"",
|
|
91
|
+
"## Current Construct template",
|
|
92
|
+
"",
|
|
93
|
+
"```md",
|
|
94
|
+
template.trimEnd(),
|
|
95
|
+
"```",
|
|
96
|
+
"",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildWorkflowProposal(existingContent) {
|
|
101
|
+
const updatedContent = existingContent.replace(
|
|
102
|
+
/(\s+- run: node bin\/construct doctor\s*\n)/,
|
|
103
|
+
"$1 - run: node bin/construct docs:verify\n",
|
|
104
|
+
);
|
|
105
|
+
if (updatedContent === existingContent) return null;
|
|
106
|
+
return [
|
|
107
|
+
"# Proposed CI Workflow Update",
|
|
108
|
+
"",
|
|
109
|
+
"Add `construct docs:verify` next to the existing doctor check.",
|
|
110
|
+
"",
|
|
111
|
+
"```yaml",
|
|
112
|
+
updatedContent.trimEnd(),
|
|
113
|
+
"```",
|
|
114
|
+
"",
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Template conflict detection ───────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function sha256File(filePath) {
|
|
121
|
+
const buf = fs.readFileSync(filePath);
|
|
122
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Compare project templates against the construct install's templates/docs/ directory.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} targetDir — project root to inspect
|
|
129
|
+
* @returns {{ conflicts: Array<{ name, constructPath, projectPath, constructSha, projectSha }>, identical: string[], missing: string[] }}
|
|
130
|
+
*/
|
|
131
|
+
export function checkTemplateConflicts(targetDir) {
|
|
132
|
+
const constructTemplatesDir = path.join(constructDir(), 'templates', 'docs');
|
|
133
|
+
const projectTemplatesDir = path.join(targetDir, 'templates', 'docs');
|
|
134
|
+
|
|
135
|
+
const conflicts = [];
|
|
136
|
+
const identical = [];
|
|
137
|
+
const missing = [];
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(constructTemplatesDir)) {
|
|
140
|
+
return { conflicts, identical, missing };
|
|
81
141
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return true;
|
|
142
|
+
|
|
143
|
+
const constructFiles = fs.readdirSync(constructTemplatesDir).filter((f) => f.endsWith('.md'));
|
|
144
|
+
|
|
145
|
+
for (const name of constructFiles) {
|
|
146
|
+
const constructPath = path.join(constructTemplatesDir, name);
|
|
147
|
+
const projectPath = path.join(projectTemplatesDir, name);
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(projectPath)) {
|
|
150
|
+
missing.push(name);
|
|
151
|
+
continue;
|
|
93
152
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
153
|
+
|
|
154
|
+
const constructSha = sha256File(constructPath);
|
|
155
|
+
const projectSha = sha256File(projectPath);
|
|
156
|
+
|
|
157
|
+
if (constructSha === projectSha) {
|
|
158
|
+
identical.push(name);
|
|
159
|
+
} else {
|
|
160
|
+
conflicts.push({ name, constructPath, projectPath, constructSha, projectSha });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { conflicts, identical, missing };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Apply a resolution to a single template conflict.
|
|
169
|
+
*
|
|
170
|
+
* Resolutions:
|
|
171
|
+
* 'keep-project' — do nothing; leave the project template as-is
|
|
172
|
+
* 'use-construct' — overwrite project template with construct's version; backup original to <name>.bak
|
|
173
|
+
* 'move-to-cx-override' — copy project template to .cx/templates/docs/<name>,
|
|
174
|
+
* then write construct's template to templates/docs/<name>
|
|
175
|
+
*
|
|
176
|
+
* @param {{ name, constructPath, projectPath, constructSha, projectSha }} conflict
|
|
177
|
+
* @param {'keep-project'|'use-construct'|'move-to-cx-override'} resolution
|
|
178
|
+
* @param {string} targetDir
|
|
179
|
+
*/
|
|
180
|
+
export function resolveTemplateConflict(conflict, resolution, targetDir) {
|
|
181
|
+
const { name, constructPath, projectPath } = conflict;
|
|
182
|
+
|
|
183
|
+
if (resolution === 'keep-project') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (resolution === 'use-construct') {
|
|
188
|
+
const backupPath = `${projectPath}.bak`;
|
|
189
|
+
fs.copyFileSync(projectPath, backupPath);
|
|
190
|
+
fs.copyFileSync(constructPath, projectPath);
|
|
191
|
+
return;
|
|
100
192
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
193
|
+
|
|
194
|
+
if (resolution === 'move-to-cx-override') {
|
|
195
|
+
const cxOverrideDir = path.join(targetDir, '.cx', 'templates', 'docs');
|
|
196
|
+
fs.mkdirSync(cxOverrideDir, { recursive: true });
|
|
197
|
+
fs.copyFileSync(projectPath, path.join(cxOverrideDir, name));
|
|
198
|
+
fs.copyFileSync(constructPath, projectPath);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
throw new Error(`Unknown resolution: ${resolution}. Expected keep-project, use-construct, or move-to-cx-override.`);
|
|
104
203
|
}
|
|
105
204
|
|
|
205
|
+
// ── Init update workflow ──────────────────────────────────────────────────────
|
|
206
|
+
|
|
106
207
|
async function main() {
|
|
107
|
-
console.log(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(`❌ AGENTS.md not found. Run 'construct init' first.`);
|
|
208
|
+
console.log(`Preparing Construct standards update for ${cwd}`);
|
|
209
|
+
|
|
210
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
211
|
+
if (!fs.existsSync(agentsPath)) {
|
|
212
|
+
console.error("AGENTS.md not found. Run `construct init` first.");
|
|
113
213
|
process.exit(1);
|
|
114
214
|
}
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
215
|
+
|
|
216
|
+
const projectName = inferProjectName(cwd);
|
|
217
|
+
const existingAgents = fs.readFileSync(agentsPath, "utf8");
|
|
218
|
+
const template = await loadAgentsTemplate(projectName);
|
|
219
|
+
const missingSections = findMissingAgentsSections(existingAgents);
|
|
220
|
+
|
|
221
|
+
const planned = [];
|
|
222
|
+
|
|
223
|
+
if (missingSections.length > 0) {
|
|
224
|
+
const proposalPath = path.join(".cx", "proposals", "AGENTS.md.construct-update.md");
|
|
225
|
+
planned.push({
|
|
226
|
+
label: `AGENTS.md proposal (${missingSections.length} missing section${missingSections.length === 1 ? "" : "s"})`,
|
|
227
|
+
write() {
|
|
228
|
+
return writeProposal(cwd, "AGENTS.md.construct-update.md", buildAgentsProposal(existingAgents, template, missingSections));
|
|
229
|
+
},
|
|
230
|
+
relativePath: proposalPath,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const workflowPath = path.join(cwd, ".github", "workflows", "ci.yml");
|
|
235
|
+
if (fs.existsSync(workflowPath)) {
|
|
236
|
+
const workflowContent = fs.readFileSync(workflowPath, "utf8");
|
|
237
|
+
if (!workflowContent.includes("node bin/construct docs:verify")) {
|
|
238
|
+
const proposal = buildWorkflowProposal(workflowContent);
|
|
239
|
+
if (proposal) {
|
|
240
|
+
planned.push({
|
|
241
|
+
label: "CI workflow proposal (add docs:verify)",
|
|
242
|
+
write() {
|
|
243
|
+
return writeProposal(cwd, "ci.yml.construct-update.md", proposal);
|
|
244
|
+
},
|
|
245
|
+
relativePath: path.join(".cx", "proposals", "ci.yml.construct-update.md"),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
127
248
|
}
|
|
128
249
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
console.log(` • AGENTS.md found (${existingAgents.length} chars)`);
|
|
134
|
-
console.log(` • Latest template loaded (${template.length} chars)`);
|
|
135
|
-
console.log(` • Project name: ${projectName}`);
|
|
136
|
-
|
|
137
|
-
// Check if AGENTS.md has documentation enforcement section
|
|
138
|
-
const hasDocEnforcement = existingAgents.includes('Documentation Rules');
|
|
139
|
-
console.log(` • Has documentation enforcement: ${hasDocEnforcement ? '✅' : '❌'}`);
|
|
140
|
-
|
|
141
|
-
// Check for CI workflow
|
|
142
|
-
const ciUpdated = updateCiWorkflow(cwd);
|
|
143
|
-
|
|
144
|
-
// Update AGENTS.md if it doesn't have the latest sections
|
|
145
|
-
if (!hasDocEnforcement) {
|
|
146
|
-
updateAgentsMd(existingAgents, template);
|
|
147
|
-
} else {
|
|
148
|
-
console.log(`✅ AGENTS.md already has documentation enforcement rules`);
|
|
250
|
+
|
|
251
|
+
if (planned.length === 0) {
|
|
252
|
+
console.log("No proposals needed. The project already satisfies the current update checks.");
|
|
253
|
+
return;
|
|
149
254
|
}
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
console.log(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
255
|
+
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log("Planned proposals:");
|
|
258
|
+
for (const item of planned) {
|
|
259
|
+
console.log(` - ${item.label} -> ${item.relativePath}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
158
262
|
if (dryRun) {
|
|
159
|
-
console.log(
|
|
263
|
+
console.log("");
|
|
264
|
+
console.log("Dry run only. No files were written.");
|
|
265
|
+
return;
|
|
160
266
|
}
|
|
267
|
+
|
|
268
|
+
console.log("");
|
|
269
|
+
for (const item of planned) {
|
|
270
|
+
const proposalPath = item.write();
|
|
271
|
+
console.log(`Wrote proposal: ${proposalPath}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log("Review the proposal files under .cx/proposals/ and merge the needed changes manually.");
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
164
|
-
main().catch(error => {
|
|
165
|
-
console.error(
|
|
279
|
+
main().catch((error) => {
|
|
280
|
+
console.error(`Error: ${error.message}`);
|
|
166
281
|
process.exit(1);
|
|
167
282
|
});
|
|
168
|
-
}
|
|
283
|
+
}
|