@geraldmaron/construct 1.0.24 → 1.1.0
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/bin/construct +185 -3
- package/lib/agent-instructions/inject.mjs +25 -4
- package/lib/audit-rules.mjs +127 -0
- package/lib/audit-skills.mjs +43 -1
- 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 +5 -1
- package/lib/comment-lint.mjs +1 -1
- package/lib/config/schema.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 +37 -10
- package/lib/document-ingest.mjs +90 -5
- package/lib/embed/roadmap.mjs +16 -1
- package/lib/engine/consolidate.mjs +160 -3
- package/lib/engine/contradiction-judge.mjs +71 -0
- package/lib/engine/contradiction.mjs +74 -0
- 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/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/platforms/capabilities.mjs +100 -0
- package/lib/prompt-composer.js +12 -8
- 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/service-manager.mjs +41 -3
- package/lib/setup.mjs +21 -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/telemetry/beads-fallback.mjs +40 -0
- package/lib/telemetry/hook-calls.mjs +138 -0
- package/package.json +1 -1
- package/personas/construct.md +1 -1
- 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 +139 -39
- package/specialists/prompts/cx-architect.md +20 -0
- package/specialists/prompts/cx-test-automation.md +12 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/telemetry/hook-calls.mjs — measure which hooks fire and which actually block.
|
|
3
|
+
*
|
|
4
|
+
* Hooks carry @p95ms latency budgets but no record of how often each one fires or
|
|
5
|
+
* blocks, so a hook that gates real mistakes and one that has never fired once look
|
|
6
|
+
* identical. The central dispatcher (`construct hook <name>`) appends one JSON line
|
|
7
|
+
* per invocation to ~/.cx/hook-calls.jsonl with the hook id, the outcome derived
|
|
8
|
+
* from its exit code, and latency, so `construct hooks:usage` can answer which hooks
|
|
9
|
+
* earn their keep before any are consolidated.
|
|
10
|
+
*
|
|
11
|
+
* Outcome is what the dispatcher can observe centrally: exit 0 = ok, exit 2 = blocked
|
|
12
|
+
* (the Claude Code convention for a refused tool call — see block-no-verify), any
|
|
13
|
+
* other non-zero = error. A hook that mutates state without blocking reads as `ok`
|
|
14
|
+
* here; per-hook mutation reporting is a separate, opt-in signal.
|
|
15
|
+
*
|
|
16
|
+
* Disable with CONSTRUCT_HOOK_TELEMETRY=off. Errors here are non-fatal — telemetry
|
|
17
|
+
* must never change a hook's exit code or add a failure mode to the hot path.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { appendBounded } from '../logging/rotate.mjs';
|
|
23
|
+
import { resolveProjectScope } from '../project-root.mjs';
|
|
24
|
+
import { cxDir } from '../paths.mjs';
|
|
25
|
+
|
|
26
|
+
// cxDir() (not os.homedir()) so CX_HOME_OVERRIDE isolates the log in tests and the
|
|
27
|
+
// path stays consistent with the rest of ~/.cx. Resolved per-call so an override
|
|
28
|
+
// set after import still takes effect.
|
|
29
|
+
|
|
30
|
+
export function defaultLogPath() {
|
|
31
|
+
return path.join(cxDir(), 'hook-calls.jsonl');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exit-code → outcome, using the Claude Code hook convention (2 = block a tool
|
|
35
|
+
// call). Anything else non-zero is a hook that errored rather than deliberately
|
|
36
|
+
// gated, which is itself a value signal (a hook erroring every fire is noise).
|
|
37
|
+
|
|
38
|
+
export function outcomeFromExit(code) {
|
|
39
|
+
if (code === 0 || code == null) return 'ok';
|
|
40
|
+
if (code === 2) return 'blocked';
|
|
41
|
+
return 'error';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fire-and-forget log of one hook invocation.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} event
|
|
48
|
+
* @param {string} event.hookId — the hook name, e.g. "block-no-verify"
|
|
49
|
+
* @param {number} [event.exitCode] — the hook's process exit code
|
|
50
|
+
* @param {'ok'|'blocked'|'error'} [event.outcome] — derived from exitCode if omitted
|
|
51
|
+
* @param {number} [event.latencyMs]
|
|
52
|
+
* @param {object} [opts]
|
|
53
|
+
* @param {string} [opts.logPath]
|
|
54
|
+
* @param {NodeJS.ProcessEnv} [opts.env]
|
|
55
|
+
*/
|
|
56
|
+
export function logHookCall(event, opts = {}) {
|
|
57
|
+
const env = opts.env || process.env;
|
|
58
|
+
if (env.CONSTRUCT_HOOK_TELEMETRY === 'off') return;
|
|
59
|
+
if (!event || !event.hookId) return;
|
|
60
|
+
|
|
61
|
+
const logPath = opts.logPath || defaultLogPath();
|
|
62
|
+
const outcome = event.outcome || outcomeFromExit(event.exitCode);
|
|
63
|
+
const scope = resolveProjectScope();
|
|
64
|
+
const entry = {
|
|
65
|
+
ts: new Date().toISOString(),
|
|
66
|
+
hookId: event.hookId,
|
|
67
|
+
outcome,
|
|
68
|
+
...(event.exitCode != null ? { exitCode: event.exitCode } : {}),
|
|
69
|
+
...(scope?.projectId ? { projectId: scope.projectId } : {}),
|
|
70
|
+
...(event.latencyMs != null ? { latencyMs: event.latencyMs } : {}),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
75
|
+
appendBounded('hook-calls', logPath, JSON.stringify(entry) + '\n');
|
|
76
|
+
} catch {
|
|
77
|
+
// Append-only telemetry; an unwritable home dir is a silent skip rather than
|
|
78
|
+
// a new failure mode on the hook that triggered the call.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Roll a hook-calls log up into per-hook stats: fire count, blocked/error counts,
|
|
84
|
+
* most-recent timestamp, latency percentiles. A hook with calls but zero blocks/
|
|
85
|
+
* errors over a long window is a pure observer — a demotion candidate.
|
|
86
|
+
*/
|
|
87
|
+
export function summarizeHookCalls({ logPath = defaultLogPath(), since } = {}) {
|
|
88
|
+
if (!fs.existsSync(logPath)) return { totalEvents: 0, hooks: {} };
|
|
89
|
+
const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter(Boolean);
|
|
90
|
+
const sinceTs = since ? _parseSince(since) : null;
|
|
91
|
+
const hooks = {};
|
|
92
|
+
for (const line of lines) {
|
|
93
|
+
let entry;
|
|
94
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
95
|
+
if (!entry?.hookId) continue;
|
|
96
|
+
if (sinceTs && entry.ts && entry.ts < sinceTs) continue;
|
|
97
|
+
const slot = hooks[entry.hookId] ||= { calls: 0, blocked: 0, errors: 0, latencies: [], lastCalledAt: null };
|
|
98
|
+
slot.calls += 1;
|
|
99
|
+
if (entry.outcome === 'blocked') slot.blocked += 1;
|
|
100
|
+
if (entry.outcome === 'error') slot.errors += 1;
|
|
101
|
+
if (entry.latencyMs != null) slot.latencies.push(entry.latencyMs);
|
|
102
|
+
if (entry.ts && (!slot.lastCalledAt || entry.ts > slot.lastCalledAt)) slot.lastCalledAt = entry.ts;
|
|
103
|
+
}
|
|
104
|
+
const result = {};
|
|
105
|
+
for (const [id, slot] of Object.entries(hooks)) {
|
|
106
|
+
result[id] = {
|
|
107
|
+
calls: slot.calls,
|
|
108
|
+
blocked: slot.blocked,
|
|
109
|
+
errors: slot.errors,
|
|
110
|
+
lastCalledAt: slot.lastCalledAt,
|
|
111
|
+
...(slot.latencies.length ? _latencyStats(slot.latencies) : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { totalEvents: lines.length, hooks: result };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Hooks with zero fire events in the window — they cost a settings entry and a
|
|
119
|
+
* spawn budget but have never run, so they are consolidation candidates.
|
|
120
|
+
*/
|
|
121
|
+
export function findIdleHooks({ logPath = defaultLogPath(), since = '30d', allHookIds = [] } = {}) {
|
|
122
|
+
const { hooks } = summarizeHookCalls({ logPath, since });
|
|
123
|
+
const active = new Set(Object.keys(hooks));
|
|
124
|
+
return allHookIds.filter((id) => !active.has(id));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _parseSince(since) {
|
|
128
|
+
const m = String(since).match(/^(\d+)(d|h|m)$/);
|
|
129
|
+
if (!m) return null;
|
|
130
|
+
const ms = parseInt(m[1], 10) * ({ d: 86400000, h: 3600000, m: 60000 }[m[2]]);
|
|
131
|
+
return new Date(Date.now() - ms).toISOString();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _latencyStats(arr) {
|
|
135
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
136
|
+
const p = (pct) => sorted[Math.floor((pct / 100) * (sorted.length - 1))];
|
|
137
|
+
return { p50LatencyMs: p(50), p95LatencyMs: p(95) };
|
|
138
|
+
}
|
package/package.json
CHANGED
package/personas/construct.md
CHANGED
|
@@ -62,7 +62,7 @@ The active profile (`construct profile show`) sets the intake taxonomy. Session-
|
|
|
62
62
|
|
|
63
63
|
Lead with the answer. One question when blocked. Confirm what changed when done.
|
|
64
64
|
|
|
65
|
-
**Output style**: format human-facing output (terminal, prose, dashboard) for neurodivergent readers — answer first, clear hierarchy,
|
|
65
|
+
**Output style**: format human-facing output (terminal, prose, dashboard) for neurodivergent readers — answer first, clear hierarchy, plain language, explicit next step. Prose for reasoning; lists only for genuinely parallel items where scanning helps, never a wall of bullets. Never rely on color or motion alone; honor `NO_COLOR` and reduced-motion. Presentation only — never reshape machine-readable output (`--json`, parsed tokens, registries, contracts). See `rules/common/neurodivergent-output.md`.
|
|
66
66
|
|
|
67
67
|
**Tool invisibility**: deliverables are about the user's project, never Construct. Never name Construct, `cx-*` role ids, or internal orchestration mechanics in artifact content unless the subject project is Construct itself. Provenance goes in a comment, not the prose. See `rules/common/tool-invisibility.md`.
|
|
68
68
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../schemas/platform-capabilities.schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"hosts": {
|
|
5
|
+
"claude": {
|
|
6
|
+
"displayName": "Claude Code",
|
|
7
|
+
"hasNativeSubagents": false,
|
|
8
|
+
"instructionsOnly": false,
|
|
9
|
+
"supportsMcp": true,
|
|
10
|
+
"configFormat": "json",
|
|
11
|
+
"localModelProvisioning": "none",
|
|
12
|
+
"hooks": {
|
|
13
|
+
"supported": true,
|
|
14
|
+
"globalAllowlist": [
|
|
15
|
+
"pre:bash:block-no-verify",
|
|
16
|
+
"pre:bash:guard-dangerous",
|
|
17
|
+
"pre:edit:config-protection",
|
|
18
|
+
"pre:edit-guard",
|
|
19
|
+
"post:edit:json-validate",
|
|
20
|
+
"post:edit:scan-secrets"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"globalMcpAllowlist": ["context7"]
|
|
24
|
+
},
|
|
25
|
+
"codex": {
|
|
26
|
+
"displayName": "Codex",
|
|
27
|
+
"hasNativeSubagents": false,
|
|
28
|
+
"instructionsOnly": false,
|
|
29
|
+
"supportsMcp": true,
|
|
30
|
+
"configFormat": "toml",
|
|
31
|
+
"localModelProvisioning": "none",
|
|
32
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
33
|
+
"globalMcpAllowlist": []
|
|
34
|
+
},
|
|
35
|
+
"copilot": {
|
|
36
|
+
"displayName": "Copilot",
|
|
37
|
+
"hasNativeSubagents": false,
|
|
38
|
+
"instructionsOnly": true,
|
|
39
|
+
"supportsMcp": false,
|
|
40
|
+
"configFormat": "markdown",
|
|
41
|
+
"localModelProvisioning": "none",
|
|
42
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
43
|
+
"globalMcpAllowlist": []
|
|
44
|
+
},
|
|
45
|
+
"opencode": {
|
|
46
|
+
"displayName": "OpenCode",
|
|
47
|
+
"hasNativeSubagents": true,
|
|
48
|
+
"instructionsOnly": false,
|
|
49
|
+
"supportsMcp": true,
|
|
50
|
+
"configFormat": "json",
|
|
51
|
+
"localModelProvisioning": "modelfile",
|
|
52
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
53
|
+
"globalMcpAllowlist": []
|
|
54
|
+
},
|
|
55
|
+
"vscode": {
|
|
56
|
+
"displayName": "VS Code",
|
|
57
|
+
"hasNativeSubagents": true,
|
|
58
|
+
"instructionsOnly": false,
|
|
59
|
+
"supportsMcp": true,
|
|
60
|
+
"configFormat": "json",
|
|
61
|
+
"localModelProvisioning": "none",
|
|
62
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
63
|
+
"globalMcpAllowlist": []
|
|
64
|
+
},
|
|
65
|
+
"cursor": {
|
|
66
|
+
"displayName": "Cursor",
|
|
67
|
+
"hasNativeSubagents": true,
|
|
68
|
+
"instructionsOnly": false,
|
|
69
|
+
"supportsMcp": true,
|
|
70
|
+
"configFormat": "json",
|
|
71
|
+
"localModelProvisioning": "none",
|
|
72
|
+
"hooks": { "supported": false, "globalAllowlist": [] },
|
|
73
|
+
"globalMcpAllowlist": []
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* sync-config.mjs — Fetch free models from OpenRouter and
|
|
4
|
-
*
|
|
3
|
+
* sync-config.mjs — Fetch free models from OpenRouter and local models from Ollama,
|
|
4
|
+
* and sync into OpenCode config.
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
-
import {
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { readOpenCodeConfig, writeOpenCodeConfig, findOpenCodeConfigPath } from "../../lib/opencode-config.mjs";
|
|
9
|
+
import { ensureLocalContextVariants, modelDigest } from "../../lib/ollama/provision-context.mjs";
|
|
10
|
+
import { isKnownCollapsed } from "../../lib/ollama/capability-store.mjs";
|
|
11
|
+
|
|
12
|
+
const LOCAL_NUM_CTX = Number(process.env.CONSTRUCT_LOCAL_NUM_CTX) || 32768;
|
|
8
13
|
|
|
9
14
|
const configPath = findOpenCodeConfigPath();
|
|
10
15
|
|
|
16
|
+
async function fetchLocalOllamaModels() {
|
|
17
|
+
try {
|
|
18
|
+
const r = spawnSync("ollama", ["list"], { encoding: "utf8" });
|
|
19
|
+
if (r.status !== 0) return [];
|
|
20
|
+
|
|
21
|
+
const lines = r.stdout.trim().split("\n").slice(1);
|
|
22
|
+
const models = lines.map(line => {
|
|
23
|
+
const parts = line.split(/\s+/).filter(Boolean);
|
|
24
|
+
const id = parts[0];
|
|
25
|
+
const size = parts[2];
|
|
26
|
+
return { id, name: id, size };
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Identify redundant tags (same family, many tags)
|
|
30
|
+
const familyMap = new Map();
|
|
31
|
+
for (const m of models) {
|
|
32
|
+
const family = m.id.split(":")[0];
|
|
33
|
+
if (!familyMap.has(family)) familyMap.set(family, []);
|
|
34
|
+
familyMap.get(family).push(m);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const [family, tags] of familyMap) {
|
|
38
|
+
if (tags.length > 2) {
|
|
39
|
+
console.log(`[cleanup] Potential redundant tags for ${family}: ${tags.map(t => t.id).join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return models;
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
async function fetchFreeModels() {
|
|
12
50
|
try {
|
|
13
51
|
const res = await fetch("https://openrouter.ai/api/v1/models");
|
|
@@ -31,39 +69,97 @@ async function main() {
|
|
|
31
69
|
|
|
32
70
|
const { config } = readOpenCodeConfig();
|
|
33
71
|
const freeModels = await fetchFreeModels();
|
|
72
|
+
const localModels = await fetchLocalOllamaModels();
|
|
34
73
|
|
|
35
|
-
if (freeModels.length === 0) {
|
|
36
|
-
console.log("No
|
|
74
|
+
if (freeModels.length === 0 && localModels.length === 0) {
|
|
75
|
+
console.log("No models fetched. Config unchanged.");
|
|
37
76
|
return;
|
|
38
77
|
}
|
|
39
78
|
|
|
40
79
|
if (!config.provider) config.provider = {};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
|
|
81
|
+
// Sync OpenRouter Free Models
|
|
82
|
+
if (freeModels.length > 0) {
|
|
83
|
+
if (!config.provider.openrouter) {
|
|
84
|
+
config.provider.openrouter = {
|
|
85
|
+
npm: "@ai-sdk/openai-compatible",
|
|
86
|
+
name: "OpenRouter",
|
|
87
|
+
options: {
|
|
88
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
89
|
+
headers: {}
|
|
90
|
+
},
|
|
91
|
+
models: {}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const existingOR = config.provider.openrouter.models ?? {};
|
|
95
|
+
for (const model of freeModels) {
|
|
96
|
+
const cleanName = model.name.replace(/\s*\(free\)/gi, "").trim();
|
|
97
|
+
existingOR[model.id] = { name: `[free] ${cleanName}` };
|
|
98
|
+
}
|
|
99
|
+
config.provider.openrouter.models = Object.fromEntries(
|
|
100
|
+
Object.entries(existingOR).sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
101
|
+
);
|
|
51
102
|
}
|
|
52
103
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
104
|
+
// Sync Local Ollama Models
|
|
105
|
+
if (localModels.length > 0) {
|
|
106
|
+
if (!config.provider.ollama) {
|
|
107
|
+
config.provider.ollama = {
|
|
108
|
+
npm: "@ai-sdk/openai-compatible",
|
|
109
|
+
name: "Ollama",
|
|
110
|
+
options: {
|
|
111
|
+
baseURL: "http://localhost:11434/v1"
|
|
112
|
+
},
|
|
113
|
+
models: {}
|
|
114
|
+
};
|
|
58
115
|
}
|
|
59
|
-
|
|
116
|
+
// A raw model runs at Ollama's 4096 default over the /v1 path, which a Construct
|
|
117
|
+
// session's tool schemas overrun. Provision a context-extended Modelfile variant
|
|
118
|
+
// for each tool-capable model lacking a baked num_ctx (any size — capability does
|
|
119
|
+
// not track size) and register the variant in place of the raw tag so the model
|
|
120
|
+
// OpenCode actually talks to has a real context window.
|
|
121
|
+
|
|
122
|
+
const { mapping: variantMap, actions } = ensureLocalContextVariants({ numCtx: LOCAL_NUM_CTX });
|
|
123
|
+
for (const a of actions) {
|
|
124
|
+
if (a.action === "created" || a.action === "would-create") console.log(`[ollama] context variant ${a.action}: ${a.variant} (num_ctx ${LOCAL_NUM_CTX})`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const existingLocal = config.provider.ollama.models ?? {};
|
|
128
|
+
for (const model of localModels) {
|
|
129
|
+
const registerId = variantMap[model.id] || model.id;
|
|
130
|
+
// Register the context-extended variant in place of the raw tag so the model
|
|
131
|
+
// picker surfaces the windowed option, not the one Ollama serves at 4096.
|
|
60
132
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
if (registerId !== model.id) delete existingLocal[model.id];
|
|
134
|
+
existingLocal[registerId] = {
|
|
135
|
+
name: registerId.replace(/\s*\(local\)/gi, "").trim(),
|
|
136
|
+
family: registerId.includes("qwen") ? "qwen2" : "llama",
|
|
137
|
+
tool_call: true
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Capability honesty: a model the probe recorded as COLLAPSED (digest still
|
|
141
|
+
// matching) word-salads on the agentic loop. Warn rather than hide — the
|
|
142
|
+
// user keeps the choice and can re-probe — so a stale or false verdict never
|
|
143
|
+
// silently strands a working model.
|
|
144
|
+
if (isKnownCollapsed(model.id, modelDigest(model.id))) {
|
|
145
|
+
console.log(`[ollama] WARNING: ${model.id} probed COLLAPSED — not agentic-capable. Re-probe: construct doctor --probe-local`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
config.provider.ollama.models = Object.fromEntries(
|
|
149
|
+
Object.entries(existingLocal).sort((a, b) => (a[1].name ?? a[0]).localeCompare(b[1].name ?? b[0]))
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Agentic coherence is model-specific and not predictable from size: some small
|
|
153
|
+
// coder models collapse into repetition on an agentic system prompt while others
|
|
154
|
+
// run the same Construct payload cleanly. Surface the empirical probe rather than
|
|
155
|
+
// silently registering a model that will produce word salad.
|
|
156
|
+
|
|
157
|
+
console.log("[ollama] Verify a model handles agentic prompts before relying on it:");
|
|
158
|
+
console.log("[ollama] node lib/ollama/provision-context.mjs --probe --model=<id>");
|
|
159
|
+
}
|
|
64
160
|
|
|
65
161
|
writeOpenCodeConfig(config, configPath);
|
|
66
|
-
console.log(`Synced ${freeModels.length} free models
|
|
162
|
+
console.log(`Synced ${freeModels.length} free models and ${localModels.length} local models.`);
|
|
67
163
|
}
|
|
68
164
|
|
|
69
165
|
main();
|
|
@@ -28,7 +28,7 @@ Use headings in order (H2, then H3) and do not skip or nest deeper. One `h1`-equ
|
|
|
28
28
|
|
|
29
29
|
## 3. Scannable chunks
|
|
30
30
|
|
|
31
|
-
Short paragraphs.
|
|
31
|
+
Short paragraphs with white space between sections; prefer three short lines over one dense one. Use a bulleted or numbered list for genuinely parallel items — a set of options, steps, or independent facts — where scanning is the point. Keep reasoning, cause-and-effect, and narrative in prose: a wall of bullets fragments the logic that connects the points and is harder to follow, not easier. Bullets for what scans, sentences for what reasons.
|
|
32
32
|
|
|
33
33
|
## 4. Plain, literal language
|
|
34
34
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web coding style rule. Use when writing or reviewing web code that involves coding style.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/coding-style.md](../common/coding-style.md) with web-specific frontend content.
|
|
5
13
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web design quality rule. Use when writing or reviewing web code that involves design quality.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/patterns.md](../common/patterns.md) with web-specific design-quality guidance.
|
|
5
13
|
|
package/rules/web/hooks.md
CHANGED
package/rules/web/patterns.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web patterns rule. Use when writing or reviewing web code that involves patterns.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/patterns.md](../common/patterns.md) with web-specific patterns.
|
|
5
13
|
|
package/rules/web/performance.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web performance rule. Use when writing or reviewing web code that involves performance.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/performance.md](../common/performance.md) with web-specific performance content.
|
|
5
13
|
|
package/rules/web/security.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web security rule. Use when writing or reviewing web code that involves security.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/security.md](../common/security.md) with web-specific security content.
|
|
5
13
|
|
package/rules/web/testing.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Construct web testing rule. Use when writing or reviewing web code that involves testing.
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.tsx"
|
|
5
|
+
- "**/*.jsx"
|
|
6
|
+
- "**/*.css"
|
|
7
|
+
- "**/*.scss"
|
|
8
|
+
- "**/*.html"
|
|
9
|
+
- "**/*.vue"
|
|
10
|
+
- "**/*.svelte"
|
|
3
11
|
---
|
|
4
12
|
> This file extends [common/testing.md](../common/testing.md) with web-specific testing content.
|
|
5
13
|
|