@shirlytaylor73/superharness 1.5.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.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +202 -0
  3. package/bin/lib/codex-installer.js +228 -0
  4. package/bin/lib/interactive-select.js +96 -0
  5. package/bin/superharness.js +67 -0
  6. package/package.json +52 -0
  7. package/plugins/superharness/.claude-plugin/plugin.json +19 -0
  8. package/plugins/superharness/.codex-plugin/plugin.json +31 -0
  9. package/plugins/superharness/.mcp.json +9 -0
  10. package/plugins/superharness/CODE_OF_CONDUCT.md +79 -0
  11. package/plugins/superharness/LICENSE +21 -0
  12. package/plugins/superharness/README.md +57 -0
  13. package/plugins/superharness/agents/code-reviewer.md +48 -0
  14. package/plugins/superharness/archived-skills/using-superpowers/SKILL.md +140 -0
  15. package/plugins/superharness/archived-skills/using-superpowers/references/codex-tools.md +25 -0
  16. package/plugins/superharness/archived-skills/using-superpowers/references/copilot-tools.md +52 -0
  17. package/plugins/superharness/archived-skills/using-superpowers/references/gemini-tools.md +33 -0
  18. package/plugins/superharness/archived-skills/using-superpowers/references/hermes-tools.md +44 -0
  19. package/plugins/superharness/commands/free.md +6 -0
  20. package/plugins/superharness/commands/rollback.md +30 -0
  21. package/plugins/superharness/commands-codex/free.md +29 -0
  22. package/plugins/superharness/commands-codex/rollback.md +33 -0
  23. package/plugins/superharness/hooks/hooks-codex.json +50 -0
  24. package/plugins/superharness/hooks/hooks.json +50 -0
  25. package/plugins/superharness/hooks/lib/free-mode-check.mjs +27 -0
  26. package/plugins/superharness/hooks/run-hook.cmd +58 -0
  27. package/plugins/superharness/hooks/workflow-context +4 -0
  28. package/plugins/superharness/hooks/workflow-context.mjs +184 -0
  29. package/plugins/superharness/hooks/workflow-post-transition +4 -0
  30. package/plugins/superharness/hooks/workflow-post-transition.mjs +89 -0
  31. package/plugins/superharness/hooks/workflow-pre-tool-use +4 -0
  32. package/plugins/superharness/hooks/workflow-pre-tool-use.mjs +97 -0
  33. package/plugins/superharness/hooks/workflow-stop +4 -0
  34. package/plugins/superharness/hooks/workflow-stop.mjs +136 -0
  35. package/plugins/superharness/scripts/rollback.mjs +86 -0
  36. package/plugins/superharness/scripts/set-free-mode.mjs +77 -0
  37. package/plugins/superharness/skills/brainstorming/SKILL.md +182 -0
  38. package/plugins/superharness/skills/brainstorming/scripts/frame-template.html +214 -0
  39. package/plugins/superharness/skills/brainstorming/scripts/helper.js +88 -0
  40. package/plugins/superharness/skills/brainstorming/scripts/server.cjs +338 -0
  41. package/plugins/superharness/skills/brainstorming/scripts/start-server.sh +153 -0
  42. package/plugins/superharness/skills/brainstorming/scripts/stop-server.sh +55 -0
  43. package/plugins/superharness/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  44. package/plugins/superharness/skills/brainstorming/visual-companion.md +286 -0
  45. package/plugins/superharness/skills/chinese-code-review/SKILL.md +277 -0
  46. package/plugins/superharness/skills/chinese-commit-conventions/SKILL.md +364 -0
  47. package/plugins/superharness/skills/chinese-documentation/SKILL.md +448 -0
  48. package/plugins/superharness/skills/chinese-git-workflow/SKILL.md +547 -0
  49. package/plugins/superharness/skills/dispatching-parallel-agents/SKILL.md +186 -0
  50. package/plugins/superharness/skills/exploration/SKILL.md +197 -0
  51. package/plugins/superharness/skills/finishing/SKILL.md +200 -0
  52. package/plugins/superharness/skills/intake/SKILL.md +134 -0
  53. package/plugins/superharness/skills/mcp-builder/SKILL.md +255 -0
  54. package/plugins/superharness/skills/parallel-execution/SKILL.md +368 -0
  55. package/plugins/superharness/skills/parallel-execution/implementer-prompt.md +144 -0
  56. package/plugins/superharness/skills/parallel-execution/spec-reviewer-prompt.md +84 -0
  57. package/plugins/superharness/skills/parallel-execution/wave-final-manual-qa-prompt.md +61 -0
  58. package/plugins/superharness/skills/parallel-execution/wave-final-quality-prompt.md +59 -0
  59. package/plugins/superharness/skills/parallel-execution/wave-final-scope-fidelity-prompt.md +69 -0
  60. package/plugins/superharness/skills/parallel-execution/wave-final-spec-prompt.md +56 -0
  61. package/plugins/superharness/skills/planning/SKILL.md +265 -0
  62. package/plugins/superharness/skills/planning/plan-document-reviewer-prompt.md +80 -0
  63. package/plugins/superharness/skills/receiving-code-review/SKILL.md +213 -0
  64. package/plugins/superharness/skills/requesting-code-review/SKILL.md +107 -0
  65. package/plugins/superharness/skills/requesting-code-review/code-reviewer.md +146 -0
  66. package/plugins/superharness/skills/serial-execution/SKILL.md +183 -0
  67. package/plugins/superharness/skills/systematic-debugging/CREATION-LOG.md +119 -0
  68. package/plugins/superharness/skills/systematic-debugging/SKILL.md +320 -0
  69. package/plugins/superharness/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  70. package/plugins/superharness/skills/systematic-debugging/condition-based-waiting.md +115 -0
  71. package/plugins/superharness/skills/systematic-debugging/defense-in-depth.md +122 -0
  72. package/plugins/superharness/skills/systematic-debugging/find-polluter.sh +63 -0
  73. package/plugins/superharness/skills/systematic-debugging/root-cause-tracing.md +169 -0
  74. package/plugins/superharness/skills/systematic-debugging/test-academic.md +14 -0
  75. package/plugins/superharness/skills/systematic-debugging/test-pressure-1.md +58 -0
  76. package/plugins/superharness/skills/systematic-debugging/test-pressure-2.md +68 -0
  77. package/plugins/superharness/skills/systematic-debugging/test-pressure-3.md +69 -0
  78. package/plugins/superharness/skills/test-driven-development/SKILL.md +371 -0
  79. package/plugins/superharness/skills/test-driven-development/testing-anti-patterns.md +299 -0
  80. package/plugins/superharness/skills/trivial/SKILL.md +118 -0
  81. package/plugins/superharness/skills/using-git-worktrees/SKILL.md +218 -0
  82. package/plugins/superharness/skills/verification/SKILL.md +139 -0
  83. package/plugins/superharness/skills/workflow-runner/SKILL.md +172 -0
  84. package/plugins/superharness/skills/writing-skills/SKILL.md +655 -0
  85. package/plugins/superharness/skills/writing-skills/anthropic-best-practices.md +1149 -0
  86. package/plugins/superharness/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  87. package/plugins/superharness/skills/writing-skills/graphviz-conventions.dot +172 -0
  88. package/plugins/superharness/skills/writing-skills/persuasion-principles.md +187 -0
  89. package/plugins/superharness/skills/writing-skills/render-graphs.js +168 -0
  90. package/plugins/superharness/skills/writing-skills/testing-skills-with-subagents.md +385 -0
  91. package/plugins/superharness/workflow/default-workflow.yaml +84 -0
  92. package/plugins/superharness/workflow-state-server/bootstrap.js +44 -0
  93. package/plugins/superharness/workflow-state-server/package-lock.json +2853 -0
  94. package/plugins/superharness/workflow-state-server/package.json +22 -0
  95. package/plugins/superharness/workflow-state-server/render-context.js +124 -0
  96. package/plugins/superharness/workflow-state-server/schema.sql +39 -0
  97. package/plugins/superharness/workflow-state-server/server.js +290 -0
  98. package/plugins/superharness/workflow-state-server/state.js +424 -0
  99. package/plugins/superharness/workflow-state-server/validate-workflow.js +165 -0
@@ -0,0 +1,424 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const IS_BUN = typeof process !== 'undefined' && !!process.versions?.bun;
6
+ const Database = IS_BUN
7
+ ? (await import('bun:sqlite')).Database
8
+ : (await import('better-sqlite3')).default;
9
+
10
+ const VALID_SOURCES = new Set(['hook', 'agent-tool', 'user-reset']);
11
+
12
+ function now() {
13
+ return Date.now();
14
+ }
15
+
16
+ function requireReason(reason) {
17
+ if (typeof reason !== 'string' || reason.trim() === '') {
18
+ throw new Error('reason must be a non-empty string');
19
+ }
20
+ return reason.trim();
21
+ }
22
+
23
+ function requireWorkspaceRoot(workspaceRoot) {
24
+ if (typeof workspaceRoot !== 'string' || workspaceRoot.trim() === '') {
25
+ throw new Error('workspaceRoot must be a non-empty string');
26
+ }
27
+ return path.resolve(workspaceRoot);
28
+ }
29
+
30
+ function requireWorkflowGraph(workflowGraph) {
31
+ if (!workflowGraph?.states || !workflowGraph?.entryState) {
32
+ throw new Error('workflowGraph is required');
33
+ }
34
+ return workflowGraph;
35
+ }
36
+
37
+ function activeSkillFor(workflowGraph, stateName) {
38
+ return workflowGraph.states.get(stateName)?.skill ?? null;
39
+ }
40
+
41
+ function statusFor(workflowGraph, stateName) {
42
+ return workflowGraph.states.get(stateName)?.terminal ? 'completed' : 'active';
43
+ }
44
+
45
+ function normalizeSource(source = 'agent-tool') {
46
+ if (!VALID_SOURCES.has(source)) {
47
+ throw new Error(`invalid source: ${source}`);
48
+ }
49
+ return source;
50
+ }
51
+
52
+ function readSchema() {
53
+ return fs.readFileSync(
54
+ path.join(path.dirname(fileURLToPath(import.meta.url)), 'schema.sql'),
55
+ 'utf8',
56
+ );
57
+ }
58
+
59
+ export function ensureTurnIdColumn(db) {
60
+ const columns = db.prepare("PRAGMA table_info(workflow_transition_log)").all();
61
+ const hasTurnId = columns.some((c) => c.name === 'turn_id');
62
+ if (!hasTurnId) {
63
+ db.exec("ALTER TABLE workflow_transition_log ADD COLUMN turn_id TEXT");
64
+ }
65
+ db.exec("CREATE INDEX IF NOT EXISTS idx_workflow_transition_turn ON workflow_transition_log(turn_id)");
66
+ }
67
+
68
+ export function ensureFreeModeColumns(db) {
69
+ const columns = db.prepare("PRAGMA table_info(workflow_state)").all();
70
+ const names = new Set(columns.map((c) => c.name));
71
+ if (!names.has('free_mode')) {
72
+ db.exec("ALTER TABLE workflow_state ADD COLUMN free_mode INTEGER NOT NULL DEFAULT 0");
73
+ }
74
+ if (!names.has('free_started_at')) {
75
+ db.exec("ALTER TABLE workflow_state ADD COLUMN free_started_at INTEGER");
76
+ }
77
+ }
78
+
79
+ export function readFreeMode(store, workspaceRoot) {
80
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
81
+ const row = store.prepare(
82
+ 'SELECT free_mode FROM workflow_state WHERE workspace_id = ?'
83
+ ).get(workspaceId);
84
+ return row?.free_mode === 1;
85
+ }
86
+
87
+ export function assertNotFreeMode(store, workspaceRoot) {
88
+ if (readFreeMode(store, workspaceRoot)) {
89
+ throw new Error('workspace is in free mode; /free off first');
90
+ }
91
+ }
92
+
93
+ export function createTurn(store, { workspaceRoot, turnId }) {
94
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
95
+ store.db.prepare(`
96
+ INSERT OR REPLACE INTO workflow_turn
97
+ (workspace_id, turn_id, block_count, stop_block_released, release_reason, created_at)
98
+ VALUES (?, ?, 0, 0, NULL, ?)
99
+ `).run(workspaceId, turnId, now());
100
+ }
101
+
102
+ export function getTurn(store, { workspaceRoot }) {
103
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
104
+ return store.db.prepare('SELECT * FROM workflow_turn WHERE workspace_id = ?').get(workspaceId) ?? null;
105
+ }
106
+
107
+ export function incrementBlockCount(store, { workspaceRoot }) {
108
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
109
+ store.db.prepare('UPDATE workflow_turn SET block_count = block_count + 1 WHERE workspace_id = ?').run(workspaceId);
110
+ }
111
+
112
+ export function releaseTurnBlock(store, { workspaceRoot, reason }) {
113
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
114
+ store.db.prepare('UPDATE workflow_turn SET stop_block_released = 1, release_reason = ? WHERE workspace_id = ?').run(reason, workspaceId);
115
+ }
116
+
117
+ function normalizeRow(row) {
118
+ if (!row) return null;
119
+ return {
120
+ workspace_id: row.workspace_id,
121
+ state: row.state,
122
+ status: row.status,
123
+ previous_state: row.previous_state ?? null,
124
+ active_skill: row.active_skill ?? null,
125
+ task_summary: row.task_summary ?? null,
126
+ failure_summary: row.failure_summary ?? null,
127
+ updated_at: row.updated_at,
128
+ free_mode: row.free_mode === 1,
129
+ free_started_at: row.free_started_at ?? null,
130
+ };
131
+ }
132
+
133
+ function getRow(store, workspaceId) {
134
+ return normalizeRow(
135
+ store.prepare('SELECT * FROM workflow_state WHERE workspace_id = ?').get(workspaceId),
136
+ );
137
+ }
138
+
139
+ function insertLog(store, {
140
+ workspaceId,
141
+ fromState,
142
+ toState,
143
+ previousState,
144
+ reason,
145
+ source,
146
+ createdAt = now(),
147
+ }) {
148
+ const turnRow = store.prepare('SELECT turn_id FROM workflow_turn WHERE workspace_id = ?').get(workspaceId);
149
+ const turnId = turnRow?.turn_id ?? null;
150
+ store.prepare(`
151
+ INSERT INTO workflow_transition_log (
152
+ workspace_id, from_state, to_state, previous_state, reason, source, turn_id, created_at
153
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
154
+ `).run(workspaceId, fromState ?? null, toState, previousState ?? null, reason, source, turnId, createdAt);
155
+ }
156
+
157
+ function upsertState(store, {
158
+ workspaceId,
159
+ workflowGraph,
160
+ state,
161
+ previousState = null,
162
+ taskSummary = null,
163
+ failureSummary = null,
164
+ updatedAt = now(),
165
+ }) {
166
+ store.prepare(`
167
+ INSERT INTO workflow_state (
168
+ workspace_id, state, status, previous_state, active_skill,
169
+ task_summary, failure_summary, updated_at
170
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
171
+ ON CONFLICT(workspace_id) DO UPDATE SET
172
+ state = excluded.state,
173
+ status = excluded.status,
174
+ previous_state = excluded.previous_state,
175
+ active_skill = excluded.active_skill,
176
+ task_summary = excluded.task_summary,
177
+ failure_summary = excluded.failure_summary,
178
+ updated_at = excluded.updated_at
179
+ `).run(
180
+ workspaceId,
181
+ state,
182
+ statusFor(workflowGraph, state),
183
+ previousState,
184
+ activeSkillFor(workflowGraph, state),
185
+ taskSummary,
186
+ failureSummary,
187
+ updatedAt,
188
+ );
189
+ return getRow(store, workspaceId);
190
+ }
191
+
192
+ function assertTransitionAllowed(workflowGraph, fromState, toState) {
193
+ const fromNode = workflowGraph.states.get(fromState);
194
+ if (!fromNode) {
195
+ throw new Error(`unknown from_state: ${fromState}`);
196
+ }
197
+ if (!fromNode.next.includes(toState)) {
198
+ throw new Error(`transition ${fromState} -> ${toState} is not allowed`);
199
+ }
200
+ }
201
+
202
+ const LEGACY_STATE_MAP = {
203
+ done: 'intake',
204
+ execution_choice: 'planning',
205
+ };
206
+
207
+ function migrateLegacyState(store, workspaceId, current) {
208
+ if (!current) return current;
209
+ const target = LEGACY_STATE_MAP[current.state];
210
+ if (!target) return current;
211
+
212
+ const updatedAt = now();
213
+ store.prepare(`
214
+ UPDATE workflow_state
215
+ SET state = ?,
216
+ status = 'active',
217
+ previous_state = NULL,
218
+ active_skill = NULL,
219
+ updated_at = ?
220
+ WHERE workspace_id = ?
221
+ `).run(target, updatedAt, workspaceId);
222
+
223
+ insertLog(store, {
224
+ workspaceId,
225
+ fromState: current.state,
226
+ toState: target,
227
+ previousState: null,
228
+ reason: `legacy migration: ${current.state} -> ${target} (state removed in v3 state machine)`,
229
+ source: 'user-reset',
230
+ createdAt: updatedAt,
231
+ });
232
+
233
+ return getRow(store, workspaceId);
234
+ }
235
+
236
+ export { migrateLegacyState };
237
+
238
+ export function resolveWorkflowDbPath({ workspaceRoot } = {}) {
239
+ if (process.env.SUPERHARNESS_WORKFLOW_STATE_DB) {
240
+ return process.env.SUPERHARNESS_WORKFLOW_STATE_DB;
241
+ }
242
+ return path.join(requireWorkspaceRoot(workspaceRoot ?? process.cwd()), '.superharness', 'workflow-state.db');
243
+ }
244
+
245
+ export function openWorkflowStateStore({ dbPath, mode } = {}) {
246
+ const resolvedDbPath = mode === 'memory' ? ':memory:' : path.resolve(dbPath ?? resolveWorkflowDbPath());
247
+ if (resolvedDbPath !== ':memory:') {
248
+ fs.mkdirSync(path.dirname(resolvedDbPath), { recursive: true });
249
+ }
250
+
251
+ const db = new Database(resolvedDbPath);
252
+ const store = {
253
+ db,
254
+ path: resolvedDbPath,
255
+ exec(sql) {
256
+ return db.exec(sql);
257
+ },
258
+ prepare(sql) {
259
+ return typeof db.prepare === 'function' ? db.prepare(sql) : db.query(sql);
260
+ },
261
+ close() {
262
+ return db.close();
263
+ },
264
+ };
265
+
266
+ if (resolvedDbPath !== ':memory:') {
267
+ store.exec('PRAGMA journal_mode = WAL');
268
+ }
269
+ store.exec('PRAGMA foreign_keys = ON');
270
+ store.exec(readSchema());
271
+ ensureTurnIdColumn(store);
272
+ ensureFreeModeColumns(store);
273
+
274
+ return store;
275
+ }
276
+
277
+ export function initializeWorkflowState(store, {
278
+ workspaceRoot,
279
+ workflowGraph,
280
+ reason = 'initialize workflow state',
281
+ } = {}) {
282
+ const graph = requireWorkflowGraph(workflowGraph);
283
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
284
+ const existing = getRow(store, workspaceId);
285
+ if (existing) {
286
+ return migrateLegacyState(store, workspaceId, existing) ?? existing;
287
+ }
288
+
289
+ const trimmedReason = requireReason(reason);
290
+ const state = upsertState(store, {
291
+ workspaceId,
292
+ workflowGraph: graph,
293
+ state: graph.entryState,
294
+ });
295
+ insertLog(store, {
296
+ workspaceId,
297
+ fromState: null,
298
+ toState: graph.entryState,
299
+ previousState: null,
300
+ reason: trimmedReason,
301
+ source: 'hook',
302
+ });
303
+ return state;
304
+ }
305
+
306
+ export function getWorkflowState(store, { workspaceRoot, workflowGraph } = {}) {
307
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
308
+ const existing = getRow(store, workspaceId);
309
+ if (existing) {
310
+ return migrateLegacyState(store, workspaceId, existing) ?? existing;
311
+ }
312
+ if (!workflowGraph) {
313
+ return null;
314
+ }
315
+ return initializeWorkflowState(store, {
316
+ workspaceRoot,
317
+ workflowGraph,
318
+ reason: 'initialize workflow state',
319
+ });
320
+ }
321
+
322
+ export function classifyRequest(store, {
323
+ workspaceRoot,
324
+ workflowGraph,
325
+ task_summary = null,
326
+ failure_summary = null,
327
+ reason,
328
+ } = {}) {
329
+ assertNotFreeMode(store, workspaceRoot);
330
+ const graph = requireWorkflowGraph(workflowGraph);
331
+ const trimmedReason = requireReason(reason);
332
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
333
+ const current = getWorkflowState(store, { workspaceRoot, workflowGraph: graph });
334
+
335
+ const updated = upsertState(store, {
336
+ workspaceId,
337
+ workflowGraph: graph,
338
+ state: current.state,
339
+ previousState: current.previous_state,
340
+ taskSummary: task_summary,
341
+ failureSummary: failure_summary,
342
+ });
343
+ insertLog(store, {
344
+ workspaceId,
345
+ fromState: current.state,
346
+ toState: current.state,
347
+ previousState: current.previous_state,
348
+ reason: trimmedReason,
349
+ source: 'agent-tool',
350
+ });
351
+ return updated;
352
+ }
353
+
354
+ export function transitionWorkflowState(store, {
355
+ workspaceRoot,
356
+ workflowGraph,
357
+ from_state,
358
+ to_state,
359
+ previous_state = null,
360
+ reason,
361
+ source = 'agent-tool',
362
+ } = {}) {
363
+ assertNotFreeMode(store, workspaceRoot);
364
+ const graph = requireWorkflowGraph(workflowGraph);
365
+ const trimmedReason = requireReason(reason);
366
+ const normalizedSource = normalizeSource(source);
367
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
368
+ const current = getWorkflowState(store, { workspaceRoot, workflowGraph: graph });
369
+
370
+ if (from_state !== current.state) {
371
+ throw new Error(`from_state ${from_state} does not match current state ${current.state}`);
372
+ }
373
+
374
+ let target = to_state;
375
+ if (target === 'previous_state') {
376
+ if (current.state !== 'systematic_debugging') {
377
+ throw new Error('previous_state transition is only valid from systematic_debugging');
378
+ }
379
+ assertTransitionAllowed(graph, current.state, 'previous_state');
380
+ if (!current.previous_state) {
381
+ throw new Error('previous_state transition has no recorded previous_state');
382
+ }
383
+ target = current.previous_state;
384
+ } else {
385
+ assertTransitionAllowed(graph, current.state, target);
386
+ }
387
+
388
+ if (!graph.states.has(target)) {
389
+ throw new Error(`unknown to_state: ${target}`);
390
+ }
391
+
392
+ const nextPreviousState = target === 'systematic_debugging'
393
+ ? (previous_state || current.state)
394
+ : null;
395
+
396
+ const updated = upsertState(store, {
397
+ workspaceId,
398
+ workflowGraph: graph,
399
+ state: target,
400
+ previousState: nextPreviousState,
401
+ taskSummary: current.task_summary,
402
+ failureSummary: current.failure_summary,
403
+ });
404
+ insertLog(store, {
405
+ workspaceId,
406
+ fromState: current.state,
407
+ toState: target,
408
+ previousState: nextPreviousState,
409
+ reason: trimmedReason,
410
+ source: normalizedSource,
411
+ });
412
+ return updated;
413
+ }
414
+
415
+ export function listWorkflowHistory(store, { workspaceRoot } = {}) {
416
+ const workspaceId = requireWorkspaceRoot(workspaceRoot);
417
+ return store.prepare(`
418
+ SELECT id, workspace_id, from_state, to_state, previous_state, reason, source, created_at
419
+ FROM workflow_transition_log
420
+ WHERE workspace_id = ?
421
+ ORDER BY id ASC
422
+ `).all(workspaceId);
423
+ }
424
+
@@ -0,0 +1,165 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import YAML from 'yaml';
5
+
6
+ const SPECIAL_TARGETS = new Set(['previous_state']);
7
+
8
+ function fail(message) {
9
+ throw new Error(`invalid workflow config: ${message}`);
10
+ }
11
+
12
+ function isObject(value) {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+
16
+ function normalizeStringList(value, field) {
17
+ if (!Array.isArray(value)) {
18
+ fail(`${field} must be an array`);
19
+ }
20
+ for (const item of value) {
21
+ if (typeof item !== 'string' || item.trim() === '') {
22
+ fail(`${field} must contain only non-empty strings`);
23
+ }
24
+ }
25
+ return value;
26
+ }
27
+
28
+ function defaultPluginRoot() {
29
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
30
+ }
31
+
32
+ export function loadWorkflowConfig({ pluginRoot, workspaceRoot } = {}) {
33
+ const resolvedPluginRoot = pluginRoot ? path.resolve(pluginRoot) : defaultPluginRoot();
34
+ const candidates = [];
35
+
36
+ if (workspaceRoot) {
37
+ candidates.push(path.join(path.resolve(workspaceRoot), '.superharness', 'workflow.yaml'));
38
+ }
39
+ candidates.push(path.join(resolvedPluginRoot, 'workflow', 'default-workflow.yaml'));
40
+
41
+ const configPath = candidates.find((candidate) => fs.existsSync(candidate));
42
+ if (!configPath) {
43
+ fail(`workflow file not found in ${candidates.join(', ')}`);
44
+ }
45
+
46
+ const content = fs.readFileSync(configPath, 'utf8');
47
+ const parsed = YAML.parse(content);
48
+ if (!isObject(parsed)) {
49
+ fail(`${configPath} must contain a workflow object`);
50
+ }
51
+ return parsed;
52
+ }
53
+
54
+ export function validateWorkflowConfig(config, { installedSkills } = {}) {
55
+ if (!isObject(config)) {
56
+ fail('root must be an object');
57
+ }
58
+ if (config.version !== 1) {
59
+ fail('version must be 1');
60
+ }
61
+ if (typeof config.entryState !== 'string' || config.entryState.trim() === '') {
62
+ fail('entryState must be a non-empty string');
63
+ }
64
+ if (!isObject(config.states)) {
65
+ fail('states must be an object');
66
+ }
67
+ if (!Object.hasOwn(config.states, config.entryState)) {
68
+ fail(`entryState references missing state ${config.entryState}`);
69
+ }
70
+
71
+ // intake hard constraint (v3 state machine requires intake as entryState)
72
+ if (!Object.hasOwn(config.states, 'intake')) {
73
+ fail('intake state must exist (entry state for v3 state machine)');
74
+ }
75
+ if (config.entryState !== 'intake') {
76
+ fail(`entryState must be 'intake', got '${config.entryState}'`);
77
+ }
78
+
79
+ const terminalStates = normalizeStringList(config.terminalStates ?? [], 'terminalStates');
80
+ const terminalSet = new Set(terminalStates);
81
+
82
+ for (const terminalState of terminalSet) {
83
+ if (!Object.hasOwn(config.states, terminalState)) {
84
+ fail(`terminalStates references missing state ${terminalState}`);
85
+ }
86
+ }
87
+
88
+ for (const [stateName, state] of Object.entries(config.states)) {
89
+ if (!isObject(state)) {
90
+ fail(`${stateName} must be an object`);
91
+ }
92
+
93
+ const isTerminal = terminalSet.has(stateName) || state.type === 'terminal';
94
+ if (isTerminal) {
95
+ if (state.skill) {
96
+ fail(`${stateName} terminal state must not define skill`);
97
+ }
98
+ if (Array.isArray(state.next) && state.next.length > 0) {
99
+ fail(`${stateName} terminal state must not define next`);
100
+ }
101
+ continue;
102
+ }
103
+
104
+ if (!state.type || typeof state.type !== 'string') {
105
+ fail(`${stateName} type must be a non-empty string`);
106
+ }
107
+
108
+ const next = normalizeStringList(state.next ?? [], `${stateName}.next`);
109
+ if (next.length === 0) {
110
+ fail(`${stateName} next must contain at least one exit`);
111
+ }
112
+
113
+ if (state.skill !== undefined) {
114
+ if (typeof state.skill !== 'string' || state.skill.trim() === '') {
115
+ fail(`${stateName} skill must be a non-empty string`);
116
+ }
117
+ if (installedSkills && !installedSkills.has(state.skill)) {
118
+ fail(`${stateName} skill references missing skill ${state.skill}`);
119
+ }
120
+ }
121
+
122
+ for (const target of next) {
123
+ if (SPECIAL_TARGETS.has(target)) continue;
124
+ if (!Object.hasOwn(config.states, target)) {
125
+ fail(`${stateName} next references missing state ${target}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ const debugState = config.states.systematic_debugging;
131
+ if (debugState && !terminalSet.has('systematic_debugging')) {
132
+ const exits = Array.isArray(debugState.next) ? debugState.next : [];
133
+ if (!exits.includes('previous_state')) {
134
+ fail('systematic_debugging next must include previous_state');
135
+ }
136
+ }
137
+
138
+ return config;
139
+ }
140
+
141
+ export function buildWorkflowGraph(config, { installedSkills } = {}) {
142
+ validateWorkflowConfig(config, { installedSkills });
143
+
144
+ const terminalSet = new Set(config.terminalStates ?? []);
145
+ const states = new Map();
146
+ for (const [name, state] of Object.entries(config.states)) {
147
+ const terminal = terminalSet.has(name) || state.type === 'terminal';
148
+ const silentStopAllowed = state.silent_stop_allowed === true;
149
+ states.set(name, {
150
+ name,
151
+ type: state.type,
152
+ skill: state.skill ?? null,
153
+ next: terminal ? [] : [...state.next],
154
+ terminal,
155
+ silent_stop_allowed: silentStopAllowed,
156
+ });
157
+ }
158
+
159
+ return {
160
+ version: config.version,
161
+ entryState: config.entryState,
162
+ terminalStates: terminalSet,
163
+ states,
164
+ };
165
+ }