@geraldmaron/construct 1.0.5 → 1.0.7
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 +24 -18
- package/agents/contracts.json +617 -2
- package/agents/contracts.schema.json +83 -0
- package/agents/prompts/cx-accessibility.md +5 -3
- package/agents/prompts/cx-ai-engineer.md +9 -7
- package/agents/prompts/cx-architect.md +8 -6
- package/agents/prompts/cx-business-strategist.md +8 -6
- package/agents/prompts/cx-data-analyst.md +9 -7
- package/agents/prompts/cx-data-engineer.md +5 -3
- package/agents/prompts/cx-debugger.md +8 -6
- package/agents/prompts/cx-designer.md +7 -5
- package/agents/prompts/cx-devil-advocate.md +6 -4
- package/agents/prompts/cx-docs-keeper.md +7 -5
- package/agents/prompts/cx-engineer.md +7 -5
- package/agents/prompts/cx-evaluator.md +4 -2
- package/agents/prompts/cx-explorer.md +10 -8
- package/agents/prompts/cx-legal-compliance.md +4 -2
- package/agents/prompts/cx-operations.md +7 -5
- package/agents/prompts/cx-orchestrator.md +12 -10
- package/agents/prompts/cx-platform-engineer.md +7 -5
- package/agents/prompts/cx-product-manager.md +6 -4
- package/agents/prompts/cx-qa.md +10 -8
- package/agents/prompts/cx-rd-lead.md +11 -9
- package/agents/prompts/cx-release-manager.md +7 -5
- package/agents/prompts/cx-researcher.md +24 -22
- package/agents/prompts/cx-reviewer.md +9 -7
- package/agents/prompts/cx-security.md +12 -10
- package/agents/prompts/cx-sre.md +9 -7
- package/agents/prompts/cx-test-automation.md +5 -3
- package/agents/prompts/cx-trace-reviewer.md +10 -8
- package/agents/prompts/cx-ux-researcher.md +5 -3
- package/agents/registry.json +8 -3
- package/bin/construct +444 -53
- package/commands/build/feature.md +4 -4
- package/commands/build/fix.md +8 -8
- package/commands/design/access.md +3 -3
- package/commands/design/flow.md +3 -3
- package/commands/design/ui.md +4 -4
- package/commands/measure/experiment.md +5 -5
- package/commands/measure/metrics.md +3 -3
- package/commands/measure/results.md +4 -4
- package/commands/plan/api.md +3 -3
- package/commands/plan/challenge.md +3 -3
- package/commands/plan/decide.md +3 -3
- package/commands/plan/feature.md +7 -7
- package/commands/plan/requirements.md +3 -3
- package/commands/remember/context.md +5 -5
- package/commands/remember/handoff.md +3 -3
- package/commands/remember/runbook.md +3 -3
- package/commands/review/code.md +8 -8
- package/commands/review/quality.md +4 -4
- package/commands/review/security.md +3 -3
- package/commands/ship/ready.md +3 -3
- package/commands/ship/release.md +3 -3
- package/commands/ship/status.md +4 -4
- package/commands/understand/docs.md +3 -3
- package/commands/understand/this.md +4 -4
- package/commands/understand/why.md +8 -8
- package/commands/work/clean.md +14 -14
- package/commands/work/drive.md +10 -10
- package/commands/work/optimize-prompts.md +9 -9
- package/commands/work/parallel-review.md +8 -8
- package/db/schema/006_graph.sql +24 -0
- package/examples/provider-plugin/README.md +7 -7
- package/examples/seed-observations/README.md +6 -6
- package/examples/seed-observations/anti-patterns.md +14 -14
- package/examples/seed-observations/decisions.md +4 -4
- package/examples/seed-observations/patterns.md +14 -14
- package/lib/auto-docs.mjs +13 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +2 -2
- package/lib/comment-lint.mjs +134 -0
- package/lib/contracts/validate.mjs +323 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/docs-verify.mjs +59 -6
- package/lib/doctor/cli.mjs +16 -1
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/watchers/consistency.mjs +310 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +211 -1
- package/lib/embed/cli.mjs +124 -3
- package/lib/embed/daemon.mjs +43 -4
- package/lib/embed/docs-lifecycle.mjs +1 -1
- package/lib/embed/inbox.mjs +2 -0
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/flavors/loader.mjs +1 -1
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/mcp-audit.mjs +2 -1
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/init-unified.mjs +55 -65
- package/lib/intake/classify.mjs +108 -24
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +6 -1
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/prepare.mjs +0 -1
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/graph.mjs +213 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +189 -1
- package/lib/mcp/tools/profile.mjs +270 -0
- package/lib/mcp/tools/workflow.mjs +25 -0
- package/lib/mcp-catalog.json +12 -8
- package/lib/mcp-platform-config.mjs +16 -8
- package/lib/migrations/index.mjs +106 -0
- package/lib/migrations/v1-baseline.mjs +33 -0
- package/lib/observation-store.mjs +9 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/reflect.mjs +49 -12
- package/lib/server/index.mjs +22 -28
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/telemetry/client.mjs +5 -1
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/package.json +4 -1
- package/personas/construct.md +3 -1
- package/platforms/claude/CLAUDE.md +6 -6
- package/rules/common/agents.md +2 -2
- package/rules/common/beads-hygiene.md +11 -11
- package/rules/common/code-review.md +1 -1
- package/rules/common/coding-style.md +1 -1
- package/rules/common/comments.md +8 -8
- package/rules/common/commit-approval.md +4 -4
- package/rules/common/cx-agent-routing.md +2 -2
- package/rules/common/cx-skill-routing.md +2 -2
- package/rules/common/development-workflow.md +3 -2
- package/rules/common/doc-ownership.md +2 -2
- package/rules/common/efficiency.md +3 -3
- package/rules/common/framing.md +1 -1
- package/rules/common/git-workflow.md +1 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +1 -1
- package/rules/common/release-gates.md +7 -7
- package/rules/common/research.md +4 -4
- package/rules/common/review-before-change.md +58 -0
- package/rules/common/security.md +1 -1
- package/rules/common/skill-composition.md +8 -8
- package/rules/common/testing.md +1 -1
- package/rules/golang/coding-style.md +2 -2
- package/rules/golang/hooks.md +1 -1
- package/rules/golang/patterns.md +1 -1
- package/rules/golang/security.md +1 -1
- package/rules/golang/testing.md +1 -1
- package/rules/python/coding-style.md +1 -1
- package/rules/python/hooks.md +1 -1
- package/rules/python/patterns.md +1 -1
- package/rules/python/security.md +1 -1
- package/rules/python/testing.md +1 -1
- package/rules/swift/coding-style.md +3 -3
- package/rules/swift/hooks.md +2 -2
- package/rules/swift/patterns.md +2 -2
- package/rules/swift/security.md +4 -4
- package/rules/swift/testing.md +2 -2
- package/rules/typescript/coding-style.md +1 -1
- package/rules/typescript/hooks.md +1 -1
- package/rules/typescript/patterns.md +1 -1
- package/rules/typescript/security.md +1 -1
- package/rules/typescript/testing.md +1 -1
- package/rules/web/coding-style.md +1 -1
- package/rules/web/design-quality.md +1 -1
- package/rules/web/hooks.md +1 -1
- package/rules/web/patterns.md +1 -1
- package/rules/web/performance.md +1 -1
- package/rules/web/security.md +1 -1
- package/rules/web/testing.md +1 -1
- package/scripts/sync-agents.mjs +45 -14
- package/skills/ai/agent-dev.md +1 -1
- package/skills/ai/llm-security.md +1 -1
- package/skills/ai/ml-ops.md +6 -6
- package/skills/ai/orchestration-workflow.md +1 -1
- package/skills/ai/prompt-and-eval.md +1 -1
- package/skills/ai/prompt-optimizer.md +13 -13
- package/skills/ai/rag-system.md +1 -1
- package/skills/architecture/api-design.md +1 -1
- package/skills/architecture/caching.md +1 -1
- package/skills/architecture/cloud-native.md +1 -1
- package/skills/architecture/message-queue.md +1 -1
- package/skills/architecture/security-arch.md +1 -1
- package/skills/compliance/ai-disclosure.md +1 -1
- package/skills/compliance/data-privacy.md +1 -1
- package/skills/compliance/license-audit.md +2 -2
- package/skills/compliance/regulatory-review.md +1 -1
- package/skills/development/cpp.md +1 -1
- package/skills/development/go.md +1 -1
- package/skills/development/java.md +1 -1
- package/skills/development/kotlin.md +9 -9
- package/skills/development/mobile-crossplatform.md +13 -13
- package/skills/development/python.md +1 -1
- package/skills/development/rust.md +1 -1
- package/skills/development/shell.md +1 -1
- package/skills/development/swift.md +6 -6
- package/skills/development/typescript.md +1 -1
- package/skills/devops/ci-cd.md +5 -5
- package/skills/devops/containerization.md +9 -9
- package/skills/devops/cost-optimization.md +1 -1
- package/skills/devops/data-engineering.md +2 -2
- package/skills/devops/database.md +1 -1
- package/skills/devops/dependency-management.md +3 -3
- package/skills/devops/devsecops.md +1 -1
- package/skills/devops/git-workflow.md +1 -1
- package/skills/devops/incident-response.md +18 -18
- package/skills/devops/monorepo.md +5 -5
- package/skills/devops/observability.md +1 -1
- package/skills/devops/performance.md +1 -1
- package/skills/devops/testing.md +1 -1
- package/skills/docs/adr-workflow.md +2 -2
- package/skills/docs/backlog-proposal-workflow.md +1 -1
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/document-ingest-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +1 -1
- package/skills/docs/init-docs.md +15 -15
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +3 -3
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +1 -1
- package/skills/docs/product-signal-workflow.md +9 -9
- package/skills/docs/research-workflow.md +10 -10
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/strategy-workflow.md +3 -3
- package/skills/exploration/repo-map.md +11 -11
- package/skills/frameworks/django.md +15 -15
- package/skills/frameworks/nextjs.md +16 -16
- package/skills/frameworks/react.md +12 -12
- package/skills/frameworks/spring-boot.md +12 -12
- package/skills/frontend-design/accessibility.md +6 -6
- package/skills/frontend-design/component-patterns.md +1 -1
- package/skills/frontend-design/engineering.md +1 -1
- package/skills/frontend-design/state-management.md +1 -1
- package/skills/frontend-design/ui-aesthetics.md +1 -1
- package/skills/frontend-design/ux-principles.md +1 -1
- package/skills/operating/orchestration-reference.md +27 -27
- package/skills/quality-gates/review-work.md +3 -3
- package/skills/quality-gates/verify-change.md +1 -1
- package/skills/quality-gates/verify-module.md +1 -1
- package/skills/quality-gates/verify-quality.md +1 -1
- package/skills/quality-gates/verify-security.md +1 -1
- package/skills/routing.md +14 -14
- package/skills/security/blue-team.md +1 -1
- package/skills/security/code-audit.md +1 -1
- package/skills/security/pentest.md +1 -1
- package/skills/security/red-team.md +1 -1
- package/skills/security/threat-intel.md +1 -1
- package/skills/security/vuln-research.md +1 -1
- package/skills/utility/clean-code.md +2 -2
- package/templates/docs/changelog-entry.md +1 -1
- package/templates/docs/construct_guide.md +18 -18
- package/templates/docs/meta-prd.md +16 -16
- package/templates/docs/one-pager.md +1 -1
- package/templates/docs/prd-business.md +1 -1
- package/templates/docs/prd-platform.md +1 -1
- package/templates/docs/prd.md +17 -17
- package/templates/docs/research-brief.md +8 -8
- package/templates/docs/rfc.md +1 -1
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/docs/strategy.md +1 -1
package/bin/construct
CHANGED
|
@@ -292,7 +292,7 @@ async function cmdList() {
|
|
|
292
292
|
async function cmdDoctor() {
|
|
293
293
|
const args = process.argv.slice(3);
|
|
294
294
|
|
|
295
|
-
const DAEMON_SUBS = new Set(['status', 'watch', 'stop', 'logs', 'tick', 'report']);
|
|
295
|
+
const DAEMON_SUBS = new Set(['status', 'watch', 'stop', 'logs', 'tick', 'report', 'consistency']);
|
|
296
296
|
if (args.length > 0 && DAEMON_SUBS.has(args[0])) {
|
|
297
297
|
const { runCli } = await import('../lib/doctor/cli.mjs');
|
|
298
298
|
const code = await runCli(args);
|
|
@@ -1053,6 +1053,59 @@ async function cmdVersion() {
|
|
|
1053
1053
|
println(`construct v${pkg.version}`);
|
|
1054
1054
|
}
|
|
1055
1055
|
|
|
1056
|
+
async function cmdMigrate(args) {
|
|
1057
|
+
const dryRun = args.includes('--dry-run');
|
|
1058
|
+
const pathArg = args.find((a) => a.startsWith('--path='))?.split('=')[1];
|
|
1059
|
+
const fromArg = args.find((a) => a.startsWith('--from='))?.split('=')[1];
|
|
1060
|
+
const { runMigrations, planMigrations, CURRENT_SCHEMA_VERSION } = await import('../lib/migrations/index.mjs');
|
|
1061
|
+
|
|
1062
|
+
const candidates = pathArg
|
|
1063
|
+
? [pathArg]
|
|
1064
|
+
: [
|
|
1065
|
+
path.join(ROOT_DIR, 'agents', 'registry.json'),
|
|
1066
|
+
path.join(ROOT_DIR, 'agents', 'contracts.json'),
|
|
1067
|
+
path.join(ROOT_DIR, 'agents', 'role-manifests.json'),
|
|
1068
|
+
path.join(process.cwd(), '.cx', 'config.json'),
|
|
1069
|
+
];
|
|
1070
|
+
|
|
1071
|
+
let anyFailed = false;
|
|
1072
|
+
for (const artifactPath of candidates) {
|
|
1073
|
+
if (!fs.existsSync(artifactPath)) continue;
|
|
1074
|
+
let from;
|
|
1075
|
+
try {
|
|
1076
|
+
const data = JSON.parse(fs.readFileSync(artifactPath, 'utf8'));
|
|
1077
|
+
from = typeof data?.version === 'number' ? data.version : Number(fromArg) || 0;
|
|
1078
|
+
} catch {
|
|
1079
|
+
println(` ⚠ ${artifactPath}: parse error, skipping`);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const plan = planMigrations(from, CURRENT_SCHEMA_VERSION);
|
|
1084
|
+
if (plan === null) {
|
|
1085
|
+
println(` ✗ ${artifactPath}: no migration path from v${from} to v${CURRENT_SCHEMA_VERSION}`);
|
|
1086
|
+
anyFailed = true;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (plan.length === 0) {
|
|
1090
|
+
println(` ✓ ${artifactPath}: already at v${CURRENT_SCHEMA_VERSION}`);
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
println(` ${dryRun ? '·' : '→'} ${artifactPath}: ${plan.length} step(s) (v${from} → v${CURRENT_SCHEMA_VERSION})`);
|
|
1095
|
+
const result = await runMigrations({ artifactPath, fromVersion: from, dryRun });
|
|
1096
|
+
for (const step of result.applied) {
|
|
1097
|
+
println(` v${step.from}→v${step.to}: ${step.summary}`);
|
|
1098
|
+
}
|
|
1099
|
+
if (!result.ok) {
|
|
1100
|
+
println(` ✗ ${result.error}`);
|
|
1101
|
+
anyFailed = true;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (dryRun) println(`\n(dry-run — no files were written)`);
|
|
1106
|
+
if (anyFailed) process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1056
1109
|
async function cmdReview(args) {
|
|
1057
1110
|
fs.mkdirSync(path.join(HOME, '.cx', 'performance-reviews'), { recursive: true });
|
|
1058
1111
|
const sub = args[0];
|
|
@@ -1550,13 +1603,16 @@ function printActiveConfig() {
|
|
|
1550
1603
|
println(`Persisted at: ${getUserEnvPath(HOME)} (${DEPLOYMENT_MODE_ENV_KEY})`);
|
|
1551
1604
|
}
|
|
1552
1605
|
|
|
1553
|
-
function printIntakeHelp() {
|
|
1554
|
-
|
|
1606
|
+
async function printIntakeHelp() {
|
|
1607
|
+
const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
|
|
1608
|
+
const { intakeQueueLabel, signalNoun } = getRebrand(process.cwd());
|
|
1609
|
+
println(`Construct intake — inspect and process the ${intakeQueueLabel}.`);
|
|
1555
1610
|
println('');
|
|
1556
1611
|
println('Usage:');
|
|
1557
|
-
println(
|
|
1612
|
+
println(` construct intake list List pending ${signalNoun}s (ID, type, stage, owner, action).`);
|
|
1558
1613
|
println(' construct intake show <id> Show a single packet: triage, source, excerpt, related docs.');
|
|
1559
|
-
println(' construct intake done <id> [--notes=…]
|
|
1614
|
+
println(' construct intake done <id> [--notes=…] [--output=<path>] [--no-output=<rejected|deferred|merged>]');
|
|
1615
|
+
println(' Mark a pending packet processed. --output stamps intake_id + confidence into the artifact frontmatter (provenance).');
|
|
1560
1616
|
println(' construct intake skip <id> [--reason=…] Mark a pending packet skipped (preserves audit trail).');
|
|
1561
1617
|
println(' construct intake reopen <id> Move a processed or skipped packet back to pending.');
|
|
1562
1618
|
println(' construct intake metrics Show pipeline volume, velocity, and processing stats.');
|
|
@@ -1580,11 +1636,12 @@ function parseKvFlags(args, keys) {
|
|
|
1580
1636
|
return out;
|
|
1581
1637
|
}
|
|
1582
1638
|
|
|
1583
|
-
function formatIntakeTable(rows) {
|
|
1639
|
+
function formatIntakeTable(rows, { intakeQueueLabel, signalNoun } = { intakeQueueLabel: 'Intake queue', signalNoun: 'signal' }) {
|
|
1584
1640
|
if (rows.length === 0) {
|
|
1585
|
-
println(
|
|
1641
|
+
println(`No pending ${signalNoun}s.`);
|
|
1586
1642
|
return;
|
|
1587
1643
|
}
|
|
1644
|
+
println(`${intakeQueueLabel} (${rows.length} pending):`);
|
|
1588
1645
|
const headers = ['ID', 'Type', 'Stage', 'Owner', 'Action'];
|
|
1589
1646
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] || '').length)));
|
|
1590
1647
|
const fmt = (cells) => cells.map((c, i) => String(c || '').padEnd(widths[i])).join(' ');
|
|
@@ -1597,12 +1654,14 @@ async function cmdIntake(args) {
|
|
|
1597
1654
|
const sub = args[0];
|
|
1598
1655
|
|
|
1599
1656
|
if (!sub || sub === '--help' || sub === '-h') {
|
|
1600
|
-
printIntakeHelp();
|
|
1657
|
+
await printIntakeHelp();
|
|
1601
1658
|
return;
|
|
1602
1659
|
}
|
|
1603
1660
|
|
|
1604
1661
|
const { createIntakeQueue } = await import('../lib/intake/queue.mjs');
|
|
1662
|
+
const { getRebrand } = await import('../lib/profiles/rebrand.mjs');
|
|
1605
1663
|
const cwd = process.cwd();
|
|
1664
|
+
const rebrand = getRebrand(cwd);
|
|
1606
1665
|
const queue = createIntakeQueue(cwd, process.env);
|
|
1607
1666
|
|
|
1608
1667
|
if (sub === 'list') {
|
|
@@ -1611,7 +1670,7 @@ async function cmdIntake(args) {
|
|
|
1611
1670
|
const t = p.triage || {};
|
|
1612
1671
|
return [p.id, t.intakeType || 'unknown', t.rdStage || 'unknown', t.primaryOwner || '—', t.recommendedAction || '—'];
|
|
1613
1672
|
});
|
|
1614
|
-
formatIntakeTable(rows);
|
|
1673
|
+
formatIntakeTable(rows, rebrand);
|
|
1615
1674
|
return;
|
|
1616
1675
|
}
|
|
1617
1676
|
|
|
@@ -1619,8 +1678,9 @@ async function cmdIntake(args) {
|
|
|
1619
1678
|
if (sub === 'show') {
|
|
1620
1679
|
if (!id) { errorln('Usage: construct intake show <id>'); process.exit(1); }
|
|
1621
1680
|
const entry = queue.read(id);
|
|
1622
|
-
if (!entry) { errorln(`No
|
|
1681
|
+
if (!entry) { errorln(`No ${rebrand.signalNoun} with id: ${id}`); process.exit(1); }
|
|
1623
1682
|
const t = entry.triage || {};
|
|
1683
|
+
println(`${rebrand.signalNoun.charAt(0).toUpperCase() + rebrand.signalNoun.slice(1)} ${entry.id}`);
|
|
1624
1684
|
println(`ID: ${entry.id}`);
|
|
1625
1685
|
println(`Status: ${entry.status}`);
|
|
1626
1686
|
println(`Source: ${entry.intake?.sourcePath || '(none)'}`);
|
|
@@ -1712,11 +1772,59 @@ async function cmdIntake(args) {
|
|
|
1712
1772
|
}
|
|
1713
1773
|
|
|
1714
1774
|
if (sub === 'done') {
|
|
1715
|
-
if (!id) {
|
|
1716
|
-
|
|
1775
|
+
if (!id) {
|
|
1776
|
+
errorln('Usage: construct intake done <id> [--notes=…] [--output=<path>] [--no-output=<rejected|deferred|merged>]');
|
|
1777
|
+
process.exit(1);
|
|
1778
|
+
}
|
|
1779
|
+
const flags = parseKvFlags(args.slice(2), ['notes', 'output', 'no-output']);
|
|
1780
|
+
const outputPath = flags.output || null;
|
|
1781
|
+
const noOutputOutcome = flags['no-output'] || null;
|
|
1782
|
+
|
|
1783
|
+
if (outputPath && noOutputOutcome) {
|
|
1784
|
+
errorln('--output and --no-output are mutually exclusive');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
if (noOutputOutcome && !['rejected', 'deferred', 'merged'].includes(noOutputOutcome)) {
|
|
1788
|
+
errorln(`--no-output value must be one of: rejected, deferred, merged (got "${noOutputOutcome}")`);
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
let stamped = null;
|
|
1793
|
+
if (outputPath) {
|
|
1794
|
+
const packet = queue.read(id);
|
|
1795
|
+
if (!packet) {
|
|
1796
|
+
errorln(`No intake packet found: ${id}`);
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1799
|
+
const triage = packet.triage || {};
|
|
1800
|
+
try {
|
|
1801
|
+
const { stampIntakeProvenance } = await import('../lib/intake/traceability.mjs');
|
|
1802
|
+
const path = (await import('node:path')).default;
|
|
1803
|
+
const absPath = path.isAbsolute(outputPath) ? outputPath : path.join(cwd, outputPath);
|
|
1804
|
+
stamped = stampIntakeProvenance(absPath, {
|
|
1805
|
+
intakeId: id,
|
|
1806
|
+
confidence: triage.confidence,
|
|
1807
|
+
rationale: triage.rationale,
|
|
1808
|
+
});
|
|
1809
|
+
} catch (err) {
|
|
1810
|
+
errorln(err.message || 'intake stamping failed');
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const notes = flags.notes
|
|
1816
|
+
|| (noOutputOutcome ? `intake_outcome: ${noOutputOutcome}` : '')
|
|
1817
|
+
|| (outputPath ? `output: ${outputPath}` : '');
|
|
1818
|
+
|
|
1717
1819
|
try {
|
|
1718
|
-
const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes
|
|
1719
|
-
|
|
1820
|
+
const r = queue.markProcessed(id, { processedBy: 'construct-intake-cli', notes });
|
|
1821
|
+
if (stamped) {
|
|
1822
|
+
info(`Marked ${r.id} processed. Stamped ${outputPath} with intake_id=${id} (confidence=${stamped.intake_confidence ?? 'n/a'}).`);
|
|
1823
|
+
} else if (noOutputOutcome) {
|
|
1824
|
+
info(`Marked ${r.id} processed (intake_outcome: ${noOutputOutcome}).`);
|
|
1825
|
+
} else {
|
|
1826
|
+
info(`Marked ${r.id} processed.`);
|
|
1827
|
+
}
|
|
1720
1828
|
} catch (err) {
|
|
1721
1829
|
errorln(err.message || 'mark processed failed');
|
|
1722
1830
|
process.exit(1);
|
|
@@ -2741,27 +2849,55 @@ async function cmdSeedTraces(args) {
|
|
|
2741
2849
|
async function cmdCleanup() {
|
|
2742
2850
|
const force = restArgsCache.includes('--pressure-release');
|
|
2743
2851
|
const quiet = restArgsCache.includes('--quiet');
|
|
2744
|
-
const
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
const
|
|
2753
|
-
|
|
2852
|
+
const dryRun = restArgsCache.includes('--dry-run');
|
|
2853
|
+
const pressureOnly = restArgsCache.includes('--pressure-only');
|
|
2854
|
+
const diskOnly = restArgsCache.includes('--disk-only');
|
|
2855
|
+
|
|
2856
|
+
// Process pressure release: kill stale dev-agent processes.
|
|
2857
|
+
if (!diskOnly) {
|
|
2858
|
+
const report = runPressureRelease({ env: process.env, force });
|
|
2859
|
+
if (!quiet) {
|
|
2860
|
+
const swap = report.swapUsage
|
|
2861
|
+
? `${(report.swapUsage.usedBytes / (1024 ** 3)).toFixed(1)} GiB used`
|
|
2862
|
+
: 'swap unavailable';
|
|
2863
|
+
println(`Pressure guard: ${report.pressureTriggered ? 'triggered' : 'standby'} · ${swap}`);
|
|
2864
|
+
for (const entry of report.warnings ?? []) {
|
|
2865
|
+
const grace = entry.graceMinutes ? `; will terminate after ${entry.graceMinutes}m if still matched` : '';
|
|
2866
|
+
println(`Warning pid ${entry.pid} (${entry.reason})${grace}`);
|
|
2867
|
+
}
|
|
2868
|
+
if (report.killed.length) {
|
|
2869
|
+
for (const entry of report.killed) {
|
|
2870
|
+
println(`Stopped pid ${entry.pid} (${entry.reason})`);
|
|
2871
|
+
}
|
|
2872
|
+
} else {
|
|
2873
|
+
println('No stale dev-agent processes needed cleanup.');
|
|
2874
|
+
}
|
|
2754
2875
|
}
|
|
2755
2876
|
}
|
|
2756
2877
|
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2878
|
+
// Disk maintenance: rotate oversized logs, prune stale cache entries.
|
|
2879
|
+
if (!pressureOnly) {
|
|
2880
|
+
const { runFullCleanup, formatBytes } = await import('../lib/maintenance/cleanup.mjs');
|
|
2881
|
+
const summary = runFullCleanup({ homeDir: HOME, env: process.env, dryRun });
|
|
2882
|
+
if (!quiet) {
|
|
2883
|
+
const verb = dryRun ? 'Would free' : 'Freed';
|
|
2884
|
+
println(`Disk maintenance${dryRun ? ' (dry-run)' : ''}: ${verb} ${formatBytes(summary.freedBytes)} in ${summary.durationMs}ms`);
|
|
2885
|
+
const log = summary.embedLog;
|
|
2886
|
+
if (log?.truncated?.length) {
|
|
2887
|
+
for (const t of log.truncated) println(` truncated ${path.relative(HOME, t.path)} (was ${formatBytes(t.sizeWas)})`);
|
|
2888
|
+
}
|
|
2889
|
+
if (log?.removed?.length) {
|
|
2890
|
+
for (const r of log.removed) println(` removed ${path.relative(HOME, r.path)} (${formatBytes(r.size)})`);
|
|
2891
|
+
}
|
|
2892
|
+
if (summary.jsonlLogs?.truncated?.length) {
|
|
2893
|
+
for (const t of summary.jsonlLogs.truncated) println(` truncated ${path.relative(HOME, t.path)} (was ${formatBytes(t.sizeWas)} → ${formatBytes(t.sizeNow)})`);
|
|
2894
|
+
}
|
|
2895
|
+
if (summary.cacheDir?.removed?.length) {
|
|
2896
|
+
println(` pruned ${summary.cacheDir.removed.length} stale cache file(s)`);
|
|
2897
|
+
}
|
|
2898
|
+
for (const err of summary.errors ?? []) {
|
|
2899
|
+
println(` ! ${err.step}: ${err.error}`);
|
|
2900
|
+
}
|
|
2765
2901
|
}
|
|
2766
2902
|
}
|
|
2767
2903
|
}
|
|
@@ -2970,6 +3106,12 @@ async function cmdMemory(args) {
|
|
|
2970
3106
|
async function cmdLintComments(args) {
|
|
2971
3107
|
const fix = args.includes('--fix');
|
|
2972
3108
|
const staged = args.includes('--staged');
|
|
3109
|
+
// CLI invocation enforces block severity for artifact-prose lint (see
|
|
3110
|
+
// lib/comment-lint.mjs#checkArtifactBanned). Operators may opt back into
|
|
3111
|
+
// warn-only by exporting CONSTRUCT_ARTIFACT_LINT_MODE=warn explicitly.
|
|
3112
|
+
if (!process.env.CONSTRUCT_ARTIFACT_LINT_MODE) {
|
|
3113
|
+
process.env.CONSTRUCT_ARTIFACT_LINT_MODE = 'block';
|
|
3114
|
+
}
|
|
2973
3115
|
const { lintRepo: lint, lintFile, formatResults: fmt } = await import('../lib/comment-lint.mjs');
|
|
2974
3116
|
|
|
2975
3117
|
let results;
|
|
@@ -3053,6 +3195,18 @@ async function cmdLintAgents() {
|
|
|
3053
3195
|
process.exit(1);
|
|
3054
3196
|
}
|
|
3055
3197
|
|
|
3198
|
+
async function cmdLintContracts() {
|
|
3199
|
+
const { validateContractsFile } = await import('../lib/contracts/validate.mjs');
|
|
3200
|
+
const result = validateContractsFile({ repoRoot: ROOT_DIR });
|
|
3201
|
+
if (result.ok) {
|
|
3202
|
+
console.log('agents/contracts.json: 0 errors');
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
console.error(`agents/contracts.json: ${result.errors.length} error(s)`);
|
|
3206
|
+
for (const err of result.errors) console.error(` ${err}`);
|
|
3207
|
+
process.exit(1);
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3056
3210
|
async function cmdBackup(args) {
|
|
3057
3211
|
const sub = args[0];
|
|
3058
3212
|
|
|
@@ -3426,6 +3580,7 @@ const handlers = new Map([
|
|
|
3426
3580
|
['evals', cmdEvals],
|
|
3427
3581
|
['cleanup', cmdCleanup],
|
|
3428
3582
|
['version', cmdVersion],
|
|
3583
|
+
['migrate', cmdMigrate],
|
|
3429
3584
|
['docs:update', cmdDocsUpdate],
|
|
3430
3585
|
['docs:check', cmdDocsCheck],
|
|
3431
3586
|
['docs:site', cmdDocsSite],
|
|
@@ -3440,6 +3595,7 @@ const handlers = new Map([
|
|
|
3440
3595
|
['provider', cmdProvider],
|
|
3441
3596
|
['lint:research', cmdLintResearch],
|
|
3442
3597
|
['lint:agents', cmdLintAgents],
|
|
3598
|
+
['lint:contracts', cmdLintContracts],
|
|
3443
3599
|
['audit', async (args) => {
|
|
3444
3600
|
const sub = args[0];
|
|
3445
3601
|
if (sub === 'skills') return runAuditSkillsCli(args.slice(1));
|
|
@@ -3994,18 +4150,85 @@ const handlers = new Map([
|
|
|
3994
4150
|
}
|
|
3995
4151
|
if (sub === 'set') {
|
|
3996
4152
|
const id = args[1];
|
|
3997
|
-
if (!id) {
|
|
4153
|
+
if (!id) {
|
|
4154
|
+
errorln('Usage: construct profile set <id> [--yes] [--dry-run]');
|
|
4155
|
+
process.exit(1);
|
|
4156
|
+
}
|
|
3998
4157
|
const target = loadProfile(id);
|
|
3999
4158
|
if (!target) {
|
|
4000
4159
|
errorln(`Unknown profile: ${id}. Available: ${listProfiles().join(', ')}`);
|
|
4001
4160
|
process.exit(1);
|
|
4002
4161
|
}
|
|
4162
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4163
|
+
const dryRun = args.includes('--dry-run');
|
|
4164
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4165
|
+
|
|
4003
4166
|
const { findProjectConfigPath, loadProjectConfig, writeProjectConfig, PROJECT_CONFIG_FILENAME } = await import('../lib/config/project-config.mjs');
|
|
4004
|
-
const fsMod = await import('node:fs');
|
|
4005
4167
|
const pathMod = await import('node:path');
|
|
4006
4168
|
const found = findProjectConfigPath(process.cwd());
|
|
4007
4169
|
const cfgPath = found || pathMod.join(process.cwd(), PROJECT_CONFIG_FILENAME);
|
|
4008
|
-
|
|
4170
|
+
// loadProjectConfig returns a wrapper { path, raw, config, source, errors };
|
|
4171
|
+
// operate on the raw user-authored object so the write call sees the
|
|
4172
|
+
// version field that validateProjectConfig requires.
|
|
4173
|
+
const loaded = found ? loadProjectConfig(process.cwd()) : null;
|
|
4174
|
+
const cfg = loaded?.raw ? structuredClone(loaded.raw) : { version: 1 };
|
|
4175
|
+
if (!cfg.version) cfg.version = 1;
|
|
4176
|
+
|
|
4177
|
+
const currentId = cfg.profile || 'rnd';
|
|
4178
|
+
if (currentId === id) {
|
|
4179
|
+
println(`Profile already set to ${id} (${target.displayName}). Nothing to do.`);
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
const current = loadProfile(currentId) || { id: currentId, displayName: currentId, roles: [], intake: { types: [], stages: [] }, departments: [] };
|
|
4183
|
+
|
|
4184
|
+
// Render a structural diff so the operator sees what loop they are switching into.
|
|
4185
|
+
const setDiff = (label, before, after) => {
|
|
4186
|
+
const b = new Set(before || []);
|
|
4187
|
+
const a = new Set(after || []);
|
|
4188
|
+
const added = [...a].filter((x) => !b.has(x));
|
|
4189
|
+
const removed = [...b].filter((x) => !a.has(x));
|
|
4190
|
+
const unchanged = [...a].filter((x) => b.has(x));
|
|
4191
|
+
println(` ${label}:`);
|
|
4192
|
+
if (added.length) println(` + ${added.join(', ')}`);
|
|
4193
|
+
if (removed.length) println(` - ${removed.join(', ')}`);
|
|
4194
|
+
if (!added.length && !removed.length) println(` (no change, ${unchanged.length} entr${unchanged.length === 1 ? 'y' : 'ies'})`);
|
|
4195
|
+
};
|
|
4196
|
+
|
|
4197
|
+
println('');
|
|
4198
|
+
println(`About to switch active profile:`);
|
|
4199
|
+
println(` from: ${currentId} (${current.displayName})`);
|
|
4200
|
+
println(` to: ${id} (${target.displayName})`);
|
|
4201
|
+
println('');
|
|
4202
|
+
println(`Structural diff:`);
|
|
4203
|
+
setDiff('roles', current.roles, target.roles);
|
|
4204
|
+
setDiff('intake.types', current.intake?.types, target.intake?.types);
|
|
4205
|
+
setDiff('intake.stages', current.intake?.stages, target.intake?.stages);
|
|
4206
|
+
const cDept = (current.departments || []).map((d) => d.id);
|
|
4207
|
+
const tDept = (target.departments || []).map((d) => d.id);
|
|
4208
|
+
setDiff('departments', cDept, tDept);
|
|
4209
|
+
println('');
|
|
4210
|
+
println(`Files that will change:`);
|
|
4211
|
+
println(` ${pathMod.relative(process.cwd(), cfgPath)} (profile field: ${currentId} -> ${id})`);
|
|
4212
|
+
println('');
|
|
4213
|
+
println(`The change only writes the profile field. No specialists are added or removed.`);
|
|
4214
|
+
println(`Sync runs against the new profile on next \`construct sync\`.`);
|
|
4215
|
+
println('');
|
|
4216
|
+
|
|
4217
|
+
if (dryRun) {
|
|
4218
|
+
println(`[dry-run] No files were written.`);
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
if (!skipConfirm && isTTY) {
|
|
4222
|
+
const readline = await import('node:readline');
|
|
4223
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4224
|
+
const answer = await new Promise((res) => rl.question(`Switch profile to ${id}? (Y/n) `, (a) => res(a.trim().toLowerCase())));
|
|
4225
|
+
rl.close();
|
|
4226
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
4227
|
+
println('Aborted. No config was changed.');
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4009
4232
|
cfg.profile = id;
|
|
4010
4233
|
writeProjectConfig(cfgPath, cfg, { validate: true });
|
|
4011
4234
|
process.stdout.write(`✓ profile set to ${id} (${target.displayName})\n`);
|
|
@@ -4014,36 +4237,111 @@ const handlers = new Map([
|
|
|
4014
4237
|
if (sub === 'create') {
|
|
4015
4238
|
const id = args[1];
|
|
4016
4239
|
if (!id) {
|
|
4017
|
-
errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...]');
|
|
4240
|
+
errorln('Usage: construct profile create <id> [--display="..."] [--role=<id>...] [--department=<id>:<displayName>...] [--yes]');
|
|
4241
|
+
errorln(' Run with no flags for interactive mode; pass --yes to skip the confirmation prompt.');
|
|
4242
|
+
process.exit(1);
|
|
4243
|
+
}
|
|
4244
|
+
if (!/^[a-z][a-z0-9-]{1,30}$/.test(id)) {
|
|
4245
|
+
errorln(`Invalid id "${id}". Must match ^[a-z][a-z0-9-]{1,30}$.`);
|
|
4018
4246
|
process.exit(1);
|
|
4019
4247
|
}
|
|
4248
|
+
|
|
4020
4249
|
const displayArg = args.find((a) => a.startsWith('--display='));
|
|
4021
|
-
const
|
|
4022
|
-
const
|
|
4250
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4251
|
+
const dryRun = args.includes('--dry-run');
|
|
4252
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4253
|
+
|
|
4254
|
+
let displayName = displayArg ? displayArg.split('=')[1] : null;
|
|
4255
|
+
let seedRoles = args
|
|
4023
4256
|
.filter((a) => a.startsWith('--role='))
|
|
4024
4257
|
.map((a) => a.slice('--role='.length))
|
|
4025
4258
|
.filter(Boolean);
|
|
4026
|
-
|
|
4259
|
+
let seedDepartments = args
|
|
4027
4260
|
.filter((a) => a.startsWith('--department='))
|
|
4028
4261
|
.map((a) => a.slice('--department='.length))
|
|
4029
4262
|
.map((spec) => {
|
|
4030
4263
|
const [deptId, deptDisplay] = spec.split(':');
|
|
4031
4264
|
return { id: deptId, displayName: deptDisplay || deptId };
|
|
4032
4265
|
});
|
|
4266
|
+
|
|
4267
|
+
// Interactive gather. Only prompt when stdin is a real terminal and the
|
|
4268
|
+
// operator did not pass enough flags to scaffold without input. A
|
|
4269
|
+
// non-TTY invocation with no flags falls through to an empty scaffold
|
|
4270
|
+
// so scripts and CI can still create drafts non-interactively.
|
|
4271
|
+
const needsInteractive = isTTY && !skipConfirm && (!displayName || (seedRoles.length === 0 && seedDepartments.length === 0));
|
|
4272
|
+
if (needsInteractive) {
|
|
4273
|
+
const readline = await import('node:readline');
|
|
4274
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4275
|
+
const ask = (q, def = '') => new Promise((res) => rl.question(`${q}${def ? ` [${def}]` : ''} `, (a) => res(a.trim() || def)));
|
|
4276
|
+
|
|
4277
|
+
println('');
|
|
4278
|
+
println(`Scaffolding a new profile draft. Three questions, then a preview.`);
|
|
4279
|
+
println(`Background: docs/concepts/persona-research.md and docs/concepts/profile-lifecycle.md`);
|
|
4280
|
+
println('');
|
|
4281
|
+
if (!displayName) displayName = await ask('Display name?', id);
|
|
4282
|
+
if (seedDepartments.length === 0) {
|
|
4283
|
+
const dInput = await ask('Departments (id:Name, id:Name, ...)?', '');
|
|
4284
|
+
if (dInput) seedDepartments = dInput.split(',').map((s) => s.trim()).filter(Boolean).map((spec) => {
|
|
4285
|
+
const [dId, dName] = spec.split(':').map((x) => x.trim());
|
|
4286
|
+
return { id: dId, displayName: dName || dId };
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
if (seedRoles.length === 0) {
|
|
4290
|
+
const rInput = await ask('Roles (role-id, role-id, ...)?', '');
|
|
4291
|
+
if (rInput) seedRoles = rInput.split(',').map((s) => s.trim()).filter(Boolean);
|
|
4292
|
+
}
|
|
4293
|
+
rl.close();
|
|
4294
|
+
}
|
|
4295
|
+
if (!displayName) displayName = id;
|
|
4296
|
+
|
|
4297
|
+
const pathMod = await import('node:path');
|
|
4298
|
+
const draftRoot = pathMod.join(process.cwd(), '.cx', 'profiles', `draft-${id}`);
|
|
4299
|
+
|
|
4300
|
+
println('');
|
|
4301
|
+
println(`About to scaffold a draft profile:`);
|
|
4302
|
+
println(` id: ${id}`);
|
|
4303
|
+
println(` displayName: ${displayName}`);
|
|
4304
|
+
println(` departments: ${seedDepartments.length}${seedDepartments.length ? ` (${seedDepartments.map((d) => d.id).join(', ')})` : ''}`);
|
|
4305
|
+
println(` roles: ${seedRoles.length}${seedRoles.length ? ` (${seedRoles.join(', ')})` : ''}`);
|
|
4306
|
+
println('');
|
|
4307
|
+
println(`Files that will be written under ${pathMod.relative(process.cwd(), draftRoot)}/:`);
|
|
4308
|
+
println(` profile.json structural manifest`);
|
|
4309
|
+
println(` requirements.md research brief naming each cx-* specialist`);
|
|
4310
|
+
for (const r of seedRoles) println(` personas/${r}.md`.padEnd(40) + ` persona artifact (filled during discover)`);
|
|
4311
|
+
for (const d of seedDepartments) println(` departments/${d.id}.md`.padEnd(40) + ` department charter (filled during frame)`);
|
|
4312
|
+
println('');
|
|
4313
|
+
println(`Nothing else is touched. No config is changed, no active profile is switched.`);
|
|
4314
|
+
println(`To activate this profile after promotion: construct profile set ${id}`);
|
|
4315
|
+
println('');
|
|
4316
|
+
|
|
4317
|
+
if (dryRun) {
|
|
4318
|
+
println(`[dry-run] No files were written.`);
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
if (!skipConfirm && isTTY) {
|
|
4322
|
+
const readline = await import('node:readline');
|
|
4323
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4324
|
+
const answer = await new Promise((res) => rl.question('Create draft? (Y/n) ', (a) => res(a.trim().toLowerCase())));
|
|
4325
|
+
rl.close();
|
|
4326
|
+
if (answer && answer !== 'y' && answer !== 'yes') {
|
|
4327
|
+
println('Aborted. Nothing was written.');
|
|
4328
|
+
return;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4033
4332
|
const { createDraftProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4034
4333
|
try {
|
|
4035
4334
|
const result = createDraftProfile({ cwd: process.cwd(), id, displayName, seedRoles, seedDepartments });
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
process.stdout.write(` 4. Validate: dispatch cx-evaluator to run persona-eval and classifier-eval against representative signals.\n`);
|
|
4335
|
+
println('');
|
|
4336
|
+
println(`✓ draft created at ${pathMod.relative(process.cwd(), result.dir)}`);
|
|
4337
|
+
println('');
|
|
4338
|
+
println(`Next steps (in order, per docs/concepts/profile-lifecycle.md):`);
|
|
4339
|
+
println(` 1. Discover (cx-ux-researcher): fill personas/<role>.md from interviews + primary sources.`);
|
|
4340
|
+
println(` 2. Frame (cx-product-manager): fill departments/<dept>.md charters and intake taxonomy.`);
|
|
4341
|
+
println(` 3. Architect (cx-architect): reconcile role reuse vs new; populate departments[] in profile.json.`);
|
|
4342
|
+
println(` 4. Validate (cx-evaluator): run persona-eval and classifier-eval against representative signals.`);
|
|
4343
|
+
println('');
|
|
4344
|
+
println(`Inspect the brief: open ${pathMod.relative(process.cwd(), result.briefPath)}`);
|
|
4047
4345
|
return;
|
|
4048
4346
|
} catch (err) {
|
|
4049
4347
|
errorln(err.message);
|
|
@@ -4063,12 +4361,84 @@ const handlers = new Map([
|
|
|
4063
4361
|
if (sub === 'archive') {
|
|
4064
4362
|
const id = args[1];
|
|
4065
4363
|
const reasonArg = args.find((a) => a.startsWith('--reason='));
|
|
4066
|
-
|
|
4067
|
-
if (!id) {
|
|
4364
|
+
let reason = reasonArg ? reasonArg.split('=')[1] : '';
|
|
4365
|
+
if (!id) {
|
|
4366
|
+
errorln('Usage: construct profile archive <id> --reason="..." [--yes] [--dry-run]');
|
|
4367
|
+
process.exit(1);
|
|
4368
|
+
}
|
|
4369
|
+
const skipConfirm = args.includes('--yes') || args.includes('-y') || args.includes('--non-interactive');
|
|
4370
|
+
const dryRun = args.includes('--dry-run');
|
|
4371
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
4372
|
+
|
|
4373
|
+
const pathMod = await import('node:path');
|
|
4374
|
+
const fsMod = await import('node:fs');
|
|
4375
|
+
const target = loadProfile(id);
|
|
4376
|
+
if (!target) {
|
|
4377
|
+
errorln(`Unknown profile: ${id}. Curated catalog: ${listProfiles().join(', ')}`);
|
|
4378
|
+
process.exit(1);
|
|
4379
|
+
}
|
|
4380
|
+
const fileURL = await import('node:url');
|
|
4381
|
+
const moduleDir = pathMod.dirname(fileURL.fileURLToPath(import.meta.url));
|
|
4382
|
+
const repoRoot = pathMod.resolve(moduleDir, '..');
|
|
4383
|
+
const srcProfile = pathMod.join(repoRoot, 'profiles', `${id}.json`);
|
|
4384
|
+
const tableRel = typeof target?.intake?.classificationTable === 'string' ? target.intake.classificationTable : null;
|
|
4385
|
+
const srcTable = tableRel ? pathMod.join(repoRoot, tableRel) : null;
|
|
4386
|
+
const dstDir = pathMod.join(repoRoot, 'archive', 'profiles', id);
|
|
4387
|
+
|
|
4388
|
+
// Gather reason interactively if missing. The lifecycle module already
|
|
4389
|
+
// enforces a >= 8 char reason, but the operator should see why before
|
|
4390
|
+
// the call fails.
|
|
4391
|
+
if (!reason && isTTY && !skipConfirm) {
|
|
4392
|
+
const readline = await import('node:readline');
|
|
4393
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4394
|
+
reason = await new Promise((res) => rl.question('Archive reason (>= 8 chars): ', (a) => res(a.trim())));
|
|
4395
|
+
rl.close();
|
|
4396
|
+
}
|
|
4397
|
+
if (!reason || reason.trim().length < 8) {
|
|
4398
|
+
errorln('A substantive --reason (>= 8 chars) is required. Archiving is permanent in repo history; the reason becomes the archive note.');
|
|
4399
|
+
process.exit(1);
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
println('');
|
|
4403
|
+
println(`About to archive curated profile:`);
|
|
4404
|
+
println(` id: ${id}`);
|
|
4405
|
+
println(` displayName: ${target.displayName || id}`);
|
|
4406
|
+
println(` reason: ${reason}`);
|
|
4407
|
+
println('');
|
|
4408
|
+
println(`Files that will move:`);
|
|
4409
|
+
println(` ${pathMod.relative(process.cwd(), srcProfile)}`);
|
|
4410
|
+
println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, `${id}.json`))}`);
|
|
4411
|
+
if (srcTable && fsMod.existsSync(srcTable)) {
|
|
4412
|
+
println(` ${pathMod.relative(process.cwd(), srcTable)}`);
|
|
4413
|
+
println(` -> ${pathMod.relative(process.cwd(), pathMod.join(dstDir, pathMod.basename(srcTable)))}`);
|
|
4414
|
+
}
|
|
4415
|
+
println(` ${pathMod.relative(process.cwd(), pathMod.join(dstDir, 'archive-note.md'))} (new)`);
|
|
4416
|
+
println('');
|
|
4417
|
+
println(`What stays:`);
|
|
4418
|
+
println(` Observations under .cx/observations/ are not touched.`);
|
|
4419
|
+
println(` Outcomes under .cx/outcomes/ are not touched.`);
|
|
4420
|
+
println(` Any project still configured with profile=${id} will fall back to rnd on next load.`);
|
|
4421
|
+
println('');
|
|
4422
|
+
|
|
4423
|
+
if (dryRun) {
|
|
4424
|
+
println(`[dry-run] No files were moved.`);
|
|
4425
|
+
return;
|
|
4426
|
+
}
|
|
4427
|
+
if (!skipConfirm && isTTY) {
|
|
4428
|
+
const readline = await import('node:readline');
|
|
4429
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4430
|
+
const answer = await new Promise((res) => rl.question(`Archive ${id}? (y/N) `, (a) => res(a.trim().toLowerCase())));
|
|
4431
|
+
rl.close();
|
|
4432
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
4433
|
+
println('Aborted. Nothing was archived.');
|
|
4434
|
+
return;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4068
4438
|
const { archiveProfile } = await import('../lib/profiles/lifecycle.mjs');
|
|
4069
4439
|
try {
|
|
4070
4440
|
const { archived } = archiveProfile({ id, reason });
|
|
4071
|
-
process.stdout.write(`✓ archived ${id} to ${archived}\n`);
|
|
4441
|
+
process.stdout.write(`✓ archived ${id} to ${pathMod.relative(process.cwd(), archived)}\n`);
|
|
4072
4442
|
return;
|
|
4073
4443
|
} catch (err) {
|
|
4074
4444
|
errorln(err.message);
|
|
@@ -4117,6 +4487,27 @@ if (!handler) {
|
|
|
4117
4487
|
process.exit(1);
|
|
4118
4488
|
}
|
|
4119
4489
|
|
|
4490
|
+
// Self-maintenance on version change. Skipped for high-frequency hook calls
|
|
4491
|
+
// (the harness invokes `construct hook <name>` many times per session and
|
|
4492
|
+
// must stay fast). When the installed version differs from the stamp at
|
|
4493
|
+
// ~/.construct/.cleanup-stamp, we run a single bounded cleanup pass and
|
|
4494
|
+
// update the stamp. Subsequent invocations are a single file read.
|
|
4495
|
+
if (command !== 'hook') {
|
|
4496
|
+
try {
|
|
4497
|
+
const { maybeRunCleanupOnUpgrade, formatBytes } = await import('../lib/maintenance/cleanup.mjs');
|
|
4498
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, 'package.json'), 'utf8'));
|
|
4499
|
+
const result = maybeRunCleanupOnUpgrade({ homeDir: HOME, env: process.env, currentVersion: pkg.version });
|
|
4500
|
+
if (result.ran && result.summary?.freedBytes > 1024 * 1024) {
|
|
4501
|
+
const previous = result.previousVersion ? ` from v${result.previousVersion}` : '';
|
|
4502
|
+
process.stderr.write(`[construct] Maintenance after upgrade${previous} to v${pkg.version}: freed ${formatBytes(result.summary.freedBytes)}.\n`);
|
|
4503
|
+
}
|
|
4504
|
+
} catch (err) {
|
|
4505
|
+
// Maintenance must never block the actual command. Surface the failure
|
|
4506
|
+
// but continue. The next invocation will retry.
|
|
4507
|
+
process.stderr.write(`[construct] startup maintenance skipped: ${err.message}\n`);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4120
4511
|
// First-invocation resource probe — skipped for hooks, setup, uninstall,
|
|
4121
4512
|
// doctor, and once BOOTSTRAP_CHECKED=1 is cached in ~/.construct/config.env.
|
|
4122
4513
|
// On TTY: if anything's missing, prints the status table and offers to run
|