@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.
- package/README.md +19 -17
- package/agents/contracts.json +617 -2
- package/agents/contracts.schema.json +83 -0
- package/agents/prompts/cx-accessibility.md +2 -0
- package/agents/prompts/cx-ai-engineer.md +2 -0
- package/agents/prompts/cx-architect.md +2 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +2 -0
- package/agents/prompts/cx-data-engineer.md +2 -0
- package/agents/prompts/cx-debugger.md +2 -0
- package/agents/prompts/cx-designer.md +2 -0
- package/agents/prompts/cx-devil-advocate.md +2 -0
- package/agents/prompts/cx-docs-keeper.md +2 -0
- package/agents/prompts/cx-engineer.md +2 -0
- package/agents/prompts/cx-evaluator.md +2 -0
- package/agents/prompts/cx-explorer.md +2 -0
- package/agents/prompts/cx-legal-compliance.md +2 -0
- package/agents/prompts/cx-operations.md +2 -0
- package/agents/prompts/cx-orchestrator.md +2 -0
- package/agents/prompts/cx-platform-engineer.md +2 -0
- package/agents/prompts/cx-product-manager.md +2 -0
- package/agents/prompts/cx-qa.md +2 -0
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-release-manager.md +2 -0
- package/agents/prompts/cx-researcher.md +2 -0
- package/agents/prompts/cx-reviewer.md +2 -0
- package/agents/prompts/cx-security.md +2 -0
- package/agents/prompts/cx-sre.md +2 -0
- package/agents/prompts/cx-test-automation.md +2 -0
- package/agents/prompts/cx-trace-reviewer.md +2 -0
- package/agents/prompts/cx-ux-researcher.md +2 -0
- package/agents/registry.json +8 -3
- package/bin/construct +209 -32
- 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 +1 -1
- package/commands/work/parallel-review.md +1 -1
- package/lib/auto-docs.mjs +7 -7
- package/lib/boundary.mjs +126 -0
- package/lib/cache-strategy-google.js +26 -31
- 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/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/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 +2 -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/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 +45 -14
- package/templates/docs/construct_guide.md +6 -6
- 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
|
|
121
|
-
*
|
|
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
|
|
133
|
+
const adapterKey = resolveAdapterKey(modelId);
|
|
134
|
+
const loader = ADAPTERS[adapterKey] || ADAPTERS.generic;
|
|
129
135
|
|
|
136
|
+
let caps;
|
|
130
137
|
if (probe) {
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
fs.mkdirSync(
|
|
12
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
51
|
+
let summarySource = 'cli';
|
|
51
52
|
if (!summary) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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',
|
|
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
|
}
|
package/lib/server/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
+
|
package/lib/session-store.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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' });
|
package/lib/telemetry/client.mjs
CHANGED
|
@@ -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))
|
|
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(),
|
package/lib/version.mjs
ADDED
|
@@ -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
|
+
}
|
package/lib/worker/trace.mjs
CHANGED
|
@@ -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))
|
|
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.
|
|
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",
|
package/personas/construct.md
CHANGED
|
@@ -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.
|
|
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.
|