create-harness-vibe-coding 0.8.8 → 0.8.10
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-CN.md +8 -2
- package/README.md +22 -3
- package/docs/images/harness-icon.png +0 -0
- package/package.json +3 -1
- package/src/generator.js +36 -0
- package/src/index.js +72 -4
- package/templates/common/.claude/agents/architect.md +31 -31
- package/templates/common/.claude/agents/codebase-explorer.md +45 -0
- package/templates/common/.claude/agents/docs-researcher.md +41 -41
- package/templates/common/.claude/agents/memory-master.md +26 -8
- package/templates/common/.claude/agents/reviewer.md +34 -34
- package/templates/common/.claude/agents/task-scribe.md +70 -0
- package/templates/common/.claude/commands/wf-help.md +9 -5
- package/templates/common/.claude/commands/wf-update.md +24 -0
- package/templates/common/.claude/rules/ecc/common.md +15 -2
- package/templates/common/.claude/settings.json +13 -0
- package/templates/common/.claude/skills/subagent-orchestrator/SKILL.md +10 -6
- package/templates/common/.claude/skills/wf/SKILL.md +15 -8
- package/templates/common/.claude/skills/wf-auto/SKILL.md +3 -2
- package/templates/common/.claude/skills/wf-learn/SKILL.md +9 -2
- package/templates/common/.claude/skills/wf-max/SKILL.md +26 -8
- package/templates/common/.claude/skills/wf-review/SKILL.md +63 -12
- package/templates/common/.claude/skills/wf-update/SKILL.md +15 -2
- package/templates/common/.codex/hooks.json +17 -0
- package/templates/common/.harness-version +93 -48
- package/templates/common/.opencode/agents/architect.md +24 -24
- package/templates/common/.opencode/agents/codebase-explorer.md +45 -0
- package/templates/common/.opencode/agents/docs-researcher.md +34 -34
- package/templates/common/.opencode/agents/memory-master.md +26 -8
- package/templates/common/.opencode/agents/reviewer.md +27 -27
- package/templates/common/.opencode/agents/task-scribe.md +70 -0
- package/templates/common/.opencode/commands/wf-auto-spark.md +15 -0
- package/templates/common/.opencode/commands/wf-auto.md +15 -0
- package/templates/common/.opencode/commands/wf-help.md +9 -5
- package/templates/common/.opencode/commands/wf-learn.md +15 -0
- package/templates/common/.opencode/commands/wf-max.md +15 -0
- package/templates/common/.opencode/commands/wf-readme.md +15 -0
- package/templates/common/.opencode/commands/wf-remove.md +15 -0
- package/templates/common/.opencode/commands/wf-review.md +15 -0
- package/templates/common/.opencode/commands/wf-update.md +24 -0
- package/templates/common/.opencode/commands/wf.md +15 -0
- package/templates/common/.opencode/plugins/harness-wf-status.mjs +126 -0
- package/templates/common/AGENTS.md +2 -29
- package/templates/common/CLAUDE.md +35 -6
- package/templates/common/Harness/ACCEPTANCE_PROTOCOL.md +2 -2
- package/templates/common/{MEMORY.md → Harness/MEMORY.md} +17 -4
- package/templates/common/Harness/MEMORY_PROTOCOL.md +80 -30
- package/templates/common/Harness/README.md +53 -25
- package/templates/common/{SETUP.md → Harness/SETUP.md} +278 -276
- package/templates/common/Harness/TASK_ARCHIVE.md +56 -0
- package/templates/common/Harness/WF-AUTO-ANGLES.md +1 -1
- package/templates/common/Harness/WF-AUTO-SPARK.md +5 -14
- package/templates/common/Harness/WF-AUTO.md +22 -84
- package/templates/common/Harness/WF-KERNEL.md +189 -0
- package/templates/common/Harness/WF-MAX.md +60 -328
- package/templates/common/Harness/WF-STATE.md +83 -0
- package/templates/common/Harness/WF.md +117 -237
- package/templates/common/Harness/agent-workflow.md +2 -2
- package/templates/common/Harness/context-loading.md +3 -3
- package/templates/common/Harness/dispatch.md +43 -35
- package/templates/common/Harness/scripts/archive-tasks.mjs +239 -0
- package/templates/common/{scripts → Harness/scripts}/scan-clean.mjs +29 -1
- package/templates/common/{scripts → Harness/scripts}/validate-harness.mjs +921 -699
- package/templates/common/Harness/scripts/wf-auto-update-prompt.mjs +258 -0
- package/templates/common/{scripts → Harness/scripts}/wf-remove.mjs +1 -0
- package/templates/common/{scripts → Harness/scripts}/wf-update-check.mjs +163 -52
- package/templates/common/Harness/subagents.md +36 -28
- package/templates/common/Harness/tasks/_template/PLAN.md +5 -0
- package/templates/common/Harness/tasks/_template/STATE.json +23 -0
- package/templates/common/README.md +2 -2
- package/templates/common/memory/agent-lessons-patterns.md +9 -8
- package/templates/common/memory/routes.md +43 -0
- package/templates/common/memory/startup-hints.md +32 -0
- package/templates/common/memory/tool-usage-reflections.md +9 -8
- package/templates/common/memory/user-corrections-preferences.md +11 -9
- package/templates/optional/catalog.json +8 -0
- package/templates/optional/skills/browser-e2e/.claude/skills/browser-e2e/SKILL.md +42 -42
- package/templates/optional/skills/browser-e2e/.opencode/commands/wf-browser.md +15 -0
- package/templates/optional/skills/github-pr-review/.claude/skills/github-pr-review/SKILL.md +40 -40
- package/templates/optional/skills/python-backend/.claude/skills/python-backend/SKILL.md +40 -40
- package/templates/optional/skills/ts-react-frontend/.claude/skills/ts-react-frontend/SKILL.md +43 -43
- package/templates/optional/skills/ui-ux-review/.claude/skills/ui-ux-review/SKILL.md +40 -40
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* wf-update-check.mjs
|
|
3
|
+
* wf-update-check.mjs - Fast harness update comparison.
|
|
4
4
|
* Fetches remote checksums, compares locally, classifies all files instantly.
|
|
5
5
|
* Only CONFLICT files need AI/user decision.
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* node Harness/scripts/wf-update-check.mjs # full dry-run plan
|
|
9
9
|
* node Harness/scripts/wf-update-check.mjs --json # JSON output for AI consumption
|
|
10
|
+
* node Harness/scripts/wf-update-check.mjs --json # summary/counts + short agent hints only (token-safe)
|
|
11
|
+
* node Harness/scripts/wf-update-check.mjs --json --full-plan # also include full plan + conflict details (verbose)
|
|
10
12
|
* node Harness/scripts/wf-update-check.mjs --apply-safe # apply SAFE+NEW, leave CONFLICT for AI
|
|
11
13
|
* node Harness/scripts/wf-update-check.mjs --apply # apply only when no CONFLICT exists
|
|
14
|
+
*
|
|
15
|
+
* --full-plan and --verbose are aliases.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, lstatSync } from 'fs';
|
|
@@ -19,9 +23,13 @@ import { fileURLToPath } from 'url';
|
|
|
19
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
24
|
const ROOT = process.env.WF_ROOT ? resolve(process.env.WF_ROOT) : resolve(__dirname, '..', '..');
|
|
21
25
|
const VERSION_FILE = resolve(ROOT, 'Harness', '.harness-version');
|
|
22
|
-
const
|
|
26
|
+
const GITHUB_REPO = 'zingspark/create-harness-vibe-coding';
|
|
27
|
+
const GITHUB_LATEST_STABLE = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
28
|
+
const RAW_GITHUB = `https://raw.githubusercontent.com/${GITHUB_REPO}`;
|
|
29
|
+
const TEMPLATE_SUBPATH = 'templates/common/';
|
|
30
|
+
const DEFAULT_SOURCE_BASE = `${RAW_GITHUB}/main/${TEMPLATE_SUBPATH}`;
|
|
23
31
|
|
|
24
|
-
//
|
|
32
|
+
// Tier classification
|
|
25
33
|
|
|
26
34
|
/** Files we NEVER overwrite or delete. */
|
|
27
35
|
const PRESERVE_PATTERNS = [
|
|
@@ -52,10 +60,15 @@ const OPTIONAL_REGISTRATION_FILES = new Set([
|
|
|
52
60
|
'Harness/README.md',
|
|
53
61
|
]);
|
|
54
62
|
|
|
55
|
-
|
|
63
|
+
const BOOTSTRAP_ONLY_FILES = new Set([
|
|
64
|
+
'Harness/SETUP.md',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// Helpers
|
|
56
68
|
|
|
57
69
|
/** Reject paths that escape ROOT (traversal, absolute, .., etc.). */
|
|
58
70
|
function safePath(file) {
|
|
71
|
+
if (/^[A-Za-z]:/.test(file)) return null;
|
|
59
72
|
let normalized = file.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
60
73
|
if (/\/\//.test(normalized)) return null;
|
|
61
74
|
if (normalized.split('/').some(p => p === '..')) return null;
|
|
@@ -132,7 +145,7 @@ function sha256(content) {
|
|
|
132
145
|
function sha256File(path) {
|
|
133
146
|
if (!existsSync(path)) return null;
|
|
134
147
|
let content = readFileSync(path, 'utf-8');
|
|
135
|
-
// Normalize CRLF
|
|
148
|
+
// Normalize CRLF to LF.
|
|
136
149
|
content = content.replace(/\r\n/g, '\n');
|
|
137
150
|
return sha256(content);
|
|
138
151
|
}
|
|
@@ -142,16 +155,18 @@ function classify(file, localHash, storedHash) {
|
|
|
142
155
|
for (const p of PRESERVE_PATTERNS) {
|
|
143
156
|
if (p.test(file)) return 'PRESERVE';
|
|
144
157
|
}
|
|
145
|
-
// MERGE
|
|
158
|
+
// MERGE: dual-purpose, check if user modified.
|
|
146
159
|
for (const p of MERGE_PATTERNS) {
|
|
147
160
|
if (p.test(file)) {
|
|
148
161
|
if (localHash === storedHash) return 'SAFE'; // unmodified, safe
|
|
149
162
|
return 'CONFLICT'; // user modified, needs decision
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
|
-
// Everything else is SAFE runtime file
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
// Everything else is SAFE runtime file: always overwrite.
|
|
166
|
+
// Harness system files (scripts, skills, agents, commands, WF docs) are
|
|
167
|
+
// not user data; the template is authoritative. Only PRESERVE and MERGE
|
|
168
|
+
// files should ever require conflict resolution.
|
|
169
|
+
return 'SAFE';
|
|
155
170
|
}
|
|
156
171
|
|
|
157
172
|
async function fetchRemote(url, timeoutMs = 30000) {
|
|
@@ -170,7 +185,49 @@ async function fetchRemote(url, timeoutMs = 30000) {
|
|
|
170
185
|
}
|
|
171
186
|
}
|
|
172
187
|
|
|
173
|
-
//
|
|
188
|
+
// Main
|
|
189
|
+
|
|
190
|
+
function isPrerelease(v) {
|
|
191
|
+
if (!v || typeof v !== 'string') return false;
|
|
192
|
+
return /-[0-9A-Za-z.-]+/.test(v.replace(/^[^0-9]*/, ''));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function resolveStableSourceBase() {
|
|
196
|
+
try {
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
199
|
+
let res;
|
|
200
|
+
try {
|
|
201
|
+
res = await fetch(GITHUB_LATEST_STABLE, {
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'harness-wf-update-check' },
|
|
204
|
+
});
|
|
205
|
+
} finally {
|
|
206
|
+
clearTimeout(timer);
|
|
207
|
+
}
|
|
208
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
209
|
+
const data = JSON.parse(await res.text());
|
|
210
|
+
const tag = data && data.tag_name;
|
|
211
|
+
if (!tag || data.prerelease) return null;
|
|
212
|
+
const source = `${RAW_GITHUB}/${tag}/${TEMPLATE_SUBPATH}`;
|
|
213
|
+
const versionController = new AbortController();
|
|
214
|
+
const versionTimer = setTimeout(() => versionController.abort(), 15000);
|
|
215
|
+
try {
|
|
216
|
+
const versionRes = await fetch(source + '.harness-version', {
|
|
217
|
+
signal: versionController.signal,
|
|
218
|
+
headers: { 'User-Agent': 'harness-wf-update-check' },
|
|
219
|
+
});
|
|
220
|
+
if (!versionRes.ok) return null;
|
|
221
|
+
const raw = await versionRes.text();
|
|
222
|
+
if (isTemplate(raw)) return null;
|
|
223
|
+
} finally {
|
|
224
|
+
clearTimeout(versionTimer);
|
|
225
|
+
}
|
|
226
|
+
return source;
|
|
227
|
+
} catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
174
231
|
|
|
175
232
|
async function main() {
|
|
176
233
|
const args = process.argv.slice(2);
|
|
@@ -182,14 +239,32 @@ async function main() {
|
|
|
182
239
|
const acceptMerged = readRepeatedFlagValues(args, '--accept-merged');
|
|
183
240
|
const acceptTemplate = readRepeatedFlagValues(args, '--accept-template');
|
|
184
241
|
const ignoreVersion = args.includes('--ignore-version') || args.includes('--force-check');
|
|
185
|
-
const
|
|
242
|
+
const explicitSource = readFlagValue(args, '--source-base') || process.env.WF_SOURCE_BASE;
|
|
243
|
+
let sourceBase = normalizeSourceBase(explicitSource || DEFAULT_SOURCE_BASE);
|
|
244
|
+
let stableTagResolved = false;
|
|
245
|
+
if (!explicitSource) {
|
|
246
|
+
const resolved = await resolveStableSourceBase();
|
|
247
|
+
if (resolved) {
|
|
248
|
+
sourceBase = normalizeSourceBase(resolved);
|
|
249
|
+
stableTagResolved = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
186
252
|
|
|
187
253
|
// 1. Read local state
|
|
188
254
|
if (!existsSync(VERSION_FILE)) {
|
|
189
255
|
if (jsonOut) {
|
|
190
|
-
console.log(JSON.stringify({
|
|
256
|
+
console.log(JSON.stringify({
|
|
257
|
+
status: 'error',
|
|
258
|
+
message: 'Local Harness/.harness-version not found. Is Harness installed?',
|
|
259
|
+
recovery: 'If this is an old Harness install that predates .harness-version, do not reinstall blindly. Restore Harness/.harness-version from backup if available. If the updater script is also missing, run from the project root: npx create-harness-vibe-coding@latest <name> . -y --on-conflict skip --json to restore missing updater infrastructure without overwriting user files.',
|
|
260
|
+
}));
|
|
191
261
|
} else {
|
|
192
262
|
console.error('ERROR: Harness/.harness-version not found. Is Harness installed?');
|
|
263
|
+
console.error(' If this is an old Harness install without version tracking, do not reinstall blindly.');
|
|
264
|
+
console.error(' Restore Harness/.harness-version from backup if available.');
|
|
265
|
+
console.error(' If the updater script is also missing, recover with:');
|
|
266
|
+
console.error(' npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip --json');
|
|
267
|
+
console.error(' This restores missing updater infrastructure without overwriting user data.');
|
|
193
268
|
}
|
|
194
269
|
process.exitCode = 1;
|
|
195
270
|
return;
|
|
@@ -200,10 +275,16 @@ async function main() {
|
|
|
200
275
|
localVersion = JSON.parse(readFileSync(VERSION_FILE, 'utf-8'));
|
|
201
276
|
} catch (e) {
|
|
202
277
|
if (jsonOut) {
|
|
203
|
-
console.log(JSON.stringify({
|
|
278
|
+
console.log(JSON.stringify({
|
|
279
|
+
status: 'error',
|
|
280
|
+
message: 'Failed to parse Harness/.harness-version: ' + e.message,
|
|
281
|
+
recovery: 'The .harness-version file may be corrupted. Do not overwrite user files. Back up or move the corrupted Harness/.harness-version before manual recovery. If the updater script is also missing, run from the project root: npx create-harness-vibe-coding@latest <name> . -y --on-conflict skip --json to restore missing updater infrastructure.',
|
|
282
|
+
}));
|
|
204
283
|
} else {
|
|
205
284
|
console.error('ERROR: Failed to parse Harness/.harness-version:', e.message);
|
|
206
|
-
console.error(' The file may be corrupted.
|
|
285
|
+
console.error(' The file may be corrupted. Back up or move Harness/.harness-version before manual recovery.');
|
|
286
|
+
console.error(' If the updater script is also missing, recover with:');
|
|
287
|
+
console.error(' npx create-harness-vibe-coding@latest <project-name> . -y --on-conflict skip --json');
|
|
207
288
|
}
|
|
208
289
|
process.exitCode = 1;
|
|
209
290
|
return;
|
|
@@ -229,9 +310,9 @@ async function main() {
|
|
|
229
310
|
if (jsonOut) {
|
|
230
311
|
console.log(JSON.stringify({ status: 'template-remote', message: 'Remote .harness-version has not been generated yet.' }));
|
|
231
312
|
} else {
|
|
232
|
-
console.log('
|
|
313
|
+
console.log('WARN: Remote .harness-version is a template (contains {{placeholders}}).');
|
|
233
314
|
console.log(' The generate step has not been run on the remote repo. No update possible.');
|
|
234
|
-
console.log(' This is expected during development
|
|
315
|
+
console.log(' This is expected during development; the update mechanism works once the remote is live.');
|
|
235
316
|
}
|
|
236
317
|
process.exitCode = 1;
|
|
237
318
|
return;
|
|
@@ -248,10 +329,10 @@ async function main() {
|
|
|
248
329
|
return;
|
|
249
330
|
}
|
|
250
331
|
|
|
251
|
-
// Compare versions
|
|
332
|
+
// Compare versions and prevent downgrades from explicit custom sources.
|
|
252
333
|
function parseSemver(v) {
|
|
253
334
|
if (!v || typeof v !== 'string') return [0, 0, 0];
|
|
254
|
-
return v.replace(/^[^0-9]*/, '').split('
|
|
335
|
+
return v.replace(/^[^0-9]*/, '').split('.').slice(0, 3).map(n => Number(n) || 0);
|
|
255
336
|
}
|
|
256
337
|
function cmpSemver(a, b) {
|
|
257
338
|
const va = parseSemver(a), vb = parseSemver(b);
|
|
@@ -261,27 +342,38 @@ async function main() {
|
|
|
261
342
|
|
|
262
343
|
const localGen = localVersion.generator || '0.0.0';
|
|
263
344
|
const remoteGen = remoteVersion.generator || '0.0.0';
|
|
345
|
+
|
|
346
|
+
if (!ignoreVersion && isPrerelease(remoteGen)) {
|
|
347
|
+
if (jsonOut) {
|
|
348
|
+
console.log(JSON.stringify({ status: 'up-to-date', version: localGen, remote: remoteGen, sourceBase }));
|
|
349
|
+
} else {
|
|
350
|
+
console.log(`Already up to date (v${localGen}). Remote ${remoteGen} is a prerelease and is ignored.`);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
264
355
|
const versionCmp = cmpSemver(remoteGen, localGen);
|
|
265
356
|
|
|
266
357
|
if (!ignoreVersion && versionCmp <= 0) {
|
|
358
|
+
const reportDowngrade = !stableTagResolved;
|
|
267
359
|
if (jsonOut) {
|
|
268
360
|
console.log(JSON.stringify({
|
|
269
|
-
status: versionCmp < 0 ? 'downgrade-refused' : 'up-to-date',
|
|
361
|
+
status: (versionCmp < 0 && reportDowngrade) ? 'downgrade-refused' : 'up-to-date',
|
|
270
362
|
version: localGen,
|
|
271
363
|
remote: remoteGen,
|
|
272
364
|
sourceBase,
|
|
273
365
|
}));
|
|
274
|
-
} else if (versionCmp < 0) {
|
|
275
|
-
console.log(
|
|
366
|
+
} else if (versionCmp < 0 && reportDowngrade) {
|
|
367
|
+
console.log(`WARN: Remote (v${remoteGen}) is OLDER than local (v${localGen}). Downgrade refused.`);
|
|
276
368
|
} else {
|
|
277
|
-
console.log(
|
|
369
|
+
console.log(`Already up to date (v${localGen})`);
|
|
278
370
|
}
|
|
279
|
-
if (versionCmp < 0) process.exitCode = 1;
|
|
371
|
+
if (versionCmp < 0 && reportDowngrade) process.exitCode = 1;
|
|
280
372
|
return;
|
|
281
373
|
}
|
|
282
374
|
|
|
283
375
|
if (ignoreVersion) {
|
|
284
|
-
if (!jsonOut) console.log('
|
|
376
|
+
if (!jsonOut) console.log('Version check bypassed (--ignore-version). Comparing files anyway.');
|
|
285
377
|
}
|
|
286
378
|
|
|
287
379
|
const remoteChecksums = remoteVersion.checksums || {};
|
|
@@ -320,16 +412,26 @@ async function main() {
|
|
|
320
412
|
}
|
|
321
413
|
|
|
322
414
|
function buildJsonPlan() {
|
|
415
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
323
416
|
return {
|
|
324
417
|
updated: plan.updated.map(withRemoteMeta),
|
|
325
418
|
created: plan.created.map(withRemoteMeta),
|
|
326
419
|
adopted: plan.adopted.map(withRemoteMeta),
|
|
327
|
-
conflict: plan.conflict.map(withConflictActions),
|
|
420
|
+
conflict: verbose ? plan.conflict.map(withConflictActions) : plan.conflict.map(withConflictActions).slice(0, 5),
|
|
421
|
+
conflictTruncated: !verbose && plan.conflict.length > 5 ? plan.conflict.length - 5 : undefined,
|
|
328
422
|
skipped: plan.skipped.map(withRemoteMeta),
|
|
329
423
|
};
|
|
330
424
|
}
|
|
331
425
|
|
|
332
426
|
function buildAgentHints(jsonPlan) {
|
|
427
|
+
// Token-safe by default: omit the conflict array; full list available via --json --verbose (or --full-plan)
|
|
428
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
429
|
+
const maxConflictDetails = verbose ? Infinity : 5;
|
|
430
|
+
const totalConflicts = plan.conflict.length;
|
|
431
|
+
const conflicts = jsonPlan.conflict.slice(0, maxConflictDetails);
|
|
432
|
+
const truncated = totalConflicts > maxConflictDetails
|
|
433
|
+
? totalConflicts - maxConflictDetails
|
|
434
|
+
: 0;
|
|
333
435
|
return {
|
|
334
436
|
mode: 'script-first-ai-conflicts',
|
|
335
437
|
dryRunJsonCommand: 'node Harness/scripts/wf-update-check.mjs --json',
|
|
@@ -338,8 +440,10 @@ async function main() {
|
|
|
338
440
|
finalizeCommand: 'node Harness/scripts/wf-update-check.mjs --finalize',
|
|
339
441
|
partialUpdate: localVersion.partialUpdate || null,
|
|
340
442
|
acceptedConflicts: localVersion.acceptedConflicts || {},
|
|
341
|
-
aiMergeRequired
|
|
342
|
-
|
|
443
|
+
// aiMergeRequired array is attached only in verbose mode to keep default output token-safe.
|
|
444
|
+
...(verbose ? { aiMergeRequired: conflicts } : {}),
|
|
445
|
+
aiMergeRequiredCount: totalConflicts,
|
|
446
|
+
aiMergeRequiredTruncated: truncated > 0 ? truncated : undefined,
|
|
343
447
|
conflictPolicy: 'Use the script for SAFE/NEW/adopted files. For each CONFLICT file, compare local content with templateHint/remoteUrl, then record the decision with --accept-local, --accept-merged, or --accept-template. Do not hand-edit Harness/.harness-version.',
|
|
344
448
|
postUpdateCommands: [
|
|
345
449
|
'node Harness/scripts/validate-harness.mjs',
|
|
@@ -438,8 +542,13 @@ async function main() {
|
|
|
438
542
|
continue;
|
|
439
543
|
}
|
|
440
544
|
|
|
545
|
+
if (BOOTSTRAP_ONLY_FILES.has(canonical) && localHash === null) {
|
|
546
|
+
plan.skipped.push({ file, reason: 'bootstrap-only file already removed locally' });
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
441
550
|
if (!storedHash) {
|
|
442
|
-
// New file from remote
|
|
551
|
+
// New file from remote: if local file exists, it is a CONFLICT.
|
|
443
552
|
if (localHash) {
|
|
444
553
|
if (localHash === remoteHash) {
|
|
445
554
|
plan.adopted.push({ file, localHash, remoteHash, reason: 'new remote file already matches local file' });
|
|
@@ -452,10 +561,10 @@ async function main() {
|
|
|
452
561
|
plan.conflict.push({ file, localHash, storedHash: 'none', remoteHash, reason: 'new remote file conflicts with existing local file' });
|
|
453
562
|
continue;
|
|
454
563
|
}
|
|
455
|
-
// New file
|
|
564
|
+
// New file: still respect PRESERVE classification.
|
|
456
565
|
const tier = classify(canonical, null, null);
|
|
457
566
|
if (tier === 'PRESERVE') {
|
|
458
|
-
plan.skipped.push({ file, reason: 'PRESERVE
|
|
567
|
+
plan.skipped.push({ file, reason: 'PRESERVE: new file would overwrite user data' });
|
|
459
568
|
} else {
|
|
460
569
|
plan.created.push({ file, remoteHash });
|
|
461
570
|
}
|
|
@@ -486,7 +595,7 @@ async function main() {
|
|
|
486
595
|
const tier = classify(canonical, localHash, storedHash);
|
|
487
596
|
|
|
488
597
|
if (tier === 'PRESERVE') {
|
|
489
|
-
plan.skipped.push({ file, reason: 'PRESERVE
|
|
598
|
+
plan.skipped.push({ file, reason: 'PRESERVE: user data' });
|
|
490
599
|
} else if (tier === 'SAFE') {
|
|
491
600
|
if (localHash === remoteHash) {
|
|
492
601
|
plan.skipped.push({ file, reason: 'already current' });
|
|
@@ -512,6 +621,7 @@ async function main() {
|
|
|
512
621
|
|
|
513
622
|
// 3. Output
|
|
514
623
|
if (jsonOut) {
|
|
624
|
+
const verbose = args.includes('--verbose') || args.includes('--full-plan');
|
|
515
625
|
const jsonPlan = buildJsonPlan();
|
|
516
626
|
console.log(JSON.stringify({
|
|
517
627
|
status: localVersion.partialUpdate ? 'partial-update' : 'update-available',
|
|
@@ -525,20 +635,21 @@ async function main() {
|
|
|
525
635
|
adopted: plan.adopted.length,
|
|
526
636
|
conflict: plan.conflict.length,
|
|
527
637
|
skipped: plan.skipped.length,
|
|
528
|
-
|
|
638
|
+
// Token-safe by default: attach the full plan only when --verbose / --full-plan is passed.
|
|
639
|
+
...(verbose ? { plan: jsonPlan } : {}),
|
|
529
640
|
agent: buildAgentHints(jsonPlan),
|
|
530
641
|
}, null, 2));
|
|
531
642
|
return;
|
|
532
643
|
}
|
|
533
644
|
|
|
534
|
-
console.log(`\
|
|
645
|
+
console.log(`\nUpdate: v${localGen} -> v${remoteGen}`);
|
|
535
646
|
console.log(` ${plan.updated.length} safe update, ${plan.created.length} new, ${plan.conflict.length} conflict, ${plan.skipped.length} skipped\n`);
|
|
536
647
|
|
|
537
648
|
// Show conflicts (these need AI/user decision)
|
|
538
649
|
if (plan.conflict.length > 0) {
|
|
539
|
-
console.log('
|
|
650
|
+
console.log('CONFLICTS (need your decision):');
|
|
540
651
|
for (const c of plan.conflict) {
|
|
541
|
-
console.log(`
|
|
652
|
+
console.log(` ! ${c.file} [${c.reason}]`);
|
|
542
653
|
}
|
|
543
654
|
if (plan.updated.length + plan.created.length > 0) {
|
|
544
655
|
console.log(' Tip: run --apply-safe to apply SAFE/NEW files first, then merge conflicts.');
|
|
@@ -548,17 +659,17 @@ async function main() {
|
|
|
548
659
|
|
|
549
660
|
// Show what will be auto-updated
|
|
550
661
|
if (plan.updated.length + plan.created.length > 0) {
|
|
551
|
-
console.log('
|
|
552
|
-
for (const u of plan.updated) console.log(`
|
|
662
|
+
console.log('AUTO (safe to apply):');
|
|
663
|
+
for (const u of plan.updated) console.log(` ^ ${u.file}`);
|
|
553
664
|
for (const c of plan.created) console.log(` + ${c.file}`);
|
|
554
665
|
console.log('');
|
|
555
666
|
}
|
|
556
667
|
|
|
557
668
|
// 4. Apply if requested
|
|
558
669
|
if (apply || applySafe) {
|
|
559
|
-
// Refuse to apply when conflicts exist
|
|
670
|
+
// Refuse to apply when conflicts exist; must resolve first.
|
|
560
671
|
if (apply && !applySafe && plan.conflict.length > 0) {
|
|
561
|
-
console.log(
|
|
672
|
+
console.log(`Cannot apply: ${plan.conflict.length} conflicts must be resolved first.`);
|
|
562
673
|
console.log(' Run --apply-safe to apply SAFE/NEW files first, or resolve conflicts manually then re-run --apply.');
|
|
563
674
|
process.exitCode = 1;
|
|
564
675
|
return plan;
|
|
@@ -574,23 +685,23 @@ async function main() {
|
|
|
574
685
|
for (const u of plan.updated) {
|
|
575
686
|
try {
|
|
576
687
|
const dest = safePath(u.file);
|
|
577
|
-
if (!dest) { console.error(`
|
|
578
|
-
// Symlink rejection
|
|
688
|
+
if (!dest) { console.error(` x Traversal rejected: ${u.file}`); failed++; continue; }
|
|
689
|
+
// Symlink rejection: do not follow symlinks.
|
|
579
690
|
if (lexists(dest)) {
|
|
580
|
-
try { if (lstatSync(dest).isSymbolicLink()) { console.error(`
|
|
691
|
+
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${u.file}`); failed++; continue; } } catch (_) {}
|
|
581
692
|
}
|
|
582
693
|
const content = await fetchRemote(sourceBase + remotePath(u.file));
|
|
583
694
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
584
695
|
const fetchedHash = sha256(normalized);
|
|
585
696
|
if (fetchedHash !== u.remoteHash) {
|
|
586
|
-
console.error(`
|
|
697
|
+
console.error(` x Hash mismatch: ${u.file}`);
|
|
587
698
|
failed++; continue;
|
|
588
699
|
}
|
|
589
700
|
mkdirSync(dirname(dest), { recursive: true });
|
|
590
701
|
writeFileSync(dest, normalized, 'utf-8');
|
|
591
702
|
applied++;
|
|
592
703
|
} catch (e) {
|
|
593
|
-
console.error(`
|
|
704
|
+
console.error(` x Failed: ${u.file} - ${e.message}`);
|
|
594
705
|
failed++;
|
|
595
706
|
}
|
|
596
707
|
}
|
|
@@ -598,25 +709,25 @@ async function main() {
|
|
|
598
709
|
for (const c of plan.created) {
|
|
599
710
|
try {
|
|
600
711
|
const dest = safePath(c.file);
|
|
601
|
-
if (!dest) { console.error(`
|
|
712
|
+
if (!dest) { console.error(` x Traversal rejected: ${c.file}`); failed++; continue; }
|
|
602
713
|
// TOCTOU: recheck file didn't appear since planning
|
|
603
714
|
if (lexists(dest)) {
|
|
604
|
-
try { if (lstatSync(dest).isSymbolicLink()) { console.error(`
|
|
605
|
-
console.error(`
|
|
715
|
+
try { if (lstatSync(dest).isSymbolicLink()) { console.error(` x Symlink rejected: ${c.file}`); failed++; continue; } } catch (_) {}
|
|
716
|
+
console.error(` x File created since plan: ${c.file} - treating as CONFLICT`);
|
|
606
717
|
failed++; continue;
|
|
607
718
|
}
|
|
608
719
|
const content = await fetchRemote(sourceBase + remotePath(c.file));
|
|
609
720
|
const normalized = content.replace(/\r\n/g, '\n');
|
|
610
721
|
const fetchedHash = sha256(normalized);
|
|
611
722
|
if (fetchedHash !== c.remoteHash) {
|
|
612
|
-
console.error(`
|
|
723
|
+
console.error(` x Hash mismatch: ${c.file}`);
|
|
613
724
|
failed++; continue;
|
|
614
725
|
}
|
|
615
726
|
mkdirSync(dirname(dest), { recursive: true });
|
|
616
727
|
writeFileSync(dest, normalized, 'utf-8');
|
|
617
728
|
applied++;
|
|
618
729
|
} catch (e) {
|
|
619
|
-
console.error(`
|
|
730
|
+
console.error(` x Failed: ${c.file} - ${e.message}`);
|
|
620
731
|
failed++;
|
|
621
732
|
}
|
|
622
733
|
}
|
|
@@ -643,12 +754,12 @@ async function main() {
|
|
|
643
754
|
}
|
|
644
755
|
writeFileSync(VERSION_FILE, JSON.stringify(localVersion, null, 2) + '\n', 'utf-8');
|
|
645
756
|
if (plan.conflict.length === 0) {
|
|
646
|
-
console.log(
|
|
757
|
+
console.log(`Applied ${applied} files. Version updated to ${remoteVersion.generator}.`);
|
|
647
758
|
} else {
|
|
648
|
-
console.log(
|
|
759
|
+
console.log(`Applied ${applied} SAFE/NEW files. Version remains ${localGen}; ${plan.conflict.length} conflicts still need merge.`);
|
|
649
760
|
}
|
|
650
761
|
} else {
|
|
651
|
-
console.log(
|
|
762
|
+
console.log(`${failed} failures. NO files were version-tracked. Fix and re-run.`);
|
|
652
763
|
process.exitCode = 1;
|
|
653
764
|
}
|
|
654
765
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Purpose: coordinate subagents for speed without losing control of scope, evidence, or integration.
|
|
4
4
|
|
|
5
|
-
Use this file when work needs multiple roles, parallel reading, independent review, broad context, repeated failures, or `/wf
|
|
5
|
+
Use this file when work needs multiple roles, parallel reading, independent review, broad context, repeated failures, or explicit `/wf` / `/wf-max` invocation.
|
|
6
6
|
|
|
7
7
|
project files are the only durable communication channel; chat/subagent transcript state is non-authoritative. Important assumptions, decisions, blockers, evidence, and handoffs must be written to `Harness/tasks/<task-id>/PROGRESS.md` and `Harness/tasks/<task-id>/PLAN.md`, the current feature doc, `Harness/MEMORY.md`, or `Harness/memory/*` as appropriate.
|
|
8
8
|
|
|
@@ -43,20 +43,22 @@ Subagents provide bounded work. They do not own final scope, architecture, relea
|
|
|
43
43
|
|
|
44
44
|
Use the installed roster under `.claude/agents/` before inventing ad hoc roles.
|
|
45
45
|
|
|
46
|
-
| Agent | Default Use |
|
|
47
|
-
| --- | --- |
|
|
48
|
-
| `planner` | decompose goals, map unknowns, define success criteria and write sets |
|
|
49
|
-
| `researcher` | local/external ecosystem context, comparable projects, current facts |
|
|
50
|
-
| `docs-researcher` | official docs, SDK/API behavior, browser/tool constraints |
|
|
51
|
-
| `architect` | boundaries, interface decoupling, state ownership, data flow, migration risk |
|
|
52
|
-
| `test-writer` | failing tests, manual check contracts, browser/API evidence plan |
|
|
53
|
-
| `implementer` | bounded code or doc changes after the second plan |
|
|
54
|
-
| `reviewer` | spec compliance, code quality, maintainability, security, missing tests |
|
|
55
|
-
| `debugger` | reproduced failures, root cause isolation, smallest safe fix |
|
|
56
|
-
| `verifier` | command execution, real browser/API checks, final evidence |
|
|
57
|
-
| `reflector` | closeout synthesis, unresolved-risk check, acceptance gate verdict |
|
|
58
|
-
| `
|
|
59
|
-
| `
|
|
46
|
+
| Agent | Default Use | Model Tier |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| `planner` | decompose goals, map unknowns, define success criteria and write sets | standard |
|
|
49
|
+
| `researcher` | local/external ecosystem context, comparable projects, current facts | standard |
|
|
50
|
+
| `docs-researcher` | official docs, SDK/API behavior, browser/tool constraints | standard |
|
|
51
|
+
| `architect` | boundaries, interface decoupling, state ownership, data flow, migration risk | standard |
|
|
52
|
+
| `test-writer` | failing tests, manual check contracts, browser/API evidence plan | standard |
|
|
53
|
+
| `implementer` | bounded code or doc changes after the second plan | standard |
|
|
54
|
+
| `reviewer` | spec compliance, code quality, maintainability, security, missing tests | standard |
|
|
55
|
+
| `debugger` | reproduced failures, root cause isolation, smallest safe fix | standard |
|
|
56
|
+
| `verifier` | command execution, real browser/API checks, final evidence | standard or small-fast |
|
|
57
|
+
| `reflector` | closeout synthesis, unresolved-risk check, acceptance gate verdict | standard |
|
|
58
|
+
| `task-scribe` | task state, heartbeat, dispatch ledger, evidence pointers — NEVER source code | small-fast (haiku) |
|
|
59
|
+
| `codebase-explorer` | scoped read-only source exploration, file discovery, symbol tracing | small-fast (haiku) |
|
|
60
|
+
| `context-master` | analyze context usage, recommend compression at ~85% window, extract durable session knowledge during closeout | small-fast (haiku) |
|
|
61
|
+
| `memory-master` | write/consolidate memory entries, dedup, cross-project extraction; dispatched on repeated failures, user corrections, and WF closeout | standard |
|
|
60
62
|
|
|
61
63
|
## Acceptance Role Passes
|
|
62
64
|
|
|
@@ -80,8 +82,10 @@ Hard rule: implementer may not be the independent validator for the same AC ID.
|
|
|
80
82
|
|
|
81
83
|
## WF Default Fan-Out
|
|
82
84
|
|
|
83
|
-
Explicit `/wf`,
|
|
84
|
-
|
|
85
|
+
Explicit `/wf`, `$wf`, or `/skills wf` requires tier-specific role coverage per `Harness/WF.md`.
|
|
86
|
+
WF-Light: planner + test-writer + implementer + verifier.
|
|
87
|
+
WF-Standard: adds research/docs + one review lens.
|
|
88
|
+
WF-Full: complete role-chain coverage from `.claude/agents/` before closeout: plan,
|
|
85
89
|
research/docs research as needed, architecture, test, implement, independent
|
|
86
90
|
validation, cross-review, reflector, and accept.
|
|
87
91
|
|
|
@@ -102,7 +106,7 @@ Then add phase-specific agents:
|
|
|
102
106
|
- `context-master` before closeout for knowledge extraction
|
|
103
107
|
- `memory-master` after repeated failures and during closeout for consolidation
|
|
104
108
|
|
|
105
|
-
Collaboration mode is determined by concrete conditions, not a fixed ratio. See `Harness/WF.md
|
|
109
|
+
Collaboration mode is determined by WF tier and concrete conditions, not a fixed ratio. See `Harness/WF.md` for the tier decision guide. Summary: WF-Light uses minimal roles. WF-Standard uses moderate roles with one review lens. WF-Full always uses the complete role chain. Non-WF complex work may still use subagents and planning without entering any WF mode. Repeated failure after two attempts escalates to next tier.
|
|
106
110
|
|
|
107
111
|
## Efficiency Ladder
|
|
108
112
|
|
|
@@ -115,22 +119,19 @@ Choose the cheapest coordination level that is safe.
|
|
|
115
119
|
| Parallel read-only | broad reading, research, architecture, multiple independent failures | 2-3 read-only agents |
|
|
116
120
|
| Serial build lane | normal feature or fix | acceptance/contract -> test-writer -> implementer -> verifier evidence -> cross-review -> reflector -> acceptance |
|
|
117
121
|
| Isolated lanes | disjoint write sets or competing approaches | separate worktrees, then review and merge |
|
|
118
|
-
| Max parallelism | 5+ disjoint files, fan-out benefit > coordination cost | /wf
|
|
122
|
+
| Max parallelism | 5+ disjoint files, fan-out benefit > coordination cost | /wf-max: write-set coloring -> wave dispatch -> parallel review |
|
|
119
123
|
|
|
120
124
|
Max parallelism removes the Harness default cap, not the runtime's physical or
|
|
121
125
|
account cap. For WF-MAX, record the current runtime budget, use native
|
|
122
126
|
subagents first, close completed agents before declaring the pool exhausted,
|
|
123
|
-
then overflow to
|
|
127
|
+
then overflow to a peer CLI (`claude -p`, `codex exec`, or `opencode run --agent <role> --dir .`) with explicit
|
|
124
128
|
dispatch packets. Generated Harness Codex config defaults to
|
|
125
129
|
`agents.max_threads = 12` and `agents.max_depth = 1`; if that becomes the
|
|
126
130
|
bottleneck, ask the user before raising `agents.max_threads` and keep
|
|
127
131
|
`max_depth = 1` unless recursive delegation is explicitly approved. Do not rely
|
|
128
132
|
on undocumented fork/derive bypasses as stable capacity.
|
|
129
133
|
|
|
130
|
-
Default for
|
|
131
|
-
planning. For explicit WF/WK mode, never use the solo pass; schedule the
|
|
132
|
-
complete role chain and use bounded role passes as the recorded fallback when
|
|
133
|
-
subagents are unavailable.
|
|
134
|
+
Default for explicit WF invocation: tier-based. WF-Light: planner + test-writer + implementer + verifier (bounded passes acceptable). WF-Standard: adds research/docs + one independent review lens. WF-Full: complete role chain, use bounded role passes as the recorded fallback when subagents are unavailable.
|
|
134
135
|
|
|
135
136
|
## WF Orchestration Shape
|
|
136
137
|
|
|
@@ -148,10 +149,10 @@ controller intake
|
|
|
148
149
|
-> close with evidence
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
Use this shape for `/wf`,
|
|
152
|
+
Use this shape for explicit `/wf`, `/wf-max`, or non-WF complex work with subagents.
|
|
152
153
|
|
|
153
154
|
```text
|
|
154
|
-
/wf
|
|
155
|
+
/wf-max orchestration shape:
|
|
155
156
|
controller intake
|
|
156
157
|
-> wave 0: max-parallel exploration (4-14 read-only agents)
|
|
157
158
|
-> E-GATE: Exploration Gate - all questions answered, findings synthesized (per WF-MAX.md)
|
|
@@ -167,14 +168,14 @@ controller intake
|
|
|
167
168
|
|
|
168
169
|
## Dispatch Pack
|
|
169
170
|
|
|
170
|
-
Use the canonical dispatch input and handoff format in `Harness/dispatch.md`. Every subagent dispatch must be self-contained - inject only the docs selected by `Harness/README.md` and `Harness/context-loading.md`.
|
|
171
|
+
Use the canonical dispatch input and handoff format in `Harness/dispatch.md`. Every subagent dispatch must be self-contained - inject only the docs selected by `Harness/README.md` and `Harness/context-loading.md`. On session start, the controller reads `Harness/tasks/<task-id>/STATE.json` per [WF-STATE.md](WF-STATE.md) to resume the dispatch ledger and ready queue.
|
|
171
172
|
|
|
172
173
|
## Parallelism Rules
|
|
173
174
|
|
|
174
175
|
- Read-only agents may run in parallel.
|
|
175
176
|
- Writing agents run serially unless write sets are disjoint and the controller has chosen an isolated worktree.
|
|
176
177
|
- Reviewers may run in parallel after implementation, but spec compliance is evaluated before code-quality approval.
|
|
177
|
-
- Subagents are readers and reporters. They return findings and PLAN patch suggestions. Only the controller (main agent) commits state changes to task files.
|
|
178
|
+
- Subagents are readers and reporters. They return findings and PLAN patch suggestions. Only the controller (main agent) or task-scribe commits state changes to task files. Production source agents (implementer, debugger, test-writer) never write task state unless explicitly dispatched as task-scribe.
|
|
178
179
|
- Do not let two agents edit `Harness/tasks/<task-id>/PROGRESS.md`, `Harness/tasks/<task-id>/PLAN.md`, `Harness/MEMORY.md`, or `Harness/memory/*` concurrently. The controller writes durable state.
|
|
179
180
|
- If two agents disagree, the controller records the conflict in `Harness/tasks/<task-id>/PLAN.md` and chooses the smallest reversible next step.
|
|
180
181
|
|
|
@@ -191,6 +192,13 @@ matrix from running behavior and evidence, not from the implementer's summary.
|
|
|
191
192
|
|
|
192
193
|
If either reviewer finds issues, the implementer or debugger fixes them and the same gate runs again. Do not move to final acceptance with open critical/high findings or without reflector PASS.
|
|
193
194
|
|
|
195
|
+
For `/wf-review`, use the installed `reviewer` role before inventing any
|
|
196
|
+
ad hoc review prompt. If no peer CLI is available, dispatch `reviewer` as an
|
|
197
|
+
independent same-runtime subagent context; for broad WF-MAX review, dispatch
|
|
198
|
+
`review-manager` when the runtime supports nested reviewer fan-out. Reviewer
|
|
199
|
+
agents report suggestions only. The controller accepts, rejects, or escalates
|
|
200
|
+
each finding and owns the final decision.
|
|
201
|
+
|
|
194
202
|
## Subagent Status Handling
|
|
195
203
|
|
|
196
204
|
| Status | Controller Action |
|
|
@@ -35,6 +35,11 @@ Forbidden:
|
|
|
35
35
|
-
|
|
36
36
|
- Truth files (PRD, ACs, UI/API contracts, test plan, validation report) unless a Change Request is recorded.
|
|
37
37
|
|
|
38
|
+
## Memory Preflight
|
|
39
|
+
|
|
40
|
+
- Memory preflight: done | exempt | blocked
|
|
41
|
+
- Memory hints: none | <file/path + reason>
|
|
42
|
+
|
|
38
43
|
## Context
|
|
39
44
|
|
|
40
45
|
- Loaded:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"taskId": "task-example",
|
|
4
|
+
"status": "active",
|
|
5
|
+
"mode": "direct",
|
|
6
|
+
"tier": "none",
|
|
7
|
+
"phase": "intake",
|
|
8
|
+
"gate": null,
|
|
9
|
+
"updatedAt": "2026-07-16T00:00:00.000Z",
|
|
10
|
+
"activeQuestion": null,
|
|
11
|
+
"nextAction": "Define goal and acceptance criteria",
|
|
12
|
+
"acceptance": [],
|
|
13
|
+
"queues": {
|
|
14
|
+
"ready": [],
|
|
15
|
+
"running": [],
|
|
16
|
+
"blocked": [],
|
|
17
|
+
"done": []
|
|
18
|
+
},
|
|
19
|
+
"dispatchLedger": [],
|
|
20
|
+
"decisions": [],
|
|
21
|
+
"risks": [],
|
|
22
|
+
"artifacts": []
|
|
23
|
+
}
|