@geraldmaron/construct 1.0.7 → 1.0.9
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 +20 -17
- package/bin/construct +1199 -69
- package/commands/work/optimize-prompts.md +1 -1
- package/db/schema/007_tags.sql +30 -0
- package/db/schema/008_skill_usage.sql +24 -0
- package/db/schema/009_scheduler.sql +14 -0
- package/examples/internal/roles/architect/bad/clever-plan-without-contracts.md +1 -1
- package/examples/internal/roles/architect/golden/explicit-tradeoff-before-plan.md +1 -1
- package/examples/internal/roles/engineer/bad/speculative-abstraction.md +1 -1
- package/examples/internal/roles/engineer/golden/read-before-write.md +1 -1
- package/examples/internal/roles/qa/bad/coverage-theater.md +1 -1
- package/examples/internal/roles/qa/golden/regression-gate.md +1 -1
- package/examples/seed-observations/decisions.md +1 -1
- package/lib/audit-skills.mjs +3 -3
- package/lib/auto-docs.mjs +2 -2
- package/lib/cli-commands.mjs +276 -122
- package/lib/comment-lint.mjs +5 -1
- package/lib/completions.mjs +1 -1
- package/lib/config/schema.mjs +15 -1
- package/lib/contracts/validate.mjs +65 -17
- package/lib/distill.mjs +22 -4
- package/lib/doc-stamp.mjs +48 -0
- package/lib/doctor/watchers/consistency.mjs +62 -12
- package/lib/document-extract.mjs +221 -28
- package/lib/document-ingest.mjs +2 -0
- package/lib/embed/inbox.mjs +48 -3
- package/lib/embed/role-framing.mjs +3 -3
- package/lib/env-config.mjs +9 -3
- package/lib/extractors/calendar.mjs +173 -0
- package/lib/extractors/shared/drop-info.mjs +22 -0
- package/lib/extractors/transcript.mjs +0 -0
- package/lib/gates-audit.mjs +8 -2
- package/lib/git-hooks/prepare-commit-msg +1 -1
- package/lib/headhunt.mjs +2 -2
- package/lib/hooks/agent-tracker.mjs +6 -11
- package/lib/hooks/guard-bash.mjs +35 -14
- package/lib/hooks/mcp-audit.mjs +12 -0
- package/lib/hooks/pre-compact.mjs +92 -5
- package/lib/hooks/pre-push-gate.mjs +41 -8
- package/lib/hooks/registry-sync.mjs +2 -2
- package/lib/hooks/stop-notify.mjs +7 -6
- package/lib/host-capabilities.mjs +10 -1
- package/lib/hygiene/scan.mjs +141 -0
- package/lib/init-unified.mjs +18 -0
- package/lib/install/stage-project.mjs +4 -4
- package/lib/intake/classify.mjs +222 -55
- package/lib/intake/filesystem-queue.mjs +25 -5
- package/lib/intake/postgres-queue.mjs +45 -3
- package/lib/intake/prepare.mjs +3 -0
- package/lib/intake/quarantine.mjs +205 -0
- package/lib/knowledge/postgres-search.mjs +132 -0
- package/lib/knowledge/search.mjs +14 -4
- package/lib/mcp/server.mjs +9 -3
- package/lib/mcp/tools/skills.mjs +32 -11
- package/lib/mcp/tools/workflow.mjs +1 -1
- package/lib/migrations/index.mjs +1 -1
- package/lib/model-registry.mjs +1 -1
- package/lib/opencode-runtime-plugin.mjs +1 -1
- package/lib/orchestration-policy.mjs +4 -4
- package/lib/overrides/resolver.mjs +3 -3
- package/lib/parity.mjs +4 -4
- package/lib/policy/engine.mjs +2 -2
- package/lib/profiles/lifecycle.mjs +1 -1
- package/lib/prompt-metadata.mjs +4 -4
- package/lib/providers/circuit-breaker.mjs +14 -0
- package/lib/providers/contract.mjs +67 -3
- package/lib/providers/creds.mjs +219 -0
- package/lib/providers/scaffold.mjs +107 -0
- package/lib/role-preload.mjs +5 -0
- package/lib/roles/catalog.mjs +1 -1
- package/lib/roles/manifest.mjs +2 -2
- package/lib/scheduler/index.mjs +112 -0
- package/lib/scheduler/solo.mjs +183 -0
- package/lib/server/index.mjs +7 -1
- package/lib/server/insights.mjs +14 -12
- package/lib/server/langfuse-login.mjs +58 -0
- package/lib/server/static/assets/index-ab25c707.js +1 -1
- package/lib/{agent-contracts-enforce.mjs → specialist-contracts-enforce.mjs} +4 -4
- package/lib/{agent-contracts.mjs → specialist-contracts.mjs} +9 -8
- package/lib/{agents → specialists}/postconditions.mjs +3 -3
- package/lib/{agents → specialists}/schema.mjs +6 -6
- package/lib/status.mjs +13 -11
- package/lib/storage/backup.mjs +2 -2
- package/lib/tags/lifecycle.mjs +278 -0
- package/lib/tags/vocabulary.mjs +140 -0
- package/lib/telemetry/otel-tracer.mjs +184 -0
- package/lib/telemetry/skill-calls.mjs +73 -12
- package/lib/uninstall/uninstall.mjs +1 -1
- package/lib/update.mjs +1 -1
- package/lib/validator.mjs +57 -56
- package/lib/workflows/instantiate.mjs +320 -0
- package/package.json +13 -3
- package/personas/construct.md +2 -2
- package/platforms/claude/CLAUDE.md +1 -1
- package/rules/common/no-fabrication.md +1 -1
- package/scripts/{sync-agents.mjs → sync-specialists.mjs} +130 -58
- package/skills/ai/prompt-optimizer.md +3 -3
- package/skills/exploration/dependency-graph-reading.md +98 -0
- package/skills/exploration/tracer-bullet-method.md +71 -0
- package/skills/exploration/unknown-codebase-onboarding.md +91 -0
- package/skills/operating/change-management.md +91 -0
- package/skills/operating/incident-response.md +75 -0
- package/skills/operating/oncall-rotation.md +95 -0
- package/skills/operating/orchestration-reference.md +2 -2
- package/skills/strategy/competitive-landscape.md +75 -0
- package/skills/strategy/market-research-methods.md +87 -0
- package/skills/strategy/narrative-arc.md +77 -0
- package/skills/strategy/pricing-positioning.md +94 -0
- package/{agents → specialists}/contracts.schema.json +1 -1
- package/specialists/policy-inventory.json +160 -0
- package/{agents → specialists}/prompts/cx-accessibility.md +1 -1
- package/{agents → specialists}/prompts/cx-ai-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-architect.md +1 -1
- package/{agents → specialists}/prompts/cx-business-strategist.md +1 -1
- package/{agents → specialists}/prompts/cx-data-analyst.md +1 -1
- package/{agents → specialists}/prompts/cx-data-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-debugger.md +1 -1
- package/{agents → specialists}/prompts/cx-designer.md +1 -1
- package/{agents → specialists}/prompts/cx-devil-advocate.md +1 -1
- package/{agents → specialists}/prompts/cx-docs-keeper.md +2 -2
- package/{agents → specialists}/prompts/cx-engineer.md +2 -2
- package/{agents → specialists}/prompts/cx-evaluator.md +1 -1
- package/{agents → specialists}/prompts/cx-explorer.md +1 -1
- package/{agents → specialists}/prompts/cx-legal-compliance.md +1 -1
- package/{agents → specialists}/prompts/cx-operations.md +1 -1
- package/{agents → specialists}/prompts/cx-orchestrator.md +2 -2
- package/{agents → specialists}/prompts/cx-platform-engineer.md +1 -1
- package/{agents → specialists}/prompts/cx-product-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-qa.md +1 -1
- package/{agents → specialists}/prompts/cx-rd-lead.md +1 -1
- package/{agents → specialists}/prompts/cx-release-manager.md +1 -1
- package/{agents → specialists}/prompts/cx-researcher.md +1 -1
- package/{agents → specialists}/prompts/cx-reviewer.md +2 -2
- package/{agents → specialists}/prompts/cx-security.md +1 -1
- package/{agents → specialists}/prompts/cx-sre.md +2 -2
- package/{agents → specialists}/prompts/cx-test-automation.md +1 -1
- package/{agents → specialists}/prompts/cx-trace-reviewer.md +3 -3
- package/{agents → specialists}/prompts/cx-ux-researcher.md +1 -1
- package/{agents → specialists}/registry.json +651 -423
- package/{agents → specialists}/role-manifests.json +8 -8
- package/templates/provider-scaffold/health.test.mjs +30 -0
- package/templates/provider-scaffold/index.mjs +48 -0
- package/templates/workflows/cross-team-handoff.yml +85 -0
- package/templates/workflows/engineering-onboarding.yml +77 -0
- package/templates/workflows/new-feature.yml +53 -0
- /package/{agents → specialists}/contracts.json +0 -0
- /package/{agents → specialists}/teams.json +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/workflows/instantiate.mjs — Workflow template instantiation engine.
|
|
3
|
+
*
|
|
4
|
+
* Resolves a workflow template (by id or path), validates required inputs,
|
|
5
|
+
* substitutes ${variable_name} placeholders in all string fields, and returns
|
|
6
|
+
* the artifact list and beads item list that the caller (CLI) should create.
|
|
7
|
+
*
|
|
8
|
+
* Key behaviors:
|
|
9
|
+
* - Template lookup: templates/workflows/<id>.yml relative to the nearest
|
|
10
|
+
* package root, or an explicit absolute/relative path.
|
|
11
|
+
* - Variable substitution: every ${name} token in string fields is replaced
|
|
12
|
+
* with the caller-supplied value for that name. Unresolved tokens are left
|
|
13
|
+
* as-is and do not cause an error unless the variable is required.
|
|
14
|
+
* - Artifact copy: for each artifact, if templates/docs/<template> exists the
|
|
15
|
+
* file is copied to the output path with a title comment prepended. If the
|
|
16
|
+
* template is not found, a stub markdown file is written instead.
|
|
17
|
+
* - Beads items are NOT created by this module. The returned beadsItems array
|
|
18
|
+
* is handed to the CLI, which calls the beads client.
|
|
19
|
+
* - File writes are skipped when cwd is null (dry-run mode).
|
|
20
|
+
*
|
|
21
|
+
* Non-obvious constraints:
|
|
22
|
+
* - YAML is parsed with a minimal line-by-line parser; complex YAML (anchors,
|
|
23
|
+
* multi-document, block scalars with special indicators) is not supported.
|
|
24
|
+
* The workflow schema is intentionally kept simple to stay within this limit.
|
|
25
|
+
* - The function is synchronous except for file I/O which uses the sync fs API,
|
|
26
|
+
* so callers do not need to await anything.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
|
|
33
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
|
|
35
|
+
// Walk upward from startDir until we find package.json, or fall back to startDir.
|
|
36
|
+
function findPackageRoot(startDir) {
|
|
37
|
+
let dir = startDir;
|
|
38
|
+
for (let i = 0; i < 12; i++) {
|
|
39
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir) break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
return startDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Replace every ${name} token in a string with its value from the vars map.
|
|
48
|
+
function substitute(str, vars) {
|
|
49
|
+
if (typeof str !== "string") return str;
|
|
50
|
+
return str.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
51
|
+
return Object.prototype.hasOwnProperty.call(vars, name) ? vars[name] : `\${${name}}`;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Apply substitution recursively to all string values in an object or array.
|
|
56
|
+
function substituteDeep(node, vars) {
|
|
57
|
+
if (typeof node === "string") return substitute(node, vars);
|
|
58
|
+
if (Array.isArray(node)) return node.map((item) => substituteDeep(item, vars));
|
|
59
|
+
if (node !== null && typeof node === "object") {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [k, v] of Object.entries(node)) out[k] = substituteDeep(v, vars);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Minimal YAML parser. Handles the flat structure used in workflow templates:
|
|
68
|
+
// top-level scalar keys, simple lists (- key: value), and nested objects.
|
|
69
|
+
// Returns a plain JS object. Does not handle anchors, multi-line strings with
|
|
70
|
+
// block indicators, or multi-document streams.
|
|
71
|
+
function parseYaml(text) {
|
|
72
|
+
const lines = text.split("\n");
|
|
73
|
+
const root = {};
|
|
74
|
+
const stack = [{ obj: root, indent: -1 }];
|
|
75
|
+
|
|
76
|
+
function currentFrame() {
|
|
77
|
+
return stack[stack.length - 1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getIndent(line) {
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < line.length && line[i] === " ") i++;
|
|
83
|
+
return i;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseValue(raw) {
|
|
87
|
+
const v = raw.trim();
|
|
88
|
+
if (v === "true") return true;
|
|
89
|
+
if (v === "false") return false;
|
|
90
|
+
if (v === "null" || v === "~" || v === "") return null;
|
|
91
|
+
const n = Number(v);
|
|
92
|
+
if (!isNaN(n) && v !== "") return n;
|
|
93
|
+
// Strip surrounding quotes.
|
|
94
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
95
|
+
return v.slice(1, -1);
|
|
96
|
+
}
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let i = 0;
|
|
101
|
+
while (i < lines.length) {
|
|
102
|
+
const rawLine = lines[i];
|
|
103
|
+
i++;
|
|
104
|
+
|
|
105
|
+
// Skip blank lines and comments.
|
|
106
|
+
const trimmed = rawLine.trim();
|
|
107
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
108
|
+
|
|
109
|
+
const indent = getIndent(rawLine);
|
|
110
|
+
|
|
111
|
+
// Pop stack frames that are deeper than the current indent.
|
|
112
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
113
|
+
stack.pop();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const frame = currentFrame();
|
|
117
|
+
|
|
118
|
+
if (trimmed.startsWith("- ")) {
|
|
119
|
+
// List item with inline key:value pairs, or a plain scalar.
|
|
120
|
+
const itemStr = trimmed.slice(2);
|
|
121
|
+
const item = {};
|
|
122
|
+
// Parse inline key: value pairs within this list item by consuming
|
|
123
|
+
// continuation lines at a deeper indent level.
|
|
124
|
+
const itemIndent = indent + 2;
|
|
125
|
+
const firstColon = itemStr.indexOf(": ");
|
|
126
|
+
if (firstColon !== -1) {
|
|
127
|
+
const k = itemStr.slice(0, firstColon).trim();
|
|
128
|
+
const v = itemStr.slice(firstColon + 2).trim();
|
|
129
|
+
item[k] = parseValue(v);
|
|
130
|
+
|
|
131
|
+
// Consume continuation lines at deeper indent.
|
|
132
|
+
while (i < lines.length) {
|
|
133
|
+
const peekRaw = lines[i];
|
|
134
|
+
const peekTrimmed = peekRaw.trim();
|
|
135
|
+
if (!peekTrimmed || peekTrimmed.startsWith("#")) { i++; continue; }
|
|
136
|
+
const peekIndent = getIndent(peekRaw);
|
|
137
|
+
if (peekIndent <= indent) break;
|
|
138
|
+
|
|
139
|
+
if (peekTrimmed.startsWith("- ")) {
|
|
140
|
+
// Nested list under this item key — assign to the last key in the item object.
|
|
141
|
+
const lastKey = Object.keys(item)[Object.keys(item).length - 1];
|
|
142
|
+
if (!Array.isArray(item[lastKey])) item[lastKey] = [];
|
|
143
|
+
item[lastKey].push(parseValue(peekTrimmed.slice(2)));
|
|
144
|
+
i++;
|
|
145
|
+
} else {
|
|
146
|
+
const subColon = peekTrimmed.indexOf(": ");
|
|
147
|
+
if (subColon !== -1) {
|
|
148
|
+
const sk = peekTrimmed.slice(0, subColon).trim();
|
|
149
|
+
const sv = peekTrimmed.slice(subColon + 2).trim();
|
|
150
|
+
item[sk] = parseValue(sv);
|
|
151
|
+
i++;
|
|
152
|
+
} else if (peekTrimmed.endsWith(":")) {
|
|
153
|
+
const sk = peekTrimmed.slice(0, -1).trim();
|
|
154
|
+
item[sk] = {};
|
|
155
|
+
i++;
|
|
156
|
+
} else {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Ensure the parent object has an array for this key.
|
|
163
|
+
const parentKey = frame.listKey;
|
|
164
|
+
if (parentKey && Array.isArray(frame.obj[parentKey])) {
|
|
165
|
+
frame.obj[parentKey].push(item);
|
|
166
|
+
} else {
|
|
167
|
+
// No listKey on the frame — fall back to frame.currentArray, which is
|
|
168
|
+
// set when a bare "key:" line initialises a new array in the parent object.
|
|
169
|
+
const parentArr = frame.currentArray;
|
|
170
|
+
if (Array.isArray(parentArr)) {
|
|
171
|
+
parentArr.push(item);
|
|
172
|
+
} else {
|
|
173
|
+
// Fallback: push to the last array-type property of the parent obj.
|
|
174
|
+
const keys = Object.keys(frame.obj);
|
|
175
|
+
for (let ki = keys.length - 1; ki >= 0; ki--) {
|
|
176
|
+
if (Array.isArray(frame.obj[keys[ki]])) {
|
|
177
|
+
frame.obj[keys[ki]].push(item);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Plain scalar list item.
|
|
185
|
+
const lastKey = frame.listKey;
|
|
186
|
+
if (lastKey && Array.isArray(frame.obj[lastKey])) {
|
|
187
|
+
frame.obj[lastKey].push(parseValue(itemStr));
|
|
188
|
+
} else if (Array.isArray(frame.currentArray)) {
|
|
189
|
+
frame.currentArray.push(parseValue(itemStr));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else if (trimmed.endsWith(":")) {
|
|
193
|
+
// Bare key — value will be an object or array on following lines.
|
|
194
|
+
const key = trimmed.slice(0, -1).trim();
|
|
195
|
+
frame.obj[key] = [];
|
|
196
|
+
stack.push({ obj: frame.obj, indent, listKey: key, currentArray: frame.obj[key] });
|
|
197
|
+
} else {
|
|
198
|
+
const colon = trimmed.indexOf(": ");
|
|
199
|
+
if (colon !== -1) {
|
|
200
|
+
const key = trimmed.slice(0, colon).trim();
|
|
201
|
+
const val = trimmed.slice(colon + 2);
|
|
202
|
+
frame.obj[key] = parseValue(val);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return root;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Locate a workflow template by id (looks in templates/workflows/<id>.yml) or
|
|
211
|
+
// by explicit path. Returns the absolute resolved path.
|
|
212
|
+
function resolveTemplatePath(templateIdOrPath, packageRoot) {
|
|
213
|
+
if (path.isAbsolute(templateIdOrPath) || templateIdOrPath.includes(path.sep)) {
|
|
214
|
+
return path.resolve(templateIdOrPath);
|
|
215
|
+
}
|
|
216
|
+
// Strip .yml extension if provided.
|
|
217
|
+
const id = templateIdOrPath.replace(/\.ya?ml$/, "");
|
|
218
|
+
return path.join(packageRoot, "templates", "workflows", `${id}.yml`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Resolve the variables map by merging declared defaults with caller-supplied
|
|
222
|
+
// values, then check for missing required inputs.
|
|
223
|
+
function resolveVars(inputs, suppliedValues) {
|
|
224
|
+
if (!Array.isArray(inputs)) return suppliedValues || {};
|
|
225
|
+
const vars = {};
|
|
226
|
+
|
|
227
|
+
for (const input of inputs) {
|
|
228
|
+
if (Object.prototype.hasOwnProperty.call(suppliedValues || {}, input.name)) {
|
|
229
|
+
vars[input.name] = String(suppliedValues[input.name]);
|
|
230
|
+
} else if (input.default != null) {
|
|
231
|
+
vars[input.name] = String(input.default);
|
|
232
|
+
} else if (input.required) {
|
|
233
|
+
throw new Error(`Required workflow input "${input.name}" was not supplied.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Also pass through any extra values the caller provided.
|
|
238
|
+
for (const [k, v] of Object.entries(suppliedValues || {})) {
|
|
239
|
+
if (!Object.prototype.hasOwnProperty.call(vars, k)) vars[k] = String(v);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return vars;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Write an artifact to disk. If the source doc template exists, copy it and
|
|
246
|
+
// prepend a title line. Otherwise write a minimal stub.
|
|
247
|
+
function writeArtifact(outputPath, docTemplatePath, title) {
|
|
248
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
249
|
+
|
|
250
|
+
if (fs.existsSync(docTemplatePath)) {
|
|
251
|
+
const original = fs.readFileSync(docTemplatePath, "utf8");
|
|
252
|
+
const withTitle = title
|
|
253
|
+
? original.replace(/^# .*$/m, `# ${title}`)
|
|
254
|
+
: original;
|
|
255
|
+
fs.writeFileSync(outputPath, withTitle, "utf8");
|
|
256
|
+
} else {
|
|
257
|
+
const stub = `# ${title || path.basename(outputPath, ".md")}\n\n<!-- stub: template not found -->\n`;
|
|
258
|
+
fs.writeFileSync(outputPath, stub, "utf8");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Instantiate a workflow template.
|
|
264
|
+
*
|
|
265
|
+
* @param {string} templateId - Template id (e.g. "new-feature") or path to a .yml file.
|
|
266
|
+
* @param {Object} inputs - Key/value map of variable values for the template.
|
|
267
|
+
* @param {Object} options
|
|
268
|
+
* @param {string|null} options.cwd - Project root for output paths. Pass null for dry-run (no files written).
|
|
269
|
+
* @returns {{ artifacts: Array<{path: string, template: string, title: string}>, beadsItems: Array<{type: string, title: string, linkTo: string|undefined}> }}
|
|
270
|
+
*/
|
|
271
|
+
export function instantiateWorkflow(templateId, inputs, { cwd } = {}) {
|
|
272
|
+
const packageRoot = findPackageRoot(__dirname);
|
|
273
|
+
|
|
274
|
+
const templatePath = resolveTemplatePath(templateId, packageRoot);
|
|
275
|
+
if (!fs.existsSync(templatePath)) {
|
|
276
|
+
throw new Error(`Workflow template not found: ${templatePath}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const raw = fs.readFileSync(templatePath, "utf8");
|
|
280
|
+
const template = parseYaml(raw);
|
|
281
|
+
|
|
282
|
+
if (!template.id || !template.title || !Array.isArray(template.artifacts)) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`Workflow template at ${templatePath} is missing required fields (id, title, artifacts).`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const vars = resolveVars(template.inputs, inputs);
|
|
289
|
+
|
|
290
|
+
// Substitute variables in the full template object.
|
|
291
|
+
const resolved = substituteDeep(template, vars);
|
|
292
|
+
|
|
293
|
+
const docsTemplatesDir = path.join(packageRoot, "templates", "docs");
|
|
294
|
+
|
|
295
|
+
const artifacts = [];
|
|
296
|
+
for (const artifact of resolved.artifacts) {
|
|
297
|
+
const outputPath = cwd ? path.join(cwd, artifact.path) : artifact.path;
|
|
298
|
+
const docTemplatePath = path.join(docsTemplatesDir, artifact.template);
|
|
299
|
+
const title = artifact.title || "";
|
|
300
|
+
|
|
301
|
+
if (cwd) {
|
|
302
|
+
writeArtifact(outputPath, docTemplatePath, title);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
artifacts.push({
|
|
306
|
+
path: artifact.path,
|
|
307
|
+
template: artifact.template,
|
|
308
|
+
title,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const beadsItems = (resolved.beads_items || []).map((item) => ({
|
|
313
|
+
type: item.type,
|
|
314
|
+
title: item.title,
|
|
315
|
+
...(item.link_to != null ? { linkTo: item.link_to } : {}),
|
|
316
|
+
...(item.depends_on_prior ? { dependsOnPrior: true } : {}),
|
|
317
|
+
}));
|
|
318
|
+
|
|
319
|
+
return { artifacts, beadsItems };
|
|
320
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geraldmaron/construct",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Construct — agent orchestration layer for OpenCode, Claude Code, and other coding surfaces",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"bin/construct",
|
|
11
11
|
"bin/construct-postinstall.mjs",
|
|
12
12
|
"lib/**",
|
|
13
|
-
"
|
|
13
|
+
"specialists/**",
|
|
14
14
|
"examples/**",
|
|
15
15
|
"personas/**",
|
|
16
16
|
"commands/**",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"templates/**",
|
|
21
21
|
"templates/distribution/**",
|
|
22
22
|
"db/**",
|
|
23
|
-
"scripts/sync-
|
|
23
|
+
"scripts/sync-specialists.mjs",
|
|
24
24
|
"README.md",
|
|
25
25
|
".env.example"
|
|
26
26
|
],
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"lint:comments": "node ./bin/construct lint:comments",
|
|
53
53
|
"lint:contracts": "node ./bin/construct lint:contracts",
|
|
54
54
|
"lint:agents": "node ./bin/construct lint:agents",
|
|
55
|
+
"build:sea": "esbuild bin/construct --bundle --platform=node --target=node20 --packages=external --outfile=dist/construct-bundle.cjs 2>&1 || (echo 'esbuild not found — install with: npm install -g esbuild' && exit 1)",
|
|
55
56
|
"lint:prose": "node scripts/lint-prose.mjs",
|
|
56
57
|
"lint:profiles": "node scripts/lint-profiles.mjs",
|
|
57
58
|
"learning:status": "node scripts/learning-status.mjs",
|
|
@@ -68,8 +69,17 @@
|
|
|
68
69
|
"@huggingface/transformers": "^4.2.0",
|
|
69
70
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
70
71
|
"@xenova/transformers": "^2.0.1",
|
|
72
|
+
"node-webvtt": "^1.9.3",
|
|
71
73
|
"postgres": "^3.4.9"
|
|
72
74
|
},
|
|
75
|
+
"optionalDependencies": {
|
|
76
|
+
"@opentelemetry/api": "^1.9.0",
|
|
77
|
+
"@opentelemetry/sdk-trace-node": "^1.25.0",
|
|
78
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
|
79
|
+
"@opentelemetry/resources": "^1.25.0",
|
|
80
|
+
"@opentelemetry/semantic-conventions": "^1.25.0",
|
|
81
|
+
"@opentelemetry/core": "^1.25.0"
|
|
82
|
+
},
|
|
73
83
|
"overrides": {
|
|
74
84
|
"express-rate-limit": "8.5.1",
|
|
75
85
|
"protobufjs": "^8.4.2"
|
package/personas/construct.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
personas/construct.md. Construct persona prompt.
|
|
3
3
|
|
|
4
4
|
Defines the single user-facing AI interface and its session-start behavior,
|
|
5
|
-
routing rules, approval boundaries, and output contract. Loaded by sync-
|
|
5
|
+
routing rules, approval boundaries, and output contract. Loaded by sync-specialists
|
|
6
6
|
and emitted to every supported platform.
|
|
7
7
|
-->
|
|
8
8
|
You are Construct. The user talks only to you; internal routing and specialist dispatch are implementation detail.
|
|
@@ -42,7 +42,7 @@ Devil's advocate is mandatory for new architectural directions, AI/agent workflo
|
|
|
42
42
|
`routeRequest` returns three artifacts; honor all three:
|
|
43
43
|
|
|
44
44
|
1. **Gates**. `framingChallenge`, `externalResearch`, `docAuthoring`
|
|
45
|
-
2. **Contract chain**. typed handoffs from `
|
|
45
|
+
2. **Contract chain**. typed handoffs from `specialists/contracts.json`. Call `agent_contract` MCP tool at handoff.
|
|
46
46
|
3. **Specialist sequence**. dispatch plan with ordering/parallel markers.
|
|
47
47
|
|
|
48
48
|
Before DONE: postconditions met · sources cited · framing logged · ADRs have Rejected alternatives.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Construct: Claude Code Integration
|
|
2
2
|
|
|
3
|
-
This project uses Construct. Personas and specialists are defined in `
|
|
3
|
+
This project uses Construct. Personas and specialists are defined in `specialists/registry.json` and synced to Claude Code via `construct sync`.
|
|
4
4
|
|
|
5
5
|
## Workflow roles
|
|
6
6
|
|
|
@@ -61,7 +61,7 @@ Fabrication is the single largest threat to trust in an agent system. A persona
|
|
|
61
61
|
## Enforcement
|
|
62
62
|
|
|
63
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
|
-
- `
|
|
64
|
+
- `specialists/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
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
66
|
|
|
67
67
|
## Bypass
|