@geraldmaron/construct 1.5.1 → 1.5.2
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 +3 -0
- package/bin/construct +336 -17
- package/lib/artifact-loop-core.mjs +26 -5
- package/lib/artifact-manifest-overlay.mjs +273 -0
- package/lib/artifact-manifest.mjs +13 -10
- package/lib/cli-commands.mjs +44 -4
- package/lib/config/source-target-registry.mjs +1 -0
- package/lib/config/source-targets.mjs +30 -0
- package/lib/doc-stamp.mjs +12 -0
- package/lib/doctor/index.mjs +2 -1
- package/lib/doctor/source-target-health.mjs +101 -0
- package/lib/doctor/watchers/source-targets.mjs +44 -0
- package/lib/document-ingest.mjs +25 -3
- package/lib/embed/demand-fetch.mjs +30 -36
- package/lib/embed/providers/directory.mjs +117 -0
- package/lib/embed/providers/registry.mjs +7 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +1 -1
- package/lib/extensions/manifests/directory.manifest.json +24 -1
- package/lib/extensions/manifests/github.manifest.json +10 -0
- package/lib/extensions/manifests/linear.manifest.json +1 -1
- package/lib/hooks/session-start.mjs +18 -11
- package/lib/knowledge/rag.mjs +52 -11
- package/lib/knowledge/search.mjs +69 -6
- package/lib/knowledge/synthesis.mjs +157 -0
- package/lib/mcp/server.mjs +2 -2
- package/lib/mcp/tool-definitions-workflow.mjs +35 -7
- package/lib/mcp/tools/artifact-author.mjs +126 -13
- package/lib/mcp/tools/orchestration-run.mjs +3 -0
- package/lib/mcp/tools/skills.mjs +17 -0
- package/lib/model-cheapest-provider.mjs +2 -1
- package/lib/model-policy.mjs +329 -0
- package/lib/model-router.mjs +10 -9
- package/lib/model-tiers.mjs +27 -0
- package/lib/models/catalog.mjs +5 -4
- package/lib/orchestration/classification.mjs +11 -0
- package/lib/orchestration/context-bindings.mjs +85 -0
- package/lib/orchestration/readiness.mjs +2 -1
- package/lib/orchestration/research-evidence-gate.mjs +104 -0
- package/lib/orchestration/runtime.mjs +35 -1
- package/lib/orchestration/worker.mjs +9 -0
- package/lib/setup.mjs +2 -1
- package/lib/sources/content-roots.mjs +147 -0
- package/lib/sources/repo-cache.mjs +142 -0
- package/lib/template-registry.mjs +2 -0
- package/lib/tracker/contribute.mjs +266 -0
- package/lib/validator.mjs +2 -3
- package/package.json +1 -1
- package/registry/agent-manifest.json +0 -4
- package/registry/capabilities.json +20 -1
- package/templates/docs/README.md +22 -0
- package/templates/docs/adhoc.md +12 -0
- package/templates/docs/strategy-comparison.md +19 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/tracker/contribute.mjs — analyze → propose → dedupe → apply pipeline for
|
|
3
|
+
* contributing governed issue proposals back to an external tracker (bead
|
|
4
|
+
* construct-760c.6). Jira first, but provider-generic: the write goes through the
|
|
5
|
+
* existing governed `provider_write` path, so a tracker's eligibility is its
|
|
6
|
+
* manifest `write` capability, not a hardcoded name here.
|
|
7
|
+
*
|
|
8
|
+
* Five stages:
|
|
9
|
+
* 1. FETCH existing issues from the tracker (injectable; default reads via
|
|
10
|
+
* the jira embed provider's JQL).
|
|
11
|
+
* 2. ANALYZE the registered project corpora (B2 content roots) for work the
|
|
12
|
+
* tracker does not yet cover — deterministic, retrieval-only, so the
|
|
13
|
+
* proposal set is stable and CI-testable without a model.
|
|
14
|
+
* 3. PROPOSE a proposal artifact (JSON + markdown): each proposed issue carries
|
|
15
|
+
* a summary, an evidence-cited description (origin.targetId +
|
|
16
|
+
* relPath — no fabrication), a suggested type, and a stable
|
|
17
|
+
* idempotency key. The artifact is the human-review anchor.
|
|
18
|
+
* 4. DEDUPE proposed vs existing issue summaries by token similarity;
|
|
19
|
+
* near-duplicates are suppressed and reported with the matched key.
|
|
20
|
+
* 5. APPLY batch through provider_write — dry_run by default (writes
|
|
21
|
+
* nothing), an approval token required to execute. Idempotent:
|
|
22
|
+
* an issue already recorded created for this proposal is skipped, so
|
|
23
|
+
* a second apply is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Beads are never touched (R5): bd stays the internal tracker; this pipeline only
|
|
26
|
+
* proposes to the external one.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import crypto from 'node:crypto';
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
34
|
+
import { resolveEffectiveSourceTargetsFromConfig } from '../config/source-targets.mjs';
|
|
35
|
+
import { resolveContentRoots, expandProjectsFilter } from '../sources/content-roots.mjs';
|
|
36
|
+
import { buildCorpus } from '../knowledge/rag.mjs';
|
|
37
|
+
|
|
38
|
+
const DEFAULT_PROPOSAL_LIMIT = 10;
|
|
39
|
+
const DEDUPE_THRESHOLD = 0.6;
|
|
40
|
+
|
|
41
|
+
const STOP = new Set(['the', 'a', 'an', 'and', 'or', 'to', 'for', 'of', 'in', 'on', 'with', 'track', 'add', 'document', 'implement']);
|
|
42
|
+
|
|
43
|
+
function tokenize(text) {
|
|
44
|
+
return new Set(String(text).toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter((t) => t.length > 2 && !STOP.has(t)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function jaccard(a, b) {
|
|
48
|
+
if (!a.size || !b.size) return 0;
|
|
49
|
+
let inter = 0;
|
|
50
|
+
for (const t of a) if (b.has(t)) inter++;
|
|
51
|
+
return inter / (a.size + b.size - inter);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function proposalsDir(cwd) {
|
|
55
|
+
return path.join(cwd, '.cx', 'tracker', 'proposals');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stableId(projectKey, items) {
|
|
59
|
+
const h = crypto.createHash('sha256');
|
|
60
|
+
h.update(projectKey);
|
|
61
|
+
for (const it of items) h.update(`|${it.key}`);
|
|
62
|
+
return `prop-${h.digest('hex').slice(0, 12)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A tracker source target names its project via the manifest selector field
|
|
66
|
+
// (jira: `project`). Resolve the requested target id to that key, or throw a
|
|
67
|
+
// hard error naming the known tracker targets.
|
|
68
|
+
function resolveTrackerTarget(targets, targetId) {
|
|
69
|
+
const target = targets.find((t) => t.id === targetId);
|
|
70
|
+
if (!target) {
|
|
71
|
+
const known = targets.map((t) => t.id).join(', ') || '(none registered)';
|
|
72
|
+
throw new Error(`unknown tracker target "${targetId}" — known targets: ${known}`);
|
|
73
|
+
}
|
|
74
|
+
const projectKey = target.selector?.project || target.selector?.[Object.keys(target.selector || {})[0]];
|
|
75
|
+
if (!projectKey) throw new Error(`tracker target "${targetId}" has no resolvable project key`);
|
|
76
|
+
return { target, projectKey };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Deterministic analyze: each source doc in a bound project's corpus is a
|
|
80
|
+
// candidate work item to track, summarized from its title and cited to its
|
|
81
|
+
// origin. Ordering is stable (project id, then relPath) so proposals reproduce.
|
|
82
|
+
function buildCandidates(roots, cwd, limit) {
|
|
83
|
+
const candidates = [];
|
|
84
|
+
for (const root of roots) {
|
|
85
|
+
const chunks = buildCorpus(cwd, { roots: [root] })
|
|
86
|
+
.filter((c) => c.origin?.targetId === root.origin.targetId && c.origin?.relPath)
|
|
87
|
+
.sort((a, b) => a.origin.relPath.localeCompare(b.origin.relPath));
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (const c of chunks) {
|
|
90
|
+
if (seen.has(c.origin.relPath)) continue;
|
|
91
|
+
seen.add(c.origin.relPath);
|
|
92
|
+
const summary = `Track: ${c.title} (${c.origin.projectKey})`;
|
|
93
|
+
candidates.push({
|
|
94
|
+
key: `${c.origin.targetId}:${c.origin.relPath}`,
|
|
95
|
+
summary,
|
|
96
|
+
issueType: 'Task',
|
|
97
|
+
evidence: [{ targetId: c.origin.targetId, projectKey: c.origin.projectKey, relPath: c.origin.relPath }],
|
|
98
|
+
title: c.title,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
candidates.sort((a, b) => a.key.localeCompare(b.key));
|
|
103
|
+
return candidates.slice(0, limit);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function renderDescription(candidate) {
|
|
107
|
+
const cite = candidate.evidence.map((e) => `\`${e.projectKey}:${e.relPath}\``).join(', ');
|
|
108
|
+
return [
|
|
109
|
+
`Proposed from analysis of registered project content.`,
|
|
110
|
+
``,
|
|
111
|
+
`Source: ${cite}`,
|
|
112
|
+
``,
|
|
113
|
+
`Scope [unverified]: derive concrete acceptance criteria from the cited source before starting — details beyond the source title are not yet verified.`,
|
|
114
|
+
].join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function dedupe(candidates, existingIssues) {
|
|
118
|
+
const existingTok = existingIssues.map((i) => ({ key: i.key, tokens: tokenize(i.summary || i.title || '') }));
|
|
119
|
+
const proposals = [];
|
|
120
|
+
const suppressed = [];
|
|
121
|
+
for (const c of candidates) {
|
|
122
|
+
const ctok = tokenize(c.summary);
|
|
123
|
+
let best = null;
|
|
124
|
+
for (const e of existingTok) {
|
|
125
|
+
const score = jaccard(ctok, e.tokens);
|
|
126
|
+
if (score >= DEDUPE_THRESHOLD && (!best || score > best.score)) best = { key: e.key, score };
|
|
127
|
+
}
|
|
128
|
+
if (best) {
|
|
129
|
+
suppressed.push({ key: c.key, summary: c.summary, matchedIssueKey: best.key, similarity: Math.round(best.score * 100) / 100 });
|
|
130
|
+
} else {
|
|
131
|
+
proposals.push(c);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { proposals, suppressed };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Default issue fetch: read the tracker target's existing issues through the
|
|
139
|
+
* jira embed provider. Injectable via deps.fetchIssues for tests.
|
|
140
|
+
*/
|
|
141
|
+
async function defaultFetchIssues({ projectKey, env }) {
|
|
142
|
+
const { JiraProvider } = await import('../embed/providers/jira.mjs');
|
|
143
|
+
const provider = new JiraProvider({ env });
|
|
144
|
+
const items = await provider.read('issues', { project: projectKey });
|
|
145
|
+
return (items || []).filter((i) => i && i.key).map((i) => ({ key: i.key, summary: i.title || i.summary || '' }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderProposalMarkdown(proposal) {
|
|
149
|
+
const lines = [
|
|
150
|
+
`# Tracker contribution proposal — ${proposal.projectKey}`,
|
|
151
|
+
``,
|
|
152
|
+
`Proposal id: \`${proposal.id}\` · target: \`${proposal.targetId}\` · generated ${proposal.createdAt}`,
|
|
153
|
+
``,
|
|
154
|
+
`## Proposed issues (${proposal.proposals.length})`,
|
|
155
|
+
``,
|
|
156
|
+
];
|
|
157
|
+
for (const p of proposal.proposals) {
|
|
158
|
+
lines.push(`### ${p.summary}`, ``, `- type: ${p.issueType}`, `- idempotency key: \`${p.key}\``, ``, renderDescription(p), ``);
|
|
159
|
+
}
|
|
160
|
+
lines.push(`## Dedupe report (${proposal.suppressed.length} suppressed)`, ``);
|
|
161
|
+
if (proposal.suppressed.length) {
|
|
162
|
+
lines.push(`| Proposed | Matched issue | Similarity |`, `|---|---|---|`);
|
|
163
|
+
for (const s of proposal.suppressed) lines.push(`| ${s.summary} | ${s.matchedIssueKey} | ${s.similarity} |`);
|
|
164
|
+
} else {
|
|
165
|
+
lines.push(`_No near-duplicates suppressed._`);
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n') + '\n';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function saveProposal(cwd, proposal) {
|
|
171
|
+
const dir = proposalsDir(cwd);
|
|
172
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
173
|
+
fs.writeFileSync(path.join(dir, `${proposal.id}.json`), `${JSON.stringify(proposal, null, 2)}\n`);
|
|
174
|
+
fs.writeFileSync(path.join(dir, `${proposal.id}.md`), renderProposalMarkdown(proposal));
|
|
175
|
+
return { json: path.join(dir, `${proposal.id}.json`), md: path.join(dir, `${proposal.id}.md`) };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function loadProposal(cwd, proposalId) {
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(fs.readFileSync(path.join(proposalsDir(cwd), `${proposalId}.json`), 'utf8'));
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Stages 1–4: fetch existing issues, analyze bound project corpora, propose
|
|
188
|
+
* evidence-cited issues, dedupe against existing, and persist the proposal
|
|
189
|
+
* artifact. No writes to the tracker.
|
|
190
|
+
*/
|
|
191
|
+
export async function analyzeAndPropose({ target, against, cwd = process.cwd(), env = process.env, limit = DEFAULT_PROPOSAL_LIMIT, deps = {} } = {}) {
|
|
192
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
193
|
+
const targets = resolveEffectiveSourceTargetsFromConfig(config, env);
|
|
194
|
+
const { projectKey } = resolveTrackerTarget(targets, target);
|
|
195
|
+
|
|
196
|
+
let selectedIds;
|
|
197
|
+
try {
|
|
198
|
+
selectedIds = expandProjectsFilter(against ?? 'all', targets).ids;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return { ok: false, message: err.message };
|
|
201
|
+
}
|
|
202
|
+
const roots = resolveContentRoots(targets, { projectRoot: cwd })
|
|
203
|
+
.filter((r) => selectedIds.has(r.origin.targetId));
|
|
204
|
+
if (!roots.length) {
|
|
205
|
+
return { ok: false, message: `no content-capable projects resolved for "${against ?? 'all'}" to analyze against` };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const fetchIssues = deps.fetchIssues ?? defaultFetchIssues;
|
|
209
|
+
const existing = await fetchIssues({ projectKey, env });
|
|
210
|
+
|
|
211
|
+
const candidates = buildCandidates(roots, cwd, limit);
|
|
212
|
+
const { proposals, suppressed } = dedupe(candidates, existing);
|
|
213
|
+
|
|
214
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
215
|
+
const proposal = {
|
|
216
|
+
id: stableId(projectKey, candidates),
|
|
217
|
+
targetId: target,
|
|
218
|
+
projectKey,
|
|
219
|
+
createdAt: now(),
|
|
220
|
+
existingIssueCount: existing.length,
|
|
221
|
+
proposals: proposals.map((p) => ({ key: p.key, summary: p.summary, issueType: p.issueType, evidence: p.evidence, description: renderDescription(p) })),
|
|
222
|
+
suppressed,
|
|
223
|
+
};
|
|
224
|
+
const paths = saveProposal(cwd, proposal);
|
|
225
|
+
return { ok: true, proposal, paths };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Stage 5: apply a proposal through the governed write path. Without
|
|
230
|
+
* approveToken every write is a dry-run (renders payloads, writes nothing).
|
|
231
|
+
* Idempotent: an issue already recorded created for this proposal is skipped, so
|
|
232
|
+
* re-apply never creates a duplicate (R3/AC5).
|
|
233
|
+
*/
|
|
234
|
+
export async function applyProposal({ proposalId, proposal: inline, approveToken = null, cwd = process.cwd(), env = process.env, deps = {} } = {}) {
|
|
235
|
+
const proposal = inline || loadProposal(cwd, proposalId);
|
|
236
|
+
if (!proposal) return { ok: false, message: `proposal not found: ${proposalId}` };
|
|
237
|
+
|
|
238
|
+
const write = deps.providerWrite ?? (async (args) => (await import('../mcp/tools/provider-write.mjs')).providerWrite(args, { rootDir: cwd, env }));
|
|
239
|
+
const dryRun = !approveToken;
|
|
240
|
+
|
|
241
|
+
const auditPath = path.join(proposalsDir(cwd), `${proposal.id}.audit.json`);
|
|
242
|
+
let audit = [];
|
|
243
|
+
try { audit = JSON.parse(fs.readFileSync(auditPath, 'utf8')); } catch { audit = []; }
|
|
244
|
+
const alreadyCreated = new Set(audit.map((a) => a.key));
|
|
245
|
+
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const p of proposal.proposals) {
|
|
248
|
+
if (!dryRun && alreadyCreated.has(p.key)) {
|
|
249
|
+
results.push({ key: p.key, status: 'skipped-idempotent' });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const item = { type: 'issue', project: proposal.projectKey, issueType: p.issueType, summary: p.summary, description: p.description };
|
|
253
|
+
const res = await write({ provider: 'jira', item, dry_run: dryRun, idempotency_key: `${proposal.id}:${p.key}`, approval_token: approveToken });
|
|
254
|
+
results.push({ key: p.key, status: res.status, dryRun });
|
|
255
|
+
if (!dryRun && res.status && !String(res.status).startsWith('denied')) {
|
|
256
|
+
const issueKey = res.envelope?.result?.key || res.envelope?.issueKey || null;
|
|
257
|
+
audit.push({ key: p.key, proposalId: proposal.id, issueKey, at: (deps.now ?? (() => new Date().toISOString()))() });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!dryRun) {
|
|
262
|
+
fs.mkdirSync(proposalsDir(cwd), { recursive: true });
|
|
263
|
+
fs.writeFileSync(auditPath, `${JSON.stringify(audit, null, 2)}\n`);
|
|
264
|
+
}
|
|
265
|
+
return { ok: true, proposalId: proposal.id, dryRun, results, audit: dryRun ? undefined : audit };
|
|
266
|
+
}
|
package/lib/validator.mjs
CHANGED
|
@@ -10,8 +10,7 @@ import { loadRegistry } from './registry/loader.mjs';
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { packageRoot, isMainModule } from './roots.mjs';
|
|
13
|
-
|
|
14
|
-
const VALID_TIERS = new Set(['reasoning', 'standard', 'fast']);
|
|
13
|
+
import { MODEL_TIERS, MODEL_TIER_SET as VALID_TIERS } from './model-tiers.mjs';
|
|
15
14
|
const VALID_REASONING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh']);
|
|
16
15
|
|
|
17
16
|
const PROMPT_HARD_CAP_WORDS = 4000;
|
|
@@ -221,7 +220,7 @@ export function validateRegistry(reg, options = {}) {
|
|
|
221
220
|
if (!reg.models || typeof reg.models !== 'object') {
|
|
222
221
|
errors.push('registry: models is missing');
|
|
223
222
|
} else {
|
|
224
|
-
for (const tier of
|
|
223
|
+
for (const tier of MODEL_TIERS) {
|
|
225
224
|
const t = reg.models[tier];
|
|
226
225
|
if (!t || typeof t !== 'object') {
|
|
227
226
|
errors.push(`models.${tier}: tier object is missing`);
|
package/package.json
CHANGED
|
@@ -70,10 +70,6 @@
|
|
|
70
70
|
"name": "artifact_workflow",
|
|
71
71
|
"use": "Plan a manifest-backed artifact workflow; validate or export locally."
|
|
72
72
|
},
|
|
73
|
-
{
|
|
74
|
-
"name": "workflow_invoke",
|
|
75
|
-
"use": "Invoke an embedded Construct workflow (prd-draft, architecture-review, …)."
|
|
76
|
-
},
|
|
77
73
|
{
|
|
78
74
|
"name": "triage_recommend",
|
|
79
75
|
"use": "Recommend intake triage — type, owner, and routing — for a new signal."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"catalog": {
|
|
4
|
-
"generatedAt": "2026-07-
|
|
4
|
+
"generatedAt": "2026-07-09T04:29:22.395Z",
|
|
5
5
|
"npmScripts": [
|
|
6
6
|
"adapters",
|
|
7
7
|
"audit:published",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"catalog:regen",
|
|
12
12
|
"catalog:validate",
|
|
13
13
|
"certify:demos",
|
|
14
|
+
"ci:local",
|
|
14
15
|
"clean:artifacts",
|
|
15
16
|
"coverage",
|
|
16
17
|
"deps:check",
|
|
@@ -746,6 +747,12 @@
|
|
|
746
747
|
"core": true,
|
|
747
748
|
"internal": false
|
|
748
749
|
},
|
|
750
|
+
{
|
|
751
|
+
"name": "synthesize",
|
|
752
|
+
"category": "Work",
|
|
753
|
+
"core": false,
|
|
754
|
+
"internal": false
|
|
755
|
+
},
|
|
749
756
|
{
|
|
750
757
|
"name": "tags",
|
|
751
758
|
"category": "Work",
|
|
@@ -788,12 +795,24 @@
|
|
|
788
795
|
"core": false,
|
|
789
796
|
"internal": false
|
|
790
797
|
},
|
|
798
|
+
{
|
|
799
|
+
"name": "templates",
|
|
800
|
+
"category": "Advanced",
|
|
801
|
+
"core": false,
|
|
802
|
+
"internal": false
|
|
803
|
+
},
|
|
791
804
|
{
|
|
792
805
|
"name": "tools",
|
|
793
806
|
"category": "Work",
|
|
794
807
|
"core": false,
|
|
795
808
|
"internal": false
|
|
796
809
|
},
|
|
810
|
+
{
|
|
811
|
+
"name": "tracker",
|
|
812
|
+
"category": "Models & Integrations",
|
|
813
|
+
"core": false,
|
|
814
|
+
"internal": false
|
|
815
|
+
},
|
|
797
816
|
{
|
|
798
817
|
"name": "uninstall",
|
|
799
818
|
"category": "Advanced",
|
package/templates/docs/README.md
CHANGED
|
@@ -62,6 +62,28 @@ cp templates/docs/prd.md .cx/templates/docs/prd.md
|
|
|
62
62
|
|
|
63
63
|
Ask Construct for a PRD; it'll follow the new shape.
|
|
64
64
|
|
|
65
|
+
### Registering a new document class
|
|
66
|
+
|
|
67
|
+
Overriding a template reshapes an *existing* class. To generate a class the builtin manifest never registered, register it — this writes the template plus a project-tier manifest overlay, and never touches the builtin `specialists/artifact-manifest.json`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
construct templates register convergence-brief \
|
|
71
|
+
--description "Cross-project strategy convergence brief" \
|
|
72
|
+
[--from .cx/templates/docs/convergence-brief.md]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
That writes `.cx/templates/docs/convergence-brief.md` (a starter, or your `--from` file) and adds `convergence-brief` to `.cx/artifact-manifest.overlay.json`. The overlay merges over the builtin by three tiers (builtin → user `~/.config/construct/` → project `.cx/`), so the registered class now resolves everywhere: `get_template("convergence-brief")` returns the project override, and `author_artifact {type:"convergence-brief", ...}` drafts from it and runs the release gate. A registered class inherits memo-like author/reviewer defaults; override them (including `outputDir`) by editing its overlay entry.
|
|
76
|
+
|
|
77
|
+
### One-off adhoc documents
|
|
78
|
+
|
|
79
|
+
For a genuinely one-off document with no fixed shape, skip registration entirely and use the sanctioned `adhoc` type:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
author_artifact {type:"adhoc", title:"Q3 strategy convergence", instructions:"..."}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`adhoc` needs an explicit `title` and `instructions`; its structure follows the instructions, but it still runs the full release gate — free-form structure, not free-form quality. It is not a bypass for a registered class: naming a known type through `adhoc` is redirected to that class. An unknown, unregistered non-adhoc class still returns a classification/registration prompt rather than becoming a PRD.
|
|
86
|
+
|
|
65
87
|
## Role Anti-Patterns
|
|
66
88
|
|
|
67
89
|
Each specialist is cognitively rooted in a **role** (product-manager, engineer, architect, etc.) with a core set of failure modes to avoid. Flavored specialists extend the core with an overlay.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# {title}
|
|
2
|
+
|
|
3
|
+
<!-- Adhoc artifact: structure follows your instructions, not a fixed shape. Keep only the sections you need; add whatever the subject demands. Quality gates still apply — cite sources or mark [unverified]. -->
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
<!-- One paragraph. What this document decides, communicates, or captures, and for whom. -->
|
|
7
|
+
|
|
8
|
+
## Detail
|
|
9
|
+
<!-- The substance, in whatever structure the instructions imply: prose, sections, tables, or a mix. -->
|
|
10
|
+
|
|
11
|
+
## Next steps
|
|
12
|
+
<!-- Actions, decisions, or open questions this leaves. Omit if not applicable. -->
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# {title}
|
|
2
|
+
|
|
3
|
+
_Generated {YYYY-MM-DD}. Every cross-project claim must cite its source as `project:path`._
|
|
4
|
+
|
|
5
|
+
## Per-project summary
|
|
6
|
+
|
|
7
|
+
One attributed subsection per project — what each project's own documents say, cited to origin.
|
|
8
|
+
|
|
9
|
+
## Convergence
|
|
10
|
+
|
|
11
|
+
Where the projects agree — shared strategy, common patterns, aligned bets. Each claim cites the projects it draws from.
|
|
12
|
+
|
|
13
|
+
## Divergence
|
|
14
|
+
|
|
15
|
+
Where the projects differ — conflicting choices, gaps, incompatible assumptions. Cite both sides.
|
|
16
|
+
|
|
17
|
+
## Recommendation
|
|
18
|
+
|
|
19
|
+
The synthesis: what the convergence and divergence imply. Mark any forward-looking claim `[unverified]` when it is not grounded in a cited source.
|