triad-plus 1.7.0 → 1.9.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/CHANGELOG.md +32 -0
- package/README.md +23 -12
- package/adapters/antigravity/.agents/agents/triad-developer/agent.md +7 -3
- package/adapters/antigravity/.agents/agents/triad-evaluator/agent.md +5 -0
- package/adapters/antigravity/.agents/agents/triad-orchestrator/agent.md +5 -0
- package/adapters/antigravity/.agents/agents/triad-reviewer/agent.md +7 -2
- package/adapters/antigravity/.agents/skills/triad/SKILL.md +22 -0
- package/adapters/claude-code/.claude/agents/triad-developer.md +9 -4
- package/adapters/claude-code/.claude/agents/triad-evaluator.md +5 -0
- package/adapters/claude-code/.claude/agents/triad-reviewer.md +10 -5
- package/adapters/claude-code/.claude/commands/triad.md +22 -0
- package/adapters/claude-code/README.md +11 -3
- package/adapters/codex/prompts/triad.md +28 -0
- package/adapters/copilot/.github/agents/triad-developer.agent.md +12 -6
- package/adapters/copilot/.github/agents/triad-evaluator.agent.md +7 -1
- package/adapters/copilot/.github/agents/triad-orchestrator.agent.md +15 -1
- package/adapters/copilot/.github/agents/triad-reviewer.agent.md +11 -5
- package/adapters/copilot/.github/skills/triad/SKILL.md +23 -0
- package/adapters/hermes/skills/triad/SKILL.md +23 -0
- package/adapters/opencode/.opencode/agents/triad-developer.md +9 -4
- package/adapters/opencode/.opencode/agents/triad-evaluator.md +6 -0
- package/adapters/opencode/.opencode/agents/triad-orchestrator.md +15 -0
- package/adapters/opencode/.opencode/agents/triad-reviewer.md +10 -3
- package/adapters/opencode/.opencode/commands/triad.md +21 -0
- package/adapters/opencode/README.md +19 -4
- package/adapters/registry.mjs +2 -0
- package/bin/triad-plus.js +8 -2
- package/docs/architecture.md +15 -0
- package/docs/assignment-packets.md +35 -0
- package/docs/bmad-integration.md +187 -82
- package/docs/configuration.md +28 -0
- package/docs/evaluator-plus.md +9 -0
- package/docs/npx-installation.md +4 -0
- package/docs/operating-guide.it.md +18 -0
- package/docs/operating-guide.md +17 -0
- package/docs/quality-contract.md +119 -0
- package/docs/verification.md +37 -0
- package/integrations/bmad/README.md +17 -9
- package/integrations/bmad/epics-parser.mjs +697 -0
- package/integrations/bmad/story-importer.mjs +143 -5
- package/package.json +2 -2
- package/runtime/lib/assignment-packet.mjs +516 -0
- package/runtime/lib/quality-baseline.mjs +205 -0
- package/runtime/triad-assignment-packet.mjs +62 -0
- package/runtime/triad-bmad-intake.mjs +150 -0
- package/runtime/triad-evaluator-validate.mjs +261 -0
- package/runtime/triad-verify.mjs +33 -7
- package/schemas/evaluator-plus-result.schema.json +16 -0
- package/schemas/quality-baseline.schema.json +42 -0
- package/schemas/verification-evidence.schema.json +9 -1
- package/skills/triad-loop-bootstrap/SKILL.md +47 -4
- package/skills/triad-loop-bootstrap/assets/loop-template/handoff-report.template.md +13 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/quality-baseline.json +15 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/run-state.yaml +7 -0
- package/skills/triad-loop-bootstrap/assets/loop-template/runtime/assignments/assignment.template.json +16 -1
- package/skills/triad-loop-bootstrap/assets/project.yaml +5 -0
- package/skills/triad-loop-developer/SKILL.md +19 -8
- package/skills/triad-loop-evaluator/SKILL.md +12 -2
- package/skills/triad-loop-orchestrator/SKILL.md +134 -11
- package/skills/triad-loop-reviewer/SKILL.md +15 -5
|
@@ -115,10 +115,18 @@ function parseLabelBlocks(body) {
|
|
|
115
115
|
if (content) blocks.push({ heading: current.heading, content });
|
|
116
116
|
};
|
|
117
117
|
for (const line of body.split(/\r?\n/)) {
|
|
118
|
-
|
|
118
|
+
// Native BMAD epics.md commonly puts the label and its first paragraph on
|
|
119
|
+
// one line (`**Intent / outcome:** ...`), while standalone Stories often
|
|
120
|
+
// put the label on its own line. Only treat a colon inside the bold label
|
|
121
|
+
// as an inline section marker so Given/When/Then lines remain content of
|
|
122
|
+
// the Acceptance Criteria block.
|
|
123
|
+
const inlineCandidate = line.match(/^\s*(?:[-*]\s*)?\*\*([^*]+:)\*\*\s*(.*)$/);
|
|
124
|
+
const criterionStep = inlineCandidate && /^(?:Given|When|Then|And|But)\s*:$/i.test(inlineCandidate[1].trim());
|
|
125
|
+
const inlineLabel = inlineCandidate && !criterionStep ? inlineCandidate : null;
|
|
126
|
+
const label = inlineLabel || line.match(/^\s*(?:[-*]\s*)?\*\*([^*]+)\*\*\s*:?\s*$/);
|
|
119
127
|
if (label) {
|
|
120
128
|
flush();
|
|
121
|
-
current = { heading: label[1], lines: [] };
|
|
129
|
+
current = { heading: label[1], lines: inlineLabel && label[2].trim() ? [label[2].trim()] : [] };
|
|
122
130
|
} else if (/^#{1,6}\s+/.test(line)) {
|
|
123
131
|
flush();
|
|
124
132
|
current = null;
|
|
@@ -141,7 +149,12 @@ function metadataLines(body, names) {
|
|
|
141
149
|
const values = [];
|
|
142
150
|
for (const line of body.split(/\r?\n/)) {
|
|
143
151
|
const match = line.match(/^\s*(?:[-*]\s*)?(?:\*\*)?([^:*]+?)(?:\*\*)?\s*:\s*(.+?)\s*$/);
|
|
144
|
-
if (match && wanted.has(normalizeHeading(match[1])))
|
|
152
|
+
if (match && wanted.has(normalizeHeading(match[1]))) {
|
|
153
|
+
// Support BMAD's inline bold labels (`**Status:** ready-for-dev`),
|
|
154
|
+
// whose closing emphasis appears before the colon and is otherwise
|
|
155
|
+
// captured as a leading `**` in the value.
|
|
156
|
+
values.push(unquote(match[2].replace(/^\*\*\s*/, '').replace(/\s*\*\*$/, '')));
|
|
157
|
+
}
|
|
145
158
|
}
|
|
146
159
|
return values;
|
|
147
160
|
}
|
|
@@ -165,6 +178,105 @@ function firstLabelOrSectionContent(sections, labelBlocks, labels) {
|
|
|
165
178
|
return labeled[0] ?? firstContent(sections, labelBlocks, labels);
|
|
166
179
|
}
|
|
167
180
|
|
|
181
|
+
function singleLabelOrSectionContent(sections, labelBlocks, labels, field) {
|
|
182
|
+
const values = contentsFor(sections, labels, labelBlocks);
|
|
183
|
+
if (values.length > 1) {
|
|
184
|
+
throw new BmadStoryImportError('bmad_story_ambiguous', `BMAD Story has conflicting ${field} values.`, {
|
|
185
|
+
field,
|
|
186
|
+
values
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return values[0] ?? '';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parse the body contract shared by a standalone BMAD Story and a Story
|
|
194
|
+
* section inside epics.md. Unlike parseBmadStory(), this helper deliberately
|
|
195
|
+
* does not require status or a target repository: epics.md is a planning
|
|
196
|
+
* artifact and those execution fields may be absent there.
|
|
197
|
+
*/
|
|
198
|
+
export function parseBmadStoryContent(body, {
|
|
199
|
+
sourcePath = null,
|
|
200
|
+
id = '',
|
|
201
|
+
title = '',
|
|
202
|
+
status = null,
|
|
203
|
+
targetRepository = null,
|
|
204
|
+
epicId = null,
|
|
205
|
+
epicTitle = null,
|
|
206
|
+
sourceHeading = null,
|
|
207
|
+
sourceHeadingMarkdown = null,
|
|
208
|
+
sourceRange = null
|
|
209
|
+
} = {}) {
|
|
210
|
+
const sections = parseSections(body);
|
|
211
|
+
const labelBlocks = parseLabelBlocks(body);
|
|
212
|
+
const bodyId = distinctStrings(metadataLines(body, ['Story ID', 'ID']), 'story id');
|
|
213
|
+
if (bodyId && id && bodyId !== id) {
|
|
214
|
+
throw new BmadStoryImportError('bmad_story_ambiguous', `BMAD Story id conflicts with its heading: ${id} vs ${bodyId}.`, { id, bodyId });
|
|
215
|
+
}
|
|
216
|
+
const resolvedId = id || bodyId;
|
|
217
|
+
if (!resolvedId) throw new BmadStoryImportError('bmad_story_not_found', `No BMAD Story id found in ${sourcePathLabel(sourcePath)}.`);
|
|
218
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolvedId)) {
|
|
219
|
+
throw new BmadStoryImportError('bmad_story_invalid', `BMAD Story id is not filename-safe: ${resolvedId}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const bodyTitle = distinctStrings(metadataLines(body, ['Title']), 'title');
|
|
223
|
+
if (bodyTitle && title && bodyTitle !== title) {
|
|
224
|
+
throw new BmadStoryImportError('bmad_story_ambiguous', `BMAD Story title conflicts with its heading: ${title} vs ${bodyTitle}.`, { title, bodyTitle });
|
|
225
|
+
}
|
|
226
|
+
const resolvedTitle = title || bodyTitle;
|
|
227
|
+
if (!resolvedTitle) throw new BmadStoryImportError('bmad_story_unmappable', 'BMAD Story title is required.');
|
|
228
|
+
|
|
229
|
+
const bodyStatus = distinctStrings([
|
|
230
|
+
...contentsFor(sections, ['Status'], labelBlocks),
|
|
231
|
+
...metadataLines(body, ['Status'])
|
|
232
|
+
], 'status');
|
|
233
|
+
const resolvedStatus = clean(status || bodyStatus).toLowerCase() || null;
|
|
234
|
+
const bodyRepository = distinctStrings([
|
|
235
|
+
...contentsFor(sections, ['Target repository', 'Target repo', 'Repository'], labelBlocks),
|
|
236
|
+
...metadataLines(body, ['Target repository', 'Target repo', 'Repository'])
|
|
237
|
+
], 'target repository');
|
|
238
|
+
if (bodyRepository && targetRepository && bodyRepository !== targetRepository) {
|
|
239
|
+
throw new BmadStoryImportError('bmad_story_ambiguous', 'BMAD Story target repository conflicts with its execution mapping.', {
|
|
240
|
+
declared: bodyRepository,
|
|
241
|
+
resolved: targetRepository
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const resolvedTargetRepository = clean(targetRepository || bodyRepository) || null;
|
|
245
|
+
const outcome = distinctStrings([
|
|
246
|
+
...contentsFor(sections, ['Intent', 'Intent / outcome', 'Outcome', 'User outcome', 'Objective', 'Story', 'Description'], labelBlocks),
|
|
247
|
+
...metadataLines(body, ['Intent', 'Outcome', 'Objective', 'Description'])
|
|
248
|
+
], 'intent/outcome');
|
|
249
|
+
if (!outcome) throw new BmadStoryImportError('bmad_story_unmappable', `BMAD Story ${resolvedId} intent/outcome is required.`);
|
|
250
|
+
|
|
251
|
+
const acceptanceCriteria = singleLabelOrSectionContent(sections, labelBlocks, ['Acceptance Criteria', 'Acceptance'], 'acceptance criteria') || '';
|
|
252
|
+
if (!acceptanceCriteria) throw new BmadStoryImportError('bmad_story_unmappable', `BMAD Story ${resolvedId} acceptance criteria are required.`);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
id: resolvedId,
|
|
256
|
+
title: resolvedTitle,
|
|
257
|
+
status: resolvedStatus,
|
|
258
|
+
targetRepository: resolvedTargetRepository,
|
|
259
|
+
epicId: epicId ? clean(epicId) : null,
|
|
260
|
+
epicTitle: epicTitle ? clean(epicTitle) : null,
|
|
261
|
+
sourceHeading: sourceHeading ? clean(sourceHeading) : null,
|
|
262
|
+
sourceHeadingMarkdown: sourceHeadingMarkdown ? clean(sourceHeadingMarkdown) : null,
|
|
263
|
+
sourceRange: sourceRange ?? null,
|
|
264
|
+
branchWorktree: distinctStrings([
|
|
265
|
+
...metadataLines(body, ['Branch', 'Worktree', 'Branch/worktree'])
|
|
266
|
+
], 'branch/worktree'),
|
|
267
|
+
outcome,
|
|
268
|
+
acceptanceCriteria,
|
|
269
|
+
tasksAcceptance: firstLabelOrSectionContent(sections, labelBlocks, ['Execution', 'Tasks / Subtasks', 'Tasks', 'Tasks & Acceptance', 'Implementation Tasks']),
|
|
270
|
+
codeMap: firstContent(sections, labelBlocks, ['Code Map', 'Technical Context', 'Technical context / code map', 'Implementation Context']),
|
|
271
|
+
designNotes: firstContent(sections, labelBlocks, ['Design Notes', 'Boundaries & Constraints', 'Constraints', 'Technical Constraints']),
|
|
272
|
+
verification: firstContent(sections, labelBlocks, ['Verification', 'Verification Expectations', 'Metrics and Gates', 'Verification expectations']),
|
|
273
|
+
references: firstContent(sections, labelBlocks, ['Source References', 'References', 'References / Provenance']),
|
|
274
|
+
inScope: firstContent(sections, labelBlocks, ['In Scope', 'Scope']),
|
|
275
|
+
outOfScope: firstContent(sections, labelBlocks, ['Out of Scope', 'Non-goals', 'Non Goals']),
|
|
276
|
+
sourcePath: sourcePath ? resolve(sourcePath) : null
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
168
280
|
function distinctStrings(values, field) {
|
|
169
281
|
const normalized = values.map((value) => clean(value)).filter(Boolean);
|
|
170
282
|
const distinct = [...new Set(normalized)];
|
|
@@ -299,7 +411,7 @@ function sectionOrFallback(value, fallback) {
|
|
|
299
411
|
return value?.trim() || fallback;
|
|
300
412
|
}
|
|
301
413
|
|
|
302
|
-
export function buildTriadCard(story, { requiredGates = [], dependsOn = [] } = {}) {
|
|
414
|
+
export function buildTriadCard(story, { requiredGates = [], dependsOn = [], sourceKind = SOURCE_KIND } = {}) {
|
|
303
415
|
if (!story || typeof story !== 'object') {
|
|
304
416
|
throw new BmadStoryImportError('bmad_story_unmappable', 'A parsed BMAD Story object is required.');
|
|
305
417
|
}
|
|
@@ -321,6 +433,12 @@ export function buildTriadCard(story, { requiredGates = [], dependsOn = [] } = {
|
|
|
321
433
|
const designNotes = sectionOrFallback(story.designNotes, 'No Design Notes or additional constraints were supplied by the BMAD Story.');
|
|
322
434
|
const verification = sectionOrFallback(story.verification, 'Use the repository-owned deterministic gates declared by the caller and the normal Triad verifier.');
|
|
323
435
|
const references = sectionOrFallback(story.references, 'No source references were declared by the BMAD Story.');
|
|
436
|
+
const epicContext = sourceKind === 'bmad-epics' && story.epicId
|
|
437
|
+
? [
|
|
438
|
+
`- BMAD Epic: \`${story.epicId}${story.epicTitle ? ` — ${story.epicTitle}` : ''}\``,
|
|
439
|
+
story.sourceHeading ? `- BMAD source heading: \`${story.sourceHeading}\`` : null
|
|
440
|
+
].filter(Boolean)
|
|
441
|
+
: [];
|
|
324
442
|
|
|
325
443
|
return [
|
|
326
444
|
`# ${story.id} — ${story.title}`,
|
|
@@ -368,12 +486,32 @@ export function buildTriadCard(story, { requiredGates = [], dependsOn = [] } = {
|
|
|
368
486
|
'',
|
|
369
487
|
'## Integration boundary',
|
|
370
488
|
'',
|
|
371
|
-
|
|
489
|
+
...epicContext,
|
|
490
|
+
`- This card was generated from one ${sourceKind === 'bmad-epics' ? 'BMAD epics.md Story' : 'BMAD Story'}; BMAD remains planning authority and Triad remains execution authority.`,
|
|
372
491
|
'- The BMAD source is read-only. Provenance is recorded in the integration-side companion record.',
|
|
373
492
|
''
|
|
374
493
|
].join('\n');
|
|
375
494
|
}
|
|
376
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Build the existing Card contract after a canonical epics.md Story has passed
|
|
498
|
+
* Triad execution-readiness checks. The source may omit BMAD's optional
|
|
499
|
+
* ready-for-dev field; execution readiness is supplied by the downstream
|
|
500
|
+
* project/repository resolver rather than written back into BMAD.
|
|
501
|
+
*/
|
|
502
|
+
export function buildTriadCardFromCanonicalStory(story, {
|
|
503
|
+
requiredGates = [],
|
|
504
|
+
dependsOn = [],
|
|
505
|
+
executionReady = false,
|
|
506
|
+
sourceKind = 'bmad-epics'
|
|
507
|
+
} = {}) {
|
|
508
|
+
if (!executionReady) {
|
|
509
|
+
throw new BmadStoryImportError('bmad_story_not_ready', `BMAD Story ${story?.id ?? '<unknown>'} is ingestible but not execution-ready.`);
|
|
510
|
+
}
|
|
511
|
+
const normalized = { ...story, status: READY_STATUS };
|
|
512
|
+
return buildTriadCard(normalized, { requiredGates, dependsOn, sourceKind });
|
|
513
|
+
}
|
|
514
|
+
|
|
377
515
|
export function validateTriadCard(card) {
|
|
378
516
|
const required = [
|
|
379
517
|
/^#\s+[^\n]+/m,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "triad-plus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A lightweight, evidence-backed engineering loop for coding agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"node": ">=20"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "node tests/runtime-forward-test.mjs && node tests/retry-scope-contracts-test.mjs && node tests/cli-install-test.mjs && node tests/codex-liveness-test.mjs && node tests/bmad-story-importer-test.mjs",
|
|
31
|
+
"test": "node tests/runtime-forward-test.mjs && node tests/retry-scope-contracts-test.mjs && node tests/cli-install-test.mjs && node tests/codex-liveness-test.mjs && node tests/bmad-story-importer-test.mjs && node tests/bmad-epics-intake-test.mjs && node tests/immutable-quality-contract-test.mjs && node tests/assignment-packet-test.mjs",
|
|
32
32
|
"pack:check": "npm pack --dry-run"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|