@orkestrel/scaffold 0.0.108 → 0.0.109

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 (157) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/bench.js +326 -91
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +110 -14
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/chain.js +280 -0
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +188 -26
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +142 -43
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +240 -62
  7. package/dist/agents/skills/orkestrel-dispatch/scripts/patch.js +323 -0
  8. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +169 -32
  9. package/dist/agents/skills/orkestrel-dispatch/scripts/sweep.js +290 -69
  10. package/dist/agents/skills/orkestrel-falsify/scripts/mutate.js +560 -0
  11. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +1052 -205
  12. package/dist/agents/skills/orkestrel-harden/scripts/measure.js +361 -0
  13. package/dist/agents/skills/orkestrel-harden/scripts/titles.js +455 -80
  14. package/dist/agents/skills/orkestrel-parallel/scripts/cases.js +327 -0
  15. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +298 -51
  16. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +170 -13
  17. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +956 -191
  18. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +707 -112
  19. package/dist/agents/skills/orkestrel-publish/scripts/window.js +235 -41
  20. package/dist/agents/skills/orkestrel-scout/scripts/map.js +341 -54
  21. package/dist/agents/templates/brief.md +1 -1
  22. package/dist/bin/main.js +51 -46
  23. package/dist/bin/main.js.map +1 -1
  24. package/dist/host/AGENTS.md +23 -21
  25. package/dist/host/agents/orchestration.md +43 -18
  26. package/dist/host/agents/platforms/windows.md +3 -3
  27. package/dist/host/agents/skills/orkestrel-align/SKILL.md +10 -13
  28. package/dist/host/agents/skills/orkestrel-align/references/fleet.md +4 -10
  29. package/dist/host/agents/skills/orkestrel-align/references/integration.md +1 -1
  30. package/dist/host/agents/skills/orkestrel-build/SKILL.md +16 -48
  31. package/dist/host/agents/skills/orkestrel-build/agents/openai.yaml +1 -1
  32. package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +19 -16
  33. package/dist/host/agents/skills/orkestrel-debrief/agents/openai.yaml +1 -1
  34. package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +15 -28
  35. package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +5 -6
  36. package/dist/host/agents/skills/orkestrel-debrief/references/retention.md +4 -6
  37. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +31 -20
  38. package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +3 -4
  39. package/dist/host/agents/skills/orkestrel-dispatch/scripts/bench.ts +366 -75
  40. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +126 -16
  41. package/dist/host/agents/skills/orkestrel-dispatch/scripts/chain.ts +323 -0
  42. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +220 -31
  43. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +176 -33
  44. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +294 -59
  45. package/dist/host/agents/skills/orkestrel-dispatch/scripts/patch.ts +401 -0
  46. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +199 -34
  47. package/dist/host/agents/skills/orkestrel-dispatch/scripts/sweep.ts +330 -69
  48. package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +10 -8
  49. package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +1 -5
  50. package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +1 -1
  51. package/dist/host/agents/skills/orkestrel-falsify/scripts/mutate.ts +701 -0
  52. package/dist/host/agents/skills/orkestrel-harden/SKILL.md +16 -12
  53. package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +23 -81
  54. package/dist/host/agents/skills/orkestrel-harden/references/centralization.md +4 -5
  55. package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -0
  56. package/dist/host/agents/skills/orkestrel-harden/references/document.md +33 -0
  57. package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +6 -3
  58. package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +8 -4
  59. package/dist/host/agents/skills/orkestrel-harden/references/research.md +2 -0
  60. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +1272 -279
  61. package/dist/host/agents/skills/orkestrel-harden/scripts/measure.ts +425 -0
  62. package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +603 -98
  63. package/dist/host/agents/skills/orkestrel-journey/SKILL.md +75 -98
  64. package/dist/host/agents/skills/orkestrel-journey/references/captures.md +54 -17
  65. package/dist/host/agents/skills/orkestrel-journey/references/decide.md +1 -20
  66. package/dist/host/agents/skills/orkestrel-journey/references/layer.md +21 -31
  67. package/dist/host/agents/skills/orkestrel-journey/references/styles.md +1 -1
  68. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +28 -24
  69. package/dist/host/agents/skills/orkestrel-parallel/references/claude.md +15 -13
  70. package/dist/host/agents/skills/orkestrel-parallel/references/codex.md +1 -0
  71. package/dist/host/agents/skills/orkestrel-parallel/references/cursor.md +13 -13
  72. package/dist/host/agents/skills/orkestrel-parallel/scripts/cases.ts +389 -0
  73. package/dist/host/agents/skills/orkestrel-polish/SKILL.md +16 -20
  74. package/dist/host/agents/skills/orkestrel-polish/references/capture-harness.md +16 -26
  75. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +12 -26
  76. package/dist/host/agents/skills/orkestrel-publish/agents/openai.yaml +2 -2
  77. package/dist/host/agents/skills/orkestrel-publish/references/adopt.md +23 -0
  78. package/dist/host/agents/skills/orkestrel-publish/references/release.md +4 -4
  79. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +10 -9
  80. package/dist/host/agents/skills/orkestrel-publish/references/window.md +20 -25
  81. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +348 -59
  82. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +202 -22
  83. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1061 -191
  84. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +812 -140
  85. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +280 -38
  86. package/dist/host/agents/skills/orkestrel-scout/SKILL.md +3 -3
  87. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +428 -70
  88. package/dist/host/agents/templates/brief.md +1 -1
  89. package/dist/host/agents/transports/claude-cli.md +3 -3
  90. package/dist/host/agents/transports/codex.md +2 -7
  91. package/dist/host/agents/transports/cursor.md +2 -3
  92. package/dist/host/claude/AGENTS.md +2 -2
  93. package/dist/host/claude/agents/analyst.md +3 -3
  94. package/dist/host/claude/agents/astra.md +3 -3
  95. package/dist/host/claude/agents/builder.md +1 -1
  96. package/dist/host/claude/agents/checker.md +1 -1
  97. package/dist/host/claude/agents/distiller.md +1 -1
  98. package/dist/host/claude/agents/grok.md +1 -1
  99. package/dist/host/claude/agents/opus.md +2 -2
  100. package/dist/host/claude/agents/orkestrel.md +57 -90
  101. package/dist/host/claude/agents/researcher.md +1 -1
  102. package/dist/host/claude/agents/reviewer.md +4 -0
  103. package/dist/host/claude/agents/scout.md +1 -1
  104. package/dist/host/claude/agents/verifier.md +1 -1
  105. package/dist/host/claude/rules/application.md +14 -21
  106. package/dist/host/claude/rules/architecture.md +34 -44
  107. package/dist/host/claude/rules/browser.md +3 -4
  108. package/dist/host/claude/rules/campaign.md +2 -2
  109. package/dist/host/claude/rules/documentation.md +25 -12
  110. package/dist/host/claude/rules/names.md +9 -4
  111. package/dist/host/claude/rules/patterns.md +6 -7
  112. package/dist/host/claude/rules/portability.md +1 -1
  113. package/dist/host/claude/rules/quality.md +4 -5
  114. package/dist/host/claude/rules/styles.md +2 -2
  115. package/dist/host/claude/rules/tests.md +57 -114
  116. package/dist/host/claude/rules/typescript.md +7 -9
  117. package/dist/host/claude/rules/workspace.md +21 -19
  118. package/dist/host/claude/rules/writing.md +2 -5
  119. package/dist/host/claude/skills/orkestrel-dispatch/SKILL.md +1 -1
  120. package/dist/host/claude/skills/orkestrel-harden/SKILL.md +1 -1
  121. package/dist/host/claude/skills/orkestrel-publish/SKILL.md +1 -1
  122. package/dist/host/claude/skills/orkestrel-scout/SKILL.md +1 -1
  123. package/dist/host/codex/agents/analyst.toml +9 -3
  124. package/dist/host/codex/agents/astra.toml +3 -4
  125. package/dist/host/codex/agents/builder.toml +1 -1
  126. package/dist/host/codex/agents/opus.toml +3 -4
  127. package/dist/host/codex/agents/orkestrel.toml +1 -1
  128. package/dist/host/codex/agents/researcher.toml +2 -2
  129. package/dist/host/codex/agents/verifier.toml +1 -1
  130. package/dist/host/configs/helpers.ts +21 -1
  131. package/dist/host/configs/policy.ts +299 -15
  132. package/dist/host/dotfiles/oxlintrc.json +137 -3
  133. package/dist/host/guides/agent.md +111 -134
  134. package/dist/host/guides/capture.md +801 -0
  135. package/dist/host/guides/probe.md +27 -8
  136. package/dist/host/guides/scaffold.md +96 -18
  137. package/dist/host/guides/study.md +534 -440
  138. package/dist/host/guides/test.md +111 -44
  139. package/dist/host/manifest.json +160 -240
  140. package/dist/host/tests/config.test.ts +971 -187
  141. package/dist/host/tests/policy.test.ts +127 -1
  142. package/dist/host/tests/setupPolicy.ts +893 -33
  143. package/dist/src/core/index.cjs +214 -86
  144. package/dist/src/core/index.cjs.map +1 -1
  145. package/dist/src/core/index.d.cts +122 -38
  146. package/dist/src/core/index.d.ts +122 -38
  147. package/dist/src/core/index.js +209 -85
  148. package/dist/src/core/index.js.map +1 -1
  149. package/dist/src/server/index.cjs +20 -27
  150. package/dist/src/server/index.cjs.map +1 -1
  151. package/dist/src/server/index.js +21 -28
  152. package/dist/src/server/index.js.map +1 -1
  153. package/package.json +11 -11
  154. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +0 -283
  155. package/dist/host/agents/skills/orkestrel-dispatch/references/bench.md +0 -25
  156. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +0 -333
  157. package/dist/host/guides/supervisor.md +0 -4890
@@ -0,0 +1,323 @@
1
+ // Apply an edit file of exact old-to-new replacements. Run from the checkout root:
2
+ // node .agents/skills/orkestrel-dispatch/scripts/patch.ts EDITS.md (--plan | --apply) [--skip N ...]
3
+ // An edit file holds blocks of this shape, in the order they apply:
4
+ // ### path/relative/to/the/working/directory
5
+ // reason: one line saying why
6
+ // old:
7
+ // ```ts
8
+ // the exact text to replace
9
+ // ```
10
+ // new:
11
+ // ```ts
12
+ // the replacement text
13
+ // ```
14
+ // A fence opens with three or more backticks and closes with the same count, so a block can carry a
15
+ // shorter fence. Prose outside the blocks is ignored, but a stray `###`, `reason:`, `old:`, or `new:`
16
+ // line that opens no complete block is refused. Every `old` text must occur exactly once in its file
17
+ // after the earlier edits to that file, however the blocks spell its path (`src/a.ts` and `./src/a.ts`
18
+ // name one file), and nothing is written unless every selected edit resolves.
19
+ // `--plan` prints each edit's resolution and writes nothing; `--apply` writes the files after every
20
+ // edit resolved. `--skip N` leaves out edit number N, as the plan numbers them, and repeats. The
21
+ // script refuses a flag it does not read, the `--flag=value` form included.
22
+ // Exit 0 when every selected edit resolved, 1 when one did not, 64 on usage.
23
+ import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
24
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
25
+ /** Lists the flags that each select one operation. */
26
+ export const MODE_FLAGS = ['--plan', '--apply'];
27
+ /** Lists the flags that take a value. */
28
+ export const VALUE_FLAGS = ['--skip'];
29
+ /** Lists every flag the script reads, value flags and switches alike. */
30
+ export const FLAGS = [...MODE_FLAGS, ...VALUE_FLAGS];
31
+ /** Holds the usage line printed when a call names no edit file or no mode, or two. */
32
+ export const USAGE = 'usage: patch.ts EDITS.md (--plan | --apply) [--skip N ...]; name one mode; --skip repeats';
33
+ /** Matches one edit block with its path, reason, and fenced old and new texts. */
34
+ export const EDIT_BLOCK = /^### (.+?)[ \t]*\n+reason:[ \t]*(.*?)[ \t]*\n+old:\n+(`{3,})[^\n`]*\n([\s\S]*?)\n\3[ \t]*\n+new:\n+(`{3,})[^\n`]*\n([\s\S]*?)\n?\5[ \t]*$/gmu;
35
+ /** Matches a line that belongs to an edit block and so must not remain outside one. */
36
+ export const STRAY_LINE = /^(?:### |reason:|old:|new:)/u;
37
+ /**
38
+ * Names the value flags a caller gave with no value, at any occurrence.
39
+ *
40
+ * @param argv - The arguments after the script path.
41
+ * @param names - The flags that take a value.
42
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
43
+ */
44
+ export function readMissingFlags(argv, names) {
45
+ return names.filter((name) => argv.some((argument, index) => argument === name && isMissingValue(argv, index)));
46
+ }
47
+ /**
48
+ * Checks whether the argument after a flag is absent or is another flag.
49
+ *
50
+ * @param argv - The arguments after the script path.
51
+ * @param index - The position of the flag.
52
+ * @returns True when no value follows the flag; false otherwise.
53
+ */
54
+ export function isMissingValue(argv, index) {
55
+ const value = argv[index + 1];
56
+ return value === undefined || value.startsWith('--');
57
+ }
58
+ /**
59
+ * Names the flags a caller gave that the script does not read.
60
+ *
61
+ * @remarks
62
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
63
+ * scan.
64
+ *
65
+ * @param argv - The arguments after the script path.
66
+ * @param flags - Every flag the script reads, value flags and switches alike.
67
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
68
+ */
69
+ export function readUnknownFlags(argv, flags) {
70
+ const end = argv.indexOf('--');
71
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
72
+ }
73
+ /**
74
+ * Reads every value that follows a repeatable flag.
75
+ *
76
+ * @param argv - The arguments after the script path.
77
+ * @param name - The flag, such as `--skip`.
78
+ * @returns The values in order, empty when the flag never appears.
79
+ */
80
+ export function readOptions(argv, name) {
81
+ return argv.flatMap((argument, index) => {
82
+ const value = argv[index + 1];
83
+ return argument === name && value !== undefined && !value.startsWith('--') ? [value] : [];
84
+ });
85
+ }
86
+ /**
87
+ * Reads the arguments that are neither a flag nor the value of a value flag.
88
+ *
89
+ * @param argv - The arguments after the script path.
90
+ * @param valueFlags - The flags that take a value.
91
+ * @returns The positional arguments in order.
92
+ */
93
+ export function readPositionals(argv, valueFlags) {
94
+ return argv.filter((argument, index) => !argument.startsWith('--') && !valueFlags.includes(argv[index - 1] ?? ''));
95
+ }
96
+ /**
97
+ * Quotes an untrusted text for a diagnostic, JSON-encoded and cut to a bounded length.
98
+ *
99
+ * @param text - The text to quote.
100
+ * @returns The JSON string literal of the text, ending in an ellipsis inside the quotes when cut.
101
+ */
102
+ export function quote(text) {
103
+ return JSON.stringify(text.length > 80 ? `${text.slice(0, 80)}…` : text);
104
+ }
105
+ /**
106
+ * Reads the edit blocks of an edit file in order.
107
+ *
108
+ * @param text - The edit file's text; a carriage return before a line break is ignored.
109
+ * @returns Each complete block as an edit numbered from 1.
110
+ */
111
+ export function parseEdits(text) {
112
+ return [...text.replace(/\r\n/gu, '\n').matchAll(EDIT_BLOCK)].map((match, index) => ({
113
+ number: index + 1,
114
+ path: match[1] ?? '',
115
+ reason: match[2] ?? '',
116
+ before: match[4] ?? '',
117
+ after: match[6] ?? '',
118
+ }));
119
+ }
120
+ /**
121
+ * Finds the lines of an edit file that open or continue a block no complete block holds.
122
+ *
123
+ * @param text - The edit file's text.
124
+ * @returns Each stray `###`, `reason:`, `old:`, or `new:` line, in order.
125
+ */
126
+ export function readStrays(text) {
127
+ return text
128
+ .replace(/\r\n/gu, '\n')
129
+ .replace(EDIT_BLOCK, '')
130
+ .split(/\r\n|\n/u)
131
+ .filter((line) => STRAY_LINE.test(line));
132
+ }
133
+ /**
134
+ * Counts how many places a text occurs in another, overlapping places included.
135
+ *
136
+ * @param text - The text to search.
137
+ * @param part - The text to find; an empty text occurs nowhere.
138
+ * @returns The number of start positions at which `part` occurs.
139
+ */
140
+ export function countOccurrences(text, part) {
141
+ if (part === '')
142
+ return 0;
143
+ const escaped = part.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
144
+ return [...text.matchAll(new RegExp(`(?=${escaped})`, 'gu'))].length;
145
+ }
146
+ /**
147
+ * Applies a file's edits in order, each against the text the earlier edits left.
148
+ *
149
+ * @param text - The file's text before any edit.
150
+ * @param edits - The file's edits in file order.
151
+ * @returns The text after the edits that resolved and one outcome per edit; an edit whose `old` text
152
+ * does not occur exactly once changes nothing and carries its problem.
153
+ */
154
+ export function applyEdits(text, edits) {
155
+ return edits.reduce((state, edit) => {
156
+ const count = countOccurrences(state.text, edit.before);
157
+ const problem = count === 1 ? undefined : `old text occurs ${String(count)} times`;
158
+ return {
159
+ text: problem === undefined ? state.text.replace(edit.before, () => edit.after) : state.text,
160
+ outcomes: [...state.outcomes, describeOutcome(edit, problem)],
161
+ };
162
+ }, { text, outcomes: [] });
163
+ }
164
+ /**
165
+ * Describes what happened to one edit.
166
+ *
167
+ * @param edit - The edit.
168
+ * @param problem - The problem that stopped it, or undefined when it resolved.
169
+ * @returns The edit's number, path, and reason with the problem.
170
+ */
171
+ export function describeOutcome(edit, problem) {
172
+ return { number: edit.number, path: edit.path, reason: edit.reason, problem };
173
+ }
174
+ /**
175
+ * Checks whether a path sits strictly inside a directory.
176
+ *
177
+ * @param root - The directory.
178
+ * @param target - The path to check.
179
+ * @returns True when `target` is below `root`; false for `root` itself and for any path outside.
180
+ */
181
+ export function isInside(root, target) {
182
+ const path = relative(root, target);
183
+ return path !== '' && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path);
184
+ }
185
+ /**
186
+ * Reads the numbers a caller asked to skip.
187
+ *
188
+ * @param values - The `--skip` values.
189
+ * @param count - How many edits the file holds.
190
+ * @returns Each value that is not a whole number from 1 to `count`, in order.
191
+ */
192
+ export function readInvalidSkips(values, count) {
193
+ return values.filter((value) => !/^[1-9]\d*$/u.test(value) || Number(value) > count);
194
+ }
195
+ /**
196
+ * Resolves every selected edit against the files under a root, writing nothing.
197
+ *
198
+ * @param root - The working directory the edit paths resolve against.
199
+ * @param edits - The selected edits in file order.
200
+ * @returns One outcome per edit in file order, and the text of each file whose edits all resolved.
201
+ */
202
+ export function resolveEdits(root, edits) {
203
+ const keyed = edits.map((edit) => {
204
+ const target = resolve(root, edit.path);
205
+ return { edit, target, key: readFileKey(target) };
206
+ });
207
+ const results = [...new Set(keyed.map((item) => item.key))].map((key) => {
208
+ const group = keyed.filter((item) => item.key === key);
209
+ const own = group.map((item) => item.edit);
210
+ const path = own[0]?.path ?? '';
211
+ const target = group[0]?.target ?? '';
212
+ const problem = group
213
+ .map((item) => readTargetProblem(root, item.target))
214
+ .find((candidate) => candidate !== undefined);
215
+ if (problem !== undefined) {
216
+ return { path, text: undefined, outcomes: own.map((edit) => describeOutcome(edit, problem)) };
217
+ }
218
+ const resolution = applyEdits(readFileSync(target, 'utf8'), own);
219
+ const failed = resolution.outcomes.some((outcome) => outcome.problem !== undefined);
220
+ return { path, text: failed ? undefined : resolution.text, outcomes: resolution.outcomes };
221
+ });
222
+ return {
223
+ outcomes: results.flatMap((result) => result.outcomes).sort((a, b) => a.number - b.number),
224
+ files: results.flatMap((result) => result.text === undefined ? [] : [{ path: result.path, text: result.text }]),
225
+ };
226
+ }
227
+ /**
228
+ * Names the file an edit target stands for, so every spelling of one file shares one key.
229
+ *
230
+ * @remarks
231
+ * The host's own path resolution folds case where the host folds it, so `./src/a.ts`, `src/a.ts`, and
232
+ * on a case-folding host `SRC/A.ts` name one key when the file exists.
233
+ *
234
+ * @param target - The resolved target path.
235
+ * @returns The file's canonical path when it exists, and the resolved path otherwise.
236
+ */
237
+ export function readFileKey(target) {
238
+ return existsSync(target) ? realpathSync.native(target) : target;
239
+ }
240
+ /**
241
+ * Names why an edit target cannot be edited.
242
+ *
243
+ * @param root - The working directory the edit paths resolve against.
244
+ * @param target - The resolved target path.
245
+ * @returns The problem, or undefined when the target is a file inside the root.
246
+ */
247
+ export function readTargetProblem(root, target) {
248
+ if (!isInside(root, target))
249
+ return 'path leaves the working directory';
250
+ if (!existsSync(target) || !statSync(target).isFile())
251
+ return 'no such file';
252
+ return undefined;
253
+ }
254
+ /**
255
+ * Runs the script on its arguments.
256
+ *
257
+ * @param argv - The arguments after the script path.
258
+ * @returns The exit code: 0 when every selected edit resolved, 1 when one did not, 64 on usage.
259
+ */
260
+ export function main(argv) {
261
+ const unknown = readUnknownFlags(argv, FLAGS);
262
+ if (unknown.length > 0) {
263
+ console.error(`patch: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
264
+ return 64;
265
+ }
266
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
267
+ if (missing.length > 0) {
268
+ console.error(`patch: ${missing.join(', ')} given with no value`);
269
+ return 64;
270
+ }
271
+ const modes = MODE_FLAGS.filter((flag) => argv.includes(flag));
272
+ const sources = readPositionals(argv, VALUE_FLAGS);
273
+ const source = sources[0];
274
+ if (modes.length !== 1 || sources.length !== 1 || source === undefined) {
275
+ console.error(USAGE);
276
+ return 64;
277
+ }
278
+ if (!existsSync(source) || !statSync(source).isFile()) {
279
+ console.error(`patch: ${quote(source)} is not a readable edit file; ${USAGE}`);
280
+ return 64;
281
+ }
282
+ const text = readFileSync(source, 'utf8');
283
+ const edits = parseEdits(text);
284
+ const strays = readStrays(text);
285
+ if (edits.length === 0 || strays.length > 0) {
286
+ console.error(`patch: ${quote(source)} ${edits.length === 0 ? 'holds no complete edit block' : `has a line that opens no complete block: ${strays.map(quote).join(', ')}`}; a block is a "### path" line, a "reason:" line, and fenced "old:" and "new:" texts`);
287
+ return 64;
288
+ }
289
+ const skips = readOptions(argv, '--skip');
290
+ const invalid = readInvalidSkips(skips, edits.length);
291
+ if (invalid.length > 0) {
292
+ console.error(`patch: --skip ${invalid.map(quote).join(', ')} names no edit; the file holds edits 1 to ${String(edits.length)}`);
293
+ return 64;
294
+ }
295
+ const skipped = new Set(skips.map(Number));
296
+ const selected = edits.filter((edit) => !skipped.has(edit.number));
297
+ const root = process.cwd();
298
+ const resolution = resolveEdits(root, selected);
299
+ for (const edit of edits) {
300
+ const outcome = resolution.outcomes.find((candidate) => candidate.number === edit.number);
301
+ if (outcome === undefined)
302
+ console.log(`#${String(edit.number)} skipped ${edit.path}`);
303
+ else if (outcome.problem === undefined) {
304
+ console.log(`#${String(edit.number)} ok ${edit.path} — ${edit.reason}`);
305
+ }
306
+ else
307
+ console.log(`#${String(edit.number)} FAIL ${edit.path}: ${outcome.problem}`);
308
+ }
309
+ if (resolution.outcomes.some((outcome) => outcome.problem !== undefined)) {
310
+ console.log('nothing written');
311
+ return 1;
312
+ }
313
+ const apply = argv.includes('--apply');
314
+ if (apply) {
315
+ for (const file of resolution.files)
316
+ writeFileSync(resolve(root, file.path), file.text);
317
+ }
318
+ console.log(`${apply ? 'applied' : 'planned'} edits to ${String(resolution.files.length)} file(s)`);
319
+ return 0;
320
+ }
321
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
322
+ process.exitCode = main(process.argv.slice(2));
323
+ }
@@ -9,30 +9,134 @@
9
9
  // launch.ts wrote (`exit`, `capped`, `durationMs`) and the journal's `bytes`, `ageMs`, and last
10
10
  // event, which is the liveness reading for a run still going. The answer is written to --out when
11
11
  // given, and a journal with no session or answer exits 3 so a driver cannot pass off an empty lane.
12
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
12
+ // Name one mode per run; the script refuses a flag it does not read, the `--flag=value` form
13
+ // included. Exit 0 when the lane answered, 3 when it carries no session or answer, 64 on usage.
14
+ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
13
15
  import { dirname } from 'node:path';
14
- import { readMissingFlags, readNumber, readOption, readString } from "./helpers.js";
15
- function readEvents(path) {
16
+ /** Lists the flags that each select one journal reader. */
17
+ export const MODE_FLAGS = ['--cursor', '--claude', '--codex'];
18
+ /** Lists the flags that take a value. */
19
+ export const VALUE_FLAGS = [...MODE_FLAGS, '--out'];
20
+ /** Lists every flag the script reads, value flags and switches alike. */
21
+ export const FLAGS = VALUE_FLAGS;
22
+ /** Holds the usage line printed when a call names no journal or two modes. */
23
+ export const USAGE = 'usage: result.ts (--cursor JOURNAL | --claude JOURNAL | --codex JOURNAL) [--out PATH]; name one mode';
24
+ /**
25
+ * Reads the value that follows a flag.
26
+ *
27
+ * @remarks
28
+ * The first occurrence of a flag wins, a following value that opens with `--` is a missing value,
29
+ * and the `--flag=value` form is not read.
30
+ *
31
+ * @param argv - The arguments after the script path.
32
+ * @param name - The flag, such as `--out`.
33
+ * @returns The value after the flag, or undefined when the flag is absent, last, or followed by
34
+ * another flag.
35
+ */
36
+ export function readOption(argv, name) {
37
+ const index = argv.indexOf(name);
38
+ const value = index === -1 ? undefined : argv[index + 1];
39
+ return value === undefined || value.startsWith('--') ? undefined : value;
40
+ }
41
+ /**
42
+ * Names the value flags a caller gave with no value.
43
+ *
44
+ * @param argv - The arguments after the script path.
45
+ * @param names - The flags that take a value.
46
+ * @returns Each named flag that appears last or is followed by another flag, in argument order.
47
+ */
48
+ export function readMissingFlags(argv, names) {
49
+ return names.filter((name) => argv.includes(name) && readOption(argv, name) === undefined);
50
+ }
51
+ /**
52
+ * Names the flags a caller gave that the script does not read.
53
+ *
54
+ * @remarks
55
+ * A `--flag=value` argument is unknown, because the script reads no such form. A bare `--` ends the
56
+ * scan.
57
+ *
58
+ * @param argv - The arguments after the script path.
59
+ * @param flags - Every flag the script reads, value flags and switches alike.
60
+ * @returns Each argument that opens with `--` and is not a read flag, in argument order.
61
+ */
62
+ export function readUnknownFlags(argv, flags) {
63
+ const end = argv.indexOf('--');
64
+ return (end === -1 ? argv : argv.slice(0, end)).filter((argument) => argument.startsWith('--') && !flags.includes(argument));
65
+ }
66
+ /**
67
+ * Reads the last non-empty line of a text as a JSON object.
68
+ *
69
+ * @param text - Text whose last line is JSON, such as one journal line.
70
+ * @returns The parsed object, or undefined when the last line is not a JSON object.
71
+ */
72
+ export function parseJsonLine(text) {
73
+ const line = text
74
+ .split(/\r\n|\n/)
75
+ .filter((candidate) => candidate.trim() !== '')
76
+ .at(-1);
77
+ if (line === undefined)
78
+ return undefined;
79
+ try {
80
+ const parsed = JSON.parse(line);
81
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
82
+ return Object.fromEntries(Object.entries(parsed));
83
+ }
84
+ }
85
+ catch { }
86
+ return undefined;
87
+ }
88
+ /**
89
+ * Reads a string member of a record.
90
+ *
91
+ * @param record - The record, or undefined.
92
+ * @param key - The member name.
93
+ * @returns The string, or undefined when the member is absent or not a string.
94
+ */
95
+ export function readString(record, key) {
96
+ const value = record?.[key];
97
+ return typeof value === 'string' ? value : undefined;
98
+ }
99
+ /**
100
+ * Reads a finite number member of a record.
101
+ *
102
+ * @param record - The record, or undefined.
103
+ * @param key - The member name.
104
+ * @returns The number, or undefined when the member is absent, not a number, or not finite.
105
+ */
106
+ export function readNumber(record, key) {
107
+ const value = record?.[key];
108
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
109
+ }
110
+ /**
111
+ * Reads every JSON object line of a journal.
112
+ *
113
+ * @param path - The journal file.
114
+ * @returns Each line that parses as a JSON object, in order; a blank or malformed line is skipped.
115
+ */
116
+ export function readEvents(path) {
16
117
  return readFileSync(path, 'utf8')
17
118
  .split(/\r\n|\n/)
18
119
  .flatMap((line) => {
19
- if (line.trim() === '')
20
- return [];
21
- try {
22
- const parsed = JSON.parse(line);
23
- return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
24
- ? [Object.fromEntries(Object.entries(parsed))]
25
- : [];
26
- }
27
- catch {
28
- return [];
29
- }
120
+ const event = parseJsonLine(line);
121
+ return event === undefined ? [] : [event];
30
122
  });
31
123
  }
32
- function readAnswer(text) {
124
+ /**
125
+ * Reads an answer text as present only when it carries something besides whitespace.
126
+ *
127
+ * @param text - The answer text, or undefined.
128
+ * @returns The text unchanged, or undefined when it is absent or blank.
129
+ */
130
+ export function readAnswer(text) {
33
131
  return text === undefined || text.trim() === '' ? undefined : text;
34
132
  }
35
- function readStream(events) {
133
+ /**
134
+ * Reads a Cursor or Claude stream journal's events.
135
+ *
136
+ * @param events - The journal's events.
137
+ * @returns The session from the `init` event and the answer and duration from the `result` event.
138
+ */
139
+ export function readStream(events) {
36
140
  const init = events.find((event) => event.type === 'system' && event.subtype === 'init');
37
141
  const result = events.find((event) => event.type === 'result');
38
142
  return {
@@ -41,7 +145,14 @@ function readStream(events) {
41
145
  durationMs: result === undefined ? undefined : readNumber(result, 'duration_ms'),
42
146
  };
43
147
  }
44
- function readCodex(path, events) {
148
+ /**
149
+ * Reads a Codex journal's events and its sibling last-message file.
150
+ *
151
+ * @param path - The journal path; the answer sits beside it in `<name>-last.md`.
152
+ * @param events - The journal's events.
153
+ * @returns The thread id from the `thread.started` event and the answer from the last-message file.
154
+ */
155
+ export function readCodex(path, events) {
45
156
  const started = events.find((event) => event.type === 'thread.started');
46
157
  const last = path.replace(/\.jsonl$/u, '-last.md');
47
158
  return {
@@ -50,7 +161,14 @@ function readCodex(path, events) {
50
161
  durationMs: undefined,
51
162
  };
52
163
  }
53
- function readErrors(path) {
164
+ /**
165
+ * Reads the exit trailer launch.ts wrote to the errors file beside a journal.
166
+ *
167
+ * @param path - The journal path; the trailer sits beside it in `<name>.err`.
168
+ * @returns The exit code, whether the run was capped, and its duration; each is undefined when the
169
+ * file is absent or carries no trailer.
170
+ */
171
+ export function readErrors(path) {
54
172
  const errors = path.replace(/\.jsonl$/u, '.err');
55
173
  if (!existsSync(errors))
56
174
  return { exit: undefined, capped: undefined, durationMs: undefined };
@@ -64,47 +182,66 @@ function readErrors(path) {
64
182
  durationMs: Number(match[3]),
65
183
  };
66
184
  }
67
- function describeEvent(event) {
185
+ /**
186
+ * Describes an event by its type and subtype.
187
+ *
188
+ * @param event - The event, or undefined.
189
+ * @returns `type` or `type/subtype`, or undefined when there is no event.
190
+ */
191
+ export function describeEvent(event) {
68
192
  if (event === undefined)
69
193
  return undefined;
70
194
  const type = readString(event, 'type');
71
195
  const subtype = readString(event, 'subtype');
72
196
  return subtype === undefined ? type : `${type}/${subtype}`;
73
197
  }
74
- function main(argv) {
75
- const missing = readMissingFlags(argv, ['--cursor', '--claude', '--codex', '--out']);
198
+ /**
199
+ * Runs the script on its arguments.
200
+ *
201
+ * @param argv - The arguments after the script path.
202
+ * @returns The exit code: 0 when the lane answered, 3 when it carries no session or answer, 64 on usage.
203
+ */
204
+ export function main(argv) {
205
+ const unknown = readUnknownFlags(argv, FLAGS);
206
+ if (unknown.length > 0) {
207
+ console.error(`result: ${unknown.join(', ')} is not a flag this script reads; ${USAGE}`);
208
+ return 64;
209
+ }
210
+ const missing = readMissingFlags(argv, VALUE_FLAGS);
76
211
  if (missing.length > 0) {
77
212
  console.error(`result: ${missing.join(', ')} given with no value`);
78
213
  return 64;
79
214
  }
80
- const modes = ['--cursor', '--claude', '--codex'].filter((flag) => argv.includes(flag));
215
+ const modes = MODE_FLAGS.filter((flag) => argv.includes(flag));
81
216
  const mode = modes[0];
82
217
  const path = mode === undefined ? undefined : readOption(argv, mode);
83
218
  const out = readOption(argv, '--out');
84
219
  if (modes.length !== 1 || mode === undefined || path === undefined || !existsSync(path)) {
85
- console.error('usage: result.ts (--cursor JOURNAL | --claude JOURNAL | --codex JOURNAL) [--out PATH]; name one mode');
220
+ console.error(USAGE);
86
221
  return 64;
87
222
  }
88
223
  const events = readEvents(path);
89
- const reading = mode === '--codex' ? readCodex(path, events) : readStream(events);
224
+ const result = mode === '--codex' ? readCodex(path, events) : readStream(events);
90
225
  const errors = readErrors(path);
91
226
  const stat = statSync(path);
92
- if (reading.answer !== undefined && out !== undefined) {
227
+ if (result.answer !== undefined && out !== undefined) {
93
228
  mkdirSync(dirname(out), { recursive: true });
94
- writeFileSync(out, reading.answer);
229
+ writeFileSync(out, result.answer);
95
230
  }
96
231
  console.log(JSON.stringify({
97
232
  journal: path,
98
- session: reading.session,
99
- durationMs: reading.durationMs ?? errors.durationMs,
233
+ session: result.session,
234
+ durationMs: result.durationMs ?? errors.durationMs,
100
235
  exit: errors.exit,
101
236
  capped: errors.capped,
102
237
  bytes: stat.size,
103
238
  ageMs: Math.max(0, Math.round(Date.now() - stat.mtimeMs)),
104
239
  lastEvent: describeEvent(events.at(-1)),
105
- answerLength: reading.answer?.length ?? 0,
106
- out: reading.answer === undefined ? undefined : out,
240
+ answerLength: result.answer?.length ?? 0,
241
+ out: result.answer === undefined ? undefined : out,
107
242
  }));
108
- return reading.session === undefined || reading.answer === undefined ? 3 : 0;
243
+ return result.session === undefined || result.answer === undefined ? 3 : 0;
244
+ }
245
+ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === import.meta.filename) {
246
+ process.exitCode = main(process.argv.slice(2));
109
247
  }
110
- process.exitCode = main(process.argv.slice(2));