create-harness-vibe-coding 0.8.17 → 0.8.18

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README-CN.md +2 -0
  3. package/README.md +2 -0
  4. package/package.json +1 -1
  5. package/src/generator.js +613 -97
  6. package/src/index.js +173 -42
  7. package/src/prompts.js +18 -0
  8. package/templates/common/.claude/commands/wf-command-create.md +58 -0
  9. package/templates/common/.claude/commands/wf-help.md +4 -0
  10. package/templates/common/.claude/commands/wf-task-archive.md +26 -0
  11. package/templates/common/.claude/commands/wf-task-list.md +24 -0
  12. package/templates/common/.claude/commands/wf-task-record.md +24 -0
  13. package/templates/common/.claude/rules/ecc/common.md +1 -1
  14. package/templates/common/.claude/skills/wf-agents-docs/SKILL.md +15 -30
  15. package/templates/common/.claude/skills/wf-command-create/SKILL.md +37 -0
  16. package/templates/common/.claude/skills/wf-max/SKILL.md +1 -1
  17. package/templates/common/.claude/skills/wf-review/SKILL.md +29 -2
  18. package/templates/common/.claude/skills/wf-task-archive/SKILL.md +28 -0
  19. package/templates/common/.claude/skills/wf-task-list/SKILL.md +28 -0
  20. package/templates/common/.claude/skills/wf-task-record/SKILL.md +28 -0
  21. package/templates/common/.harness-version +66 -32
  22. package/templates/common/.opencode/commands/wf-command-create.md +61 -0
  23. package/templates/common/.opencode/commands/wf-help.md +4 -0
  24. package/templates/common/.opencode/commands/wf-task-archive.md +29 -0
  25. package/templates/common/.opencode/commands/wf-task-list.md +27 -0
  26. package/templates/common/.opencode/commands/wf-task-record.md +27 -0
  27. package/templates/common/CLAUDE.md +8 -6
  28. package/templates/common/Harness/MEMORY.md +9 -0
  29. package/templates/common/Harness/README.md +17 -36
  30. package/templates/common/Harness/ownership.manifest.json +87 -2
  31. package/templates/common/Harness/scripts/task-state.mjs +395 -5
  32. package/templates/common/Harness/scripts/validate-harness.mjs +411 -46
  33. package/templates/common/Harness/scripts/wf-remove.mjs +34 -2
  34. package/templates/common/Harness/specs/guides/SETUP.md +8 -0
  35. package/templates/common/Harness/specs/protocols/MEMORY_PROTOCOL.md +15 -0
  36. package/templates/common/Harness/specs/protocols/TASK_ARCHIVE.md +9 -3
  37. package/templates/common/Harness/specs/runtime/command-surface.json +215 -0
  38. package/templates/common/Harness/specs/runtime/subagents.md +6 -0
  39. package/templates/common/Harness/specs/workflows/WF-MAX.md +5 -0
  40. package/templates/common/Harness/specs/workflows/WF-STATE.md +66 -0
  41. package/templates/common/Harness/tasks/_template/STATE.json +6 -0
@@ -71,6 +71,8 @@ const STATUS_ALIASES = new Map([
71
71
  ['need-user-decision', 'needs-user-decision'],
72
72
  ['close-out', 'closeout'],
73
73
  ]);
74
+ const OPEN_TASK_STATUSES = new Set(['active', 'blocked', 'in_progress', 'running', 'pending', 'needs-user-decision']);
75
+ const VALUE_FLAGS = new Set(['--keep', '--mode', '--phase', '--status', '--task', '--text', '--title', '--note', '--context']);
74
76
 
75
77
  function hasFlag(name) {
76
78
  return args.includes(name);
@@ -82,6 +84,22 @@ function flagValue(name, fallback = null) {
82
84
  return args[index + 1];
83
85
  }
84
86
 
87
+ function findTaskIdArg(startIndex = 1) {
88
+ for (let i = startIndex; i < args.length; i++) {
89
+ const a = args[i];
90
+ if (a.startsWith('--')) {
91
+ if (a.includes('=')) continue;
92
+ const next = args[i + 1];
93
+ if (VALUE_FLAGS.has(a) && next && !next.startsWith('--')) {
94
+ i++; // skip the value
95
+ }
96
+ continue;
97
+ }
98
+ return a;
99
+ }
100
+ return null;
101
+ }
102
+
85
103
  const outputJson = hasFlag('--json');
86
104
 
87
105
  function print(payload) {
@@ -127,12 +145,15 @@ function usage() {
127
145
  message: `Usage: node Harness/scripts/task-state.mjs <command> [options]
128
146
 
129
147
  Commands:
130
- list [--json] List task state.
131
- validate [--strict] [--json] Validate state consistency.
148
+ list [--json] List task state with dependency/resume info.
149
+ validate [--strict] [--json] Validate state consistency (includes link checks).
132
150
  reconcile [--dry-run|--apply] [--json] Normalize STATE.json and root PROGRESS.md.
133
151
  set-active <task-id> [--dry-run] Set the single active task.
134
152
  transition <task-id> --status <s> --phase <p> [--dry-run]
135
153
  archive [--dry-run|--apply] [--keep n] [--task id] [--json]
154
+ record <task-id> [--create] [--text "description"] [--status <s>] [--mode <m>] [--dry-run|--apply] [--json]
155
+ Create or update a task record.
156
+ open [--json] List open (non-archived, active-status) tasks.
136
157
 
137
158
  Archive defaults to dry-run and keeps ${OUTER_TASK_CAP} non-archived task capsules.`,
138
159
  }, 0);
@@ -226,6 +247,23 @@ function defaultQueues() {
226
247
  };
227
248
  }
228
249
 
250
+ const VALID_MODES = new Set([
251
+ 'direct',
252
+ 'wf',
253
+ 'wf-max',
254
+ 'wf-auto',
255
+ 'wf-auto-spark',
256
+ 'wf-review',
257
+ 'wf-browser',
258
+ ]);
259
+
260
+ function normalizeMode(value) {
261
+ if (value === null || value === undefined) return '';
262
+ const raw = String(value).trim().toLowerCase();
263
+ if (!raw) return '';
264
+ if (VALID_MODES.has(raw)) return raw;
265
+ return '';
266
+ }
229
267
  function normalizeQueues(state) {
230
268
  const source = state && typeof state.queues === 'object' && state.queues ? state.queues : state || {};
231
269
  return {
@@ -398,6 +436,48 @@ function validateState({ strict = false } = {}) {
398
436
  if (task.id === rootProgress.activeTask && normalizeStatus(task.state.status) && normalizeStatus(task.state.status) !== 'active') {
399
437
  issue(`${task.id}: root Active Task points here but STATE.json status is "${normalizeStatus(task.state.status)}"`, true);
400
438
  }
439
+
440
+ const links = task.state.links || {};
441
+ if (Array.isArray(links.dependsOn)) {
442
+ for (const depId of links.dependsOn) {
443
+ if (!taskIds.has(depId)) issue(`${task.id}: links.dependsOn references non-existent task "${depId}"`);
444
+ }
445
+ }
446
+ if (Array.isArray(links.blocks)) {
447
+ for (const blockId of links.blocks) {
448
+ if (!taskIds.has(blockId)) issue(`${task.id}: links.blocks references non-existent task "${blockId}"`);
449
+ }
450
+ }
451
+ if (Array.isArray(task.state.workItems)) {
452
+ const runningItems = task.state.workItems.filter(wi => wi && normalizeStatus(wi.status) === 'running');
453
+ if (runningItems.length > 0 && (!Array.isArray(task.state.dispatchLedger) || task.state.dispatchLedger.length === 0)) {
454
+ issue(`${task.id}: workItems has ${runningItems.length} running item(s) but no dispatchLedger entries`);
455
+ }
456
+ }
457
+
458
+ const queues = normalizeQueues(task.state);
459
+ const queueMembership = new Map();
460
+ for (const queueName of ['ready', 'running', 'blocked', 'done']) {
461
+ for (const item of queues[queueName]) {
462
+ const itemId = typeof item === 'string' ? item : (item && typeof item.id === 'string' ? item.id : null);
463
+ if (!itemId) {
464
+ issue(`${task.id}: queues.${queueName} contains an item without an id`, true);
465
+ continue;
466
+ }
467
+ const priorQueue = queueMembership.get(itemId);
468
+ if (priorQueue) {
469
+ issue(`${task.id}: queue item "${itemId}" appears in both ${priorQueue} and ${queueName}`, true);
470
+ } else {
471
+ queueMembership.set(itemId, queueName);
472
+ }
473
+ }
474
+ }
475
+ const status = normalizeStatus(task.state.status);
476
+ const phase = normalizePhase(task.state.phase);
477
+ if ((SAFE_ARCHIVE_STATUSES.has(status) || SAFE_ARCHIVE_STATUSES.has(phase)) &&
478
+ (queues.ready.length > 0 || queues.running.length > 0 || queues.blocked.length > 0)) {
479
+ issue(`${task.id}: closed task has non-empty ready/running/blocked queues`, true);
480
+ }
401
481
  }
402
482
 
403
483
  const activeStateTasks = tasks.filter(task => normalizeStatus(task.state?.status) === 'active');
@@ -405,7 +485,7 @@ function validateState({ strict = false } = {}) {
405
485
  issue(`Multiple STATE.json files are active: ${activeStateTasks.map(task => task.id).join(', ')}`, true);
406
486
  }
407
487
  if (tasks.length > OUTER_TASK_CAP) {
408
- issue(`Harness/tasks/ has ${tasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); run node Harness/scripts/task-state.mjs archive --apply`);
488
+ issue(`Harness/tasks/ has ${tasks.length} outer task capsules (cap ${OUTER_TASK_CAP}); remind the user to run $wf-task-archive when they want to archive completed tasks`);
409
489
  }
410
490
 
411
491
  return {
@@ -688,8 +768,37 @@ function applyOperations(operations) {
688
768
  }
689
769
 
690
770
  function runList() {
691
- const validation = validateState();
692
- finish({ ...validation, command: 'list', ok: true }, 0);
771
+ const { rootProgress, tasks } = collectTasks();
772
+
773
+ const expandedTasks = tasks.map(task => {
774
+ const state = task.state || {};
775
+ const links = state.links || {};
776
+ const status = normalizeStatus(state.status) || task.status;
777
+ return {
778
+ id: task.id,
779
+ status,
780
+ phase: normalizePhase(state.phase) || task.phase,
781
+ rootPhase: task.rootPhase,
782
+ progressPhase: task.progressPhase,
783
+ dependsOn: Array.isArray(links.dependsOn) ? links.dependsOn : [],
784
+ blocks: Array.isArray(links.blocks) ? links.blocks : [],
785
+ statusDisplay: status || '-',
786
+ openTasks: OPEN_TASK_STATUSES.has(status),
787
+ nextAction: state.nextAction || null,
788
+ archive: (state ? archiveEligibility(task, rootProgress.activeTask) : { ok: false, reason: 'no state' }),
789
+ };
790
+ });
791
+
792
+ const payload = {
793
+ ok: true,
794
+ command: 'list',
795
+ taskCount: expandedTasks.length,
796
+ activeTask: rootProgress.activeTask,
797
+ tasks: expandedTasks,
798
+ errors: [],
799
+ warnings: [],
800
+ };
801
+ finish(payload, 0);
693
802
  }
694
803
 
695
804
  function runValidate() {
@@ -873,6 +982,285 @@ function runArchive() {
873
982
  finish(plan, 0);
874
983
  }
875
984
 
985
+ function readTemplateState() {
986
+ const templatePath = path.join(tasksDir, '_template', 'STATE.json');
987
+ if (!fs.existsSync(templatePath)) return null;
988
+ try {
989
+ return JSON.parse(fs.readFileSync(templatePath, 'utf8'));
990
+ } catch {
991
+ return null;
992
+ }
993
+ }
994
+
995
+ function generateTaskId(title, note, context) {
996
+ // lowercase first, then sanitize — ensures uppercase letters survive as lowercase
997
+ const raw = (title || note || context || 'record')
998
+ .toLowerCase()
999
+ .replace(/[^a-z0-9]+/g, '-')
1000
+ .replace(/^-+|-+$/g, '');
1001
+ const parts = raw.split('-').filter(Boolean).slice(0, 3).join('-');
1002
+ // fallback: empty slug (pure CJK/emoji) defaults to 'task'
1003
+ const slug = parts.slice(0, 25) || 'task';
1004
+ const now = new Date();
1005
+ const suffix = `${now.getFullYear().toString().slice(2)}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}`;
1006
+ return `task-${slug}-${suffix}`;
1007
+ }
1008
+
1009
+ function findMatchingTask(title, note, context) {
1010
+ const { tasks } = collectTasks();
1011
+ const openTasks = tasks.filter(t => OPEN_TASK_STATUSES.has(normalizeStatus(t.state?.status) || t.status));
1012
+
1013
+ // Step 1: title match (highest priority)
1014
+ if (title) {
1015
+ const slugKey = title.replace(/\s+/g, '-').toLowerCase();
1016
+ const matches = openTasks.filter(t => {
1017
+ if (t.id === `task-${slugKey}`) return true;
1018
+ if (t.id.includes(slugKey)) return true;
1019
+ const goal = (t.rootRow?.goal || t.state?.nextAction || '').toLowerCase();
1020
+ return slugKey.split('-').every(part => goal.includes(part));
1021
+ });
1022
+ if (matches.length === 1) return { matched: matches[0], candidates: null };
1023
+ if (matches.length > 1) return { matched: null, candidates: matches };
1024
+ }
1025
+
1026
+ // Step 2: note/context overlap match
1027
+ if (note || context) {
1028
+ const query = ((note || '') + ' ' + (context || '')).toLowerCase();
1029
+ const terms = query.split(/\s+/).filter(t => t.length > 3);
1030
+ if (terms.length === 0) return { matched: null, candidates: null };
1031
+ const scored = openTasks.map(t => {
1032
+ const fields = [t.state?.nextAction || '', t.state?.goal || '', t.rootRow?.goal || ''].join(' ').toLowerCase();
1033
+ const score = terms.filter(term => fields.includes(term)).length;
1034
+ return { task: t, score };
1035
+ }).filter(s => s.score > 0);
1036
+ scored.sort((a, b) => b.score - a.score);
1037
+ if (scored.length === 0) return { matched: null, candidates: null };
1038
+ const bestScore = scored[0].score;
1039
+ const best = scored.filter(s => s.score === bestScore);
1040
+ if (best.length === 1 && bestScore >= Math.max(2, Math.ceil(terms.length * 0.5))) {
1041
+ return { matched: best[0].task, candidates: null };
1042
+ }
1043
+ if (best.length > 1) {
1044
+ return { matched: null, candidates: best.map(s => s.task) };
1045
+ }
1046
+ }
1047
+
1048
+ return { matched: null, candidates: null };
1049
+ }
1050
+
1051
+ function runRecord() {
1052
+ const title = flagValue('--title');
1053
+ const note = flagValue('--note');
1054
+ const context = flagValue('--context');
1055
+ const forceNew = hasFlag('--new');
1056
+
1057
+ let taskId = findTaskIdArg(1);
1058
+ let createOrResume = false;
1059
+
1060
+ if (!taskId) {
1061
+ if (!title && !note && !context) {
1062
+ finish({ ok: false, command: 'record', errors: ['requires <task-id> or --title/--note/--context'], warnings: [] }, 1);
1063
+ return;
1064
+ }
1065
+ if (!forceNew) {
1066
+ const { matched, candidates } = findMatchingTask(title, note, context);
1067
+ if (matched) {
1068
+ taskId = matched.id;
1069
+ } else if (candidates && candidates.length > 0) {
1070
+ finish({ ok: false, command: 'record', errors: [`Ambiguous match: ${candidates.map(t => t.id).join(', ')}. Use --new to force new, or specify --title more precisely.`], warnings: [] }, 1);
1071
+ return;
1072
+ }
1073
+ }
1074
+ if (!taskId) {
1075
+ taskId = generateTaskId(title, note, context);
1076
+ createOrResume = true;
1077
+ }
1078
+
1079
+ // --new uniqueness: probe existing task dirs, append incrementing suffix on collision
1080
+ if (forceNew && createOrResume) {
1081
+ const existingNames = new Set(listOuterTaskNames());
1082
+ let counter = 1;
1083
+ const baseId = taskId;
1084
+ while (existingNames.has(taskId)) {
1085
+ counter++;
1086
+ taskId = `${baseId}-${counter}`;
1087
+ }
1088
+ // if suffix made it invalid, fall back to base id
1089
+ try {
1090
+ ensureValidTaskId(taskId);
1091
+ } catch {
1092
+ taskId = baseId;
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ if (!taskId) {
1098
+ finish({ ok: false, command: 'record', errors: ['record requires a <task-id>'], warnings: [] }, 1);
1099
+ return;
1100
+ }
1101
+ try {
1102
+ ensureValidTaskId(taskId);
1103
+ } catch (err) {
1104
+ finish({ ok: false, command: 'record', errors: [err.message], warnings: [] }, 1);
1105
+ return;
1106
+ }
1107
+
1108
+ const isCreate = hasFlag('--create') || createOrResume;
1109
+ const isDryRun = hasFlag('--dry-run');
1110
+ const isApply = hasFlag('--apply');
1111
+ const actuallyApply = isApply && !isDryRun;
1112
+
1113
+ const existing = readState(taskId);
1114
+ if (!existing.state && !isCreate) {
1115
+ finish({ ok: false, command: 'record', errors: [`Task "${taskId}" not found; use --create to create`], warnings: [] }, 1);
1116
+ return;
1117
+ }
1118
+
1119
+ const text = flagValue('--text');
1120
+ const statusRaw = flagValue('--status');
1121
+ const modeRaw = flagValue('--mode');
1122
+
1123
+ if (!existing.state && isCreate) {
1124
+ const now = new Date().toISOString();
1125
+ if (statusRaw) {
1126
+ const ns = normalizeStatus(statusRaw);
1127
+ if (!ns) {
1128
+ finish({ ok: false, command: 'record', errors: [`Invalid status "${statusRaw}". Valid: ${[...VALID_STATUSES].join(', ')}`], warnings: [] }, 1);
1129
+ return;
1130
+ }
1131
+ }
1132
+ const status = statusRaw ? normalizeStatus(statusRaw) : 'pending';
1133
+ const mode = modeRaw || 'direct';
1134
+ const newState = defaultState(taskId, status, 'intake', now);
1135
+ newState.mode = mode;
1136
+ if (text) newState.nextAction = text;
1137
+ if (modeRaw) {
1138
+ const normalizedMode = normalizeMode(modeRaw);
1139
+ if (!normalizedMode) {
1140
+ finish({ ok: false, command: 'record', errors: [`Invalid mode "${modeRaw}". Valid: ${[...VALID_MODES].join(', ')}`], warnings: [] }, 1);
1141
+ return;
1142
+ }
1143
+ newState.mode = normalizedMode;
1144
+ }
1145
+
1146
+ const templateState = readTemplateState();
1147
+ if (templateState) {
1148
+ if (Array.isArray(templateState.acceptance)) newState.acceptance = [...templateState.acceptance];
1149
+ if (templateState.links) newState.links = JSON.parse(JSON.stringify(templateState.links));
1150
+ }
1151
+
1152
+ if (actuallyApply) {
1153
+ const taskDir = safeTaskPath(taskId);
1154
+ fs.mkdirSync(taskDir, { recursive: true });
1155
+ writeJsonAtomic(path.join(taskDir, 'STATE.json'), newState);
1156
+ const progressFile = path.join(taskDir, 'PROGRESS.md');
1157
+ writeTextAtomic(progressFile, renderTaskProgress(readText(progressFile), taskId, newState));
1158
+
1159
+ const planFile = path.join(taskDir, 'PLAN.md');
1160
+ if (!fs.existsSync(planFile)) {
1161
+ writeTextAtomic(planFile, `# ${taskId} - PLAN\n\n## Goal\n\n${text || taskTitle(taskId)}\n\n## Scope\n\n.\n\n## Decisions\n\n.\n\n## Acceptance\n\n.\n`);
1162
+ }
1163
+
1164
+ const rootText = readText(progressPath);
1165
+ const parsed = parseRootProgress();
1166
+ const newRow = { id: taskId, goal: text || taskTitle(taskId), phase: displayPhase('intake'), closed: '-' };
1167
+ parsed.rows.push(newRow);
1168
+ const rows = parsed.rows.map(r => ({ id: r.id, goal: r.goal, phase: r.phase, closed: r.closed }));
1169
+ const newRoot = renderRootProgress(rootText, parsed.activeTask, rows);
1170
+ writeTextAtomic(progressPath, newRoot);
1171
+ }
1172
+
1173
+ finish({
1174
+ ok: true,
1175
+ command: 'record',
1176
+ action: 'created',
1177
+ dryRun: !actuallyApply,
1178
+ taskId,
1179
+ state: newState,
1180
+ warnings: [],
1181
+ }, 0);
1182
+ return;
1183
+ }
1184
+
1185
+ if (existing.state) {
1186
+ const now = new Date().toISOString();
1187
+ const updated = { ...existing.state };
1188
+ updated.updatedAt = now;
1189
+ if (statusRaw) {
1190
+ const ns = normalizeStatus(statusRaw);
1191
+ if (!ns) {
1192
+ finish({ ok: false, command: 'record', errors: [`Invalid status "${statusRaw}". Valid: ${[...VALID_STATUSES].join(', ')}`], warnings: [] }, 1);
1193
+ return;
1194
+ }
1195
+ updated.status = ns;
1196
+ }
1197
+ if (modeRaw) {
1198
+ const normalizedMode = normalizeMode(modeRaw);
1199
+ if (!normalizedMode) {
1200
+ finish({ ok: false, command: 'record', errors: [`Invalid mode "${modeRaw}". Valid: ${[...VALID_MODES].join(', ')}`], warnings: [] }, 1);
1201
+ return;
1202
+ }
1203
+ updated.mode = normalizedMode;
1204
+ }
1205
+ if (text) updated.nextAction = text;
1206
+
1207
+ if (actuallyApply) {
1208
+ writeJsonAtomic(existing.path, updated);
1209
+ }
1210
+
1211
+ finish({
1212
+ ok: true,
1213
+ command: 'record',
1214
+ action: 'updated',
1215
+ dryRun: !actuallyApply,
1216
+ taskId,
1217
+ state: updated,
1218
+ warnings: [],
1219
+ }, 0);
1220
+ return;
1221
+ }
1222
+ }
1223
+
1224
+ function runOpen() {
1225
+ const { rootProgress, tasks } = collectTasks();
1226
+
1227
+ const openTasks = tasks.filter(task => {
1228
+ const status = normalizeStatus(task.state?.status) || task.status;
1229
+ return OPEN_TASK_STATUSES.has(status);
1230
+ }).map(task => {
1231
+ const state = task.state || {};
1232
+ const links = state.links || {};
1233
+ const status = normalizeStatus(state.status) || task.status;
1234
+ const dependsOn = Array.isArray(links.dependsOn) ? links.dependsOn : [];
1235
+ const openDepTasks = dependsOn.filter(depId => {
1236
+ const depTask = tasks.find(t => t.id === depId);
1237
+ if (!depTask) return false;
1238
+ const depStatus = normalizeStatus(depTask.state?.status) || depTask.status;
1239
+ return OPEN_TASK_STATUSES.has(depStatus);
1240
+ });
1241
+ return {
1242
+ id: task.id,
1243
+ status,
1244
+ phase: normalizePhase(state.phase) || task.phase,
1245
+ dependsOn,
1246
+ blocks: Array.isArray(links.blocks) ? links.blocks : [],
1247
+ blockedByOpenDeps: openDepTasks,
1248
+ nextAction: state.nextAction || null,
1249
+ statusDisplay: status || '-',
1250
+ openTasks: true,
1251
+ };
1252
+ });
1253
+
1254
+ finish({
1255
+ ok: true,
1256
+ command: 'open',
1257
+ taskCount: openTasks.length,
1258
+ tasks: openTasks,
1259
+ errors: [],
1260
+ warnings: [],
1261
+ }, 0);
1262
+ }
1263
+
876
1264
  if (command === 'help' || hasFlag('--help') || hasFlag('-h')) usage();
877
1265
  if (command === 'list') runList();
878
1266
  if (command === 'validate') runValidate();
@@ -880,6 +1268,8 @@ if (command === 'reconcile') runReconcile();
880
1268
  if (command === 'set-active') runSetActive();
881
1269
  if (command === 'transition') runTransition();
882
1270
  if (command === 'archive') runArchive();
1271
+ if (command === 'record') runRecord();
1272
+ if (command === 'open') runOpen();
883
1273
 
884
1274
  finish({
885
1275
  ok: false,