compounding-loop 0.2.0 → 0.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 (149) hide show
  1. package/README.md +19 -3
  2. package/dist/cli/claim.js +84 -0
  3. package/dist/cli/claim.js.map +1 -0
  4. package/dist/cli/draft.js +69 -0
  5. package/dist/cli/draft.js.map +1 -0
  6. package/dist/cli/index.js +284 -1
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/laneRound.js +96 -0
  9. package/dist/cli/laneRound.js.map +1 -0
  10. package/dist/cli/laneStatus.js +3 -1
  11. package/dist/cli/laneStatus.js.map +1 -1
  12. package/dist/cli/lanesInit.js +8 -2
  13. package/dist/cli/lanesInit.js.map +1 -1
  14. package/dist/cli/lock.js +22 -2
  15. package/dist/cli/lock.js.map +1 -1
  16. package/dist/cli/loops.js +7 -0
  17. package/dist/cli/loops.js.map +1 -1
  18. package/dist/cli/next.js +18 -0
  19. package/dist/cli/next.js.map +1 -0
  20. package/dist/cli/pace.js +41 -0
  21. package/dist/cli/pace.js.map +1 -0
  22. package/dist/cli/pause.js +81 -0
  23. package/dist/cli/pause.js.map +1 -0
  24. package/dist/cli/run.js +18 -6
  25. package/dist/cli/run.js.map +1 -1
  26. package/dist/cli/runs.js +118 -0
  27. package/dist/cli/runs.js.map +1 -0
  28. package/dist/cli/schedule.js +177 -0
  29. package/dist/cli/schedule.js.map +1 -0
  30. package/dist/cli/shipCheck.js +59 -0
  31. package/dist/cli/shipCheck.js.map +1 -0
  32. package/dist/core/brief.js +35 -12
  33. package/dist/core/brief.js.map +1 -1
  34. package/dist/core/draftBrief.js +68 -0
  35. package/dist/core/draftBrief.js.map +1 -0
  36. package/dist/core/laneRound.js +63 -0
  37. package/dist/core/laneRound.js.map +1 -0
  38. package/dist/core/lanes.js +15 -2
  39. package/dist/core/lanes.js.map +1 -1
  40. package/dist/core/lanesInit.js +19 -0
  41. package/dist/core/lanesInit.js.map +1 -1
  42. package/dist/core/next.js +34 -0
  43. package/dist/core/next.js.map +1 -0
  44. package/dist/core/parse/changelog.js +41 -0
  45. package/dist/core/parse/changelog.js.map +1 -0
  46. package/dist/core/parse/claim.js +24 -0
  47. package/dist/core/parse/claim.js.map +1 -0
  48. package/dist/core/parse/controlRoom.js +38 -0
  49. package/dist/core/parse/controlRoom.js.map +1 -0
  50. package/dist/core/parse/index.js +17 -0
  51. package/dist/core/parse/index.js.map +1 -0
  52. package/dist/core/parse/markers.js +20 -0
  53. package/dist/core/parse/markers.js.map +1 -0
  54. package/dist/core/parse/outcomes.js +51 -0
  55. package/dist/core/parse/outcomes.js.map +1 -0
  56. package/dist/core/parse/paused.js +26 -0
  57. package/dist/core/parse/paused.js.map +1 -0
  58. package/dist/core/parse/policy.js +56 -0
  59. package/dist/core/parse/policy.js.map +1 -0
  60. package/dist/core/parse/runs.js +132 -0
  61. package/dist/core/parse/runs.js.map +1 -0
  62. package/dist/core/parse/schedule.js +101 -0
  63. package/dist/core/parse/schedule.js.map +1 -0
  64. package/dist/core/parse/steer.js +68 -0
  65. package/dist/core/parse/steer.js.map +1 -0
  66. package/dist/core/parse/task.js +96 -0
  67. package/dist/core/parse/task.js.map +1 -0
  68. package/dist/core/repo.js +18 -8
  69. package/dist/core/repo.js.map +1 -1
  70. package/dist/core/runPrompt.js +8 -2
  71. package/dist/core/runPrompt.js.map +1 -1
  72. package/dist/core/shipCheck.js +45 -0
  73. package/dist/core/shipCheck.js.map +1 -0
  74. package/dist/core/table.js +3 -1
  75. package/dist/core/table.js.map +1 -1
  76. package/dist/server/actionBody.js +54 -0
  77. package/dist/server/actionBody.js.map +1 -0
  78. package/dist/server/checkLabels.js +32 -0
  79. package/dist/server/checkLabels.js.map +1 -0
  80. package/dist/server/claude.js +18 -0
  81. package/dist/server/claude.js.map +1 -0
  82. package/dist/server/controls.js +99 -0
  83. package/dist/server/controls.js.map +1 -0
  84. package/dist/server/data.js +9 -3
  85. package/dist/server/data.js.map +1 -1
  86. package/dist/server/draftInputs.js +33 -0
  87. package/dist/server/draftInputs.js.map +1 -0
  88. package/dist/server/health.js +9 -30
  89. package/dist/server/health.js.map +1 -1
  90. package/dist/server/index.js +77 -0
  91. package/dist/server/index.js.map +1 -1
  92. package/dist/server/laneChecks.js +25 -0
  93. package/dist/server/laneChecks.js.map +1 -0
  94. package/dist/server/laneSchedule.js +15 -0
  95. package/dist/server/laneSchedule.js.map +1 -0
  96. package/dist/server/live/build.js +178 -0
  97. package/dist/server/live/build.js.map +1 -0
  98. package/dist/server/live/github.js +54 -0
  99. package/dist/server/live/github.js.map +1 -0
  100. package/dist/server/live/index.js +11 -0
  101. package/dist/server/live/index.js.map +1 -0
  102. package/dist/server/live/merge.js +16 -0
  103. package/dist/server/live/merge.js.map +1 -0
  104. package/dist/server/live/urls.js +61 -0
  105. package/dist/server/live/urls.js.map +1 -0
  106. package/dist/server/loops.js +263 -41
  107. package/dist/server/loops.js.map +1 -1
  108. package/dist/server/paceNote.js +13 -0
  109. package/dist/server/paceNote.js.map +1 -0
  110. package/dist/server/roundState.js +51 -0
  111. package/dist/server/roundState.js.map +1 -0
  112. package/dist/server/runs.js +144 -0
  113. package/dist/server/runs.js.map +1 -0
  114. package/dist/server/shipped.js +35 -0
  115. package/dist/server/shipped.js.map +1 -0
  116. package/dist/server/static-export.js +22 -1
  117. package/dist/server/static-export.js.map +1 -1
  118. package/dist/server/taskBullets.js +33 -0
  119. package/dist/server/taskBullets.js.map +1 -0
  120. package/dist/server/this-repo.json +2818 -106
  121. package/dist/web/assets/index-BozIcdeb.js +88 -0
  122. package/dist/web/assets/index-Te_olhNA.css +2 -0
  123. package/dist/web/index.html +2 -2
  124. package/method/COMPOUNDING_LOOP.md +26 -0
  125. package/method/retro.md +7 -0
  126. package/package.json +1 -1
  127. package/plugin/.claude-plugin/plugin.json +1 -1
  128. package/plugin/commands/loop-lanes.md +11 -0
  129. package/plugin/commands/loop-next.md +11 -0
  130. package/plugin/commands/loop-pause.md +10 -0
  131. package/runners/github-actions/lanes.yml +55 -2
  132. package/runners/local/README.md +3 -1
  133. package/runners/local/compounding-loop.service +20 -0
  134. package/runners/local/compounding-loop.timer +11 -0
  135. package/runners/routine/README.md +5 -1
  136. package/runners/routine/control-room-prompt.md +4 -14
  137. package/runners/routine/lane-prompt.md +4 -10
  138. package/runners/routine/prompt.md +2 -2
  139. package/templates/lanes/README.md +5 -1
  140. package/templates/lanes/claude-section.md +5 -1
  141. package/templates/lanes/control-run.md +37 -0
  142. package/templates/lanes/lane-run.md +33 -0
  143. package/templates/lanes/schedule.json +4 -0
  144. package/templates/lanes/steer.md +3 -0
  145. package/templates/lanes/task.md +1 -1
  146. package/templates/static-site/.ai/loop-prompt.md +1 -1
  147. package/templates/static-site/CLAUDE.md +2 -0
  148. package/dist/web/assets/index-B16QCSnY.css +0 -2
  149. package/dist/web/assets/index-CS82DKbF.js +0 -49
@@ -0,0 +1,26 @@
1
+ // `.ai/PAUSED`: one line, `who · paused <UTC ISO>[ · reason]`. Its existence stops every lane run. Pure.
2
+ export const PAUSED_FILE = '.ai/PAUSED';
3
+ export function formatPaused(by, at, reason) {
4
+ return `${by} · paused ${at.toISOString()}${reason ? ` · ${reason}` : ''}\n`;
5
+ }
6
+ /**
7
+ * Reads a PAUSED file. The file's existence is what pauses the loop, so a hand-written or malformed file still
8
+ * yields a value (`by` is then its first line).
9
+ */
10
+ export function parsePaused(text) {
11
+ const line = text.split(/\r?\n/).find((l) => l.trim() !== '')?.trim() ?? '';
12
+ const match = /^(.+?)\s+·\s+paused\s+(\S+)(?:\s+·\s+(.+))?$/.exec(line);
13
+ const at = match ? new Date(match[2]) : undefined;
14
+ if (!match || !at || Number.isNaN(at.getTime()))
15
+ return { by: line || 'unknown' };
16
+ return { by: match[1].trim(), at, ...(match[3] && { reason: match[3].trim() }) };
17
+ }
18
+ /** True when the repo-relative file list contains `.ai/PAUSED`. */
19
+ export function isPaused(files) {
20
+ return files.some((f) => f.replace(/\\/g, '/') === PAUSED_FILE);
21
+ }
22
+ /** One line for `loop status` and the CLI: who paused and when. */
23
+ export function describePaused(paused) {
24
+ return `Paused by ${paused.by}${paused.at ? ` since ${paused.at.toISOString().slice(0, 16).replace('T', ' ')} UTC` : ''}${paused.reason ? ` (${paused.reason})` : ''}`;
25
+ }
26
+ //# sourceMappingURL=paused.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paused.js","sourceRoot":"","sources":["../../../src/core/parse/paused.ts"],"names":[],"mappings":"AAAA,yGAAyG;AASzG,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAExC,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,EAAQ,EAAE,MAAe;IAChE,OAAO,GAAG,EAAE,aAAa,EAAE,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;AAC/E,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC5E,MAAM,KAAK,GAAG,8CAA8C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,CAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5D,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,IAAI,SAAS,EAAE,CAAC;IAClF,OAAO,EAAE,EAAE,EAAG,KAAK,CAAC,CAAC,CAAY,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;AAC/F,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,QAAQ,CAAC,KAAwB;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,WAAW,CAAC,CAAC;AAClE,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,aAAa,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AACzK,CAAC"}
@@ -0,0 +1,56 @@
1
+ export const POLICY_FILE = '.ai/policy.json';
2
+ export const PACES = ['careful', 'balanced', 'fast', 'sprint'];
3
+ export const DEFAULT_PACE = 'balanced';
4
+ export const isPace = (value) => typeof value === 'string' && PACES.includes(value);
5
+ export const emptyPolicy = () => ({ version: 1, pace: DEFAULT_PACE, lanes: {} });
6
+ /** One plain line per preset on what it skips; the floor (no push with a failing check, CI on every push) holds for all. */
7
+ export const PACE_SUMMARY = {
8
+ careful: 'Full tests after every unit and before push; a reviewer on every unit; 40 minutes per run.',
9
+ balanced: 'Related tests after each unit, full tests before push; one reviewer per run; 40 minutes.',
10
+ fast: 'Like balanced, but a reviewer only for shared files or another lane\'s contract; 50 minutes.',
11
+ sprint: 'Typecheck and lint per unit, unit tests before push, e2e once at the end; no reviewer; 50 minutes or 70% context.',
12
+ };
13
+ /** Reads a policy file. Anything unreadable, or any unknown preset, falls back to `balanced`. */
14
+ export function parsePolicy(text) {
15
+ try {
16
+ const raw = JSON.parse(text);
17
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
18
+ return emptyPolicy();
19
+ const { pace, lanes } = raw;
20
+ const overrides = {};
21
+ if (lanes && typeof lanes === 'object' && !Array.isArray(lanes)) {
22
+ for (const [lane, value] of Object.entries(lanes)) {
23
+ const lanePace = value && typeof value === 'object' ? value.pace : undefined;
24
+ if (lane !== '__proto__' && isPace(lanePace))
25
+ overrides[lane] = { pace: lanePace };
26
+ }
27
+ }
28
+ return { version: 1, pace: isPace(pace) ? pace : DEFAULT_PACE, lanes: overrides };
29
+ }
30
+ catch {
31
+ return emptyPolicy();
32
+ }
33
+ }
34
+ /** Why a present policy file is not used as written (so `loop pace show` can say it reads as balanced), or undefined when it is fine. */
35
+ export function policyProblem(text) {
36
+ let raw;
37
+ try {
38
+ raw = JSON.parse(text);
39
+ }
40
+ catch {
41
+ return `${POLICY_FILE} is not valid JSON, so every lane runs at ${DEFAULT_PACE}.`;
42
+ }
43
+ const pace = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw.pace : undefined;
44
+ return isPace(pace) ? undefined : `${POLICY_FILE} has no valid "pace" (${PACES.join(', ')}), so it reads as ${DEFAULT_PACE}.`;
45
+ }
46
+ export function formatPolicy(policy) {
47
+ const lanes = Object.entries(policy.lanes).map(([lane, e]) => ` ${JSON.stringify(lane)}: ${JSON.stringify(e)}`);
48
+ return `{\n "version": 1,\n "pace": ${JSON.stringify(policy.pace)},\n "lanes": {${lanes.length ? `\n${lanes.join(',\n')}\n ` : ''}}\n}\n`;
49
+ }
50
+ /** The pace a lane runs at: its own override, else the global one. `lane` undefined (the control room) gets the global pace. */
51
+ export const paceFor = (policy, lane) => (lane !== undefined ? policy.lanes[lane]?.pace : undefined) ?? policy.pace;
52
+ /** The policy after setting the global pace (no lane) or one lane's override. */
53
+ export function withPace(policy, pace, lane) {
54
+ return lane === undefined ? { ...policy, pace } : { ...policy, lanes: { ...policy.lanes, [lane]: { pace } } };
55
+ }
56
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../../../src/core/parse/policy.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAE7C,MAAM,CAAC,MAAM,KAAK,GAAoB,CAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;AAChF,MAAM,CAAC,MAAM,YAAY,GAAS,UAAU,CAAC;AAE7C,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,KAAc,EAAiB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAK,KAA2B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAEnI,MAAM,CAAC,MAAM,WAAW,GAAG,GAAW,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;AAEzF,4HAA4H;AAC5H,MAAM,CAAC,MAAM,YAAY,GAAyB;IAChD,OAAO,EAAE,4FAA4F;IACrG,QAAQ,EAAE,0FAA0F;IACpG,IAAI,EAAE,8FAA8F;IACpG,MAAM,EAAE,mHAAmH;CAC5H,CAAC;AAEF,iGAAiG;AACjG,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,WAAW,EAAE,CAAC;QAChF,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,GAA8B,CAAC;QACvD,MAAM,SAAS,GAAoB,EAAE,CAAC;QACtC,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;gBAClD,MAAM,QAAQ,GAAG,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,KAAiC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC1G,IAAI,IAAI,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,CAAC;oBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;YACrF,CAAC;QACH,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED,yIAAyI;AACzI,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,WAAW,6CAA6C,YAAY,GAAG,CAAC;IACpF,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,GAA+B,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACvH,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,WAAW,yBAAyB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB,YAAY,GAAG,CAAC;AAChI,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnH,OAAO,iCAAiC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC;AAChJ,CAAC;AAED,gIAAgI;AAChI,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAc,EAAE,IAAa,EAAQ,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC;AAE3I,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,IAAU,EAAE,IAAa;IAChE,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAChH,CAAC"}
@@ -0,0 +1,132 @@
1
+ // The per-lane run log (`.ai/lanes/<lane>/runs.jsonl`) and the scheduled slots a lane's cron promises. Pure.
2
+ export const runLogPath = (lane) => `.ai/lanes/${lane}/runs.jsonl`;
3
+ export function formatRunEntry(entry) {
4
+ return `${JSON.stringify(entry)}\n`;
5
+ }
6
+ const isCount = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 0;
7
+ function toEntry(value) {
8
+ if (!value || typeof value !== 'object')
9
+ return undefined;
10
+ const v = value;
11
+ if (typeof v.at !== 'string' || Number.isNaN(new Date(v.at).getTime()) || typeof v.lane !== 'string' || typeof v.pushed !== 'boolean')
12
+ return undefined;
13
+ const tests = v.tests;
14
+ return {
15
+ at: v.at,
16
+ lane: v.lane,
17
+ ...(typeof v.session === 'string' && { session: v.session }),
18
+ units: Array.isArray(v.units) ? v.units.filter((u) => typeof u === 'string') : [],
19
+ ...(tests && isCount(tests.passed) && isCount(tests.failed) && { tests: { passed: tests.passed, failed: tests.failed } }),
20
+ pushed: v.pushed,
21
+ ...(typeof v.note === 'string' && { note: v.note }),
22
+ };
23
+ }
24
+ /** Reads a runs.jsonl; lines that are not valid entries (a half-written line, a merge marker) are skipped. Oldest first. */
25
+ export function parseRunLog(text) {
26
+ const entries = [];
27
+ for (const line of text.split(/\r?\n/)) {
28
+ if (!line.trim())
29
+ continue;
30
+ try {
31
+ const entry = toEntry(JSON.parse(line));
32
+ if (entry)
33
+ entries.push(entry);
34
+ }
35
+ catch {
36
+ // skipped on purpose: see above
37
+ }
38
+ }
39
+ return entries.sort((a, b) => a.at.localeCompare(b.at));
40
+ }
41
+ // ---- cron ----
42
+ const RANGES = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
43
+ /** Expands one cron field (star, a number, a list, a range, or a step such as star-slash-15) into the matching numbers; undefined when unreadable. */
44
+ function expandField(field, [min, max]) {
45
+ const out = new Set();
46
+ for (const part of field.split(',')) {
47
+ const match = /^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/.exec(part);
48
+ if (!match)
49
+ return undefined;
50
+ const step = match[2] === undefined ? 1 : Number(match[2]);
51
+ if (step < 1)
52
+ return undefined;
53
+ let [from, to] = [min, max];
54
+ if (match[1] !== '*') {
55
+ const [a, b] = match[1].split('-');
56
+ from = Number(a);
57
+ to = b === undefined ? (match[2] === undefined ? from : max) : Number(b);
58
+ }
59
+ if (from < min || to > max || from > to)
60
+ return undefined;
61
+ for (let n = from; n <= to; n += step)
62
+ out.add(n);
63
+ }
64
+ return out;
65
+ }
66
+ /**
67
+ * The UTC minutes in (from, to] at which a 5-field cron expression fires. Empty for an expression this cannot read.
68
+ * Like cron, day-of-month and day-of-week both restrict when both are set (either may match).
69
+ */
70
+ export function expectedSlots(cron, from, to) {
71
+ const fields = cron.trim().split(/\s+/);
72
+ if (fields.length !== 5)
73
+ return [];
74
+ const sets = fields.map((f, i) => expandField(f, RANGES[i]));
75
+ const [minutes, hours, days, months, weekdays] = sets;
76
+ if (!minutes || !hours || !days || !months || !weekdays)
77
+ return [];
78
+ const dayRestricted = fields[2] !== '*';
79
+ const weekdayRestricted = fields[4] !== '*';
80
+ const slots = [];
81
+ const start = Math.floor(from.getTime() / 60_000) * 60_000 + 60_000;
82
+ for (let t = start; t <= to.getTime() && slots.length < 10_000; t += 60_000) {
83
+ const d = new Date(t);
84
+ if (!minutes.has(d.getUTCMinutes()) || !hours.has(d.getUTCHours()) || !months.has(d.getUTCMonth() + 1))
85
+ continue;
86
+ const dayOk = days.has(d.getUTCDate());
87
+ const weekdayOk = weekdays.has(d.getUTCDay()) || (d.getUTCDay() === 0 && weekdays.has(7));
88
+ if (dayRestricted && weekdayRestricted ? dayOk || weekdayOk : dayOk && weekdayOk)
89
+ slots.push(d);
90
+ }
91
+ return slots;
92
+ }
93
+ /**
94
+ * Pairs each scheduled slot with the run that answered it. A run belongs to the latest slot at or before its end
95
+ * time. A slot with no line but a commit before the next slot still counts as run (the agent forgot the log step)
96
+ * and is reported as `pushed`. Anything else is "no run recorded (usage limit, idle or failed)".
97
+ */
98
+ export function matchSlots(slots, entries, commitTimes = []) {
99
+ const sorted = [...slots].sort((a, b) => a.getTime() - b.getTime());
100
+ const slotFor = (time) => {
101
+ let found = -1;
102
+ for (let i = 0; i < sorted.length && sorted[i].getTime() <= time; i++)
103
+ found = i;
104
+ return found;
105
+ };
106
+ const bySlot = new Map();
107
+ for (const entry of entries) {
108
+ // A long run can end after the next slot starts: walk back to the latest slot nobody has claimed yet.
109
+ for (let i = slotFor(new Date(entry.at).getTime()); i >= 0; i--) {
110
+ if (!bySlot.has(i)) {
111
+ bySlot.set(i, entry);
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ const committed = new Set(commitTimes.map((c) => slotFor(c.getTime())).filter((i) => i >= 0));
117
+ return sorted.map((slot, i) => {
118
+ const entry = bySlot.get(i);
119
+ const iso = slot.toISOString();
120
+ if (entry)
121
+ return { slot: iso, entry, outcome: entry.tests && entry.tests.failed > 0 ? 'tests-failed' : entry.pushed ? 'pushed' : 'nothing-pushed' };
122
+ return { slot: iso, outcome: committed.has(i) ? 'pushed' : 'no-run-recorded' };
123
+ });
124
+ }
125
+ /** How many scheduled runs in a row, newest first, left no trace. The dashboard raises an attention item at 3. */
126
+ export function missedInARow(results) {
127
+ let count = 0;
128
+ for (let i = results.length - 1; i >= 0 && results[i]?.outcome === 'no-run-recorded'; i--)
129
+ count++;
130
+ return count;
131
+ }
132
+ //# sourceMappingURL=runs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runs.js","sourceRoot":"","sources":["../../../src/core/parse/runs.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAa7G,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,aAAa,IAAI,aAAa,CAAC;AAE3E,MAAM,UAAU,cAAc,CAAC,KAAe;IAC5C,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC;AACtC,CAAC;AAED,MAAM,OAAO,GAAG,CAAC,CAAU,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEpG,SAAS,OAAO,CAAC,KAAc;IAC7B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxJ,MAAM,KAAK,GAAG,CAAC,CAAC,KAA4C,CAAC;IAC7D,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,GAAG,CAAC,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC5D,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;QAC9F,GAAG,CAAC,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QACzH,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;KACpD,CAAC;AACJ,CAAC;AAED,4HAA4H;AAC5H,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,OAAO,GAAe,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;YAAE,SAAS;QAC3B,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YACxC,IAAI,KAAK;gBAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,gCAAgC;QAClC,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED,iBAAiB;AAEjB,MAAM,MAAM,GAA6C,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAEtG,sJAAsJ;AACtJ,SAAS,WAAW,CAAC,KAAa,EAAE,CAAC,GAAG,EAAE,GAAG,CAA4B;IACvE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,IAAI,GAAG,CAAC;YAAE,OAAO,SAAS,CAAC;QAC/B,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YACrB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAI,KAAK,CAAC,CAAC,CAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC/C,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACjB,EAAE,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,IAAI,GAAG,GAAG,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,GAAG,EAAE;YAAE,OAAO,SAAS,CAAC;QAC1D,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI;YAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,IAAU,EAAE,EAAQ;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAA8B,CAAC,CAAC,CAAC;IAC1F,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC;IACtD,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IACnE,MAAM,aAAa,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;IACxC,MAAM,iBAAiB,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;IAC5C,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,MAAM,CAAC,GAAG,MAAM,GAAG,MAAM,CAAC;IACpE,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC,IAAI,MAAM,EAAE,CAAC;QAC5E,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;YAAE,SAAS;QACjH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC;QACvC,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1F,IAAI,aAAa,IAAI,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC,KAAK,IAAI,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClG,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAWD;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB,EAAE,OAA4B,EAAE,WAAW,GAAoB,EAAE;IAChH,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IACpE,MAAM,OAAO,GAAG,CAAC,IAAY,EAAE,EAAE;QAC/B,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,IAAK,MAAM,CAAC,CAAC,CAAU,CAAC,OAAO,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE;YAAE,KAAK,GAAG,CAAC,CAAC;QAC3F,OAAO,KAAK,CAAC;IACf,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,sGAAsG;QACtG,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAChE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;gBACnB,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;gBACrB,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9F,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QAC/B,IAAI,KAAK;YAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC;QACrJ,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC;IACjF,CAAC,CAAC,CAAC;AACL,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,YAAY,CAAC,OAA8B;IACzD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,KAAK,iBAAiB,EAAE,CAAC,EAAE;QAAE,KAAK,EAAE,CAAC;IACnG,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,101 @@
1
+ import { unacknowledged } from './steer.js';
2
+ export const SCHEDULE_FILE = '.ai/schedule.json';
3
+ const HOUR_MS = 3_600_000;
4
+ export const emptySchedule = () => ({ version: 1, lanes: {} });
5
+ /** A lane with nothing in the file is due. */
6
+ export const dueEntry = () => ({ notBefore: null, sleeping: false, paused: false, reason: null });
7
+ const validTime = (v) => typeof v === 'string' && !Number.isNaN(new Date(v).getTime());
8
+ function toEntry(value) {
9
+ const v = value && typeof value === 'object' ? value : {};
10
+ return {
11
+ notBefore: validTime(v.notBefore) ? v.notBefore : null,
12
+ sleeping: v.sleeping === true,
13
+ paused: v.paused === true,
14
+ reason: typeof v.reason === 'string' && v.reason.trim() ? v.reason : null,
15
+ };
16
+ }
17
+ /** Reads a schedule file. Anything unreadable gives an empty schedule, so every lane stays due. */
18
+ export function parseSchedule(text) {
19
+ try {
20
+ const raw = JSON.parse(text);
21
+ const lanes = raw && typeof raw === 'object' ? raw.lanes : undefined;
22
+ if (!lanes || typeof lanes !== 'object' || Array.isArray(lanes))
23
+ return emptySchedule();
24
+ return { version: 1, lanes: Object.fromEntries(Object.entries(lanes).map(([lane, entry]) => [lane, toEntry(entry)])) };
25
+ }
26
+ catch {
27
+ return emptySchedule();
28
+ }
29
+ }
30
+ export function formatSchedule(schedule) {
31
+ const lanes = Object.entries(schedule.lanes).map(([lane, e]) => ` ${JSON.stringify(lane)}: ${JSON.stringify(e)}`);
32
+ return `{\n "version": 1,\n "lanes": {\n${lanes.join(',\n')}${lanes.length ? '\n' : ''} }\n}\n`;
33
+ }
34
+ /** Why a run at `now` should reply "Not due"; undefined when the lane is due. A missing entry is due. */
35
+ export function notDueReason(entry, now) {
36
+ if (!entry)
37
+ return undefined;
38
+ if (entry.paused)
39
+ return 'paused';
40
+ if (entry.sleeping)
41
+ return 'sleeping';
42
+ if (entry.notBefore && new Date(entry.notBefore).getTime() > now.getTime())
43
+ return 'snoozed';
44
+ return undefined;
45
+ }
46
+ export const isDue = (entry, now) => notDueReason(entry, now) === undefined;
47
+ /** Idle runs at the end of the log (a run with no units is idle; any run with units resets the count). */
48
+ export function trailingIdleRuns(runs) {
49
+ const sorted = [...runs].sort((a, b) => a.at.localeCompare(b.at));
50
+ let count = 0;
51
+ for (let i = sorted.length - 1; i >= 0 && sorted[i].units.length === 0; i--)
52
+ count++;
53
+ return count;
54
+ }
55
+ /**
56
+ * The idle backoff for one lane: 1 idle run -> not before last run + 3 h, 2 -> + 6 h, 3 or more -> sleeping.
57
+ * It only looks at the log, so a caller that has just woken the lane must skip it until a new run is logged.
58
+ * No trailing idle run leaves the entry alone (an owner's snooze or pause survives). A paused entry is never changed.
59
+ */
60
+ export function applyBackoff(runs, entry, _now) {
61
+ const current = entry ?? dueEntry();
62
+ const idle = trailingIdleRuns(runs);
63
+ if (idle === 0 || current.paused)
64
+ return current;
65
+ if (idle >= 3)
66
+ return { ...current, sleeping: true, notBefore: null, reason: `${idle} idle runs in a row` };
67
+ const last = [...runs].sort((a, b) => a.at.localeCompare(b.at)).at(-1);
68
+ const backoff = new Date(last.at).getTime() + (idle === 1 ? 3 : 6) * HOUR_MS;
69
+ // An owner's later snooze wins over the automatic backoff.
70
+ const notBefore = new Date(Math.max(backoff, current.notBefore ? new Date(current.notBefore).getTime() : 0)).toISOString();
71
+ return { ...current, sleeping: false, notBefore, reason: `${idle} idle run${idle === 1 ? '' : 's'} in a row` };
72
+ }
73
+ /** Why a sleeping or snoozed lane should be due again. Empty means keep sleeping. */
74
+ export function wakeReasons(input) {
75
+ const reasons = [];
76
+ if (unacknowledged(input.steer).length > 0)
77
+ reasons.push('steer');
78
+ const lane = input.lane.toLowerCase();
79
+ const since = input.lastRunAt ? new Date(input.lastRunAt).getTime() : -Infinity;
80
+ const stamp = (at) => new Date(at.includes('T') ? at : `${at.replace(' UTC', '').replace(' ', 'T')}:00Z`).getTime();
81
+ if (input.messages.some((m) => stamp(m.at) > since && m.to.some((t) => t.toLowerCase() === lane || t.toLowerCase() === 'all')))
82
+ reasons.push('message');
83
+ if (input.newRound)
84
+ reasons.push('new-round');
85
+ return reasons;
86
+ }
87
+ /** The entry after a wake: due now. A pause is a separate decision and stays. */
88
+ export const wakeEntry = (entry) => ({ ...(entry ?? dueEntry()), sleeping: false, notBefore: null, reason: null });
89
+ /** True when the entry holds the lane back only because of the automatic backoff (not an owner's pause). */
90
+ export const isBackedOff = (entry, now) => !entry?.paused && notDueReason(entry, now) !== undefined;
91
+ /**
92
+ * Reads the time a snooze lasts until: an ISO time, a date (midnight UTC), or a span from now such as `3h`, `2d`, `90m`.
93
+ * Undefined when unreadable or not after `now`.
94
+ */
95
+ export function parseUntil(text, now) {
96
+ const t = text.trim();
97
+ const span = /^\+?(\d+)\s*([mhd])$/i.exec(t);
98
+ const at = span ? new Date(now.getTime() + Number(span[1]) * ({ m: 60_000, h: HOUR_MS, d: 24 * HOUR_MS }[span[2].toLowerCase()])) : new Date(/^\d{4}-\d\d-\d\d$/.test(t) ? `${t}T00:00:00Z` : t);
99
+ return Number.isNaN(at.getTime()) || at.getTime() <= now.getTime() ? undefined : at;
100
+ }
101
+ //# sourceMappingURL=schedule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.js","sourceRoot":"","sources":["../../../src/core/parse/schedule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ export const steerPath = (lane) => `.ai/lanes/${lane}/steer.md`;
2
+ const HEADING = /^## (\d{4}-\d\d-\d\d) (\d\d:\d\d) UTC · (.+?) · (next-run|standing)\s*$/;
3
+ const ACK_DONE = /^→ done in run (\d+)\s*$/;
4
+ const ACK_NOT_DONE = /^→ not done:\s*(.*)$/;
5
+ export function steerHeading(at, author, scope) {
6
+ return `## ${at.toISOString().slice(0, 16).replace('T', ' ')} UTC · ${author} · ${scope}`;
7
+ }
8
+ /** One entry as it is written to the file (heading, text, blank line). Newlines in the author would break the heading. */
9
+ export function formatSteerEntry(entry) {
10
+ const author = entry.author.replace(/[\r\n·]+/g, ' ').replace(/\s+/g, ' ').trim() || 'unknown';
11
+ return `${steerHeading(entry.at, author, entry.scope)}\n${entry.text.trim().replace(/^##(?= )/gm, '###')}\n`;
12
+ }
13
+ function parseAck(line) {
14
+ const done = ACK_DONE.exec(line);
15
+ if (done)
16
+ return { state: 'done', run: Number(done[1]) };
17
+ const not = ACK_NOT_DONE.exec(line);
18
+ return not ? { state: 'not-done', why: (not[1] ?? '').trim() } : undefined;
19
+ }
20
+ /** Entries in file order (newest first by convention). Text before the first heading is the file's intro and is ignored. */
21
+ export function parseSteer(text) {
22
+ const entries = [];
23
+ let current;
24
+ const flush = () => {
25
+ if (!current)
26
+ return;
27
+ const [, date, time, author, scope] = current.head;
28
+ const lines = current.body;
29
+ const ackAt = lines.findIndex((l) => parseAck(l.trim()) !== undefined);
30
+ const ack = ackAt >= 0 ? parseAck(lines[ackAt].trim()) : undefined;
31
+ const body = (ackAt >= 0 ? lines.filter((_, i) => i !== ackAt) : lines).join('\n').trim();
32
+ entries.push({ at: `${date}T${time}:00.000Z`, author: author.trim(), scope, text: body, ...(ack && { ack }) });
33
+ };
34
+ for (const line of text.split(/\r?\n/)) {
35
+ const head = HEADING.exec(line);
36
+ if (head) {
37
+ flush();
38
+ current = { head, body: [] };
39
+ }
40
+ else if (/^## /.test(line)) {
41
+ flush();
42
+ current = undefined;
43
+ }
44
+ else
45
+ current?.body.push(line);
46
+ }
47
+ flush();
48
+ return entries;
49
+ }
50
+ /** Entries the lane has not read yet. A standing entry counts once, until its first acknowledgement. */
51
+ export const unacknowledged = (entries) => entries.filter((e) => !e.ack);
52
+ /** `next-run` entries first, then standing; each group newest first. This is the order a lane works in. */
53
+ export function steerOrder(entries) {
54
+ const byTimeDesc = (a, b) => b.at.localeCompare(a.at);
55
+ return [...unacknowledged(entries).filter((e) => e.scope === 'next-run').sort(byTimeDesc), ...entries.filter((e) => e.scope === 'standing').sort(byTimeDesc)];
56
+ }
57
+ /** Standing entries that still apply (acknowledged or not; they stay until removed). */
58
+ export const standingEntries = (entries) => entries.filter((e) => e.scope === 'standing');
59
+ /** Puts a new entry first (newest first), directly after the intro. An empty or missing file gets the intro-less entry alone. */
60
+ export function addSteerEntry(fileText, entry) {
61
+ const block = formatSteerEntry(entry);
62
+ const text = fileText.replace(/\r\n/g, '\n');
63
+ const firstEntry = text.search(/^## /m);
64
+ if (firstEntry < 0)
65
+ return `${text.trimEnd()}${text.trim() ? '\n\n' : ''}${block}`;
66
+ return `${text.slice(0, firstEntry)}${block}\n${text.slice(firstEntry)}`;
67
+ }
68
+ //# sourceMappingURL=steer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steer.js","sourceRoot":"","sources":["../../../src/core/parse/steer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ // Task-file parsing. Pure (no Node imports): the live dashboard runs it in the browser.
2
+ /** Reads the `Run: N / LIMIT` line of a task.md; undefined when the line is missing or malformed. */
3
+ export function parseRunCounter(taskText) {
4
+ const match = /^Run:\s*(\d+)\s*\/\s*(\d+)\b/m.exec(taskText);
5
+ if (!match)
6
+ return undefined;
7
+ return { run: Number(match[1]), limit: Number(match[2]) };
8
+ }
9
+ /** Body of the `## <heading>` section (up to the next `## `), matched by heading prefix, case-insensitive. Handoffs are appended, so callers wanting the newest ask for `last`. */
10
+ export function sectionOf(text, heading, which = 'first') {
11
+ const lines = text.split(/\r?\n/);
12
+ const want = heading.toLowerCase();
13
+ const isMatch = (l) => /^##\s/.test(l) && l.replace(/^##\s+/, '').trim().toLowerCase().startsWith(want);
14
+ const start = which === 'last' ? lines.map(isMatch).lastIndexOf(true) : lines.findIndex(isMatch);
15
+ if (start < 0)
16
+ return undefined;
17
+ const rest = lines.slice(start + 1);
18
+ const end = rest.findIndex((l) => /^##\s/.test(l));
19
+ return (end < 0 ? rest : rest.slice(0, end)).join('\n');
20
+ }
21
+ export function parseUnits(taskText) {
22
+ const body = sectionOf(taskText, 'Units');
23
+ if (!body)
24
+ return [];
25
+ const units = [];
26
+ for (const line of body.split(/\r?\n/)) {
27
+ const match = /^(\d+)\.\s+(.+?)\s*$/.exec(line);
28
+ if (match)
29
+ units.push({ number: Number(match[1]), text: match[2] });
30
+ }
31
+ return units;
32
+ }
33
+ /** Parses the parts of a lane's task.md that status screens need. Missing parts are simply absent. */
34
+ export function parseTaskFile(taskText) {
35
+ const status = /^Status:[ \t]*(.*)$/m.exec(taskText)?.[1]?.trim();
36
+ const handoff = sectionOf(taskText, 'Handoff', 'last')
37
+ ?.split(/\r?\n/)
38
+ .map((l) => l.trim())
39
+ .find((l) => l !== '');
40
+ const parsed = { units: parseUnits(taskText) };
41
+ const run = parseRunCounter(taskText);
42
+ if (run)
43
+ parsed.run = run;
44
+ if (status)
45
+ parsed.status = status;
46
+ if (handoff)
47
+ parsed.handoffFirstLine = handoff;
48
+ return parsed;
49
+ }
50
+ const NUMBER_LIST = String.raw `\d+(?:\s*[-\u2013]\s*\d+)?(?:\s*(?:,|and|&)\s*\d+(?:\s*[-\u2013]\s*\d+)?)*`;
51
+ const DONE_AFTER = /\b(?:shipped|finished|completed|done)\b[:\s]+([^\n]*)/gi;
52
+ const UNITS_IN_LINE = new RegExp(String.raw `\bunits?\s+(${NUMBER_LIST})\b(?!\s+(?:partly|partially|not))`, 'gi');
53
+ const BARE_LIST = new RegExp(String.raw `^\s*(?:with\s+)?(${NUMBER_LIST})\b(?!\s+(?:partly|partially|not))`, 'i');
54
+ const DONE_BEFORE = new RegExp(String.raw `\bunits?\s+(${NUMBER_LIST})\s+(?:are\s+|is\s+|were\s+)?(?:done|shipped|finished|complete[d]?)\b`, 'gi');
55
+ function expandNumbers(list, into) {
56
+ for (const part of list.split(/,|and|&/)) {
57
+ const range = /^\s*(\d+)(?:\s*[-\u2013]\s*(\d+))?\s*$/.exec(part);
58
+ if (!range)
59
+ continue;
60
+ const from = Number(range[1]);
61
+ const to = range[2] === undefined ? from : Number(range[2]);
62
+ for (let n = from; n <= Math.min(to, from + 200); n++)
63
+ into.add(n);
64
+ }
65
+ }
66
+ /**
67
+ * Unit numbers a lane says it finished, read from its `Status:` line and newest Handoff, e.g.
68
+ * "units 1-3 done" or "Run 2 shipped units 1, 2 and 4". "unit 5 partly" does not count.
69
+ */
70
+ export function doneUnits(taskText) {
71
+ const status = /^Status:[ \t]*(.*)$/m.exec(taskText)?.[1] ?? '';
72
+ const handoff = sectionOf(taskText, 'Handoff', 'last') ?? '';
73
+ // Only the lines before "Next:" describe finished work; what follows plans it.
74
+ const finished = handoff.split(/\r?\n/).filter((l) => !/^\s*next\b/i.test(l)).join('\n');
75
+ const done = new Set();
76
+ for (const text of [status, finished]) {
77
+ for (const m of text.matchAll(DONE_BEFORE))
78
+ expandNumbers(m[1], done);
79
+ for (const m of text.matchAll(DONE_AFTER)) {
80
+ const rest = m[1];
81
+ const named = [...rest.matchAll(UNITS_IN_LINE)];
82
+ for (const u of named)
83
+ expandNumbers(u[1], done);
84
+ const bare = named.length === 0 ? BARE_LIST.exec(rest) : null;
85
+ if (bare)
86
+ expandNumbers(bare[1], done);
87
+ }
88
+ }
89
+ return done;
90
+ }
91
+ /** Units not yet reported done, in file order. */
92
+ export function openUnits(taskText) {
93
+ const done = doneUnits(taskText);
94
+ return parseUnits(taskText).filter((u) => !done.has(u.number));
95
+ }
96
+ //# sourceMappingURL=task.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task.js","sourceRoot":"","sources":["../../../src/core/parse/task.ts"],"names":[],"mappings":"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"}
package/dist/core/repo.js CHANGED
@@ -2,14 +2,9 @@ import { execFile } from 'node:child_process';
2
2
  import { readdir, readFile, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { promisify } from 'node:util';
5
+ import { parseRunCounter } from './parse/task.js';
5
6
  const execFileAsync = promisify(execFile);
6
- /** Reads the `Run: N / LIMIT` line of a task.md; undefined when the line is missing or malformed. */
7
- export function parseRunCounter(taskText) {
8
- const match = /^Run:\s*(\d+)\s*\/\s*(\d+)\b/m.exec(taskText);
9
- if (!match)
10
- return undefined;
11
- return { run: Number(match[1]), limit: Number(match[2]) };
12
- }
7
+ export { parseRunCounter };
13
8
  /** Parses the single UTC timestamp line of `.ai/session.lock`; undefined when it is not a valid date. */
14
9
  export function parseLockTime(lockText) {
15
10
  const line = lockText.trim().split(/\r?\n/)[0]?.trim();
@@ -24,6 +19,21 @@ export function parseLockTime(lockText) {
24
19
  export function countDoneRounds(fileNames) {
25
20
  return fileNames.filter((name) => /^done-v\d+\.md$/.test(name)).length;
26
21
  }
22
+ /**
23
+ * Finished rounds. A brief that has `## Round N` headings is the source: every round heading not marked "current"
24
+ * counts once. Without such headings (a single-loop repo) the `done-vN.md` files are counted, since a handoff note
25
+ * named `done-v5.md` must not make a fifth round.
26
+ */
27
+ export function countRounds(briefText, doneFileNames) {
28
+ const rounds = new Set();
29
+ for (const line of (briefText ?? '').split(/\r?\n/)) {
30
+ const m = /^#{1,3}\s+Round\s+(\d+)\b(.*)$/i.exec(line);
31
+ // "### Round 5 done when" is a checklist inside a round, not a finished round.
32
+ if (m && !/\bcurrent\b/i.test(m[2] ?? '') && !/^\s*done when\b/i.test(m[2] ?? ''))
33
+ rounds.add(Number(m[1]));
34
+ }
35
+ return rounds.size > 0 ? rounds.size : countDoneRounds(doneFileNames);
36
+ }
27
37
  async function exists(file) {
28
38
  try {
29
39
  await stat(file);
@@ -67,7 +77,7 @@ export async function readLoopFacts(dir) {
67
77
  // The stop files sit at the repo root, per CLAUDE.md "Unattended mode".
68
78
  hasBlocked: await exists(path.join(dir, 'BLOCKED.md')),
69
79
  hasDone: await exists(path.join(dir, 'DONE.md')),
70
- roundsDone: countDoneRounds(aiFiles),
80
+ roundsDone: countRounds(await readOptional(path.join(dir, 'IDEA.md')), aiFiles),
71
81
  ...(lockAt && { lockAt }),
72
82
  ...(lastCommitAt && { lastCommitAt }),
73
83
  healthFailures: [],
@@ -1 +1 @@
1
- {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/core/repo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"repo.js","sourceRoot":"","sources":["../../src/core/repo.ts"],"names":[],"mappings":"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"}