dsh-pstack 0.2.0-beta.1 → 0.2.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -3
- package/README.md +135 -84
- package/README.zh.md +185 -0
- package/cordis.patch.yml +97 -27
- package/dist/host-contract-CUPSpeNk.d.mts +22 -0
- package/dist/index.d.mts +49 -238
- package/dist/index.mjs +250 -123
- package/dist/models.d.mts +44 -0
- package/dist/models.mjs +201 -0
- package/dist/paths-BT77khwm.mjs +48 -0
- package/dist/pstack/LICENSE +21 -0
- package/dist/pstack/README.md +291 -0
- package/dist/pstack/agents/poteto-agent.md +9 -0
- package/{assets → dist/pstack}/docs/guide/01-setup.md +13 -11
- package/{assets → dist/pstack}/docs/guide/02-poteto-mode.md +4 -4
- package/{assets → dist/pstack}/docs/guide/04-design.md +4 -4
- package/{assets → dist/pstack}/docs/guide/05-build-and-clean.md +2 -2
- package/{assets → dist/pstack}/docs/guide/06-verify-and-ship.md +13 -3
- package/{assets → dist/pstack}/docs/guide/07-overnight.md +15 -13
- package/{assets → dist/pstack}/docs/guide/09-make-it-yours.md +6 -2
- package/{assets → dist/pstack}/docs/guide/10-recipes-and-pitfalls.md +7 -5
- package/{assets → dist/pstack}/docs/guide/README.md +1 -1
- package/dist/pstack/docs/harness/dsh.md +98 -0
- package/{assets → dist/pstack}/skills/architect/SKILL.md +12 -12
- package/dist/pstack/skills/arena/SKILL.md +50 -0
- package/{assets → dist/pstack}/skills/automate-me/SKILL.md +14 -14
- package/{assets → dist/pstack}/skills/benchmark-checklist/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/blast-radius/SKILL.md +4 -6
- package/{assets → dist/pstack}/skills/correct/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/create-formal-verification-skill/SKILL.md +4 -2
- package/{assets → dist/pstack}/skills/create-verification-skill/SKILL.md +5 -5
- package/dist/pstack/skills/figure-it-out/SKILL.md +54 -0
- package/dist/pstack/skills/how/SKILL.md +38 -0
- package/{assets → dist/pstack}/skills/in-your-own-words/SKILL.md +1 -1
- package/dist/pstack/skills/interrogate/SKILL.md +33 -0
- package/{assets → dist/pstack}/skills/ketchup/SKILL.md +3 -5
- package/dist/pstack/skills/ketchup-video/SKILL.md +29 -0
- package/{assets → dist/pstack}/skills/maintain-formal-verification-skill/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/maintain-verification-skill/SKILL.md +3 -3
- package/dist/pstack/skills/make-bot-ui/SKILL.md +117 -0
- package/dist/pstack/skills/no-comments/SKILL.md +26 -0
- package/{assets → dist/pstack}/skills/poteto-help/SKILL.md +34 -31
- package/{assets → dist/pstack}/skills/poteto-help/references/prompting.md +1 -1
- package/{assets → dist/pstack}/skills/poteto-help/references/recipes.md +3 -3
- package/{assets → dist/pstack}/skills/poteto-mode/SKILL.md +20 -19
- package/dist/pstack/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autopilot-full.md +25 -0
- package/dist/pstack/skills/poteto-mode/playbooks/autopilot-stack.md +8 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/babysit.md +4 -4
- package/dist/pstack/skills/poteto-mode/playbooks/bug-fix.md +15 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/eval.md +3 -3
- package/dist/pstack/skills/poteto-mode/playbooks/feature.md +21 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/hillclimb.md +3 -3
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/investigation.md +2 -2
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/multi-phase-plan.md +23 -23
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/opening-a-pr.md +6 -6
- package/dist/pstack/skills/poteto-mode/playbooks/orchestrate.md +25 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/pause-safely.md +2 -2
- package/dist/pstack/skills/poteto-mode/playbooks/perf-issue.md +25 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/prototype.md +4 -4
- package/dist/pstack/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/dist/pstack/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/dist/pstack/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/shipping.md +2 -2
- package/{assets → dist/pstack}/skills/poteto-mode/playbooks/trace-forensics.md +2 -2
- package/dist/pstack/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/dist/pstack/skills/poteto-mode/playbooks/worktree-cleanup.md +12 -0
- package/{assets → dist/pstack}/skills/poteto-mode/scripts/check-plan.mjs +6 -8
- package/dist/pstack/skills/poteto-mode/scripts/check-plan.test.mjs +128 -0
- package/{assets/skills/poteto-mode/scripts/check-plan.test.mjs → dist/pstack/skills/poteto-mode/scripts/expect.mjs} +12 -101
- package/dist/pstack/skills/poteto-mode/scripts/support.mjs +93 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/cli.mjs +123 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/cli.test.mjs +217 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.mjs +119 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/github.mjs +626 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/github.test.mjs +521 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/policy.mjs +657 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/policy.test.mjs +523 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/render.mjs +106 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/types.mjs +9 -0
- package/dist/pstack/skills/poteto-mode/scripts/watch-pr/watch-pr.mjs +5 -0
- package/{assets → dist/pstack}/skills/poteto-mode/scripts/worktree-audit.mjs +15 -22
- package/dist/pstack/skills/poteto-mode/scripts/worktree-audit.test.mjs +134 -0
- package/{assets → dist/pstack}/skills/principle-attack-the-premise/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-boundary-discipline/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-encode-lessons-in-structure/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-exhaust-the-design-space/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-experience-first/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-explain-the-number/SKILL.md +3 -2
- package/{assets → dist/pstack}/skills/principle-fix-root-causes/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-laziness-protocol/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-make-operations-idempotent/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-minimize-reader-load/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-model-the-domain/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-never-block-on-the-human/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-outcome-oriented-execution/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-prove-it-works/SKILL.md +2 -2
- package/{assets → dist/pstack}/skills/principle-redesign-from-first-principles/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-separate-before-serializing-shared-state/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-sequence-verifiable-units/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-subtract-before-you-add/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-test-behavior-not-implementation/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/principle-type-system-discipline/SKILL.md +1 -1
- package/dist/pstack/skills/recall/SKILL.md +37 -0
- package/dist/pstack/skills/reflect/SKILL.md +71 -0
- package/{assets → dist/pstack}/skills/reflect/references/divergent-reviewer.md +4 -4
- package/{assets → dist/pstack}/skills/reflect/references/judgment-reviewer.md +2 -2
- package/{assets → dist/pstack}/skills/reflect/references/synthesizer.md +5 -5
- package/{assets → dist/pstack}/skills/reflect/references/tooling-reviewer.md +2 -2
- package/dist/pstack/skills/setup-pstack/SKILL.md +80 -0
- package/{assets → dist/pstack}/skills/sharpen/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/show-me-your-work/SKILL.md +8 -8
- package/{assets → dist/pstack}/skills/show-me-your-work/scripts/log.mjs +1 -3
- package/dist/pstack/skills/show-me-your-work/scripts/log.test.mjs +56 -0
- package/dist/pstack/skills/statechart/SKILL.md +82 -0
- package/dist/pstack/skills/statechart/references/render.md +93 -0
- package/dist/pstack/skills/statechart/scripts/backends/cfg.mjs +31 -0
- package/dist/pstack/skills/statechart/scripts/backends/mermaid.mjs +186 -0
- package/dist/pstack/skills/statechart/scripts/backends/plain-text.mjs +62 -0
- package/dist/pstack/skills/statechart/scripts/backends/report.mjs +84 -0
- package/dist/pstack/skills/statechart/scripts/frontends/config.mjs +268 -0
- package/dist/pstack/skills/statechart/scripts/frontends/dot.mjs +67 -0
- package/dist/pstack/skills/statechart/scripts/frontends/marks.mjs +94 -0
- package/dist/pstack/skills/statechart/scripts/frontends/messages.mjs +48 -0
- package/dist/pstack/skills/statechart/scripts/frontends/spec.mjs +101 -0
- package/dist/pstack/skills/statechart/scripts/frontends/trace.mjs +39 -0
- package/dist/pstack/skills/statechart/scripts/frontends/value.mjs +180 -0
- package/dist/pstack/skills/statechart/scripts/frontends/verdict.mjs +40 -0
- package/dist/pstack/skills/statechart/scripts/frontends/view.mjs +99 -0
- package/dist/pstack/skills/statechart/scripts/ir.mjs +277 -0
- package/dist/pstack/skills/statechart/scripts/passes/annotate.mjs +92 -0
- package/dist/pstack/skills/statechart/scripts/passes/label.mjs +210 -0
- package/dist/pstack/skills/statechart/scripts/passes/lift.mjs +84 -0
- package/dist/pstack/skills/statechart/scripts/passes/loop-check.mjs +47 -0
- package/dist/pstack/skills/statechart/scripts/passes/paths.mjs +72 -0
- package/dist/pstack/skills/statechart/scripts/passes/reach.mjs +135 -0
- package/dist/pstack/skills/statechart/scripts/passes/region-check.mjs +82 -0
- package/dist/pstack/skills/statechart/scripts/passes/regions.mjs +268 -0
- package/dist/pstack/skills/statechart/scripts/pipeline.mjs +72 -0
- package/dist/pstack/skills/statechart/scripts/run.mjs +440 -0
- package/dist/pstack/skills/statechart/scripts/statechart.mjs +155 -0
- package/dist/pstack/skills/statechart/scripts/statechart.test.mjs +198 -0
- package/dist/pstack/skills/statechart/scripts/worker.mjs +31 -0
- package/dist/pstack/skills/swarm/SKILL.md +43 -0
- package/{assets → dist/pstack}/skills/tdd/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/teach/SKILL.md +2 -2
- package/{assets → dist/pstack}/skills/technical-writing/SKILL.md +1 -1
- package/{assets → dist/pstack}/skills/typescript-best-practices/SKILL.md +3 -6
- package/{assets → dist/pstack}/skills/why/SKILL.md +8 -8
- package/dist/script.d.mts +9 -111
- package/dist/script.mjs +32 -329
- package/dist/skill.d.mts +38 -0
- package/dist/skill.mjs +183 -0
- package/dist/skills/authored/decision-audit/SKILL.md +99 -0
- package/dist/skills/authored/decision-audit/SKILL.zh.md +99 -0
- package/dist/skills/authored/decision-audit/references/recover-commit.md +17 -0
- package/dist/skills/authored/decision-audit/references/recover-commit.zh.md +17 -0
- package/dist/skills/authored/decision-write/SKILL.md +131 -0
- package/dist/skills/authored/decision-write/SKILL.zh.md +131 -0
- package/dist/skills/authored/decision-write/references/land.md +48 -0
- package/dist/skills/authored/decision-write/references/land.zh.md +48 -0
- package/dist/skills/authored/setup-project-docs/SKILL.md +58 -0
- package/dist/skills/authored/setup-project-docs/SKILL.zh.md +58 -0
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.md +11 -0
- package/dist/skills/authored/setup-project-docs/references/decision-conventions.zh.md +11 -0
- package/dist/skills/vendored/retro/SKILL.md +44 -0
- package/dist/skills/vendored/retro/agents/openai.yaml +5 -0
- package/dist/skills/vendored/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/dist/skills/vendored/writing-for-agents/SKILL.md +81 -0
- package/dist/skills/vendored/writing-for-agents/agents/openai.yaml +3 -0
- package/dist/subagent.d.mts +56 -193
- package/dist/subagent.mjs +443 -741
- package/locale/en.json +3 -2
- package/locale/zh.json +3 -2
- package/package.json +63 -66
- package/assets/agents/poteto-agent.md +0 -8
- package/assets/docs/harness/dsh.md +0 -60
- package/assets/docs/pstack-models.example.md +0 -54
- package/assets/skills/arena/SKILL.md +0 -50
- package/assets/skills/figure-it-out/SKILL.md +0 -54
- package/assets/skills/how/SKILL.md +0 -38
- package/assets/skills/interrogate/SKILL.md +0 -71
- package/assets/skills/interrogate/references/code-quality-review.md +0 -47
- package/assets/skills/interrogate/references/lead-judgment.md +0 -39
- package/assets/skills/interrogate/references/reviewer-prompt.md +0 -62
- package/assets/skills/interrogate/references/rubric.md +0 -77
- package/assets/skills/no-comments/SKILL.md +0 -26
- package/assets/skills/poteto-mode/playbooks/authoring-a-skill.md +0 -12
- package/assets/skills/poteto-mode/playbooks/autonomous-run.md +0 -15
- package/assets/skills/poteto-mode/playbooks/autopilot-full.md +0 -25
- package/assets/skills/poteto-mode/playbooks/autopilot-stack.md +0 -12
- package/assets/skills/poteto-mode/playbooks/bug-fix.md +0 -15
- package/assets/skills/poteto-mode/playbooks/feature.md +0 -21
- package/assets/skills/poteto-mode/playbooks/orchestrate.md +0 -112
- package/assets/skills/poteto-mode/playbooks/perf-issue.md +0 -25
- package/assets/skills/poteto-mode/playbooks/refactoring.md +0 -16
- package/assets/skills/poteto-mode/playbooks/runtime-forensics.md +0 -11
- package/assets/skills/poteto-mode/playbooks/session-pickup.md +0 -11
- package/assets/skills/poteto-mode/playbooks/visual-parity.md +0 -11
- package/assets/skills/poteto-mode/playbooks/worktree-cleanup.md +0 -20
- package/assets/skills/poteto-mode/scripts/orch/orch.mjs +0 -4282
- package/assets/skills/poteto-mode/scripts/orch/orch.test.mjs +0 -2042
- package/assets/skills/poteto-mode/scripts/watch-pr/cli.test.mjs +0 -5101
- package/assets/skills/poteto-mode/scripts/watch-pr/github.test.mjs +0 -1177
- package/assets/skills/poteto-mode/scripts/watch-pr/policy.test.mjs +0 -1441
- package/assets/skills/poteto-mode/scripts/watch-pr/watch-pr.mjs +0 -4601
- package/assets/skills/recall/SKILL.md +0 -37
- package/assets/skills/reflect/SKILL.md +0 -69
- package/assets/skills/setup-pstack/SKILL.md +0 -67
- package/assets/skills/swarm/SKILL.md +0 -43
- package/dist/frontmatter-BQX3ekwP.mjs +0 -229
- package/dist/frontmatter-BQX3ekwP.mjs.map +0 -1
- package/dist/history.d.mts +0 -429
- package/dist/history.mjs +0 -1129
- package/dist/history.mjs.map +0 -1
- package/dist/host-contract-o47JH_r8.d.mts +0 -527
- package/dist/index.mjs.map +0 -1
- package/dist/paths-D_JlVmBk.mjs +0 -42
- package/dist/paths-D_JlVmBk.mjs.map +0 -1
- package/dist/script.mjs.map +0 -1
- package/dist/subagent.mjs.map +0 -1
- /package/{static/icon.png → assets/logo.png} +0 -0
- /package/{assets → dist/pstack}/agents/comment-sicko.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/03-understand.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/08-principles.md +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/design.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/overnight.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/recipes.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/router.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/understanding.jpg +0 -0
- /package/{assets → dist/pstack}/docs/guide/images/verification.jpg +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/design-red-flags.md +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/rationale-template.md +0 -0
- /package/{assets → dist/pstack}/skills/architect/references/runner-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/bro/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/README.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/create-note.md +0 -0
- /package/{assets → dist/pstack}/skills/create-verification-skill/references/feature-map-example/search.md +0 -0
- /package/{assets → dist/pstack}/skills/how/references/explainer-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/how/references/explorer-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/poteto-mode/references/bugbot-triage.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-build-the-lever/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-foundational-thinking/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/principle-guard-the-context-window/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/show-me-your-work/references/decision-log-template.tsv +0 -0
- /package/{assets → dist/pstack}/skills/typescript-best-practices/references/patterns.md +0 -0
- /package/{assets → dist/pstack}/skills/unslop/SKILL.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/epistemics.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/investigator-prompt.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/source-playbook.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/code-archaeology.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/databricks.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/datadog.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/incident-postmortem.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/linear.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/notion.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/sentry.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/sources/slack.md +0 -0
- /package/{assets → dist/pstack}/skills/why/references/synthesizer-prompt.md +0 -0
|
@@ -1,2042 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { afterEach, describe, it } from "node:test";
|
|
3
|
-
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
4
|
-
import { inspect } from "node:util";
|
|
5
|
-
import { access, chmod, mkdir, mkdtemp, open, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
6
|
-
import { realpathSync } from "node:fs";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/expect.ts
|
|
11
|
-
/**
|
|
12
|
-
* A small `expect` for `node:test`.
|
|
13
|
-
*
|
|
14
|
-
* The five upstream test files were written against `bun:test`. Their assertions
|
|
15
|
-
* are ordinary value comparisons, so this module supplies exactly the matchers
|
|
16
|
-
* they use rather than a general test framework. It is deliberately not a drop-in
|
|
17
|
-
* replacement: a matcher that is not listed here fails to compile, which keeps the
|
|
18
|
-
* port auditable against the calls that actually exist.
|
|
19
|
-
*
|
|
20
|
-
* Covered: `toBe`, `toEqual`, `toContain`, `toMatch`, `toThrow`, `toBeNull`,
|
|
21
|
-
* `toBeInstanceOf`, `toHaveLength`, `toEndWith`, `toMatchObject`, `.not` on each,
|
|
22
|
-
* and `rejects` / `resolves` for promises.
|
|
23
|
-
*
|
|
24
|
-
* Two differences from `bun:test`, stated so a failure is not misread:
|
|
25
|
-
*
|
|
26
|
-
* - `toEqual` is structural for arrays and plain objects, `Object.is` otherwise.
|
|
27
|
-
* A class instance only equals itself; `toBeInstanceOf` is the matcher for type
|
|
28
|
-
* identity. The ported tests compare plain data, which is why this is enough.
|
|
29
|
-
* - A failure throws `AssertionError` with the two values rendered, not Bun's
|
|
30
|
-
* coloured diff.
|
|
31
|
-
*/
|
|
32
|
-
/** Raised by every failing assertion. */
|
|
33
|
-
var AssertionError = class extends Error {
|
|
34
|
-
constructor(message) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.name = "AssertionError";
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
function display(value) {
|
|
40
|
-
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
41
|
-
return inspect(value, {
|
|
42
|
-
depth: 3,
|
|
43
|
-
breakLength: 120,
|
|
44
|
-
maxArrayLength: 20
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function messageOf(error) {
|
|
48
|
-
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
49
|
-
return String(error);
|
|
50
|
-
}
|
|
51
|
-
/** Structural equality for the shapes the ported tests compare. */
|
|
52
|
-
function deepEqual(left, right) {
|
|
53
|
-
if (Object.is(left, right)) return true;
|
|
54
|
-
if (left instanceof Date || right instanceof Date) return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
|
|
55
|
-
if (Array.isArray(left) || Array.isArray(right)) {
|
|
56
|
-
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
|
57
|
-
return left.every((item, index) => deepEqual(item, right[index]));
|
|
58
|
-
}
|
|
59
|
-
if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false;
|
|
60
|
-
const leftProto = Object.getPrototypeOf(left);
|
|
61
|
-
if (leftProto !== Object.getPrototypeOf(right)) return false;
|
|
62
|
-
if (leftProto !== Object.prototype && leftProto !== null) return false;
|
|
63
|
-
const leftKeys = Object.keys(left);
|
|
64
|
-
const rightKeys = Object.keys(right);
|
|
65
|
-
if (leftKeys.length !== rightKeys.length) return false;
|
|
66
|
-
return leftKeys.every((key) => deepEqual(left[key], right[key]));
|
|
67
|
-
}
|
|
68
|
-
function contains(haystack, needle) {
|
|
69
|
-
if (typeof haystack === "string") return haystack.includes(String(needle));
|
|
70
|
-
if (Array.isArray(haystack)) return haystack.some((item) => deepEqual(item, needle));
|
|
71
|
-
if (haystack instanceof Set) return [...haystack].some((item) => deepEqual(item, needle));
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Whether `expected` is a partial description of `actual`.
|
|
76
|
-
*
|
|
77
|
-
* `bun:test`'s `toMatchObject` is recursive: every key in `expected` must exist and
|
|
78
|
-
* match, arrays match element by element with the same partial rule, and extra keys
|
|
79
|
-
* in `actual` are allowed. That is what lets an assertion name only the fields it
|
|
80
|
-
* cares about, which several ported cases rely on.
|
|
81
|
-
*/
|
|
82
|
-
function matchesObject(actual, expected) {
|
|
83
|
-
if (Array.isArray(expected)) {
|
|
84
|
-
if (!Array.isArray(actual) || actual.length !== expected.length) return false;
|
|
85
|
-
return expected.every((item, index) => matchesObject(actual[index], item));
|
|
86
|
-
}
|
|
87
|
-
if (expected instanceof Date) return deepEqual(actual, expected);
|
|
88
|
-
if (expected === null || typeof expected !== "object") return deepEqual(actual, expected);
|
|
89
|
-
if (actual === null || typeof actual !== "object") return false;
|
|
90
|
-
return Object.entries(expected).every(([key, value]) => matchesObject(actual[key], value));
|
|
91
|
-
}
|
|
92
|
-
function thrownBy(call) {
|
|
93
|
-
try {
|
|
94
|
-
call();
|
|
95
|
-
return { thrown: false };
|
|
96
|
-
} catch (error) {
|
|
97
|
-
return {
|
|
98
|
-
thrown: true,
|
|
99
|
-
error
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function buildMatchers(actual, negate) {
|
|
104
|
-
/**
|
|
105
|
-
* Fail when `ok` is not what the matcher asked for.
|
|
106
|
-
*
|
|
107
|
-
* `negate` inverts the requirement, so `.not.toContain(x)` passes on a haystack
|
|
108
|
-
* that does not contain `x` and fails when it does.
|
|
109
|
-
*/
|
|
110
|
-
const check = (ok, matcher, expected) => {
|
|
111
|
-
if (ok === !negate) return;
|
|
112
|
-
const relation = negate ? `not to ${matcher}` : matcher;
|
|
113
|
-
throw new AssertionError(`expected ${display(actual)} ${relation} ${display(expected)}`);
|
|
114
|
-
};
|
|
115
|
-
const checkPlain = (ok, matcher) => {
|
|
116
|
-
if (ok === !negate) return;
|
|
117
|
-
throw new AssertionError(`expected ${display(actual)} ${negate ? `not to ${matcher}` : matcher}`);
|
|
118
|
-
};
|
|
119
|
-
const matchers = {
|
|
120
|
-
toBe(expected) {
|
|
121
|
-
check(Object.is(actual, expected), "toBe", expected);
|
|
122
|
-
},
|
|
123
|
-
toEqual(expected) {
|
|
124
|
-
check(deepEqual(actual, expected), "toEqual", expected);
|
|
125
|
-
},
|
|
126
|
-
toContain(expected) {
|
|
127
|
-
check(contains(actual, expected), "toContain", expected);
|
|
128
|
-
},
|
|
129
|
-
toMatch(expected) {
|
|
130
|
-
const text = String(actual);
|
|
131
|
-
check(typeof expected === "string" ? text.includes(expected) : expected.test(text), "toMatch", expected);
|
|
132
|
-
},
|
|
133
|
-
toThrow(expected) {
|
|
134
|
-
if (typeof actual !== "function") throw new AssertionError(`toThrow needs a function, received ${display(actual)}`);
|
|
135
|
-
const result = thrownBy(actual);
|
|
136
|
-
if (!result.thrown) {
|
|
137
|
-
checkPlain(false, "toThrow");
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (expected === void 0) {
|
|
141
|
-
checkPlain(true, "toThrow");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const message = messageOf(result.error);
|
|
145
|
-
const ok = typeof expected === "string" ? message.includes(expected) : expected instanceof RegExp ? expected.test(message) : result.error instanceof expected;
|
|
146
|
-
check(ok, "toThrow", expected);
|
|
147
|
-
},
|
|
148
|
-
toBeNull() {
|
|
149
|
-
checkPlain(actual === null, "toBeNull");
|
|
150
|
-
},
|
|
151
|
-
toBeInstanceOf(expected) {
|
|
152
|
-
check(actual instanceof expected, "toBeInstanceOf", expected);
|
|
153
|
-
},
|
|
154
|
-
toHaveLength(expected) {
|
|
155
|
-
const length = actual?.length;
|
|
156
|
-
check(length === expected, "toHaveLength", expected);
|
|
157
|
-
},
|
|
158
|
-
toEndWith(expected) {
|
|
159
|
-
check(String(actual).endsWith(expected), "toEndWith", expected);
|
|
160
|
-
},
|
|
161
|
-
toMatchObject(expected) {
|
|
162
|
-
check(matchesObject(actual, expected), "toMatchObject", expected);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
Object.defineProperties(matchers, {
|
|
166
|
-
not: {
|
|
167
|
-
get: () => buildMatchers(actual, !negate),
|
|
168
|
-
enumerable: true
|
|
169
|
-
},
|
|
170
|
-
rejects: {
|
|
171
|
-
get: () => buildAsyncMatchers(actual, "rejects", negate),
|
|
172
|
-
enumerable: true
|
|
173
|
-
},
|
|
174
|
-
resolves: {
|
|
175
|
-
get: () => buildAsyncMatchers(actual, "resolves", negate),
|
|
176
|
-
enumerable: true
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
return matchers;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Compare against a settled promise.
|
|
183
|
-
*
|
|
184
|
-
* `rejects` reads the rejection reason and fails when the promise resolved;
|
|
185
|
-
* `resolves` is the mirror. Each matcher awaits the settlement, which is what
|
|
186
|
-
* makes `await expect(promise).rejects.toThrow(...)` wait before it compares.
|
|
187
|
-
*/
|
|
188
|
-
function buildAsyncMatchers(actual, mode, negate) {
|
|
189
|
-
if (typeof actual?.then !== "function") throw new AssertionError(`expected a promise, received ${display(actual)}`);
|
|
190
|
-
const wantRejection = mode === "rejects" !== negate;
|
|
191
|
-
const settle = async () => {
|
|
192
|
-
let resolved;
|
|
193
|
-
let rejected;
|
|
194
|
-
await Promise.resolve(actual).then((value) => {
|
|
195
|
-
resolved = { value };
|
|
196
|
-
}, (error) => {
|
|
197
|
-
rejected = { error };
|
|
198
|
-
});
|
|
199
|
-
if (wantRejection) {
|
|
200
|
-
if (resolved !== void 0) throw new AssertionError(`expected the promise to reject, it resolved to ${display(resolved.value)}`);
|
|
201
|
-
return rejected?.error;
|
|
202
|
-
}
|
|
203
|
-
if (rejected !== void 0) throw new AssertionError(`expected the promise to resolve, it rejected with ${messageOf(rejected.error)}`);
|
|
204
|
-
return resolved?.value;
|
|
205
|
-
};
|
|
206
|
-
/**
|
|
207
|
-
* Run one matcher against the settled value.
|
|
208
|
-
*
|
|
209
|
-
* `toThrow` and `toBeInstanceOf` both read the rejection reason, so a thrown
|
|
210
|
-
* reason is turned back into a thunk for `toThrow` rather than special-cased
|
|
211
|
-
* twice. `negate` is already folded into the settlement mode, so the inner
|
|
212
|
-
* matchers run un-negated.
|
|
213
|
-
*/
|
|
214
|
-
const wrap = async (name, expected) => {
|
|
215
|
-
const subject = await settle();
|
|
216
|
-
if (name === "toThrow") {
|
|
217
|
-
buildMatchers(() => {
|
|
218
|
-
throw subject;
|
|
219
|
-
}, false).toThrow(expected);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
if (name === "toBeNull") {
|
|
223
|
-
buildMatchers(subject, false).toBeNull();
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
const matchers = buildMatchers(subject, false);
|
|
227
|
-
const matcher = matchers[name];
|
|
228
|
-
if (typeof matcher !== "function") throw new AssertionError(`unknown matcher ${name}`);
|
|
229
|
-
matcher.call(matchers, expected);
|
|
230
|
-
};
|
|
231
|
-
const matchers = {
|
|
232
|
-
toBe: (expected) => wrap("toBe", expected),
|
|
233
|
-
toEqual: (expected) => wrap("toEqual", expected),
|
|
234
|
-
toContain: (expected) => wrap("toContain", expected),
|
|
235
|
-
toMatch: (expected) => wrap("toMatch", expected),
|
|
236
|
-
toThrow: (expected) => wrap("toThrow", expected),
|
|
237
|
-
toBeNull: () => wrap("toBeNull"),
|
|
238
|
-
toBeInstanceOf: (expected) => wrap("toBeInstanceOf", expected),
|
|
239
|
-
toHaveLength: (expected) => wrap("toHaveLength", expected),
|
|
240
|
-
toEndWith: (expected) => wrap("toEndWith", expected),
|
|
241
|
-
toMatchObject: (expected) => wrap("toMatchObject", expected)
|
|
242
|
-
};
|
|
243
|
-
Object.defineProperty(matchers, "not", {
|
|
244
|
-
get: () => buildAsyncMatchers(actual, mode, !negate),
|
|
245
|
-
enumerable: true
|
|
246
|
-
});
|
|
247
|
-
return matchers;
|
|
248
|
-
}
|
|
249
|
-
const expect = Object.assign((actual) => buildMatchers(actual, false), {
|
|
250
|
-
not: (actual) => buildMatchers(actual, true),
|
|
251
|
-
rejects: (actual) => buildAsyncMatchers(actual, "rejects", false),
|
|
252
|
-
resolves: (actual) => buildAsyncMatchers(actual, "resolves", false)
|
|
253
|
-
});
|
|
254
|
-
//#endregion
|
|
255
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/support.ts
|
|
256
|
-
/**
|
|
257
|
-
* The one support module the ported upstream tests import.
|
|
258
|
-
*
|
|
259
|
-
* The five upstream test files were written for `bun:test` and use exactly two
|
|
260
|
-
* Bun facilities: its `expect`, and `Bun.spawnSync` / `Bun.spawn` for running git
|
|
261
|
-
* and short-lived processes. Rather than rewrite every call site, this module
|
|
262
|
-
* supplies those two shapes and the test patch only replaces the import line. The
|
|
263
|
-
* bodies below stay byte-for-byte the upstream text, so a future upstream change
|
|
264
|
-
* is still readable against this tree.
|
|
265
|
-
*
|
|
266
|
-
* `Bun.spawnSync` returns `{ exitCode, stdout, stderr, success }` with Buffer
|
|
267
|
-
* output and never throws on a nonzero exit; `node:child_process.spawnSync`
|
|
268
|
-
* returns `{ status, stdout, stderr }`. The adapter is the mapping between them,
|
|
269
|
-
* not a second implementation.
|
|
270
|
-
*
|
|
271
|
-
* `Bun.spawn` returns `{ pid, exited: Promise<number> }`. The adapter runs the
|
|
272
|
-
* child to completion and resolves `exited` with the exit status, which is what
|
|
273
|
-
* the one call site awaits.
|
|
274
|
-
*/
|
|
275
|
-
/**
|
|
276
|
-
* Run a command to completion, Bun-shaped.
|
|
277
|
-
*
|
|
278
|
-
* A signal-terminated child reports `exitCode: 1` rather than `null`, so a test
|
|
279
|
-
* that formats the code never prints `undefined` for a killed process.
|
|
280
|
-
*/
|
|
281
|
-
function spawnSync$1(argv, options = {}) {
|
|
282
|
-
const [command, ...args] = argv;
|
|
283
|
-
if (command === void 0) throw new TypeError("spawnSync needs a command");
|
|
284
|
-
const result = spawnSync(command, args, {
|
|
285
|
-
cwd: options.cwd,
|
|
286
|
-
env: options.env,
|
|
287
|
-
stdio: [
|
|
288
|
-
options.stdin === "inherit" ? "inherit" : "pipe",
|
|
289
|
-
options.stdout === "inherit" ? "inherit" : "pipe",
|
|
290
|
-
options.stderr === "inherit" ? "inherit" : "pipe"
|
|
291
|
-
],
|
|
292
|
-
encoding: "buffer"
|
|
293
|
-
});
|
|
294
|
-
const exitCode = result.status ?? 1;
|
|
295
|
-
return {
|
|
296
|
-
exitCode,
|
|
297
|
-
stdout: Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.alloc(0),
|
|
298
|
-
stderr: Buffer.isBuffer(result.stderr) ? result.stderr : Buffer.alloc(0),
|
|
299
|
-
success: exitCode === 0
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* A child that exits immediately, as a real process.
|
|
304
|
-
*
|
|
305
|
-
* The current interpreter running an empty program is a process like any other: its
|
|
306
|
-
* `pid` is real and its exit status is 0.
|
|
307
|
-
*/
|
|
308
|
-
function immediateExit() {
|
|
309
|
-
return spawn(process.execPath, ["-e", ""], { stdio: "ignore" });
|
|
310
|
-
}
|
|
311
|
-
/** Whether `command` starts at all, checked without a shell. */
|
|
312
|
-
function resolves(command) {
|
|
313
|
-
return spawnSync(command, ["--version"], { stdio: "ignore" }).error === void 0;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Start a command and expose Bun's `{ pid, exited }` shape.
|
|
317
|
-
*
|
|
318
|
-
* The one upstream call is `spawn(["true"])`, used to obtain a pid that has already
|
|
319
|
-
* exited so the test can plant a stale lock. It reads `pid` *before* awaiting
|
|
320
|
-
* `exited`, so the pid must be real and already dead when this returns.
|
|
321
|
-
*
|
|
322
|
-
* That rules out "spawn it and fall back on failure": the failure is asynchronous, so
|
|
323
|
-
* the caller would read `-1`. The probe is synchronous instead, which lets this
|
|
324
|
-
* function choose the right child before returning. `true` is used when it exists and
|
|
325
|
-
* the current interpreter otherwise — a `PATH` without the usual Unix utilities is
|
|
326
|
-
* exactly the environment this release has to work in.
|
|
327
|
-
*/
|
|
328
|
-
function spawn$1(argv) {
|
|
329
|
-
const [command, ...args] = argv;
|
|
330
|
-
if (command === void 0) throw new TypeError("spawn needs a command");
|
|
331
|
-
const child = resolves(command) ? spawn(command, args, { stdio: "ignore" }) : immediateExit();
|
|
332
|
-
return {
|
|
333
|
-
pid: child.pid ?? -1,
|
|
334
|
-
exited: new Promise((resolve) => {
|
|
335
|
-
child.once("close", (code) => resolve(code ?? 1));
|
|
336
|
-
child.once("error", () => resolve(1));
|
|
337
|
-
})
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
/** The `Bun` global the upstream tests reference, limited to what they call. */
|
|
341
|
-
const Bun = {
|
|
342
|
-
spawnSync: spawnSync$1,
|
|
343
|
-
spawn: spawn$1
|
|
344
|
-
};
|
|
345
|
-
//#endregion
|
|
346
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/orch/store.ts
|
|
347
|
-
const UNIT_HEADER = "id track state branch pr sha brief";
|
|
348
|
-
const LEDGER_HEADER = "pr sha verdict evidence verifier ts";
|
|
349
|
-
const LOCK_FILE = ".orch.lock";
|
|
350
|
-
var UserError = class extends Error {};
|
|
351
|
-
var NotFoundError = class extends UserError {
|
|
352
|
-
output;
|
|
353
|
-
constructor(message, output) {
|
|
354
|
-
super(message);
|
|
355
|
-
this.output = output;
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
function errorCode(error) {
|
|
359
|
-
if (error !== null && typeof error === "object" && "code" in error && typeof error.code === "string") return error.code;
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
function errorMessage(error) {
|
|
363
|
-
return error instanceof Error ? error.message : String(error);
|
|
364
|
-
}
|
|
365
|
-
function isRecord(value) {
|
|
366
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
367
|
-
}
|
|
368
|
-
function isUnknownArray(value) {
|
|
369
|
-
return Array.isArray(value);
|
|
370
|
-
}
|
|
371
|
-
function verdictOrNull(value) {
|
|
372
|
-
switch (value) {
|
|
373
|
-
case "live-ui-verified":
|
|
374
|
-
case "unit-test-verified":
|
|
375
|
-
case "type-check-only":
|
|
376
|
-
case "verifier-blocked":
|
|
377
|
-
case "verifier-failed": return value;
|
|
378
|
-
default: return null;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
function frontierPrStateOrNull(value) {
|
|
382
|
-
switch (value) {
|
|
383
|
-
case "OPEN":
|
|
384
|
-
case "MERGED":
|
|
385
|
-
case "CLOSED": return value;
|
|
386
|
-
default: return null;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
function parseVerdict(value) {
|
|
390
|
-
const verdict = verdictOrNull(value);
|
|
391
|
-
if (verdict === null) throw new UserError("verdict must be live-ui-verified, unit-test-verified, type-check-only, verifier-blocked, or verifier-failed");
|
|
392
|
-
return verdict;
|
|
393
|
-
}
|
|
394
|
-
function cleanCell(value) {
|
|
395
|
-
const cleaned = value.replace(/[\t\n\r]/g, " ");
|
|
396
|
-
return /^[=+\-@]/.test(cleaned) ? `'${cleaned}` : cleaned;
|
|
397
|
-
}
|
|
398
|
-
function requiredCell(value, label) {
|
|
399
|
-
const cleaned = cleanCell(value);
|
|
400
|
-
if (cleaned.trim().length === 0) throw new UserError(`${label} must not be empty`);
|
|
401
|
-
return cleaned;
|
|
402
|
-
}
|
|
403
|
-
function requiredLine(value, label) {
|
|
404
|
-
const cleaned = value.replace(/[\n\r]/g, " ").trim();
|
|
405
|
-
if (cleaned.length === 0) throw new UserError(`${label} must not be empty`);
|
|
406
|
-
return cleaned;
|
|
407
|
-
}
|
|
408
|
-
function positiveInteger(value, label) {
|
|
409
|
-
if (!Number.isSafeInteger(value) || value < 1) throw new UserError(`${label} must be a positive integer`);
|
|
410
|
-
return value;
|
|
411
|
-
}
|
|
412
|
-
async function exists(path) {
|
|
413
|
-
try {
|
|
414
|
-
await access(path);
|
|
415
|
-
return true;
|
|
416
|
-
} catch (error) {
|
|
417
|
-
if (errorCode(error) === "ENOENT") return false;
|
|
418
|
-
throw error;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
async function atomicWrite(path, contents) {
|
|
422
|
-
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
423
|
-
try {
|
|
424
|
-
await writeFile(temporary, contents, { flag: "wx" });
|
|
425
|
-
await rename(temporary, path);
|
|
426
|
-
} finally {
|
|
427
|
-
await rm(temporary, { force: true });
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
async function writeIfMissing(path, contents) {
|
|
431
|
-
if (!await exists(path)) await atomicWrite(path, contents);
|
|
432
|
-
}
|
|
433
|
-
async function requiredFile(path) {
|
|
434
|
-
try {
|
|
435
|
-
return await readFile(path, "utf8");
|
|
436
|
-
} catch (error) {
|
|
437
|
-
if (errorCode(error) === "ENOENT") throw new UserError(`store is not initialized at ${dirname(path)}; run orch init`);
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
function holderIsDead(holder) {
|
|
442
|
-
const pid = Number.parseInt(holder, 10);
|
|
443
|
-
if (!Number.isSafeInteger(pid) || pid <= 0 || String(pid) !== holder) return false;
|
|
444
|
-
try {
|
|
445
|
-
process.kill(pid, 0);
|
|
446
|
-
return false;
|
|
447
|
-
} catch (error) {
|
|
448
|
-
return errorCode(error) === "ESRCH";
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
async function acquireLock(store, options) {
|
|
452
|
-
const path = join(store, LOCK_FILE);
|
|
453
|
-
const pid = String(process.pid);
|
|
454
|
-
const create = async () => {
|
|
455
|
-
const handle = await open(path, "wx");
|
|
456
|
-
await handle.writeFile(`${pid}\n`);
|
|
457
|
-
await handle.close();
|
|
458
|
-
};
|
|
459
|
-
const takeOver = async () => {
|
|
460
|
-
await unlink(path);
|
|
461
|
-
try {
|
|
462
|
-
await create();
|
|
463
|
-
} catch (retryError) {
|
|
464
|
-
if (errorCode(retryError) === "EEXIST") throw new UserError(`store lock held by pid ${(await readFile(path, "utf8")).trim() || "unknown"}`);
|
|
465
|
-
throw retryError;
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
try {
|
|
469
|
-
await create();
|
|
470
|
-
} catch (error) {
|
|
471
|
-
if (errorCode(error) !== "EEXIST") throw error;
|
|
472
|
-
let holder = "unknown";
|
|
473
|
-
try {
|
|
474
|
-
holder = (await readFile(path, "utf8")).trim() || "unknown";
|
|
475
|
-
} catch {
|
|
476
|
-
holder = "unknown";
|
|
477
|
-
}
|
|
478
|
-
if (holderIsDead(holder)) {
|
|
479
|
-
options.onStaleLock?.(holder);
|
|
480
|
-
await takeOver();
|
|
481
|
-
} else if (options.force) {
|
|
482
|
-
options.onLockStolen?.(holder);
|
|
483
|
-
await takeOver();
|
|
484
|
-
} else throw new UserError(`store lock held by pid ${holder}`);
|
|
485
|
-
}
|
|
486
|
-
return async () => {
|
|
487
|
-
try {
|
|
488
|
-
if ((await readFile(path, "utf8")).trim() === pid) await unlink(path);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
if (errorCode(error) !== "ENOENT") throw error;
|
|
491
|
-
}
|
|
492
|
-
};
|
|
493
|
-
}
|
|
494
|
-
async function readTsv(path, header, width) {
|
|
495
|
-
const lines = (await requiredFile(path)).replace(/\r/g, "").split("\n");
|
|
496
|
-
if (lines.shift() !== header) throw new UserError(`${basename(path)} has an invalid header`);
|
|
497
|
-
return lines.filter((value) => value.length > 0).map((value) => {
|
|
498
|
-
const cells = value.split(" ");
|
|
499
|
-
if (cells.length !== width) throw new UserError(`${basename(path)} has a malformed row`);
|
|
500
|
-
return cells;
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
async function writeTsv(path, header, rows) {
|
|
504
|
-
const body = rows.map((row) => row.map(cleanCell).join(" ")).join("\n");
|
|
505
|
-
await atomicWrite(path, `${header}\n${body}${body.length > 0 ? "\n" : ""}`);
|
|
506
|
-
}
|
|
507
|
-
async function readUnits(store) {
|
|
508
|
-
return (await readTsv(join(store, "units.tsv"), UNIT_HEADER, 7)).map((row) => ({
|
|
509
|
-
id: row[0] ?? "",
|
|
510
|
-
track: row[1] ?? "",
|
|
511
|
-
state: row[2] ?? "",
|
|
512
|
-
branch: row[3] ?? "",
|
|
513
|
-
pr: row[4] ?? "",
|
|
514
|
-
sha: row[5] ?? "",
|
|
515
|
-
brief: row[6] ?? ""
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
function unitCells(unit) {
|
|
519
|
-
return [
|
|
520
|
-
unit.id,
|
|
521
|
-
unit.track,
|
|
522
|
-
unit.state,
|
|
523
|
-
unit.branch,
|
|
524
|
-
unit.pr,
|
|
525
|
-
unit.sha,
|
|
526
|
-
unit.brief
|
|
527
|
-
];
|
|
528
|
-
}
|
|
529
|
-
async function saveUnits(store, rows) {
|
|
530
|
-
await writeTsv(join(store, "units.tsv"), UNIT_HEADER, rows.map(unitCells));
|
|
531
|
-
}
|
|
532
|
-
async function readLedger(store) {
|
|
533
|
-
return (await readTsv(join(store, "ledger.tsv"), LEDGER_HEADER, 6)).map((row) => {
|
|
534
|
-
const rawVerdict = row[2] ?? "";
|
|
535
|
-
const verdict = verdictOrNull(rawVerdict);
|
|
536
|
-
if (verdict === null) throw new UserError(`ledger.tsv has invalid verdict ${rawVerdict}`);
|
|
537
|
-
return {
|
|
538
|
-
pr: row[0] ?? "",
|
|
539
|
-
sha: row[1] ?? "",
|
|
540
|
-
verdict,
|
|
541
|
-
evidence: row[3] ?? "",
|
|
542
|
-
verifier: row[4] ?? "",
|
|
543
|
-
ts: row[5] ?? ""
|
|
544
|
-
};
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
function ledgerCells(row) {
|
|
548
|
-
return [
|
|
549
|
-
row.pr,
|
|
550
|
-
row.sha,
|
|
551
|
-
row.verdict,
|
|
552
|
-
row.evidence,
|
|
553
|
-
row.verifier,
|
|
554
|
-
row.ts
|
|
555
|
-
];
|
|
556
|
-
}
|
|
557
|
-
async function saveLedger(store, rows) {
|
|
558
|
-
await writeTsv(join(store, "ledger.tsv"), LEDGER_HEADER, rows.map(ledgerCells));
|
|
559
|
-
}
|
|
560
|
-
function pointerCells(pointer) {
|
|
561
|
-
return [
|
|
562
|
-
pointer.ts,
|
|
563
|
-
pointer.agent,
|
|
564
|
-
pointer.unit,
|
|
565
|
-
pointer.status,
|
|
566
|
-
pointer.report
|
|
567
|
-
];
|
|
568
|
-
}
|
|
569
|
-
async function readPointers(directory) {
|
|
570
|
-
let entries;
|
|
571
|
-
try {
|
|
572
|
-
entries = await readdir(directory, { withFileTypes: true });
|
|
573
|
-
} catch (error) {
|
|
574
|
-
if (errorCode(error) === "ENOENT") throw new UserError(`store is not initialized at ${dirname(directory)}; run orch init`);
|
|
575
|
-
throw error;
|
|
576
|
-
}
|
|
577
|
-
const result = [];
|
|
578
|
-
const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".tsv")).sort((left, right) => left.name.localeCompare(right.name));
|
|
579
|
-
for (const entry of files) {
|
|
580
|
-
const raw = (await readFile(join(directory, entry.name), "utf8")).replace(/\r?\n$/, "");
|
|
581
|
-
const row = raw.split(" ");
|
|
582
|
-
if (/[\r\n]/.test(raw) || row.length !== 5) throw new UserError(`inbox pointer ${entry.name} is malformed`);
|
|
583
|
-
result.push({
|
|
584
|
-
ts: row[0] ?? "",
|
|
585
|
-
agent: row[1] ?? "",
|
|
586
|
-
unit: row[2] ?? "",
|
|
587
|
-
status: row[3] ?? "",
|
|
588
|
-
report: row[4] ?? ""
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
return result;
|
|
592
|
-
}
|
|
593
|
-
function renderGates(rows) {
|
|
594
|
-
if (rows.length === 0) return "";
|
|
595
|
-
return `# Gates\n\n${rows.map((gate) => {
|
|
596
|
-
const answer = gate.kind === "resolved" ? `\n- Answer: ${gate.answer}` : "";
|
|
597
|
-
return `## ${gate.id}
|
|
598
|
-
|
|
599
|
-
- Status: ${gate.kind}
|
|
600
|
-
- Question: ${gate.question}
|
|
601
|
-
- Options: ${gate.options}
|
|
602
|
-
- Default: ${gate.defaultAnswer}${answer}`;
|
|
603
|
-
}).join("\n\n")}\n`;
|
|
604
|
-
}
|
|
605
|
-
async function readGates(store) {
|
|
606
|
-
const raw = (await requiredFile(join(store, "gates.md"))).replace(/\r/g, "").trim();
|
|
607
|
-
if (raw.length === 0) return [];
|
|
608
|
-
if (!raw.startsWith("# Gates\n\n## ")) throw new UserError("gates.md has an invalid heading");
|
|
609
|
-
const result = [];
|
|
610
|
-
for (const block of raw.slice(12).split("\n\n## ")) {
|
|
611
|
-
const lines = block.split("\n").filter((value) => value.length > 0);
|
|
612
|
-
const id = lines.shift() ?? "";
|
|
613
|
-
const fields = /* @__PURE__ */ new Map();
|
|
614
|
-
for (const value of lines) {
|
|
615
|
-
const match = /^- ([^:]+): (.*)$/.exec(value);
|
|
616
|
-
if (match === null) throw new UserError(`gates.md has a malformed gate ${id}`);
|
|
617
|
-
fields.set(match[1] ?? "", match[2] ?? "");
|
|
618
|
-
}
|
|
619
|
-
const status = fields.get("Status");
|
|
620
|
-
const question = fields.get("Question");
|
|
621
|
-
const options = fields.get("Options");
|
|
622
|
-
const defaultAnswer = fields.get("Default");
|
|
623
|
-
if (id.length === 0 || question === void 0 || options === void 0 || defaultAnswer === void 0) throw new UserError(`gates.md has a malformed gate ${id}`);
|
|
624
|
-
if (status === "open") result.push({
|
|
625
|
-
kind: "open",
|
|
626
|
-
id,
|
|
627
|
-
question,
|
|
628
|
-
options,
|
|
629
|
-
defaultAnswer
|
|
630
|
-
});
|
|
631
|
-
else if (status === "resolved" && fields.has("Answer")) result.push({
|
|
632
|
-
kind: "resolved",
|
|
633
|
-
id,
|
|
634
|
-
question,
|
|
635
|
-
options,
|
|
636
|
-
defaultAnswer,
|
|
637
|
-
answer: fields.get("Answer") ?? ""
|
|
638
|
-
});
|
|
639
|
-
else throw new UserError(`gates.md has invalid status ${status ?? ""}`);
|
|
640
|
-
}
|
|
641
|
-
if (new Set(result.map((gate) => gate.id)).size !== result.length) throw new UserError("gates.md has duplicate gate ids");
|
|
642
|
-
return result;
|
|
643
|
-
}
|
|
644
|
-
function parseFrontier(raw) {
|
|
645
|
-
let value;
|
|
646
|
-
try {
|
|
647
|
-
value = JSON.parse(raw);
|
|
648
|
-
} catch {
|
|
649
|
-
throw new UserError("frontier.json is not valid JSON");
|
|
650
|
-
}
|
|
651
|
-
if (!isRecord(value)) throw new UserError("frontier.json must contain an object");
|
|
652
|
-
if (Object.keys(value).length === 0) return {
|
|
653
|
-
generation: 0,
|
|
654
|
-
prs: [],
|
|
655
|
-
lowestUnmerged: null
|
|
656
|
-
};
|
|
657
|
-
if (typeof value.generation !== "number" || !Number.isSafeInteger(value.generation) || value.generation < 0 || !isUnknownArray(value.prs) || !(value.lowestUnmerged === null || typeof value.lowestUnmerged === "number" && Number.isSafeInteger(value.lowestUnmerged))) throw new UserError("frontier.json has an invalid shape");
|
|
658
|
-
const prs = [];
|
|
659
|
-
for (const row of value.prs) {
|
|
660
|
-
const state = isRecord(row) ? frontierPrStateOrNull(row.state) : null;
|
|
661
|
-
if (!isRecord(row) || typeof row.pr !== "number" || !Number.isSafeInteger(row.pr) || row.pr < 1 || typeof row.branches !== "string" || row.branches.length === 0 || typeof row.sha !== "string" || state === null) throw new UserError("frontier.json has an invalid PR row");
|
|
662
|
-
prs.push({
|
|
663
|
-
pr: row.pr,
|
|
664
|
-
branches: row.branches,
|
|
665
|
-
sha: row.sha,
|
|
666
|
-
state
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
return {
|
|
670
|
-
generation: value.generation,
|
|
671
|
-
prs,
|
|
672
|
-
lowestUnmerged: value.lowestUnmerged
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
async function readFrontier(store) {
|
|
676
|
-
return parseFrontier(await requiredFile(join(store, "frontier.json")));
|
|
677
|
-
}
|
|
678
|
-
async function readStanding(store) {
|
|
679
|
-
const raw = (await requiredFile(join(store, "preferences.md"))).replace(/\r/g, "");
|
|
680
|
-
if (raw.trim().length === 0) return [];
|
|
681
|
-
const result = [];
|
|
682
|
-
for (const value of raw.split("\n").filter((item) => item.length > 0)) {
|
|
683
|
-
const match = /^([1-9]\d*)\. (.+)$/.exec(value);
|
|
684
|
-
const number = Number(match?.[1] ?? 0);
|
|
685
|
-
if (match === null || number !== result.length + 1) throw new UserError("preferences.md has malformed numbering");
|
|
686
|
-
result.push({
|
|
687
|
-
number,
|
|
688
|
-
line: match[2] ?? ""
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
return result;
|
|
692
|
-
}
|
|
693
|
-
function countValues(values) {
|
|
694
|
-
const result = {};
|
|
695
|
-
for (const value of values) result[value] = (result[value] ?? 0) + 1;
|
|
696
|
-
return Object.fromEntries(Object.entries(result).sort(([left], [right]) => left.localeCompare(right)));
|
|
697
|
-
}
|
|
698
|
-
function summarize(unitRows, ledgerRows, currentFrontier, gateRows) {
|
|
699
|
-
return {
|
|
700
|
-
unitStates: countValues(unitRows.map((unit) => unit.state)),
|
|
701
|
-
ledgerVerdicts: countValues(ledgerRows.map((row) => row.verdict)),
|
|
702
|
-
frontierGeneration: currentFrontier.generation,
|
|
703
|
-
openGateIds: gateRows.filter((gate) => gate.kind === "open").map((gate) => gate.id).sort()
|
|
704
|
-
};
|
|
705
|
-
}
|
|
706
|
-
function countRecord(value) {
|
|
707
|
-
if (!isRecord(value)) return null;
|
|
708
|
-
const result = {};
|
|
709
|
-
for (const [name, count] of Object.entries(value)) {
|
|
710
|
-
if (typeof count !== "number" || !Number.isSafeInteger(count) || count < 0) return null;
|
|
711
|
-
result[name] = count;
|
|
712
|
-
}
|
|
713
|
-
return result;
|
|
714
|
-
}
|
|
715
|
-
function previousSummary(raw) {
|
|
716
|
-
const match = /<!-- orch-summary (.+) -->/.exec(raw);
|
|
717
|
-
if (match === null) return null;
|
|
718
|
-
let value;
|
|
719
|
-
try {
|
|
720
|
-
value = JSON.parse(match[1] ?? "");
|
|
721
|
-
} catch {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
if (!isRecord(value) || typeof value.frontierGeneration !== "number" || !isUnknownArray(value.openGateIds)) return null;
|
|
725
|
-
const unitStates = countRecord(value.unitStates);
|
|
726
|
-
const ledgerVerdicts = countRecord(value.ledgerVerdicts);
|
|
727
|
-
const openGateIds = value.openGateIds.filter((item) => typeof item === "string");
|
|
728
|
-
if (unitStates === null || ledgerVerdicts === null || openGateIds.length !== value.openGateIds.length) return null;
|
|
729
|
-
return {
|
|
730
|
-
unitStates,
|
|
731
|
-
ledgerVerdicts,
|
|
732
|
-
frontierGeneration: value.frontierGeneration,
|
|
733
|
-
openGateIds
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
function changed(before, after) {
|
|
737
|
-
if (before === null) return "first render";
|
|
738
|
-
const result = [];
|
|
739
|
-
const groups = [{
|
|
740
|
-
label: "units",
|
|
741
|
-
oldCounts: before.unitStates,
|
|
742
|
-
newCounts: after.unitStates
|
|
743
|
-
}, {
|
|
744
|
-
label: "ledger",
|
|
745
|
-
oldCounts: before.ledgerVerdicts,
|
|
746
|
-
newCounts: after.ledgerVerdicts
|
|
747
|
-
}];
|
|
748
|
-
for (const { label, oldCounts, newCounts } of groups) {
|
|
749
|
-
const names = [.../* @__PURE__ */ new Set([...Object.keys(oldCounts), ...Object.keys(newCounts)])].sort();
|
|
750
|
-
for (const name of names) {
|
|
751
|
-
const oldCount = oldCounts[name] ?? 0;
|
|
752
|
-
const newCount = newCounts[name] ?? 0;
|
|
753
|
-
if (oldCount !== newCount) result.push(`${label} ${name} ${oldCount}->${newCount}`);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
if (before.frontierGeneration !== after.frontierGeneration) result.push(`frontier generation ${before.frontierGeneration}->${after.frontierGeneration}`);
|
|
757
|
-
if (before.openGateIds.join("\0") !== after.openGateIds.join("\0")) result.push(`open gates ${before.openGateIds.length}->${after.openGateIds.length}`);
|
|
758
|
-
return result.length === 0 ? "no derived changes" : result.join("; ");
|
|
759
|
-
}
|
|
760
|
-
function markdown(value) {
|
|
761
|
-
return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
762
|
-
}
|
|
763
|
-
function table(headers, rows) {
|
|
764
|
-
if (rows.length === 0) return "(none)";
|
|
765
|
-
return [
|
|
766
|
-
`| ${headers.join(" | ")} |`,
|
|
767
|
-
`| ${headers.map(() => "---").join(" | ")} |`,
|
|
768
|
-
...rows.map((row) => `| ${row.map(markdown).join(" | ")} |`)
|
|
769
|
-
].join("\n");
|
|
770
|
-
}
|
|
771
|
-
function statusMarkdown(unitRows, ledgerRows, currentFrontier, gateRows, currentSummary) {
|
|
772
|
-
return `# Orchestrate status
|
|
773
|
-
|
|
774
|
-
Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
775
|
-
|
|
776
|
-
## Units
|
|
777
|
-
|
|
778
|
-
States: ${countLine(currentSummary.unitStates)}
|
|
779
|
-
|
|
780
|
-
${table([
|
|
781
|
-
"ID",
|
|
782
|
-
"Track",
|
|
783
|
-
"State",
|
|
784
|
-
"Branch",
|
|
785
|
-
"PR",
|
|
786
|
-
"SHA",
|
|
787
|
-
"Brief"
|
|
788
|
-
], unitRows.map(unitCells))}
|
|
789
|
-
|
|
790
|
-
## Verification ledger
|
|
791
|
-
|
|
792
|
-
Verdicts: ${countLine(currentSummary.ledgerVerdicts)}
|
|
793
|
-
|
|
794
|
-
${table([
|
|
795
|
-
"PR",
|
|
796
|
-
"SHA",
|
|
797
|
-
"Verdict",
|
|
798
|
-
"Evidence",
|
|
799
|
-
"Verifier",
|
|
800
|
-
"Timestamp"
|
|
801
|
-
], ledgerRows.map(ledgerCells))}
|
|
802
|
-
|
|
803
|
-
## Frontier
|
|
804
|
-
|
|
805
|
-
Generation: ${currentFrontier.generation}
|
|
806
|
-
Lowest unmerged: ${currentFrontier.lowestUnmerged ?? "none"}
|
|
807
|
-
|
|
808
|
-
${table([
|
|
809
|
-
"Branch",
|
|
810
|
-
"PR",
|
|
811
|
-
"SHA",
|
|
812
|
-
"State"
|
|
813
|
-
], currentFrontier.prs.map((row) => [
|
|
814
|
-
row.branches,
|
|
815
|
-
String(row.pr),
|
|
816
|
-
row.sha,
|
|
817
|
-
row.state
|
|
818
|
-
]))}
|
|
819
|
-
|
|
820
|
-
## Gates
|
|
821
|
-
|
|
822
|
-
${table([
|
|
823
|
-
"ID",
|
|
824
|
-
"Status",
|
|
825
|
-
"Question",
|
|
826
|
-
"Options",
|
|
827
|
-
"Default",
|
|
828
|
-
"Answer"
|
|
829
|
-
], gateRows.map((gate) => [
|
|
830
|
-
gate.id,
|
|
831
|
-
gate.kind,
|
|
832
|
-
gate.question,
|
|
833
|
-
gate.options,
|
|
834
|
-
gate.defaultAnswer,
|
|
835
|
-
gate.kind === "resolved" ? gate.answer : ""
|
|
836
|
-
]))}
|
|
837
|
-
|
|
838
|
-
<!-- orch-summary ${JSON.stringify(currentSummary)} -->
|
|
839
|
-
`;
|
|
840
|
-
}
|
|
841
|
-
function countLine(value) {
|
|
842
|
-
const entries = Object.entries(value);
|
|
843
|
-
return entries.length === 0 ? "none" : entries.map(([name, count]) => `${name}=${count}`).join(", ");
|
|
844
|
-
}
|
|
845
|
-
const FORGE_PULL_REQUEST_QUERIES = {
|
|
846
|
-
gh: {
|
|
847
|
-
stateField: "state",
|
|
848
|
-
headField: "headRefName",
|
|
849
|
-
baseField: "baseRefName",
|
|
850
|
-
states: /* @__PURE__ */ new Map([
|
|
851
|
-
["OPEN", "OPEN"],
|
|
852
|
-
["MERGED", "MERGED"],
|
|
853
|
-
["CLOSED", "CLOSED"]
|
|
854
|
-
])
|
|
855
|
-
},
|
|
856
|
-
origin: {
|
|
857
|
-
stateField: "status",
|
|
858
|
-
headField: "headRef",
|
|
859
|
-
baseField: "baseRef",
|
|
860
|
-
states: /* @__PURE__ */ new Map([
|
|
861
|
-
["open", "OPEN"],
|
|
862
|
-
["draft", "OPEN"],
|
|
863
|
-
["merged", "MERGED"],
|
|
864
|
-
["closed", "CLOSED"]
|
|
865
|
-
])
|
|
866
|
-
}
|
|
867
|
-
};
|
|
868
|
-
function parseForge(value) {
|
|
869
|
-
if (value === "gh" || value === "origin") return value;
|
|
870
|
-
throw new UserError("forge must be gh or origin");
|
|
871
|
-
}
|
|
872
|
-
function runTool({ command, args, repo }) {
|
|
873
|
-
try {
|
|
874
|
-
return execFileSync(command, args, {
|
|
875
|
-
cwd: repo,
|
|
876
|
-
encoding: "utf8",
|
|
877
|
-
env: process.env,
|
|
878
|
-
stdio: [
|
|
879
|
-
"ignore",
|
|
880
|
-
"pipe",
|
|
881
|
-
"pipe"
|
|
882
|
-
]
|
|
883
|
-
});
|
|
884
|
-
} catch (error) {
|
|
885
|
-
throw new UserError(`${command} ${args.join(" ")} failed: ${errorMessage(error)}`);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
function listPullRequests({ filter, branch, forge, repo }) {
|
|
889
|
-
const query = FORGE_PULL_REQUEST_QUERIES[forge];
|
|
890
|
-
const command = `${forge} pr list ${filter} ${branch}`;
|
|
891
|
-
const raw = runTool({
|
|
892
|
-
command: forge,
|
|
893
|
-
args: [
|
|
894
|
-
"pr",
|
|
895
|
-
"list",
|
|
896
|
-
filter,
|
|
897
|
-
branch,
|
|
898
|
-
"--state",
|
|
899
|
-
"all",
|
|
900
|
-
"--json",
|
|
901
|
-
[
|
|
902
|
-
"number",
|
|
903
|
-
query.stateField,
|
|
904
|
-
query.headField,
|
|
905
|
-
query.baseField
|
|
906
|
-
].join(",")
|
|
907
|
-
],
|
|
908
|
-
repo
|
|
909
|
-
});
|
|
910
|
-
let value;
|
|
911
|
-
try {
|
|
912
|
-
value = JSON.parse(raw);
|
|
913
|
-
} catch {
|
|
914
|
-
throw new UserError(`${command} output is not valid JSON`);
|
|
915
|
-
}
|
|
916
|
-
if (!isUnknownArray(value)) throw new UserError(`${command} output is not a JSON array`);
|
|
917
|
-
return value.map((row) => {
|
|
918
|
-
const status = isRecord(row) ? row[query.stateField] : void 0;
|
|
919
|
-
const state = typeof status === "string" ? query.states.get(status) ?? null : null;
|
|
920
|
-
const head = isRecord(row) ? row[query.headField] : void 0;
|
|
921
|
-
const base = isRecord(row) ? row[query.baseField] : void 0;
|
|
922
|
-
if (!isRecord(row) || typeof row.number !== "number" || !Number.isSafeInteger(row.number) || row.number < 1 || state === null || typeof head !== "string" || head.length === 0 || typeof base !== "string" || base.length === 0) throw new UserError(`${command} output has an invalid PR row`);
|
|
923
|
-
return {
|
|
924
|
-
pr: row.number,
|
|
925
|
-
state,
|
|
926
|
-
branch: head,
|
|
927
|
-
base
|
|
928
|
-
};
|
|
929
|
-
});
|
|
930
|
-
}
|
|
931
|
-
function branchPullRequest({ branch, forge, repo }) {
|
|
932
|
-
const rows = listPullRequests({
|
|
933
|
-
filter: "--head",
|
|
934
|
-
branch,
|
|
935
|
-
forge,
|
|
936
|
-
repo
|
|
937
|
-
});
|
|
938
|
-
const row = rows[0];
|
|
939
|
-
if (row === void 0) throw new UserError(`branch ${branch} has no pull request; open one with ${forge} pr create, or resolve the frontier from a branch whose chain is fully submitted`);
|
|
940
|
-
if (rows.length > 1) throw new UserError(`branch ${branch} has multiple pull requests (${rows.map((item) => `#${item.pr}`).join(", ")}); close the stale ones first`);
|
|
941
|
-
return row;
|
|
942
|
-
}
|
|
943
|
-
function openChildren({ branch, forge, repo }) {
|
|
944
|
-
return listPullRequests({
|
|
945
|
-
filter: "--base",
|
|
946
|
-
branch,
|
|
947
|
-
forge,
|
|
948
|
-
repo
|
|
949
|
-
}).filter((row) => row.state === "OPEN");
|
|
950
|
-
}
|
|
951
|
-
function runGit({ args, repo }) {
|
|
952
|
-
return runTool({
|
|
953
|
-
command: "git",
|
|
954
|
-
args,
|
|
955
|
-
repo
|
|
956
|
-
}).trim();
|
|
957
|
-
}
|
|
958
|
-
function branchSha({ branch, repo }) {
|
|
959
|
-
const sha = runGit({
|
|
960
|
-
args: ["rev-parse", branch],
|
|
961
|
-
repo
|
|
962
|
-
});
|
|
963
|
-
if (!/^[0-9a-f]{40,64}$/i.test(sha)) throw new UserError(`git rev-parse ${branch} returned an invalid SHA`);
|
|
964
|
-
return sha;
|
|
965
|
-
}
|
|
966
|
-
function currentBranch(repo) {
|
|
967
|
-
return runGit({
|
|
968
|
-
args: [
|
|
969
|
-
"symbolic-ref",
|
|
970
|
-
"--short",
|
|
971
|
-
"HEAD"
|
|
972
|
-
],
|
|
973
|
-
repo
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
function trunkBranch(repo) {
|
|
977
|
-
try {
|
|
978
|
-
return runGit({
|
|
979
|
-
args: [
|
|
980
|
-
"symbolic-ref",
|
|
981
|
-
"--short",
|
|
982
|
-
"refs/remotes/origin/HEAD"
|
|
983
|
-
],
|
|
984
|
-
repo
|
|
985
|
-
}).replace(/^origin\//, "");
|
|
986
|
-
} catch {
|
|
987
|
-
return "main";
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
function resolveFrontier(repo, forge) {
|
|
991
|
-
const trunk = trunkBranch(repo);
|
|
992
|
-
const current = currentBranch(repo);
|
|
993
|
-
if (current === trunk) return [];
|
|
994
|
-
const seen = /* @__PURE__ */ new Set([current]);
|
|
995
|
-
let bottom = branchPullRequest({
|
|
996
|
-
branch: current,
|
|
997
|
-
forge,
|
|
998
|
-
repo
|
|
999
|
-
});
|
|
1000
|
-
const chain = [bottom];
|
|
1001
|
-
while (bottom.base !== trunk) {
|
|
1002
|
-
if (seen.has(bottom.base)) throw new UserError(`pull request base chain loops at branch ${bottom.base}`);
|
|
1003
|
-
seen.add(bottom.base);
|
|
1004
|
-
bottom = branchPullRequest({
|
|
1005
|
-
branch: bottom.base,
|
|
1006
|
-
forge,
|
|
1007
|
-
repo
|
|
1008
|
-
});
|
|
1009
|
-
chain.unshift(bottom);
|
|
1010
|
-
}
|
|
1011
|
-
let tip = current;
|
|
1012
|
-
for (;;) {
|
|
1013
|
-
const children = openChildren({
|
|
1014
|
-
branch: tip,
|
|
1015
|
-
forge,
|
|
1016
|
-
repo
|
|
1017
|
-
});
|
|
1018
|
-
const child = children[0];
|
|
1019
|
-
if (child === void 0) break;
|
|
1020
|
-
if (children.length > 1) throw new UserError(`stack is not linear: branches ${children.map((row) => row.branch).join(", ")} all target ${tip}`);
|
|
1021
|
-
if (seen.has(child.branch)) throw new UserError(`pull request base chain loops at branch ${child.branch}`);
|
|
1022
|
-
seen.add(child.branch);
|
|
1023
|
-
chain.push(child);
|
|
1024
|
-
tip = child.branch;
|
|
1025
|
-
}
|
|
1026
|
-
if (new Set(chain.map((row) => row.pr)).size !== chain.length) throw new UserError("pull request base chain contains duplicate pull requests");
|
|
1027
|
-
return chain.map((row) => ({
|
|
1028
|
-
pr: row.pr,
|
|
1029
|
-
branches: row.branch,
|
|
1030
|
-
sha: branchSha({
|
|
1031
|
-
branch: row.branch,
|
|
1032
|
-
repo
|
|
1033
|
-
}),
|
|
1034
|
-
state: row.state
|
|
1035
|
-
}));
|
|
1036
|
-
}
|
|
1037
|
-
function validateFrontierPin({ actual, expected }) {
|
|
1038
|
-
if (actual.length === expected.length && actual.every((pr, index) => pr === expected[index])) return;
|
|
1039
|
-
const actualSet = new Set(actual);
|
|
1040
|
-
const expectedSet = new Set(expected);
|
|
1041
|
-
const missing = expected.filter((pr) => !actualSet.has(pr));
|
|
1042
|
-
const extra = actual.filter((pr) => !expectedSet.has(pr));
|
|
1043
|
-
const drift = [];
|
|
1044
|
-
if (missing.length > 0) drift.push(`missing from resolved chain: ${missing.join(",")}`);
|
|
1045
|
-
if (extra.length > 0) drift.push(`extra in resolved chain: ${extra.join(",")}`);
|
|
1046
|
-
if (missing.length === 0 && extra.length === 0) drift.push(`order differs: expected ${expected.join(",")}; resolved ${actual.join(",")}`);
|
|
1047
|
-
throw new UserError(`frontier pin mismatch: ${drift.join("; ")}`);
|
|
1048
|
-
}
|
|
1049
|
-
function openStore(directory, options = {}) {
|
|
1050
|
-
const store = resolve(directory);
|
|
1051
|
-
let closed = false;
|
|
1052
|
-
let releaseLock = null;
|
|
1053
|
-
let lockRequest = null;
|
|
1054
|
-
const ensureOpen = () => {
|
|
1055
|
-
if (closed) throw new UserError("store is closed");
|
|
1056
|
-
};
|
|
1057
|
-
const ensureLock = async () => {
|
|
1058
|
-
ensureOpen();
|
|
1059
|
-
if (releaseLock !== null) return;
|
|
1060
|
-
if (lockRequest === null) lockRequest = acquireLock(store, options).then((release) => {
|
|
1061
|
-
releaseLock = release;
|
|
1062
|
-
});
|
|
1063
|
-
try {
|
|
1064
|
-
await lockRequest;
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
lockRequest = null;
|
|
1067
|
-
throw error;
|
|
1068
|
-
}
|
|
1069
|
-
};
|
|
1070
|
-
const beginWrite = async () => {
|
|
1071
|
-
ensureOpen();
|
|
1072
|
-
if (!await exists(store)) throw new UserError(`store is not initialized at ${store}; run orch init`);
|
|
1073
|
-
await ensureLock();
|
|
1074
|
-
};
|
|
1075
|
-
return {
|
|
1076
|
-
units: {
|
|
1077
|
-
add: async (params) => {
|
|
1078
|
-
await beginWrite();
|
|
1079
|
-
const row = {
|
|
1080
|
-
id: requiredCell(params.id, "unit id"),
|
|
1081
|
-
track: requiredCell(params.track, "track"),
|
|
1082
|
-
state: "pending",
|
|
1083
|
-
branch: "",
|
|
1084
|
-
pr: "",
|
|
1085
|
-
sha: "",
|
|
1086
|
-
brief: params.brief === void 0 ? "" : requiredCell(params.brief, "brief")
|
|
1087
|
-
};
|
|
1088
|
-
const rows = [...await readUnits(store)];
|
|
1089
|
-
if (rows.some((unit) => unit.id === row.id)) throw new UserError(`unit ${row.id} already exists`);
|
|
1090
|
-
rows.push(row);
|
|
1091
|
-
await saveUnits(store, rows);
|
|
1092
|
-
return row;
|
|
1093
|
-
},
|
|
1094
|
-
set: async (params) => {
|
|
1095
|
-
await beginWrite();
|
|
1096
|
-
const id = requiredCell(params.id, "unit id");
|
|
1097
|
-
const state = requiredCell(params.state, "state");
|
|
1098
|
-
const rows = [...await readUnits(store)];
|
|
1099
|
-
const index = rows.findIndex((unit) => unit.id === id);
|
|
1100
|
-
const old = rows[index];
|
|
1101
|
-
if (index < 0 || old === void 0) throw new NotFoundError(`unit ${id} not found`);
|
|
1102
|
-
const row = {
|
|
1103
|
-
...old,
|
|
1104
|
-
state,
|
|
1105
|
-
branch: params.branch === void 0 ? old.branch : requiredCell(params.branch, "branch"),
|
|
1106
|
-
pr: params.pr === void 0 ? old.pr : String(positiveInteger(params.pr, "PR")),
|
|
1107
|
-
sha: params.sha === void 0 ? old.sha : requiredCell(params.sha, "SHA")
|
|
1108
|
-
};
|
|
1109
|
-
rows[index] = row;
|
|
1110
|
-
await saveUnits(store, rows);
|
|
1111
|
-
return row;
|
|
1112
|
-
},
|
|
1113
|
-
get: async (id) => {
|
|
1114
|
-
ensureOpen();
|
|
1115
|
-
const cleanId = requiredCell(id, "unit id");
|
|
1116
|
-
const row = (await readUnits(store)).find((unit) => unit.id === cleanId);
|
|
1117
|
-
if (row === void 0) throw new NotFoundError(`unit ${cleanId} not found`);
|
|
1118
|
-
return row;
|
|
1119
|
-
},
|
|
1120
|
-
list: async (params = {}) => {
|
|
1121
|
-
ensureOpen();
|
|
1122
|
-
const state = params.state === void 0 ? void 0 : requiredCell(params.state, "state");
|
|
1123
|
-
const track = params.track === void 0 ? void 0 : requiredCell(params.track, "track");
|
|
1124
|
-
return (await readUnits(store)).filter((unit) => (state === void 0 || unit.state === state) && (track === void 0 || unit.track === track));
|
|
1125
|
-
},
|
|
1126
|
-
counts: async () => {
|
|
1127
|
-
ensureOpen();
|
|
1128
|
-
return countValues((await readUnits(store)).map((unit) => unit.state));
|
|
1129
|
-
}
|
|
1130
|
-
},
|
|
1131
|
-
ledger: {
|
|
1132
|
-
record: async (params) => {
|
|
1133
|
-
await beginWrite();
|
|
1134
|
-
const verdict = parseVerdict(params.verdict);
|
|
1135
|
-
const row = {
|
|
1136
|
-
pr: String(positiveInteger(params.pr, "PR")),
|
|
1137
|
-
sha: requiredCell(params.sha, "SHA"),
|
|
1138
|
-
verdict,
|
|
1139
|
-
evidence: requiredCell(params.evidence, "evidence"),
|
|
1140
|
-
verifier: params.verifier === void 0 ? "" : requiredCell(params.verifier, "verifier"),
|
|
1141
|
-
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1142
|
-
};
|
|
1143
|
-
const rows = [...await readLedger(store)];
|
|
1144
|
-
const index = rows.findIndex((old) => old.pr === row.pr && old.sha === row.sha);
|
|
1145
|
-
if (index < 0) rows.push(row);
|
|
1146
|
-
else rows[index] = row;
|
|
1147
|
-
await saveLedger(store, rows);
|
|
1148
|
-
return row;
|
|
1149
|
-
},
|
|
1150
|
-
check: async (params) => {
|
|
1151
|
-
ensureOpen();
|
|
1152
|
-
const pr = String(positiveInteger(params.pr, "PR"));
|
|
1153
|
-
const sha = requiredCell(params.sha, "SHA");
|
|
1154
|
-
const row = (await readLedger(store)).find((value) => value.pr === pr && value.sha === sha);
|
|
1155
|
-
if (row === void 0) throw new NotFoundError("NOT-VERIFIED", {
|
|
1156
|
-
compact: "NOT-VERIFIED",
|
|
1157
|
-
json: {
|
|
1158
|
-
pr,
|
|
1159
|
-
sha,
|
|
1160
|
-
verdict: "NOT-VERIFIED"
|
|
1161
|
-
}
|
|
1162
|
-
});
|
|
1163
|
-
return row;
|
|
1164
|
-
},
|
|
1165
|
-
summary: async () => {
|
|
1166
|
-
ensureOpen();
|
|
1167
|
-
return countValues((await readLedger(store)).map((row) => row.verdict));
|
|
1168
|
-
}
|
|
1169
|
-
},
|
|
1170
|
-
inbox: {
|
|
1171
|
-
push: async (params) => {
|
|
1172
|
-
await beginWrite();
|
|
1173
|
-
const pointer = {
|
|
1174
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1175
|
-
agent: requiredCell(params.agent, "agent"),
|
|
1176
|
-
unit: requiredCell(params.unit, "unit"),
|
|
1177
|
-
status: requiredCell(params.status, "status"),
|
|
1178
|
-
report: params.report === void 0 ? "" : requiredCell(params.report, "report")
|
|
1179
|
-
};
|
|
1180
|
-
const inbox = join(store, "inbox");
|
|
1181
|
-
if (!await exists(inbox)) throw new UserError(`store is not initialized at ${store}; run orch init`);
|
|
1182
|
-
const filename = `${pointer.ts.replace(/[:.]/g, "-")}-${process.pid}-${randomUUID()}.tsv`;
|
|
1183
|
-
const contents = `${pointerCells(pointer).map(cleanCell).join(" ")}\n`;
|
|
1184
|
-
await atomicWrite(join(inbox, filename), contents);
|
|
1185
|
-
return {
|
|
1186
|
-
pointer,
|
|
1187
|
-
filename
|
|
1188
|
-
};
|
|
1189
|
-
},
|
|
1190
|
-
drain: async () => {
|
|
1191
|
-
await beginWrite();
|
|
1192
|
-
const inbox = join(store, "inbox");
|
|
1193
|
-
const rows = await readPointers(inbox);
|
|
1194
|
-
const drained = join(store, `.inbox-drain-${process.pid}-${randomUUID()}`);
|
|
1195
|
-
await rename(inbox, drained);
|
|
1196
|
-
try {
|
|
1197
|
-
await mkdir(inbox);
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
await rename(drained, inbox);
|
|
1200
|
-
throw error;
|
|
1201
|
-
}
|
|
1202
|
-
await rm(drained, {
|
|
1203
|
-
recursive: true,
|
|
1204
|
-
force: true
|
|
1205
|
-
});
|
|
1206
|
-
return rows;
|
|
1207
|
-
},
|
|
1208
|
-
peek: async () => {
|
|
1209
|
-
ensureOpen();
|
|
1210
|
-
return readPointers(join(store, "inbox"));
|
|
1211
|
-
},
|
|
1212
|
-
count: async () => {
|
|
1213
|
-
ensureOpen();
|
|
1214
|
-
return (await readPointers(join(store, "inbox"))).length;
|
|
1215
|
-
}
|
|
1216
|
-
},
|
|
1217
|
-
gates: {
|
|
1218
|
-
park: async (params) => {
|
|
1219
|
-
await beginWrite();
|
|
1220
|
-
const gate = {
|
|
1221
|
-
kind: "open",
|
|
1222
|
-
id: requiredLine(params.id, "gate id"),
|
|
1223
|
-
question: requiredLine(params.question, "question"),
|
|
1224
|
-
options: requiredLine(params.options, "options"),
|
|
1225
|
-
defaultAnswer: requiredLine(params.defaultAnswer, "default")
|
|
1226
|
-
};
|
|
1227
|
-
const rows = [...await readGates(store)];
|
|
1228
|
-
const index = rows.findIndex((old) => old.id === gate.id);
|
|
1229
|
-
if (index < 0) rows.push(gate);
|
|
1230
|
-
else rows[index] = gate;
|
|
1231
|
-
await atomicWrite(join(store, "gates.md"), renderGates(rows));
|
|
1232
|
-
return gate;
|
|
1233
|
-
},
|
|
1234
|
-
list: async () => {
|
|
1235
|
-
ensureOpen();
|
|
1236
|
-
return (await readGates(store)).filter((gate) => gate.kind === "open");
|
|
1237
|
-
},
|
|
1238
|
-
resolve: async (params) => {
|
|
1239
|
-
await beginWrite();
|
|
1240
|
-
const id = requiredLine(params.id, "gate id");
|
|
1241
|
-
const rows = [...await readGates(store)];
|
|
1242
|
-
const index = rows.findIndex((gate) => gate.id === id);
|
|
1243
|
-
const old = rows[index];
|
|
1244
|
-
if (index < 0 || old === void 0) throw new NotFoundError(`gate ${id} not found`);
|
|
1245
|
-
const gate = {
|
|
1246
|
-
kind: "resolved",
|
|
1247
|
-
id: old.id,
|
|
1248
|
-
question: old.question,
|
|
1249
|
-
options: old.options,
|
|
1250
|
-
defaultAnswer: old.defaultAnswer,
|
|
1251
|
-
answer: requiredLine(params.answer, "answer")
|
|
1252
|
-
};
|
|
1253
|
-
rows[index] = gate;
|
|
1254
|
-
await atomicWrite(join(store, "gates.md"), renderGates(rows));
|
|
1255
|
-
return gate;
|
|
1256
|
-
}
|
|
1257
|
-
},
|
|
1258
|
-
frontier: {
|
|
1259
|
-
set: async (params) => {
|
|
1260
|
-
await beginWrite();
|
|
1261
|
-
const repo = resolve(requiredLine(params.repo, "repo directory"));
|
|
1262
|
-
const forge = parseForge(params.forge ?? "gh");
|
|
1263
|
-
const pin = params.prs === void 0 ? void 0 : params.prs.map((pr) => positiveInteger(pr, "PR"));
|
|
1264
|
-
if (pin !== void 0 && new Set(pin).size !== pin.length) throw new UserError("--prs must not contain duplicates");
|
|
1265
|
-
const old = await readFrontier(store);
|
|
1266
|
-
const prs = resolveFrontier(repo, forge);
|
|
1267
|
-
if (pin !== void 0) validateFrontierPin({
|
|
1268
|
-
actual: prs.map((row) => row.pr),
|
|
1269
|
-
expected: pin
|
|
1270
|
-
});
|
|
1271
|
-
const value = {
|
|
1272
|
-
generation: old.generation + 1,
|
|
1273
|
-
prs,
|
|
1274
|
-
lowestUnmerged: prs.find((row) => row.state === "OPEN")?.pr ?? null
|
|
1275
|
-
};
|
|
1276
|
-
await atomicWrite(join(store, "frontier.json"), `${JSON.stringify(value, null, 2)}\n`);
|
|
1277
|
-
return value;
|
|
1278
|
-
},
|
|
1279
|
-
show: async () => {
|
|
1280
|
-
ensureOpen();
|
|
1281
|
-
return readFrontier(store);
|
|
1282
|
-
}
|
|
1283
|
-
},
|
|
1284
|
-
standing: {
|
|
1285
|
-
show: async () => {
|
|
1286
|
-
ensureOpen();
|
|
1287
|
-
return readStanding(store);
|
|
1288
|
-
},
|
|
1289
|
-
add: async (params) => {
|
|
1290
|
-
await beginWrite();
|
|
1291
|
-
const rows = [...await readStanding(store)];
|
|
1292
|
-
const item = {
|
|
1293
|
-
number: rows.length + 1,
|
|
1294
|
-
line: requiredLine(params.line, "standing order")
|
|
1295
|
-
};
|
|
1296
|
-
rows.push(item);
|
|
1297
|
-
await atomicWrite(join(store, "preferences.md"), `${rows.map((row) => `${row.number}. ${row.line}`).join("\n")}\n`);
|
|
1298
|
-
return item;
|
|
1299
|
-
}
|
|
1300
|
-
},
|
|
1301
|
-
status: { render: async () => {
|
|
1302
|
-
await beginWrite();
|
|
1303
|
-
const unitRows = await readUnits(store);
|
|
1304
|
-
const ledgerRows = await readLedger(store);
|
|
1305
|
-
const currentFrontier = await readFrontier(store);
|
|
1306
|
-
const gateRows = await readGates(store);
|
|
1307
|
-
const currentSummary = summarize(unitRows, ledgerRows, currentFrontier, gateRows);
|
|
1308
|
-
const path = join(store, "status.md");
|
|
1309
|
-
const change = changed(await exists(path) ? previousSummary(await readFile(path, "utf8")) : null, currentSummary);
|
|
1310
|
-
await atomicWrite(path, statusMarkdown(unitRows, ledgerRows, currentFrontier, gateRows, currentSummary));
|
|
1311
|
-
return {
|
|
1312
|
-
units: unitRows,
|
|
1313
|
-
ledger: ledgerRows,
|
|
1314
|
-
frontier: currentFrontier,
|
|
1315
|
-
gates: gateRows,
|
|
1316
|
-
summary: currentSummary,
|
|
1317
|
-
changed: change
|
|
1318
|
-
};
|
|
1319
|
-
} },
|
|
1320
|
-
init: async () => {
|
|
1321
|
-
ensureOpen();
|
|
1322
|
-
await mkdir(store, { recursive: true });
|
|
1323
|
-
await ensureLock();
|
|
1324
|
-
await writeIfMissing(join(store, "units.tsv"), `${UNIT_HEADER}\n`);
|
|
1325
|
-
await writeIfMissing(join(store, "ledger.tsv"), `${LEDGER_HEADER}\n`);
|
|
1326
|
-
await mkdir(join(store, "inbox"), { recursive: true });
|
|
1327
|
-
await writeIfMissing(join(store, "gates.md"), "");
|
|
1328
|
-
await writeIfMissing(join(store, "preferences.md"), "");
|
|
1329
|
-
await writeIfMissing(join(store, "frontier.json"), "{}\n");
|
|
1330
|
-
return { store };
|
|
1331
|
-
},
|
|
1332
|
-
close: async () => {
|
|
1333
|
-
if (closed) return;
|
|
1334
|
-
if (lockRequest !== null) try {
|
|
1335
|
-
await lockRequest;
|
|
1336
|
-
} catch {}
|
|
1337
|
-
const release = releaseLock;
|
|
1338
|
-
releaseLock = null;
|
|
1339
|
-
closed = true;
|
|
1340
|
-
if (release !== null) await release();
|
|
1341
|
-
}
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
//#endregion
|
|
1345
|
-
//#region .runtime-sources/skills/poteto-mode/scripts/orch/orch.test.ts
|
|
1346
|
-
const SCRIPT = join(import.meta.dirname, "orch.mjs");
|
|
1347
|
-
const directories = [];
|
|
1348
|
-
const handles = [];
|
|
1349
|
-
async function makeDirectory() {
|
|
1350
|
-
const directory = await mkdtemp(join(tmpdir(), "orch-test-"));
|
|
1351
|
-
directories.push(directory);
|
|
1352
|
-
return directory;
|
|
1353
|
-
}
|
|
1354
|
-
function useStore(directory, options) {
|
|
1355
|
-
const store = openStore(directory, options);
|
|
1356
|
-
handles.push(store);
|
|
1357
|
-
return store;
|
|
1358
|
-
}
|
|
1359
|
-
async function initializedStore() {
|
|
1360
|
-
const directory = await makeDirectory();
|
|
1361
|
-
const store = useStore(directory);
|
|
1362
|
-
await store.init();
|
|
1363
|
-
return {
|
|
1364
|
-
directory,
|
|
1365
|
-
store
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
function git({ args, repo }) {
|
|
1369
|
-
const result = Bun.spawnSync([
|
|
1370
|
-
"git",
|
|
1371
|
-
"-C",
|
|
1372
|
-
repo,
|
|
1373
|
-
...args
|
|
1374
|
-
]);
|
|
1375
|
-
if (result.exitCode !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr.toString()}`);
|
|
1376
|
-
return result.stdout.toString().trim();
|
|
1377
|
-
}
|
|
1378
|
-
async function makeGitStack(directory) {
|
|
1379
|
-
const repo = join(directory, "repo");
|
|
1380
|
-
await mkdir(repo);
|
|
1381
|
-
git({
|
|
1382
|
-
repo,
|
|
1383
|
-
args: ["init", "--initial-branch=main"]
|
|
1384
|
-
});
|
|
1385
|
-
git({
|
|
1386
|
-
repo,
|
|
1387
|
-
args: [
|
|
1388
|
-
"config",
|
|
1389
|
-
"user.name",
|
|
1390
|
-
"Orch Test"
|
|
1391
|
-
]
|
|
1392
|
-
});
|
|
1393
|
-
git({
|
|
1394
|
-
repo,
|
|
1395
|
-
args: [
|
|
1396
|
-
"config",
|
|
1397
|
-
"user.email",
|
|
1398
|
-
"orch@example.com"
|
|
1399
|
-
]
|
|
1400
|
-
});
|
|
1401
|
-
await writeFile(join(repo, "main.txt"), "main\n");
|
|
1402
|
-
git({
|
|
1403
|
-
repo,
|
|
1404
|
-
args: ["add", "."]
|
|
1405
|
-
});
|
|
1406
|
-
git({
|
|
1407
|
-
repo,
|
|
1408
|
-
args: [
|
|
1409
|
-
"commit",
|
|
1410
|
-
"-m",
|
|
1411
|
-
"main"
|
|
1412
|
-
]
|
|
1413
|
-
});
|
|
1414
|
-
for (const [index, branch] of [
|
|
1415
|
-
"stack/merged",
|
|
1416
|
-
"stack/closed",
|
|
1417
|
-
"stack/open",
|
|
1418
|
-
"stack/draft"
|
|
1419
|
-
].entries()) {
|
|
1420
|
-
git({
|
|
1421
|
-
repo,
|
|
1422
|
-
args: [
|
|
1423
|
-
"checkout",
|
|
1424
|
-
"-b",
|
|
1425
|
-
branch
|
|
1426
|
-
]
|
|
1427
|
-
});
|
|
1428
|
-
await writeFile(join(repo, `stack-${index}.txt`), `${branch}\n`);
|
|
1429
|
-
git({
|
|
1430
|
-
repo,
|
|
1431
|
-
args: ["add", "."]
|
|
1432
|
-
});
|
|
1433
|
-
git({
|
|
1434
|
-
repo,
|
|
1435
|
-
args: [
|
|
1436
|
-
"commit",
|
|
1437
|
-
"-m",
|
|
1438
|
-
branch
|
|
1439
|
-
]
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
git({
|
|
1443
|
-
repo,
|
|
1444
|
-
args: ["checkout", "stack/open"]
|
|
1445
|
-
});
|
|
1446
|
-
return {
|
|
1447
|
-
repo,
|
|
1448
|
-
mergedSha: git({
|
|
1449
|
-
repo,
|
|
1450
|
-
args: ["rev-parse", "stack/merged"]
|
|
1451
|
-
}),
|
|
1452
|
-
closedSha: git({
|
|
1453
|
-
repo,
|
|
1454
|
-
args: ["rev-parse", "stack/closed"]
|
|
1455
|
-
}),
|
|
1456
|
-
openSha: git({
|
|
1457
|
-
repo,
|
|
1458
|
-
args: ["rev-parse", "stack/open"]
|
|
1459
|
-
}),
|
|
1460
|
-
draftSha: git({
|
|
1461
|
-
repo,
|
|
1462
|
-
args: ["rev-parse", "stack/draft"]
|
|
1463
|
-
})
|
|
1464
|
-
};
|
|
1465
|
-
}
|
|
1466
|
-
const FORGE_FIXTURES = {
|
|
1467
|
-
gh: {
|
|
1468
|
-
fields: "number,state,headRefName,baseRefName",
|
|
1469
|
-
row: ({ number, state, head, base }) => ({
|
|
1470
|
-
number,
|
|
1471
|
-
state: state === "draft" ? "OPEN" : state.toUpperCase(),
|
|
1472
|
-
headRefName: head,
|
|
1473
|
-
baseRefName: base
|
|
1474
|
-
})
|
|
1475
|
-
},
|
|
1476
|
-
origin: {
|
|
1477
|
-
fields: "number,status,headRef,baseRef",
|
|
1478
|
-
row: ({ number, state, head, base }) => ({
|
|
1479
|
-
number,
|
|
1480
|
-
status: state,
|
|
1481
|
-
headRef: head,
|
|
1482
|
-
baseRef: base
|
|
1483
|
-
})
|
|
1484
|
-
}
|
|
1485
|
-
};
|
|
1486
|
-
function forgeRows(forge, prs) {
|
|
1487
|
-
return JSON.stringify(prs.map((pr) => FORGE_FIXTURES[forge].row(pr)));
|
|
1488
|
-
}
|
|
1489
|
-
async function withFakeForge({ directory, forge, operation, currentRows }) {
|
|
1490
|
-
const bin = join(directory, "bin");
|
|
1491
|
-
const outputPath = join(directory, "forge-output.txt");
|
|
1492
|
-
await mkdir(bin, { recursive: true });
|
|
1493
|
-
await writeFile(outputPath, `${currentRows ?? forgeRows(forge, [{
|
|
1494
|
-
number: 11,
|
|
1495
|
-
state: "open",
|
|
1496
|
-
head: "stack/open",
|
|
1497
|
-
base: "stack/closed"
|
|
1498
|
-
}])}\n`);
|
|
1499
|
-
const { fields } = FORGE_FIXTURES[forge];
|
|
1500
|
-
const list = (filter, branch) => `pr list ${filter} ${branch} --state all --json ${fields}`;
|
|
1501
|
-
const fake = join(bin, forge);
|
|
1502
|
-
await writeFile(fake, `#!/usr/bin/env node
|
|
1503
|
-
const { readFileSync, realpathSync } = require("node:fs");
|
|
1504
|
-
const args = process.argv.slice(2).join(" ");
|
|
1505
|
-
const here = realpathSync(process.cwd());
|
|
1506
|
-
const expectedCwd = ${JSON.stringify(realpathSync(join(directory, "repo")))};
|
|
1507
|
-
if (here !== expectedCwd) {
|
|
1508
|
-
process.stderr.write(${JSON.stringify(`${forge} ran outside the fixture repo: `)} + here + "\\n");
|
|
1509
|
-
process.exit(2);
|
|
1510
|
-
}
|
|
1511
|
-
const expected = [
|
|
1512
|
-
${JSON.stringify(list("--head", "stack/open"))},
|
|
1513
|
-
${JSON.stringify(list("--head", "stack/closed"))},
|
|
1514
|
-
${JSON.stringify(list("--head", "stack/merged"))},
|
|
1515
|
-
${JSON.stringify(list("--base", "stack/open"))},
|
|
1516
|
-
${JSON.stringify(list("--base", "stack/draft"))}
|
|
1517
|
-
];
|
|
1518
|
-
const answers = [
|
|
1519
|
-
() => readFileSync(${JSON.stringify(outputPath)}, "utf8"),
|
|
1520
|
-
() => ${JSON.stringify(`${forgeRows(forge, [{
|
|
1521
|
-
number: 13,
|
|
1522
|
-
state: "closed",
|
|
1523
|
-
head: "stack/closed",
|
|
1524
|
-
base: "stack/merged"
|
|
1525
|
-
}])}\n`)},
|
|
1526
|
-
() => ${JSON.stringify(`${forgeRows(forge, [{
|
|
1527
|
-
number: 10,
|
|
1528
|
-
state: "merged",
|
|
1529
|
-
head: "stack/merged",
|
|
1530
|
-
base: "main"
|
|
1531
|
-
}])}\n`)},
|
|
1532
|
-
() => ${JSON.stringify(`${forgeRows(forge, [{
|
|
1533
|
-
number: 14,
|
|
1534
|
-
state: "draft",
|
|
1535
|
-
head: "stack/draft",
|
|
1536
|
-
base: "stack/open"
|
|
1537
|
-
}, {
|
|
1538
|
-
number: 15,
|
|
1539
|
-
state: "closed",
|
|
1540
|
-
head: "stack/abandoned",
|
|
1541
|
-
base: "stack/open"
|
|
1542
|
-
}])}\n`)},
|
|
1543
|
-
() => ${JSON.stringify("[]\n")}
|
|
1544
|
-
];
|
|
1545
|
-
const index = expected.indexOf(args);
|
|
1546
|
-
if (index === -1) {
|
|
1547
|
-
process.stderr.write(${JSON.stringify(`unexpected ${forge} arguments: `)} + args + "\\n");
|
|
1548
|
-
process.exit(2);
|
|
1549
|
-
}
|
|
1550
|
-
process.stdout.write(answers[index]());
|
|
1551
|
-
`);
|
|
1552
|
-
await chmod(fake, 493);
|
|
1553
|
-
const originalPath = process.env.PATH;
|
|
1554
|
-
process.env.PATH = `${bin}:${originalPath ?? ""}`;
|
|
1555
|
-
try {
|
|
1556
|
-
return await operation();
|
|
1557
|
-
} finally {
|
|
1558
|
-
if (originalPath === void 0) delete process.env.PATH;
|
|
1559
|
-
else process.env.PATH = originalPath;
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
function runCli(args, env = process.env) {
|
|
1563
|
-
const result = Bun.spawnSync([
|
|
1564
|
-
process.execPath,
|
|
1565
|
-
SCRIPT,
|
|
1566
|
-
...args
|
|
1567
|
-
], { env });
|
|
1568
|
-
return {
|
|
1569
|
-
code: result.exitCode,
|
|
1570
|
-
stdout: result.stdout.toString(),
|
|
1571
|
-
stderr: result.stderr.toString()
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
afterEach(async () => {
|
|
1575
|
-
for (const store of handles.splice(0).reverse()) await store.close();
|
|
1576
|
-
for (const directory of directories.splice(0)) await rm(directory, {
|
|
1577
|
-
recursive: true,
|
|
1578
|
-
force: true
|
|
1579
|
-
});
|
|
1580
|
-
});
|
|
1581
|
-
describe("Store", () => {
|
|
1582
|
-
it("initializes an idempotent plain-file store and releases its lock", async () => {
|
|
1583
|
-
const directory = await makeDirectory();
|
|
1584
|
-
const store = useStore(directory);
|
|
1585
|
-
expect(await store.init()).toEqual({ store: directory });
|
|
1586
|
-
const firstUnits = await readFile(join(directory, "units.tsv"), "utf8");
|
|
1587
|
-
const firstLedger = await readFile(join(directory, "ledger.tsv"), "utf8");
|
|
1588
|
-
expect(await store.init()).toEqual({ store: directory });
|
|
1589
|
-
expect(await readFile(join(directory, "units.tsv"), "utf8")).toBe(firstUnits);
|
|
1590
|
-
expect(await readFile(join(directory, "ledger.tsv"), "utf8")).toBe(firstLedger);
|
|
1591
|
-
expect((await readdir(directory)).sort()).toEqual([
|
|
1592
|
-
".orch.lock",
|
|
1593
|
-
"frontier.json",
|
|
1594
|
-
"gates.md",
|
|
1595
|
-
"inbox",
|
|
1596
|
-
"ledger.tsv",
|
|
1597
|
-
"preferences.md",
|
|
1598
|
-
"units.tsv"
|
|
1599
|
-
]);
|
|
1600
|
-
await store.close();
|
|
1601
|
-
expect(await readdir(directory)).not.toContain(".orch.lock");
|
|
1602
|
-
});
|
|
1603
|
-
it("composes unit add, set, get, list, and counts", async () => {
|
|
1604
|
-
const { store } = await initializedStore();
|
|
1605
|
-
expect(await store.units.add({
|
|
1606
|
-
id: "u1",
|
|
1607
|
-
track: "build",
|
|
1608
|
-
brief: "briefs/u1.md"
|
|
1609
|
-
})).toMatchObject({
|
|
1610
|
-
id: "u1",
|
|
1611
|
-
state: "pending"
|
|
1612
|
-
});
|
|
1613
|
-
expect(await store.units.add({
|
|
1614
|
-
id: "=SUM(A1)",
|
|
1615
|
-
track: "+build"
|
|
1616
|
-
})).toMatchObject({
|
|
1617
|
-
id: "'=SUM(A1)",
|
|
1618
|
-
track: "'+build"
|
|
1619
|
-
});
|
|
1620
|
-
const updated = await store.units.set({
|
|
1621
|
-
id: "u1",
|
|
1622
|
-
state: "done",
|
|
1623
|
-
branch: "poteto/u1",
|
|
1624
|
-
pr: 184530,
|
|
1625
|
-
sha: "abc123"
|
|
1626
|
-
});
|
|
1627
|
-
expect(updated).toEqual({
|
|
1628
|
-
id: "u1",
|
|
1629
|
-
track: "build",
|
|
1630
|
-
state: "done",
|
|
1631
|
-
branch: "poteto/u1",
|
|
1632
|
-
pr: "184530",
|
|
1633
|
-
sha: "abc123",
|
|
1634
|
-
brief: "briefs/u1.md"
|
|
1635
|
-
});
|
|
1636
|
-
expect(await store.units.get("u1")).toEqual(updated);
|
|
1637
|
-
expect(await store.units.list({
|
|
1638
|
-
state: "done",
|
|
1639
|
-
track: "build"
|
|
1640
|
-
})).toEqual([updated]);
|
|
1641
|
-
expect(await store.units.counts()).toEqual({
|
|
1642
|
-
done: 1,
|
|
1643
|
-
pending: 1
|
|
1644
|
-
});
|
|
1645
|
-
await expect(store.units.add({
|
|
1646
|
-
id: "u1",
|
|
1647
|
-
track: "build"
|
|
1648
|
-
})).rejects.toThrow("unit u1 already exists");
|
|
1649
|
-
await expect(store.units.set({
|
|
1650
|
-
id: "missing",
|
|
1651
|
-
state: "done"
|
|
1652
|
-
})).rejects.toBeInstanceOf(NotFoundError);
|
|
1653
|
-
});
|
|
1654
|
-
it("records, replaces, checks, and summarizes typed ledger verdicts", async () => {
|
|
1655
|
-
const { store } = await initializedStore();
|
|
1656
|
-
try {
|
|
1657
|
-
await store.ledger.check({
|
|
1658
|
-
pr: 184530,
|
|
1659
|
-
sha: "abc123"
|
|
1660
|
-
});
|
|
1661
|
-
throw new Error("expected ledger check to fail");
|
|
1662
|
-
} catch (error) {
|
|
1663
|
-
expect(error).toBeInstanceOf(NotFoundError);
|
|
1664
|
-
if (error instanceof NotFoundError) expect(error.output).toEqual({
|
|
1665
|
-
compact: "NOT-VERIFIED",
|
|
1666
|
-
json: {
|
|
1667
|
-
pr: "184530",
|
|
1668
|
-
sha: "abc123",
|
|
1669
|
-
verdict: "NOT-VERIFIED"
|
|
1670
|
-
}
|
|
1671
|
-
});
|
|
1672
|
-
}
|
|
1673
|
-
expect(() => parseVerdict("looks-good")).toThrow("verdict must be");
|
|
1674
|
-
const recorded = await store.ledger.record({
|
|
1675
|
-
pr: 184530,
|
|
1676
|
-
sha: "abc123",
|
|
1677
|
-
verdict: "unit-test-verified",
|
|
1678
|
-
evidence: "reports/verify.md",
|
|
1679
|
-
verifier: "opus"
|
|
1680
|
-
});
|
|
1681
|
-
expect(await store.ledger.check({
|
|
1682
|
-
pr: 184530,
|
|
1683
|
-
sha: "abc123"
|
|
1684
|
-
})).toEqual(recorded);
|
|
1685
|
-
expect(await store.ledger.summary()).toEqual({ "unit-test-verified": 1 });
|
|
1686
|
-
await store.ledger.record({
|
|
1687
|
-
pr: 184530,
|
|
1688
|
-
sha: "abc123",
|
|
1689
|
-
verdict: "live-ui-verified",
|
|
1690
|
-
evidence: "reports/live.md"
|
|
1691
|
-
});
|
|
1692
|
-
expect(await store.ledger.summary()).toEqual({ "live-ui-verified": 1 });
|
|
1693
|
-
});
|
|
1694
|
-
it("pushes, peeks, and atomically drains inbox pointers", async () => {
|
|
1695
|
-
const { directory, store } = await initializedStore();
|
|
1696
|
-
const first = await store.inbox.push({
|
|
1697
|
-
agent: "worker-1",
|
|
1698
|
-
unit: "u1",
|
|
1699
|
-
status: "done",
|
|
1700
|
-
report: "reports/u1.md"
|
|
1701
|
-
});
|
|
1702
|
-
expect(first.pointer).toMatchObject({
|
|
1703
|
-
unit: "u1",
|
|
1704
|
-
status: "done"
|
|
1705
|
-
});
|
|
1706
|
-
expect(first.filename).toEndWith(".tsv");
|
|
1707
|
-
await store.inbox.push({
|
|
1708
|
-
agent: "worker-2",
|
|
1709
|
-
unit: "u2",
|
|
1710
|
-
status: "failed"
|
|
1711
|
-
});
|
|
1712
|
-
expect(await store.inbox.count()).toBe(2);
|
|
1713
|
-
expect(await store.inbox.peek()).toHaveLength(2);
|
|
1714
|
-
expect(await store.inbox.count()).toBe(2);
|
|
1715
|
-
expect(await store.inbox.drain()).toHaveLength(2);
|
|
1716
|
-
expect(await store.inbox.count()).toBe(0);
|
|
1717
|
-
expect(await readdir(join(directory, "inbox"))).toEqual([]);
|
|
1718
|
-
expect((await readdir(directory)).filter((name) => name.startsWith(".inbox-drain-"))).toEqual([]);
|
|
1719
|
-
});
|
|
1720
|
-
it("replaces a stale lock whose holder pid is dead", async () => {
|
|
1721
|
-
const { directory } = await initializedStore();
|
|
1722
|
-
const exited = Bun.spawn(["true"]);
|
|
1723
|
-
await exited.exited;
|
|
1724
|
-
await writeFile(join(directory, ".orch.lock"), `${exited.pid}\n`);
|
|
1725
|
-
const stale = [];
|
|
1726
|
-
const recovered = useStore(directory, { onStaleLock: (holder) => stale.push(holder) });
|
|
1727
|
-
expect(await recovered.units.add({
|
|
1728
|
-
id: "u1",
|
|
1729
|
-
track: "build"
|
|
1730
|
-
})).toMatchObject({ id: "u1" });
|
|
1731
|
-
expect(stale).toEqual([String(exited.pid)]);
|
|
1732
|
-
await recovered.close();
|
|
1733
|
-
expect(await readdir(directory)).not.toContain(".orch.lock");
|
|
1734
|
-
});
|
|
1735
|
-
it("blocks a writer and steals the pid lock only with force", async () => {
|
|
1736
|
-
const { directory, store } = await initializedStore();
|
|
1737
|
-
await store.close();
|
|
1738
|
-
await writeFile(join(directory, ".orch.lock"), `${process.pid}\n`);
|
|
1739
|
-
const blocked = useStore(directory);
|
|
1740
|
-
await expect(blocked.units.add({
|
|
1741
|
-
id: "u1",
|
|
1742
|
-
track: "build"
|
|
1743
|
-
})).rejects.toThrow(`store lock held by pid ${process.pid}`);
|
|
1744
|
-
const stolen = [];
|
|
1745
|
-
const forced = useStore(directory, {
|
|
1746
|
-
force: true,
|
|
1747
|
-
onLockStolen: (holder) => stolen.push(holder)
|
|
1748
|
-
});
|
|
1749
|
-
expect(await forced.units.add({
|
|
1750
|
-
id: "u1",
|
|
1751
|
-
track: "build"
|
|
1752
|
-
})).toMatchObject({ id: "u1" });
|
|
1753
|
-
expect(stolen).toEqual([String(process.pid)]);
|
|
1754
|
-
await forced.close();
|
|
1755
|
-
expect(await readdir(directory)).not.toContain(".orch.lock");
|
|
1756
|
-
});
|
|
1757
|
-
it("parks gates, stores standing orders, and renders status", async () => {
|
|
1758
|
-
const { directory, store } = await initializedStore();
|
|
1759
|
-
await store.units.add({
|
|
1760
|
-
id: "u1",
|
|
1761
|
-
track: "build"
|
|
1762
|
-
});
|
|
1763
|
-
expect(await store.gates.park({
|
|
1764
|
-
id: "release",
|
|
1765
|
-
question: "Ship now?",
|
|
1766
|
-
options: "ship,wait",
|
|
1767
|
-
defaultAnswer: "wait"
|
|
1768
|
-
})).toMatchObject({
|
|
1769
|
-
kind: "open",
|
|
1770
|
-
id: "release"
|
|
1771
|
-
});
|
|
1772
|
-
expect(await store.standing.add({ line: "Never force push." })).toEqual({
|
|
1773
|
-
number: 1,
|
|
1774
|
-
line: "Never force push."
|
|
1775
|
-
});
|
|
1776
|
-
const first = await store.status.render();
|
|
1777
|
-
expect(first.changed).toBe("first render");
|
|
1778
|
-
expect(first.summary.openGateIds).toEqual(["release"]);
|
|
1779
|
-
expect(await readFile(join(directory, "status.md"), "utf8")).toContain("| release | open | Ship now? |");
|
|
1780
|
-
expect((await store.status.render()).changed).toBe("no derived changes");
|
|
1781
|
-
expect(await store.gates.resolve({
|
|
1782
|
-
id: "release",
|
|
1783
|
-
answer: "ship"
|
|
1784
|
-
})).toMatchObject({
|
|
1785
|
-
kind: "resolved",
|
|
1786
|
-
answer: "ship"
|
|
1787
|
-
});
|
|
1788
|
-
expect((await store.status.render()).changed).toBe("open gates 1->0");
|
|
1789
|
-
expect(await store.gates.list()).toEqual([]);
|
|
1790
|
-
expect(await store.standing.show()).toEqual([{
|
|
1791
|
-
number: 1,
|
|
1792
|
-
line: "Never force push."
|
|
1793
|
-
}]);
|
|
1794
|
-
});
|
|
1795
|
-
for (const forge of ["gh", "origin"]) {
|
|
1796
|
-
it(`resolves the ordered frontier from the ${forge} base chain and validates an optional pin`, async () => {
|
|
1797
|
-
const { directory, store } = await initializedStore();
|
|
1798
|
-
const stack = await makeGitStack(directory);
|
|
1799
|
-
const forgeParams = forge === "gh" ? {} : { forge };
|
|
1800
|
-
await withFakeForge({
|
|
1801
|
-
directory,
|
|
1802
|
-
forge,
|
|
1803
|
-
operation: async () => {
|
|
1804
|
-
expect(await store.frontier.set({
|
|
1805
|
-
repo: stack.repo,
|
|
1806
|
-
...forgeParams
|
|
1807
|
-
})).toEqual({
|
|
1808
|
-
generation: 1,
|
|
1809
|
-
prs: [
|
|
1810
|
-
{
|
|
1811
|
-
pr: 10,
|
|
1812
|
-
branches: "stack/merged",
|
|
1813
|
-
sha: stack.mergedSha,
|
|
1814
|
-
state: "MERGED"
|
|
1815
|
-
},
|
|
1816
|
-
{
|
|
1817
|
-
pr: 13,
|
|
1818
|
-
branches: "stack/closed",
|
|
1819
|
-
sha: stack.closedSha,
|
|
1820
|
-
state: "CLOSED"
|
|
1821
|
-
},
|
|
1822
|
-
{
|
|
1823
|
-
pr: 11,
|
|
1824
|
-
branches: "stack/open",
|
|
1825
|
-
sha: stack.openSha,
|
|
1826
|
-
state: "OPEN"
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
pr: 14,
|
|
1830
|
-
branches: "stack/draft",
|
|
1831
|
-
sha: stack.draftSha,
|
|
1832
|
-
state: "OPEN"
|
|
1833
|
-
}
|
|
1834
|
-
],
|
|
1835
|
-
lowestUnmerged: 11
|
|
1836
|
-
});
|
|
1837
|
-
expect((await store.frontier.set({
|
|
1838
|
-
repo: stack.repo,
|
|
1839
|
-
...forgeParams,
|
|
1840
|
-
prs: [
|
|
1841
|
-
10,
|
|
1842
|
-
13,
|
|
1843
|
-
11,
|
|
1844
|
-
14
|
|
1845
|
-
]
|
|
1846
|
-
})).generation).toBe(2);
|
|
1847
|
-
expect((await store.frontier.show()).generation).toBe(2);
|
|
1848
|
-
await expect(store.frontier.set({
|
|
1849
|
-
repo: stack.repo,
|
|
1850
|
-
...forgeParams,
|
|
1851
|
-
prs: [
|
|
1852
|
-
10,
|
|
1853
|
-
11,
|
|
1854
|
-
12
|
|
1855
|
-
]
|
|
1856
|
-
})).rejects.toThrow("frontier pin mismatch: missing from resolved chain: 12; extra in resolved chain: 13,14");
|
|
1857
|
-
await expect(store.frontier.set({
|
|
1858
|
-
repo: stack.repo,
|
|
1859
|
-
...forgeParams,
|
|
1860
|
-
prs: [
|
|
1861
|
-
13,
|
|
1862
|
-
10,
|
|
1863
|
-
11,
|
|
1864
|
-
14
|
|
1865
|
-
]
|
|
1866
|
-
})).rejects.toThrow("frontier pin mismatch: order differs: expected 13,10,11,14; resolved 10,13,11,14");
|
|
1867
|
-
await expect(store.frontier.set({
|
|
1868
|
-
repo: stack.repo,
|
|
1869
|
-
...forgeParams,
|
|
1870
|
-
prs: [10, 10]
|
|
1871
|
-
})).rejects.toThrow("--prs must not contain duplicates");
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
});
|
|
1875
|
-
it(`rejects unparseable ${forge} output loudly`, async () => {
|
|
1876
|
-
const { directory, store } = await initializedStore();
|
|
1877
|
-
const stack = await makeGitStack(directory);
|
|
1878
|
-
await withFakeForge({
|
|
1879
|
-
directory,
|
|
1880
|
-
forge,
|
|
1881
|
-
currentRows: "this line is not forge output",
|
|
1882
|
-
operation: async () => {
|
|
1883
|
-
await expect(store.frontier.set({
|
|
1884
|
-
repo: stack.repo,
|
|
1885
|
-
forge
|
|
1886
|
-
})).rejects.toThrow(`${forge} pr list --head stack/open output is not valid JSON`);
|
|
1887
|
-
}
|
|
1888
|
-
});
|
|
1889
|
-
});
|
|
1890
|
-
}
|
|
1891
|
-
it("names the missing pull request when a chain branch has none", async () => {
|
|
1892
|
-
const { directory, store } = await initializedStore();
|
|
1893
|
-
const stack = await makeGitStack(directory);
|
|
1894
|
-
await withFakeForge({
|
|
1895
|
-
directory,
|
|
1896
|
-
forge: "gh",
|
|
1897
|
-
currentRows: "[]",
|
|
1898
|
-
operation: async () => {
|
|
1899
|
-
await expect(store.frontier.set({ repo: stack.repo })).rejects.toThrow("branch stack/open has no pull request; open one with gh pr create");
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
});
|
|
1903
|
-
it("rejects malformed TSV, verdict, frontier, and inbox data", async () => {
|
|
1904
|
-
const { directory, store } = await initializedStore();
|
|
1905
|
-
await writeFile(join(directory, "units.tsv"), "wrong\n");
|
|
1906
|
-
await expect(store.units.list()).rejects.toThrow("units.tsv has an invalid header");
|
|
1907
|
-
await writeFile(join(directory, "units.tsv"), "id track state branch pr sha brief\nshort row\n");
|
|
1908
|
-
await expect(store.units.list()).rejects.toThrow("units.tsv has a malformed row");
|
|
1909
|
-
await writeFile(join(directory, "ledger.tsv"), "pr sha verdict evidence verifier ts\n1 sha invalid report me now\n");
|
|
1910
|
-
await expect(store.ledger.summary()).rejects.toThrow("ledger.tsv has invalid verdict invalid");
|
|
1911
|
-
await writeFile(join(directory, "frontier.json"), "{\"generation\":\"1\"}\n");
|
|
1912
|
-
await expect(store.frontier.show()).rejects.toThrow("frontier.json has an invalid shape");
|
|
1913
|
-
await writeFile(join(directory, "inbox", "bad.tsv"), "too short\n");
|
|
1914
|
-
await expect(store.inbox.peek()).rejects.toThrow("inbox pointer bad.tsv is malformed");
|
|
1915
|
-
});
|
|
1916
|
-
it("rejects operations after close", async () => {
|
|
1917
|
-
const { store } = await initializedStore();
|
|
1918
|
-
await store.close();
|
|
1919
|
-
await expect(store.units.list()).rejects.toThrow("store is closed");
|
|
1920
|
-
await expect(store.status.render()).rejects.toBeInstanceOf(UserError);
|
|
1921
|
-
});
|
|
1922
|
-
});
|
|
1923
|
-
describe("orch CLI", () => {
|
|
1924
|
-
it("prints commander help and rejects invalid parsing with exit 1", async () => {
|
|
1925
|
-
const help = runCli(["--help"]);
|
|
1926
|
-
expect(help.code).toBe(0);
|
|
1927
|
-
expect(help.stdout).toContain("Commands:");
|
|
1928
|
-
expect(help.stdout).toContain("unit");
|
|
1929
|
-
expect(help.stdout).toContain("ledger");
|
|
1930
|
-
const frontierHelp = runCli([
|
|
1931
|
-
"frontier",
|
|
1932
|
-
"set",
|
|
1933
|
-
"--help"
|
|
1934
|
-
]);
|
|
1935
|
-
expect(frontierHelp.code).toBe(0);
|
|
1936
|
-
expect(frontierHelp.stdout).toContain("--repo <dir>");
|
|
1937
|
-
expect(frontierHelp.stdout).toContain("--forge <forge>");
|
|
1938
|
-
expect(frontierHelp.stdout).toContain("--prs <n,...>");
|
|
1939
|
-
const directory = await makeDirectory();
|
|
1940
|
-
const invalid = runCli([
|
|
1941
|
-
"--store",
|
|
1942
|
-
directory,
|
|
1943
|
-
"unit",
|
|
1944
|
-
"add",
|
|
1945
|
-
"u1"
|
|
1946
|
-
]);
|
|
1947
|
-
expect(invalid.code).toBe(1);
|
|
1948
|
-
expect(invalid.stderr).toContain("required option '--track <track>'");
|
|
1949
|
-
const badForge = runCli([
|
|
1950
|
-
"--store",
|
|
1951
|
-
directory,
|
|
1952
|
-
"frontier",
|
|
1953
|
-
"set",
|
|
1954
|
-
"--repo",
|
|
1955
|
-
directory,
|
|
1956
|
-
"--forge",
|
|
1957
|
-
"gt"
|
|
1958
|
-
]);
|
|
1959
|
-
expect(badForge.code).toBe(1);
|
|
1960
|
-
expect(badForge.stderr).toContain("Allowed choices are gh, origin");
|
|
1961
|
-
});
|
|
1962
|
-
it("accepts ORCH_STORE and emits complete JSON", async () => {
|
|
1963
|
-
const directory = await makeDirectory();
|
|
1964
|
-
const env = {
|
|
1965
|
-
...process.env,
|
|
1966
|
-
ORCH_STORE: directory
|
|
1967
|
-
};
|
|
1968
|
-
expect(runCli(["init"], env).code).toBe(0);
|
|
1969
|
-
const added = runCli([
|
|
1970
|
-
"unit",
|
|
1971
|
-
"add",
|
|
1972
|
-
"u1",
|
|
1973
|
-
"--track",
|
|
1974
|
-
"build",
|
|
1975
|
-
"--json"
|
|
1976
|
-
], env);
|
|
1977
|
-
expect(added.code).toBe(0);
|
|
1978
|
-
expect(JSON.parse(added.stdout)).toEqual({
|
|
1979
|
-
id: "u1",
|
|
1980
|
-
track: "build",
|
|
1981
|
-
state: "pending",
|
|
1982
|
-
branch: "",
|
|
1983
|
-
pr: "",
|
|
1984
|
-
sha: "",
|
|
1985
|
-
brief: ""
|
|
1986
|
-
});
|
|
1987
|
-
});
|
|
1988
|
-
it("maps user and not-found outcomes to the preserved exit codes", async () => {
|
|
1989
|
-
const directory = await makeDirectory();
|
|
1990
|
-
expect(runCli([
|
|
1991
|
-
"--store",
|
|
1992
|
-
directory,
|
|
1993
|
-
"init"
|
|
1994
|
-
]).code).toBe(0);
|
|
1995
|
-
const missingRepo = runCli([
|
|
1996
|
-
"--store",
|
|
1997
|
-
directory,
|
|
1998
|
-
"frontier",
|
|
1999
|
-
"set"
|
|
2000
|
-
]);
|
|
2001
|
-
expect(missingRepo.code).toBe(1);
|
|
2002
|
-
expect(missingRepo.stderr).toContain("set --repo <dir> or ORCH_REPO");
|
|
2003
|
-
const userError = runCli([
|
|
2004
|
-
"--store",
|
|
2005
|
-
directory,
|
|
2006
|
-
"unit",
|
|
2007
|
-
"add",
|
|
2008
|
-
"",
|
|
2009
|
-
"--track",
|
|
2010
|
-
"build"
|
|
2011
|
-
]);
|
|
2012
|
-
expect(userError.code).toBe(1);
|
|
2013
|
-
expect(userError.stderr).toContain("unit id must not be empty");
|
|
2014
|
-
const missingUnit = runCli([
|
|
2015
|
-
"--store",
|
|
2016
|
-
directory,
|
|
2017
|
-
"unit",
|
|
2018
|
-
"get",
|
|
2019
|
-
"missing"
|
|
2020
|
-
]);
|
|
2021
|
-
expect(missingUnit.code).toBe(2);
|
|
2022
|
-
expect(missingUnit.stderr).toContain("unit missing not found");
|
|
2023
|
-
const missingLedger = runCli([
|
|
2024
|
-
"--store",
|
|
2025
|
-
directory,
|
|
2026
|
-
"--json",
|
|
2027
|
-
"ledger",
|
|
2028
|
-
"check",
|
|
2029
|
-
"184530",
|
|
2030
|
-
"abc123"
|
|
2031
|
-
]);
|
|
2032
|
-
expect(missingLedger.code).toBe(2);
|
|
2033
|
-
expect(JSON.parse(missingLedger.stdout)).toEqual({
|
|
2034
|
-
pr: "184530",
|
|
2035
|
-
sha: "abc123",
|
|
2036
|
-
verdict: "NOT-VERIFIED"
|
|
2037
|
-
});
|
|
2038
|
-
expect(missingLedger.stderr).toBe("");
|
|
2039
|
-
});
|
|
2040
|
-
});
|
|
2041
|
-
//#endregion
|
|
2042
|
-
export {};
|