@cristochang/spec-execution 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1282 @@
1
+ // src/types.ts
2
+ var EMPTY_BOUNDARIES = {
3
+ objective: "",
4
+ inclusions: [],
5
+ exclusions: [],
6
+ constraints: [],
7
+ maxIterations: 0
8
+ };
9
+ var EMPTY_COMPLETION = {
10
+ criteria: [],
11
+ verification: { mode: "none" }
12
+ };
13
+ var EMPTY_SNAPSHOT = {
14
+ id: "",
15
+ status: "",
16
+ created: ""
17
+ };
18
+
19
+ // src/contract.ts
20
+ var ContractFactory;
21
+ ((ContractFactory2) => {
22
+ function create(spec) {
23
+ if (!spec) {
24
+ return {
25
+ gate: { allowed: false, reason: "spec_not_found" },
26
+ boundaries: EMPTY_BOUNDARIES,
27
+ completion: EMPTY_COMPLETION,
28
+ specSnapshot: EMPTY_SNAPSHOT
29
+ };
30
+ }
31
+ const gate = evaluateGate(spec.status);
32
+ const boundaries = buildBoundaries(spec);
33
+ const completion = buildCompletion(spec);
34
+ const specSnapshot = buildSnapshot(spec);
35
+ return {
36
+ gate,
37
+ boundaries,
38
+ completion,
39
+ specSnapshot
40
+ };
41
+ }
42
+ ContractFactory2.create = create;
43
+ function evaluateGate(status) {
44
+ switch (status) {
45
+ case "pending":
46
+ return { allowed: false, reason: "spec_not_approved" };
47
+ case "approved":
48
+ return { allowed: true };
49
+ case "implemented":
50
+ return { allowed: false, reason: "spec_already_implemented" };
51
+ case "cancelled":
52
+ return { allowed: false, reason: "spec_cancelled" };
53
+ default:
54
+ return { allowed: false, reason: "spec_not_approved" };
55
+ }
56
+ }
57
+ function buildBoundaries(spec) {
58
+ return {
59
+ objective: spec.objective,
60
+ inclusions: spec.scope.inclusions,
61
+ exclusions: spec.scope.exclusions,
62
+ constraints: spec.constraints ?? [],
63
+ maxIterations: 50
64
+ // Default, can be overridden by config
65
+ };
66
+ }
67
+ function buildCompletion(spec) {
68
+ const criteria = buildCriteria(spec);
69
+ const verification = inferVerificationMode(spec);
70
+ return {
71
+ criteria,
72
+ verification
73
+ };
74
+ }
75
+ function buildCriteria(spec) {
76
+ return spec.successCriteria.map((description, index) => ({
77
+ id: `criterion_${index}`,
78
+ description,
79
+ status: "pending"
80
+ }));
81
+ }
82
+ function inferVerificationMode(spec) {
83
+ const constraintsLower = spec.constraints?.map((c) => c.toLowerCase()) ?? [];
84
+ const inclusionsLower = spec.scope.inclusions.map((i) => i.toLowerCase());
85
+ const requiresTest = constraintsLower.some(
86
+ (c) => c.includes("test") || c.includes("ci") || c.includes("coverage")
87
+ );
88
+ if (requiresTest) {
89
+ return { mode: "automated" };
90
+ }
91
+ const isUI = inclusionsLower.some(
92
+ (s) => s.includes("ui") || s.includes("component") || s.includes("page") || s.includes("screen") || s.includes("view")
93
+ );
94
+ if (isUI) {
95
+ return { mode: "manual" };
96
+ }
97
+ return { mode: "none" };
98
+ }
99
+ function buildSnapshot(spec) {
100
+ return {
101
+ id: spec.id,
102
+ status: spec.status,
103
+ created: spec.created
104
+ };
105
+ }
106
+ })(ContractFactory || (ContractFactory = {}));
107
+
108
+ // src/gate.ts
109
+ var ExecutionGate;
110
+ ((ExecutionGate2) => {
111
+ function check(spec, session) {
112
+ if (!spec) {
113
+ return { allowed: false, reason: "spec_not_found" };
114
+ }
115
+ if (spec.status === "cancelled") {
116
+ return { allowed: false, reason: "spec_cancelled" };
117
+ }
118
+ if (spec.status === "implemented") {
119
+ return { allowed: false, reason: "spec_already_implemented" };
120
+ }
121
+ if (spec.status !== "approved") {
122
+ return { allowed: false, reason: "spec_not_approved" };
123
+ }
124
+ if (session.iteration >= session.config.maxIterations) {
125
+ return { allowed: false, reason: "max_iterations_exceeded" };
126
+ }
127
+ if (session.isBlockedByFailures()) {
128
+ return { allowed: false, reason: "blocked_by_failures" };
129
+ }
130
+ const contract = ContractFactory.create(spec);
131
+ return {
132
+ allowed: true,
133
+ contract
134
+ };
135
+ }
136
+ ExecutionGate2.check = check;
137
+ function checkSpecOnly(spec) {
138
+ if (!spec) {
139
+ return { allowed: false, reason: "spec_not_found" };
140
+ }
141
+ if (spec.status === "cancelled") {
142
+ return { allowed: false, reason: "spec_cancelled" };
143
+ }
144
+ if (spec.status === "implemented") {
145
+ return { allowed: false, reason: "spec_already_implemented" };
146
+ }
147
+ if (spec.status !== "approved") {
148
+ return { allowed: false, reason: "spec_not_approved" };
149
+ }
150
+ return { allowed: true };
151
+ }
152
+ ExecutionGate2.checkSpecOnly = checkSpecOnly;
153
+ function isRetryableReason(reason) {
154
+ const retryable = /* @__PURE__ */ new Set([
155
+ "spec_not_approved",
156
+ "max_iterations_exceeded",
157
+ "blocked_by_failures"
158
+ ]);
159
+ return retryable.has(reason);
160
+ }
161
+ ExecutionGate2.isRetryableReason = isRetryableReason;
162
+ function isTerminalReason(reason) {
163
+ const terminal = /* @__PURE__ */ new Set([
164
+ "spec_not_found",
165
+ "spec_cancelled",
166
+ "spec_already_implemented"
167
+ ]);
168
+ return terminal.has(reason);
169
+ }
170
+ ExecutionGate2.isTerminalReason = isTerminalReason;
171
+ })(ExecutionGate || (ExecutionGate = {}));
172
+
173
+ // src/state.ts
174
+ function computeProgress(counts, total) {
175
+ return {
176
+ total,
177
+ pending: counts.pending ?? 0,
178
+ inProgress: counts.in_progress ?? 0,
179
+ satisfied: counts.satisfied ?? 0,
180
+ verified: counts.verified ?? 0,
181
+ blocked: counts.blocked ?? 0,
182
+ failed: counts.failed ?? 0,
183
+ percentage: total > 0 ? (counts.verified ?? 0) / total * 100 : 0
184
+ };
185
+ }
186
+ function createToolCallSummary(tool, success, files, error) {
187
+ return {
188
+ tool,
189
+ success,
190
+ timestamp: Date.now(),
191
+ files: files ?? [],
192
+ error
193
+ };
194
+ }
195
+ var ExecutionState = class {
196
+ _filesTouched = /* @__PURE__ */ new Set();
197
+ _criteriaStatus = /* @__PURE__ */ new Map();
198
+ _criteriaVerifiedAt = /* @__PURE__ */ new Map();
199
+ _criteriaVerificationMethod = /* @__PURE__ */ new Map();
200
+ _lastToolCall;
201
+ _startedAt;
202
+ _lastUpdated;
203
+ constructor() {
204
+ this._startedAt = Date.now();
205
+ this._lastUpdated = this._startedAt;
206
+ }
207
+ /**
208
+ * Initialize criteria from a list of criterion IDs
209
+ */
210
+ initializeCriteria(criterionIds) {
211
+ for (const id of criterionIds) {
212
+ if (!this._criteriaStatus.has(id)) {
213
+ this._criteriaStatus.set(id, "pending");
214
+ }
215
+ }
216
+ this._touch();
217
+ }
218
+ /**
219
+ * Get the status of a criterion
220
+ */
221
+ getCriterionStatus(criterionId) {
222
+ return this._criteriaStatus.get(criterionId);
223
+ }
224
+ /**
225
+ * Set the status of a criterion
226
+ */
227
+ setCriterionStatus(criterionId, status, verifiedAt, verificationMethod) {
228
+ this._criteriaStatus.set(criterionId, status);
229
+ if (verifiedAt) {
230
+ this._criteriaVerifiedAt.set(criterionId, verifiedAt);
231
+ }
232
+ if (verificationMethod) {
233
+ this._criteriaVerificationMethod.set(criterionId, verificationMethod);
234
+ }
235
+ this._touch();
236
+ }
237
+ /**
238
+ * Get all criteria statuses
239
+ */
240
+ getAllCriteriaStatus() {
241
+ return this._criteriaStatus;
242
+ }
243
+ /**
244
+ * Mark a file as touched
245
+ */
246
+ touchFile(file) {
247
+ this._filesTouched.add(file);
248
+ this._touch();
249
+ }
250
+ /**
251
+ * Mark multiple files as touched
252
+ */
253
+ touchFiles(files) {
254
+ for (const file of files) {
255
+ this._filesTouched.add(file);
256
+ }
257
+ this._touch();
258
+ }
259
+ /**
260
+ * Check if a file has been touched
261
+ */
262
+ hasTouchedFile(file) {
263
+ return this._filesTouched.has(file);
264
+ }
265
+ /**
266
+ * Get all touched files
267
+ */
268
+ getTouchedFiles() {
269
+ return Array.from(this._filesTouched);
270
+ }
271
+ /**
272
+ * Get count of touched files
273
+ */
274
+ getTouchedFileCount() {
275
+ return this._filesTouched.size;
276
+ }
277
+ /**
278
+ * Record a tool call
279
+ */
280
+ recordToolCall(summary) {
281
+ this._lastToolCall = summary;
282
+ this._touch();
283
+ }
284
+ /**
285
+ * Get the last tool call summary
286
+ */
287
+ getLastToolCall() {
288
+ return this._lastToolCall;
289
+ }
290
+ /**
291
+ * Get progress summary for all criteria
292
+ */
293
+ getProgress() {
294
+ const counts = {
295
+ pending: 0,
296
+ in_progress: 0,
297
+ satisfied: 0,
298
+ verified: 0,
299
+ blocked: 0,
300
+ failed: 0
301
+ };
302
+ for (const status of this._criteriaStatus.values()) {
303
+ counts[status] = (counts[status] ?? 0) + 1;
304
+ }
305
+ return computeProgress(counts, this._criteriaStatus.size);
306
+ }
307
+ /**
308
+ * Check if all criteria are verified
309
+ */
310
+ isAllVerified() {
311
+ if (this._criteriaStatus.size === 0) return false;
312
+ for (const status of this._criteriaStatus.values()) {
313
+ if (status !== "verified") return false;
314
+ }
315
+ return true;
316
+ }
317
+ /**
318
+ * Get criteria with a specific status
319
+ */
320
+ getCriteriaByStatus(status) {
321
+ const result = [];
322
+ for (const [id, s] of this._criteriaStatus) {
323
+ if (s === status) {
324
+ result.push(id);
325
+ }
326
+ }
327
+ return result;
328
+ }
329
+ /**
330
+ * Get the next pending criterion
331
+ */
332
+ getNextPendingCriterion() {
333
+ for (const [id, status] of this._criteriaStatus) {
334
+ if (status === "pending") {
335
+ return id;
336
+ }
337
+ }
338
+ return void 0;
339
+ }
340
+ /**
341
+ * Get session duration in milliseconds
342
+ */
343
+ getDuration() {
344
+ return this._lastUpdated - this._startedAt;
345
+ }
346
+ /**
347
+ * Create a snapshot of the current state
348
+ */
349
+ snapshot() {
350
+ return {
351
+ filesTouched: Array.from(this._filesTouched),
352
+ criteriaStatus: Object.fromEntries(this._criteriaStatus),
353
+ lastToolCall: this._lastToolCall,
354
+ startedAt: this._startedAt,
355
+ lastUpdated: this._lastUpdated,
356
+ duration: this.getDuration()
357
+ };
358
+ }
359
+ /**
360
+ * Update internal timestamp
361
+ */
362
+ _touch() {
363
+ this._lastUpdated = Date.now();
364
+ }
365
+ };
366
+ function restoreFromSnapshot(snapshot) {
367
+ const state = new ExecutionState();
368
+ const accessor = state;
369
+ accessor._startedAt = snapshot.startedAt;
370
+ accessor._lastUpdated = snapshot.lastUpdated;
371
+ for (const file of snapshot.filesTouched) {
372
+ state.touchFile(file);
373
+ }
374
+ for (const [id, status] of Object.entries(snapshot.criteriaStatus)) {
375
+ state.setCriterionStatus(id, status);
376
+ }
377
+ if (snapshot.lastToolCall) {
378
+ state.recordToolCall(snapshot.lastToolCall);
379
+ }
380
+ return state;
381
+ }
382
+
383
+ // src/failure.ts
384
+ var DEFAULT_FAILURE_CONFIG = {
385
+ capacity: 10,
386
+ retryThreshold: 3
387
+ };
388
+ var PatternExtractor;
389
+ ((PatternExtractor2) => {
390
+ function fromToolOutput(tool, output) {
391
+ const normalizedTool = normalizeToolName(tool);
392
+ const recognizedTools = ["typescript", "tsc", "test", "jest", "bun", "bash", "write", "edit"];
393
+ if (!recognizedTools.includes(normalizedTool)) {
394
+ return null;
395
+ }
396
+ if (output == null) {
397
+ return null;
398
+ }
399
+ if (typeof output === "string") {
400
+ return extractFromString(normalizedTool, output);
401
+ }
402
+ if (typeof output === "object") {
403
+ return extractFromObject(normalizedTool, output);
404
+ }
405
+ return null;
406
+ }
407
+ PatternExtractor2.fromToolOutput = fromToolOutput;
408
+ function normalizeToolName(tool) {
409
+ const aliases = {
410
+ "type_error": "typescript",
411
+ "test_failure": "test",
412
+ "runtime_error": "bash"
413
+ };
414
+ return aliases[tool] ?? tool;
415
+ }
416
+ function shouldRecord(pattern) {
417
+ return pattern !== null;
418
+ }
419
+ PatternExtractor2.shouldRecord = shouldRecord;
420
+ function extractHint(output) {
421
+ if (typeof output === "string" && output.length > 0) {
422
+ return output.slice(0, 100);
423
+ }
424
+ if (typeof output === "object" && output !== null) {
425
+ if ("message" in output && typeof output.message === "string") {
426
+ return output.message.slice(0, 100);
427
+ }
428
+ if ("error" in output && typeof output.error === "string") {
429
+ return output.error.slice(0, 100);
430
+ }
431
+ }
432
+ return void 0;
433
+ }
434
+ PatternExtractor2.extractHint = extractHint;
435
+ function extractFromString(tool, output) {
436
+ const lower = output.toLowerCase();
437
+ if (lower.includes("type error") || lower.includes("ts(") || lower.includes("cannot find type") || lower.includes("not assignable") || lower.includes("type '") && lower.includes("' is not assignable")) {
438
+ return "type_error";
439
+ }
440
+ if (lower.includes("cannot find module") || lower.includes("module not found") || lower.includes("missing import")) {
441
+ return "missing_import";
442
+ }
443
+ if (lower.includes("syntax error") || lower.includes("unexpected token")) {
444
+ return "syntax_error";
445
+ }
446
+ if (lower.includes("enoent") || lower.includes("no such file")) {
447
+ return "file_not_found";
448
+ }
449
+ if (tool === "test" || tool === "jest" || tool === "bun") {
450
+ if (lower.includes("fail") || lower.includes("error") || lower.includes("assertion")) {
451
+ return "test_failure";
452
+ }
453
+ }
454
+ return null;
455
+ }
456
+ function extractFromObject(tool, output) {
457
+ const o = output;
458
+ if ("code" in o && typeof o.code === "string") {
459
+ const code = o.code.toLowerCase();
460
+ if (code === "enoent") return "file_not_found";
461
+ if (code === "eacces" || code === "eperm") return "permission_denied";
462
+ }
463
+ if ("message" in o && typeof o.message === "string") {
464
+ return extractFromString(tool, o.message);
465
+ }
466
+ if ("error" in o) {
467
+ if (typeof o.error === "string") {
468
+ return extractFromString(tool, o.error);
469
+ }
470
+ if (typeof o.error === "object" && o.error !== null) {
471
+ return extractFromObject(tool, o.error);
472
+ }
473
+ }
474
+ if ("failed" in o && typeof o.failed === "number" && o.failed > 0) {
475
+ return "test_failure";
476
+ }
477
+ return null;
478
+ }
479
+ })(PatternExtractor || (PatternExtractor = {}));
480
+ var FailureMemory = class {
481
+ _config;
482
+ _entries;
483
+ constructor(config = DEFAULT_FAILURE_CONFIG) {
484
+ this._config = config;
485
+ this._entries = /* @__PURE__ */ new Map();
486
+ }
487
+ /**
488
+ * Record a failure
489
+ *
490
+ * If the same failure (same criterion + pattern) already exists,
491
+ * increments the count. Otherwise, adds a new entry.
492
+ */
493
+ record(input) {
494
+ if (!PatternExtractor.shouldRecord(input.pattern)) {
495
+ return;
496
+ }
497
+ const key = this._makeKey(input);
498
+ const existing = this._entries.get(key);
499
+ if (existing) {
500
+ const updated = {
501
+ ...existing,
502
+ count: existing.count + 1,
503
+ lastSeen: Date.now()
504
+ };
505
+ this._entries.set(key, updated);
506
+ } else {
507
+ if (this._entries.size >= this._config.capacity) {
508
+ const lruKey = this._findLRU();
509
+ if (lruKey) {
510
+ this._entries.delete(lruKey);
511
+ }
512
+ }
513
+ const entry = {
514
+ id: `failure_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
515
+ criterionId: input.criterionId,
516
+ pattern: input.pattern,
517
+ hint: input.hint,
518
+ count: 1,
519
+ firstSeen: Date.now(),
520
+ lastSeen: Date.now()
521
+ };
522
+ this._entries.set(key, entry);
523
+ }
524
+ }
525
+ /**
526
+ * Check if a criterion should be skipped due to repeated failures
527
+ *
528
+ * Returns true if the same failure has occurred >= retryThreshold times.
529
+ */
530
+ shouldSkip(criterionId, pattern) {
531
+ const key = this._makeKey({ criterionId, pattern });
532
+ const entry = this._entries.get(key);
533
+ return entry ? entry.count >= this._config.retryThreshold : false;
534
+ }
535
+ /**
536
+ * Get failure count for a specific criterion + pattern
537
+ */
538
+ getFailureCount(criterionId, pattern) {
539
+ const key = this._makeKey({ criterionId, pattern });
540
+ return this._entries.get(key)?.count ?? 0;
541
+ }
542
+ /**
543
+ * Get all failures for a criterion
544
+ */
545
+ getFailuresForCriterion(criterionId) {
546
+ const result = [];
547
+ for (const [key, entry] of this._entries) {
548
+ if (key.startsWith(`${criterionId}:`)) {
549
+ result.push(entry);
550
+ }
551
+ }
552
+ return result.sort((a, b) => b.lastSeen - a.lastSeen);
553
+ }
554
+ /**
555
+ * Get compressed failure summary
556
+ */
557
+ summarize() {
558
+ const entries = Array.from(this._entries.values());
559
+ const patterns = [...new Set(entries.map((e) => e.pattern))];
560
+ return {
561
+ total: this._entries.size,
562
+ patterns,
563
+ recent: entries.sort((a, b) => b.lastSeen - a.lastSeen).slice(0, 3)
564
+ };
565
+ }
566
+ /**
567
+ * Clear all entries
568
+ */
569
+ clear() {
570
+ this._entries.clear();
571
+ }
572
+ /**
573
+ * Get total entry count
574
+ */
575
+ get size() {
576
+ return this._entries.size;
577
+ }
578
+ /**
579
+ * Create key for storing entries
580
+ */
581
+ _makeKey(input) {
582
+ return `${input.criterionId}:${input.pattern}`;
583
+ }
584
+ /**
585
+ * Find least recently used entry
586
+ */
587
+ _findLRU() {
588
+ let lruKey = null;
589
+ let oldestTime = Infinity;
590
+ for (const [key, entry] of this._entries) {
591
+ if (entry.lastSeen < oldestTime) {
592
+ oldestTime = entry.lastSeen;
593
+ lruKey = key;
594
+ }
595
+ }
596
+ return lruKey;
597
+ }
598
+ };
599
+
600
+ // src/context.ts
601
+ var DEFAULT_ASSEMBLE_OPTIONS = {
602
+ maxFiles: 5,
603
+ maxFailures: 3,
604
+ includeCodeSnippet: false,
605
+ maxCodeLines: 100
606
+ };
607
+ function createWorkingFocus(criterionId, criterionText, targetFile, contextSnippet) {
608
+ return {
609
+ targetCriterionId: criterionId,
610
+ targetCriterionText: criterionText,
611
+ targetFile,
612
+ contextSnippet
613
+ };
614
+ }
615
+ var ContextAssembler;
616
+ ((ContextAssembler2) => {
617
+ function assemble(input, options = DEFAULT_ASSEMBLE_OPTIONS) {
618
+ const opts = { ...DEFAULT_ASSEMBLE_OPTIONS, ...options };
619
+ return {
620
+ immutable: assembleImmutable(input),
621
+ state: assembleState(input, opts),
622
+ failures: assembleFailures(input, opts),
623
+ working: assembleWorking(input)
624
+ };
625
+ }
626
+ ContextAssembler2.assemble = assemble;
627
+ function assembleImmutable(input) {
628
+ const { boundaries, completion } = input.contract;
629
+ return {
630
+ spec: {
631
+ objective: boundaries.objective,
632
+ scope: {
633
+ in: boundaries.inclusions,
634
+ out: boundaries.exclusions
635
+ },
636
+ criteria: completion.criteria.map((c) => ({
637
+ id: c.id,
638
+ text: c.description
639
+ })),
640
+ constraints: boundaries.constraints
641
+ }
642
+ };
643
+ }
644
+ function assembleState(input, options) {
645
+ const { iteration, progress, touchedFiles, criteriaStatus } = input;
646
+ const maxFiles = options.maxFiles ?? 5;
647
+ const fileList = touchedFiles.slice(0, maxFiles);
648
+ const moreCount = Math.max(0, touchedFiles.length - maxFiles);
649
+ const displayList = moreCount > 0 ? [...fileList, `... and ${moreCount} more`] : fileList;
650
+ const criteriaEntries = Array.from(criteriaStatus.entries()).map(([id, status]) => ({ id, status }));
651
+ return {
652
+ iteration,
653
+ progress: {
654
+ current: progress.verified,
655
+ total: progress.total,
656
+ percentage: Math.round(progress.percentage)
657
+ },
658
+ files: {
659
+ count: touchedFiles.length,
660
+ list: displayList
661
+ },
662
+ criteria: criteriaEntries
663
+ };
664
+ }
665
+ function assembleFailures(input, options) {
666
+ const { failureSummary } = input;
667
+ const maxFailures = options.maxFailures ?? 3;
668
+ const recent = failureSummary.recent.slice(0, maxFailures).map((entry) => ({
669
+ criterion: entry.criterionId,
670
+ error: entry.pattern,
671
+ hint: entry.hint
672
+ }));
673
+ return {
674
+ count: failureSummary.total,
675
+ patterns: failureSummary.patterns,
676
+ recent
677
+ };
678
+ }
679
+ function assembleWorking(input) {
680
+ const { currentFocus, lastToolCall } = input;
681
+ if (!currentFocus) {
682
+ return {
683
+ task: "Continue working on the next criterion",
684
+ criterion: "No specific criterion selected",
685
+ lastResult: lastToolCall?.success ? `Last tool (${lastToolCall.tool}) succeeded` : lastToolCall?.error ? `Last tool (${lastToolCall.tool}) failed: ${lastToolCall.error}` : void 0
686
+ };
687
+ }
688
+ return {
689
+ task: `Work on criterion: ${currentFocus.targetCriterionText}`,
690
+ criterion: currentFocus.targetCriterionText,
691
+ file: currentFocus.targetFile,
692
+ lastResult: lastToolCall?.success ? `Last tool (${lastToolCall.tool}) succeeded` : lastToolCall?.error ? `Last tool (${lastToolCall.tool}) failed: ${lastToolCall.error}` : void 0
693
+ };
694
+ }
695
+ function estimateTokens(context) {
696
+ const json = JSON.stringify(context);
697
+ return Math.ceil(json.length / 4);
698
+ }
699
+ ContextAssembler2.estimateTokens = estimateTokens;
700
+ function formatAsText(context) {
701
+ const lines = [];
702
+ lines.push("=== Immutable Truth ===");
703
+ lines.push(`Objective: ${context.immutable.spec.objective}`);
704
+ lines.push(`In scope: ${context.immutable.spec.scope.in.join(", ")}`);
705
+ lines.push(`Out of scope: ${context.immutable.spec.scope.out.join(", ") || "none"}`);
706
+ lines.push(`Criteria (${context.immutable.spec.criteria.length}):`);
707
+ for (const c of context.immutable.spec.criteria) {
708
+ lines.push(` - ${c.id}: ${c.text}`);
709
+ }
710
+ lines.push("\n=== Execution State ===");
711
+ lines.push(`Iteration: ${context.state.iteration}`);
712
+ lines.push(`Progress: ${context.state.progress.current}/${context.state.progress.total} (${context.state.progress.percentage}%)`);
713
+ lines.push(`Files touched: ${context.state.files.count}`);
714
+ if (context.state.files.list.length > 0) {
715
+ lines.push(` ${context.state.files.list.join(", ")}`);
716
+ }
717
+ if (context.failures.count > 0) {
718
+ lines.push("\n=== Failure Memory ===");
719
+ lines.push(`Total failures: ${context.failures.count}`);
720
+ lines.push(`Patterns: ${context.failures.patterns.join(", ")}`);
721
+ for (const f of context.failures.recent) {
722
+ lines.push(` - ${f.criterion}: ${f.error}`);
723
+ }
724
+ }
725
+ lines.push("\n=== Working Context ===");
726
+ lines.push(`Task: ${context.working.task}`);
727
+ if (context.working.file) {
728
+ lines.push(`File: ${context.working.file}`);
729
+ }
730
+ if (context.working.lastResult) {
731
+ lines.push(`Last result: ${context.working.lastResult}`);
732
+ }
733
+ return lines.join("\n");
734
+ }
735
+ ContextAssembler2.formatAsText = formatAsText;
736
+ })(ContextAssembler || (ContextAssembler = {}));
737
+
738
+ // src/completion.ts
739
+ function createEvidence(criteriaStatus, filesModified, testResults, verificationSummary) {
740
+ return {
741
+ criteriaStatus,
742
+ filesModified,
743
+ testResults,
744
+ verificationSummary,
745
+ timestamp: Date.now()
746
+ };
747
+ }
748
+ function createRecommendation(specID, ready, confidence, evidence, suggestedNextStep, reason, requiresUserConfirmation = false) {
749
+ return {
750
+ specID,
751
+ ready,
752
+ confidence,
753
+ evidence,
754
+ suggestedNextStep,
755
+ reason,
756
+ requiresUserConfirmation
757
+ };
758
+ }
759
+ function createProposal(recommendation) {
760
+ const type = recommendation.ready ? "completion" : recommendation.confidence === "low" ? "failed" : "partial";
761
+ return {
762
+ specID: recommendation.specID,
763
+ type,
764
+ content: {
765
+ evidence: recommendation.evidence,
766
+ summary: generateSummary(recommendation),
767
+ confidence: recommendation.confidence,
768
+ suggestedAction: recommendation.ready ? {
769
+ tool: "spec_update",
770
+ params: {
771
+ specID: recommendation.specID,
772
+ changes: recommendation.evidence.filesModified,
773
+ verification: recommendation.evidence.verificationSummary
774
+ }
775
+ } : void 0
776
+ },
777
+ nextStep: {
778
+ action: recommendation.suggestedNextStep,
779
+ reason: recommendation.reason,
780
+ requiresUserConfirmation: recommendation.requiresUserConfirmation
781
+ }
782
+ };
783
+ }
784
+ function generateSummary(rec) {
785
+ const verifiedCount = Array.from(rec.evidence.criteriaStatus.values()).filter((s) => s === "verified").length;
786
+ const totalCount = rec.evidence.criteriaStatus.size;
787
+ if (rec.ready) {
788
+ return `All ${totalCount} criteria verified. Ready to mark as implemented.`;
789
+ }
790
+ return `${verifiedCount}/${totalCount} criteria verified. ${rec.suggestedNextStep === "continue" ? "Continuing execution..." : rec.reason}`;
791
+ }
792
+ var CompletionDetector;
793
+ ((CompletionDetector2) => {
794
+ function evaluate(spec, progress, evidence, verificationMode) {
795
+ const allSatisfied = checkAllCriteriaSatisfied(progress);
796
+ if (!allSatisfied) {
797
+ return createRecommendation(
798
+ spec.id,
799
+ false,
800
+ "low",
801
+ evidence,
802
+ "continue",
803
+ `${progress.verified}/${progress.total} criteria verified. Continue execution.`,
804
+ false
805
+ );
806
+ }
807
+ switch (verificationMode) {
808
+ case "none":
809
+ return createRecommendation(
810
+ spec.id,
811
+ true,
812
+ "high",
813
+ evidence,
814
+ "mark_implemented",
815
+ "All criteria satisfied. No verification required.",
816
+ false
817
+ );
818
+ case "automated":
819
+ if (evidence.testResults?.passed) {
820
+ return createRecommendation(
821
+ spec.id,
822
+ true,
823
+ "high",
824
+ evidence,
825
+ "mark_implemented",
826
+ `All criteria satisfied and tests passing (${evidence.testResults.passedCount}/${evidence.testResults.total}).`,
827
+ false
828
+ );
829
+ } else if (evidence.testResults) {
830
+ return createRecommendation(
831
+ spec.id,
832
+ false,
833
+ "medium",
834
+ evidence,
835
+ "needs_verification",
836
+ `All criteria satisfied but tests failed (${evidence.testResults.failed} failures).`,
837
+ false
838
+ );
839
+ } else {
840
+ return createRecommendation(
841
+ spec.id,
842
+ false,
843
+ "medium",
844
+ evidence,
845
+ "needs_verification",
846
+ "All criteria satisfied but no test results yet.",
847
+ false
848
+ );
849
+ }
850
+ case "manual":
851
+ return createRecommendation(
852
+ spec.id,
853
+ true,
854
+ "medium",
855
+ evidence,
856
+ "needs_user_review",
857
+ "All criteria satisfied. Manual review required before marking as implemented.",
858
+ true
859
+ // Requires user confirmation
860
+ );
861
+ default:
862
+ return createRecommendation(
863
+ spec.id,
864
+ false,
865
+ "low",
866
+ evidence,
867
+ "continue",
868
+ "Unknown verification mode. Continue execution.",
869
+ false
870
+ );
871
+ }
872
+ }
873
+ CompletionDetector2.evaluate = evaluate;
874
+ function checkAllCriteriaSatisfied(progress) {
875
+ const completedCount = progress.satisfied + progress.verified;
876
+ return completedCount === progress.total && progress.failed === 0;
877
+ }
878
+ function isComplete(evidence) {
879
+ if (evidence.criteriaStatus.size === 0) return false;
880
+ for (const status of evidence.criteriaStatus.values()) {
881
+ if (status !== "verified") return false;
882
+ }
883
+ return true;
884
+ }
885
+ CompletionDetector2.isComplete = isComplete;
886
+ function canContinue(evidence) {
887
+ return !isComplete(evidence);
888
+ }
889
+ CompletionDetector2.canContinue = canContinue;
890
+ })(CompletionDetector || (CompletionDetector = {}));
891
+
892
+ // src/runner/executor.ts
893
+ function createSuccessResult(output, tool, filesModified) {
894
+ return {
895
+ success: true,
896
+ output,
897
+ tool,
898
+ filesModified
899
+ };
900
+ }
901
+ function createFailureResult(error, tool) {
902
+ return {
903
+ success: false,
904
+ error,
905
+ tool
906
+ };
907
+ }
908
+ function isExecutorAvailable(executor) {
909
+ return typeof executor === "object" && executor !== null && "execute" in executor && typeof executor.execute === "function";
910
+ }
911
+
912
+ // src/runner/ralph.ts
913
+ var DEFAULT_RALPH_CONFIG = {
914
+ command: "ralph",
915
+ args: [],
916
+ timeout: 12e4
917
+ };
918
+ var RalphAdapter = class {
919
+ _config;
920
+ constructor(config = DEFAULT_RALPH_CONFIG) {
921
+ this._config = { ...DEFAULT_RALPH_CONFIG, ...config };
922
+ }
923
+ async execute(context) {
924
+ const startTime = Date.now();
925
+ try {
926
+ const prompt = ContextAssembler.formatAsText(context);
927
+ void this._config;
928
+ const ralphResult = await this._callRalph({ prompt, tools: ["edit", "write", "bash", "test"], maxIterations: 50 });
929
+ const duration = Date.now() - startTime;
930
+ return {
931
+ success: ralphResult.success,
932
+ output: ralphResult.output,
933
+ error: ralphResult.error,
934
+ tool: ralphResult.lastTool,
935
+ filesModified: ralphResult.filesModified,
936
+ duration
937
+ };
938
+ } catch (error) {
939
+ return { success: false, error, duration: Date.now() - startTime };
940
+ }
941
+ }
942
+ /**
943
+ * Call Ralph - IMPLEMENT THIS based on your Ralph setup
944
+ *
945
+ * Examples:
946
+ * - Spawn Ralph as child process
947
+ * - Call Ralph's HTTP API
948
+ * - Import Ralph module directly
949
+ */
950
+ async _callRalph(_input) {
951
+ throw new Error(
952
+ "Ralph adapter not connected. Implement _callRalph() based on your Ralph setup.\nExamples: fetch(this._config.apiUrl, {...}), spawn(this._config.command, {...}), etc."
953
+ );
954
+ }
955
+ };
956
+ function createRalphAdapter(config) {
957
+ return new RalphAdapter(config);
958
+ }
959
+
960
+ // src/runner/session.ts
961
+ var ExecutionSession = class {
962
+ _config;
963
+ _spec;
964
+ _state;
965
+ _failures;
966
+ _contract;
967
+ _sessionStatus = "idle";
968
+ _iteration = 0;
969
+ _startedAt = 0;
970
+ _lastUpdate = 0;
971
+ _completionRecommendation = null;
972
+ constructor(spec, config) {
973
+ this._spec = spec;
974
+ this._config = config;
975
+ this._state = new ExecutionState();
976
+ this._failures = new FailureMemory(config.failureMemory);
977
+ this._contract = spec ? ContractFactory.create(spec) : null;
978
+ if (this._contract?.completion.criteria) {
979
+ const criteriaIds = this._contract.completion.criteria.map((c) => c.id);
980
+ this._state.initializeCriteria(criteriaIds);
981
+ }
982
+ }
983
+ // ========================================================================
984
+ // Public API
985
+ // ========================================================================
986
+ /**
987
+ * Get the current session state
988
+ */
989
+ getSessionState() {
990
+ return {
991
+ iteration: this._iteration,
992
+ status: this._sessionStatus,
993
+ startedAt: this._startedAt,
994
+ lastUpdate: this._lastUpdate
995
+ };
996
+ }
997
+ /**
998
+ * Get the completion recommendation (if any)
999
+ */
1000
+ getCompletionRecommendation() {
1001
+ return this._completionRecommendation;
1002
+ }
1003
+ /**
1004
+ * Get the execution contract
1005
+ */
1006
+ getContract() {
1007
+ return this._contract;
1008
+ }
1009
+ /**
1010
+ * Get the execution state
1011
+ */
1012
+ getExecutionState() {
1013
+ return this._state;
1014
+ }
1015
+ /**
1016
+ * Get the failure memory
1017
+ */
1018
+ getFailureMemory() {
1019
+ return this._failures;
1020
+ }
1021
+ /**
1022
+ * Check if session is blocked by failures (for gate check)
1023
+ */
1024
+ isBlockedByFailures() {
1025
+ const criteria = this._state.getAllCriteriaStatus();
1026
+ for (const [criterionId] of criteria.entries()) {
1027
+ const criterionFailures = this._failures.getFailuresForCriterion(criterionId);
1028
+ for (const failure of criterionFailures) {
1029
+ if (failure.count >= this._config.failureMemory.retryThreshold) {
1030
+ return true;
1031
+ }
1032
+ }
1033
+ }
1034
+ return false;
1035
+ }
1036
+ /**
1037
+ * Run one execution iteration
1038
+ *
1039
+ * This is the core loop method:
1040
+ * 1. Gate check
1041
+ * 2. Context assembly
1042
+ * 3. Executor execution
1043
+ * 4. Result recording
1044
+ * 5. Completion detection
1045
+ *
1046
+ * @returns Result of the iteration
1047
+ */
1048
+ async runIteration() {
1049
+ this._lastUpdate = Date.now();
1050
+ if (this._iteration === 0) {
1051
+ this._startedAt = this._lastUpdate;
1052
+ }
1053
+ const gateResult = this._checkGate();
1054
+ if (!gateResult.allowed) {
1055
+ this._sessionStatus = this._mapGateReasonToStatus(gateResult.reason);
1056
+ return {
1057
+ continue: false,
1058
+ reason: gateResult.reason ?? "unknown",
1059
+ gateResult
1060
+ };
1061
+ }
1062
+ this._sessionStatus = "running";
1063
+ const context = this._assembleContext();
1064
+ const executorResult = await this._execute(context);
1065
+ this._recordResult(executorResult);
1066
+ const completion = this._checkCompletion();
1067
+ this._iteration++;
1068
+ if (completion.ready) {
1069
+ this._sessionStatus = "completed";
1070
+ this._completionRecommendation = completion;
1071
+ return {
1072
+ continue: false,
1073
+ reason: "complete",
1074
+ completion
1075
+ };
1076
+ }
1077
+ return {
1078
+ continue: true,
1079
+ executorResult,
1080
+ progress: this._state.getProgress()
1081
+ };
1082
+ }
1083
+ /**
1084
+ * Run the full execution loop until completion
1085
+ *
1086
+ * @returns Final session state
1087
+ */
1088
+ async run() {
1089
+ while (true) {
1090
+ const result = await this.runIteration();
1091
+ if (!result.continue) {
1092
+ break;
1093
+ }
1094
+ if (this._iteration >= this._config.maxIterations) {
1095
+ this._sessionStatus = "blocked";
1096
+ break;
1097
+ }
1098
+ }
1099
+ return this.getSessionState();
1100
+ }
1101
+ /**
1102
+ * Abort the session
1103
+ */
1104
+ abort() {
1105
+ this._sessionStatus = "aborted";
1106
+ this._lastUpdate = Date.now();
1107
+ }
1108
+ // ========================================================================
1109
+ // Private Methods
1110
+ // ========================================================================
1111
+ /**
1112
+ * Step 1: Gate check
1113
+ */
1114
+ _checkGate() {
1115
+ if (!this._spec) {
1116
+ return { allowed: false, reason: "spec_not_found" };
1117
+ }
1118
+ return ExecutionGate.check(this._spec, {
1119
+ iteration: this._iteration,
1120
+ config: { maxIterations: this._config.maxIterations },
1121
+ isBlockedByFailures: () => this.isBlockedByFailures()
1122
+ });
1123
+ }
1124
+ /**
1125
+ * Step 2: Context assembly
1126
+ */
1127
+ _assembleContext() {
1128
+ if (!this._contract) {
1129
+ throw new Error("Cannot assemble context without contract");
1130
+ }
1131
+ const nextPending = this._state.getNextPendingCriterion();
1132
+ if (nextPending) {
1133
+ this._state.setCriterionStatus(nextPending, "in_progress");
1134
+ }
1135
+ const focus = nextPending ? createWorkingFocus(
1136
+ nextPending,
1137
+ this._getCriterionText(nextPending),
1138
+ this._state.getLastToolCall()?.files?.[0]
1139
+ ) : void 0;
1140
+ return ContextAssembler.assemble(
1141
+ {
1142
+ contract: this._contract,
1143
+ iteration: this._iteration,
1144
+ progress: this._state.getProgress(),
1145
+ touchedFiles: this._state.getTouchedFiles(),
1146
+ criteriaStatus: this._state.getAllCriteriaStatus(),
1147
+ failureSummary: this._failures.summarize(),
1148
+ currentFocus: focus,
1149
+ lastToolCall: this._state.getLastToolCall()
1150
+ },
1151
+ this._config.contextAssembler
1152
+ );
1153
+ }
1154
+ /**
1155
+ * Step 3: Executor execution
1156
+ */
1157
+ async _execute(context) {
1158
+ return await this._config.executor.execute(context);
1159
+ }
1160
+ /**
1161
+ * Step 4: Record result
1162
+ */
1163
+ _recordResult(result) {
1164
+ const summary = createToolCallSummary(
1165
+ result.tool ?? "unknown",
1166
+ result.success,
1167
+ result.filesModified,
1168
+ result.error ? String(result.error) : void 0
1169
+ );
1170
+ this._state.recordToolCall(summary);
1171
+ if (result.filesModified) {
1172
+ this._state.touchFiles(result.filesModified);
1173
+ }
1174
+ if (!result.success) {
1175
+ const pattern = result.tool ? PatternExtractor.fromToolOutput(result.tool, result.error) : null;
1176
+ if (pattern && result.tool) {
1177
+ const currentCriterion = this._state.getCriteriaByStatus("in_progress")[0] ?? this._state.getCriteriaByStatus("failed")[0] ?? "unknown";
1178
+ this._failures.record({
1179
+ criterionId: currentCriterion,
1180
+ pattern,
1181
+ hint: PatternExtractor.extractHint(result.error)
1182
+ });
1183
+ if (currentCriterion !== "unknown") {
1184
+ const inProgress = this._state.getCriteriaByStatus("in_progress")[0];
1185
+ if (inProgress) {
1186
+ this._state.setCriterionStatus(inProgress, "pending");
1187
+ }
1188
+ }
1189
+ }
1190
+ } else {
1191
+ const currentCriterion = this._state.getCriteriaByStatus("in_progress")[0];
1192
+ if (currentCriterion) {
1193
+ const verificationMode = this._contract?.completion.verification.mode ?? "none";
1194
+ const finalStatus = verificationMode === "none" ? "verified" : "satisfied";
1195
+ this._state.setCriterionStatus(currentCriterion, finalStatus);
1196
+ }
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Step 5: Check completion
1201
+ */
1202
+ _checkCompletion() {
1203
+ if (!this._spec) {
1204
+ return createRecommendation(
1205
+ "",
1206
+ false,
1207
+ "low",
1208
+ createEvidence(/* @__PURE__ */ new Map(), []),
1209
+ "abort",
1210
+ "No spec provided",
1211
+ false
1212
+ );
1213
+ }
1214
+ const evidence = this._buildEvidence();
1215
+ const progress = this._state.getProgress();
1216
+ const verificationMode = this._contract?.completion.verification.mode ?? "none";
1217
+ return CompletionDetector.evaluate(this._spec, progress, evidence, verificationMode);
1218
+ }
1219
+ /**
1220
+ * Build completion evidence from current state
1221
+ */
1222
+ _buildEvidence() {
1223
+ return createEvidence(
1224
+ this._state.getAllCriteriaStatus(),
1225
+ this._state.getTouchedFiles()
1226
+ );
1227
+ }
1228
+ /**
1229
+ * Get criterion text by ID
1230
+ */
1231
+ _getCriterionText(criterionId) {
1232
+ const criterion = this._contract?.completion.criteria.find((c) => c.id === criterionId);
1233
+ return criterion?.description ?? criterionId;
1234
+ }
1235
+ /**
1236
+ * Map gate reason to session status
1237
+ */
1238
+ _mapGateReasonToStatus(reason) {
1239
+ switch (reason) {
1240
+ case "spec_not_found":
1241
+ case "spec_cancelled":
1242
+ case "spec_already_implemented":
1243
+ return "blocked";
1244
+ case "max_iterations_exceeded":
1245
+ case "blocked_by_failures":
1246
+ return "failed";
1247
+ default:
1248
+ return "blocked";
1249
+ }
1250
+ }
1251
+ };
1252
+ function createSession(spec, config) {
1253
+ return new ExecutionSession(spec, config);
1254
+ }
1255
+ export {
1256
+ CompletionDetector,
1257
+ ContextAssembler,
1258
+ ContractFactory,
1259
+ DEFAULT_ASSEMBLE_OPTIONS,
1260
+ DEFAULT_FAILURE_CONFIG,
1261
+ EMPTY_BOUNDARIES,
1262
+ EMPTY_COMPLETION,
1263
+ EMPTY_SNAPSHOT,
1264
+ ExecutionGate,
1265
+ ExecutionSession,
1266
+ ExecutionState,
1267
+ FailureMemory,
1268
+ PatternExtractor,
1269
+ RalphAdapter,
1270
+ computeProgress,
1271
+ createEvidence,
1272
+ createFailureResult,
1273
+ createProposal,
1274
+ createRalphAdapter,
1275
+ createRecommendation,
1276
+ createSession,
1277
+ createSuccessResult,
1278
+ createToolCallSummary,
1279
+ createWorkingFocus,
1280
+ isExecutorAvailable,
1281
+ restoreFromSnapshot
1282
+ };