@geraldmaron/construct 1.0.24 → 1.1.1
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 +2 -0
- package/bin/construct +266 -13
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- package/lib/audit-trail.mjs +77 -10
- package/lib/beads-client.mjs +9 -0
- package/lib/beads-optimistic.mjs +23 -71
- package/lib/bridges/copilot-proxy.mjs +116 -0
- package/lib/cli-commands.mjs +112 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.mjs +1 -1
- package/lib/demo.mjs +245 -0
- package/lib/diagram.mjs +300 -0
- package/lib/doctor/index.mjs +1 -1
- package/lib/doctor/watchers/process-pressure.mjs +1 -1
- package/lib/doctor/watchers/service-health.mjs +1 -1
- package/lib/document-extract/docling-client.mjs +16 -6
- package/lib/document-extract/docling-sidecar.py +32 -2
- package/lib/document-extract.mjs +85 -10
- package/lib/document-ingest.mjs +97 -7
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/embed/semantic.mjs +5 -2
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- package/lib/handoffs/cleanup.mjs +1 -1
- package/lib/hooks/audit-trail.mjs +14 -28
- package/lib/host-capabilities.mjs +30 -0
- package/lib/ingest/docling-remote.mjs +90 -0
- package/lib/ingest/strategy.mjs +1 -1
- package/lib/init-unified.mjs +9 -13
- package/lib/logging/rotate.mjs +18 -0
- package/lib/mcp/server.mjs +124 -12
- package/lib/mcp/tool-budget.mjs +53 -0
- package/lib/mcp-catalog.json +1 -1
- package/lib/model-router.mjs +52 -1
- package/lib/ollama/capability-store.mjs +78 -0
- package/lib/ollama/provision-context.mjs +344 -0
- package/lib/opencode-config.mjs +148 -0
- package/lib/opencode-telemetry.mjs +7 -0
- package/lib/orchestration-policy.mjs +41 -6
- package/lib/persona-sections.mjs +66 -0
- package/lib/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +14 -9
- package/lib/reconcile/agent-instructions-rewrap.mjs +8 -4
- package/lib/reflect/extractor.mjs +14 -1
- package/lib/reflect/salience.mjs +65 -0
- package/lib/rules-delivery.mjs +122 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -17
- package/lib/server/index.mjs +1 -1
- package/lib/server/langfuse-login.mjs +3 -3
- package/lib/service-manager.mjs +42 -4
- package/lib/session-store.mjs +1 -1
- package/lib/setup.mjs +58 -2
- package/lib/specialists/prompt-schema.mjs +162 -0
- package/lib/specialists/scaffold.mjs +109 -0
- package/lib/storage/embeddings-engine.mjs +19 -5
- package/lib/storage/embeddings-local.mjs +6 -3
- package/lib/storage/file-lock.mjs +18 -11
- package/lib/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/package.json +4 -2
- package/personas/construct.md +12 -12
- package/platforms/capabilities.json +76 -0
- package/platforms/opencode/sync-config.mjs +121 -25
- package/rules/common/neurodivergent-output.md +1 -1
- package/rules/web/coding-style.md +8 -0
- package/rules/web/design-quality.md +8 -0
- package/rules/web/hooks.md +8 -0
- package/rules/web/patterns.md +8 -0
- package/rules/web/performance.md +8 -0
- package/rules/web/security.md +8 -0
- package/rules/web/testing.md +8 -0
- package/scripts/sync-specialists.mjs +277 -63
- package/skills/docs/init-project.md +1 -1
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-test-automation.md +12 -0
- package/templates/docs/construct_guide.md +2 -2
- package/lib/ingest/chunker.mjs +0 -94
- package/lib/ingest/pipeline.mjs +0 -53
- package/lib/ingest/store.mjs +0 -82
- package/lib/mode-commands.mjs +0 -122
- package/lib/policy/unified-gates.mjs +0 -96
- package/lib/profiles/validate-custom.mjs +0 -114
- package/lib/services/telemetry-backend.mjs +0 -177
- package/lib/storage/fusion.mjs +0 -95
package/lib/audit-trail.mjs
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/audit-trail.mjs —
|
|
3
|
-
* lib/hooks/audit-trail.mjs and
|
|
2
|
+
* lib/audit-trail.mjs — owns the append-only audit log written by
|
|
3
|
+
* lib/hooks/audit-trail.mjs and rendered for `construct audit trail`.
|
|
4
4
|
*
|
|
5
5
|
* The log is JSONL at ~/.cx/audit-trail.jsonl. Each line carries a
|
|
6
6
|
* prev_line_hash pointing at the SHA-256 of the previous line — tampering
|
|
7
7
|
* (reorder, delete, edit) breaks the chain and is surfaced by --verify.
|
|
8
|
+
*
|
|
9
|
+
* A serial hash chain cannot survive concurrent appends: a read-prev-hash +
|
|
10
|
+
* append done outside a critical section lets two processes (hook, daemon,
|
|
11
|
+
* parallel tool calls in one session) both chain off the same predecessor, so
|
|
12
|
+
* the second link points at a stale tail. appendAuditRecord
|
|
13
|
+
* closes that window by computing prev_line_hash and appending inside a single
|
|
14
|
+
* cross-process file lock, making every writer effectively serialized.
|
|
15
|
+
*
|
|
16
|
+
* Corrupted links cannot be re-derived, so a `chain_reset` boundary line seals
|
|
17
|
+
* the damaged prefix as immutable legacy and re-bases the live chain;
|
|
18
|
+
* verifyChain validates only from the last boundary forward. See
|
|
19
|
+
* docs/concepts/observability.mdx.
|
|
8
20
|
*/
|
|
9
|
-
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
21
|
+
import { readFileSync, existsSync, statSync, mkdirSync } from 'node:fs';
|
|
10
22
|
import { createHash } from 'node:crypto';
|
|
11
|
-
import { join } from 'node:path';
|
|
23
|
+
import { join, dirname } from 'node:path';
|
|
12
24
|
import { homedir } from 'node:os';
|
|
13
25
|
|
|
26
|
+
import { appendBounded, readLastLineAcrossSegments } from './logging/rotate.mjs';
|
|
27
|
+
import { withFileLockSync } from './storage/file-lock.mjs';
|
|
28
|
+
|
|
14
29
|
const AUDIT_FILE = join(homedir(), '.cx', 'audit-trail.jsonl');
|
|
15
30
|
|
|
16
31
|
function sha256(input) {
|
|
17
32
|
return createHash('sha256').update(input).digest('hex');
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
// prev_line_hash and the append must be one atomic step or concurrent writers
|
|
36
|
+
// race; the file lock serializes them across processes.
|
|
37
|
+
|
|
38
|
+
export function appendAuditRecord(partial, { file = AUDIT_FILE } = {}) {
|
|
39
|
+
return withFileLockSync(file, () => {
|
|
40
|
+
let prev = null;
|
|
41
|
+
try {
|
|
42
|
+
const last = readLastLineAcrossSegments(file);
|
|
43
|
+
if (last !== null) prev = sha256(last);
|
|
44
|
+
} catch { /* first record, or unreadable tail */ }
|
|
45
|
+
const record = { ...partial, prev_line_hash: prev };
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
appendBounded('audit-trail', file, JSON.stringify(record) + '\n');
|
|
48
|
+
return record;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A reset boundary seals the records it follows as legacy excluded from
|
|
53
|
+
// verification and re-bases the live chain from this line.
|
|
54
|
+
|
|
55
|
+
export function writeChainReset({ file = AUDIT_FILE, reason = 'manual reset' } = {}) {
|
|
56
|
+
return appendAuditRecord({
|
|
57
|
+
ts: new Date().toISOString(),
|
|
58
|
+
chain_reset: true,
|
|
59
|
+
reason,
|
|
60
|
+
agent: 'construct',
|
|
61
|
+
tool: null,
|
|
62
|
+
target: null,
|
|
63
|
+
}, { file });
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
export function readAuditTrail({ limit = 50, since = null, agent = null, tool = null } = {}) {
|
|
21
67
|
if (!existsSync(AUDIT_FILE)) return [];
|
|
22
68
|
const lines = readFileSync(AUDIT_FILE, 'utf8').split('\n').filter(Boolean);
|
|
@@ -34,19 +80,29 @@ export function readAuditTrail({ limit = 50, since = null, agent = null, tool =
|
|
|
34
80
|
return filtered.slice(-limit);
|
|
35
81
|
}
|
|
36
82
|
|
|
37
|
-
export function verifyChain() {
|
|
38
|
-
if (!existsSync(
|
|
39
|
-
const lines = readFileSync(
|
|
83
|
+
export function verifyChain(file = AUDIT_FILE) {
|
|
84
|
+
if (!existsSync(file)) return { ok: true, verified: 0, broken: [], legacy: 0 };
|
|
85
|
+
const lines = readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
86
|
+
|
|
87
|
+
// Validation starts at the last reset boundary; the prefix it seals is legacy
|
|
88
|
+
// and exempt, so concurrency damage predating the migration can't fail the chain.
|
|
89
|
+
|
|
90
|
+
let start = 0;
|
|
91
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92
|
+
try { if (JSON.parse(lines[i]).chain_reset) start = i; } catch { /* legacy malformed */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
40
95
|
const broken = [];
|
|
41
96
|
let prevHash = null;
|
|
42
|
-
for (let i =
|
|
97
|
+
for (let i = start; i < lines.length; i += 1) {
|
|
43
98
|
const line = lines[i];
|
|
44
99
|
let row;
|
|
45
100
|
try { row = JSON.parse(line); } catch {
|
|
46
101
|
broken.push({ line: i + 1, reason: 'malformed JSON' });
|
|
102
|
+
prevHash = sha256(line);
|
|
47
103
|
continue;
|
|
48
104
|
}
|
|
49
|
-
if (i >
|
|
105
|
+
if (i > start && !row.chain_reset && row.prev_line_hash !== prevHash) {
|
|
50
106
|
broken.push({
|
|
51
107
|
line: i + 1,
|
|
52
108
|
expected: prevHash,
|
|
@@ -56,7 +112,7 @@ export function verifyChain() {
|
|
|
56
112
|
}
|
|
57
113
|
prevHash = sha256(line);
|
|
58
114
|
}
|
|
59
|
-
return { ok: broken.length === 0, verified: lines.length, broken };
|
|
115
|
+
return { ok: broken.length === 0, verified: lines.length - start, broken, legacy: start };
|
|
60
116
|
}
|
|
61
117
|
|
|
62
118
|
function formatRow(row) {
|
|
@@ -74,6 +130,9 @@ export async function runAuditTrailCli(args = []) {
|
|
|
74
130
|
for (let i = 0; i < args.length; i += 1) {
|
|
75
131
|
const arg = args[i];
|
|
76
132
|
if (arg === '--verify') flags.add('verify');
|
|
133
|
+
else if (arg === '--reset') flags.add('reset');
|
|
134
|
+
else if (arg.startsWith('--reason=')) options.reason = arg.slice(9);
|
|
135
|
+
else if (arg === '--reason') options.reason = args[++i];
|
|
77
136
|
else if (arg === '--json') flags.add('json');
|
|
78
137
|
else if (arg === '--all') flags.add('all');
|
|
79
138
|
else if (arg.startsWith('--limit=')) options.limit = Number(arg.slice(8));
|
|
@@ -97,6 +156,8 @@ Options:
|
|
|
97
156
|
--agent <name> Filter to a specific agent (e.g. cx-engineer)
|
|
98
157
|
--tool <name> Filter to a specific tool (Edit, Write, MultiEdit, NotebookEdit, Bash)
|
|
99
158
|
--verify Verify the tamper-evidence chain and exit
|
|
159
|
+
--reset Seal the current chain as legacy and re-base a fresh chain
|
|
160
|
+
--reason <text> Reason recorded on the --reset boundary
|
|
100
161
|
--json Emit raw JSONL lines instead of formatted output
|
|
101
162
|
-h, --help Show this message
|
|
102
163
|
|
|
@@ -105,6 +166,12 @@ Log location: ${AUDIT_FILE}
|
|
|
105
166
|
return;
|
|
106
167
|
}
|
|
107
168
|
|
|
169
|
+
if (flags.has('reset')) {
|
|
170
|
+
writeChainReset({ reason: options.reason || 'manual reset' });
|
|
171
|
+
console.log(`Audit chain re-based at ${AUDIT_FILE}. Prior records sealed as legacy; verification resumes from the new boundary.`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
108
175
|
if (flags.has('verify')) {
|
|
109
176
|
const result = verifyChain();
|
|
110
177
|
if (result.ok) {
|
package/lib/beads-client.mjs
CHANGED
|
@@ -252,6 +252,15 @@ export async function runBd(args, options = {}) {
|
|
|
252
252
|
* Used as fallback when optimistic locking fails.
|
|
253
253
|
*/
|
|
254
254
|
async function runWithLegacyLock(args, opts, cwd, commandDesc) {
|
|
255
|
+
// Telemetry: every legacy-fallback firing is recorded so the lock+queue removal
|
|
256
|
+
// decision rests on observed data — if this never fires over a representative
|
|
257
|
+
// window, the whole exclusive-lock path is removable; if it fires, the entries
|
|
258
|
+
// name the bd errors that actually need handling (bead construct-nhn5).
|
|
259
|
+
try {
|
|
260
|
+
const { logBeadsFallback } = await import('./telemetry/beads-fallback.mjs');
|
|
261
|
+
logBeadsFallback({ command: commandDesc || args[0] || 'unknown' });
|
|
262
|
+
} catch { /* telemetry is best-effort */ }
|
|
263
|
+
|
|
255
264
|
// Clean up any stale locks before trying
|
|
256
265
|
cleanupStaleLock({ cwd });
|
|
257
266
|
cleanupStaleQueue({ cwd });
|
package/lib/beads-optimistic.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/beads-optimistic.mjs —
|
|
2
|
+
* lib/beads-optimistic.mjs — concurrent reads + retry-on-conflict writes for beads.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Dolt commits are atomic, so it is the serializer: a write is executed and, on a
|
|
5
|
+
* transient conflict, retried with exponential backoff. There is no separate
|
|
6
|
+
* read-then-compare version check — that spanned two `bd` processes (a `bd show`
|
|
7
|
+
* to read the commit hash, then a `bd update`) and guarded nothing, since `bd
|
|
8
|
+
* update` accepts no expected version, so the window between the read and the
|
|
9
|
+
* write left the "optimistic lock" unable to actually detect a conflicting commit
|
|
10
|
+
* (bead construct-iufy). Reads run lock-free; `getBeadVersion` remains as an
|
|
11
|
+
* advisory reader, not a write-path primitive.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
@@ -34,8 +34,9 @@ const DEFAULT_RETRY_OPTIONS = {
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* Read the current version of a bead from the database.
|
|
38
|
-
*
|
|
37
|
+
* Read the current version of a bead from the database (the Dolt commit hash).
|
|
38
|
+
* Advisory only — exposed for callers that want to observe a bead's version; the
|
|
39
|
+
* write path does not gate on it (see file header).
|
|
39
40
|
*/
|
|
40
41
|
export async function getBeadVersion(beadId, cwd = process.cwd()) {
|
|
41
42
|
try {
|
|
@@ -56,75 +57,37 @@ export async function getBeadVersion(beadId, cwd = process.cwd()) {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
* Execute a write
|
|
60
|
-
*
|
|
60
|
+
* Execute a bead write, retrying on a transient conflict. Dolt's atomic commit is
|
|
61
|
+
* the serializer; this does not pre-read or compare a version (see file header).
|
|
62
|
+
*
|
|
61
63
|
* @param {Object} options
|
|
62
|
-
* @param {string} options.beadId - The bead being modified
|
|
63
64
|
* @param {Function} options.execute - Async function that performs the write
|
|
64
|
-
* @param {string} options.expectedVersion - Version we expect the bead to have
|
|
65
65
|
* @param {Object} options.retry - Retry configuration
|
|
66
66
|
* @returns {Promise<{success: boolean, result: any, attempts: number}>}
|
|
67
67
|
*/
|
|
68
68
|
export async function optimisticWrite({
|
|
69
|
-
beadId,
|
|
70
69
|
execute,
|
|
71
|
-
expectedVersion,
|
|
72
70
|
retry = DEFAULT_RETRY_OPTIONS,
|
|
73
|
-
cwd = process.cwd(),
|
|
74
71
|
} = {}) {
|
|
75
72
|
let attempts = 0;
|
|
76
73
|
let delay = retry.baseDelayMs;
|
|
77
|
-
|
|
74
|
+
|
|
78
75
|
while (attempts < retry.maxRetries) {
|
|
79
76
|
attempts++;
|
|
80
|
-
|
|
81
|
-
// Verify version hasn't changed (optimistic check)
|
|
82
|
-
const currentVersion = await getBeadVersion(beadId, cwd);
|
|
83
|
-
|
|
84
|
-
if (expectedVersion && currentVersion !== expectedVersion) {
|
|
85
|
-
// Conflict detected - another writer modified the bead
|
|
86
|
-
if (attempts >= retry.maxRetries) {
|
|
87
|
-
return {
|
|
88
|
-
success: false,
|
|
89
|
-
error: `Optimistic lock conflict after ${attempts} attempts. Bead ${beadId} was modified by another process.`,
|
|
90
|
-
attempts,
|
|
91
|
-
currentVersion,
|
|
92
|
-
expectedVersion,
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Wait with exponential backoff and jitter
|
|
97
|
-
const jitter = Math.random() * 50;
|
|
98
|
-
await new Promise(r => setTimeout(r, delay + jitter));
|
|
99
|
-
delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
77
|
try {
|
|
104
|
-
// Execute the write operation
|
|
105
78
|
const result = await execute();
|
|
106
79
|
return { success: true, result, attempts };
|
|
107
80
|
} catch (error) {
|
|
108
81
|
if (attempts >= retry.maxRetries) {
|
|
109
|
-
return {
|
|
110
|
-
success: false,
|
|
111
|
-
error: error.message,
|
|
112
|
-
attempts,
|
|
113
|
-
};
|
|
82
|
+
return { success: false, error: error.message, attempts };
|
|
114
83
|
}
|
|
115
|
-
|
|
116
|
-
// Retry on transient errors
|
|
117
84
|
const jitter = Math.random() * 50;
|
|
118
|
-
await new Promise(r => setTimeout(r, delay + jitter));
|
|
85
|
+
await new Promise((r) => setTimeout(r, delay + jitter));
|
|
119
86
|
delay = Math.min(delay * retry.backoffMultiplier, retry.maxDelayMs);
|
|
120
87
|
}
|
|
121
88
|
}
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
success: false,
|
|
125
|
-
error: `Max retries (${retry.maxRetries}) exceeded`,
|
|
126
|
-
attempts,
|
|
127
|
-
};
|
|
89
|
+
|
|
90
|
+
return { success: false, error: `Max retries (${retry.maxRetries}) exceeded`, attempts };
|
|
128
91
|
}
|
|
129
92
|
|
|
130
93
|
// ---------------------------------------------------------------------------
|
|
@@ -187,21 +150,10 @@ export async function updateBeadOptimistic(
|
|
|
187
150
|
options = {}
|
|
188
151
|
) {
|
|
189
152
|
const { actor = 'construct', cwd = process.cwd(), notes } = options;
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!currentVersion) {
|
|
195
|
-
return {
|
|
196
|
-
success: false,
|
|
197
|
-
error: `Bead ${beadId} not found`,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
153
|
+
|
|
154
|
+
// A `bd update` on a missing bead exits non-zero and surfaces as a failed
|
|
155
|
+
// result, so no separate existence pre-check is needed.
|
|
201
156
|
return optimisticWrite({
|
|
202
|
-
beadId,
|
|
203
|
-
expectedVersion: currentVersion,
|
|
204
|
-
cwd,
|
|
205
157
|
execute: async () => {
|
|
206
158
|
const args = ['update', beadId];
|
|
207
159
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lib/bridges/copilot-proxy.mjs — Minimal GitHub Copilot to OpenAI-compatible bridge.
|
|
4
|
+
*
|
|
5
|
+
* Translates standard OpenAI /v1/chat/completions requests to the Copilot API
|
|
6
|
+
* using the user's active `gh` CLI session.
|
|
7
|
+
*/
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
const PORT = parseInt(process.argv.find(a => a.startsWith('--port='))?.split('=')[1] || '5174', 10);
|
|
12
|
+
|
|
13
|
+
let cachedSession = null;
|
|
14
|
+
|
|
15
|
+
async function getCopilotToken() {
|
|
16
|
+
if (cachedSession && cachedSession.expires_at > (Date.now() / 1000) + 60) {
|
|
17
|
+
return cachedSession.token;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const ghToken = execSync('gh auth token', { encoding: 'utf8' }).trim();
|
|
22
|
+
const res = await fetch('https://api.github.com/copilot_internal/v2/token', {
|
|
23
|
+
headers: {
|
|
24
|
+
'Authorization': `Bearer ${ghToken}`,
|
|
25
|
+
'Accept': 'application/json',
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!res.ok) throw new Error(`Failed to get session token: ${res.statusText}`);
|
|
30
|
+
|
|
31
|
+
cachedSession = await res.json();
|
|
32
|
+
return cachedSession.token;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error('Error fetching Copilot token:', err.message);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const server = http.createServer(async (req, res) => {
|
|
40
|
+
// CORS
|
|
41
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
42
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
44
|
+
|
|
45
|
+
if (req.method === 'OPTIONS') {
|
|
46
|
+
res.writeHead(204);
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (req.url === '/v1/chat/completions' && req.method === 'POST') {
|
|
52
|
+
const token = await getCopilotToken();
|
|
53
|
+
if (!token) {
|
|
54
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify({ error: 'Failed to authenticate with GitHub Copilot via gh CLI.' }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let body = '';
|
|
60
|
+
req.on('data', chunk => { body += chunk; });
|
|
61
|
+
req.on('end', async () => {
|
|
62
|
+
try {
|
|
63
|
+
const payload = JSON.parse(body);
|
|
64
|
+
|
|
65
|
+
// Map common model names to Copilot equivalents if needed
|
|
66
|
+
// For now, we pass them through or default to gpt-4o
|
|
67
|
+
const model = payload.model?.includes('gpt-4o') ? 'gpt-4o' :
|
|
68
|
+
payload.model?.includes('claude-3.5-sonnet') ? 'claude-3.5-sonnet' :
|
|
69
|
+
'gpt-4o';
|
|
70
|
+
|
|
71
|
+
const copilotRes = await fetch('https://api.githubcopilot.com/chat/completions', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Authorization': `Bearer ${token}`,
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
'Accept': 'application/json',
|
|
77
|
+
'X-Github-Api-Version': '2023-07-07',
|
|
78
|
+
'Editor-Version': 'vscode/1.90.0', // Spoof VS Code for compatibility
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
...payload,
|
|
82
|
+
model
|
|
83
|
+
})
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
res.writeHead(copilotRes.status, {
|
|
87
|
+
'Content-Type': copilotRes.headers.get('Content-Type'),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (payload.stream) {
|
|
91
|
+
const reader = copilotRes.body.getReader();
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) break;
|
|
95
|
+
res.write(value);
|
|
96
|
+
}
|
|
97
|
+
res.end();
|
|
98
|
+
} else {
|
|
99
|
+
const data = await copilotRes.text();
|
|
100
|
+
res.end(data);
|
|
101
|
+
}
|
|
102
|
+
} catch (err) {
|
|
103
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
res.writeHead(404);
|
|
111
|
+
res.end('Not Found');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
115
|
+
console.log(`Copilot Bridge listening on http://127.0.0.1:${PORT}`);
|
|
116
|
+
});
|
package/lib/cli-commands.mjs
CHANGED
|
@@ -23,6 +23,10 @@ export const CLI_COMMANDS = [
|
|
|
23
23
|
core: true,
|
|
24
24
|
description: 'Start services for development',
|
|
25
25
|
usage: 'construct dev [--select] [--only=postgres,dashboard,...]',
|
|
26
|
+
examples: [
|
|
27
|
+
{ cmd: 'construct dev', desc: 'Start the default service set' },
|
|
28
|
+
{ cmd: 'construct dev --only=dashboard', desc: 'Start just the dashboard' },
|
|
29
|
+
],
|
|
26
30
|
options: [
|
|
27
31
|
{ flag: '--select', desc: 'Pick which services to start from an interactive checklist' },
|
|
28
32
|
{ flag: '--only=<a,b,c>', desc: 'Start only the named services (postgres, dashboard, telemetry, memory, opencode)' },
|
|
@@ -35,6 +39,10 @@ export const CLI_COMMANDS = [
|
|
|
35
39
|
core: true,
|
|
36
40
|
description: 'Start the local dashboard/orchestration daemon (or --token to mint a dashboard token)',
|
|
37
41
|
usage: 'construct dashboard [--token]',
|
|
42
|
+
examples: [
|
|
43
|
+
{ cmd: 'construct dashboard', desc: 'Launch the local dashboard daemon' },
|
|
44
|
+
{ cmd: 'construct dashboard --token', desc: 'Mint a dashboard access token' },
|
|
45
|
+
],
|
|
38
46
|
},
|
|
39
47
|
{
|
|
40
48
|
name: 'stop',
|
|
@@ -43,6 +51,9 @@ export const CLI_COMMANDS = [
|
|
|
43
51
|
core: true,
|
|
44
52
|
description: 'Stop all running services',
|
|
45
53
|
usage: 'construct stop',
|
|
54
|
+
examples: [
|
|
55
|
+
{ cmd: 'construct stop', desc: 'Stop every running service' },
|
|
56
|
+
],
|
|
46
57
|
},
|
|
47
58
|
{
|
|
48
59
|
name: 'status',
|
|
@@ -51,6 +62,11 @@ export const CLI_COMMANDS = [
|
|
|
51
62
|
core: true,
|
|
52
63
|
description: 'Show system health and credentials',
|
|
53
64
|
usage: 'construct status',
|
|
65
|
+
strictFlags: true,
|
|
66
|
+
examples: [
|
|
67
|
+
{ cmd: 'construct status', desc: 'Human-readable health summary' },
|
|
68
|
+
{ cmd: 'construct status --json', desc: 'Full payload for scripting' },
|
|
69
|
+
],
|
|
54
70
|
options: [
|
|
55
71
|
{ flag: '--json', desc: 'Output full status payload as JSON' },
|
|
56
72
|
],
|
|
@@ -61,13 +77,19 @@ export const CLI_COMMANDS = [
|
|
|
61
77
|
category: 'Core',
|
|
62
78
|
core: true,
|
|
63
79
|
description: 'Machine setup (scoped per ADR-0029): --scope=project|user|both, default project',
|
|
64
|
-
usage: 'construct install [--scope=project|user|both] [--yes] [--no-docker] [--no-launch-agent] [--reconfigure]',
|
|
80
|
+
usage: 'construct install [--scope=project|user|both] [--yes] [--dry-run] [--no-docker] [--no-launch-agent] [--reconfigure] [--with-docling]',
|
|
81
|
+
examples: [
|
|
82
|
+
{ cmd: 'construct install --dry-run', desc: 'Preview the plan before writing anything' },
|
|
83
|
+
{ cmd: 'construct install --scope=user --yes', desc: 'Non-interactive user-scope install' },
|
|
84
|
+
],
|
|
65
85
|
options: [
|
|
66
86
|
{ flag: '--scope=<s>', desc: 'project (default, no-op + guidance) | user (writes ~/.construct/, MCP, ~/.claude/* via consent) | both' },
|
|
67
87
|
{ flag: '--yes', desc: 'Apply defaults without prompts (only meaningful with --scope=user|both)' },
|
|
88
|
+
{ flag: '--dry-run', desc: 'Preview the install plan (scopes, files, services) without writing anything' },
|
|
68
89
|
{ flag: '--no-docker', desc: 'Skip Docker-based service setup (local Postgres)' },
|
|
69
90
|
{ flag: '--no-launch-agent', desc: 'Skip background macOS LaunchAgent registration' },
|
|
70
91
|
{ flag: '--reconfigure', desc: 'Re-prompt for service consent, ignoring cached answers' },
|
|
92
|
+
{ flag: '--with-docling', desc: 'Eagerly provision the docling document-extraction venv now (heavy, ~10 min; else lazy on first ingest)' },
|
|
71
93
|
],
|
|
72
94
|
},
|
|
73
95
|
{
|
|
@@ -77,6 +99,10 @@ export const CLI_COMMANDS = [
|
|
|
77
99
|
core: true,
|
|
78
100
|
description: 'Project setup (once per repo): scaffold .cx/, AGENTS.md, plan.md, adapters',
|
|
79
101
|
usage: 'construct init [path] [options]',
|
|
102
|
+
examples: [
|
|
103
|
+
{ cmd: 'construct init', desc: 'Scaffold the current repo interactively' },
|
|
104
|
+
{ cmd: 'construct init --yes --with-adrs', desc: 'Non-interactive init with the ADR doc lane' },
|
|
105
|
+
],
|
|
80
106
|
options: [
|
|
81
107
|
{ flag: '--yes', desc: 'Accept all defaults (non-interactive)' },
|
|
82
108
|
{ flag: '--no-start', desc: 'Do not start services after init' },
|
|
@@ -102,6 +128,10 @@ export const CLI_COMMANDS = [
|
|
|
102
128
|
core: true,
|
|
103
129
|
description: 'Sync agent adapters to AI tools',
|
|
104
130
|
usage: 'construct sync [--project] [--dry-run] [--no-docs] [--compress-personas]',
|
|
131
|
+
examples: [
|
|
132
|
+
{ cmd: 'construct sync', desc: 'Sync adapters to detected AI tools' },
|
|
133
|
+
{ cmd: 'construct sync --dry-run', desc: 'Preview adapter changes without writing' },
|
|
134
|
+
],
|
|
105
135
|
options: [
|
|
106
136
|
{ flag: '--project', desc: 'Write project-local Claude adapters into the current repo only' },
|
|
107
137
|
{ flag: '--dry-run', desc: 'Preview adapter changes without writing files' },
|
|
@@ -116,6 +146,10 @@ export const CLI_COMMANDS = [
|
|
|
116
146
|
core: true,
|
|
117
147
|
description: 'View and process the active profile\'s intake queue (queue label varies by profile)',
|
|
118
148
|
usage: 'construct intake list|show|done|skip|reopen|integrate|classify',
|
|
149
|
+
examples: [
|
|
150
|
+
{ cmd: 'construct intake list', desc: 'See pending packets' },
|
|
151
|
+
{ cmd: 'construct intake done <id> --output=path', desc: 'Mark processed and stamp the artifact' },
|
|
152
|
+
],
|
|
119
153
|
subcommands: [
|
|
120
154
|
{ name: 'list', desc: 'List pending packets' },
|
|
121
155
|
{ name: 'show <id>', desc: 'Show one packet (triage, related docs, excerpt, tag suggestions)' },
|
|
@@ -133,6 +167,10 @@ export const CLI_COMMANDS = [
|
|
|
133
167
|
core: true,
|
|
134
168
|
description: 'View and manage artifact recommendations',
|
|
135
169
|
usage: 'construct recommendations list|show|dismiss|stats',
|
|
170
|
+
examples: [
|
|
171
|
+
{ cmd: 'construct recommendations list', desc: 'See open recommendations' },
|
|
172
|
+
{ cmd: 'construct recommendations dismiss <id>', desc: 'Dismiss one by id' },
|
|
173
|
+
],
|
|
136
174
|
},
|
|
137
175
|
{
|
|
138
176
|
name: 'integrations',
|
|
@@ -179,6 +217,10 @@ export const CLI_COMMANDS = [
|
|
|
179
217
|
core: true,
|
|
180
218
|
description: 'Isolated tmpdir-based environment for QA / specialist dry-runs',
|
|
181
219
|
usage: 'construct sandbox create|list|delete|prune [--profile=<id>]',
|
|
220
|
+
examples: [
|
|
221
|
+
{ cmd: 'construct sandbox create --profile=<id>', desc: 'Spin up an isolated sandbox' },
|
|
222
|
+
{ cmd: 'construct sandbox prune --days=7', desc: 'Remove sandboxes older than a week' },
|
|
223
|
+
],
|
|
182
224
|
subcommands: [
|
|
183
225
|
{ name: 'create [--profile=<id>]', desc: 'Create a new sandbox under ~/.cx/sandboxes/' },
|
|
184
226
|
{ name: 'list', desc: 'List existing sandboxes, newest first' },
|
|
@@ -193,6 +235,10 @@ export const CLI_COMMANDS = [
|
|
|
193
235
|
core: true,
|
|
194
236
|
description: 'Manage the active org profile and its lifecycle (draft, promote, archive, health)',
|
|
195
237
|
usage: 'construct profile show|list|set|create|drafts|archive|health',
|
|
238
|
+
examples: [
|
|
239
|
+
{ cmd: 'construct profile list', desc: 'List curated profiles' },
|
|
240
|
+
{ cmd: 'construct profile set <id>', desc: 'Switch the active profile' },
|
|
241
|
+
],
|
|
196
242
|
subcommands: [
|
|
197
243
|
{ name: 'show', desc: 'Show the active profile' },
|
|
198
244
|
{ name: 'list', desc: 'List curated profiles' },
|
|
@@ -224,6 +270,10 @@ export const CLI_COMMANDS = [
|
|
|
224
270
|
core: true,
|
|
225
271
|
description: 'Documentation commands',
|
|
226
272
|
usage: 'construct docs check|verify|update',
|
|
273
|
+
examples: [
|
|
274
|
+
{ cmd: 'construct docs verify', desc: 'Validate the docs tree' },
|
|
275
|
+
{ cmd: 'construct docs update', desc: 'Regenerate AUTO-managed regions' },
|
|
276
|
+
],
|
|
227
277
|
subcommands: [
|
|
228
278
|
{ name: 'check', desc: 'Check for missing how-to guides' },
|
|
229
279
|
{ name: 'verify', desc: 'Validate documentation quality' },
|
|
@@ -237,6 +287,10 @@ export const CLI_COMMANDS = [
|
|
|
237
287
|
core: true,
|
|
238
288
|
description: 'Check installation health',
|
|
239
289
|
usage: 'construct doctor [<status|logs|tick|report|consistency|watch|stop|credentials>] [--fix-legacy-agents]',
|
|
290
|
+
examples: [
|
|
291
|
+
{ cmd: 'construct doctor', desc: 'Run all health checks once' },
|
|
292
|
+
{ cmd: 'construct doctor report', desc: 'Print the latest health report' },
|
|
293
|
+
],
|
|
240
294
|
subcommands: [
|
|
241
295
|
{ name: 'status', desc: 'Doctor daemon status' },
|
|
242
296
|
{ name: 'logs', desc: 'Tail doctor daemon logs' },
|
|
@@ -487,6 +541,7 @@ export const CLI_COMMANDS = [
|
|
|
487
541
|
core: false,
|
|
488
542
|
description: 'Release dev-agent memory pressure by cleaning stale helper and bridge processes',
|
|
489
543
|
usage: 'construct cleanup [--dry-run] [--quiet] [--pressure-release] [--pressure-only] [--disk-only]',
|
|
544
|
+
strictFlags: true,
|
|
490
545
|
options: [
|
|
491
546
|
{ flag: '--dry-run', desc: 'Show what would be cleaned without changing anything' },
|
|
492
547
|
{ flag: '--quiet', desc: 'Minimal output' },
|
|
@@ -559,6 +614,36 @@ export const CLI_COMMANDS = [
|
|
|
559
614
|
description: 'Generate wireframes from description',
|
|
560
615
|
usage: 'construct wireframe <description>',
|
|
561
616
|
},
|
|
617
|
+
{
|
|
618
|
+
name: 'diagram',
|
|
619
|
+
emoji: '📊',
|
|
620
|
+
category: 'Work',
|
|
621
|
+
core: false,
|
|
622
|
+
description: 'Render code-driven diagrams via D2/Graphviz (optional system binaries; ADR-0001)',
|
|
623
|
+
usage: 'construct diagram <description> [--type=architecture|flow|sequence|state|er|class] [--format=svg|png] [--theme=<name>] [--out=<path>] [--source-only]',
|
|
624
|
+
strictFlags: true,
|
|
625
|
+
options: [
|
|
626
|
+
{ flag: '--type=<t>', desc: 'architecture (default) | flow | sequence | state | er | class' },
|
|
627
|
+
{ flag: '--format=<f>', desc: 'svg (default) | png' },
|
|
628
|
+
{ flag: '--theme=<name>', desc: 'D2 theme name (e.g. neutral, sketch, cool-classics)' },
|
|
629
|
+
{ flag: '--out=<path>', desc: 'Output path (default: .cx/diagrams/<slug>-<ts>.<ext>)' },
|
|
630
|
+
{ flag: '--source-only', desc: 'Always write the source file; skip rendering' },
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: 'demo',
|
|
635
|
+
emoji: '🎬',
|
|
636
|
+
category: 'Work',
|
|
637
|
+
core: false,
|
|
638
|
+
description: 'Record reproducible terminal demos via VHS/asciinema (optional system binaries; ADR-0001)',
|
|
639
|
+
usage: 'construct demo <name> [--format=gif|mp4|webm] [--out=<path>] [--source-only]',
|
|
640
|
+
strictFlags: true,
|
|
641
|
+
options: [
|
|
642
|
+
{ flag: '--format=<f>', desc: 'gif (default) | mp4 | webm (VHS only)' },
|
|
643
|
+
{ flag: '--out=<path>', desc: 'Output path (default: .cx/demos/<name>-<ts>.<ext>)' },
|
|
644
|
+
{ flag: '--source-only', desc: 'Always write the .tape source; skip recording' },
|
|
645
|
+
],
|
|
646
|
+
},
|
|
562
647
|
{
|
|
563
648
|
name: 'ollama',
|
|
564
649
|
emoji: '🦙',
|
|
@@ -936,6 +1021,9 @@ export const CLI_COMMANDS = [
|
|
|
936
1021
|
{ name: 'lint:contracts', category: 'Internal', core: false, internal: true, description: 'Internal lint: specialist contracts', usage: 'construct lint:contracts' },
|
|
937
1022
|
{ name: 'lint:research', category: 'Internal', core: false, internal: true, description: 'Internal lint: research artifacts', usage: 'construct lint:research' },
|
|
938
1023
|
{ name: 'lint:templates', category: 'Internal', core: false, internal: true, description: 'Internal lint: shipped templates', usage: 'construct lint:templates' },
|
|
1024
|
+
{ name: 'lint:prompts', category: 'Internal', core: false, internal: true, description: 'Internal lint: specialist prompt frontmatter + sections', usage: 'construct lint:prompts' },
|
|
1025
|
+
{ name: 'specialist', category: 'Internal', core: false, internal: true, description: 'Maintainer tool: scaffold, edit, and lint specialist prompts', usage: 'construct specialist <create|edit|lint>' },
|
|
1026
|
+
{ name: 'registry:status', category: 'Internal', core: false, internal: true, description: 'Dev: capability-matrix registry inspector', usage: 'construct registry:status' },
|
|
939
1027
|
{ name: 'evaluator:rubrics', category: 'Internal', core: false, internal: true, description: 'Internal: list registered evaluator rubrics', usage: 'construct evaluator:rubrics' },
|
|
940
1028
|
{ name: 'activation:status', category: 'Internal', core: false, internal: true, description: 'Internal: agent activation telemetry', usage: 'construct activation:status' },
|
|
941
1029
|
{ name: 'prune', category: 'Internal', core: false, internal: true, description: 'Internal: prune ephemeral storage entries', usage: 'construct prune' },
|
|
@@ -1031,5 +1119,28 @@ export function formatCommandHelp(name, { colors = false } = {}) {
|
|
|
1031
1119
|
}
|
|
1032
1120
|
lines.push('');
|
|
1033
1121
|
}
|
|
1122
|
+
|
|
1123
|
+
if (spec.examples && spec.examples.length > 0) {
|
|
1124
|
+
lines.push(`${c.bold}Examples${c.reset}`);
|
|
1125
|
+
for (const ex of spec.examples) {
|
|
1126
|
+
lines.push(` ${ex.cmd}`);
|
|
1127
|
+
if (ex.desc) lines.push(` ${c.dim}${ex.desc}${c.reset}`);
|
|
1128
|
+
}
|
|
1129
|
+
lines.push('');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Every help block closes with the obvious next action: an explicit `next`,
|
|
1133
|
+
// else "run a subcommand" for grouped commands, plus a See also pointer to the
|
|
1134
|
+
// full reference — so no surface is a dead end.
|
|
1135
|
+
|
|
1136
|
+
const firstSub = spec.subcommands?.[0]?.name.split(/\s+/)[0];
|
|
1137
|
+
const next = spec.next
|
|
1138
|
+
|| (firstSub ? `Run \`construct ${spec.name} ${firstSub}\` to start; add --help to any subcommand for details.` : null);
|
|
1139
|
+
if (next) {
|
|
1140
|
+
lines.push(`${c.dim}Next:${c.reset} ${next}`);
|
|
1141
|
+
lines.push('');
|
|
1142
|
+
}
|
|
1143
|
+
lines.push(`${c.dim}See also:${c.reset} run \`construct --help\` for all commands; full reference in docs/reference/cli/.`);
|
|
1144
|
+
lines.push('');
|
|
1034
1145
|
return lines.join('\n');
|
|
1035
1146
|
}
|
package/lib/comment-lint.mjs
CHANGED
|
@@ -86,7 +86,7 @@ function isConstructSelfRepo(rootDir) {
|
|
|
86
86
|
|
|
87
87
|
function requiresHeader(rel) {
|
|
88
88
|
const ext = path.extname(rel);
|
|
89
|
-
if (['.yaml', '.yml', '.json', '.jsonl', '.toml'].includes(ext)) return { required: false, type: null };
|
|
89
|
+
if (['.yaml', '.yml', '.json', '.jsonl', '.toml', '.txt'].includes(ext)) return { required: false, type: null };
|
|
90
90
|
const jsMatch = JS_HEADER_GLOBS.some(r => r.test(rel));
|
|
91
91
|
const mdMatch = MD_HEADER_GLOBS.some(r => r.test(rel));
|
|
92
92
|
// Exclude static assets under lib/server/static from requiring headers
|