@geraldmaron/construct 1.0.6 → 1.0.8
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 +39 -34
- package/bin/construct +1403 -96
- package/commands/build/feature.md +1 -1
- package/commands/build/fix.md +1 -1
- package/commands/design/access.md +1 -1
- package/commands/design/flow.md +1 -1
- package/commands/design/ui.md +1 -1
- package/commands/measure/experiment.md +1 -1
- package/commands/measure/metrics.md +1 -1
- package/commands/measure/results.md +1 -1
- package/commands/plan/api.md +1 -1
- package/commands/plan/challenge.md +1 -1
- package/commands/plan/decide.md +1 -1
- package/commands/plan/feature.md +1 -1
- package/commands/plan/requirements.md +1 -1
- package/commands/remember/context.md +1 -1
- package/commands/remember/handoff.md +1 -1
- package/commands/remember/runbook.md +1 -1
- package/commands/review/code.md +1 -1
- package/commands/review/quality.md +1 -1
- package/commands/review/security.md +1 -1
- package/commands/ship/ready.md +1 -1
- package/commands/ship/release.md +1 -1
- package/commands/ship/status.md +1 -1
- package/commands/understand/docs.md +1 -1
- package/commands/understand/this.md +1 -1
- package/commands/understand/why.md +1 -1
- package/commands/work/clean.md +1 -1
- package/commands/work/drive.md +1 -1
- package/commands/work/optimize-prompts.md +2 -2
- package/commands/work/parallel-review.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +8 -8
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +139 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +371 -0
- package/lib/daemons/contract.mjs +210 -0
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -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 +360 -0
- package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
- package/lib/document-extract.mjs +420 -17
- package/lib/document-ingest.mjs +2 -0
- 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 +50 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/embed/scheduler.mjs +33 -5
- package/lib/env-config.mjs +9 -3
- package/lib/evaluator-optimizer.mjs +2 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/comment-lint.mjs +16 -0
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +14 -1
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/proactive-activation.mjs +0 -14
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/rule-verifier.mjs +217 -0
- package/lib/hooks/session-optimize.mjs +2 -1
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +73 -65
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +309 -58
- package/lib/intake/daemon.mjs +121 -0
- package/lib/intake/filesystem-queue.mjs +31 -6
- package/lib/intake/intake-config.mjs +2 -1
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -1
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/intake/session-prelude.mjs +7 -1
- package/lib/intake/traceability.mjs +90 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/research-store.mjs +2 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/maintenance/cleanup.mjs +315 -0
- package/lib/mcp/memory-bridge.mjs +276 -0
- package/lib/mcp/server.mjs +11 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- 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/model-registry.mjs +1 -1
- package/lib/observation-store.mjs +9 -4
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/outcomes/record.mjs +2 -0
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/profiles/rebrand.mjs +46 -0
- package/lib/project-init-shared.mjs +12 -0
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/provider-capabilities.js +20 -7
- package/lib/providers/auth-manager.mjs +58 -17
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/reflect.mjs +49 -12
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +29 -29
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/session-store.mjs +6 -4
- package/lib/setup.mjs +14 -3
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/client.mjs +5 -1
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/version.mjs +51 -0
- package/lib/worker/trace.mjs +5 -1
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +16 -3
- package/personas/construct.md +5 -3
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/development-workflow.md +2 -1
- package/rules/common/no-fabrication.md +69 -0
- package/rules/common/review-before-change.md +58 -0
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +171 -68
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/specialists/contracts.json +1002 -0
- package/specialists/contracts.schema.json +83 -0
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +3 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-architect.md +3 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +3 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +3 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-debugger.md +3 -1
- package/{agents → specialists}/prompts/cx-designer.md +3 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +3 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +4 -2
- package/{agents → specialists}/prompts/cx-engineer.md +4 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +3 -1
- package/{agents → specialists}/prompts/cx-explorer.md +3 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +3 -1
- package/{agents → specialists}/prompts/cx-operations.md +3 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +4 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +3 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-qa.md +3 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +3 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +3 -1
- package/{agents → specialists}/prompts/cx-researcher.md +3 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +4 -2
- package/{agents → specialists}/prompts/cx-security.md +3 -1
- package/{agents → specialists}/prompts/cx-sre.md +4 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +3 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +5 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +3 -1
- package/{agents → specialists}/registry.json +651 -418
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/docs/construct_guide.md +6 -6
- package/templates/docs/skill-artifact.md +1 -1
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- package/agents/contracts.json +0 -387
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* sync-
|
|
3
|
+
* sync-specialists.mjs — regenerate agent adapters for all platforms from specialists/registry.json.
|
|
4
4
|
*
|
|
5
5
|
* Reads registry.json, resolves env vars and model tiers, then writes Claude Code,
|
|
6
6
|
* OpenCode, Codex, Copilot, VS Code, and Cursor adapters. Called by 'construct sync'.
|
|
@@ -50,7 +50,7 @@ for (const [key, value] of Object.entries(mergedEnv)) {
|
|
|
50
50
|
if (!(key in process.env)) process.env[key] = value;
|
|
51
51
|
}
|
|
52
52
|
if (!process.env.CX_TOOLKIT_DIR) process.env.CX_TOOLKIT_DIR = root;
|
|
53
|
-
const registryPath = path.join(root, "
|
|
53
|
+
const registryPath = path.join(root, "specialists", "registry.json");
|
|
54
54
|
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
|
|
55
55
|
|
|
56
56
|
function validateRegistry(registry) {
|
|
@@ -58,31 +58,32 @@ function validateRegistry(registry) {
|
|
|
58
58
|
if (!registry.version) errors.push("Missing 'version' field");
|
|
59
59
|
if (!registry.system) errors.push("Missing 'system' field");
|
|
60
60
|
if (!registry.prefix) errors.push("Missing 'prefix' field");
|
|
61
|
-
if (!
|
|
62
|
-
if (!Array.isArray(registry.
|
|
61
|
+
if (!registry.orchestrator) errors.push("Missing 'orchestrator' field");
|
|
62
|
+
if (!Array.isArray(registry.specialists)) errors.push("Missing or invalid 'specialists' array");
|
|
63
63
|
const validTiers = new Set(["reasoning", "standard", "fast"]);
|
|
64
64
|
const names = new Set();
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
names.
|
|
70
|
-
|
|
71
|
-
if (!
|
|
72
|
-
if (!
|
|
66
|
+
if (registry.orchestrator) {
|
|
67
|
+
const o = registry.orchestrator;
|
|
68
|
+
if (!o.name) errors.push("Orchestrator missing 'name'");
|
|
69
|
+
if (names.has(o.name)) errors.push(`Duplicate name: ${o.name}`);
|
|
70
|
+
names.add(o.name);
|
|
71
|
+
if (!o.description) errors.push("Orchestrator: missing 'description'");
|
|
72
|
+
if (!o.promptFile) errors.push("Orchestrator: missing 'promptFile'");
|
|
73
|
+
if (!o.displayName) errors.push("Orchestrator: missing 'displayName'");
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
for (const
|
|
76
|
-
if (!
|
|
77
|
-
if (names.has(
|
|
78
|
-
names.add(
|
|
79
|
-
if (!
|
|
80
|
-
if (!
|
|
81
|
-
if (!
|
|
82
|
-
if (
|
|
83
|
-
if (!
|
|
84
|
-
if (
|
|
85
|
-
errors.push(`${
|
|
76
|
+
for (const specialist of registry.specialists ?? []) {
|
|
77
|
+
if (!specialist.name) { errors.push("Specialist missing 'name'"); continue; }
|
|
78
|
+
if (names.has(specialist.name)) errors.push(`Duplicate name: ${specialist.name}`);
|
|
79
|
+
names.add(specialist.name);
|
|
80
|
+
if (!specialist.prompt && !specialist.promptFile) errors.push(`${specialist.name}: missing 'prompt' or 'promptFile'`);
|
|
81
|
+
if (!specialist.description) errors.push(`${specialist.name}: missing 'description'`);
|
|
82
|
+
if (!specialist.model && !specialist.modelTier) errors.push(`${specialist.name}: needs 'model' or 'modelTier'`);
|
|
83
|
+
if (specialist.modelTier && !validTiers.has(specialist.modelTier)) errors.push(`${specialist.name}: invalid modelTier '${specialist.modelTier}'`);
|
|
84
|
+
if (!specialist.claudeTools) errors.push(`${specialist.name}: missing 'claudeTools'`);
|
|
85
|
+
if (specialist.modelGuidance && typeof specialist.modelGuidance !== "object") {
|
|
86
|
+
errors.push(`${specialist.name}: modelGuidance must be an object`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -121,6 +122,18 @@ if (validationErrors.length > 0) {
|
|
|
121
122
|
process.exit(1);
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
// Sync-time contract validation: contracts.json shape, schema refs, and
|
|
126
|
+
// producer/consumer name resolution against the registry above.
|
|
127
|
+
{
|
|
128
|
+
const { validateContractsFile } = await import("../lib/contracts/validate.mjs");
|
|
129
|
+
const contractsResult = validateContractsFile();
|
|
130
|
+
if (!contractsResult.ok) {
|
|
131
|
+
console.error("Contract validation failed:");
|
|
132
|
+
for (const err of contractsResult.errors) console.error(` - ${err}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
124
137
|
// --- Dry-run + lockfile + two-phase write infrastructure ---
|
|
125
138
|
|
|
126
139
|
const DRY_RUN = process.argv.includes("--dry-run");
|
|
@@ -168,7 +181,7 @@ const _stagedPairs = []; // [{ staging, real, content }]
|
|
|
168
181
|
|
|
169
182
|
function writeFile(file, content) {
|
|
170
183
|
mkdirp(path.dirname(file));
|
|
171
|
-
const stamped = file.endsWith('.md') ? stampFrontmatter(content, { generator: 'construct/sync-
|
|
184
|
+
const stamped = file.endsWith('.md') ? stampFrontmatter(content, { generator: 'construct/sync-specialists' }) : content;
|
|
172
185
|
|
|
173
186
|
if (DRY_RUN) {
|
|
174
187
|
// Stage in memory only — compare against current on-disk content.
|
|
@@ -216,15 +229,15 @@ const sharedGuidance = registry.sharedGuidance ?? [];
|
|
|
216
229
|
const platformGuidance = registry.platformGuidance ?? {};
|
|
217
230
|
const globalModelGuidance = registry.modelGuidance ?? {};
|
|
218
231
|
|
|
219
|
-
const generatedHeader = `# Generated by ${systemName}/sync-
|
|
232
|
+
const generatedHeader = `# Generated by ${systemName}/sync-specialists.mjs. Edit specialists/registry.json instead.`;
|
|
220
233
|
const generatedMarkdownNote = [
|
|
221
234
|
'<!--',
|
|
222
|
-
`Generated by construct sync from
|
|
235
|
+
`Generated by construct sync from specialists/registry.json.`,
|
|
223
236
|
'Do not edit this file directly — changes will be overwritten on next sync.',
|
|
224
237
|
'Regenerate: construct sync',
|
|
225
238
|
'-->',
|
|
226
239
|
'',
|
|
227
|
-
'> Generated from `
|
|
240
|
+
'> Generated from `specialists/registry.json`. Edit the registry, then run `construct sync`.',
|
|
228
241
|
].join('\n');
|
|
229
242
|
|
|
230
243
|
const standardConstructTools = [
|
|
@@ -350,7 +363,7 @@ function resolveModelChain(entry) {
|
|
|
350
363
|
function mkdirp(dir) { fs.mkdirSync(dir, { recursive: true }); }
|
|
351
364
|
|
|
352
365
|
function adapterName(entry) {
|
|
353
|
-
return entry.
|
|
366
|
+
return entry.isOrchestrator ? entry.name : `${agentPrefix}${entry.name}`;
|
|
354
367
|
}
|
|
355
368
|
|
|
356
369
|
function loadPersonaPrompt(persona) {
|
|
@@ -386,7 +399,7 @@ function buildRoleFooter(entry) {
|
|
|
386
399
|
if (collaborators.length > 0) {
|
|
387
400
|
lines.push(`Collaborators: ${collaborators.map((c) => (c.startsWith("cx-") ? c : `cx-${c}`)).join(", ")}.`);
|
|
388
401
|
}
|
|
389
|
-
if (entry.
|
|
402
|
+
if (entry.isOrchestrator !== true && entry.canEdit === false) {
|
|
390
403
|
lines.push("Do not implement code or edit source files.");
|
|
391
404
|
}
|
|
392
405
|
if (entry.returnsStructured !== false) {
|
|
@@ -432,7 +445,7 @@ function buildPrompt(entry, allEntries, platform) {
|
|
|
432
445
|
console.error(
|
|
433
446
|
`[sync] Hard cap exceeded. Options:\n` +
|
|
434
447
|
` - trim the prompt body or move detail to a skill (preferred)\n` +
|
|
435
|
-
` - set "wordCapOverride": <N> on this entry in
|
|
448
|
+
` - set "wordCapOverride": <N> on this entry in specialists/registry.json with a written reason\n` +
|
|
436
449
|
` - re-run with --force or CONSTRUCT_SYNC_FORCE=1 as a temporary escape hatch\n` +
|
|
437
450
|
`Prompt budget is a hard contract because every over-cap agent degrades every session that dispatches it.`,
|
|
438
451
|
);
|
|
@@ -489,18 +502,19 @@ function removeStaleAdapters(dir, ext, entries) {
|
|
|
489
502
|
function buildEntries() {
|
|
490
503
|
const entries = [];
|
|
491
504
|
|
|
492
|
-
|
|
493
|
-
|
|
505
|
+
// Orchestrator entry
|
|
506
|
+
if (registry.orchestrator) {
|
|
507
|
+
const orchestratorCanEdit = registry.orchestrator.permissions?.edit === "allow";
|
|
494
508
|
entries.push({
|
|
495
|
-
...
|
|
496
|
-
|
|
497
|
-
prompt: loadPersonaPrompt(
|
|
498
|
-
codexSandbox:
|
|
499
|
-
reasoningEffort:
|
|
509
|
+
...registry.orchestrator,
|
|
510
|
+
isOrchestrator: true,
|
|
511
|
+
prompt: loadPersonaPrompt(registry.orchestrator),
|
|
512
|
+
codexSandbox: registry.orchestrator.codexSandbox ?? (orchestratorCanEdit ? "workspace-write" : "read-only"),
|
|
513
|
+
reasoningEffort: registry.orchestrator.reasoningEffort ?? "high",
|
|
500
514
|
});
|
|
501
515
|
}
|
|
502
516
|
|
|
503
|
-
// Filter
|
|
517
|
+
// Filter specialists by the active profile's role set. RND lists every specialist
|
|
504
518
|
// so behavior is unchanged for the default; non-RND profiles emit only the
|
|
505
519
|
// specialists they declare. Profiles whose role list is empty (legacy/test)
|
|
506
520
|
// fall through to no filter so callers don't get a silent empty registry.
|
|
@@ -509,11 +523,11 @@ function buildEntries() {
|
|
|
509
523
|
? new Set(activeProfile.roles)
|
|
510
524
|
: null;
|
|
511
525
|
|
|
512
|
-
for (const
|
|
513
|
-
if (profileRoles && !profileRoles.has(
|
|
526
|
+
for (const specialist of registry.specialists ?? []) {
|
|
527
|
+
if (profileRoles && !profileRoles.has(specialist.name)) continue;
|
|
514
528
|
entries.push({
|
|
515
|
-
...
|
|
516
|
-
|
|
529
|
+
...specialist,
|
|
530
|
+
isSpecialist: true,
|
|
517
531
|
});
|
|
518
532
|
}
|
|
519
533
|
|
|
@@ -638,16 +652,16 @@ function syncClaude(entries, targetDir = null) {
|
|
|
638
652
|
if (!targetDir) {
|
|
639
653
|
const claudeMdPath = path.join(home, ".claude", "CLAUDE.md");
|
|
640
654
|
const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, "utf8") : "# Claude Global Instructions\n";
|
|
641
|
-
const personaList = entries.filter((e) => e.
|
|
642
|
-
// Only surface non-internal
|
|
643
|
-
const
|
|
655
|
+
const personaList = entries.filter((e) => e.isOrchestrator).map((e) => `- \`${adapterName(e)}\`: ${e.role} — ${e.description}`).join("\n");
|
|
656
|
+
// Only surface non-internal specialists in CLAUDE.md — internal specialists are dispatched by Construct, not invoked by users
|
|
657
|
+
const specialistList = entries.filter((e) => !e.isOrchestrator && !e.internal).map((e) => `- \`${adapterName(e)}\`: ${e.description}`).join("\n");
|
|
644
658
|
const note = `## ${systemName.charAt(0).toUpperCase() + systemName.slice(1)} Personas
|
|
645
659
|
|
|
646
660
|
${personaList}
|
|
647
661
|
|
|
648
662
|
## Internal Specialists
|
|
649
663
|
|
|
650
|
-
${
|
|
664
|
+
${specialistList || "(all specialists are internal — routed through Construct)"}`;
|
|
651
665
|
writeFile(claudeMdPath, replaceManagedBlock(existing, note, mdManagedStart, mdManagedEnd));
|
|
652
666
|
|
|
653
667
|
// Sync MCP servers into ~/.claude/settings.json if it exists
|
|
@@ -671,9 +685,13 @@ ${agentList || "(all specialists are internal — routed through Construct)"}`;
|
|
|
671
685
|
if (!settings.mcpServers) settings.mcpServers = {};
|
|
672
686
|
const registryMcp = registry.mcpServers ?? {};
|
|
673
687
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
674
|
-
const
|
|
688
|
+
const existingEntry = settings.mcpServers[id];
|
|
689
|
+
const existing = JSON.stringify(existingEntry ?? "");
|
|
675
690
|
const hasPlaceholder = existing.includes("__");
|
|
676
|
-
|
|
691
|
+
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
692
|
+
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
693
|
+
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
694
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
677
695
|
settings.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
678
696
|
}
|
|
679
697
|
if (!DRY_RUN) fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
@@ -711,8 +729,8 @@ function hasCodexMcpTable(text, id) {
|
|
|
711
729
|
return new RegExp(`^\\[mcp_servers\\.(?:${escapeRegExp(id)}|${escapeRegExp(tomlString(id))})\\]`, "m").test(text);
|
|
712
730
|
}
|
|
713
731
|
|
|
714
|
-
function isCodexMcpSupported(
|
|
715
|
-
return
|
|
732
|
+
function isCodexMcpSupported() {
|
|
733
|
+
return true;
|
|
716
734
|
}
|
|
717
735
|
|
|
718
736
|
function syncCodex(entries) {
|
|
@@ -834,9 +852,13 @@ function syncVSCode() {
|
|
|
834
852
|
if (!settings["github.copilot.mcpServers"]) settings["github.copilot.mcpServers"] = {};
|
|
835
853
|
const mcpServers = settings["github.copilot.mcpServers"];
|
|
836
854
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
837
|
-
const
|
|
855
|
+
const existingEntry = mcpServers[id];
|
|
856
|
+
const existing = JSON.stringify(existingEntry ?? "");
|
|
838
857
|
const hasPlaceholder = existing.includes("__");
|
|
839
|
-
|
|
858
|
+
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
859
|
+
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
860
|
+
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
861
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
840
862
|
mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
841
863
|
}
|
|
842
864
|
settings["github.copilot.mcpServers"] = mcpServers;
|
|
@@ -862,9 +884,13 @@ function syncCursor() {
|
|
|
862
884
|
const config = JSON.parse(fs.readFileSync(cursorMcpPath, "utf8"));
|
|
863
885
|
if (!config.mcpServers) config.mcpServers = {};
|
|
864
886
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
865
|
-
const
|
|
887
|
+
const existingEntry = config.mcpServers[id];
|
|
888
|
+
const existing = JSON.stringify(existingEntry ?? "");
|
|
866
889
|
const hasPlaceholder = existing.includes("__");
|
|
867
|
-
|
|
890
|
+
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
891
|
+
const existingIsRemote = existingEntry && (existingEntry.type === 'http' || existingEntry.type === 'remote');
|
|
892
|
+
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
893
|
+
if (existingEntry && !hasPlaceholder && !transportMismatch) continue;
|
|
868
894
|
config.mcpServers[id] = buildClaudeMcpEntry(id, mcpDef, process.env);
|
|
869
895
|
}
|
|
870
896
|
if (!DRY_RUN) fs.writeFileSync(cursorMcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
@@ -963,14 +989,21 @@ function syncOpencode(entries) {
|
|
|
963
989
|
for (const [id, mcpDef] of Object.entries(registryMcp)) {
|
|
964
990
|
const openCodeId = getOpenCodeMcpId(id);
|
|
965
991
|
if (openCodeId !== id) delete config.mcp[id];
|
|
966
|
-
if (id === 'memory' && config.mcp.cass && !config.mcp[openCodeId]) {
|
|
967
|
-
config.mcp[openCodeId] = config.mcp.cass;
|
|
968
|
-
}
|
|
969
992
|
if (id === 'memory') delete config.mcp.cass;
|
|
970
|
-
|
|
971
|
-
|
|
993
|
+
|
|
994
|
+
// Migrate the legacy cass-memory HTTP entry to the stdio bridge. cm v0.2.x
|
|
995
|
+
// rejects the MCP handshake (OpenCode surfaces 405 on the SSE GET), so
|
|
996
|
+
// any remote/http memory entry must be rewritten when the registry
|
|
997
|
+
// defines a command-based bridge.
|
|
998
|
+
const existingEntry = config.mcp[openCodeId];
|
|
999
|
+
const registryWantsCommand = !mcpDef.type && Array.isArray(mcpDef.args);
|
|
1000
|
+
const existingIsRemote = existingEntry && (existingEntry.type === 'remote' || existingEntry.type === 'http');
|
|
1001
|
+
const transportMismatch = registryWantsCommand && existingIsRemote;
|
|
1002
|
+
|
|
1003
|
+
const existing = JSON.stringify(existingEntry ?? "");
|
|
1004
|
+
const hasPlaceholder = existing.includes("__");
|
|
972
1005
|
const argsHaveTemplates = (mcpDef.args ?? []).some((a) => typeof a === 'string' && a.includes('__'));
|
|
973
|
-
if (!
|
|
1006
|
+
if (!existingEntry || hasPlaceholder || argsHaveTemplates || transportMismatch) {
|
|
974
1007
|
config.mcp[openCodeId] = buildOpenCodeMcpEntry(id, mcpDef, process.env).entry;
|
|
975
1008
|
}
|
|
976
1009
|
}
|
|
@@ -980,8 +1013,8 @@ function syncOpencode(entries) {
|
|
|
980
1013
|
const prefixes = [agentPrefix];
|
|
981
1014
|
for (const key of Object.keys(config.agent)) {
|
|
982
1015
|
const isManaged = prefixes.some((p) => key.startsWith(p));
|
|
983
|
-
const
|
|
984
|
-
if ((isManaged ||
|
|
1016
|
+
const isOrchestrator = registry.orchestrator?.name === key;
|
|
1017
|
+
if ((isManaged || isOrchestrator) && !entries.find((e) => adapterName(e) === key)) {
|
|
985
1018
|
delete config.agent[key];
|
|
986
1019
|
}
|
|
987
1020
|
}
|
|
@@ -991,10 +1024,10 @@ function syncOpencode(entries) {
|
|
|
991
1024
|
const name = adapterName(entry);
|
|
992
1025
|
const perms = opencodePermissions(entry);
|
|
993
1026
|
config.agent[name] = {
|
|
994
|
-
description: entry.
|
|
1027
|
+
description: entry.isOrchestrator
|
|
995
1028
|
? `${entry.role} — ${entry.description}`
|
|
996
1029
|
: entry.description,
|
|
997
|
-
mode: entry.
|
|
1030
|
+
mode: entry.isOrchestrator ? "all" : "subagent",
|
|
998
1031
|
prompt: buildPrompt(entry, entries, "opencode"),
|
|
999
1032
|
permission: {
|
|
1000
1033
|
...perms,
|
|
@@ -1071,6 +1104,75 @@ function syncCommands(targetDir = null) {
|
|
|
1071
1104
|
return count;
|
|
1072
1105
|
}
|
|
1073
1106
|
|
|
1107
|
+
// --- Skills sync ---
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Walk skills/ recursively and collect every .md file (flat) and every
|
|
1111
|
+
* <name>/SKILL.md directory entry. Returns [{ name, content }] where name
|
|
1112
|
+
* is the slash-relative path without extension, e.g. "strategy/narrative-arc".
|
|
1113
|
+
*/
|
|
1114
|
+
function collectSkills() {
|
|
1115
|
+
const skillsDir = path.join(root, "skills");
|
|
1116
|
+
const results = [];
|
|
1117
|
+
|
|
1118
|
+
function walk(dir, prefix) {
|
|
1119
|
+
if (!fs.existsSync(dir)) return;
|
|
1120
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1121
|
+
const full = path.join(dir, entry.name);
|
|
1122
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1123
|
+
|
|
1124
|
+
if (entry.isDirectory()) {
|
|
1125
|
+
// Check if this is a SKILL.md directory form: <name>/SKILL.md
|
|
1126
|
+
const skillMdPath = path.join(full, "SKILL.md");
|
|
1127
|
+
if (fs.existsSync(skillMdPath)) {
|
|
1128
|
+
// Avoid double-walking; record this skill and do not descend further.
|
|
1129
|
+
const name = rel;
|
|
1130
|
+
results.push({ name, content: fs.readFileSync(skillMdPath, "utf8") });
|
|
1131
|
+
} else {
|
|
1132
|
+
walk(full, rel);
|
|
1133
|
+
}
|
|
1134
|
+
} else if (entry.name.endsWith(".md") && entry.name !== "routing.md" && entry.name !== "SKILL.md") {
|
|
1135
|
+
const name = rel.replace(/\.md$/, "");
|
|
1136
|
+
results.push({ name, content: fs.readFileSync(full, "utf8") });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
walk(skillsDir, "");
|
|
1142
|
+
return results;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Prefix prepended to every generated SKILL.md. The comment must survive
|
|
1147
|
+
* round-trips — it is the canonical signal that a file was produced by sync
|
|
1148
|
+
* and must not be hand-edited.
|
|
1149
|
+
*/
|
|
1150
|
+
const SKILL_GENERATED_PREFIX = "# Generated by construct sync\n\n";
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Write collected skills to both .claude/skills/ and .agents/skills/ in
|
|
1154
|
+
* SKILL.md directory format.
|
|
1155
|
+
*
|
|
1156
|
+
* Idempotency: content written is deterministic; writing the same content
|
|
1157
|
+
* twice is a no-op because writeFile skips unchanged files in dry-run mode
|
|
1158
|
+
* and two-phase staging in normal mode produces identical results.
|
|
1159
|
+
*/
|
|
1160
|
+
function syncSkills() {
|
|
1161
|
+
const skills = collectSkills();
|
|
1162
|
+
if (skills.length === 0) return 0;
|
|
1163
|
+
|
|
1164
|
+
const claudeSkillsDir = path.join(home, ".claude", "skills");
|
|
1165
|
+
const agentsSkillsDir = path.join(home, ".agents", "skills");
|
|
1166
|
+
|
|
1167
|
+
for (const { name, content } of skills) {
|
|
1168
|
+
const generated = `${SKILL_GENERATED_PREFIX}${content}`;
|
|
1169
|
+
writeFile(path.join(claudeSkillsDir, name, "SKILL.md"), generated);
|
|
1170
|
+
writeFile(path.join(agentsSkillsDir, name, "SKILL.md"), generated);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
return skills.length;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1074
1176
|
// --- Main ---
|
|
1075
1177
|
|
|
1076
1178
|
const projectDir = process.argv.includes("--project") ? process.cwd() : null;
|
|
@@ -1086,7 +1188,7 @@ if (COMPRESS_PERSONAS) {
|
|
|
1086
1188
|
let totalIn = 0;
|
|
1087
1189
|
let totalOut = 0;
|
|
1088
1190
|
for (const entry of entries) {
|
|
1089
|
-
if (!entry.
|
|
1191
|
+
if (!entry.isOrchestrator || !entry.prompt) continue;
|
|
1090
1192
|
const before = entry.prompt;
|
|
1091
1193
|
try {
|
|
1092
1194
|
const after = await compressor.compress(before, { ratio: 0.6 });
|
|
@@ -1101,7 +1203,7 @@ if (COMPRESS_PERSONAS) {
|
|
|
1101
1203
|
}
|
|
1102
1204
|
if (totalIn > 0) {
|
|
1103
1205
|
const ratio = ((totalOut / totalIn) * 100).toFixed(0);
|
|
1104
|
-
console.log(`compress-personas: ${totalIn} → ${totalOut} chars (${ratio}%) across ${entries.filter((e) => e.
|
|
1206
|
+
console.log(`compress-personas: ${totalIn} → ${totalOut} chars (${ratio}%) across ${entries.filter((e) => e.isOrchestrator).length} personas`);
|
|
1105
1207
|
}
|
|
1106
1208
|
}
|
|
1107
1209
|
|
|
@@ -1117,8 +1219,8 @@ try {
|
|
|
1117
1219
|
console.log(`Synced ${entries.length} agents + ${cmdCount} commands to ${path.join(projectDir, ".claude/")} (project mode).`);
|
|
1118
1220
|
}
|
|
1119
1221
|
} else {
|
|
1120
|
-
const personaCount = entries.filter((e) => e.
|
|
1121
|
-
const
|
|
1222
|
+
const personaCount = entries.filter((e) => e.isOrchestrator).length;
|
|
1223
|
+
const specialistCount = entries.filter((e) => !e.isOrchestrator).length;
|
|
1122
1224
|
|
|
1123
1225
|
syncCodex(entries);
|
|
1124
1226
|
syncClaude(entries);
|
|
@@ -1127,6 +1229,7 @@ try {
|
|
|
1127
1229
|
const vscodeOk = syncVSCode();
|
|
1128
1230
|
const cursorOk = syncCursor();
|
|
1129
1231
|
const cmdCount = syncCommands();
|
|
1232
|
+
const skillCount = syncSkills();
|
|
1130
1233
|
|
|
1131
1234
|
if (DRY_RUN) {
|
|
1132
1235
|
printDryRunDiff();
|
|
@@ -1141,7 +1244,7 @@ try {
|
|
|
1141
1244
|
vscodeOk && "VS Code",
|
|
1142
1245
|
cursorOk && "Cursor",
|
|
1143
1246
|
].filter(Boolean).join(", ");
|
|
1144
|
-
console.log(`Synced ${personaCount}
|
|
1247
|
+
console.log(`Synced ${personaCount} orchestrator + ${specialistCount} specialists + ${cmdCount} commands + ${skillCount} skills to ${targets}.`);
|
|
1145
1248
|
|
|
1146
1249
|
// Regenerate shell completions so new commands are immediately tab-completable
|
|
1147
1250
|
const completionsDir = generateCompletions();
|
|
@@ -6,7 +6,7 @@ and the agent registry + construct sync as the deployment layer.
|
|
|
6
6
|
-->
|
|
7
7
|
# Prompt Auto-Optimization Loop
|
|
8
8
|
|
|
9
|
-
Construct's prompt improvement system uses telemetry traces and quality scores as the feedback signal, Claude as the optimizer, and the agent registry (`
|
|
9
|
+
Construct's prompt improvement system uses telemetry traces and quality scores as the feedback signal, Claude as the optimizer, and the agent registry (`specialists/registry.json`) as the deployment layer. This is a closed loop: production data → failure analysis → improved prompt → staging → promotion.
|
|
10
10
|
|
|
11
11
|
## Running the optimizer
|
|
12
12
|
|
|
@@ -34,7 +34,7 @@ The optimizer requires Python 3.12+ (`pip3` must be available). It will auto-ins
|
|
|
34
34
|
|
|
35
35
|
## Step 1: Gather signal
|
|
36
36
|
|
|
37
|
-
Retrieve the current production prompt for the target agent from `
|
|
37
|
+
Retrieve the current production prompt for the target agent from `specialists/registry.json` (or the corresponding `promptFile` if using extracted prompts).
|
|
38
38
|
|
|
39
39
|
Then retrieve recent traces via the telemetry backend REST API:
|
|
40
40
|
|
|
@@ -76,7 +76,7 @@ Write an improved prompt that directly addresses the diagnosed failures. Rules:
|
|
|
76
76
|
|
|
77
77
|
## Step 4: Push to staging
|
|
78
78
|
|
|
79
|
-
Update the agent's prompt in `
|
|
79
|
+
Update the agent's prompt in `specialists/registry.json` (or the corresponding `promptFile`) with a staging marker comment. Tag the version by writing to `.cx/decisions/prompt-staging-{agent}-{date}.md`.
|
|
80
80
|
|
|
81
81
|
Log the candidate prompt as a span attribute on a test run batch using `cx_trace`. Tag spans with `promptVersion: staging-{timestamp}` and score them with `cx_score` as traces complete.
|
|
82
82
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
skills/exploration/dependency-graph-reading.md (Dependency Graph Reading)
|
|
3
|
+
Use when auditing a project's supply chain risk, evaluating upgrade paths,
|
|
4
|
+
or assessing the risk surface of a new codebase. Covers npm/cargo/maven
|
|
5
|
+
lockfile interpretation, transitive dependency patterns, and risk signals.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Dependency Graph Reading
|
|
9
|
+
|
|
10
|
+
Use when assessing the risk surface of a project's dependencies, planning upgrades, or evaluating a new codebase's supply chain posture.
|
|
11
|
+
|
|
12
|
+
## npm / package-lock.json
|
|
13
|
+
|
|
14
|
+
The lockfile is the authoritative record of what actually runs in production.
|
|
15
|
+
|
|
16
|
+
**What `package-lock.json` tells you:**
|
|
17
|
+
- Exact resolved versions of every dependency (direct + transitive).
|
|
18
|
+
- The registry URL each package resolved from (watch for non-npm registry sources).
|
|
19
|
+
- Integrity hashes for tamper detection (missing or mismatched hashes are a supply chain signal).
|
|
20
|
+
|
|
21
|
+
**Commands:**
|
|
22
|
+
```bash
|
|
23
|
+
# Tree of direct dependencies with their transitive deps
|
|
24
|
+
npm ls --all --depth=5 <package>
|
|
25
|
+
|
|
26
|
+
# Why is this package installed? (shows the resolution path)
|
|
27
|
+
npm ls <package>
|
|
28
|
+
|
|
29
|
+
# Outdated direct dependencies
|
|
30
|
+
npm outdated
|
|
31
|
+
|
|
32
|
+
# Audit for known CVEs
|
|
33
|
+
npm audit --json | jq '.vulnerabilities | to_entries | sort_by(.value.severity) | reverse[]'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Risk signals in the lockfile:**
|
|
37
|
+
- Packages sourced from `github:`, `file:`, or private registries, higher supply-chain risk.
|
|
38
|
+
- Very old `lockfileVersion` (v1), may not capture integrity hashes properly.
|
|
39
|
+
- Transitive dependencies with `requires: {}` instead of resolved versions, resolution ambiguity.
|
|
40
|
+
|
|
41
|
+
## Cargo.lock (Rust)
|
|
42
|
+
|
|
43
|
+
Cargo maintains a flat lockfile with explicit source declarations.
|
|
44
|
+
|
|
45
|
+
**Key fields:**
|
|
46
|
+
- `[[package]]`: each entry is a resolved crate version.
|
|
47
|
+
- `source = "registry+..."`: standard crates.io source. `source = "git+..."`: git dependency (less auditable).
|
|
48
|
+
- `checksum`: SHA256 of the downloaded crate.
|
|
49
|
+
|
|
50
|
+
**Commands:**
|
|
51
|
+
```bash
|
|
52
|
+
# Dependency tree with feature flags
|
|
53
|
+
cargo tree
|
|
54
|
+
|
|
55
|
+
# Find all versions of a crate (version conflicts)
|
|
56
|
+
cargo tree -d
|
|
57
|
+
|
|
58
|
+
# Audit for known vulnerabilities
|
|
59
|
+
cargo audit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Maven / gradle (JVM)
|
|
63
|
+
|
|
64
|
+
The effective POM / resolved configuration tells you what actually runs.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Full dependency tree with transitive
|
|
68
|
+
mvn dependency:tree -Dverbose
|
|
69
|
+
|
|
70
|
+
# Resolved versions for a specific scope
|
|
71
|
+
mvn dependency:list -Dscope=compile
|
|
72
|
+
|
|
73
|
+
# Gradle
|
|
74
|
+
./gradlew dependencies --configuration runtimeClasspath
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Risk signals:**
|
|
78
|
+
- Version range declarations (`[1.0,)`), means any future version could be resolved at build time.
|
|
79
|
+
- `SNAPSHOT` versions in non-snapshot builds, unstable artifact references.
|
|
80
|
+
- Duplicate versions of the same artifact (version conflict, one wins, one is silently dropped).
|
|
81
|
+
|
|
82
|
+
## Cross-ecosystem risk signals
|
|
83
|
+
|
|
84
|
+
| Signal | Risk | Action |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| Dependency with <100 weekly downloads | Supply chain (abandoned / obscure) | Evaluate inlining or replacing |
|
|
87
|
+
| Dependency last published >2 years ago | Maintenance risk | Check for forks or replacements |
|
|
88
|
+
| Deep transitive chain (>10 levels) | Blast radius | Map critical path; isolate |
|
|
89
|
+
| Single maintainer with no 2FA | Takeover risk | Monitor; prefer alternatives with org ownership |
|
|
90
|
+
| Package name squatting (typosquat) | Injection risk | Verify exact package name + registry source |
|
|
91
|
+
| Wildcard `*` version pin | Reproducibility failure | Lock to exact version |
|
|
92
|
+
|
|
93
|
+
## Practical audit workflow
|
|
94
|
+
|
|
95
|
+
1. **Enumerate the blast radius**: which packages, if compromised, have write access to your production environment or sensitive data? These are the highest-risk nodes.
|
|
96
|
+
2. **Check freshness on the critical path**: `npm audit`, `cargo audit`, `snyk test`. Focus on high/critical severity findings that affect runtime code (not devDependencies).
|
|
97
|
+
3. **Identify orphaned dependencies**: packages no code imports directly but appear in the lockfile. May be safe to remove, reducing surface area.
|
|
98
|
+
4. **Check for version conflicts**: multiple resolved versions of the same package indicate an incomplete deduplication. Usually benign; occasionally a security issue if an old insecure version is in the tree.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
skills/exploration/tracer-bullet-method.md (Tracer Bullet Method)
|
|
3
|
+
Use when starting implementation in an unfamiliar or complex system.
|
|
4
|
+
A tracer bullet is a minimal end-to-end path through the system,
|
|
5
|
+
real code, real infrastructure, real calls, before any deep work begins.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Tracer Bullet Method
|
|
9
|
+
|
|
10
|
+
Use when beginning implementation in a new system, new integration, or new architectural layer. A tracer bullet is not a prototype; it is the thinnest possible complete path through the real system.
|
|
11
|
+
|
|
12
|
+
## What a tracer bullet is
|
|
13
|
+
|
|
14
|
+
A tracer bullet hits every layer of the architecture, UI, API, business logic, data store, external service, with real code and real infrastructure. It is not a mock; it is a thin vertical slice.
|
|
15
|
+
|
|
16
|
+
**The goal:** prove that the end-to-end path works before committing to the full implementation.
|
|
17
|
+
|
|
18
|
+
**What it is not:**
|
|
19
|
+
- A prototype (a prototype tests a concept; a tracer bullet tests the infrastructure).
|
|
20
|
+
- A spike (a spike is time-boxed exploration; a tracer bullet produces code you keep).
|
|
21
|
+
- A stub (stubs replace real behavior; the tracer bullet uses real behavior).
|
|
22
|
+
|
|
23
|
+
## When to use it
|
|
24
|
+
|
|
25
|
+
Use a tracer bullet when:
|
|
26
|
+
- You're integrating two systems for the first time and don't know where the friction is.
|
|
27
|
+
- You're adding a new capability to a system with unfamiliar internals.
|
|
28
|
+
- The architecture requires multiple components to cooperate, and you need to verify the cooperation works before building each component out.
|
|
29
|
+
- You're prototyping a new data model that touches multiple layers.
|
|
30
|
+
|
|
31
|
+
## How to fire one
|
|
32
|
+
|
|
33
|
+
**Step 1: Define the thinnest path**
|
|
34
|
+
|
|
35
|
+
What is the simplest possible user action (or API call) that touches every layer you need to implement? This is your target path.
|
|
36
|
+
|
|
37
|
+
Example: for a new feature "user uploads an image":
|
|
38
|
+
- Thinnest path: user sends a single 1KB image, it is stored, and the storage URL is returned.
|
|
39
|
+
|
|
40
|
+
**Step 2: Identify every seam**
|
|
41
|
+
|
|
42
|
+
Map the layers the path crosses:
|
|
43
|
+
- Presentation: what receives the request?
|
|
44
|
+
- Transport: what format does the request take?
|
|
45
|
+
- Validation: what checks the input?
|
|
46
|
+
- Business logic: what transforms or routes it?
|
|
47
|
+
- Persistence: what stores it?
|
|
48
|
+
- External services: what calls are made?
|
|
49
|
+
|
|
50
|
+
**Step 3: Implement one thin vertical slice**
|
|
51
|
+
|
|
52
|
+
Write just enough code to make the tracer path work end-to-end. No error handling, no edge cases, no optimization. The only question is: does the path close?
|
|
53
|
+
|
|
54
|
+
**Step 4: Verify closure**
|
|
55
|
+
|
|
56
|
+
Run it. Check each layer produced the expected artifact. A tracer bullet that doesn't reach the end is valuable, it tells you exactly where the friction is before you've invested in the full implementation.
|
|
57
|
+
|
|
58
|
+
**Step 5: Expand**
|
|
59
|
+
|
|
60
|
+
Once the path closes, add error handling, validation, edge cases, and tests. You are now implementing on a system you know works.
|
|
61
|
+
|
|
62
|
+
## Tracer bullet vs. walking skeleton
|
|
63
|
+
|
|
64
|
+
A walking skeleton (Cockburn) is similar but broader: the minimum architecture that demonstrates a complete feature. A tracer bullet is more specific, it is a single request path through an existing or new system. The terms are used interchangeably in practice; the distinction matters less than the principle.
|
|
65
|
+
|
|
66
|
+
## Anti-patterns
|
|
67
|
+
|
|
68
|
+
- **Mocking too much**: if the tracer bullet uses mocked services, you haven't proved the real path works. Mocks are fine for unit tests, not for the tracer.
|
|
69
|
+
- **Doing too much at once**: if the tracer path covers 5 new capabilities, you won't know which one caused the failure. Fire one bullet at a time.
|
|
70
|
+
- **Skipping the tracer for "simple" changes**: the changes most likely to surprise you are the ones that seem simple. Fire anyway.
|
|
71
|
+
- **Throwing the bullet away**: a tracer bullet is production-quality code on a thin path. It is not throwaway. The skeleton expands from it.
|