@geraldmaron/construct 1.0.6 → 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.
Files changed (123) hide show
  1. package/README.md +19 -17
  2. package/agents/contracts.json +617 -2
  3. package/agents/contracts.schema.json +83 -0
  4. package/agents/prompts/cx-accessibility.md +2 -0
  5. package/agents/prompts/cx-ai-engineer.md +2 -0
  6. package/agents/prompts/cx-architect.md +2 -0
  7. package/agents/prompts/cx-business-strategist.md +2 -0
  8. package/agents/prompts/cx-data-analyst.md +2 -0
  9. package/agents/prompts/cx-data-engineer.md +2 -0
  10. package/agents/prompts/cx-debugger.md +2 -0
  11. package/agents/prompts/cx-designer.md +2 -0
  12. package/agents/prompts/cx-devil-advocate.md +2 -0
  13. package/agents/prompts/cx-docs-keeper.md +2 -0
  14. package/agents/prompts/cx-engineer.md +2 -0
  15. package/agents/prompts/cx-evaluator.md +2 -0
  16. package/agents/prompts/cx-explorer.md +2 -0
  17. package/agents/prompts/cx-legal-compliance.md +2 -0
  18. package/agents/prompts/cx-operations.md +2 -0
  19. package/agents/prompts/cx-orchestrator.md +2 -0
  20. package/agents/prompts/cx-platform-engineer.md +2 -0
  21. package/agents/prompts/cx-product-manager.md +2 -0
  22. package/agents/prompts/cx-qa.md +2 -0
  23. package/agents/prompts/cx-rd-lead.md +2 -0
  24. package/agents/prompts/cx-release-manager.md +2 -0
  25. package/agents/prompts/cx-researcher.md +2 -0
  26. package/agents/prompts/cx-reviewer.md +2 -0
  27. package/agents/prompts/cx-security.md +2 -0
  28. package/agents/prompts/cx-sre.md +2 -0
  29. package/agents/prompts/cx-test-automation.md +2 -0
  30. package/agents/prompts/cx-trace-reviewer.md +2 -0
  31. package/agents/prompts/cx-ux-researcher.md +2 -0
  32. package/agents/registry.json +8 -3
  33. package/bin/construct +209 -32
  34. package/commands/build/feature.md +1 -1
  35. package/commands/build/fix.md +1 -1
  36. package/commands/design/access.md +1 -1
  37. package/commands/design/flow.md +1 -1
  38. package/commands/design/ui.md +1 -1
  39. package/commands/measure/experiment.md +1 -1
  40. package/commands/measure/metrics.md +1 -1
  41. package/commands/measure/results.md +1 -1
  42. package/commands/plan/api.md +1 -1
  43. package/commands/plan/challenge.md +1 -1
  44. package/commands/plan/decide.md +1 -1
  45. package/commands/plan/feature.md +1 -1
  46. package/commands/plan/requirements.md +1 -1
  47. package/commands/remember/context.md +1 -1
  48. package/commands/remember/handoff.md +1 -1
  49. package/commands/remember/runbook.md +1 -1
  50. package/commands/review/code.md +1 -1
  51. package/commands/review/quality.md +1 -1
  52. package/commands/review/security.md +1 -1
  53. package/commands/ship/ready.md +1 -1
  54. package/commands/ship/release.md +1 -1
  55. package/commands/ship/status.md +1 -1
  56. package/commands/understand/docs.md +1 -1
  57. package/commands/understand/this.md +1 -1
  58. package/commands/understand/why.md +1 -1
  59. package/commands/work/clean.md +1 -1
  60. package/commands/work/drive.md +1 -1
  61. package/commands/work/optimize-prompts.md +1 -1
  62. package/commands/work/parallel-review.md +1 -1
  63. package/lib/auto-docs.mjs +7 -7
  64. package/lib/boundary.mjs +126 -0
  65. package/lib/cache-strategy-google.js +26 -31
  66. package/lib/comment-lint.mjs +134 -0
  67. package/lib/contracts/validate.mjs +323 -0
  68. package/lib/daemons/contract.mjs +210 -0
  69. package/lib/docs-verify.mjs +59 -6
  70. package/lib/doctor/cli.mjs +16 -1
  71. package/lib/doctor/index.mjs +3 -1
  72. package/lib/doctor/watchers/consistency.mjs +310 -0
  73. package/lib/doctor/watchers/mcp-protocol.mjs +232 -0
  74. package/lib/document-extract.mjs +211 -1
  75. package/lib/embed/cli.mjs +124 -3
  76. package/lib/embed/daemon.mjs +43 -4
  77. package/lib/embed/docs-lifecycle.mjs +1 -1
  78. package/lib/embed/inbox.mjs +2 -0
  79. package/lib/embed/scheduler.mjs +33 -5
  80. package/lib/evaluator-optimizer.mjs +2 -3
  81. package/lib/hooks/comment-lint.mjs +16 -0
  82. package/lib/hooks/mcp-audit.mjs +2 -1
  83. package/lib/hooks/proactive-activation.mjs +0 -14
  84. package/lib/hooks/rule-verifier.mjs +217 -0
  85. package/lib/hooks/session-optimize.mjs +2 -1
  86. package/lib/init-unified.mjs +55 -65
  87. package/lib/intake/classify.mjs +108 -24
  88. package/lib/intake/daemon.mjs +121 -0
  89. package/lib/intake/filesystem-queue.mjs +6 -1
  90. package/lib/intake/intake-config.mjs +2 -1
  91. package/lib/intake/prepare.mjs +0 -1
  92. package/lib/intake/session-prelude.mjs +7 -1
  93. package/lib/intake/traceability.mjs +90 -0
  94. package/lib/knowledge/research-store.mjs +2 -0
  95. package/lib/maintenance/cleanup.mjs +315 -0
  96. package/lib/mcp/memory-bridge.mjs +276 -0
  97. package/lib/mcp/server.mjs +2 -0
  98. package/lib/mcp/tools/workflow.mjs +25 -0
  99. package/lib/mcp-catalog.json +12 -8
  100. package/lib/mcp-platform-config.mjs +16 -8
  101. package/lib/migrations/index.mjs +106 -0
  102. package/lib/migrations/v1-baseline.mjs +33 -0
  103. package/lib/observation-store.mjs +9 -4
  104. package/lib/outcomes/record.mjs +2 -0
  105. package/lib/profiles/rebrand.mjs +46 -0
  106. package/lib/project-init-shared.mjs +12 -0
  107. package/lib/provider-capabilities.js +20 -7
  108. package/lib/providers/auth-manager.mjs +58 -17
  109. package/lib/reflect.mjs +49 -12
  110. package/lib/server/index.mjs +22 -28
  111. package/lib/session-store.mjs +6 -4
  112. package/lib/setup.mjs +14 -3
  113. package/lib/telemetry/client.mjs +5 -1
  114. package/lib/version.mjs +51 -0
  115. package/lib/worker/trace.mjs +5 -1
  116. package/package.json +4 -1
  117. package/personas/construct.md +3 -1
  118. package/rules/common/development-workflow.md +2 -1
  119. package/rules/common/no-fabrication.md +69 -0
  120. package/rules/common/review-before-change.md +58 -0
  121. package/scripts/sync-agents.mjs +45 -14
  122. package/templates/docs/construct_guide.md +6 -6
  123. package/templates/docs/skill-artifact.md +1 -1
@@ -235,6 +235,18 @@ Describe the current objective in one clear sentence.
235
235
  `;
236
236
  }
237
237
 
238
+ export function ensureCxDir(rootDir) {
239
+ const cxDir = path.join(rootDir, '.cx');
240
+ const contextPath = path.join(cxDir, 'context.md');
241
+ if (!fs.existsSync(cxDir)) {
242
+ fs.mkdirSync(cxDir, { recursive: true });
243
+ }
244
+ if (!fs.existsSync(contextPath)) {
245
+ fs.writeFileSync(contextPath, buildContextMarkdown(), 'utf8');
246
+ }
247
+ return cxDir;
248
+ }
249
+
238
250
  export function buildContextMarkdown() {
239
251
  return `<!--
240
252
  .cx/context.md — concise resumable project context for human and agent handoff.
@@ -117,23 +117,36 @@ export function resolveProviderCapabilitiesSync(modelId) {
117
117
  }
118
118
 
119
119
  /**
120
- * Probe provider for live capabilities (async).
121
- * Results are cached for 24h.
120
+ * Probe provider for capabilities and cache the result for 24h.
121
+ *
122
+ * Static path (probe=false): resolves capabilities from the adapter's static
123
+ * declaration. Live path (probe=true): dispatches to the adapter's optional
124
+ * `probe(modelId)` export — adapters that don't implement it fall back to the
125
+ * static declaration. Construct itself stays provider-agnostic; per-vendor
126
+ * probing is the adapter's responsibility.
122
127
  *
123
128
  * @param {string} modelId
124
129
  * @param {object} opts - { probe: boolean }
125
130
  * @returns {Promise<object>}
126
131
  */
127
132
  export async function probeProviderCapabilities(modelId, { probe = false } = {}) {
128
- const caps = await resolveProviderCapabilities(modelId);
133
+ const adapterKey = resolveAdapterKey(modelId);
134
+ const loader = ADAPTERS[adapterKey] || ADAPTERS.generic;
129
135
 
136
+ let caps;
130
137
  if (probe) {
131
- // TODO: implement live probing (API call) in Phase A follow-up
132
- // For now, just return the static capabilities
138
+ try {
139
+ const mod = await loader();
140
+ caps = typeof mod.probe === 'function'
141
+ ? await mod.probe(modelId)
142
+ : mod.capabilities(modelId);
143
+ } catch {
144
+ caps = await resolveProviderCapabilities(modelId);
145
+ }
146
+ } else {
147
+ caps = await resolveProviderCapabilities(modelId);
133
148
  }
134
149
 
135
- // Cache by adapter key
136
- const adapterKey = resolveAdapterKey(modelId);
137
150
  const cache = getCache();
138
151
  cache[adapterKey] = caps;
139
152
  writeCapabilityCache(cache);
@@ -1,15 +1,23 @@
1
- // lib/providers/auth-manager.mjs
2
- // OAuth refresh and token lifecycle management
1
+ /**
2
+ * lib/providers/auth-manager.mjs Token lifecycle and refresh dispatch.
3
+ *
4
+ * Provider-agnostic store and contract for credential rotation. The actual
5
+ * refresh flow (OAuth, JWT, vendor-specific) lives in a registered adapter;
6
+ * dispatch goes to the adapter's `refresh(state)` and the result is persisted
7
+ * back to the auth store. Adapters lacking a refresh implementation leave the
8
+ * operator on a reauthenticate-manually path — the safe agnostic default.
9
+ */
3
10
 
4
11
  import fs from 'node:fs';
5
12
  import path from 'node:path';
6
13
  import { homedir } from 'node:os';
7
14
 
8
- const AUTH_DIR = path.join(homedir(), '.construct', 'auth');
9
-
10
- try {
11
- fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
12
- } catch { /* ignore */ }
15
+ function authDir() {
16
+ const home = process.env.HOME || homedir();
17
+ const dir = path.join(home, '.construct', 'auth');
18
+ try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } catch { /* ignore */ }
19
+ return dir;
20
+ }
13
21
 
14
22
  const PROVIDER_CONFIGS = {
15
23
  github: {
@@ -28,9 +36,20 @@ const PROVIDER_CONFIGS = {
28
36
  },
29
37
  };
30
38
 
39
+ const refreshAdapters = new Map();
40
+
41
+ /**
42
+ * Register a refresh adapter for a provider. Contract:
43
+ * adapter(state) -> Promise<{ success, token, refreshToken?, expiresAt? }>
44
+ * If no adapter is registered, refresh returns a manual-reauth instruction.
45
+ */
46
+ export function registerRefreshAdapter(provider, adapter) {
47
+ if (typeof adapter === 'function') refreshAdapters.set(provider, adapter);
48
+ }
49
+
31
50
  export function loadAuthState(provider) {
32
51
  try {
33
- const file = path.join(AUTH_DIR, `${provider}.json`);
52
+ const file = path.join(authDir(), `${provider}.json`);
34
53
  if (fs.existsSync(file)) {
35
54
  return JSON.parse(fs.readFileSync(file, 'utf8'));
36
55
  }
@@ -40,7 +59,7 @@ export function loadAuthState(provider) {
40
59
 
41
60
  export function saveAuthState(provider, state) {
42
61
  try {
43
- const file = path.join(AUTH_DIR, `${provider}.json`);
62
+ const file = path.join(authDir(), `${provider}.json`);
44
63
  fs.writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
45
64
  return true;
46
65
  } catch {
@@ -111,16 +130,38 @@ async function refreshToken(provider) {
111
130
  if (!config?.refreshable) {
112
131
  return { success: false, error: 'Provider does not support refresh' };
113
132
  }
114
-
133
+
115
134
  const state = loadAuthState(provider);
116
135
  if (!state?.refreshToken) {
117
136
  return { success: false, error: 'No refresh token available' };
118
137
  }
119
-
120
- // Provider-specific refresh logic would go here
121
- // For now, just indicate refresh is needed
122
- return {
123
- success: false,
124
- error: 'Token refresh required - reauthenticate manually',
125
- };
138
+
139
+ const adapter = refreshAdapters.get(provider);
140
+ if (!adapter) {
141
+ return {
142
+ success: false,
143
+ error: 'Token refresh required - reauthenticate manually',
144
+ };
145
+ }
146
+
147
+ try {
148
+ const result = await adapter(state);
149
+ if (result?.success && result.token) {
150
+ const next = {
151
+ ...state,
152
+ token: result.token,
153
+ refreshToken: result.refreshToken || state.refreshToken,
154
+ expiresAt: result.expiresAt || null,
155
+ rotatedAt: new Date().toISOString(),
156
+ };
157
+ saveAuthState(provider, next);
158
+ return { success: true, expiresAt: next.expiresAt };
159
+ }
160
+ return {
161
+ success: false,
162
+ error: result?.error || 'Refresh adapter returned no token',
163
+ };
164
+ } catch (err) {
165
+ return { success: false, error: `Refresh adapter threw: ${err.message}` };
166
+ }
126
167
  }
package/lib/reflect.mjs CHANGED
@@ -8,10 +8,11 @@
8
8
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
9
9
  import { join, resolve, basename } from 'node:path';
10
10
  import { execSync } from 'node:child_process';
11
- import { addObservation } from './observation-store.mjs';
11
+ import { addObservation, listObservations } from './observation-store.mjs';
12
12
  import { loadConstructEnv } from './env-config.mjs';
13
13
  import { KNOWLEDGE_ROOT, KNOWLEDGE_SUBDIRS, inferKnowledgeTarget } from './knowledge/layout.mjs';
14
14
  import { extractSessionObservation } from './reflect/extractor.mjs';
15
+ import { readContextState, contextSummaryLine } from './context-state.mjs';
15
16
 
16
17
  const HOME = process.env.HOME || process.env.USERPROFILE;
17
18
  const USER_ENV_PATH = join(HOME, '.construct', 'config.env');
@@ -47,12 +48,19 @@ export async function runReflectCli(args) {
47
48
  target = `knowledge/${target}`;
48
49
  }
49
50
 
50
- // If no summary provided, try to get it from stdin or prompt
51
+ let summarySource = 'cli';
51
52
  if (!summary) {
52
- // For now, we'll require explicit summary - in future could read from chat context
53
- console.error('Error: --summary=<text> is required to capture improvement feedback');
54
- console.error('Example: construct reflect --target=internal --summary="Improve Slack channel intent parsing to handle edge cases"');
55
- process.exit(1);
53
+ const derived = deriveSummaryFromContext(process.cwd());
54
+ if (derived) {
55
+ summary = derived;
56
+ summarySource = 'auto-derived';
57
+ console.error(`Note: --summary not provided; using auto-derived summary from .cx/context.md and recent observations.`);
58
+ console.error(`Captured summary: "${summary.slice(0, 180)}${summary.length > 180 ? '…' : ''}"`);
59
+ } else {
60
+ console.error('Error: --summary=<text> is required (no .cx/context.md or recent session summaries to derive from)');
61
+ console.error('Example: construct reflect --target=internal --summary="Improve Slack channel intent parsing to handle edge cases"');
62
+ process.exit(1);
63
+ }
56
64
  }
57
65
 
58
66
  // Resolve the knowledge directory for this target
@@ -110,15 +118,14 @@ export async function runReflectCli(args) {
110
118
  // Write the file
111
119
  writeFileSync(filePath, content);
112
120
 
113
- // Record an observation about this feedback
114
- const observationSummary = `[reflect] Improvement feedback captured: ${summary.slice(0, 100)}${summary.length > 100 ? '...' : ''}`;
121
+ const observationSummary = `[reflect] Improvement feedback captured (${summarySource}): ${summary.slice(0, 100)}${summary.length > 100 ? '...' : ''}`;
115
122
  addObservation(process.cwd(), {
116
123
  role: 'construct',
117
- category: 'insight', // Feedback is typically an insight unless it's about a problem
124
+ category: 'insight',
118
125
  summary: observationSummary,
119
- content: `Feedback captured via construct reflect:\n\n${summary}\n\nStored as: ${filePath}\nTarget: ${knowledgeSubdir}/`,
120
- tags: ['reflect', 'improvement-feedback', `knowledge:${knowledgeSubdir}`],
121
- confidence: 0.9,
126
+ content: `Feedback captured via construct reflect (${summarySource}):\n\n${summary}\n\nStored as: ${filePath}\nTarget: ${knowledgeSubdir}/`,
127
+ tags: ['reflect', 'improvement-feedback', `knowledge:${knowledgeSubdir}`, `source:${summarySource}`],
128
+ confidence: summarySource === 'auto-derived' ? 0.65 : 0.9,
122
129
  source: 'reflect-command',
123
130
  });
124
131
 
@@ -186,6 +193,36 @@ export async function runReflectAuto({ transcriptPath, cwd, sessionId, durationM
186
193
  return result?.id ?? null;
187
194
  }
188
195
 
196
+ /**
197
+ * Derive a one-paragraph reflect summary from durable session state — the
198
+ * context.md digest first, then the most recent session-summary observation.
199
+ * Returns null when neither source yields useful content.
200
+ */
201
+ export function deriveSummaryFromContext(cwd) {
202
+ const parts = [];
203
+
204
+ try {
205
+ const state = readContextState(cwd);
206
+ const line = contextSummaryLine(state);
207
+ if (line && line.trim().length > 20) parts.push(line.trim());
208
+ } catch { /* best effort */ }
209
+
210
+ try {
211
+ const recent = listObservations(cwd, { limit: 5 }) || [];
212
+ const sessionSummary = recent.find((o) => Array.isArray(o.tags) && o.tags.includes('session-summary'));
213
+ if (sessionSummary?.summary) {
214
+ const text = String(sessionSummary.summary).replace(/^\[[^\]]+\]\s*/, '').trim();
215
+ if (text.length > 20 && !parts.some((p) => p.includes(text.slice(0, 40)))) {
216
+ parts.push(text);
217
+ }
218
+ }
219
+ } catch { /* best effort */ }
220
+
221
+ if (parts.length === 0) return null;
222
+ const joined = parts.join(' — ');
223
+ return joined.length > 600 ? joined.slice(0, 580) + '…' : joined;
224
+ }
225
+
189
226
  function safeProject(cwd) {
190
227
  try { return basename(cwd) || null; } catch { return null; }
191
228
  }
@@ -8,6 +8,7 @@
8
8
  * Runs on port 4242 (overridable via PORT env var), bound to 127.0.0.1.
9
9
  */
10
10
  import { createServer } from 'http';
11
+ import { registerBoundary } from '../boundary.mjs';
11
12
  import { readFileSync, writeFileSync, statSync, watch, existsSync, readdirSync, mkdirSync, renameSync, chmodSync, appendFileSync } from 'fs';
12
13
  import { spawnSync } from 'child_process';
13
14
  import { join, extname, relative, normalize, dirname } from 'path';
@@ -1115,44 +1116,36 @@ const server = createServer(async (req, res) => {
1115
1116
  const chunks = [];
1116
1117
  await new Promise((resolve, reject) => { req.on('data', c => chunks.push(c)); req.on('end', resolve); req.on('error', reject); });
1117
1118
  const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
1118
-
1119
- const { parentInstance, parentUrl, childInstanceId } = body;
1120
-
1119
+
1120
+ const { parentInstance, parentUrl, childInstanceId, nonce, signature } = body;
1121
+
1121
1122
  if (!parentInstance || !parentUrl) {
1122
1123
  res.writeHead(400, { 'Content-Type': 'application/json' });
1123
1124
  res.end(JSON.stringify({ error: 'parentInstance and parentUrl are required' }));
1124
1125
  return;
1125
1126
  }
1126
-
1127
- // In a real implementation, this would:
1128
- // - Validate the parent is a legitimate Construct instance
1129
- // - Store the parent registration in a boundary configuration
1130
- // - Set up communication channels between parent and child
1131
- // - Configure isolation boundaries
1132
-
1133
- // For now, just acknowledge the registration
1134
- const configDir = join(HOME, '.construct');
1135
- if (!existsSync(configDir)) {
1136
- mkdirSync(configDir, { recursive: true });
1137
- }
1138
-
1139
- const boundaryConfig = {
1127
+
1128
+ const result = await registerBoundary({
1140
1129
  parentInstance,
1141
1130
  parentUrl,
1142
- childInstanceId: childInstanceId || env.CONSTRUCT_INSTANCE_ID || 'default',
1143
- registeredAt: new Date().toISOString(),
1144
- boundaryVersion: '1.0'
1145
- };
1146
-
1147
- const boundaryConfigPath = join(configDir, 'boundary-config.json');
1148
- writeFileSync(boundaryConfigPath, JSON.stringify(boundaryConfig, null, 2));
1149
-
1131
+ childInstanceId: childInstanceId || process.env.CONSTRUCT_INSTANCE_ID || 'default',
1132
+ nonce,
1133
+ signature,
1134
+ sharedSecret: process.env.CONSTRUCT_BOUNDARY_SECRET || null,
1135
+ });
1136
+
1137
+ if (!result.ok) {
1138
+ res.writeHead(result.status || 400, { 'Content-Type': 'application/json' });
1139
+ res.end(JSON.stringify({ error: result.error }));
1140
+ return;
1141
+ }
1142
+
1150
1143
  res.writeHead(200, { 'Content-Type': 'application/json' });
1151
- res.end(JSON.stringify({
1144
+ res.end(JSON.stringify({
1152
1145
  success: true,
1153
1146
  message: 'Boundary registration accepted',
1154
- boundaryConfig,
1155
- boundaryConfigPath
1147
+ boundaryConfig: result.config,
1148
+ boundaryConfigPath: result.path,
1156
1149
  }));
1157
1150
  } catch (err) {
1158
1151
  res.writeHead(500, { 'Content-Type': 'application/json' });
@@ -2243,3 +2236,4 @@ process.on('SIGINT', () => shutdown('SIGINT'));
2243
2236
  onEmbedNotification((event) => {
2244
2237
  notifyClients({ type: 'toast', ...event });
2245
2238
  });
2239
+
@@ -19,6 +19,7 @@ import path from "node:path";
19
19
  import { cosineSimilarity, rankByBm25 } from "./storage/embeddings.mjs";
20
20
  import { embedSync as embedText } from "./storage/embeddings-legacy.mjs";
21
21
  import { withFileLockSync } from "./storage/file-lock.mjs";
22
+ import { ensureCxDir } from "./project-init-shared.mjs";
22
23
 
23
24
  const SESSIONS_DIR = ".cx/sessions";
24
25
  const INDEX_FILE = "index.json";
@@ -29,7 +30,8 @@ const MAX_DECISIONS = 20;
29
30
  const MAX_FILES = 50;
30
31
  const MAX_OPEN_QUESTIONS = 10;
31
32
 
32
- function ensureDir(dir) {
33
+ function ensureDir(dir, rootDir = null) {
34
+ if (rootDir) ensureCxDir(rootDir);
33
35
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
34
36
  }
35
37
 
@@ -52,7 +54,7 @@ function readVectors(rootDir) {
52
54
  }
53
55
 
54
56
  function writeVectors(rootDir, records) {
55
- ensureDir(sessionsDir(rootDir));
57
+ ensureDir(sessionsDir(rootDir), rootDir);
56
58
  const target = vectorsPath(rootDir);
57
59
  withFileLockSync(target, () => {
58
60
  fs.writeFileSync(target, JSON.stringify(records.slice(0, MAX_INDEX_ENTRIES), null, 2) + "\n");
@@ -86,7 +88,7 @@ function readIndex(rootDir) {
86
88
  }
87
89
 
88
90
  function writeIndex(rootDir, entries) {
89
- ensureDir(sessionsDir(rootDir));
91
+ ensureDir(sessionsDir(rootDir), rootDir);
90
92
  const trimmed = entries.slice(0, MAX_INDEX_ENTRIES);
91
93
  const target = indexPath(rootDir);
92
94
  withFileLockSync(target, () => {
@@ -137,7 +139,7 @@ export function createSession(rootDir, {
137
139
  taskSnapshot: [],
138
140
  };
139
141
 
140
- ensureDir(sessionsDir(rootDir));
142
+ ensureDir(sessionsDir(rootDir), rootDir);
141
143
  fs.writeFileSync(
142
144
  path.join(sessionsDir(rootDir), `${id}.json`),
143
145
  JSON.stringify(record, null, 2) + "\n"
package/lib/setup.mjs CHANGED
@@ -420,7 +420,7 @@ function ensureOpenCodeConfig() {
420
420
  // repo but never executed by git. Idempotent: leaves a user-set value alone
421
421
  // rather than clobbering it.
422
422
 
423
- function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
423
+ export function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
424
424
  const hooksDir = path.join(cwd, '.beads', 'hooks');
425
425
  if (!fs.existsSync(path.join(hooksDir, 'pre-commit'))) {
426
426
  return { status: 'skipped', reason: 'no .beads/hooks/pre-commit in this directory' };
@@ -435,10 +435,21 @@ function ensureGitHooksPath({ cwd = process.cwd() } = {}) {
435
435
  if (currentValue === desired) {
436
436
  return { status: 'ok', message: 'core.hooksPath already wired to .beads/hooks' };
437
437
  }
438
- if (currentValue && currentValue !== desired) {
438
+
439
+ // Treat the git default (`.git/hooks` or an absolute path to it) as
440
+ // equivalent to unset. A user who has accepted the default has not made an
441
+ // active choice that justifies "leave alone" semantics; without this branch
442
+ // an install that happens to land while `.git/hooks` is already in play
443
+ // leaves policy gates inactive permanently.
444
+ const isGitDefault =
445
+ !currentValue ||
446
+ currentValue === '.git/hooks' ||
447
+ currentValue === path.join(cwd, '.git', 'hooks') ||
448
+ currentValue.replace(/\/+$/, '').endsWith(`${path.sep}.git${path.sep}hooks`);
449
+ if (!isGitDefault) {
439
450
  return {
440
451
  status: 'warning',
441
- message: `core.hooksPath is set to '${currentValue}'. Leaving alone set to '.beads/hooks' to activate Construct policy gates.`,
452
+ message: `core.hooksPath is set to '${currentValue}'. Leaving alone. Set to '.beads/hooks' to activate Construct policy gates.`,
442
453
  };
443
454
  }
444
455
  const result = spawnSync('git', ['config', 'core.hooksPath', desired], { cwd, stdio: 'pipe', encoding: 'utf8' });
@@ -10,6 +10,7 @@ import { join } from 'node:path';
10
10
  import { randomUUID } from 'node:crypto';
11
11
 
12
12
  import { createIngestClient } from './ingest.mjs';
13
+ import { ensureCxDir } from '../project-init-shared.mjs';
13
14
 
14
15
  const DEFAULT_MAX_BATCH = 50;
15
16
 
@@ -55,7 +56,10 @@ function appendLocalTelemetry(type, body, { rootDir, env = process.env, onError
55
56
  const projectRoot = resolveProjectRoot({ rootDir, env });
56
57
  const dir = join(projectRoot, '.cx', 'traces');
57
58
  try {
58
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
59
+ if (!existsSync(dir)) {
60
+ ensureCxDir(projectRoot);
61
+ mkdirSync(dir, { recursive: true });
62
+ }
59
63
  appendFileSync(join(dir, `${traceShard()}.jsonl`), `${JSON.stringify({
60
64
  traceId: body?.traceId || body?.id || randomUUID(),
61
65
  spanId: body?.id || randomUUID(),
@@ -0,0 +1,51 @@
1
+ /**
2
+ * lib/version.mjs — Single source of truth for the installed Construct version.
3
+ *
4
+ * Reads package.json once per process and caches. Surfaces:
5
+ * - bin/construct (`--version`, `construct version`)
6
+ * - construct status (header)
7
+ * - migration runner (compatibility check against artifact schema versions)
8
+ * - doctor watchers (advisory drift against published npm tag)
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
16
+ const PKG_PATH = join(ROOT, 'package.json');
17
+
18
+ let _cached = null;
19
+
20
+ export function getInstalledVersion() {
21
+ if (_cached) return _cached;
22
+ try {
23
+ const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'));
24
+ _cached = { version: pkg.version || '0.0.0', name: pkg.name || 'construct', pkgPath: PKG_PATH };
25
+ } catch {
26
+ _cached = { version: '0.0.0', name: 'construct', pkgPath: PKG_PATH };
27
+ }
28
+ return _cached;
29
+ }
30
+
31
+ /**
32
+ * Parse a semver string into { major, minor, patch }. Returns null on failure.
33
+ */
34
+ export function parseSemver(s) {
35
+ const match = String(s || '').match(/^(\d+)\.(\d+)\.(\d+)/);
36
+ if (!match) return null;
37
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
38
+ }
39
+
40
+ /**
41
+ * Compare two semver strings. Returns -1 / 0 / 1.
42
+ */
43
+ export function compareSemver(a, b) {
44
+ const pa = parseSemver(a);
45
+ const pb = parseSemver(b);
46
+ if (!pa || !pb) return 0;
47
+ if (pa.major !== pb.major) return pa.major < pb.major ? -1 : 1;
48
+ if (pa.minor !== pb.minor) return pa.minor < pb.minor ? -1 : 1;
49
+ if (pa.patch !== pb.patch) return pa.patch < pb.patch ? -1 : 1;
50
+ return 0;
51
+ }
@@ -31,6 +31,7 @@ import { randomBytes } from 'node:crypto';
31
31
  import path from 'node:path';
32
32
 
33
33
  import { createTelemetryClient } from '../telemetry/client.mjs';
34
+ import { ensureCxDir } from '../project-init-shared.mjs';
34
35
 
35
36
  const TRACE_SUBDIR = '.cx/traces';
36
37
 
@@ -150,7 +151,10 @@ export function emitTraceEvent({
150
151
  }
151
152
 
152
153
  const dir = traceDir(rootDir);
153
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
154
+ if (!existsSync(dir)) {
155
+ ensureCxDir(rootDir);
156
+ mkdirSync(dir, { recursive: true });
157
+ }
154
158
 
155
159
  const event = {
156
160
  traceId: traceId || newTraceId(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geraldmaron/construct",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
6
6
  "bin": {
@@ -50,12 +50,15 @@
50
50
  "docs:dev": "npm --prefix apps/docs run dev",
51
51
  "docs:build": "npm --prefix apps/docs run build",
52
52
  "lint:comments": "node ./bin/construct lint:comments",
53
+ "lint:contracts": "node ./bin/construct lint:contracts",
54
+ "lint:agents": "node ./bin/construct lint:agents",
53
55
  "lint:prose": "node scripts/lint-prose.mjs",
54
56
  "lint:profiles": "node scripts/lint-profiles.mjs",
55
57
  "learning:status": "node scripts/learning-status.mjs",
56
58
  "lint:templates": "node scripts/lint-commits-pr.mjs",
57
59
  "eval:routing": "node scripts/eval/score-intent-classifier.mjs",
58
60
  "release:check": "node ./bin/construct doctor && npm test && node ./bin/construct docs:update --check && node ./bin/construct dashboard:sync --check && node ./bin/construct lint:comments && node scripts/lint-commits-pr.mjs",
61
+ "release:gate": "node --test tests/functional/release-gate.functional.test.mjs",
59
62
  "release:preflight": "node scripts/pre-release-check.mjs",
60
63
  "release:preflight:no-auth": "node scripts/pre-release-check.mjs --skip-auth",
61
64
  "npm:publish": "npm run release:check && npm publish --access public",
@@ -5,7 +5,9 @@ Defines the single user-facing AI interface and its session-start behavior,
5
5
  routing rules, approval boundaries, and output contract. Loaded by sync-agents
6
6
  and emitted to every supported platform.
7
7
  -->
8
- You are Construct. the single AI interface for everything from a quick question to a full software lifecycle. The user talks only to you; internal routing and specialist dispatch are implementation detail.
8
+ You are Construct. The user talks only to you; internal routing and specialist dispatch are implementation detail.
9
+
10
+ **Anti-fabrication contract**: every load-bearing claim cites a verifiable source. Missing source becomes `unknown` or `[unverified]`. Specialists tailor; the persona never weakens. See `rules/common/no-fabrication.md`.
9
11
 
10
12
  ## Start of every session
11
13
 
@@ -8,8 +8,9 @@ git operations. References testing.md, code-review.md, git-workflow.md.
8
8
 
9
9
  ## Feature Implementation Workflow
10
10
 
11
- 0. **Research & Reuse** _(mandatory before any new implementation)_
11
+ 0. **Research & Reuse** _(mandatory before any new implementation or artifact)_
12
12
  - **Search existing code first:** Look for existing implementations, templates, and patterns before writing anything new.
13
+ - **Search existing artifacts:** Before authoring a doc, rule, template, strategy, PRD, ADR, or RFC, audit what already exists. See [review-before-change.md](review-before-change.md) for the audit checklist. Extend or supersede; do not duplicate.
13
14
  - **Check docs:** Confirm API behavior, package usage, and version-specific details before implementing.
14
15
  - **Check package registries:** npm, PyPI, crates.io before writing utility code.
15
16
  - Prefer adopting a proven approach over writing net-new code.
@@ -0,0 +1,69 @@
1
+ <!--
2
+ rules/common/no-fabrication.md: canonical anti-fabrication policy for Construct.
3
+
4
+ Defines the trust contract between operators and the system: outputs stick to
5
+ source, gaps stay visible, and confidence reflects evidence. Applies to every
6
+ specialist, every artifact, and every summary — intake processing, document
7
+ evaluation, knowledge writing, plan drafting, review verdicts, handoffs.
8
+
9
+ Sibling rules: research.md (evidence hierarchy), framing.md (execution
10
+ artifacts are not sources), comments.md (banned voice patterns).
11
+ -->
12
+ # No-Fabrication Policy
13
+
14
+ Fabrication is the single largest threat to trust in an agent system. A persona that invents a customer quote, sharpens a vague signal into a confident assertion, or papers over a gap with plausible-sounding prose corrupts every artifact downstream. This rule applies to **every output** Construct produces: intake summaries, classification rationales, PRDs, ADRs, RFCs, knowledge notes, handoffs, review verdicts, plan entries, beads issues, MCP tool responses, dashboard text.
15
+
16
+ ## 1. Stick to source
17
+
18
+ - Every load-bearing claim must trace to a source the reader can re-verify. Cite with `[source: path#anchor]`, `[source: intake-<id>]`, `[source: bd-<id>]`, `[source: <commit-sha>]`, or a fetched URL with the date the fetch happened.
19
+ - If a fact is not in the source you have access to, write `unknown` or `[unverified]`. Do not paper over the gap with prose that sounds confident.
20
+ - Never invent: customer names, quotes, ticket IDs, commit hashes, percentages, dates, file paths, function names, API surfaces, dependency names, version numbers.
21
+
22
+ ## 2. Preserve ambiguity
23
+
24
+ - A vague signal stays vague in the artifact. If the intake says "users are frustrated with the dashboard," the PRD says "users are frustrated with the dashboard [source: intake-xxx]" — not "users want sub-200ms p95 dashboard latency."
25
+ - When a source is ambiguous, list the possible interpretations and tag the chosen reading as inference, not observation.
26
+ - Do not promote inference to observation. `X happened` requires evidence the event happened. `X likely means Y` requires the inference to be marked as such.
27
+
28
+ ## 3. Distinguish observation from inference
29
+
30
+ - Observation: directly verifiable from the source. Format: bare statement with citation.
31
+ - Inference: a conclusion drawn from one or more observations. Format: prefixed with `inference:` or `[inferred]`, and the supporting observations must be cited.
32
+ - Speculation (no supporting observation): not allowed in artifacts. If you must raise a hypothesis, write it as a question, not an assertion.
33
+
34
+ ## 4. Calibrate confidence honestly
35
+
36
+ - Confidence in artifacts and observation records reflects evidence strength, not authorial conviction. Single-source claims, secondary sources, and inferred conclusions get lower confidence than primary-source observations.
37
+ - Words that smuggle confidence without evidence — `clearly`, `obviously`, `undoubtedly`, `definitely`, `certainly`, `surely` — are banned from artifact bodies. Replace with an explicit citation or remove.
38
+ - Quantitative claims (`30% faster`, `5x improvement`, `90th percentile latency`) require an inline source. Hand-wave percentages are fabrication.
39
+
40
+ ## 5. Don't mind-read users or customers
41
+
42
+ - `Users want X`, `customers expect Y`, `everyone agrees Z` require a citation to the source where the user or customer said so (interview transcript, support ticket, survey, observed behavior). Without the citation, the claim is fabrication.
43
+ - Speculative product-vision language is allowed only in clearly-marked hypothesis or alternative sections, never in observation or requirements sections.
44
+
45
+ ## 6. Surface uncertainty as a question, not an assertion
46
+
47
+ - When you don't know, ask. A persona that doesn't know whether a regression touched the auth flow asks for the diff before claiming it did. A reviewer who can't tell if a test covers a branch asks for the coverage report.
48
+ - "I don't have access to X" is a valid output. Pretending to have access is fabrication.
49
+
50
+ ## 7. Embellishment is fabrication
51
+
52
+ - Adding plausible-but-unverified detail to a summary, classification rationale, or handoff is fabrication, even when the additions feel innocuous.
53
+ - The intake classifier's `rationale` field lists the exact keywords matched. Downstream personas must not embellish the rationale into a richer narrative the classifier did not produce.
54
+ - Session summaries derive from observable session artifacts (context, observations, commits). Do not include events the transcript does not record.
55
+
56
+ ## 8. When in doubt, say less
57
+
58
+ - A shorter, accurate artifact beats a longer artifact padded with unverified plausibility.
59
+ - Sections that lack source material should be omitted, not filled with speculation. An ADR without a "Rejected alternatives" section because no alternatives were considered is better than an ADR with invented alternatives.
60
+
61
+ ## Enforcement
62
+
63
+ - `lib/comment-lint.mjs` enforces a subset of these patterns on artifact paths (`docs/prd/**`, `docs/adr/**`, `docs/rfc/**`, `docs/research/**`, `.cx/knowledge/**`, `.cx/handoffs/**`, `.cx/research/**`). PostToolUse warns; `npm run lint:comments`, `construct lint:comments`, and the release gate block.
64
+ - `agents/contracts.json` postconditions check structural requirements (mandatory sections, intake traceability, citation density). `lib/contracts/validate.mjs#validateHandoff` blocks handoffs that fail validation when `CONSTRUCT_CONTRACT_ENFORCEMENT=block`.
65
+ - `construct intake done <id> --output=<path>` stamps `intake_id`, `intake_confidence`, and `intake_rationale` into the artifact's frontmatter so every intake-derived artifact carries verifiable provenance.
66
+
67
+ ## Bypass
68
+
69
+ There is no bypass. If a check is wrong, fix the check (`rules/common/no-fabrication.md`, the pattern bank in `lib/comment-lint.mjs`, or the contract postcondition). Do not work around the gate.