@on-par/factory-cli 0.0.0-stage → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -2
  3. package/dist/cli/admission.d.ts +17 -0
  4. package/dist/cli/admission.d.ts.map +1 -0
  5. package/dist/cli/admission.js +37 -0
  6. package/dist/cli/admission.js.map +1 -0
  7. package/dist/cli/base-hold.d.ts +63 -0
  8. package/dist/cli/base-hold.d.ts.map +1 -0
  9. package/dist/cli/base-hold.js +122 -0
  10. package/dist/cli/base-hold.js.map +1 -0
  11. package/dist/cli/branch-prefix.d.ts +8 -0
  12. package/dist/cli/branch-prefix.d.ts.map +1 -0
  13. package/dist/cli/branch-prefix.js +16 -0
  14. package/dist/cli/branch-prefix.js.map +1 -0
  15. package/dist/cli/classifier-backtest.d.ts +50 -0
  16. package/dist/cli/classifier-backtest.d.ts.map +1 -0
  17. package/dist/cli/classifier-backtest.js +112 -0
  18. package/dist/cli/classifier-backtest.js.map +1 -0
  19. package/dist/cli/cost-json.d.ts +37 -0
  20. package/dist/cli/cost-json.d.ts.map +1 -0
  21. package/dist/cli/cost-json.js +48 -0
  22. package/dist/cli/cost-json.js.map +1 -0
  23. package/dist/cli/daemon.d.ts +162 -0
  24. package/dist/cli/daemon.d.ts.map +1 -0
  25. package/dist/cli/daemon.js +393 -0
  26. package/dist/cli/daemon.js.map +1 -0
  27. package/dist/cli/doctor.d.ts +127 -0
  28. package/dist/cli/doctor.d.ts.map +1 -0
  29. package/dist/cli/doctor.js +400 -0
  30. package/dist/cli/doctor.js.map +1 -0
  31. package/dist/cli/feedback.d.ts +74 -0
  32. package/dist/cli/feedback.d.ts.map +1 -0
  33. package/dist/cli/feedback.js +180 -0
  34. package/dist/cli/feedback.js.map +1 -0
  35. package/dist/cli/filing.d.ts +22 -0
  36. package/dist/cli/filing.d.ts.map +1 -0
  37. package/dist/cli/filing.js +53 -0
  38. package/dist/cli/filing.js.map +1 -0
  39. package/dist/cli/first-run.d.ts +9 -0
  40. package/dist/cli/first-run.d.ts.map +1 -0
  41. package/dist/cli/first-run.js +34 -0
  42. package/dist/cli/first-run.js.map +1 -0
  43. package/dist/cli/help-groups.d.ts +17 -0
  44. package/dist/cli/help-groups.d.ts.map +1 -0
  45. package/dist/cli/help-groups.js +59 -0
  46. package/dist/cli/help-groups.js.map +1 -0
  47. package/dist/cli/hosted-queue.d.ts +45 -0
  48. package/dist/cli/hosted-queue.d.ts.map +1 -0
  49. package/dist/cli/hosted-queue.js +74 -0
  50. package/dist/cli/hosted-queue.js.map +1 -0
  51. package/dist/cli/hosted-runner.d.ts +44 -0
  52. package/dist/cli/hosted-runner.d.ts.map +1 -0
  53. package/dist/cli/hosted-runner.js +68 -0
  54. package/dist/cli/hosted-runner.js.map +1 -0
  55. package/dist/cli/hosted.d.ts +38 -0
  56. package/dist/cli/hosted.d.ts.map +1 -0
  57. package/dist/cli/hosted.js +82 -0
  58. package/dist/cli/hosted.js.map +1 -0
  59. package/dist/cli/index.d.ts +674 -0
  60. package/dist/cli/index.d.ts.map +1 -0
  61. package/dist/cli/index.js +4916 -0
  62. package/dist/cli/index.js.map +1 -0
  63. package/dist/cli/init-files.d.ts +8 -0
  64. package/dist/cli/init-files.d.ts.map +1 -0
  65. package/dist/cli/init-files.js +45 -0
  66. package/dist/cli/init-files.js.map +1 -0
  67. package/dist/cli/logs.d.ts +27 -0
  68. package/dist/cli/logs.d.ts.map +1 -0
  69. package/dist/cli/logs.js +52 -0
  70. package/dist/cli/logs.js.map +1 -0
  71. package/dist/cli/merge-scope.d.ts +7 -0
  72. package/dist/cli/merge-scope.d.ts.map +1 -0
  73. package/dist/cli/merge-scope.js +14 -0
  74. package/dist/cli/merge-scope.js.map +1 -0
  75. package/dist/cli/octokit.d.ts +43 -0
  76. package/dist/cli/octokit.d.ts.map +1 -0
  77. package/dist/cli/octokit.js +87 -0
  78. package/dist/cli/octokit.js.map +1 -0
  79. package/dist/cli/queue-add-json.d.ts +25 -0
  80. package/dist/cli/queue-add-json.d.ts.map +1 -0
  81. package/dist/cli/queue-add-json.js +24 -0
  82. package/dist/cli/queue-add-json.js.map +1 -0
  83. package/dist/cli/queue-clear.d.ts +15 -0
  84. package/dist/cli/queue-clear.d.ts.map +1 -0
  85. package/dist/cli/queue-clear.js +58 -0
  86. package/dist/cli/queue-clear.js.map +1 -0
  87. package/dist/cli/queue-list.d.ts +24 -0
  88. package/dist/cli/queue-list.d.ts.map +1 -0
  89. package/dist/cli/queue-list.js +25 -0
  90. package/dist/cli/queue-list.js.map +1 -0
  91. package/dist/cli/ready-check.d.ts +98 -0
  92. package/dist/cli/ready-check.d.ts.map +1 -0
  93. package/dist/cli/ready-check.js +169 -0
  94. package/dist/cli/ready-check.js.map +1 -0
  95. package/dist/cli/reset.d.ts +40 -0
  96. package/dist/cli/reset.d.ts.map +1 -0
  97. package/dist/cli/reset.js +215 -0
  98. package/dist/cli/reset.js.map +1 -0
  99. package/dist/cli/run-children.d.ts +24 -0
  100. package/dist/cli/run-children.d.ts.map +1 -0
  101. package/dist/cli/run-children.js +73 -0
  102. package/dist/cli/run-children.js.map +1 -0
  103. package/dist/cli/run-flags.d.ts +14 -0
  104. package/dist/cli/run-flags.d.ts.map +1 -0
  105. package/dist/cli/run-flags.js +43 -0
  106. package/dist/cli/run-flags.js.map +1 -0
  107. package/dist/cli/runs-diffstat.d.ts +43 -0
  108. package/dist/cli/runs-diffstat.d.ts.map +1 -0
  109. package/dist/cli/runs-diffstat.js +91 -0
  110. package/dist/cli/runs-diffstat.js.map +1 -0
  111. package/dist/cli/runs.d.ts +36 -0
  112. package/dist/cli/runs.d.ts.map +1 -0
  113. package/dist/cli/runs.js +102 -0
  114. package/dist/cli/runs.js.map +1 -0
  115. package/dist/cli/self-update.d.ts +68 -0
  116. package/dist/cli/self-update.d.ts.map +1 -0
  117. package/dist/cli/self-update.js +167 -0
  118. package/dist/cli/self-update.js.map +1 -0
  119. package/dist/cli/staleness.d.ts +30 -0
  120. package/dist/cli/staleness.d.ts.map +1 -0
  121. package/dist/cli/staleness.js +132 -0
  122. package/dist/cli/staleness.js.map +1 -0
  123. package/dist/cli/state-root.d.ts +5 -0
  124. package/dist/cli/state-root.d.ts.map +1 -0
  125. package/dist/cli/state-root.js +12 -0
  126. package/dist/cli/state-root.js.map +1 -0
  127. package/dist/cli/status-json.d.ts +49 -0
  128. package/dist/cli/status-json.d.ts.map +1 -0
  129. package/dist/cli/status-json.js +23 -0
  130. package/dist/cli/status-json.js.map +1 -0
  131. package/dist/cli/stop-resume-json.d.ts +18 -0
  132. package/dist/cli/stop-resume-json.d.ts.map +1 -0
  133. package/dist/cli/stop-resume-json.js +10 -0
  134. package/dist/cli/stop-resume-json.js.map +1 -0
  135. package/dist/cli/stop-sentinel.d.ts +23 -0
  136. package/dist/cli/stop-sentinel.d.ts.map +1 -0
  137. package/dist/cli/stop-sentinel.js +61 -0
  138. package/dist/cli/stop-sentinel.js.map +1 -0
  139. package/dist/cli/sweep-heartbeat.d.ts +33 -0
  140. package/dist/cli/sweep-heartbeat.d.ts.map +1 -0
  141. package/dist/cli/sweep-heartbeat.js +65 -0
  142. package/dist/cli/sweep-heartbeat.js.map +1 -0
  143. package/dist/cli/unpark.d.ts +28 -0
  144. package/dist/cli/unpark.d.ts.map +1 -0
  145. package/dist/cli/unpark.js +68 -0
  146. package/dist/cli/unpark.js.map +1 -0
  147. package/dist/cli/update-state.d.ts +44 -0
  148. package/dist/cli/update-state.d.ts.map +1 -0
  149. package/dist/cli/update-state.js +154 -0
  150. package/dist/cli/update-state.js.map +1 -0
  151. package/dist/cli/usage-json.d.ts +20 -0
  152. package/dist/cli/usage-json.d.ts.map +1 -0
  153. package/dist/cli/usage-json.js +36 -0
  154. package/dist/cli/usage-json.js.map +1 -0
  155. package/dist/cli.d.ts +3 -0
  156. package/dist/cli.d.ts.map +1 -0
  157. package/dist/cli.js +6 -0
  158. package/dist/cli.js.map +1 -0
  159. package/dist/entry.d.ts +6 -0
  160. package/dist/entry.d.ts.map +1 -0
  161. package/dist/entry.js +13 -0
  162. package/dist/entry.js.map +1 -0
  163. package/package.json +56 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 On PAR Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # @on-par/factory-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `factory` command for On PAR Dev's Software Factory.
4
+
5
+ On PAR Dev's Software Factory is a local CLI that takes a GitHub issue through plan, build and check with AI agents, then opens a ready-for-review PR. You decide what merges.
6
+
7
+ Built in TypeScript. Routes work across Claude and Codex, with experimental local models through Ollama, and logs cost per task.
8
+
9
+ ![On PAR Dev's Software Factory running a Sound Buddy lane (sped up)](https://raw.githubusercontent.com/on-par/software-factory/main/docs/assets/factory-run.gif)
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g @on-par/factory-cli
15
+ factory --version
16
+ factory doctor
17
+ ```
18
+
19
+ Requires Node.js >= 24, `git`, Claude Code (`claude`) installed and signed in, and the GitHub CLI signed in with `gh auth login`. The factory reads `GITHUB_TOKEN`, then `GH_TOKEN`, and falls back to `gh auth token`. `factory doctor` checks all of this and prints a fix for anything missing.
20
+
21
+ ## Safety
22
+
23
+ Agents run with their permission prompts off (Claude Code with `--permission-mode bypassPermissions`, Codex with `approval_policy=never`) in a separate git worktree, with your permissions and your GitHub token. Only point the factory at repos and issues you trust. It never merges by default: `factory run` and `factory supervise` merge only if you turn auto-merge on (`FACTORY_MERGE=1`, `run.merge.auto: true`, or `--auto-merge`), and `factory land` merges when you run it. Details are in the [Safety section](https://github.com/on-par/software-factory#safety) and [SECURITY.md](https://github.com/on-par/software-factory/blob/main/SECURITY.md).
24
+
25
+ ## Updating
26
+
27
+ ```bash
28
+ npm install -g @on-par/factory-cli@latest
29
+ ```
30
+
31
+ Automatic updates are planned: [#2354](https://github.com/on-par/software-factory/issues/2354), [#2355](https://github.com/on-par/software-factory/issues/2355).
32
+
33
+ ## Documentation
34
+
35
+ - [README and quick start](https://github.com/on-par/software-factory#readme)
36
+ - [Architecture and package graph](https://github.com/on-par/software-factory/blob/main/docs/architecture.md)
37
+ - [Changelog](https://github.com/on-par/software-factory/blob/main/CHANGELOG.md)
38
+
39
+ ## License
40
+
41
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,17 @@
1
+ import { type AdmissionStateReader, type QueueCompatibilityVerdict, type QueueSnapshot } from '@on-par/factory-core/internal';
2
+ export interface QueueAdmissionRow {
3
+ lane: string;
4
+ issue: number;
5
+ verdict: QueueCompatibilityVerdict;
6
+ }
7
+ export declare function admissionStateReaderFor(stateDir: string): AdmissionStateReader;
8
+ export declare function formatQueueAdmissionReport(rows: readonly QueueAdmissionRow[]): string;
9
+ export declare function runQueueReconcile(deps: {
10
+ readSnapshot: () => Promise<QueueSnapshot>;
11
+ reader: AdmissionStateReader;
12
+ lane?: string;
13
+ }): Promise<{
14
+ report: string;
15
+ conflicts: number;
16
+ }>;
17
+ //# sourceMappingURL=admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.d.ts","sourceRoot":"","sources":["../../src/cli/admission.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EACnB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,yBAAyB,CAAC;CACpC;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,CAE9E;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,SAAS,iBAAiB,EAAE,GAAG,MAAM,CAgBrF;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBjD"}
@@ -0,0 +1,37 @@
1
+ // packages/cli/src/cli/admission.ts — Factory App admission-state reader wiring + `factory queue reconcile` (#1497).
2
+ import { classifyQueueCompatibility, createFileAdmissionStateReader, formatAdmissionConflict, } from '@on-par/factory-core/internal';
3
+ export function admissionStateReaderFor(stateDir) {
4
+ return createFileAdmissionStateReader({ stateDir });
5
+ }
6
+ export function formatQueueAdmissionReport(rows) {
7
+ const conflicts = rows.filter((row) => row.verdict.kind === 'refuse');
8
+ const lines = [`queue reconcile — ${rows.length} queued issue(s), ${conflicts.length} conflict(s)`];
9
+ if (conflicts.length === 0) {
10
+ lines.push(' no Factory App admission conflicts — the GitHub label queue is authoritative here');
11
+ return lines.join('\n');
12
+ }
13
+ for (const row of conflicts) {
14
+ const verdict = row.verdict;
15
+ const [head, ...rest] = formatAdmissionConflict(row.issue, verdict).split('\n');
16
+ lines.push(` [${row.lane}] ${head}`, ...rest.map((line) => ` ${line}`));
17
+ }
18
+ return lines.join('\n');
19
+ }
20
+ export async function runQueueReconcile(deps) {
21
+ const snapshot = await deps.readSnapshot();
22
+ const entries = deps.lane === undefined ? snapshot.entries : snapshot.entries.filter((entry) => entry.lane === deps.lane);
23
+ const rows = [];
24
+ for (const entry of entries) {
25
+ let lookup;
26
+ try {
27
+ lookup = await deps.reader.read(entry.issue);
28
+ }
29
+ catch (err) {
30
+ lookup = { kind: 'unreadable', detail: err instanceof Error ? err.message : String(err) };
31
+ }
32
+ rows.push({ lane: entry.lane, issue: entry.issue, verdict: classifyQueueCompatibility(entry.issue, lookup) });
33
+ }
34
+ const conflicts = rows.filter((row) => row.verdict.kind === 'refuse').length;
35
+ return { report: formatQueueAdmissionReport(rows), conflicts };
36
+ }
37
+ //# sourceMappingURL=admission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admission.js","sourceRoot":"","sources":["../../src/cli/admission.ts"],"names":[],"mappings":"AAAA,qHAAqH;AAErH,OAAO,EACL,0BAA0B,EAC1B,8BAA8B,EAC9B,uBAAuB,GAIxB,MAAM,+BAA+B,CAAC;AAQvC,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,8BAA8B,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAkC;IAC3E,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,CAAC,qBAAqB,IAAI,CAAC,MAAM,qBAAqB,SAAS,CAAC,MAAM,cAAc,CAAC,CAAC;IAEpG,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,qFAAqF,CAAC,CAAC;QAClG,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,GAAG,CAAC,OAAiE,CAAC;QACtF,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,uBAAuB,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAChF,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAIvC;IACC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;IAC3C,MAAM,OAAO,GACX,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC;IAE5G,MAAM,IAAI,GAAwB,EAAE,CAAC;IACrC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,MAAM,CAAC;QACX,IAAI,CAAC;YACH,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,EAAE,IAAI,EAAE,YAAqB,EAAE,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACrG,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,OAAO,EAAE,0BAA0B,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAChH,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC;IAC7E,OAAO,EAAE,MAAM,EAAE,0BAA0B,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;AACjE,CAAC"}
@@ -0,0 +1,63 @@
1
+ /** A base SHA found broken by an environment pause (ADR-0141). */
2
+ export interface BaseHold {
3
+ /** `owner/name` of the repo the hold applies to. */
4
+ repo: string;
5
+ baseSha: string;
6
+ failingChecks: string[];
7
+ /** ISO-8601 time the hold was recorded. */
8
+ since: string;
9
+ }
10
+ export declare function writeBaseHold(file: string, hold: BaseHold): void;
11
+ /** Fails soft: a missing, unreadable, non-JSON, non-object or wrongly-typed file is no hold (null). */
12
+ export declare function readBaseHold(file: string): BaseHold | null;
13
+ export declare function deleteBaseHold(file: string): void;
14
+ /** Why a hold ended (#2350). */
15
+ export type BaseHoldClearReason = 'sha-changed' | 'ci-green';
16
+ export interface BaseHoldCleared {
17
+ hold: BaseHold;
18
+ reason: BaseHoldClearReason;
19
+ /** origin/main SHA seen on the clearing poll. */
20
+ currentSha: string;
21
+ }
22
+ /** Deletes the hold only if it is still on `baseSha`; true only when this call deleted it. Fully
23
+ * synchronous, so concurrent lanes in one process cannot both get true. */
24
+ export declare function clearBaseHold(file: string, baseSha: string): boolean;
25
+ export declare function formatBaseHoldCleared(c: BaseHoldCleared): string;
26
+ /** `factory status` lines for an active hold (#2351). */
27
+ export declare function formatBaseHoldStatus(hold: BaseHold): string[];
28
+ export type BaseHoldDecision = {
29
+ kind: 'claim';
30
+ } | {
31
+ kind: 'hold';
32
+ hold: BaseHold;
33
+ };
34
+ export interface BaseHoldGateDeps {
35
+ /** Current origin/main SHA; null (or a throw) means unknown. */
36
+ currentBaseSha: () => Promise<string | null>;
37
+ /** True only when main's CI on `sha` is green. */
38
+ baseCiGreen: (sha: string) => Promise<boolean>;
39
+ /** Called once, only by the poll that actually deleted the hold. A throw is swallowed. */
40
+ onCleared?: (cleared: BaseHoldCleared) => void;
41
+ }
42
+ /** Decides whether a lane may claim: hold only while origin/main is still at the held SHA and
43
+ * its CI is not green. Unknown origin/main fails open; a failed CI lookup counts as not green.
44
+ * Clears the hold when origin/main moved or its CI is green (#2350). */
45
+ export declare function evaluateBaseHold(file: string, repo: string, deps: BaseHoldGateDeps): Promise<BaseHoldDecision>;
46
+ /** Green = at least one check run, all completed, all on the ADR-0014 passing allow-list. */
47
+ export declare function isGreenCheckRunSet(runs: ReadonlyArray<{
48
+ status: string | null;
49
+ conclusion: string | null;
50
+ }>): boolean;
51
+ /** SHA from the first line of `git ls-remote` output, or null when absent or malformed. */
52
+ export declare function parseLsRemoteSha(stdout: string): string | null;
53
+ export declare function createBaseHoldGate(opts: {
54
+ file: string;
55
+ repo: string;
56
+ git: (cmd: string) => Promise<string>;
57
+ listCheckRuns: (ref: string) => Promise<Array<{
58
+ status: string | null;
59
+ conclusion: string | null;
60
+ }>>;
61
+ onCleared?: (cleared: BaseHoldCleared) => void;
62
+ }): () => Promise<BaseHoldDecision>;
63
+ //# sourceMappingURL=base-hold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-hold.d.ts","sourceRoot":"","sources":["../../src/cli/base-hold.ts"],"names":[],"mappings":"AAWA,kEAAkE;AAClE,MAAM,WAAW,QAAQ;IACvB,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAShE;AAED,uGAAuG;AACvG,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAa1D;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,gCAAgC;AAChC,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG,UAAU,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,mBAAmB,CAAC;IAC5B,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;4EAC4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAKpE;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,eAAe,GAAG,MAAM,CAMhE;AAED,yDAAyD;AACzD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,EAAE,CAG7D;AAED,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEpF,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,cAAc,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,kDAAkD;IAClD,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,0FAA0F;IAC1F,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD;AAED;;yEAEyE;AACzE,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA+BpH;AAED,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GAAG,OAAO,CAErH;AAED,2FAA2F;AAC3F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC,CAAC;IACrG,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD,GAAG,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAOlC"}
@@ -0,0 +1,122 @@
1
+ // packages/cli/src/cli/base-hold.ts — base hold persisted when a lane pauses on a broken
2
+ // base SHA (#2348), so a restarted supervisor remembers the pause. #2349 reads it back as the
3
+ // claim gate: no claiming while origin/main is still at the held SHA and its CI is not green.
4
+ // #2350 clears the hold once origin/main moves or its CI goes green. #2351 reads it for
5
+ // `factory status`.
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ import { isPassingConclusion } from '@on-par/factory-core/internal';
9
+ export function writeBaseHold(file, hold) {
10
+ mkdirSync(dirname(file), { recursive: true });
11
+ const record = {
12
+ repo: hold.repo,
13
+ baseSha: hold.baseSha,
14
+ failingChecks: [...hold.failingChecks],
15
+ since: hold.since,
16
+ };
17
+ writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`);
18
+ }
19
+ /** Fails soft: a missing, unreadable, non-JSON, non-object or wrongly-typed file is no hold (null). */
20
+ export function readBaseHold(file) {
21
+ if (!existsSync(file))
22
+ return null;
23
+ let raw;
24
+ try {
25
+ raw = JSON.parse(readFileSync(file, 'utf-8'));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
31
+ return null;
32
+ const { repo, baseSha, failingChecks, since } = raw;
33
+ if (typeof repo !== 'string' || typeof baseSha !== 'string' || typeof since !== 'string')
34
+ return null;
35
+ if (!Array.isArray(failingChecks) || !failingChecks.every((c) => typeof c === 'string'))
36
+ return null;
37
+ return { repo, baseSha, failingChecks: [...failingChecks], since };
38
+ }
39
+ export function deleteBaseHold(file) {
40
+ rmSync(file, { force: true });
41
+ }
42
+ /** Deletes the hold only if it is still on `baseSha`; true only when this call deleted it. Fully
43
+ * synchronous, so concurrent lanes in one process cannot both get true. */
44
+ export function clearBaseHold(file, baseSha) {
45
+ const current = readBaseHold(file);
46
+ if (current === null || current.baseSha !== baseSha)
47
+ return false;
48
+ deleteBaseHold(file);
49
+ return true;
50
+ }
51
+ export function formatBaseHoldCleared(c) {
52
+ const held = c.hold.baseSha.slice(0, 8);
53
+ const now = c.currentSha.slice(0, 8);
54
+ return c.reason === 'sha-changed'
55
+ ? `base hold on ${held} cleared (reason: sha-changed): origin/main moved to ${now}`
56
+ : `base hold on ${held} cleared (reason: ci-green): main CI on ${now} is green`;
57
+ }
58
+ /** `factory status` lines for an active hold (#2351). */
59
+ export function formatBaseHoldStatus(hold) {
60
+ const checks = hold.failingChecks.length > 0 ? hold.failingChecks.join(', ') : '(none recorded)';
61
+ return [` repo: ${hold.repo}`, ` baseSha: ${hold.baseSha}`, ` failingChecks: ${checks}`, ` since: ${hold.since}`];
62
+ }
63
+ /** Decides whether a lane may claim: hold only while origin/main is still at the held SHA and
64
+ * its CI is not green. Unknown origin/main fails open; a failed CI lookup counts as not green.
65
+ * Clears the hold when origin/main moved or its CI is green (#2350). */
66
+ export async function evaluateBaseHold(file, repo, deps) {
67
+ const hold = readBaseHold(file);
68
+ if (hold === null || hold.repo !== repo)
69
+ return { kind: 'claim' };
70
+ let sha;
71
+ try {
72
+ sha = await deps.currentBaseSha();
73
+ }
74
+ catch {
75
+ sha = null;
76
+ }
77
+ if (sha === null)
78
+ return { kind: 'claim' };
79
+ const clear = (reason, currentSha) => {
80
+ if (!clearBaseHold(file, hold.baseSha))
81
+ return;
82
+ try {
83
+ deps.onCleared?.({ hold, reason, currentSha });
84
+ }
85
+ catch {
86
+ // an observer failure must not change the decision
87
+ }
88
+ };
89
+ if (sha !== hold.baseSha) {
90
+ clear('sha-changed', sha);
91
+ return { kind: 'claim' };
92
+ }
93
+ let green = false;
94
+ try {
95
+ green = await deps.baseCiGreen(sha);
96
+ }
97
+ catch {
98
+ green = false;
99
+ }
100
+ if (!green)
101
+ return { kind: 'hold', hold };
102
+ clear('ci-green', sha);
103
+ return { kind: 'claim' };
104
+ }
105
+ /** Green = at least one check run, all completed, all on the ADR-0014 passing allow-list. */
106
+ export function isGreenCheckRunSet(runs) {
107
+ return runs.length > 0 && runs.every((r) => r.status === 'completed' && isPassingConclusion(r.conclusion));
108
+ }
109
+ /** SHA from the first line of `git ls-remote` output, or null when absent or malformed. */
110
+ export function parseLsRemoteSha(stdout) {
111
+ const line = stdout.split('\n').find((l) => l.trim() !== '');
112
+ const field = line?.trim().split(/\s+/)[0] ?? '';
113
+ return /^[0-9a-f]{40}$/i.test(field) ? field : null;
114
+ }
115
+ export function createBaseHoldGate(opts) {
116
+ return () => evaluateBaseHold(opts.file, opts.repo, {
117
+ currentBaseSha: async () => parseLsRemoteSha(await opts.git('git ls-remote origin refs/heads/main')),
118
+ baseCiGreen: async (sha) => isGreenCheckRunSet(await opts.listCheckRuns(sha)),
119
+ onCleared: opts.onCleared,
120
+ });
121
+ }
122
+ //# sourceMappingURL=base-hold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-hold.js","sourceRoot":"","sources":["../../src/cli/base-hold.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Command } from 'commander';
2
+ export declare const BRANCH_PREFIX_OPTION_DESCRIPTION = "Branch-name prefix for branches this command creates (default: factory)";
3
+ export declare const BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION = "Branch-name prefix of factory branches to act on; legacy ship-it/ branches are always included (default: factory)";
4
+ export declare const INVALID_BRANCH_PREFIX_MESSAGE = "factory: --branch-prefix must contain at least one letter or digit";
5
+ /** undefined -> resolveBranchPrefix(); otherwise the slug-normalized value; null when it has no [a-z0-9]. */
6
+ export declare function resolveBranchPrefixOption(raw: string | undefined): string | null;
7
+ export declare function addBranchPrefixOption(cmd: Command): Command;
8
+ //# sourceMappingURL=branch-prefix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-prefix.d.ts","sourceRoot":"","sources":["../../src/cli/branch-prefix.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,eAAO,MAAM,gCAAgC,4EAC8B,CAAC;AAC5E,eAAO,MAAM,sCAAsC,sHAAsI,CAAC;AAC1L,eAAO,MAAM,6BAA6B,uEAAuE,CAAC;AAElH,6GAA6G;AAC7G,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIhF;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE3D"}
@@ -0,0 +1,16 @@
1
+ import { LEGACY_BRANCH_PREFIX, branchPrefixSlug, resolveBranchPrefix, slugify } from '@on-par/factory-core/internal';
2
+ export const BRANCH_PREFIX_OPTION_DESCRIPTION = 'Branch-name prefix for branches this command creates (default: factory)';
3
+ export const BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION = `Branch-name prefix of factory branches to act on; legacy ${LEGACY_BRANCH_PREFIX}/ branches are always included (default: factory)`;
4
+ export const INVALID_BRANCH_PREFIX_MESSAGE = 'factory: --branch-prefix must contain at least one letter or digit';
5
+ /** undefined -> resolveBranchPrefix(); otherwise the slug-normalized value; null when it has no [a-z0-9]. */
6
+ export function resolveBranchPrefixOption(raw) {
7
+ if (raw === undefined)
8
+ return resolveBranchPrefix();
9
+ if (slugify(raw) === '')
10
+ return null;
11
+ return branchPrefixSlug(raw);
12
+ }
13
+ export function addBranchPrefixOption(cmd) {
14
+ return cmd.option('--branch-prefix <prefix>', BRANCH_PREFIX_OPTION_DESCRIPTION);
15
+ }
16
+ //# sourceMappingURL=branch-prefix.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"branch-prefix.js","sourceRoot":"","sources":["../../src/cli/branch-prefix.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAErH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,yEAAyE,CAAC;AAC5E,MAAM,CAAC,MAAM,sCAAsC,GAAG,4DAA4D,oBAAoB,mDAAmD,CAAC;AAC1L,MAAM,CAAC,MAAM,6BAA6B,GAAG,oEAAoE,CAAC;AAElH,6GAA6G;AAC7G,MAAM,UAAU,yBAAyB,CAAC,GAAuB;IAC/D,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,mBAAmB,EAAE,CAAC;IACpD,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACrC,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,GAAY;IAChD,OAAO,GAAG,CAAC,MAAM,CAAC,0BAA0B,EAAE,gCAAgC,CAAC,CAAC;AAClF,CAAC"}
@@ -0,0 +1,50 @@
1
+ import { type DefectSourceClient, type FactoryEvent, type getFactoryPaths, type HumanSourceClient, type ModelRouter } from '@on-par/factory-core';
2
+ import { type ReviewFloorRuleSet } from '@on-par/factory-core/internal';
3
+ export interface ClassifierBacktestOptions {
4
+ since: string;
5
+ limit?: string;
6
+ labels?: string;
7
+ maxCost?: string;
8
+ json?: boolean;
9
+ branchPrefix?: string;
10
+ }
11
+ export interface ClassifierBacktestDeps {
12
+ repoRoot: string;
13
+ paths: ReturnType<typeof getFactoryPaths>;
14
+ owner: string;
15
+ repo: string;
16
+ octokit: HumanSourceClient & DefectSourceClient & {
17
+ rest: {
18
+ issues: {
19
+ get(params: {
20
+ owner: string;
21
+ repo: string;
22
+ issue_number: number;
23
+ }): Promise<{
24
+ data: any;
25
+ }>;
26
+ };
27
+ };
28
+ };
29
+ router: ModelRouter;
30
+ rules: ReviewFloorRuleSet;
31
+ modelPin?: string;
32
+ windowDays: number;
33
+ /** git, run in repoRoot. */
34
+ exec(cmd: string): Promise<{
35
+ stdout: string;
36
+ }>;
37
+ readFile(path: string): string;
38
+ appendFile(path: string, text: string): void;
39
+ ensureDir(path: string): void;
40
+ /** Reads the local events log; [] when it does not exist. */
41
+ readEvents(path: string): FactoryEvent[];
42
+ now(): string;
43
+ log(line: string): void;
44
+ }
45
+ /** Bad command-line input; the caller maps it to exit code 2. */
46
+ export declare class ClassifierBacktestInputError extends Error {
47
+ constructor(message: string);
48
+ }
49
+ export declare function cmdClassifierBacktest(opts: ClassifierBacktestOptions, deps: ClassifierBacktestDeps): Promise<void>;
50
+ //# sourceMappingURL=classifier-backtest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier-backtest.d.ts","sourceRoot":"","sources":["../../src/cli/classifier-backtest.ts"],"names":[],"mappings":"AAIA,OAAO,EAQL,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAOL,KAAK,kBAAkB,EACxB,MAAM,+BAA+B,CAAC;AAEvC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,iBAAiB,GACxB,kBAAkB,GAAG;QACnB,IAAI,EAAE;YAAE,MAAM,EAAE;gBAAE,GAAG,CAAC,MAAM,EAAE;oBAAE,KAAK,EAAE,MAAM,CAAC;oBAAC,IAAI,EAAE,MAAM,CAAC;oBAAC,YAAY,EAAE,MAAM,CAAA;iBAAE,GAAG,OAAO,CAAC;oBAAE,IAAI,EAAE,GAAG,CAAA;iBAAE,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC;KAClH,CAAC;IACJ,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,kBAAkB,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,6DAA6D;IAC7D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IACzC,GAAG,IAAI,MAAM,CAAC;IACd,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,iEAAiE;AACjE,qBAAa,4BAA6B,SAAQ,KAAK;IACrD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAkCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,yBAAyB,EAC/B,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
@@ -0,0 +1,112 @@
1
+ // packages/cli/src/cli/classifier-backtest.ts — `factory classifier backtest`: replay the floor + shadow classifier over merged PRs (#1728)
2
+ import { join } from 'node:path';
3
+ import { styleText } from 'node:util';
4
+ import { detectPostMergeDefects, fetchDefectSources, fetchHumanEventSources, formatClassifierReport, mergedPrRefs, reconstructHumanEvents, summarizeClassifierOutcomes, } from '@on-par/factory-core';
5
+ import { backtestFileName, classifiedRecords, parseBacktestLabels, runClassifierBacktest, selectBacktestPrs, } from '@on-par/factory-core/internal';
6
+ /** Bad command-line input; the caller maps it to exit code 2. */
7
+ export class ClassifierBacktestInputError extends Error {
8
+ constructor(message) {
9
+ super(`factory: ${message}`);
10
+ this.name = 'ClassifierBacktestInputError';
11
+ }
12
+ }
13
+ const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
14
+ function validateOptions(opts) {
15
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.since) || Number.isNaN(Date.parse(opts.since))) {
16
+ throw new ClassifierBacktestInputError(`invalid --since '${opts.since}' — expected YYYY-MM-DD`);
17
+ }
18
+ let limit;
19
+ if (opts.limit !== undefined) {
20
+ limit = Number(opts.limit);
21
+ if (!/^\d+$/.test(opts.limit) || limit < 1) {
22
+ throw new ClassifierBacktestInputError(`invalid --limit '${opts.limit}' — expected a positive integer`);
23
+ }
24
+ }
25
+ let maxCost;
26
+ if (opts.maxCost !== undefined) {
27
+ maxCost = Number(opts.maxCost);
28
+ if (opts.maxCost.trim() === '' || !Number.isFinite(maxCost) || maxCost <= 0) {
29
+ throw new ClassifierBacktestInputError(`invalid --max-cost '${opts.maxCost}' — expected a number of USD > 0`);
30
+ }
31
+ }
32
+ return { limit, maxCost };
33
+ }
34
+ function loadLabels(opts, deps) {
35
+ if (opts.labels === undefined)
36
+ return new Map();
37
+ try {
38
+ return parseBacktestLabels(deps.readFile(opts.labels));
39
+ }
40
+ catch (err) {
41
+ throw new ClassifierBacktestInputError(`--labels ${opts.labels}: ${err?.message ?? err}`);
42
+ }
43
+ }
44
+ export async function cmdClassifierBacktest(opts, deps) {
45
+ const { limit, maxCost } = validateOptions(opts);
46
+ const labels = loadLabels(opts, deps);
47
+ const now = deps.now();
48
+ const { owner, repo, paths } = deps;
49
+ const sources = await fetchHumanEventSources(deps.octokit, owner, repo, null, opts.branchPrefix);
50
+ const selected = selectBacktestPrs(sources, { since: opts.since, limit });
51
+ if (selected.length === 0) {
52
+ deps.log(`No merged factory PRs since ${opts.since}.`);
53
+ return;
54
+ }
55
+ const local = deps.readEvents(paths.events);
56
+ let events = [...local, ...reconstructHumanEvents(selected, local)];
57
+ try {
58
+ const defectSources = await fetchDefectSources(deps.octokit, owner, repo, mergedPrRefs(selected), {
59
+ now,
60
+ windowDays: deps.windowDays,
61
+ });
62
+ events = [...events, ...detectPostMergeDefects(defectSources, events, { now, windowDays: deps.windowDays })];
63
+ }
64
+ catch (err) {
65
+ console.error(styleText('yellow', `factory: post-merge defect signals unavailable (${err?.message ?? err}) — heuristic defect labels omitted (hand labels still apply)`));
66
+ }
67
+ const file = join(paths.state, backtestFileName(now));
68
+ deps.ensureDir(paths.state);
69
+ const checkSha = (sha) => {
70
+ if (!SHA_PATTERN.test(sha))
71
+ throw new Error(`refusing unsafe merge commit sha '${sha}'`);
72
+ return sha;
73
+ };
74
+ const result = await runClassifierBacktest({ sources: selected, events, labels, rules: deps.rules, maxCostUsd: maxCost, now, windowDays: deps.windowDays }, {
75
+ numstat: async (sha) => (await deps.exec(`git diff --numstat --no-renames ${checkSha(sha)}^1 ${sha}`)).stdout,
76
+ diff: async (sha) => (await deps.exec(`git diff --no-renames ${checkSha(sha)}^1 ${sha}`)).stdout,
77
+ getIssue: async (n) => {
78
+ const { data } = await deps.octokit.rest.issues.get({ owner, repo, issue_number: n });
79
+ return { title: data.title ?? '', body: data.body ?? '' };
80
+ },
81
+ specPath: (issue) => join(paths.plans, `issue-${issue}.md`),
82
+ worktree: deps.repoRoot,
83
+ router: deps.router,
84
+ modelPin: deps.modelPin,
85
+ onRecord: (r) => deps.appendFile(file, `${JSON.stringify(r)}\n`),
86
+ });
87
+ const spent = `$${result.spentUsd.toFixed(2)} spent`;
88
+ const covered = result.records.length;
89
+ if (result.budgetReached) {
90
+ deps.log(`budget reached: covered ${covered} of ${result.candidates} PRs (${spent})`);
91
+ }
92
+ else {
93
+ const unpriced = result.unpricedCalls > 0 ? `, ${result.unpricedCalls} unpriced` : '';
94
+ deps.log(`covered ${covered} of ${result.candidates} PRs (${spent}${unpriced})`);
95
+ }
96
+ const report = summarizeClassifierOutcomes(classifiedRecords(result));
97
+ if (opts.json) {
98
+ deps.log(JSON.stringify({
99
+ file,
100
+ covered,
101
+ candidates: result.candidates,
102
+ spentUsd: result.spentUsd,
103
+ budgetReached: result.budgetReached,
104
+ report,
105
+ }));
106
+ return;
107
+ }
108
+ for (const line of formatClassifierReport(report))
109
+ deps.log(line);
110
+ deps.log(`Wrote ${file}`);
111
+ }
112
+ //# sourceMappingURL=classifier-backtest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classifier-backtest.js","sourceRoot":"","sources":["../../src/cli/classifier-backtest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { type CostEntry } from '@on-par/factory-core';
2
+ /** One cost row in `factory cost --json` (#2269). CostEntry field names. */
3
+ export interface CostRowJson {
4
+ ts: string;
5
+ issue: string;
6
+ task: string;
7
+ model: string;
8
+ inputTokens: number;
9
+ outputTokens: number;
10
+ cacheReadTokens: number | null;
11
+ cacheCreationTokens: number | null;
12
+ /** null when unpriced (ADR-0020); never 0 for an unpriced row. */
13
+ cost: number | null;
14
+ }
15
+ /** Per-model rollup over the (filtered) rows, first-seen model order. */
16
+ export interface CostModelRowJson {
17
+ model: string;
18
+ tasks: number;
19
+ inputTokens: number;
20
+ outputTokens: number;
21
+ /** Sum of priced rows; null when every row for the model is unpriced. */
22
+ cost: number | null;
23
+ unpricedCount: number;
24
+ }
25
+ /** `factory cost --json` payload (#2269). Additive changes only; bump schemaVersion on a breaking change. */
26
+ export interface CostJson {
27
+ schemaVersion: 1;
28
+ rows: CostRowJson[];
29
+ perModel: CostModelRowJson[];
30
+ total: {
31
+ cost: number | null;
32
+ unpricedCount: number;
33
+ };
34
+ }
35
+ /** Build the `factory cost --json` payload; `issue` keeps only that issue's rows. */
36
+ export declare function buildCostJson(entries: CostEntry[], issue?: string): CostJson;
37
+ //# sourceMappingURL=cost-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost-json.d.ts","sourceRoot":"","sources":["../../src/cli/cost-json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtE,4EAA4E;AAC5E,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,kEAAkE;IAClE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,6GAA6G;AAC7G,MAAM,WAAW,QAAQ;IACvB,aAAa,EAAE,CAAC,CAAC;IACjB,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACvD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,QAAQ,CA8C5E"}
@@ -0,0 +1,48 @@
1
+ import { aggregateCosts } from '@on-par/factory-core';
2
+ /** Build the `factory cost --json` payload; `issue` keeps only that issue's rows. */
3
+ export function buildCostJson(entries, issue) {
4
+ const filtered = issue === undefined ? entries : entries.filter((e) => e.issue === String(issue));
5
+ const rows = [];
6
+ const models = new Map();
7
+ for (const e of filtered) {
8
+ const cost = e.unpriced === true || e.cost == null ? null : e.cost;
9
+ const inputTokens = e.inputTokens ?? 0;
10
+ const outputTokens = e.outputTokens ?? 0;
11
+ rows.push({
12
+ ts: e.ts,
13
+ issue: e.issue,
14
+ task: e.task,
15
+ model: e.model,
16
+ inputTokens,
17
+ outputTokens,
18
+ cacheReadTokens: e.cacheReadTokens ?? null,
19
+ cacheCreationTokens: e.cacheCreationTokens ?? null,
20
+ cost,
21
+ });
22
+ let m = models.get(e.model);
23
+ if (m === undefined) {
24
+ m = { tasks: 0, inputTokens: 0, outputTokens: 0, sum: 0, priced: 0, unpriced: 0 };
25
+ models.set(e.model, m);
26
+ }
27
+ m.tasks += 1;
28
+ m.inputTokens += inputTokens;
29
+ m.outputTokens += outputTokens;
30
+ if (cost === null)
31
+ m.unpriced += 1;
32
+ else {
33
+ m.sum += cost;
34
+ m.priced += 1;
35
+ }
36
+ }
37
+ const perModel = [...models].map(([model, m]) => ({
38
+ model,
39
+ tasks: m.tasks,
40
+ inputTokens: m.inputTokens,
41
+ outputTokens: m.outputTokens,
42
+ cost: m.priced === 0 && m.unpriced > 0 ? null : m.sum,
43
+ unpricedCount: m.unpriced,
44
+ }));
45
+ const t = aggregateCosts(filtered).total;
46
+ return { schemaVersion: 1, rows, perModel, total: { cost: t.cost, unpricedCount: t.unpricedCount } };
47
+ }
48
+ //# sourceMappingURL=cost-json.js.map