@orkestrel/scaffold 0.0.101 → 0.0.103

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 (59) hide show
  1. package/dist/agents/skills/orkestrel-dispatch/scripts/brief.js +6 -14
  2. package/dist/agents/skills/orkestrel-dispatch/scripts/cite.js +41 -28
  3. package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +5 -12
  4. package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +25 -13
  5. package/dist/agents/skills/orkestrel-dispatch/scripts/login.js +18 -25
  6. package/dist/agents/skills/orkestrel-dispatch/scripts/result.js +11 -9
  7. package/dist/agents/skills/orkestrel-harden/scripts/discovery.js +233 -232
  8. package/dist/agents/skills/orkestrel-publish/scripts/compare.js +95 -89
  9. package/dist/agents/skills/orkestrel-publish/scripts/pins.js +5 -7
  10. package/dist/agents/skills/orkestrel-publish/scripts/swap.js +1273 -0
  11. package/dist/agents/skills/orkestrel-publish/scripts/wave.js +99 -89
  12. package/dist/agents/skills/orkestrel-publish/scripts/window.js +54 -52
  13. package/dist/agents/skills/orkestrel-scout/scripts/map.js +160 -161
  14. package/dist/bin/main.js +483 -444
  15. package/dist/bin/main.js.map +1 -1
  16. package/dist/host/AGENTS.md +1 -0
  17. package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
  18. package/dist/host/agents/skills/orkestrel-dispatch/scripts/brief.ts +8 -11
  19. package/dist/host/agents/skills/orkestrel-dispatch/scripts/cite.ts +58 -28
  20. package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +5 -10
  21. package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +31 -12
  22. package/dist/host/agents/skills/orkestrel-dispatch/scripts/login.ts +28 -23
  23. package/dist/host/agents/skills/orkestrel-dispatch/scripts/result.ts +12 -10
  24. package/dist/host/agents/skills/orkestrel-harden/scripts/discovery.ts +318 -237
  25. package/dist/host/agents/skills/orkestrel-journey/references/statechart.md +2 -5
  26. package/dist/host/agents/skills/orkestrel-publish/SKILL.md +7 -6
  27. package/dist/host/agents/skills/orkestrel-publish/references/release.md +12 -10
  28. package/dist/host/agents/skills/orkestrel-publish/references/wave.md +12 -5
  29. package/dist/host/agents/skills/orkestrel-publish/scripts/compare.ts +124 -82
  30. package/dist/host/agents/skills/orkestrel-publish/scripts/pins.ts +5 -7
  31. package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +1655 -0
  32. package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +128 -97
  33. package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +70 -55
  34. package/dist/host/agents/skills/orkestrel-scout/scripts/map.ts +215 -155
  35. package/dist/host/claude/rules/architecture.md +2 -2
  36. package/dist/host/claude/rules/tests.md +8 -2
  37. package/dist/host/claude/rules/typescript.md +130 -0
  38. package/dist/host/claude/rules/workspace.md +33 -18
  39. package/dist/host/configs/helpers.ts +209 -148
  40. package/dist/host/configs/policy.ts +110 -105
  41. package/dist/host/dotfiles/oxlintrc.json +1 -11
  42. package/dist/host/guides/scaffold.md +244 -170
  43. package/dist/host/manifest.json +36 -30
  44. package/dist/host/tests/config.test.ts +1263 -687
  45. package/dist/host/tests/policy.test.ts +19 -0
  46. package/dist/host/tests/setupPolicy.ts +1699 -1026
  47. package/dist/src/core/index.cjs +1595 -1686
  48. package/dist/src/core/index.cjs.map +1 -1
  49. package/dist/src/core/index.d.cts +202 -18
  50. package/dist/src/core/index.d.ts +202 -18
  51. package/dist/src/core/index.js +1590 -1687
  52. package/dist/src/core/index.js.map +1 -1
  53. package/dist/src/server/index.cjs +448 -392
  54. package/dist/src/server/index.cjs.map +1 -1
  55. package/dist/src/server/index.d.cts +1182 -1052
  56. package/dist/src/server/index.d.ts +1182 -1052
  57. package/dist/src/server/index.js +447 -395
  58. package/dist/src/server/index.js.map +1 -1
  59. package/package.json +2 -2
@@ -32,22 +32,14 @@ function readsAsPath(token) {
32
32
  return token.includes('/');
33
33
  }
34
34
  function extractPaths(content) {
35
- const found = new Set();
36
- for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
37
- const token = (match[1] ?? '').replace(/\\/gu, '/').replace(/:\d+(?:[-–]\d+)?$/u, '');
38
- if (readsAsPath(token))
39
- found.add(token);
40
- }
35
+ const found = new Set([...content.matchAll(/`([^`\n]+)`/gu)]
36
+ .map((match) => (match[1] ?? '').replace(/\\/gu, '/').replace(/:\d+(?:[-–]\d+)?$/u, ''))
37
+ .filter(readsAsPath));
41
38
  return [...found].sort();
42
39
  }
43
- function resolveBriefPath(directory, unit) {
44
- const base = join(directory, `${unit}-brief.md`);
45
- if (!existsSync(base))
46
- return base;
47
- let n = 2;
48
- while (existsSync(join(directory, `${unit}-brief-${n}.md`)))
49
- n += 1;
50
- return join(directory, `${unit}-brief-${n}.md`);
40
+ function resolveBriefPath(directory, unit, count = 1) {
41
+ const path = join(directory, count === 1 ? `${unit}-brief.md` : `${unit}-brief-${count}.md`);
42
+ return existsSync(path) ? resolveBriefPath(directory, unit, count + 1) : path;
51
43
  }
52
44
  function writeBrief(argv) {
53
45
  const unit = readOption(argv, '--unit');
@@ -12,14 +12,10 @@ import { basename, relative, resolve } from 'node:path';
12
12
  import { listFiles, normalizeSlashes, readMissingFlags, readOption } from "./helpers.js";
13
13
  const CITATION = /(?<![\w/\\.-])((?:\.{1,2}[/\\])?\.?[A-Za-z0-9_@][A-Za-z0-9_./\\@-]*\.[a-z]{1,5}):(\d+)(?:[-–](\d+))?(?![\w:])/gu;
14
14
  function indexBasenames(root) {
15
- const index = new Map();
16
- for (const file of listFiles(root)) {
17
- const name = basename(file.path);
18
- const paths = index.get(name) ?? [];
19
- paths.push(normalizeSlashes(relative(root, file.path)));
20
- index.set(name, paths);
21
- }
22
- return index;
15
+ return new Map([...Map.groupBy(listFiles(root), (file) => basename(file.path))].map(([name, files]) => [
16
+ name,
17
+ files.map((file) => normalizeSlashes(relative(root, file.path))),
18
+ ]));
23
19
  }
24
20
  function countLines(path) {
25
21
  const text = readFileSync(path, 'utf8');
@@ -29,13 +25,21 @@ function countLines(path) {
29
25
  // A trailing line break ends the last line rather than opening a phantom one.
30
26
  return lines.at(-1) === '' ? lines.length - 1 : lines.length;
31
27
  }
32
- function resolveFile(root, file, index) {
28
+ function resolveDirect(root, file) {
33
29
  const direct = resolve(root, normalizeSlashes(file));
34
- if (existsSync(direct) && statSync(direct).isFile())
30
+ return existsSync(direct) && statSync(direct).isFile() ? direct : undefined;
31
+ }
32
+ // A bare filename that no root-relative file matches is the one citation the basename index serves.
33
+ function needsIndex(root, file) {
34
+ return resolveDirect(root, file) === undefined && !/[/\\]/u.test(file);
35
+ }
36
+ function resolveFile(root, file, index) {
37
+ const direct = resolveDirect(root, file);
38
+ if (direct !== undefined)
35
39
  return { path: direct, reason: undefined };
36
40
  if (/[/\\]/u.test(file))
37
41
  return { path: undefined, reason: 'file does not exist' };
38
- const candidates = index().get(basename(file)) ?? [];
42
+ const candidates = index?.get(basename(file)) ?? [];
39
43
  const only = candidates.length === 1 ? candidates[0] : undefined;
40
44
  if (only !== undefined)
41
45
  return { path: resolve(root, only), reason: undefined };
@@ -45,7 +49,6 @@ function resolveFile(root, file, index) {
45
49
  }
46
50
  function judgeLines(path, line, end, counts) {
47
51
  const total = counts.get(path) ?? countLines(path);
48
- counts.set(path, total);
49
52
  if (line < 1 || line > total)
50
53
  return `line ${line} is outside 1-${total}`;
51
54
  if (end !== undefined && (end < line || end > total))
@@ -64,24 +67,34 @@ function main(argv) {
64
67
  console.error('usage: cite.ts PATH [--root DIR] [--json]');
65
68
  return 64;
66
69
  }
67
- let index;
68
- const counts = new Map();
69
- const unresolved = [];
70
- let citations = 0;
71
- for (const match of readFileSync(path, 'utf8').matchAll(CITATION)) {
72
- const file = match[1];
73
- const line = Number(match[2]);
74
- const end = match[3] === undefined ? undefined : Number(match[3]);
75
- if (file === undefined)
76
- continue;
77
- citations += 1;
78
- const resolved = resolveFile(root, file, () => (index ??= indexBasenames(root)));
70
+ const named = [...readFileSync(path, 'utf8').matchAll(CITATION)].flatMap((match) => match[1] === undefined
71
+ ? []
72
+ : [
73
+ {
74
+ citation: match[0],
75
+ file: match[1],
76
+ line: Number(match[2]),
77
+ end: match[3] === undefined ? undefined : Number(match[3]),
78
+ },
79
+ ]);
80
+ const citations = named.length;
81
+ // The basename index walks the root, so it is built once and only when a citation needs it.
82
+ const index = named.some(({ file }) => needsIndex(root, file)) ? indexBasenames(root) : undefined;
83
+ const resolutions = named.map((entry) => ({
84
+ entry,
85
+ resolved: resolveFile(root, entry.file, index),
86
+ }));
87
+ // Each resolved file is counted once, however many citations name it.
88
+ const counts = new Map([...new Set(resolutions.flatMap(({ resolved }) => resolved.path ?? []))].map((file) => [
89
+ file,
90
+ countLines(file),
91
+ ]));
92
+ const unresolved = resolutions.flatMap(({ entry, resolved }) => {
79
93
  const reason = resolved.path === undefined
80
94
  ? (resolved.reason ?? 'file does not exist')
81
- : judgeLines(resolved.path, line, end, counts);
82
- if (reason !== undefined)
83
- unresolved.push({ citation: match[0], reason });
84
- }
95
+ : judgeLines(resolved.path, entry.line, entry.end, counts);
96
+ return reason === undefined ? [] : [{ citation: entry.citation, reason }];
97
+ });
85
98
  if (argv.includes('--json')) {
86
99
  console.log(JSON.stringify({ path, root, citations, unresolved }));
87
100
  }
@@ -45,13 +45,10 @@ export function readMissingFlags(argv, names) {
45
45
  * @returns The values in order, empty when the flag never appears.
46
46
  */
47
47
  export function readOptions(argv, name) {
48
- const values = [];
49
- argv.forEach((argument, index) => {
48
+ return argv.flatMap((argument, index) => {
50
49
  const value = argv[index + 1];
51
- if (argument === name && value !== undefined && !value.startsWith('--'))
52
- values.push(value);
50
+ return argument === name && value !== undefined && !value.startsWith('--') ? [value] : [];
53
51
  });
54
- return values;
55
52
  }
56
53
  /**
57
54
  * Reads the values that follow a flag up to the next flag.
@@ -64,13 +61,9 @@ export function readList(argv, name) {
64
61
  const index = argv.indexOf(name);
65
62
  if (index === -1)
66
63
  return [];
67
- const values = [];
68
- for (const argument of argv.slice(index + 1)) {
69
- if (argument.startsWith('--'))
70
- break;
71
- values.push(argument);
72
- }
73
- return values;
64
+ const rest = argv.slice(index + 1);
65
+ const end = rest.findIndex((argument) => argument.startsWith('--'));
66
+ return end === -1 ? rest : rest.slice(0, end);
74
67
  }
75
68
  /**
76
69
  * Normalizes a host path to forward slashes.
@@ -2,7 +2,8 @@
2
2
  // node .agents/skills/orkestrel-dispatch/scripts/launch.ts --journal tmp/codex/UNIT.jsonl --errors tmp/codex/UNIT.err --cap 1500 [--status] -- codex exec --json ...
3
3
  // The command's stdout goes to --journal and its stderr to --errors; stdin is closed. Arguments after
4
4
  // `--` pass to the command verbatim, so no shell quoting is involved. --cap is in seconds: when it
5
- // expires the process tree is killed and the errors file records `capped=true`. --status records
5
+ // expires the process tree is killed and the errors file records `capped=true`. A fractional cap
6
+ // truncates to whole milliseconds, with 1 ms the least. --status records
6
7
  // `git status --porcelain` before and after beside the journal, which is how a read-only lane's
7
8
  // containment is checked. Stdout carries up to two JSON lines. The spawn line
8
9
  // `{"pid","journal","errors"}` prints as soon as the command starts, and the pid, as decimal digits
@@ -12,11 +13,20 @@
12
13
  // Windows kills the process tree; any other host kills the command alone, not its descendants.
13
14
  // The launcher's own flags are read before `--` alone. Exit: the command's code; 124 when capped;
14
15
  // 127 when the command could not start (the errors file names why); 64 on usage, including a
15
- // journal and errors path that are the same file and a cap that is not a number of seconds.
16
+ // journal and errors path that are the same file and a cap that is not a number of seconds from 0
17
+ // to 2147483.
16
18
  import { spawn, spawnSync } from 'node:child_process';
17
19
  import { appendFileSync, closeSync, mkdirSync, openSync, rmSync, writeFileSync } from 'node:fs';
18
20
  import { dirname } from 'node:path';
19
21
  import { readMissingFlags, readOption } from "./helpers.js";
22
+ // The longest wait a Node timer holds is 2147483647 ms; floored to whole seconds, a longer cap would
23
+ // throw from `AbortSignal.timeout` after the spawn or clamp to 1 ms and kill the command at once.
24
+ const MAX_CAP_SECONDS = 2_147_483;
25
+ // `AbortSignal.timeout` throws on a fractional delay, which `setTimeout` truncates and raises to 1 ms
26
+ // from below, so the cap takes that form before the command starts.
27
+ function computeCapMilliseconds(cap) {
28
+ return Math.max(1, Math.trunc(cap * 1000));
29
+ }
20
30
  function readStatus() {
21
31
  const result = spawnSync('git', ['status', '--porcelain'], { encoding: 'utf8' });
22
32
  return result.status === 0 ? result.stdout : `git status failed: ${result.stderr}`;
@@ -53,12 +63,14 @@ async function main(argv) {
53
63
  journal === errors ||
54
64
  !Number.isFinite(cap) ||
55
65
  cap < 0 ||
66
+ cap > MAX_CAP_SECONDS ||
56
67
  command === undefined ||
57
68
  command === '') {
58
- console.error('usage: launch.ts --journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]; the journal and the errors file differ, and the cap is a number of seconds (0 for none)');
69
+ console.error(`usage: launch.ts --journal PATH --errors PATH --cap SECONDS [--status] -- COMMAND [ARGS...]; the journal and the errors file differ, and the cap is a number of seconds from 0 to ${String(MAX_CAP_SECONDS)} (0 for none)`);
59
70
  return 64;
60
71
  }
61
72
  const status = own.includes('--status');
73
+ const capMilliseconds = cap > 0 ? computeCapMilliseconds(cap) : undefined;
62
74
  mkdirSync(dirname(journal), { recursive: true });
63
75
  mkdirSync(dirname(errors), { recursive: true });
64
76
  if (status)
@@ -72,17 +84,17 @@ async function main(argv) {
72
84
  writeFileSync(`${journal}.pid`, `${child.pid}\n`);
73
85
  console.log(JSON.stringify({ pid: child.pid, journal, errors }));
74
86
  }
75
- let capped = false;
76
- const timer = cap > 0
77
- ? setTimeout(() => {
78
- capped = true;
79
- if (child.pid !== undefined)
80
- killTree(child.pid);
81
- }, cap * 1000)
82
- : undefined;
87
+ // The signal aborts once the cap elapses, so whether it aborted is whether the cap ended the run.
88
+ // The timer cannot be cleared, so a cap that elapses after the child exited kills nothing: a
89
+ // reused pid is not this command's.
90
+ const deadline = capMilliseconds === undefined ? undefined : AbortSignal.timeout(capMilliseconds);
91
+ deadline?.addEventListener('abort', () => {
92
+ if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
93
+ killTree(child.pid);
94
+ }
95
+ }, { once: true });
83
96
  const exit = await waitForExit(child);
84
- if (timer !== undefined)
85
- clearTimeout(timer);
97
+ const capped = deadline?.aborted === true;
86
98
  closeSync(journalFd);
87
99
  closeSync(errorsFd);
88
100
  const durationMs = Date.now() - started;
@@ -35,6 +35,23 @@ function waitFor(ms) {
35
35
  setTimeout(resolve, ms);
36
36
  });
37
37
  }
38
+ async function pollLogin(started, capSeconds, url, code) {
39
+ if (Date.now() - started >= capSeconds * 1000)
40
+ return { url, code, live: false };
41
+ await waitFor(POLL_MS);
42
+ const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : '';
43
+ const nextUrl = url ?? text.match(URL_PATTERN)?.[0];
44
+ if (url === undefined && nextUrl !== undefined)
45
+ console.error(`login: open ${nextUrl}`);
46
+ const nextCode = code ?? text.match(CODE_PATTERN)?.[1];
47
+ if (code === undefined && nextCode !== undefined) {
48
+ console.error(`login: enter the code ${nextCode}`);
49
+ }
50
+ if (readStatus('codex', ['login', 'status']).live) {
51
+ return { url: nextUrl, code: nextCode, live: true };
52
+ }
53
+ return pollLogin(started, capSeconds, nextUrl, nextCode);
54
+ }
38
55
  async function loginCodex(capSeconds) {
39
56
  const before = readStatus('codex', ['login', 'status']);
40
57
  if (before.live) {
@@ -48,31 +65,7 @@ async function loginCodex(capSeconds) {
48
65
  stdio: ['ignore', journal, journal],
49
66
  windowsHide: true,
50
67
  });
51
- let url;
52
- let code;
53
- let live = false;
54
- while (Date.now() - started < capSeconds * 1000) {
55
- await waitFor(POLL_MS);
56
- const text = existsSync(JOURNAL) ? readFileSync(JOURNAL, 'utf8') : '';
57
- if (url === undefined) {
58
- url = text.match(URL_PATTERN)?.[0];
59
- if (url !== undefined)
60
- console.error(`login: open ${url}`);
61
- }
62
- if (code === undefined) {
63
- code = text.match(CODE_PATTERN)?.[1];
64
- if (code !== undefined)
65
- console.error(`login: enter the code ${code}`);
66
- }
67
- if (child.exitCode !== null && readStatus('codex', ['login', 'status']).live) {
68
- live = true;
69
- break;
70
- }
71
- if (child.exitCode === null && readStatus('codex', ['login', 'status']).live) {
72
- live = true;
73
- break;
74
- }
75
- }
68
+ const { url, code, live } = await pollLogin(started, capSeconds);
76
69
  if (child.exitCode === null && child.pid !== undefined)
77
70
  killTree(child.pid);
78
71
  closeSync(journal);
@@ -13,19 +13,21 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
13
13
  import { dirname } from 'node:path';
14
14
  import { readMissingFlags, readNumber, readOption, readString } from "./helpers.js";
15
15
  function readEvents(path) {
16
- const events = [];
17
- for (const line of readFileSync(path, 'utf8').split(/\r\n|\n/)) {
16
+ return readFileSync(path, 'utf8')
17
+ .split(/\r\n|\n/)
18
+ .flatMap((line) => {
18
19
  if (line.trim() === '')
19
- continue;
20
+ return [];
20
21
  try {
21
22
  const parsed = JSON.parse(line);
22
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
23
- events.push(Object.fromEntries(Object.entries(parsed)));
24
- }
23
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
24
+ ? [Object.fromEntries(Object.entries(parsed))]
25
+ : [];
25
26
  }
26
- catch { }
27
- }
28
- return events;
27
+ catch {
28
+ return [];
29
+ }
30
+ });
29
31
  }
30
32
  function readAnswer(text) {
31
33
  return text === undefined || text.trim() === '' ? undefined : text;