zouroboros-workflow 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 marlandoj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # zouroboros-workflow
2
+
3
+ > Spec-first development tools: interview, evaluation, unstuck, and autoloop
4
+
5
+ ## Features
6
+
7
+ - **Spec-First Interview** — Socratic interview process to clarify requirements before building
8
+ - **Three-Stage Evaluation** — Mechanical → Semantic → Consensus verification pipeline
9
+ - **Unstuck Lateral** — 5 lateral-thinking personas for breaking through stagnation
10
+ - **Autoloop** — Autonomous single-metric optimization with git-based improvement tracking
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install zouroboros-workflow
16
+ # or
17
+ pnpm add zouroboros-workflow
18
+ ```
19
+
20
+ ## CLI Usage
21
+
22
+ ### Spec-First Interview
23
+
24
+ ```bash
25
+ # Score ambiguity of a request
26
+ zouroboros-interview score --request "Make the site faster"
27
+
28
+ # Generate seed specification
29
+ zouroboros-interview seed --topic "Build a webhook retry system"
30
+
31
+ # Interactive interview guide
32
+ zouroboros-interview --topic "Your topic here"
33
+ ```
34
+
35
+ ### Three-Stage Evaluation
36
+
37
+ ```bash
38
+ # Run full evaluation pipeline
39
+ zouroboros-evaluate --seed seed.yaml --artifact ./src/
40
+
41
+ # Run only mechanical checks
42
+ zouroboros-evaluate --seed seed.yaml --artifact ./src/ --stage 1
43
+
44
+ # Self-test current workspace
45
+ zouroboros-evaluate --self-test
46
+ ```
47
+
48
+ ### Unstuck Lateral
49
+
50
+ ```bash
51
+ # Auto-select persona based on problem
52
+ zouroboros-unstuck --problem "The API keeps returning 403 errors"
53
+
54
+ # Use specific persona
55
+ zouroboros-unstuck --problem "This refactor touches 20 files" --persona architect
56
+
57
+ # List all personas
58
+ zouroboros-unstuck --list
59
+ ```
60
+
61
+ ### Autoloop
62
+
63
+ ```bash
64
+ # Validate program configuration
65
+ zouroboros-autoloop --program ./program.md --dry-run
66
+
67
+ # Run optimization loop
68
+ zouroboros-autoloop --program ./program.md
69
+
70
+ # Resume from existing branch
71
+ zouroboros-autoloop --program ./program.md --resume
72
+ ```
73
+
74
+ ## Programmatic API
75
+
76
+ ### Spec-First Interview
77
+
78
+ ```typescript
79
+ import { scoreAmbiguity, generateSeedTemplate } from 'zouroboros-workflow';
80
+
81
+ // Score ambiguity before building
82
+ const score = scoreAmbiguity("Build a webhook retry system");
83
+ console.log(score.ambiguity); // 0.35 (needs clarification)
84
+ console.log(score.assessment); // "NEEDS CLARIFICATION"
85
+
86
+ // Generate seed after interview
87
+ const seed = generateSeedTemplate("Webhook Retry System", "./interview-notes.md");
88
+ ```
89
+
90
+ ### Three-Stage Evaluation
91
+
92
+ ```typescript
93
+ import { parseSeed, runMechanicalChecks, evaluateSemantic } from 'zouroboros-workflow';
94
+
95
+ // Parse seed specification
96
+ const seed = parseSeed('./seed.yaml');
97
+
98
+ // Stage 1: Mechanical verification
99
+ const checks = runMechanicalChecks('./src/');
100
+ // [ { name: 'TypeScript compile', passed: true, detail: 'No errors' }, ... ]
101
+
102
+ // Stage 2: Semantic evaluation
103
+ const result = evaluateSemantic(seed, './src/');
104
+ console.log(result.overallScore); // 0.85
105
+ console.log(result.decision); // 'APPROVED' | 'NEEDS_WORK'
106
+ ```
107
+
108
+ ### Unstuck Lateral
109
+
110
+ ```typescript
111
+ import { autoSelectPersona, getStrategy } from 'zouroboros-workflow';
112
+
113
+ // Auto-select best persona
114
+ const selection = autoSelectPersona("I'm stuck on this API error");
115
+ console.log(selection.persona); // 'hacker'
116
+ console.log(selection.confidence); // 0.85
117
+
118
+ // Get strategy details
119
+ const strategy = getStrategy('hacker');
120
+ console.log(strategy.philosophy);
121
+ console.log(strategy.approach);
122
+ ```
123
+
124
+ ### Autoloop
125
+
126
+ ```typescript
127
+ import { parseProgram, initState, shouldContinue, runExperiment } from 'zouroboros-workflow';
128
+
129
+ // Parse program configuration
130
+ const config = parseProgram('./program.md');
131
+
132
+ // Initialize loop
133
+ const state = initState(config, 'autoloop/my-optimization');
134
+
135
+ // Run loop
136
+ while (shouldContinue(state, config).continue) {
137
+ // Propose change via AI executor
138
+ // Apply change to target file
139
+ // Git commit
140
+
141
+ // Run experiment
142
+ const result = await runExperiment(config, './');
143
+
144
+ // Decide keep or revert
145
+ if (result.metric !== null) {
146
+ // Update state, commit or revert
147
+ }
148
+ }
149
+ ```
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Core autoloop optimization engine
3
+ */
4
+ import type { ProgramConfig, LoopState, ExperimentRecord } from './types.js';
5
+ /**
6
+ * Initialize the loop state
7
+ */
8
+ export declare function initState(config: ProgramConfig, branch: string): LoopState;
9
+ /**
10
+ * Check if the loop should continue
11
+ */
12
+ export declare function shouldContinue(state: LoopState, config: ProgramConfig): {
13
+ continue: boolean;
14
+ reason?: string;
15
+ };
16
+ /**
17
+ * Check if a metric is better than the current best
18
+ */
19
+ export declare function isBetter(metric: number, best: number, direction: 'lower_is_better' | 'higher_is_better'): boolean;
20
+ /**
21
+ * Extract metric from command output
22
+ */
23
+ export declare function extractMetric(output: string, extractPattern: string): number | null;
24
+ /**
25
+ * Run the experiment and extract metric
26
+ */
27
+ export declare function runExperiment(config: ProgramConfig, workDir: string): Promise<{
28
+ metric: number | null;
29
+ output: string;
30
+ durationMs: number;
31
+ error?: string;
32
+ }>;
33
+ /**
34
+ * Format results as TSV
35
+ */
36
+ export declare function formatResultsTSV(results: ExperimentRecord[]): string;
37
+ /**
38
+ * Save loop results to file
39
+ */
40
+ export declare function saveResults(state: LoopState, config: ProgramConfig, outputPath: string): void;
41
+ /**
42
+ * Calculate stagnation level
43
+ */
44
+ export declare function getStagnationLevel(state: LoopState, config: ProgramConfig): 0 | 1 | 2 | 3;
45
+ /**
46
+ * Get prompt modifier based on stagnation level
47
+ */
48
+ export declare function getStagnationModifier(level: number): string;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Core autoloop optimization engine
3
+ */
4
+ import { $ } from 'bun';
5
+ import { writeFileSync } from 'fs';
6
+ /**
7
+ * Initialize the loop state
8
+ */
9
+ export function initState(config, branch) {
10
+ return {
11
+ bestMetric: config.metric.direction === 'lower_is_better' ? Infinity : -Infinity,
12
+ bestCommit: '',
13
+ experimentCount: 0,
14
+ stagnationCount: 0,
15
+ totalCostUSD: 0,
16
+ startTime: Date.now(),
17
+ results: [],
18
+ branch
19
+ };
20
+ }
21
+ /**
22
+ * Check if the loop should continue
23
+ */
24
+ export function shouldContinue(state, config) {
25
+ // Check experiment limit
26
+ if (state.experimentCount >= config.constraints.maxExperiments) {
27
+ return { continue: false, reason: `Reached max experiments (${config.constraints.maxExperiments})` };
28
+ }
29
+ // Check duration limit
30
+ const elapsedHours = (Date.now() - state.startTime) / (1000 * 60 * 60);
31
+ if (elapsedHours >= config.constraints.maxDurationHours) {
32
+ return { continue: false, reason: `Reached max duration (${config.constraints.maxDurationHours}h)` };
33
+ }
34
+ // Check cost limit
35
+ if (state.totalCostUSD >= config.constraints.maxCostUSD) {
36
+ return { continue: false, reason: `Reached max cost ($${config.constraints.maxCostUSD})` };
37
+ }
38
+ return { continue: true };
39
+ }
40
+ /**
41
+ * Check if a metric is better than the current best
42
+ */
43
+ export function isBetter(metric, best, direction) {
44
+ if (direction === 'lower_is_better') {
45
+ return metric < best;
46
+ }
47
+ return metric > best;
48
+ }
49
+ /**
50
+ * Extract metric from command output
51
+ */
52
+ export function extractMetric(output, extractPattern) {
53
+ try {
54
+ // Try to parse as a number directly
55
+ const direct = parseFloat(output.trim());
56
+ if (!isNaN(direct))
57
+ return direct;
58
+ // Try regex pattern
59
+ const regex = new RegExp(extractPattern);
60
+ const match = output.match(regex);
61
+ if (match) {
62
+ const num = parseFloat(match[1] || match[0]);
63
+ if (!isNaN(num))
64
+ return num;
65
+ }
66
+ return null;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ /**
73
+ * Run the experiment and extract metric
74
+ */
75
+ export async function runExperiment(config, workDir) {
76
+ const startTime = Date.now();
77
+ try {
78
+ // Run the command with timeout
79
+ const shell = $ `cd ${workDir} && ${config.runCommand}`.nothrow();
80
+ const result = await shell.timeout(config.constraints.timeBudgetSeconds * 1000);
81
+ const durationMs = Date.now() - startTime;
82
+ const output = result.stdout.toString() + result.stderr.toString();
83
+ // Extract metric
84
+ const metric = extractMetric(output, config.metric.extract);
85
+ return {
86
+ metric,
87
+ output,
88
+ durationMs,
89
+ error: result.exitCode !== 0 ? `Exit code ${result.exitCode}` : undefined
90
+ };
91
+ }
92
+ catch (e) {
93
+ const durationMs = Date.now() - startTime;
94
+ return {
95
+ metric: null,
96
+ output: '',
97
+ durationMs,
98
+ error: e.message
99
+ };
100
+ }
101
+ }
102
+ /**
103
+ * Format results as TSV
104
+ */
105
+ export function formatResultsTSV(results) {
106
+ const lines = ['commit\tmetric\tstatus\tdescription\ttimestamp\tduration_ms'];
107
+ for (const r of results) {
108
+ lines.push(`${r.commit}\t${r.metric}\t${r.status}\t"${r.description}"\t${r.timestamp}\t${r.durationMs}`);
109
+ }
110
+ return lines.join('\n');
111
+ }
112
+ /**
113
+ * Save loop results to file
114
+ */
115
+ export function saveResults(state, config, outputPath) {
116
+ const tsv = formatResultsTSV(state.results);
117
+ writeFileSync(outputPath, tsv);
118
+ }
119
+ /**
120
+ * Calculate stagnation level
121
+ */
122
+ export function getStagnationLevel(state, config) {
123
+ if (state.stagnationCount >= config.stagnation.tripleThreshold)
124
+ return 3;
125
+ if (state.stagnationCount >= config.stagnation.doubleThreshold)
126
+ return 2;
127
+ if (state.stagnationCount >= config.stagnation.threshold)
128
+ return 1;
129
+ return 0;
130
+ }
131
+ /**
132
+ * Get prompt modifier based on stagnation level
133
+ */
134
+ export function getStagnationModifier(level) {
135
+ switch (level) {
136
+ case 1:
137
+ return '\n[Stagnation detected — try a more aggressive change or different approach]';
138
+ case 2:
139
+ return '\n[Significant stagnation — consider a completely different strategy]';
140
+ case 3:
141
+ return '\n[Critical stagnation — radical change required, explore very different solutions]';
142
+ default:
143
+ return '';
144
+ }
145
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Parse program.md files for autoloop
3
+ */
4
+ import type { ProgramConfig } from './types.js';
5
+ /**
6
+ * Parse a program.md file into configuration
7
+ */
8
+ export declare function parseProgram(path: string): ProgramConfig;
9
+ /**
10
+ * Validate a program configuration
11
+ */
12
+ export declare function validateProgram(config: ProgramConfig): string[];
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Parse program.md files for autoloop
3
+ */
4
+ import { readFileSync } from 'fs';
5
+ /**
6
+ * Extract a section from markdown by heading
7
+ */
8
+ function getSection(content, heading) {
9
+ const re = new RegExp(`^##\\s+${heading}\\s*$`, 'im');
10
+ const match = content.match(re);
11
+ if (!match || match.index === undefined)
12
+ return '';
13
+ const start = match.index + match[0].length;
14
+ const nextHeading = content.slice(start).search(/^##\s+/m);
15
+ const end = nextHeading === -1 ? content.length : start + nextHeading;
16
+ return content.slice(start, end).trim();
17
+ }
18
+ /**
19
+ * Extract a field from a section
20
+ */
21
+ function getField(section, field) {
22
+ const re = new RegExp(`^-\\s+\\*\\*${field}\\*\\*:\\s*(.+)$`, 'im');
23
+ const match = section.match(re);
24
+ return match ? match[1].trim() : '';
25
+ }
26
+ /**
27
+ * Parse a number from constraint text
28
+ */
29
+ function parseConstraint(content, label, fallback) {
30
+ const re = new RegExp(`\\*\\*${label}\\*\\*:\\s*([\\d.]+)`, 'i');
31
+ const m = content.match(re);
32
+ return m ? parseFloat(m[1]) : fallback;
33
+ }
34
+ /**
35
+ * Parse a stagnation threshold
36
+ */
37
+ function parseStagnation(content, label, fallback) {
38
+ const re = new RegExp(`\\*\\*${label}\\*\\*:\\s*(\\d+)`, 'i');
39
+ const m = content.match(re);
40
+ return m ? parseInt(m[1]) : fallback;
41
+ }
42
+ /**
43
+ * Parse a program.md file into configuration
44
+ */
45
+ export function parseProgram(path) {
46
+ const raw = readFileSync(path, 'utf-8');
47
+ // Extract name from title
48
+ const nameMatch = raw.match(/^#\s+Program:\s*(.+)$/m);
49
+ const name = nameMatch ? nameMatch[1].trim() : 'unnamed';
50
+ // Extract metric section
51
+ const metricSection = getSection(raw, 'Metric');
52
+ const metricName = getField(metricSection, 'name');
53
+ const direction = getField(metricSection, 'direction');
54
+ const extract = getField(metricSection, 'extract').replace(/^`|`$/g, '');
55
+ // Extract constraints
56
+ const constraintsSection = getSection(raw, 'Constraints');
57
+ // Extract stagnation config
58
+ const stagnationSection = getSection(raw, 'Stagnation');
59
+ // Extract read-only files
60
+ const readOnlySection = getSection(raw, 'Read-Only Files');
61
+ const readOnlyFiles = readOnlySection
62
+ .split('\n')
63
+ .map(l => l.replace(/^-\s*/, '').trim())
64
+ .filter(Boolean);
65
+ // Extract setup code
66
+ const setupSection = getSection(raw, 'Setup');
67
+ const setupMatch = setupSection.match(/```(?:bash)?\n([\s\S]*?)```/);
68
+ // Extract run command
69
+ const runSection = getSection(raw, 'Run Command');
70
+ const runMatch = runSection.match(/```(?:bash)?\n([\s\S]*?)```/);
71
+ // Extract target file
72
+ const targetSection = getSection(raw, 'Target File');
73
+ const targetFile = targetSection.split('\n')[0].replace(/^`|`$/g, '').trim();
74
+ return {
75
+ name,
76
+ objective: getSection(raw, 'Objective'),
77
+ metric: {
78
+ name: metricName,
79
+ direction: direction || 'lower_is_better',
80
+ extract
81
+ },
82
+ setup: setupMatch ? setupMatch[1].trim() : setupSection,
83
+ targetFile,
84
+ runCommand: runMatch ? runMatch[1].trim() : runSection.split('\n')[0],
85
+ readOnlyFiles,
86
+ constraints: {
87
+ timeBudgetSeconds: parseConstraint(constraintsSection, 'Time budget per run', 300),
88
+ maxExperiments: parseConstraint(constraintsSection, 'Max experiments', 100),
89
+ maxDurationHours: parseConstraint(constraintsSection, 'Max duration', 8),
90
+ maxCostUSD: parseConstraint(constraintsSection, 'Max cost', 10)
91
+ },
92
+ stagnation: {
93
+ threshold: parseStagnation(stagnationSection, 'Threshold', 10),
94
+ doubleThreshold: parseStagnation(stagnationSection, 'Double threshold', 20),
95
+ tripleThreshold: parseStagnation(stagnationSection, 'Triple threshold', 30)
96
+ },
97
+ notes: getSection(raw, 'Notes')
98
+ };
99
+ }
100
+ /**
101
+ * Validate a program configuration
102
+ */
103
+ export function validateProgram(config) {
104
+ const errors = [];
105
+ if (!config.name || config.name === 'unnamed') {
106
+ errors.push('Program must have a name');
107
+ }
108
+ if (!config.metric.name) {
109
+ errors.push('Metric must have a name');
110
+ }
111
+ if (!config.metric.extract) {
112
+ errors.push('Metric must have an extract command/pattern');
113
+ }
114
+ if (!config.targetFile) {
115
+ errors.push('Target file must be specified');
116
+ }
117
+ if (!config.runCommand) {
118
+ errors.push('Run command must be specified');
119
+ }
120
+ return errors;
121
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Types for autoloop optimization
3
+ */
4
+ export interface MetricConfig {
5
+ name: string;
6
+ direction: 'lower_is_better' | 'higher_is_better';
7
+ extract: string;
8
+ }
9
+ export interface ConstraintConfig {
10
+ timeBudgetSeconds: number;
11
+ maxExperiments: number;
12
+ maxDurationHours: number;
13
+ maxCostUSD: number;
14
+ }
15
+ export interface StagnationConfig {
16
+ threshold: number;
17
+ doubleThreshold: number;
18
+ tripleThreshold: number;
19
+ }
20
+ export interface ProgramConfig {
21
+ name: string;
22
+ objective: string;
23
+ metric: MetricConfig;
24
+ setup: string;
25
+ targetFile: string;
26
+ runCommand: string;
27
+ readOnlyFiles: string[];
28
+ constraints: ConstraintConfig;
29
+ stagnation: StagnationConfig;
30
+ notes: string;
31
+ }
32
+ export interface ExperimentRecord {
33
+ commit: string;
34
+ metric: number;
35
+ status: 'keep' | 'discard' | 'crash';
36
+ description: string;
37
+ timestamp: string;
38
+ durationMs: number;
39
+ }
40
+ export interface LoopState {
41
+ bestMetric: number;
42
+ bestCommit: string;
43
+ experimentCount: number;
44
+ stagnationCount: number;
45
+ totalCostUSD: number;
46
+ startTime: number;
47
+ results: ExperimentRecord[];
48
+ branch: string;
49
+ }
50
+ export interface LoopStatus {
51
+ running: boolean;
52
+ state: LoopState | null;
53
+ config: ProgramConfig | null;
54
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Types for autoloop optimization
3
+ */
4
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * CLI for autoloop optimization
4
+ *
5
+ * Usage: zouroboros-autoloop --program <program.md>
6
+ */
7
+ export {};