@geraldmaron/construct 1.0.21 → 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 +191 -13
- 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 +43 -15
- package/lib/comment-lint.mjs +115 -0
- package/lib/completions.mjs +7 -1
- package/lib/config/schema.mjs +4 -0
- package/lib/decisions/enforced-baseline.json +2 -0
- package/lib/docs-verify.mjs +15 -18
- package/lib/doctor/cli.mjs +8 -1
- package/lib/document-export.mjs +124 -0
- package/lib/embed/daemon.mjs +1 -1
- package/lib/embed/inbox.mjs +25 -7
- 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/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/runtime.mjs +70 -9
- package/lib/orchestration/worker.mjs +69 -21
- package/lib/orchestration-policy.mjs +27 -3
- package/lib/parity.mjs +46 -24
- 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
|
@@ -14,11 +14,18 @@
|
|
|
14
14
|
* - auto: removed when --yes is passed; included in the interactive list pre-checked
|
|
15
15
|
* - ask : never auto-removed; interactive list shows them unchecked
|
|
16
16
|
*
|
|
17
|
+
* Per ADR-0027 §Consequences, shared Docker images stay by default because
|
|
18
|
+
* other projects may use the same pgvector base; the pgvector image is removed
|
|
19
|
+
* only under --with-images. The LaunchAgent registration and a Construct-set
|
|
20
|
+
* `core.hooksPath` are reversed because they are state install/init created and
|
|
21
|
+
* own; a user-customized hooksPath is left untouched.
|
|
22
|
+
*
|
|
17
23
|
* Flags:
|
|
18
24
|
* --dry-run print the plan, change nothing
|
|
19
25
|
* --yes non-interactive; remove auto-risk items, skip ask-risk items
|
|
20
26
|
* --yes --all non-interactive; remove both auto- and ask-risk items
|
|
21
27
|
* --keep-state only remove project-scope launcher + adapters; preserve .cx/, ~/.construct, Postgres
|
|
28
|
+
* --with-images also remove the shared pgvector Docker image (off by default; ADR-0027)
|
|
22
29
|
* --scope=project|machine|all limit which categories are evaluated (default: all)
|
|
23
30
|
*/
|
|
24
31
|
|
|
@@ -30,11 +37,15 @@ import { fileURLToPath } from 'node:url';
|
|
|
30
37
|
|
|
31
38
|
import { multiSelect } from '../tty-prompts.mjs';
|
|
32
39
|
import { getUserConfigDir, getUserEnvPath } from '../env-config.mjs';
|
|
40
|
+
import { removeTomlTables, tomlString } from '../codex-config.mjs';
|
|
33
41
|
|
|
34
42
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
35
43
|
const PKG_ROOT = path.resolve(__dirname, '..', '..');
|
|
36
44
|
|
|
37
45
|
const POSTGRES_CONTAINER = 'construct-postgres';
|
|
46
|
+
const PGVECTOR_IMAGE = 'pgvector/pgvector:pg16';
|
|
47
|
+
const PRESSURE_GUARD_LABEL = 'dev.construct.pressure-release';
|
|
48
|
+
const MEMORY_MCP_KEYS = ['memory', 'cass'];
|
|
38
49
|
|
|
39
50
|
export function parseArgs(argv) {
|
|
40
51
|
const args = {
|
|
@@ -42,6 +53,7 @@ export function parseArgs(argv) {
|
|
|
42
53
|
yes: false,
|
|
43
54
|
all: false,
|
|
44
55
|
keepState: false,
|
|
56
|
+
withImages: false,
|
|
45
57
|
scope: 'all',
|
|
46
58
|
home: os.homedir(),
|
|
47
59
|
cwd: process.cwd(),
|
|
@@ -51,6 +63,7 @@ export function parseArgs(argv) {
|
|
|
51
63
|
else if (arg === '--yes' || arg === '-y') args.yes = true;
|
|
52
64
|
else if (arg === '--all') args.all = true;
|
|
53
65
|
else if (arg === '--keep-state') args.keepState = true;
|
|
66
|
+
else if (arg === '--with-images') args.withImages = true;
|
|
54
67
|
else if (arg.startsWith('--scope=')) args.scope = arg.slice('--scope='.length);
|
|
55
68
|
else if (arg.startsWith('--home=')) args.home = arg.slice('--home='.length);
|
|
56
69
|
else if (arg.startsWith('--cwd=')) args.cwd = arg.slice('--cwd='.length);
|
|
@@ -61,7 +74,7 @@ export function parseArgs(argv) {
|
|
|
61
74
|
return args;
|
|
62
75
|
}
|
|
63
76
|
|
|
64
|
-
function buildCategories({ cwd, home }) {
|
|
77
|
+
function buildCategories({ cwd, home, withImages = false }) {
|
|
65
78
|
const dotConstruct = path.join(cwd, '.construct');
|
|
66
79
|
const dotClaudeAgents = path.join(cwd, '.claude', 'agents');
|
|
67
80
|
const dotClaudeCommands = path.join(cwd, '.claude', 'commands');
|
|
@@ -77,6 +90,11 @@ function buildCategories({ cwd, home }) {
|
|
|
77
90
|
const userWorkspace = path.join(userConstructDir, 'workspace');
|
|
78
91
|
const userPgComposeDir = path.join(userConstructDir, 'services', 'postgres');
|
|
79
92
|
const userCxDir = path.join(userHome, '.cx');
|
|
93
|
+
const launchAgentPlist = path.join(userHome, 'Library', 'LaunchAgents', `${PRESSURE_GUARD_LABEL}.plist`);
|
|
94
|
+
const claudeSettings = path.join(userHome, '.claude', 'settings.json');
|
|
95
|
+
const opencodeConfig = path.join(userHome, '.config', 'opencode', 'opencode.json');
|
|
96
|
+
const codexConfig = path.join(userHome, '.codex', 'config.toml');
|
|
97
|
+
const memoryConfigPaths = { claudeSettings, opencodeConfig, codexConfig };
|
|
80
98
|
|
|
81
99
|
return [
|
|
82
100
|
{
|
|
@@ -209,12 +227,49 @@ function buildCategories({ cwd, home }) {
|
|
|
209
227
|
describe: () => 'Removes the local docker-compose.yml. Run after removing the container.',
|
|
210
228
|
execute: () => removePath(userPgComposeDir),
|
|
211
229
|
},
|
|
230
|
+
{
|
|
231
|
+
id: 'project-git-hookspath',
|
|
232
|
+
scope: 'project',
|
|
233
|
+
risk: 'auto',
|
|
234
|
+
label: 'git core.hooksPath (.beads/hooks)',
|
|
235
|
+
detect: () => gitHooksPathIsConstruct(cwd),
|
|
236
|
+
describe: () => 'Unsets core.hooksPath set by `construct init` to .beads/hooks. A custom hooksPath is left untouched.',
|
|
237
|
+
execute: () => unsetConstructGitHooksPath(cwd),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'machine-launchagent',
|
|
241
|
+
scope: 'machine',
|
|
242
|
+
risk: 'auto',
|
|
243
|
+
label: `LaunchAgent ${PRESSURE_GUARD_LABEL} (unload + plist)`,
|
|
244
|
+
detect: () => process.platform === 'darwin' && existsAny(launchAgentPlist),
|
|
245
|
+
describe: () => `Unregisters the pressure-release LaunchAgent (launchctl bootout) and removes ${rel(userHome, launchAgentPlist)}.`,
|
|
246
|
+
execute: () => removePressureGuardLaunchAgent(launchAgentPlist),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'machine-memory-mcp',
|
|
250
|
+
scope: 'machine',
|
|
251
|
+
risk: 'auto',
|
|
252
|
+
label: 'memory MCP registration (Claude, OpenCode, Codex)',
|
|
253
|
+
detect: () => memoryMcpRegistered(memoryConfigPaths),
|
|
254
|
+
describe: () => 'Strips the Construct memory MCP bridge from ~/.claude/settings.json, ~/.config/opencode/opencode.json, and ~/.codex/config.toml. Preserves all other entries.',
|
|
255
|
+
execute: () => removeMemoryMcp(memoryConfigPaths),
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'machine-pgvector-image',
|
|
259
|
+
scope: 'machine',
|
|
260
|
+
risk: 'ask',
|
|
261
|
+
optIn: true,
|
|
262
|
+
label: `Docker image "${PGVECTOR_IMAGE}" (--with-images only)`,
|
|
263
|
+
detect: () => withImages && dockerImageExists(PGVECTOR_IMAGE),
|
|
264
|
+
describe: () => `Removes the cached ${PGVECTOR_IMAGE} image. Off unless --with-images, since other projects may share it (ADR-0027).`,
|
|
265
|
+
execute: () => removeDockerImage(PGVECTOR_IMAGE),
|
|
266
|
+
},
|
|
212
267
|
];
|
|
213
268
|
}
|
|
214
269
|
|
|
215
270
|
export async function runUninstall(rawArgs = []) {
|
|
216
271
|
const args = parseArgs(rawArgs);
|
|
217
|
-
const categories = buildCategories({ cwd: args.cwd, home: args.home });
|
|
272
|
+
const categories = buildCategories({ cwd: args.cwd, home: args.home, withImages: args.withImages });
|
|
218
273
|
|
|
219
274
|
const inScope = categories.filter((cat) => {
|
|
220
275
|
if (args.scope !== 'all' && cat.scope !== args.scope) return false;
|
|
@@ -235,7 +290,7 @@ export async function runUninstall(rawArgs = []) {
|
|
|
235
290
|
}
|
|
236
291
|
|
|
237
292
|
if (args.yes) {
|
|
238
|
-
toRemove = inScope.filter((cat) => cat.risk === 'auto' || args.all).map((cat) => cat.id);
|
|
293
|
+
toRemove = inScope.filter((cat) => cat.risk === 'auto' || cat.optIn || args.all).map((cat) => cat.id);
|
|
239
294
|
} else if (!process.stdin.isTTY) {
|
|
240
295
|
console.error('[uninstall] No TTY available. Re-run with --yes (auto-risk only) or --yes --all (everything).');
|
|
241
296
|
process.exit(2);
|
|
@@ -275,9 +330,9 @@ async function promptCategories(categories) {
|
|
|
275
330
|
const options = categories.map((cat) => ({
|
|
276
331
|
label: cat.label,
|
|
277
332
|
value: cat.id,
|
|
278
|
-
checked: cat.risk === 'auto',
|
|
333
|
+
checked: cat.risk === 'auto' || Boolean(cat.optIn),
|
|
279
334
|
description: cat.describe(),
|
|
280
|
-
meta: cat.risk === 'auto' ? 'pre-checked' : 'ask',
|
|
335
|
+
meta: cat.risk === 'auto' || cat.optIn ? 'pre-checked' : 'ask',
|
|
281
336
|
}));
|
|
282
337
|
try {
|
|
283
338
|
return await multiSelect({
|
|
@@ -294,7 +349,7 @@ async function promptCategories(categories) {
|
|
|
294
349
|
function printPlan(categories, args) {
|
|
295
350
|
console.log(`[uninstall] dry-run plan (scope=${args.scope}${args.keepState ? ', keep-state' : ''}):`);
|
|
296
351
|
for (const cat of categories) {
|
|
297
|
-
const mark = cat.risk === 'auto' ? '✓' : '◐';
|
|
352
|
+
const mark = cat.risk === 'auto' || cat.optIn ? '✓' : '◐';
|
|
298
353
|
console.log(` ${mark} ${cat.label}`);
|
|
299
354
|
console.log(` ${cat.describe()}`);
|
|
300
355
|
}
|
|
@@ -311,7 +366,7 @@ function printFollowups() {
|
|
|
311
366
|
console.log('');
|
|
312
367
|
console.log('Follow-ups (run by hand if you want):');
|
|
313
368
|
console.log(' npm uninstall @geraldmaron/construct # drop the package itself');
|
|
314
|
-
console.log('
|
|
369
|
+
console.log(' construct uninstall --with-images # also drop the shared pgvector image (kept by default)');
|
|
315
370
|
console.log(' brew uninstall cm cass # if installed by `construct init`');
|
|
316
371
|
}
|
|
317
372
|
|
|
@@ -421,3 +476,121 @@ function removeDockerContainer(name) {
|
|
|
421
476
|
}
|
|
422
477
|
return stop.status === 0 ? 'stopped and removed (with volume)' : 'removed (with volume)';
|
|
423
478
|
}
|
|
479
|
+
|
|
480
|
+
function dockerImageExists(image) {
|
|
481
|
+
const probe = spawnSync('docker', ['image', 'inspect', image], {
|
|
482
|
+
encoding: 'utf8',
|
|
483
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
484
|
+
});
|
|
485
|
+
return probe.status === 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function removeDockerImage(image) {
|
|
489
|
+
const rm = spawnSync('docker', ['rmi', image], { encoding: 'utf8' });
|
|
490
|
+
if (rm.status !== 0) {
|
|
491
|
+
throw new Error((rm.stderr || '').trim() || 'docker rmi failed');
|
|
492
|
+
}
|
|
493
|
+
return `removed image ${image}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// core.hooksPath is reversed only when it still points at the value install/init
|
|
497
|
+
// set (.beads/hooks); a user who repointed it owns that choice and is left alone.
|
|
498
|
+
|
|
499
|
+
function gitHooksPathIsConstruct(cwd) {
|
|
500
|
+
const inGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
501
|
+
if (inGit.status !== 0) return false;
|
|
502
|
+
const current = spawnSync('git', ['config', '--get', 'core.hooksPath'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
503
|
+
if (current.status !== 0) return false;
|
|
504
|
+
return (current.stdout || '').trim() === '.beads/hooks';
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function unsetConstructGitHooksPath(cwd) {
|
|
508
|
+
if (!gitHooksPathIsConstruct(cwd)) return 'core.hooksPath not set to .beads/hooks';
|
|
509
|
+
const result = spawnSync('git', ['config', '--unset', 'core.hooksPath'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
510
|
+
if (result.status !== 0) {
|
|
511
|
+
throw new Error((result.stderr || '').trim() || 'git config --unset failed');
|
|
512
|
+
}
|
|
513
|
+
return 'unset core.hooksPath';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// launchctl bootout takes the GUI domain target; unload is the legacy fallback
|
|
517
|
+
// for older macOS. Either deregisters the agent before the plist is deleted so a
|
|
518
|
+
// reinstall starts clean.
|
|
519
|
+
|
|
520
|
+
function removePressureGuardLaunchAgent(plistPath) {
|
|
521
|
+
if (!existsAny(plistPath)) return 'no LaunchAgent plist';
|
|
522
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
523
|
+
if (process.platform === 'darwin') {
|
|
524
|
+
if (uid != null) {
|
|
525
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}/${PRESSURE_GUARD_LABEL}`], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
526
|
+
}
|
|
527
|
+
spawnSync('launchctl', ['unload', plistPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
528
|
+
}
|
|
529
|
+
const removed = removePath(plistPath);
|
|
530
|
+
return removed ? 'unregistered and removed plist' : 'unregistered (plist already gone)';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function memoryMcpRegistered({ claudeSettings, opencodeConfig, codexConfig }) {
|
|
534
|
+
return claudeHasMemoryMcp(claudeSettings)
|
|
535
|
+
|| opencodeHasMemoryMcp(opencodeConfig)
|
|
536
|
+
|| codexHasMemoryMcp(codexConfig);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function claudeHasMemoryMcp(filePath) {
|
|
540
|
+
const settings = readJsonOrNull(filePath);
|
|
541
|
+
if (!settings || !settings.mcpServers) return false;
|
|
542
|
+
return MEMORY_MCP_KEYS.some((key) => key in settings.mcpServers);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function opencodeHasMemoryMcp(filePath) {
|
|
546
|
+
const config = readJsonOrNull(filePath);
|
|
547
|
+
if (!config || !config.mcp) return false;
|
|
548
|
+
return MEMORY_MCP_KEYS.some((key) => key in config.mcp);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function codexHasMemoryMcp(filePath) {
|
|
552
|
+
if (!fs.existsSync(filePath)) return false;
|
|
553
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
554
|
+
return MEMORY_MCP_KEYS.some((key) => text.includes(`[mcp_servers.${key}]`) || text.includes(`[mcp_servers.${tomlString(key)}]`));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// The memory MCP is registered into all three host configs by `construct mcp add
|
|
558
|
+
// memory` during install; each host is stripped independently so a missing or
|
|
559
|
+
// hand-edited config never blocks the others.
|
|
560
|
+
|
|
561
|
+
function removeMemoryMcp({ claudeSettings, opencodeConfig, codexConfig }) {
|
|
562
|
+
const stripped = [];
|
|
563
|
+
if (stripMemoryFromJsonMcp(claudeSettings, 'mcpServers')) stripped.push('claude');
|
|
564
|
+
if (stripMemoryFromJsonMcp(opencodeConfig, 'mcp')) stripped.push('opencode');
|
|
565
|
+
if (stripMemoryFromCodex(codexConfig)) stripped.push('codex');
|
|
566
|
+
return stripped.length ? `stripped from ${stripped.join(', ')}` : 'nothing to strip';
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function stripMemoryFromJsonMcp(filePath, mcpKey) {
|
|
570
|
+
const config = readJsonOrNull(filePath);
|
|
571
|
+
if (!config || !config[mcpKey]) return false;
|
|
572
|
+
let changed = false;
|
|
573
|
+
for (const key of MEMORY_MCP_KEYS) {
|
|
574
|
+
if (key in config[mcpKey]) { delete config[mcpKey][key]; changed = true; }
|
|
575
|
+
}
|
|
576
|
+
if (!changed) return false;
|
|
577
|
+
if (Object.keys(config[mcpKey]).length === 0) delete config[mcpKey];
|
|
578
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
579
|
+
return true;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function stripMemoryFromCodex(filePath) {
|
|
583
|
+
if (!fs.existsSync(filePath)) return false;
|
|
584
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
585
|
+
const tables = MEMORY_MCP_KEYS.flatMap((key) => [`mcp_servers.${key}`, `mcp_servers.${tomlString(key)}`]);
|
|
586
|
+
const cleaned = removeTomlTables(existing, tables);
|
|
587
|
+
if (cleaned === existing.trimEnd()) return false;
|
|
588
|
+
fs.writeFileSync(filePath, `${cleaned.trimEnd()}\n`, 'utf8');
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function readJsonOrNull(filePath) {
|
|
593
|
+
if (!fs.existsSync(filePath)) return null;
|
|
594
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); }
|
|
595
|
+
catch { return null; }
|
|
596
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geraldmaron/construct",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "npm@11.5.1",
|
|
6
6
|
"description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"release:check": "node ./bin/construct doctor && npm test && node ./bin/construct docs:update --check && node ./bin/construct dashboard:sync --check && node ./bin/construct lint:comments && node scripts/lint-commits-pr.mjs",
|
|
74
74
|
"release:gate": "node --test tests/functional/release-gate.functional.test.mjs",
|
|
75
75
|
"audit:published": "node scripts/audit-published-artifact.mjs",
|
|
76
|
+
"version": "node scripts/sync-construct-version.mjs && git add .construct/version",
|
|
76
77
|
"release:preflight": "node scripts/pre-release-check.mjs",
|
|
77
78
|
"release:preflight:no-auth": "node scripts/pre-release-check.mjs --skip-auth",
|
|
78
79
|
"npm:publish": "npm run release:check && npm publish --access public",
|
|
@@ -81,7 +82,6 @@
|
|
|
81
82
|
"dependencies": {
|
|
82
83
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
83
84
|
"js-yaml": "^4.2.0",
|
|
84
|
-
"node-webvtt": "^1.9.3",
|
|
85
85
|
"postgres": "^3.4.9"
|
|
86
86
|
},
|
|
87
87
|
"optionalDependencies": {
|
package/personas/construct.md
CHANGED
|
@@ -25,20 +25,15 @@ Use the single-writer rule whenever multiple sessions are active: if two session
|
|
|
25
25
|
|
|
26
26
|
## Classify before acting
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Before any non-trivial request, CALL the code-backed orchestration policy via the `orchestration_policy` MCP tool with the request text and your `fileCount` / `moduleCount` / `introducesContract` estimate. Do not classify from memory. Honor the returned `track` and `specialists`. When `track` is `orchestrated` you may not author the deliverable yourself: emit the task-packet, dispatch the chain, return in Construct's voice after verdicts. Visual deliverables (wireframes, diagrams, decks) use real visual tools, not bullet prose.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- **Immediate**. answer or act directly when no hidden worker is needed.
|
|
32
|
-
- **Focused**. dispatch one bounded specialist path; return in Construct's voice.
|
|
33
|
-
- **Orchestrated**. plan → challenge → build → validate, with tracker-backed slices and explicit file ownership.
|
|
34
|
-
|
|
35
|
-
Devil's advocate is mandatory for new architectural directions, AI/agent workflow changes, security or data-integrity changes, and promoting a temporary capability to persistent.
|
|
30
|
+
Tracks: immediate (act directly), focused (one bounded specialist), orchestrated (plan → challenge → build → validate, tracker-backed). cx-devil-advocate is mandatory whenever `riskFlags` include architecture, security, dataIntegrity, or ai.
|
|
36
31
|
|
|
37
32
|
Orchestrated dispatches emit a task-packet with `goal`, `intent`, `workCategory`, `riskFlags`, `acceptanceCriteria` before naming specialists (`specialists/contracts.json:construct-to-orchestrator`).
|
|
38
33
|
|
|
39
34
|
## Gates and contracts (org-in-a-box)
|
|
40
35
|
|
|
41
|
-
`
|
|
36
|
+
`orchestration_policy` returns three artifacts; honor all three:
|
|
42
37
|
|
|
43
38
|
1. **Gates**. `framingChallenge`, `externalResearch`, `docAuthoring`
|
|
44
39
|
2. **Contract chain**. typed handoffs from `specialists/contracts.json`. Call `agent_contract` MCP tool at handoff.
|
|
@@ -67,6 +62,10 @@ The active profile (`construct profile show`) sets the intake taxonomy. Session-
|
|
|
67
62
|
|
|
68
63
|
Lead with the answer. One question when blocked. Confirm what changed when done.
|
|
69
64
|
|
|
65
|
+
**Output style**: format human-facing output (terminal, prose, dashboard) for neurodivergent readers — answer first, clear hierarchy, scannable chunks, plain language, explicit next step; never rely on color or motion alone; honor `NO_COLOR` and reduced-motion. Presentation only — never reshape machine-readable output (`--json`, parsed tokens, registries, contracts). See `rules/common/neurodivergent-output.md`.
|
|
66
|
+
|
|
67
|
+
**Tool invisibility**: deliverables are about the user's project, never Construct. Never name Construct, `cx-*` role ids, or internal orchestration mechanics in artifact content unless the subject project is Construct itself. Provenance goes in a comment, not the prose. See `rules/common/tool-invisibility.md`.
|
|
68
|
+
|
|
70
69
|
Non-trivial work: update Beads (`bd note <id>`), `plan.md`, docs with owner / acceptance / verification. Preserve tracker ids in handoffs. Surface NEEDS_MAIN_INPUT in your voice; resume after the answer. End every session with a handoff at `.cx/handoffs/{date}-{slug}.md` and updates to `.cx/context.md`.
|
|
71
70
|
|
|
72
71
|
Load-bearing state: `AGENTS.md`, `.cx/context.md`/`.json`, `docs/README.md`, `docs/architecture.md` (read at session start, update before DONE, prune stale sections). `plan.md` is local-only.
|
|
@@ -30,6 +30,18 @@
|
|
|
30
30
|
"id": "pre:policy-engine",
|
|
31
31
|
"description": "Consolidated policy gating (bootstrap rule): block mutations until session is grounded via project_context+memory_search or 3+ exploratory reads"
|
|
32
32
|
},
|
|
33
|
+
{
|
|
34
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "node \"$HOME/.construct/lib/hooks/orchestration-dispatch-guard.mjs\"",
|
|
39
|
+
"timeout": 5
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"id": "pre:edit:dispatch-guard",
|
|
43
|
+
"description": "Block solo-authoring a substantial deliverable when orchestration_policy classified the request as orchestrated and no specialist dispatch has run yet"
|
|
44
|
+
},
|
|
33
45
|
{
|
|
34
46
|
"matcher": "Bash",
|
|
35
47
|
"hooks": [
|
|
@@ -304,6 +316,18 @@
|
|
|
304
316
|
],
|
|
305
317
|
"id": "post:audit-trail",
|
|
306
318
|
"description": "Append-only audit log of every mutation — attribution (agent/task) and tamper-evidence chain"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"matcher": "mcp__.*__orchestration_policy|mcp__.*__orchestration_run|mcp__.*__worker_run|Task",
|
|
322
|
+
"hooks": [
|
|
323
|
+
{
|
|
324
|
+
"type": "command",
|
|
325
|
+
"command": "node \"$HOME/.construct/lib/hooks/orchestration-dispatch-guard.mjs\"",
|
|
326
|
+
"timeout": 5
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
"id": "post:orchestration:dispatch-guard",
|
|
330
|
+
"description": "Record orchestration_policy verdict and specialist dispatch so the PreToolUse guard can tell an orchestrated-but-undispatched deliverable write from legitimate post-dispatch synthesis"
|
|
307
331
|
}
|
|
308
332
|
],
|
|
309
333
|
"PostToolUseFailure": [
|
|
@@ -392,11 +416,12 @@
|
|
|
392
416
|
{
|
|
393
417
|
"type": "command",
|
|
394
418
|
"command": "node \"$HOME/.construct/lib/hooks/stop-typecheck.mjs\"",
|
|
395
|
-
"timeout": 120
|
|
419
|
+
"timeout": 120,
|
|
420
|
+
"async": true
|
|
396
421
|
}
|
|
397
422
|
],
|
|
398
423
|
"id": "stop:typecheck",
|
|
399
|
-
"description": "Batch TypeScript typecheck of edited files at end of response"
|
|
424
|
+
"description": "Batch TypeScript typecheck of edited files at end of response (async: writes ~/.cx/pending-typecheck.txt for the next session-start to surface; does not gate the current turn)"
|
|
400
425
|
},
|
|
401
426
|
{
|
|
402
427
|
"matcher": "*",
|
|
@@ -430,11 +455,12 @@
|
|
|
430
455
|
{
|
|
431
456
|
"type": "command",
|
|
432
457
|
"command": "node \"$HOME/.construct/lib/hooks/pre-compact.mjs\"",
|
|
433
|
-
"timeout": 15
|
|
458
|
+
"timeout": 15,
|
|
459
|
+
"async": true
|
|
434
460
|
}
|
|
435
461
|
],
|
|
436
462
|
"id": "stop:snapshot",
|
|
437
|
-
"description": "Snapshot .cx/context.md at session end so resume has real progress"
|
|
463
|
+
"description": "Snapshot .cx/context.md at session end so resume has real progress (async: written for the NEXT session to read; does not gate the current turn)"
|
|
438
464
|
},
|
|
439
465
|
{
|
|
440
466
|
"matcher": "*",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"options": {
|
|
21
21
|
"baseURL": "https://openrouter.ai/api/v1",
|
|
22
22
|
"headers": {
|
|
23
|
-
"HTTP-Referer": "
|
|
24
|
-
"X-Title": "
|
|
23
|
+
"HTTP-Referer": "https://github.com/geraldmaron/construct",
|
|
24
|
+
"X-Title": "Construct",
|
|
25
25
|
"Authorization": "Bearer __OPENROUTER_API_KEY__"
|
|
26
26
|
}
|
|
27
27
|
},
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: every human-facing Construct output is formatted for neurodivergent readers — without reshaping machine-readable output.
|
|
3
|
+
enforced_by: (persona prompt), tests/term-format.test.mjs, construct dashboard:sync --check
|
|
4
|
+
precedence_tier: style
|
|
5
|
+
---
|
|
6
|
+
# Neurodivergent-Friendly Output
|
|
7
|
+
|
|
8
|
+
Construct's output is read under load — in a terminal, mid-task, often by people who process text differently. Dense walls of prose, buried conclusions, inconsistent structure, and meaning carried only by color cost every reader attention, and cost neurodivergent readers more. This rule applies to **every human-facing output** Construct produces: terminal output, agent prose to the user, reports, error messages, and the dashboard.
|
|
9
|
+
|
|
10
|
+
It is a `style` rule by design. The precedence resolver (`rules/common/precedence.md`) guarantees a style rule never overrides a higher tier — so when accessible formatting would conflict with correct machine output, correctness wins automatically. Section 0 states that boundary explicitly.
|
|
11
|
+
|
|
12
|
+
## 0. The human/machine boundary (read this first)
|
|
13
|
+
|
|
14
|
+
Two layers, treated differently:
|
|
15
|
+
|
|
16
|
+
- **Presentation layer (human-facing)** — section titles, prose, help text, report narratives, guidance notes, error wording, dashboard markup and CSS. This is what the rest of this rule shapes.
|
|
17
|
+
- **Data layer (machine-readable)** — never reshape it for readability. Off-limits: any `--json` / `--plain` output; `specialists/registry.json`, `contracts.json`, `role-manifests.json`; the parsed tokens hooks emit (for example the session-start `## Working branch: **<name>**` line, the efficiency status enum `degraded` / `configured` / `healthy`, commit hashes, counts); auxiliary state JSON; any text one component parses out of another.
|
|
18
|
+
|
|
19
|
+
When a format choice would change a value, key, ordering, or token that something downstream parses, do not make it. Accessibility is presentation; it never edits the contract.
|
|
20
|
+
|
|
21
|
+
## 1. Lead with the answer
|
|
22
|
+
|
|
23
|
+
State the conclusion first, then the support. The reader should get the outcome — what happened, what is needed, what changed — in the first line, before any reasoning. Put deliberation after the answer, or leave it out.
|
|
24
|
+
|
|
25
|
+
## 2. One clear hierarchy
|
|
26
|
+
|
|
27
|
+
Use headings in order (H2, then H3) and do not skip or nest deeper. One `h1`-equivalent per surface. A reader should be able to scan headings alone and know the shape of the output.
|
|
28
|
+
|
|
29
|
+
## 3. Scannable chunks
|
|
30
|
+
|
|
31
|
+
Short paragraphs. Bulleted or numbered lists in place of long sentences that pack several facts together. White space between sections. Prefer three short lines over one dense one.
|
|
32
|
+
|
|
33
|
+
## 4. Plain, literal language
|
|
34
|
+
|
|
35
|
+
Say the thing directly. Avoid idioms, sarcasm, irony, and figurative phrasing that require inference to decode. Define a term on first use or link to where it is defined. One name for one concept — do not alternate synonyms for the same thing.
|
|
36
|
+
|
|
37
|
+
## 5. Consistent structure and terminology
|
|
38
|
+
|
|
39
|
+
The same kind of output keeps the same shape every time. Status reports, error messages, and command help follow a predictable order so the reader learns the pattern once. Predictability lowers cognitive load more than cleverness raises it.
|
|
40
|
+
|
|
41
|
+
## 6. Explicit state and next step
|
|
42
|
+
|
|
43
|
+
End human-facing output with the current state and the next action in plain words — what is done, what is blocked, what the reader does next. Never bury the ask. Never imply a next step through tone.
|
|
44
|
+
|
|
45
|
+
## 7. Never let color, emoji, or motion be the only signal
|
|
46
|
+
|
|
47
|
+
- Meaning carried by color must also be carried by text, shape, or label. A red number is also labelled `error`; a green check is also the word `passed`.
|
|
48
|
+
- Honor `NO_COLOR`, non-TTY, and `TERM=dumb`: degrade to plain text, never to nothing.
|
|
49
|
+
- Respect terminal width — wrap rather than overflow.
|
|
50
|
+
- On the dashboard, honor `prefers-reduced-motion` and `prefers-color-scheme`; motion is never required to understand state, and animation can be fully disabled.
|
|
51
|
+
|
|
52
|
+
## Applies to
|
|
53
|
+
|
|
54
|
+
- **Terminal / CLI** — help text, status, reports, hook display. Routed through the shared formatter (`lib/term-format.mjs`); color and width handled there, never per-call.
|
|
55
|
+
- **Agent prose** — every response Construct and its specialists return to the user.
|
|
56
|
+
- **Dashboard** — `apps/dashboard/` source (semantic landmarks, labels, focus, contrast, reduced-motion), rebuilt into `lib/server/static/`.
|
|
57
|
+
|
|
58
|
+
## Enforcement
|
|
59
|
+
|
|
60
|
+
- Persona prompt: `personas/construct.md` carries the output-style directive; specialists inherit the standard through the front-door agent.
|
|
61
|
+
- `tests/term-format.test.mjs` asserts the terminal formatter honors `NO_COLOR` / non-TTY and that machine-readable `--json` output is unchanged by presentation changes.
|
|
62
|
+
- `construct dashboard:sync --check` keeps the built dashboard in step with the accessible source.
|
|
63
|
+
|
|
64
|
+
## Bypass
|
|
65
|
+
|
|
66
|
+
There is no bypass. If a format choice and a machine contract conflict, the contract wins (Section 0) — change the presentation, never the data. If this rule is wrong-shaped, fix the rule, not the output.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: deliverable artifacts are about the user's project, never about Construct or its internal machinery.
|
|
3
|
+
enforced_by: (persona prompt), lib/comment-lint.mjs
|
|
4
|
+
precedence_tier: correctness
|
|
5
|
+
---
|
|
6
|
+
# Tool Invisibility
|
|
7
|
+
|
|
8
|
+
Construct is scaffolding. The artifacts it helps produce — strategies, PRDs, ADRs, research briefs, memos, knowledge notes — belong to the user and their project. The user's document must carry no fingerprints of the tool that built it. A strategy authored through Construct should read exactly as if the user wrote it about their own product, with zero trace of the orchestration underneath.
|
|
9
|
+
|
|
10
|
+
This failed once, instructively: a strategy for a third-party project came back saying "Construct's bet is to own the layer…" and named `cx-product-manager` as a metric owner. The tooling had written itself into the user's deliverable. That is the failure this rule prevents.
|
|
11
|
+
|
|
12
|
+
## 1. The artifact is about its subject, not its author
|
|
13
|
+
|
|
14
|
+
A deliverable is about the project it concerns. Do not make Construct — or the act of using Construct — the subject of content it produces for another project. When the strategy is for *their* platform, the bets are *theirs*, not "Construct's."
|
|
15
|
+
|
|
16
|
+
## 2. Never name the tool or its internals in artifact content
|
|
17
|
+
|
|
18
|
+
In the body of any deliverable artifact, do not write:
|
|
19
|
+
|
|
20
|
+
- **`Construct`** as the product/company/subject (when the subject is not Construct).
|
|
21
|
+
- **`cx-*` specialist role ids** (`cx-business-strategist`, `cx-product-manager`, `cx-researcher`, …). These are internal routing detail — the persona already states that "internal routing and specialist dispatch are implementation detail." A metric owner is a real role on the user's team (e.g. "Product", "Eng lead"), never a `cx-*` id.
|
|
22
|
+
- **Internal orchestration mechanics** — task-packets, `orchestration_run`, dispatch chains, handoff contracts — as if they were part of the user's product.
|
|
23
|
+
|
|
24
|
+
Provenance belongs in a comment or a separate handoff, not in the deliverable's prose.
|
|
25
|
+
|
|
26
|
+
## 3. The one exception: when the subject *is* Construct
|
|
27
|
+
|
|
28
|
+
When the project being worked on is Construct itself (this repo, package `@geraldmaron/construct`), naming Construct and its specialists is correct and required — the artifact is genuinely about them. The deterministic check below is disabled for the Construct repo for exactly this reason.
|
|
29
|
+
|
|
30
|
+
## Enforcement
|
|
31
|
+
|
|
32
|
+
- **Prevention (persona + shared guidance):** every specialist and the Construct persona carry the invisibility directive, so the leak is avoided at generation time. This is the primary control; framing ("don't make the doc about Construct") is a judgment call the prompt owns.
|
|
33
|
+
- **Backstop (deterministic):** `lib/comment-lint.mjs` flags `cx-*` internal role-id tokens that appear in a *consuming project's* deliverable markdown — an unambiguous leak with near-zero false positives. The check is skipped when the working repo is Construct itself (package name `@geraldmaron/construct`). Severity follows `CONSTRUCT_ARTIFACT_LINT_MODE` (warn by default; block in the release gate).
|
|
34
|
+
|
|
35
|
+
## Bypass
|
|
36
|
+
|
|
37
|
+
There is no bypass. If the check fires on a legitimate use, the use is almost certainly a leak — rewrite the artifact to be about the user's project. If the subject genuinely is Construct, the self-repo skip already covers it.
|