@matteeh/osq 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +415 -72
- package/dist/cli/approve.d.ts +13 -3
- package/dist/cli/approve.d.ts.map +1 -1
- package/dist/cli/approve.js +59 -4
- package/dist/cli/approve.js.map +1 -1
- package/dist/cli/bin.js +0 -0
- package/dist/cli/confirm.d.ts +21 -0
- package/dist/cli/confirm.d.ts.map +1 -0
- package/dist/cli/confirm.js +28 -0
- package/dist/cli/confirm.js.map +1 -0
- package/dist/cli/doctor.d.ts +13 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +23 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/done.d.ts +12 -0
- package/dist/cli/done.d.ts.map +1 -0
- package/dist/cli/done.js +26 -0
- package/dist/cli/done.js.map +1 -0
- package/dist/cli/inbox.d.ts +22 -0
- package/dist/cli/inbox.d.ts.map +1 -0
- package/dist/cli/inbox.js +41 -0
- package/dist/cli/inbox.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +127 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +9 -3
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/lint.d.ts +26 -0
- package/dist/cli/lint.d.ts.map +1 -0
- package/dist/cli/lint.js +60 -0
- package/dist/cli/lint.js.map +1 -0
- package/dist/cli/migrate.d.ts +18 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +42 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/new.js +1 -1
- package/dist/cli/new.js.map +1 -1
- package/dist/cli/plan-queue.d.ts +50 -0
- package/dist/cli/plan-queue.d.ts.map +1 -0
- package/dist/cli/plan-queue.js +167 -0
- package/dist/cli/plan-queue.js.map +1 -0
- package/dist/cli/plan.d.ts +35 -0
- package/dist/cli/plan.d.ts.map +1 -0
- package/dist/cli/plan.js +153 -0
- package/dist/cli/plan.js.map +1 -0
- package/dist/cli/queue.d.ts +9 -0
- package/dist/cli/queue.d.ts.map +1 -0
- package/dist/cli/queue.js +24 -0
- package/dist/cli/queue.js.map +1 -0
- package/dist/cli/reject.d.ts +8 -0
- package/dist/cli/reject.d.ts.map +1 -0
- package/dist/cli/reject.js +18 -0
- package/dist/cli/reject.js.map +1 -0
- package/dist/cli/report.d.ts +1 -1
- package/dist/cli/report.d.ts.map +1 -1
- package/dist/cli/report.js +141 -14
- package/dist/cli/report.js.map +1 -1
- package/dist/cli/retry.d.ts +7 -0
- package/dist/cli/retry.d.ts.map +1 -0
- package/dist/cli/retry.js +29 -0
- package/dist/cli/retry.js.map +1 -0
- package/dist/cli/serve.d.ts +30 -0
- package/dist/cli/serve.d.ts.map +1 -0
- package/dist/cli/serve.js +136 -0
- package/dist/cli/serve.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +7 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/show.d.ts +2 -1
- package/dist/cli/show.d.ts.map +1 -1
- package/dist/cli/show.js +20 -3
- package/dist/cli/show.js.map +1 -1
- package/dist/cli/status.d.ts +1 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +2 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/watch.d.ts +9 -6
- package/dist/cli/watch.d.ts.map +1 -1
- package/dist/cli/watch.js +15 -2
- package/dist/cli/watch.js.map +1 -1
- package/dist/core/foundation/config-codex.d.ts +22 -0
- package/dist/core/foundation/config-codex.d.ts.map +1 -0
- package/dist/core/foundation/config-codex.js +58 -0
- package/dist/core/foundation/config-codex.js.map +1 -0
- package/dist/core/foundation/config-doctor.d.ts +14 -0
- package/dist/core/foundation/config-doctor.d.ts.map +1 -0
- package/dist/core/foundation/config-doctor.js +29 -0
- package/dist/core/foundation/config-doctor.js.map +1 -0
- package/dist/core/foundation/config-env.d.ts +3 -0
- package/dist/core/foundation/config-env.d.ts.map +1 -0
- package/dist/core/foundation/config-env.js +22 -0
- package/dist/core/foundation/config-env.js.map +1 -0
- package/dist/core/foundation/config-gates.d.ts +22 -0
- package/dist/core/foundation/config-gates.d.ts.map +1 -0
- package/dist/core/foundation/config-gates.js +43 -0
- package/dist/core/foundation/config-gates.js.map +1 -0
- package/dist/core/foundation/config-pi.d.ts +64 -0
- package/dist/core/foundation/config-pi.d.ts.map +1 -0
- package/dist/core/foundation/config-pi.js +131 -0
- package/dist/core/foundation/config-pi.js.map +1 -0
- package/dist/core/foundation/config-planning.d.ts +24 -0
- package/dist/core/foundation/config-planning.d.ts.map +1 -0
- package/dist/core/foundation/config-planning.js +60 -0
- package/dist/core/foundation/config-planning.js.map +1 -0
- package/dist/core/foundation/config-queue.d.ts +15 -0
- package/dist/core/foundation/config-queue.d.ts.map +1 -0
- package/dist/core/foundation/config-queue.js +24 -0
- package/dist/core/foundation/config-queue.js.map +1 -0
- package/dist/core/foundation/config-serve.d.ts +20 -0
- package/dist/core/foundation/config-serve.d.ts.map +1 -0
- package/dist/core/foundation/config-serve.js +35 -0
- package/dist/core/foundation/config-serve.js.map +1 -0
- package/dist/core/{config.d.ts → foundation/config.d.ts} +34 -3
- package/dist/core/foundation/config.d.ts.map +1 -0
- package/dist/core/{config.js → foundation/config.js} +35 -18
- package/dist/core/foundation/config.js.map +1 -0
- package/dist/core/foundation/doctor-managed.d.ts +13 -0
- package/dist/core/foundation/doctor-managed.d.ts.map +1 -0
- package/dist/core/foundation/doctor-managed.js +77 -0
- package/dist/core/foundation/doctor-managed.js.map +1 -0
- package/dist/core/foundation/doctor.d.ts +19 -0
- package/dist/core/foundation/doctor.d.ts.map +1 -0
- package/dist/core/foundation/doctor.js +187 -0
- package/dist/core/foundation/doctor.js.map +1 -0
- package/dist/core/foundation/harness-catalog.d.ts +96 -0
- package/dist/core/foundation/harness-catalog.d.ts.map +1 -0
- package/dist/core/foundation/harness-catalog.js +182 -0
- package/dist/core/foundation/harness-catalog.js.map +1 -0
- package/dist/core/foundation/init-blocks.d.ts +33 -0
- package/dist/core/foundation/init-blocks.d.ts.map +1 -0
- package/dist/core/foundation/init-blocks.js +165 -0
- package/dist/core/foundation/init-blocks.js.map +1 -0
- package/dist/core/foundation/init-managed.d.ts +10 -0
- package/dist/core/foundation/init-managed.d.ts.map +1 -0
- package/dist/core/foundation/init-managed.js +115 -0
- package/dist/core/foundation/init-managed.js.map +1 -0
- package/dist/core/foundation/init.d.ts +18 -0
- package/dist/core/foundation/init.d.ts.map +1 -0
- package/dist/core/foundation/init.js +113 -0
- package/dist/core/foundation/init.js.map +1 -0
- package/dist/core/{logger.d.ts → foundation/logger.d.ts} +1 -0
- package/dist/core/foundation/logger.d.ts.map +1 -0
- package/dist/core/{logger.js → foundation/logger.js} +18 -2
- package/dist/core/foundation/logger.js.map +1 -0
- package/dist/core/{new.d.ts → foundation/new.d.ts} +3 -0
- package/dist/core/foundation/new.d.ts.map +1 -0
- package/dist/core/foundation/new.js +206 -0
- package/dist/core/foundation/new.js.map +1 -0
- package/dist/core/foundation/package-root.d.ts +9 -0
- package/dist/core/foundation/package-root.d.ts.map +1 -0
- package/dist/core/foundation/package-root.js +11 -0
- package/dist/core/foundation/package-root.js.map +1 -0
- package/dist/core/lifecycle/done.d.ts +17 -0
- package/dist/core/lifecycle/done.d.ts.map +1 -0
- package/dist/core/lifecycle/done.js +97 -0
- package/dist/core/lifecycle/done.js.map +1 -0
- package/dist/core/lifecycle/reject.d.ts +26 -0
- package/dist/core/lifecycle/reject.d.ts.map +1 -0
- package/dist/core/lifecycle/reject.js +83 -0
- package/dist/core/lifecycle/reject.js.map +1 -0
- package/dist/core/lifecycle/retry-transition.d.ts +23 -0
- package/dist/core/lifecycle/retry-transition.d.ts.map +1 -0
- package/dist/core/lifecycle/retry-transition.js +41 -0
- package/dist/core/lifecycle/retry-transition.js.map +1 -0
- package/dist/core/lifecycle/retry.d.ts +18 -0
- package/dist/core/lifecycle/retry.d.ts.map +1 -0
- package/dist/core/lifecycle/retry.js +179 -0
- package/dist/core/lifecycle/retry.js.map +1 -0
- package/dist/core/report/approval-flags.d.ts +29 -0
- package/dist/core/report/approval-flags.d.ts.map +1 -0
- package/dist/core/report/approval-flags.js +138 -0
- package/dist/core/report/approval-flags.js.map +1 -0
- package/dist/core/report/planning-economics.d.ts +39 -0
- package/dist/core/report/planning-economics.d.ts.map +1 -0
- package/dist/core/report/planning-economics.js +168 -0
- package/dist/core/report/planning-economics.js.map +1 -0
- package/dist/core/report/planning-observed.d.ts +59 -0
- package/dist/core/report/planning-observed.d.ts.map +1 -0
- package/dist/core/report/planning-observed.js +135 -0
- package/dist/core/report/planning-observed.js.map +1 -0
- package/dist/core/report/planning-records.d.ts +69 -0
- package/dist/core/report/planning-records.d.ts.map +1 -0
- package/dist/core/report/planning-records.js +149 -0
- package/dist/core/report/planning-records.js.map +1 -0
- package/dist/core/report/planning-slice-lookup.d.ts +13 -0
- package/dist/core/report/planning-slice-lookup.d.ts.map +1 -0
- package/dist/core/report/planning-slice-lookup.js +146 -0
- package/dist/core/report/planning-slice-lookup.js.map +1 -0
- package/dist/core/report/planning-slice-measures.d.ts +17 -0
- package/dist/core/report/planning-slice-measures.d.ts.map +1 -0
- package/dist/core/report/planning-slice-measures.js +85 -0
- package/dist/core/report/planning-slice-measures.js.map +1 -0
- package/dist/core/report/planning-slice-record.d.ts +4 -0
- package/dist/core/report/planning-slice-record.d.ts.map +1 -0
- package/dist/core/report/planning-slice-record.js +66 -0
- package/dist/core/report/planning-slice-record.js.map +1 -0
- package/dist/core/report/planning-slice-turns.d.ts +13 -0
- package/dist/core/report/planning-slice-turns.d.ts.map +1 -0
- package/dist/core/report/planning-slice-turns.js +41 -0
- package/dist/core/report/planning-slice-turns.js.map +1 -0
- package/dist/core/report/planning-slice.d.ts +70 -0
- package/dist/core/report/planning-slice.d.ts.map +1 -0
- package/dist/core/report/planning-slice.js +147 -0
- package/dist/core/report/planning-slice.js.map +1 -0
- package/dist/core/report/planning.d.ts +31 -0
- package/dist/core/report/planning.d.ts.map +1 -0
- package/dist/core/report/planning.js +75 -0
- package/dist/core/report/planning.js.map +1 -0
- package/dist/core/report/report-events.d.ts +52 -0
- package/dist/core/report/report-events.d.ts.map +1 -0
- package/dist/core/report/report-events.js +198 -0
- package/dist/core/report/report-events.js.map +1 -0
- package/dist/core/report/report-pre-spawn.d.ts +25 -0
- package/dist/core/report/report-pre-spawn.d.ts.map +1 -0
- package/dist/core/report/report-pre-spawn.js +40 -0
- package/dist/core/report/report-pre-spawn.js.map +1 -0
- package/dist/core/report/report-retries.d.ts +30 -0
- package/dist/core/report/report-retries.d.ts.map +1 -0
- package/dist/core/report/report-retries.js +100 -0
- package/dist/core/report/report-retries.js.map +1 -0
- package/dist/core/report/report.d.ts +276 -0
- package/dist/core/report/report.d.ts.map +1 -0
- package/dist/core/report/report.js +1421 -0
- package/dist/core/report/report.js.map +1 -0
- package/dist/core/{lock.d.ts → run/lock.d.ts} +6 -0
- package/dist/core/run/lock.d.ts.map +1 -0
- package/dist/core/{lock.js → run/lock.js} +7 -15
- package/dist/core/run/lock.js.map +1 -0
- package/dist/core/run/manifest.d.ts +39 -0
- package/dist/core/run/manifest.d.ts.map +1 -0
- package/dist/core/run/manifest.js +137 -0
- package/dist/core/run/manifest.js.map +1 -0
- package/dist/core/run/scope-hash.d.ts +71 -0
- package/dist/core/run/scope-hash.d.ts.map +1 -0
- package/dist/core/run/scope-hash.js +177 -0
- package/dist/core/run/scope-hash.js.map +1 -0
- package/dist/core/run/scope.d.ts +30 -0
- package/dist/core/run/scope.d.ts.map +1 -0
- package/dist/core/run/scope.js +193 -0
- package/dist/core/run/scope.js.map +1 -0
- package/dist/core/run/summary.d.ts +8 -0
- package/dist/core/run/summary.d.ts.map +1 -0
- package/dist/core/run/summary.js +25 -0
- package/dist/core/run/summary.js.map +1 -0
- package/dist/core/run/verification.d.ts +23 -0
- package/dist/core/run/verification.d.ts.map +1 -0
- package/dist/core/run/verification.js +78 -0
- package/dist/core/run/verification.js.map +1 -0
- package/dist/core/spec/approve.d.ts +31 -0
- package/dist/core/spec/approve.d.ts.map +1 -0
- package/dist/core/spec/approve.js +111 -0
- package/dist/core/spec/approve.js.map +1 -0
- package/dist/core/spec/delta.d.ts +77 -0
- package/dist/core/spec/delta.d.ts.map +1 -0
- package/dist/core/spec/delta.js +581 -0
- package/dist/core/spec/delta.js.map +1 -0
- package/dist/core/spec/digest-flags.d.ts +21 -0
- package/dist/core/spec/digest-flags.d.ts.map +1 -0
- package/dist/core/spec/digest-flags.js +194 -0
- package/dist/core/spec/digest-flags.js.map +1 -0
- package/dist/core/spec/digest-verify-starts.d.ts +5 -0
- package/dist/core/spec/digest-verify-starts.d.ts.map +1 -0
- package/dist/core/spec/digest-verify-starts.js +31 -0
- package/dist/core/spec/digest-verify-starts.js.map +1 -0
- package/dist/core/spec/digest.d.ts +42 -0
- package/dist/core/spec/digest.d.ts.map +1 -0
- package/dist/core/spec/digest.js +170 -0
- package/dist/core/spec/digest.js.map +1 -0
- package/dist/core/spec/hasher.d.ts +11 -0
- package/dist/core/spec/hasher.d.ts.map +1 -0
- package/dist/core/{hasher.js → spec/hasher.js} +27 -3
- package/dist/core/spec/hasher.js.map +1 -0
- package/dist/core/spec/linter.d.ts +79 -0
- package/dist/core/spec/linter.d.ts.map +1 -0
- package/dist/core/spec/linter.js +826 -0
- package/dist/core/spec/linter.js.map +1 -0
- package/dist/core/spec/migrate.d.ts +54 -0
- package/dist/core/spec/migrate.d.ts.map +1 -0
- package/dist/core/spec/migrate.js +297 -0
- package/dist/core/spec/migrate.js.map +1 -0
- package/dist/core/spec/openspec-version.d.ts +30 -0
- package/dist/core/spec/openspec-version.d.ts.map +1 -0
- package/dist/core/spec/openspec-version.js +118 -0
- package/dist/core/spec/openspec-version.js.map +1 -0
- package/dist/core/spec/parser.d.ts +82 -0
- package/dist/core/spec/parser.d.ts.map +1 -0
- package/dist/core/spec/parser.js +221 -0
- package/dist/core/spec/parser.js.map +1 -0
- package/dist/core/spec/verify-paths.d.ts +18 -0
- package/dist/core/spec/verify-paths.d.ts.map +1 -0
- package/dist/core/spec/verify-paths.js +139 -0
- package/dist/core/spec/verify-paths.js.map +1 -0
- package/dist/core/spec/verify-starts.d.ts +45 -0
- package/dist/core/spec/verify-starts.d.ts.map +1 -0
- package/dist/core/spec/verify-starts.js +61 -0
- package/dist/core/spec/verify-starts.js.map +1 -0
- package/dist/core/status/inbox-cursor.d.ts +10 -0
- package/dist/core/status/inbox-cursor.d.ts.map +1 -0
- package/dist/core/status/inbox-cursor.js +33 -0
- package/dist/core/status/inbox-cursor.js.map +1 -0
- package/dist/core/status/inbox-projection.d.ts +15 -0
- package/dist/core/status/inbox-projection.d.ts.map +1 -0
- package/dist/core/status/inbox-projection.js +20 -0
- package/dist/core/status/inbox-projection.js.map +1 -0
- package/dist/core/status/inbox-text.d.ts +4 -0
- package/dist/core/status/inbox-text.d.ts.map +1 -0
- package/dist/core/status/inbox-text.js +48 -0
- package/dist/core/status/inbox-text.js.map +1 -0
- package/dist/core/status/inbox.d.ts +53 -0
- package/dist/core/status/inbox.d.ts.map +1 -0
- package/dist/core/status/inbox.js +167 -0
- package/dist/core/status/inbox.js.map +1 -0
- package/dist/core/status/layout.d.ts +29 -0
- package/dist/core/status/layout.d.ts.map +1 -0
- package/dist/core/status/layout.js +77 -0
- package/dist/core/status/layout.js.map +1 -0
- package/dist/core/status/queue-parser.d.ts +13 -0
- package/dist/core/status/queue-parser.d.ts.map +1 -0
- package/dist/core/status/queue-parser.js +88 -0
- package/dist/core/status/queue-parser.js.map +1 -0
- package/dist/core/status/queue-planning.d.ts +62 -0
- package/dist/core/status/queue-planning.d.ts.map +1 -0
- package/dist/core/status/queue-planning.js +158 -0
- package/dist/core/status/queue-planning.js.map +1 -0
- package/dist/core/status/queue-report-detail.d.ts +50 -0
- package/dist/core/status/queue-report-detail.d.ts.map +1 -0
- package/dist/core/status/queue-report-detail.js +140 -0
- package/dist/core/status/queue-report-detail.js.map +1 -0
- package/dist/core/status/queue-report.d.ts +14 -0
- package/dist/core/status/queue-report.d.ts.map +1 -0
- package/dist/core/status/queue-report.js +41 -0
- package/dist/core/status/queue-report.js.map +1 -0
- package/dist/core/status/queue-state.d.ts +53 -0
- package/dist/core/status/queue-state.d.ts.map +1 -0
- package/dist/core/status/queue-state.js +167 -0
- package/dist/core/status/queue-state.js.map +1 -0
- package/dist/core/status/queue.d.ts +7 -0
- package/dist/core/status/queue.d.ts.map +1 -0
- package/dist/core/status/queue.js +4 -0
- package/dist/core/status/queue.js.map +1 -0
- package/dist/core/status/show.d.ts +102 -0
- package/dist/core/status/show.d.ts.map +1 -0
- package/dist/core/status/show.js +704 -0
- package/dist/core/status/show.js.map +1 -0
- package/dist/core/status/state.d.ts +56 -0
- package/dist/core/status/state.d.ts.map +1 -0
- package/dist/core/status/state.js +187 -0
- package/dist/core/status/state.js.map +1 -0
- package/dist/core/status/status.d.ts +23 -0
- package/dist/core/status/status.d.ts.map +1 -0
- package/dist/core/status/status.js +173 -0
- package/dist/core/status/status.js.map +1 -0
- package/dist/core/web/web-data-change.d.ts +10 -0
- package/dist/core/web/web-data-change.d.ts.map +1 -0
- package/dist/core/web/web-data-change.js +106 -0
- package/dist/core/web/web-data-change.js.map +1 -0
- package/dist/core/web/web-data-folders.d.ts +39 -0
- package/dist/core/web/web-data-folders.d.ts.map +1 -0
- package/dist/core/web/web-data-folders.js +125 -0
- package/dist/core/web/web-data-folders.js.map +1 -0
- package/dist/core/web/web-data-graph.d.ts +8 -0
- package/dist/core/web/web-data-graph.d.ts.map +1 -0
- package/dist/core/web/web-data-graph.js +113 -0
- package/dist/core/web/web-data-graph.js.map +1 -0
- package/dist/core/web/web-data-lifecycle.d.ts +43 -0
- package/dist/core/web/web-data-lifecycle.d.ts.map +1 -0
- package/dist/core/web/web-data-lifecycle.js +141 -0
- package/dist/core/web/web-data-lifecycle.js.map +1 -0
- package/dist/core/web/web-data-observations.d.ts +30 -0
- package/dist/core/web/web-data-observations.d.ts.map +1 -0
- package/dist/core/web/web-data-observations.js +181 -0
- package/dist/core/web/web-data-observations.js.map +1 -0
- package/dist/core/web/web-data-tokens.d.ts +29 -0
- package/dist/core/web/web-data-tokens.d.ts.map +1 -0
- package/dist/core/web/web-data-tokens.js +62 -0
- package/dist/core/web/web-data-tokens.js.map +1 -0
- package/dist/core/web/web-data-types.d.ts +156 -0
- package/dist/core/web/web-data-types.d.ts.map +1 -0
- package/dist/core/web/web-data-types.js +15 -0
- package/dist/core/web/web-data-types.js.map +1 -0
- package/dist/core/web/web-data.d.ts +10 -0
- package/dist/core/web/web-data.d.ts.map +1 -0
- package/dist/core/web/web-data.js +8 -0
- package/dist/core/web/web-data.js.map +1 -0
- package/dist/core/web/web-events.d.ts +54 -0
- package/dist/core/web/web-events.d.ts.map +1 -0
- package/dist/core/web/web-events.js +162 -0
- package/dist/core/web/web-events.js.map +1 -0
- package/dist/core/web/web-export.d.ts +18 -0
- package/dist/core/web/web-export.d.ts.map +1 -0
- package/dist/core/web/web-export.js +112 -0
- package/dist/core/web/web-export.js.map +1 -0
- package/dist/core/web/web-server.d.ts +34 -0
- package/dist/core/web/web-server.d.ts.map +1 -0
- package/dist/core/web/web-server.js +182 -0
- package/dist/core/web/web-server.js.map +1 -0
- package/dist/core/web/web-static.d.ts +20 -0
- package/dist/core/web/web-static.d.ts.map +1 -0
- package/dist/core/web/web-static.js +104 -0
- package/dist/core/web/web-static.js.map +1 -0
- package/dist/harness/{agy.d.ts → agy/agy.d.ts} +8 -6
- package/dist/harness/agy/agy.d.ts.map +1 -0
- package/dist/harness/{agy.js → agy/agy.js} +49 -34
- package/dist/harness/agy/agy.js.map +1 -0
- package/dist/harness/claude/claude-turns.d.ts +29 -0
- package/dist/harness/claude/claude-turns.d.ts.map +1 -0
- package/dist/harness/claude/claude-turns.js +162 -0
- package/dist/harness/claude/claude-turns.js.map +1 -0
- package/dist/harness/claude/claude-usage.d.ts +12 -0
- package/dist/harness/claude/claude-usage.d.ts.map +1 -0
- package/dist/harness/claude/claude-usage.js +144 -0
- package/dist/harness/claude/claude-usage.js.map +1 -0
- package/dist/harness/codex/codex-observe-usage.d.ts +6 -0
- package/dist/harness/codex/codex-observe-usage.d.ts.map +1 -0
- package/dist/harness/codex/codex-observe-usage.js +215 -0
- package/dist/harness/codex/codex-observe-usage.js.map +1 -0
- package/dist/harness/codex/codex-prompt.d.ts +17 -0
- package/dist/harness/codex/codex-prompt.d.ts.map +1 -0
- package/dist/harness/codex/codex-prompt.js +43 -0
- package/dist/harness/codex/codex-prompt.js.map +1 -0
- package/dist/harness/codex/codex-stream.d.ts +23 -0
- package/dist/harness/codex/codex-stream.d.ts.map +1 -0
- package/dist/harness/codex/codex-stream.js +182 -0
- package/dist/harness/codex/codex-stream.js.map +1 -0
- package/dist/harness/codex/codex-usage.d.ts +26 -0
- package/dist/harness/codex/codex-usage.d.ts.map +1 -0
- package/dist/harness/codex/codex-usage.js +122 -0
- package/dist/harness/codex/codex-usage.js.map +1 -0
- package/dist/harness/codex/codex.d.ts +16 -0
- package/dist/harness/codex/codex.d.ts.map +1 -0
- package/dist/harness/codex/codex.js +92 -0
- package/dist/harness/codex/codex.js.map +1 -0
- package/dist/harness/index.d.ts +11 -2
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +24 -9
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/mock.d.ts +5 -2
- package/dist/harness/mock.d.ts.map +1 -1
- package/dist/harness/mock.js +10 -11
- package/dist/harness/mock.js.map +1 -1
- package/dist/harness/opencode/opencode-observe-usage.d.ts +4 -0
- package/dist/harness/opencode/opencode-observe-usage.d.ts.map +1 -0
- package/dist/harness/opencode/opencode-observe-usage.js +100 -0
- package/dist/harness/opencode/opencode-observe-usage.js.map +1 -0
- package/dist/harness/opencode/opencode-usage.d.ts +24 -0
- package/dist/harness/opencode/opencode-usage.d.ts.map +1 -0
- package/dist/harness/opencode/opencode-usage.js +181 -0
- package/dist/harness/opencode/opencode-usage.js.map +1 -0
- package/dist/harness/{opencode.d.ts → opencode/opencode.d.ts} +10 -7
- package/dist/harness/opencode/opencode.d.ts.map +1 -0
- package/dist/harness/{opencode.js → opencode/opencode.js} +159 -70
- package/dist/harness/opencode/opencode.js.map +1 -0
- package/dist/harness/pi/pi-args.d.ts +9 -0
- package/dist/harness/pi/pi-args.d.ts.map +1 -0
- package/dist/harness/pi/pi-args.js +33 -0
- package/dist/harness/pi/pi-args.js.map +1 -0
- package/dist/harness/pi/pi-stream.d.ts +27 -0
- package/dist/harness/pi/pi-stream.d.ts.map +1 -0
- package/dist/harness/pi/pi-stream.js +176 -0
- package/dist/harness/pi/pi-stream.js.map +1 -0
- package/dist/harness/pi/pi-tools.d.ts +5 -0
- package/dist/harness/pi/pi-tools.d.ts.map +1 -0
- package/dist/harness/pi/pi-tools.js +31 -0
- package/dist/harness/pi/pi-tools.js.map +1 -0
- package/dist/harness/pi/pi.d.ts +11 -0
- package/dist/harness/pi/pi.d.ts.map +1 -0
- package/dist/harness/pi/pi.js +109 -0
- package/dist/harness/pi/pi.js.map +1 -0
- package/dist/harness/prompt.d.ts +11 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +105 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/types.d.ts +287 -8
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js +125 -0
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/watcher/archive-verify.d.ts +9 -0
- package/dist/watcher/archive-verify.d.ts.map +1 -0
- package/dist/watcher/archive-verify.js +80 -0
- package/dist/watcher/archive-verify.js.map +1 -0
- package/dist/watcher/archiver.d.ts +14 -2
- package/dist/watcher/archiver.d.ts.map +1 -1
- package/dist/watcher/archiver.js +112 -33
- package/dist/watcher/archiver.js.map +1 -1
- package/dist/watcher/attempt.d.ts +19 -0
- package/dist/watcher/attempt.d.ts.map +1 -0
- package/dist/watcher/attempt.js +94 -0
- package/dist/watcher/attempt.js.map +1 -0
- package/dist/watcher/auto-retry.d.ts +11 -0
- package/dist/watcher/auto-retry.d.ts.map +1 -0
- package/dist/watcher/auto-retry.js +180 -0
- package/dist/watcher/auto-retry.js.map +1 -0
- package/dist/watcher/build.d.ts +24 -0
- package/dist/watcher/build.d.ts.map +1 -0
- package/dist/watcher/build.js +169 -0
- package/dist/watcher/build.js.map +1 -0
- package/dist/watcher/change-verify.d.ts +25 -0
- package/dist/watcher/change-verify.d.ts.map +1 -0
- package/dist/watcher/change-verify.js +57 -0
- package/dist/watcher/change-verify.js.map +1 -0
- package/dist/watcher/dev.d.ts +51 -0
- package/dist/watcher/dev.d.ts.map +1 -0
- package/dist/watcher/dev.js +124 -0
- package/dist/watcher/dev.js.map +1 -0
- package/dist/watcher/failure-reason.d.ts +7 -0
- package/dist/watcher/failure-reason.d.ts.map +1 -0
- package/dist/watcher/failure-reason.js +2 -0
- package/dist/watcher/failure-reason.js.map +1 -0
- package/dist/watcher/fingerprint.d.ts +19 -0
- package/dist/watcher/fingerprint.d.ts.map +1 -0
- package/dist/watcher/fingerprint.js +53 -0
- package/dist/watcher/fingerprint.js.map +1 -0
- package/dist/watcher/heartbeat.d.ts +33 -0
- package/dist/watcher/heartbeat.d.ts.map +1 -0
- package/dist/watcher/heartbeat.js +135 -0
- package/dist/watcher/heartbeat.js.map +1 -0
- package/dist/watcher/lock.d.ts +10 -0
- package/dist/watcher/lock.d.ts.map +1 -0
- package/dist/watcher/lock.js +14 -0
- package/dist/watcher/lock.js.map +1 -0
- package/dist/watcher/loop.d.ts +26 -6
- package/dist/watcher/loop.d.ts.map +1 -1
- package/dist/watcher/loop.js +154 -37
- package/dist/watcher/loop.js.map +1 -1
- package/dist/watcher/measures.d.ts +46 -0
- package/dist/watcher/measures.d.ts.map +1 -0
- package/dist/watcher/measures.js +203 -0
- package/dist/watcher/measures.js.map +1 -0
- package/dist/watcher/outcome.d.ts +32 -0
- package/dist/watcher/outcome.d.ts.map +1 -0
- package/dist/watcher/outcome.js +127 -0
- package/dist/watcher/outcome.js.map +1 -0
- package/dist/watcher/regression.d.ts +43 -0
- package/dist/watcher/regression.d.ts.map +1 -0
- package/dist/watcher/regression.js +175 -0
- package/dist/watcher/regression.js.map +1 -0
- package/dist/watcher/runner.d.ts +6 -68
- package/dist/watcher/runner.d.ts.map +1 -1
- package/dist/watcher/runner.js +74 -523
- package/dist/watcher/runner.js.map +1 -1
- package/dist/watcher/spawn.d.ts +48 -0
- package/dist/watcher/spawn.d.ts.map +1 -0
- package/dist/watcher/spawn.js +130 -0
- package/dist/watcher/spawn.js.map +1 -0
- package/dist/watcher/task-verify.d.ts +59 -0
- package/dist/watcher/task-verify.d.ts.map +1 -0
- package/dist/watcher/task-verify.js +108 -0
- package/dist/watcher/task-verify.js.map +1 -0
- package/dist/watcher/verify.d.ts +44 -0
- package/dist/watcher/verify.d.ts.map +1 -0
- package/dist/watcher/verify.js +156 -0
- package/dist/watcher/verify.js.map +1 -0
- package/package.json +21 -9
- package/templates/PLANNER.md +77 -0
- package/templates/openspec/config.yaml +24 -0
- package/templates/openspec/schemas/osq/README.md +30 -0
- package/templates/openspec/schemas/osq/schema.yaml +133 -0
- package/templates/openspec/schemas/osq/templates/proposal.md +66 -0
- package/templates/openspec/schemas/osq/templates/spec.md +13 -0
- package/templates/openspec/schemas/osq/templates/tasks.md +11 -0
- package/templates/proposal.md +66 -0
- package/templates/tasks.md +5 -0
- package/ui/dist/assets/index-CZYeQ8la.js +9 -0
- package/ui/dist/assets/index-D6zVSjO7.css +1 -0
- package/ui/dist/index.html +14 -0
- package/dist/core/approve.d.ts +0 -11
- package/dist/core/approve.d.ts.map +0 -1
- package/dist/core/approve.js +0 -52
- package/dist/core/approve.js.map +0 -1
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js.map +0 -1
- package/dist/core/hasher.d.ts +0 -4
- package/dist/core/hasher.d.ts.map +0 -1
- package/dist/core/hasher.js.map +0 -1
- package/dist/core/init.d.ts +0 -12
- package/dist/core/init.d.ts.map +0 -1
- package/dist/core/init.js +0 -151
- package/dist/core/init.js.map +0 -1
- package/dist/core/linter.d.ts +0 -8
- package/dist/core/linter.d.ts.map +0 -1
- package/dist/core/linter.js +0 -100
- package/dist/core/linter.js.map +0 -1
- package/dist/core/lock.d.ts.map +0 -1
- package/dist/core/lock.js.map +0 -1
- package/dist/core/logger.d.ts.map +0 -1
- package/dist/core/logger.js.map +0 -1
- package/dist/core/new.d.ts.map +0 -1
- package/dist/core/new.js +0 -79
- package/dist/core/new.js.map +0 -1
- package/dist/core/parser.d.ts +0 -32
- package/dist/core/parser.d.ts.map +0 -1
- package/dist/core/parser.js +0 -87
- package/dist/core/parser.js.map +0 -1
- package/dist/core/report.d.ts +0 -54
- package/dist/core/report.d.ts.map +0 -1
- package/dist/core/report.js +0 -475
- package/dist/core/report.js.map +0 -1
- package/dist/core/show.d.ts +0 -50
- package/dist/core/show.d.ts.map +0 -1
- package/dist/core/show.js +0 -374
- package/dist/core/show.js.map +0 -1
- package/dist/core/state.d.ts +0 -25
- package/dist/core/state.d.ts.map +0 -1
- package/dist/core/state.js +0 -180
- package/dist/core/state.js.map +0 -1
- package/dist/core/status.d.ts +0 -10
- package/dist/core/status.d.ts.map +0 -1
- package/dist/core/status.js +0 -106
- package/dist/core/status.js.map +0 -1
- package/dist/harness/agy.d.ts.map +0 -1
- package/dist/harness/agy.js.map +0 -1
- package/dist/harness/opencode.d.ts.map +0 -1
- package/dist/harness/opencode.js.map +0 -1
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEFAULT_CONFIG } from '../foundation/config.js';
|
|
4
|
+
import { parseFrontmatter, parseTaskMd } from '../spec/parser.js';
|
|
5
|
+
import { getArchiveDir, getChangesDir, getRejectedDir } from '../status/layout.js';
|
|
6
|
+
import { readQueueReport } from '../status/queue-report.js';
|
|
7
|
+
import { compareNumericPrefix, deriveSpecState } from '../status/state.js';
|
|
8
|
+
import { collectApprovalFlagOutcomes, formatApprovalFlagOutcomes, } from './approval-flags.js';
|
|
9
|
+
import { buildPlanningComparison, computePlanningByChange, } from './planning-economics.js';
|
|
10
|
+
import { readPlanningSessions } from './planning.js';
|
|
11
|
+
import { asData, eventTimestampMs, observeAttempts, observeTaskStream, parseEventLines, parseTokenEvent, } from './report-events.js';
|
|
12
|
+
import { observePreSpawnEvents } from './report-pre-spawn.js';
|
|
13
|
+
import { addRetryHistory, emptyRetryHistory, observeRetries, } from './report-retries.js';
|
|
14
|
+
export function formatDuration(ms) {
|
|
15
|
+
if (ms <= 0)
|
|
16
|
+
return '0s';
|
|
17
|
+
const totalSec = Math.round(ms / 1000);
|
|
18
|
+
if (totalSec < 60)
|
|
19
|
+
return `${totalSec}s`;
|
|
20
|
+
const mins = Math.floor(totalSec / 60);
|
|
21
|
+
const secs = totalSec % 60;
|
|
22
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
23
|
+
}
|
|
24
|
+
export function formatCost(total) {
|
|
25
|
+
const fixed = total >= 0.01 ? total.toFixed(2) : total.toFixed(4);
|
|
26
|
+
return `$${fixed}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Formats a cost only when something was actually reported. A non-zero sum
|
|
30
|
+
* proves a reported value even when it was not attributed to a counted attempt;
|
|
31
|
+
* a counted reporter with a zero sum proves a reported zero. Otherwise the
|
|
32
|
+
* honest label is `not reported` rather than a dollar amount summed from nothing.
|
|
33
|
+
*/
|
|
34
|
+
function formatReportedCost(total, reportedCount) {
|
|
35
|
+
return total !== 0 || reportedCount > 0 ? formatCost(total) : 'not reported';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether the queue's planning spend has at least one reported value. Complete
|
|
39
|
+
* coverage with sessions proves every session reported, including recorded
|
|
40
|
+
* zeros; a non-zero sum proves one did and is handled by the formatter. An
|
|
41
|
+
* incomplete zero sum cannot tell an unrecorded cost from a reported zero, so
|
|
42
|
+
* it is treated as unreported rather than invented.
|
|
43
|
+
*/
|
|
44
|
+
function queuePlanningCostReported(queue) {
|
|
45
|
+
return queue.planning.sessions > 0 && queue.planning.costCoverageComplete;
|
|
46
|
+
}
|
|
47
|
+
const FILE_CHANGE_TOOLS = new Set(['edit', 'write']);
|
|
48
|
+
/**
|
|
49
|
+
* Trims surrounding whitespace and strips a leading `./` so paths written by
|
|
50
|
+
* different harnesses collapse to a single canonical form before deduplication.
|
|
51
|
+
*/
|
|
52
|
+
function normalizeFilePath(value) {
|
|
53
|
+
if (typeof value !== 'string')
|
|
54
|
+
return null;
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return null;
|
|
58
|
+
const cleaned = trimmed.replace(/^(?:\.\/)+/, '');
|
|
59
|
+
return cleaned || null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extracts a file path from a tool or file_changed event payload. The summary
|
|
63
|
+
* field carries the target path for file tools such as edit and write.
|
|
64
|
+
*/
|
|
65
|
+
function extractFilePath(data) {
|
|
66
|
+
for (const candidate of [data.summary, data.path, data.filePath, data.file, data.filename]) {
|
|
67
|
+
const filePath = normalizeFilePath(candidate);
|
|
68
|
+
if (filePath !== null)
|
|
69
|
+
return filePath;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/** A done marker declares manual completion when its frontmatter has `manual: true`. */
|
|
74
|
+
async function doneMarkerIsManual(markerPath) {
|
|
75
|
+
const content = await fs.readFile(markerPath, 'utf8').catch(() => null);
|
|
76
|
+
if (content === null)
|
|
77
|
+
return false;
|
|
78
|
+
return parseFrontmatter(content).data.manual === true;
|
|
79
|
+
}
|
|
80
|
+
async function fileExists(filePath) {
|
|
81
|
+
return fs
|
|
82
|
+
.stat(filePath)
|
|
83
|
+
.then(() => true)
|
|
84
|
+
.catch(() => false);
|
|
85
|
+
}
|
|
86
|
+
const SIZE_BUCKETS = ['1-2', '3-4', '5-8', 'over-8'];
|
|
87
|
+
function round2(value) {
|
|
88
|
+
return Math.round(value * 100) / 100;
|
|
89
|
+
}
|
|
90
|
+
/** Ordered bucket label for a non-negative size observation. */
|
|
91
|
+
function bucketForSize(value) {
|
|
92
|
+
if (value <= 2)
|
|
93
|
+
return '1-2';
|
|
94
|
+
if (value <= 4)
|
|
95
|
+
return '3-4';
|
|
96
|
+
if (value <= 8)
|
|
97
|
+
return '5-8';
|
|
98
|
+
return 'over-8';
|
|
99
|
+
}
|
|
100
|
+
/** Resolver generation for scope-file evidence; malformed versions fit neither. */
|
|
101
|
+
function scopeFileGeneration(task) {
|
|
102
|
+
if (task.scopeResolver === undefined)
|
|
103
|
+
return 'legacy';
|
|
104
|
+
if (task.scopeResolver === 2)
|
|
105
|
+
return 'resolver-2';
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** Title and acceptance-line count from a task file; missing files yield empties. */
|
|
109
|
+
async function readTaskMetadata(folderPath, taskNumber) {
|
|
110
|
+
const content = await fs
|
|
111
|
+
.readFile(path.join(folderPath, 'tasks', `${taskNumber}.md`), 'utf8')
|
|
112
|
+
.catch(() => null);
|
|
113
|
+
if (content === null)
|
|
114
|
+
return { title: '', acceptanceLines: 0 };
|
|
115
|
+
const task = parseTaskMd(content);
|
|
116
|
+
return { title: task.title, acceptanceLines: task.acceptance.length };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Projects one task's event stream. A task contributes only with a valid first
|
|
120
|
+
* `measures` start carrying a finite `scopeFiles`. Attempts count `started`
|
|
121
|
+
* events; the first attempt passes only when a typed `done` arrives before the
|
|
122
|
+
* next `started`, `dead`, or `regressed` outcome. Duration sums valid measures
|
|
123
|
+
* start-to-end pairs and is null when no pair completes.
|
|
124
|
+
*/
|
|
125
|
+
function deriveMeasuredTask(change, task, events) {
|
|
126
|
+
let scopeFiles = null;
|
|
127
|
+
let scopeResolver;
|
|
128
|
+
let pendingStartMs = null;
|
|
129
|
+
let coveredMs = 0;
|
|
130
|
+
let hasDuration = false;
|
|
131
|
+
for (const event of events) {
|
|
132
|
+
if (event.type !== 'measures')
|
|
133
|
+
continue;
|
|
134
|
+
const data = asData(event);
|
|
135
|
+
if (data?.phase === 'start') {
|
|
136
|
+
const rawScopeFiles = data.scopeFiles;
|
|
137
|
+
if (scopeFiles === null &&
|
|
138
|
+
typeof rawScopeFiles === 'number' &&
|
|
139
|
+
Number.isFinite(rawScopeFiles) &&
|
|
140
|
+
rawScopeFiles >= 0) {
|
|
141
|
+
scopeFiles = rawScopeFiles;
|
|
142
|
+
scopeResolver = data.scopeResolver;
|
|
143
|
+
}
|
|
144
|
+
pendingStartMs = eventTimestampMs(event);
|
|
145
|
+
}
|
|
146
|
+
else if (data?.phase === 'end') {
|
|
147
|
+
const endMs = eventTimestampMs(event);
|
|
148
|
+
if (pendingStartMs !== null && endMs !== null && endMs >= pendingStartMs) {
|
|
149
|
+
coveredMs += endMs - pendingStartMs;
|
|
150
|
+
hasDuration = true;
|
|
151
|
+
}
|
|
152
|
+
pendingStartMs = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (scopeFiles === null)
|
|
156
|
+
return null;
|
|
157
|
+
const { attempts, firstAttemptPass } = observeAttempts(events);
|
|
158
|
+
return {
|
|
159
|
+
change,
|
|
160
|
+
task,
|
|
161
|
+
scopeResolver,
|
|
162
|
+
scopeFiles,
|
|
163
|
+
attempts,
|
|
164
|
+
firstAttemptPass,
|
|
165
|
+
durationSeconds: hasDuration ? coveredMs / 1000 : null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Every measured task across the given change folders, in stable folder order. */
|
|
169
|
+
async function projectMeasuredTasks(folders) {
|
|
170
|
+
const measured = [];
|
|
171
|
+
for (const folderPath of folders) {
|
|
172
|
+
const change = path.basename(folderPath);
|
|
173
|
+
const tasksDir = path.join(folderPath, 'tasks');
|
|
174
|
+
let taskFiles = [];
|
|
175
|
+
try {
|
|
176
|
+
taskFiles = (await fs.readdir(tasksDir))
|
|
177
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
178
|
+
.sort(compareNumericPrefix);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
for (const taskFile of taskFiles) {
|
|
184
|
+
const taskNumber = taskFile.replace(/\.md$/, '');
|
|
185
|
+
const eventContent = await fs
|
|
186
|
+
.readFile(path.join(folderPath, '.run', 'events', `${taskNumber}.jsonl`), 'utf8')
|
|
187
|
+
.catch(() => null);
|
|
188
|
+
if (eventContent === null)
|
|
189
|
+
continue;
|
|
190
|
+
const derived = deriveMeasuredTask(change, taskNumber, parseEventLines(eventContent));
|
|
191
|
+
if (!derived)
|
|
192
|
+
continue;
|
|
193
|
+
const metadata = await readTaskMetadata(folderPath, taskNumber);
|
|
194
|
+
measured.push({
|
|
195
|
+
...derived,
|
|
196
|
+
title: metadata.title,
|
|
197
|
+
acceptanceLines: metadata.acceptanceLines,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return measured;
|
|
202
|
+
}
|
|
203
|
+
/** Ordinary sorted median rounded to two decimals, or null for no values. */
|
|
204
|
+
function medianSeconds(values) {
|
|
205
|
+
if (values.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
208
|
+
const middle = Math.floor(sorted.length / 2);
|
|
209
|
+
const median = sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
|
|
210
|
+
return round2(median);
|
|
211
|
+
}
|
|
212
|
+
/** Ordered bucket rows for one size key; empty buckets report zero and null. */
|
|
213
|
+
function aggregateSizeBuckets(tasks, key) {
|
|
214
|
+
return SIZE_BUCKETS.map((bucket) => {
|
|
215
|
+
const rows = tasks.filter((task) => bucketForSize(task[key]) === bucket);
|
|
216
|
+
const count = rows.length;
|
|
217
|
+
const passed = rows.filter((task) => task.firstAttemptPass).length;
|
|
218
|
+
const durations = rows
|
|
219
|
+
.map((task) => task.durationSeconds)
|
|
220
|
+
.filter((value) => value !== null);
|
|
221
|
+
return {
|
|
222
|
+
bucket,
|
|
223
|
+
tasks: count,
|
|
224
|
+
firstAttemptPassRate: count > 0 ? round2(passed / count) : 0,
|
|
225
|
+
meanAttempts: count > 0 ? round2(rows.reduce((sum, task) => sum + task.attempts, 0) / count) : 0,
|
|
226
|
+
medianDurationSeconds: medianSeconds(durations),
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/** The first-attempt pass with the greatest scope files, then acceptance lines. */
|
|
231
|
+
function selectLargestFirstAttemptPass(tasks) {
|
|
232
|
+
const passing = tasks.filter((task) => task.firstAttemptPass);
|
|
233
|
+
if (passing.length === 0)
|
|
234
|
+
return null;
|
|
235
|
+
const [top] = [...passing].sort((a, b) => {
|
|
236
|
+
if (b.scopeFiles !== a.scopeFiles)
|
|
237
|
+
return b.scopeFiles - a.scopeFiles;
|
|
238
|
+
if (b.acceptanceLines !== a.acceptanceLines)
|
|
239
|
+
return b.acceptanceLines - a.acceptanceLines;
|
|
240
|
+
const byChange = compareNumericPrefix(a.change, b.change);
|
|
241
|
+
return byChange !== 0 ? byChange : compareNumericPrefix(a.task, b.task);
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
change: top.change,
|
|
245
|
+
task: top.task,
|
|
246
|
+
title: top.title,
|
|
247
|
+
scopeFiles: top.scopeFiles,
|
|
248
|
+
acceptanceLines: top.acceptanceLines,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** The smallest change id by stable numeric ordering, or null for no tasks. */
|
|
252
|
+
function earliestChange(tasks) {
|
|
253
|
+
let earliest = null;
|
|
254
|
+
for (const task of tasks) {
|
|
255
|
+
if (earliest === null || compareNumericPrefix(task.change, earliest) < 0) {
|
|
256
|
+
earliest = task.change;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return earliest;
|
|
260
|
+
}
|
|
261
|
+
/** Build one resolver generation's scope-file bucket and largest-pass evidence. */
|
|
262
|
+
function buildScopeFileSeries(tasks, resolver) {
|
|
263
|
+
return {
|
|
264
|
+
resolver,
|
|
265
|
+
startsAtChange: resolver === 'resolver-2' ? earliestChange(tasks) : null,
|
|
266
|
+
byScopeFiles: aggregateSizeBuckets(tasks, 'scopeFiles'),
|
|
267
|
+
largestFirstAttemptPass: selectLargestFirstAttemptPass(tasks),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
/** Ordered legacy then resolver-2 scope-file series, excluding malformed versions. */
|
|
271
|
+
function deriveScopeFileSeries(tasks) {
|
|
272
|
+
const legacy = tasks.filter((task) => scopeFileGeneration(task) === 'legacy');
|
|
273
|
+
const resolver2 = tasks.filter((task) => scopeFileGeneration(task) === 'resolver-2');
|
|
274
|
+
return [buildScopeFileSeries(legacy, 'legacy'), buildScopeFileSeries(resolver2, 'resolver-2')];
|
|
275
|
+
}
|
|
276
|
+
/** The scope-file series whose largest pass drives the near-limit hint. */
|
|
277
|
+
function selectHintSeries(series) {
|
|
278
|
+
const resolver2 = series.find((entry) => entry.resolver === 'resolver-2');
|
|
279
|
+
if (resolver2?.byScopeFiles.some((row) => row.tasks > 0))
|
|
280
|
+
return resolver2;
|
|
281
|
+
return series.find((entry) => entry.resolver === 'legacy');
|
|
282
|
+
}
|
|
283
|
+
/** One retry-group line in the `Automatic retries:` block. */
|
|
284
|
+
function formatRetryGroupLine(name, group) {
|
|
285
|
+
return ` ${name}: ${group.count} retries, ${group.reachedDone} reached done, cost ${formatReportedCost(group.cost, group.costReportedAttempts)} (${group.costReportedAttempts} of ${group.count} attempts reported cost)`;
|
|
286
|
+
}
|
|
287
|
+
/** The retry block printed after the verification lines. */
|
|
288
|
+
function formatRetryHistoryLines(retries) {
|
|
289
|
+
return [
|
|
290
|
+
' Automatic retries:',
|
|
291
|
+
formatRetryGroupLine('automatic', retries.automatic),
|
|
292
|
+
formatRetryGroupLine('manual', retries.manual),
|
|
293
|
+
` Stuck: ${retries.stuck}`,
|
|
294
|
+
];
|
|
295
|
+
}
|
|
296
|
+
/** Rows printed for one size-against-outcome table. */
|
|
297
|
+
function formatSizeBucketLines(rows) {
|
|
298
|
+
const lines = [' bucket tasks first-attempt pass rate mean attempts median duration'];
|
|
299
|
+
for (const row of rows) {
|
|
300
|
+
const median = row.medianDurationSeconds === null
|
|
301
|
+
? 'unavailable'
|
|
302
|
+
: formatDuration(row.medianDurationSeconds * 1000);
|
|
303
|
+
lines.push(` ${row.bucket.padEnd(10)} ${String(row.tasks).padEnd(6)} ${String(row.firstAttemptPassRate).padEnd(23)} ${String(row.meanAttempts).padEnd(14)} ${median}`);
|
|
304
|
+
}
|
|
305
|
+
return lines;
|
|
306
|
+
}
|
|
307
|
+
/** Parses a `brief.md` frontmatter `date` value into epoch milliseconds. */
|
|
308
|
+
function parseBriefDateMs(data) {
|
|
309
|
+
const value = data.date;
|
|
310
|
+
if (value instanceof Date) {
|
|
311
|
+
const ms = value.getTime();
|
|
312
|
+
return Number.isFinite(ms) ? ms : null;
|
|
313
|
+
}
|
|
314
|
+
if (typeof value !== 'string')
|
|
315
|
+
return null;
|
|
316
|
+
const trimmed = value.trim();
|
|
317
|
+
if (!trimmed)
|
|
318
|
+
return null;
|
|
319
|
+
const ms = Date.parse(trimmed);
|
|
320
|
+
return Number.isFinite(ms) ? ms : null;
|
|
321
|
+
}
|
|
322
|
+
/** Brief frontmatter date in milliseconds, or null when missing or invalid. */
|
|
323
|
+
async function readBriefDateMs(folderPath) {
|
|
324
|
+
const content = await fs.readFile(path.join(folderPath, 'brief.md'), 'utf8').catch(() => null);
|
|
325
|
+
if (content === null)
|
|
326
|
+
return null;
|
|
327
|
+
return parseBriefDateMs(parseFrontmatter(content).data);
|
|
328
|
+
}
|
|
329
|
+
/** Manifest `approvedAt` in milliseconds, or null when missing or invalid. */
|
|
330
|
+
async function readApprovedAtMs(folderPath) {
|
|
331
|
+
const content = await fs
|
|
332
|
+
.readFile(path.join(folderPath, '.run', 'manifest.json'), 'utf8')
|
|
333
|
+
.catch(() => null);
|
|
334
|
+
if (content === null)
|
|
335
|
+
return null;
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(content);
|
|
338
|
+
if (typeof parsed.approvedAt !== 'string')
|
|
339
|
+
return null;
|
|
340
|
+
const ms = Date.parse(parsed.approvedAt);
|
|
341
|
+
return Number.isFinite(ms) ? ms : null;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Earliest `started` event timestamp across numeric `<n>.jsonl` task streams.
|
|
349
|
+
* The change-level `change.jsonl` is never a task stream.
|
|
350
|
+
*/
|
|
351
|
+
async function readFirstTaskStartMs(folderPath) {
|
|
352
|
+
const eventsDir = path.join(folderPath, '.run', 'events');
|
|
353
|
+
let entries = [];
|
|
354
|
+
try {
|
|
355
|
+
entries = (await fs.readdir(eventsDir)).filter((entry) => /^\d+\.jsonl$/.test(entry));
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
let earliest = null;
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
const content = await fs.readFile(path.join(eventsDir, entry), 'utf8').catch(() => '');
|
|
363
|
+
for (const event of parseEventLines(content)) {
|
|
364
|
+
if (event.type !== 'started' || typeof event.timestamp !== 'string')
|
|
365
|
+
continue;
|
|
366
|
+
const ms = Date.parse(event.timestamp);
|
|
367
|
+
if (!Number.isFinite(ms))
|
|
368
|
+
continue;
|
|
369
|
+
if (earliest === null || ms < earliest)
|
|
370
|
+
earliest = ms;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return earliest;
|
|
374
|
+
}
|
|
375
|
+
/** Authoritative archive time from a `change.jsonl` `archived` event, if any. */
|
|
376
|
+
async function readArchivedAtMs(folderPath) {
|
|
377
|
+
const content = await fs
|
|
378
|
+
.readFile(path.join(folderPath, '.run', 'events', 'change.jsonl'), 'utf8')
|
|
379
|
+
.catch(() => null);
|
|
380
|
+
if (content === null)
|
|
381
|
+
return null;
|
|
382
|
+
for (const event of parseEventLines(content)) {
|
|
383
|
+
if (event.type !== 'archived' || typeof event.timestamp !== 'string')
|
|
384
|
+
continue;
|
|
385
|
+
const ms = Date.parse(event.timestamp);
|
|
386
|
+
if (Number.isFinite(ms))
|
|
387
|
+
return ms;
|
|
388
|
+
}
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
/** Non-negative rounded seconds between two endpoints, or null when invalid. */
|
|
392
|
+
function phaseSeconds(startMs, endMs) {
|
|
393
|
+
if (startMs === null || endMs === null)
|
|
394
|
+
return null;
|
|
395
|
+
const delta = endMs - startMs;
|
|
396
|
+
if (delta < 0)
|
|
397
|
+
return null;
|
|
398
|
+
return Math.round(delta / 1000);
|
|
399
|
+
}
|
|
400
|
+
/** True when a change-level stream contains at least one valid `rejected` event. */
|
|
401
|
+
async function hasRejectedEvent(folderPath) {
|
|
402
|
+
const content = await fs
|
|
403
|
+
.readFile(path.join(folderPath, '.run', 'events', 'change.jsonl'), 'utf8')
|
|
404
|
+
.catch(() => null);
|
|
405
|
+
if (content === null)
|
|
406
|
+
return false;
|
|
407
|
+
return parseEventLines(content).some((event) => event.type === 'rejected');
|
|
408
|
+
}
|
|
409
|
+
/** Non-empty `planner` value from `brief.md` frontmatter, else `unknown`. */
|
|
410
|
+
async function readPlannerModel(folderPath) {
|
|
411
|
+
const content = await fs.readFile(path.join(folderPath, 'brief.md'), 'utf8').catch(() => null);
|
|
412
|
+
if (content === null)
|
|
413
|
+
return 'unknown';
|
|
414
|
+
const value = parseFrontmatter(content).data.planner;
|
|
415
|
+
if (typeof value === 'string' && value.trim())
|
|
416
|
+
return value.trim();
|
|
417
|
+
return 'unknown';
|
|
418
|
+
}
|
|
419
|
+
/** Rejected change folders, sorted deterministically by numeric prefix. */
|
|
420
|
+
async function listRejectedFolders(rejectedDir) {
|
|
421
|
+
let entries = [];
|
|
422
|
+
try {
|
|
423
|
+
entries = await fs.readdir(rejectedDir);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const folders = [];
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
if (entry.startsWith('.') || entry.startsWith('_'))
|
|
431
|
+
continue;
|
|
432
|
+
const fullPath = path.join(rejectedDir, entry);
|
|
433
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
434
|
+
if (stat?.isDirectory())
|
|
435
|
+
folders.push(fullPath);
|
|
436
|
+
}
|
|
437
|
+
return folders.sort((a, b) => compareNumericPrefix(path.basename(a), path.basename(b)));
|
|
438
|
+
}
|
|
439
|
+
/** Total, average, and coverage over the rows that report a finite phase. */
|
|
440
|
+
function aggregatePhase(rows, key) {
|
|
441
|
+
let totalSeconds = 0;
|
|
442
|
+
let coveredChanges = 0;
|
|
443
|
+
for (const row of rows) {
|
|
444
|
+
const value = row[key];
|
|
445
|
+
if (value !== null) {
|
|
446
|
+
totalSeconds += value;
|
|
447
|
+
coveredChanges++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
totalSeconds,
|
|
452
|
+
averageSeconds: coveredChanges > 0 ? Math.round(totalSeconds / coveredChanges) : 0,
|
|
453
|
+
coveredChanges,
|
|
454
|
+
totalChanges: rows.length,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
export async function getMetricsReport(projectRoot, config = DEFAULT_CONFIG) {
|
|
458
|
+
let specsDir = getChangesDir(config.paths.openspecRoot, projectRoot);
|
|
459
|
+
let archiveDir = getArchiveDir(config.paths.openspecRoot, projectRoot);
|
|
460
|
+
let rejectedDir = getRejectedDir(config.paths.openspecRoot, projectRoot);
|
|
461
|
+
const specsDirStat = await fs.stat(specsDir).catch(() => null);
|
|
462
|
+
const archiveDirStat = await fs.stat(archiveDir).catch(() => null);
|
|
463
|
+
if (!specsDirStat && !archiveDirStat) {
|
|
464
|
+
const legacySpecs = path.join(projectRoot, 'specs');
|
|
465
|
+
const legacyArchive = path.join(projectRoot, 'specs', 'archive');
|
|
466
|
+
const legacySpecsStat = await fs.stat(legacySpecs).catch(() => null);
|
|
467
|
+
const legacyArchiveStat = await fs.stat(legacyArchive).catch(() => null);
|
|
468
|
+
if (legacySpecsStat || legacyArchiveStat) {
|
|
469
|
+
specsDir = legacySpecs;
|
|
470
|
+
archiveDir = legacyArchive;
|
|
471
|
+
rejectedDir = path.join(legacySpecs, 'rejected');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 1. Identify active specs
|
|
475
|
+
let activeEntries = [];
|
|
476
|
+
try {
|
|
477
|
+
activeEntries = await fs.readdir(specsDir);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
activeEntries = [];
|
|
481
|
+
}
|
|
482
|
+
const archiveRel = path.relative(specsDir, archiveDir);
|
|
483
|
+
const archiveFolder = !archiveRel.startsWith('..') && !path.isAbsolute(archiveRel)
|
|
484
|
+
? archiveRel.split(path.sep)[0]
|
|
485
|
+
: 'archive';
|
|
486
|
+
const rejectedRel = path.relative(specsDir, rejectedDir);
|
|
487
|
+
const rejectedFolder = !rejectedRel.startsWith('..') && !path.isAbsolute(rejectedRel)
|
|
488
|
+
? rejectedRel.split(path.sep)[0]
|
|
489
|
+
: 'rejected';
|
|
490
|
+
const candidateActive = activeEntries.filter((e) => !e.startsWith('_') && !e.startsWith('.') && e !== archiveFolder && e !== rejectedFolder);
|
|
491
|
+
const activeFolders = [];
|
|
492
|
+
for (const folder of candidateActive) {
|
|
493
|
+
const fullPath = path.join(specsDir, folder);
|
|
494
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
495
|
+
if (stat?.isDirectory()) {
|
|
496
|
+
activeFolders.push(fullPath);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// 2. Identify archived specs
|
|
500
|
+
let archiveEntries = [];
|
|
501
|
+
try {
|
|
502
|
+
archiveEntries = await fs.readdir(archiveDir);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
archiveEntries = [];
|
|
506
|
+
}
|
|
507
|
+
const candidateArchived = archiveEntries.filter((e) => !e.startsWith('_') && !e.startsWith('.'));
|
|
508
|
+
const archivedFolders = [];
|
|
509
|
+
for (const folder of candidateArchived) {
|
|
510
|
+
const fullPath = path.join(archiveDir, folder);
|
|
511
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
512
|
+
if (stat?.isDirectory()) {
|
|
513
|
+
archivedFolders.push(fullPath);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const allSpecFolders = [...activeFolders, ...archivedFolders];
|
|
517
|
+
// Rejected history is discovered separately from the active and archive
|
|
518
|
+
// scans, so preserved tasks, planning logs, and events never leak into any
|
|
519
|
+
// other aggregate. A folder counts once, only with a valid `rejected` event.
|
|
520
|
+
let rejectionTotal = 0;
|
|
521
|
+
const rejectionsByPlanner = {};
|
|
522
|
+
for (const folderPath of await listRejectedFolders(rejectedDir)) {
|
|
523
|
+
if (!(await hasRejectedEvent(folderPath)))
|
|
524
|
+
continue;
|
|
525
|
+
rejectionTotal++;
|
|
526
|
+
const model = await readPlannerModel(folderPath);
|
|
527
|
+
rejectionsByPlanner[model] = (rejectionsByPlanner[model] ?? 0) + 1;
|
|
528
|
+
}
|
|
529
|
+
const rejectionsByPlannerModel = Object.fromEntries(Object.entries(rejectionsByPlanner).sort(([a], [b]) => a.localeCompare(b)));
|
|
530
|
+
// 3. Shared task discovery. Both the marker and event views iterate this same
|
|
531
|
+
// set, so current state and history can never disagree about which tasks exist.
|
|
532
|
+
const archivedFolderSet = new Set(archivedFolders);
|
|
533
|
+
const discoveredTasks = [];
|
|
534
|
+
for (const folderPath of allSpecFolders) {
|
|
535
|
+
const changeId = path.basename(folderPath);
|
|
536
|
+
const tasksDir = path.join(folderPath, 'tasks');
|
|
537
|
+
let taskFiles = [];
|
|
538
|
+
try {
|
|
539
|
+
taskFiles = (await fs.readdir(tasksDir))
|
|
540
|
+
.filter((e) => e.endsWith('.md'))
|
|
541
|
+
.sort(compareNumericPrefix);
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
taskFiles = [];
|
|
545
|
+
}
|
|
546
|
+
const statusByTask = new Map();
|
|
547
|
+
try {
|
|
548
|
+
const specState = await deriveSpecState(projectRoot, folderPath);
|
|
549
|
+
for (const task of specState.tasks) {
|
|
550
|
+
statusByTask.set(task.taskNumber, task.status);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
for (const taskFile of taskFiles) {
|
|
555
|
+
const taskNumber = taskFile.replace(/\.md$/, '');
|
|
556
|
+
discoveredTasks.push({
|
|
557
|
+
changeId,
|
|
558
|
+
taskNumber,
|
|
559
|
+
id: `${changeId}/${taskNumber}`,
|
|
560
|
+
folderPath,
|
|
561
|
+
eventFilePath: path.join(folderPath, '.run', 'events', `${taskNumber}.jsonl`),
|
|
562
|
+
archived: archivedFolderSet.has(folderPath),
|
|
563
|
+
status: statusByTask.get(taskNumber) ?? 'pending',
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// 4. Current state: markers only. A task in an archived change that carries no
|
|
568
|
+
// terminal or running marker can never run again, so it is unmarked rather
|
|
569
|
+
// than pending.
|
|
570
|
+
let doneTasks = 0;
|
|
571
|
+
let verifiedTasks = 0;
|
|
572
|
+
let manualTasks = 0;
|
|
573
|
+
let deadTasks = 0;
|
|
574
|
+
let regressedTasks = 0;
|
|
575
|
+
let runningTasks = 0;
|
|
576
|
+
let pendingTasks = 0;
|
|
577
|
+
let unmarkedTasks = 0;
|
|
578
|
+
const manualCache = new Map();
|
|
579
|
+
for (const task of discoveredTasks) {
|
|
580
|
+
switch (task.status) {
|
|
581
|
+
case 'done': {
|
|
582
|
+
doneTasks++;
|
|
583
|
+
const markerPath = path.join(task.folderPath, '.run', 'done', task.taskNumber);
|
|
584
|
+
let manual = manualCache.get(markerPath);
|
|
585
|
+
if (manual === undefined) {
|
|
586
|
+
manual = await doneMarkerIsManual(markerPath);
|
|
587
|
+
manualCache.set(markerPath, manual);
|
|
588
|
+
}
|
|
589
|
+
if (manual)
|
|
590
|
+
manualTasks++;
|
|
591
|
+
else
|
|
592
|
+
verifiedTasks++;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 'dead':
|
|
596
|
+
deadTasks++;
|
|
597
|
+
break;
|
|
598
|
+
case 'regressed':
|
|
599
|
+
regressedTasks++;
|
|
600
|
+
break;
|
|
601
|
+
case 'running':
|
|
602
|
+
runningTasks++;
|
|
603
|
+
break;
|
|
604
|
+
case 'pending':
|
|
605
|
+
if (task.archived)
|
|
606
|
+
unmarkedTasks++;
|
|
607
|
+
else
|
|
608
|
+
pendingTasks++;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const totalTasks = discoveredTasks.length;
|
|
613
|
+
// 5. History and coverage: task event files only.
|
|
614
|
+
let attemptsTotal = 0;
|
|
615
|
+
const attemptsByTask = {};
|
|
616
|
+
const multipleAttempts = [];
|
|
617
|
+
let unexplainedTotal = 0;
|
|
618
|
+
const unexplainedByTask = {};
|
|
619
|
+
const deadByReason = {};
|
|
620
|
+
let verifyTotal = 0;
|
|
621
|
+
let verifyMissingExitCode = 0;
|
|
622
|
+
const verifyByTask = {};
|
|
623
|
+
let preSpawnRuns = 0;
|
|
624
|
+
let preSpawnMismatches = 0;
|
|
625
|
+
const preSpawnMismatchedTasks = [];
|
|
626
|
+
let preSpawnMissingPathRuns = 0;
|
|
627
|
+
const preSpawnByStart = {
|
|
628
|
+
red: { runs: 0, passed: 0 },
|
|
629
|
+
green: { runs: 0, passed: 0 },
|
|
630
|
+
any: { runs: 0, passed: 0 },
|
|
631
|
+
};
|
|
632
|
+
let totalCost = 0;
|
|
633
|
+
const perSpecCost = {};
|
|
634
|
+
let reportedCostAttempts = 0;
|
|
635
|
+
let retries = emptyRetryHistory();
|
|
636
|
+
let scopeDetected = 0;
|
|
637
|
+
let scopeVerificationPassed = 0;
|
|
638
|
+
let scopeVerificationFailed = 0;
|
|
639
|
+
let scopeRecertifiedByHuman = 0;
|
|
640
|
+
let scopeRequeuedForAgent = 0;
|
|
641
|
+
let withEventsCount = 0;
|
|
642
|
+
let withoutEventsCount = 0;
|
|
643
|
+
const coverageByChange = {};
|
|
644
|
+
for (const task of discoveredTasks) {
|
|
645
|
+
if (!coverageByChange[task.changeId]) {
|
|
646
|
+
coverageByChange[task.changeId] = { withEvents: [], withoutEvents: [] };
|
|
647
|
+
}
|
|
648
|
+
const changeCoverage = coverageByChange[task.changeId];
|
|
649
|
+
const hasEventFile = await fileExists(task.eventFilePath);
|
|
650
|
+
if (hasEventFile) {
|
|
651
|
+
withEventsCount++;
|
|
652
|
+
changeCoverage.withEvents.push(task.taskNumber);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
withoutEventsCount++;
|
|
656
|
+
changeCoverage.withoutEvents.push(task.taskNumber);
|
|
657
|
+
}
|
|
658
|
+
const content = hasEventFile
|
|
659
|
+
? await fs.readFile(task.eventFilePath, 'utf8').catch(() => '')
|
|
660
|
+
: '';
|
|
661
|
+
const events = parseEventLines(content);
|
|
662
|
+
const observation = observeTaskStream(events);
|
|
663
|
+
const preSpawnEvents = observePreSpawnEvents(events);
|
|
664
|
+
preSpawnMissingPathRuns += preSpawnEvents.missingPathRuns;
|
|
665
|
+
for (const start of ['red', 'green', 'any']) {
|
|
666
|
+
preSpawnByStart[start].runs += preSpawnEvents.byStart[start].runs;
|
|
667
|
+
preSpawnByStart[start].passed += preSpawnEvents.byStart[start].passed;
|
|
668
|
+
}
|
|
669
|
+
retries = addRetryHistory(retries, observeRetries(events));
|
|
670
|
+
const taskAttempts = observation.attempts;
|
|
671
|
+
attemptsTotal += taskAttempts;
|
|
672
|
+
unexplainedTotal += observation.unexplained;
|
|
673
|
+
for (const [reason, count] of Object.entries(observation.deadByReason)) {
|
|
674
|
+
deadByReason[reason] = (deadByReason[reason] ?? 0) + count;
|
|
675
|
+
}
|
|
676
|
+
verifyTotal += observation.verifyCodes.length;
|
|
677
|
+
verifyMissingExitCode += observation.verifyCodes.filter((code) => code === null).length;
|
|
678
|
+
preSpawnRuns += observation.preSpawnRuns;
|
|
679
|
+
preSpawnMismatches += observation.preSpawnMismatches;
|
|
680
|
+
if (observation.preSpawnMismatches > 0)
|
|
681
|
+
preSpawnMismatchedTasks.push(task.id);
|
|
682
|
+
for (const value of observation.costValues) {
|
|
683
|
+
totalCost += value;
|
|
684
|
+
perSpecCost[task.changeId] = (perSpecCost[task.changeId] ?? 0) + value;
|
|
685
|
+
}
|
|
686
|
+
reportedCostAttempts += observation.costReportedAttempts;
|
|
687
|
+
scopeDetected += observation.scopeDetected;
|
|
688
|
+
scopeVerificationPassed += observation.scopeVerificationPassed;
|
|
689
|
+
scopeVerificationFailed += observation.scopeVerificationFailed;
|
|
690
|
+
scopeRecertifiedByHuman += observation.scopeRecertifiedByHuman;
|
|
691
|
+
scopeRequeuedForAgent += observation.scopeRequeuedForAgent;
|
|
692
|
+
attemptsByTask[task.id] = taskAttempts;
|
|
693
|
+
if (taskAttempts > 1)
|
|
694
|
+
multipleAttempts.push(task.id);
|
|
695
|
+
if (observation.unexplained > 0)
|
|
696
|
+
unexplainedByTask[task.id] = observation.unexplained;
|
|
697
|
+
if (observation.verifyCodes.length > 0)
|
|
698
|
+
verifyByTask[task.id] = [...observation.verifyCodes];
|
|
699
|
+
}
|
|
700
|
+
for (const changeCoverage of Object.values(coverageByChange)) {
|
|
701
|
+
changeCoverage.withEvents.sort(compareNumericPrefix);
|
|
702
|
+
changeCoverage.withoutEvents.sort(compareNumericPrefix);
|
|
703
|
+
}
|
|
704
|
+
multipleAttempts.sort();
|
|
705
|
+
preSpawnMismatchedTasks.sort();
|
|
706
|
+
// 6. Unrelated aggregate metrics still read every event stream, including
|
|
707
|
+
// legacy `change.jsonl`, exactly as before. The change-level stream is
|
|
708
|
+
// excluded from execution durations: since 035 it can carry archive events
|
|
709
|
+
// whose span is not a task duration.
|
|
710
|
+
let totalDurationMs = 0;
|
|
711
|
+
let tasksWithDurationCount = 0;
|
|
712
|
+
let totalInput = 0;
|
|
713
|
+
let totalCachedInput = 0;
|
|
714
|
+
let totalOutput = 0;
|
|
715
|
+
let totalReasoning = 0;
|
|
716
|
+
let totalTokens = 0;
|
|
717
|
+
let totalFileChanges = 0;
|
|
718
|
+
const uniqueFiles = new Set();
|
|
719
|
+
for (const folderPath of allSpecFolders) {
|
|
720
|
+
const eventsDir = path.join(folderPath, '.run', 'events');
|
|
721
|
+
let eventFiles = [];
|
|
722
|
+
try {
|
|
723
|
+
eventFiles = (await fs.readdir(eventsDir)).filter((e) => e.endsWith('.jsonl'));
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
eventFiles = [];
|
|
727
|
+
}
|
|
728
|
+
for (const eventFile of eventFiles) {
|
|
729
|
+
const isChangeStream = eventFile === 'change.jsonl';
|
|
730
|
+
const eventFilePath = path.join(eventsDir, eventFile);
|
|
731
|
+
const fileContent = await fs.readFile(eventFilePath, 'utf8').catch(() => '');
|
|
732
|
+
if (!fileContent)
|
|
733
|
+
continue;
|
|
734
|
+
let startedTime = null;
|
|
735
|
+
let exitedTime = null;
|
|
736
|
+
let minTime = null;
|
|
737
|
+
let maxTime = null;
|
|
738
|
+
for (const event of parseEventLines(fileContent)) {
|
|
739
|
+
const data = asData(event);
|
|
740
|
+
const ts = event.timestamp ? new Date(String(event.timestamp)).getTime() : Number.NaN;
|
|
741
|
+
if (!Number.isNaN(ts)) {
|
|
742
|
+
if (minTime === null || ts < minTime)
|
|
743
|
+
minTime = ts;
|
|
744
|
+
if (maxTime === null || ts > maxTime)
|
|
745
|
+
maxTime = ts;
|
|
746
|
+
if (event.type === 'started' && startedTime === null) {
|
|
747
|
+
startedTime = ts;
|
|
748
|
+
}
|
|
749
|
+
if (event.type === 'exited') {
|
|
750
|
+
exitedTime = ts;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (event.type === 'tokens' && data) {
|
|
754
|
+
const delta = parseTokenEvent(data);
|
|
755
|
+
totalInput += delta.input;
|
|
756
|
+
totalCachedInput += delta.cachedInput;
|
|
757
|
+
totalOutput += delta.output;
|
|
758
|
+
totalReasoning += delta.reasoning;
|
|
759
|
+
totalTokens += delta.total;
|
|
760
|
+
}
|
|
761
|
+
if (event.type === 'tool' && data) {
|
|
762
|
+
const tool = data.tool;
|
|
763
|
+
if (typeof tool === 'string' && FILE_CHANGE_TOOLS.has(tool.toLowerCase())) {
|
|
764
|
+
totalFileChanges++;
|
|
765
|
+
const filePath = extractFilePath(data);
|
|
766
|
+
if (filePath !== null)
|
|
767
|
+
uniqueFiles.add(filePath);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (event.type === 'file_changed' && data) {
|
|
771
|
+
totalFileChanges++;
|
|
772
|
+
const filePath = extractFilePath(data);
|
|
773
|
+
if (filePath !== null)
|
|
774
|
+
uniqueFiles.add(filePath);
|
|
775
|
+
if (Array.isArray(data.files)) {
|
|
776
|
+
for (const f of data.files) {
|
|
777
|
+
const normalized = normalizeFilePath(f);
|
|
778
|
+
if (normalized !== null)
|
|
779
|
+
uniqueFiles.add(normalized);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (!isChangeStream && (startedTime !== null || minTime !== null)) {
|
|
785
|
+
let taskDuration = 0;
|
|
786
|
+
if (startedTime !== null && exitedTime !== null && exitedTime >= startedTime) {
|
|
787
|
+
taskDuration = exitedTime - startedTime;
|
|
788
|
+
}
|
|
789
|
+
else if (minTime !== null && maxTime !== null && maxTime > minTime) {
|
|
790
|
+
taskDuration = maxTime - minTime;
|
|
791
|
+
}
|
|
792
|
+
if (taskDuration > 0) {
|
|
793
|
+
totalDurationMs += taskDuration;
|
|
794
|
+
tasksWithDurationCount++;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// 7. Planning telemetry: correlated `.run/plan.jsonl` pairs across every
|
|
800
|
+
// active and archived change. Only a valid start counts as a session.
|
|
801
|
+
let planningSessions = 0;
|
|
802
|
+
let changesWithPlanningRecords = 0;
|
|
803
|
+
let planningWallSeconds = 0;
|
|
804
|
+
const planningWallByChange = {};
|
|
805
|
+
let planningInput = null;
|
|
806
|
+
let planningOutput = null;
|
|
807
|
+
let planningCached = null;
|
|
808
|
+
let planningReasoning = null;
|
|
809
|
+
let planningCost = 0;
|
|
810
|
+
let planningReportedSessions = 0;
|
|
811
|
+
let planningCostReportedSessions = 0;
|
|
812
|
+
for (const folderPath of allSpecFolders) {
|
|
813
|
+
const changeId = path.basename(folderPath);
|
|
814
|
+
let changeHasPlanningRecord = false;
|
|
815
|
+
for (const session of await readPlanningSessions(folderPath)) {
|
|
816
|
+
if (!session.started)
|
|
817
|
+
continue;
|
|
818
|
+
planningSessions++;
|
|
819
|
+
changeHasPlanningRecord = true;
|
|
820
|
+
planningWallByChange[changeId] ??= 0;
|
|
821
|
+
const exited = session.exited;
|
|
822
|
+
if (!exited)
|
|
823
|
+
continue;
|
|
824
|
+
planningWallSeconds += exited.data.wallSeconds;
|
|
825
|
+
planningWallByChange[changeId] += exited.data.wallSeconds;
|
|
826
|
+
const usage = exited.data.usage;
|
|
827
|
+
let reported = false;
|
|
828
|
+
if (usage.inputTokens !== null) {
|
|
829
|
+
planningInput = (planningInput ?? 0) + usage.inputTokens;
|
|
830
|
+
reported = true;
|
|
831
|
+
}
|
|
832
|
+
if (usage.outputTokens !== null) {
|
|
833
|
+
planningOutput = (planningOutput ?? 0) + usage.outputTokens;
|
|
834
|
+
reported = true;
|
|
835
|
+
}
|
|
836
|
+
if (usage.cachedTokens !== null) {
|
|
837
|
+
planningCached = (planningCached ?? 0) + usage.cachedTokens;
|
|
838
|
+
reported = true;
|
|
839
|
+
}
|
|
840
|
+
if (usage.reasoningTokens !== null) {
|
|
841
|
+
planningReasoning = (planningReasoning ?? 0) + usage.reasoningTokens;
|
|
842
|
+
reported = true;
|
|
843
|
+
}
|
|
844
|
+
if (usage.cost !== null) {
|
|
845
|
+
planningCost += usage.cost;
|
|
846
|
+
reported = true;
|
|
847
|
+
planningCostReportedSessions++;
|
|
848
|
+
}
|
|
849
|
+
if (reported)
|
|
850
|
+
planningReportedSessions++;
|
|
851
|
+
}
|
|
852
|
+
if (changeHasPlanningRecord)
|
|
853
|
+
changesWithPlanningRecords++;
|
|
854
|
+
}
|
|
855
|
+
const planningWallSecondsByChange = Object.fromEntries(Object.entries(planningWallByChange).sort(([a], [b]) => a.localeCompare(b)));
|
|
856
|
+
const planningByChange = await computePlanningByChange(allSpecFolders);
|
|
857
|
+
const planningComparison = buildPlanningComparison({
|
|
858
|
+
input: planningInput,
|
|
859
|
+
output: planningOutput,
|
|
860
|
+
cached: planningCached,
|
|
861
|
+
reasoning: planningReasoning,
|
|
862
|
+
cost: planningCostReportedSessions > 0 ? planningCost : null,
|
|
863
|
+
}, {
|
|
864
|
+
input: totalInput,
|
|
865
|
+
output: totalOutput,
|
|
866
|
+
cached: totalCachedInput,
|
|
867
|
+
reasoning: totalReasoning,
|
|
868
|
+
cost: reportedCostAttempts > 0 || totalCost !== 0 ? totalCost : null,
|
|
869
|
+
});
|
|
870
|
+
// 8. Archived change cycle time. Only archived folders get a row; every phase
|
|
871
|
+
// is null unless both endpoints exist, parse, and are ordered correctly.
|
|
872
|
+
const cycleRows = [];
|
|
873
|
+
for (const folderPath of archivedFolders) {
|
|
874
|
+
const change = path.basename(folderPath);
|
|
875
|
+
const briefMs = await readBriefDateMs(folderPath);
|
|
876
|
+
const approvedMs = await readApprovedAtMs(folderPath);
|
|
877
|
+
const firstStartMs = await readFirstTaskStartMs(folderPath);
|
|
878
|
+
const archivedMs = await readArchivedAtMs(folderPath);
|
|
879
|
+
const briefToApprovalSeconds = phaseSeconds(briefMs, approvedMs);
|
|
880
|
+
const approvalToFirstTaskSeconds = phaseSeconds(approvedMs, firstStartMs);
|
|
881
|
+
const firstTaskToArchiveSeconds = phaseSeconds(firstStartMs, archivedMs);
|
|
882
|
+
const totalSeconds = briefToApprovalSeconds !== null &&
|
|
883
|
+
approvalToFirstTaskSeconds !== null &&
|
|
884
|
+
firstTaskToArchiveSeconds !== null
|
|
885
|
+
? briefToApprovalSeconds + approvalToFirstTaskSeconds + firstTaskToArchiveSeconds
|
|
886
|
+
: null;
|
|
887
|
+
cycleRows.push({
|
|
888
|
+
change,
|
|
889
|
+
briefToApprovalSeconds,
|
|
890
|
+
approvalToFirstTaskSeconds,
|
|
891
|
+
firstTaskToArchiveSeconds,
|
|
892
|
+
totalSeconds,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
cycleRows.sort((a, b) => a.change.localeCompare(b.change));
|
|
896
|
+
const cyclePhases = {
|
|
897
|
+
briefToApproval: aggregatePhase(cycleRows, 'briefToApprovalSeconds'),
|
|
898
|
+
approvalToFirstTask: aggregatePhase(cycleRows, 'approvalToFirstTaskSeconds'),
|
|
899
|
+
firstTaskToArchive: aggregatePhase(cycleRows, 'firstTaskToArchiveSeconds'),
|
|
900
|
+
total: aggregatePhase(cycleRows, 'totalSeconds'),
|
|
901
|
+
};
|
|
902
|
+
const activeSpecs = activeFolders.length;
|
|
903
|
+
const archivedSpecs = archivedFolders.length;
|
|
904
|
+
const totalSpecs = activeSpecs + archivedSpecs;
|
|
905
|
+
const completionRate = totalTasks > 0 ? Math.round((doneTasks / totalTasks) * 10000) / 100 : 0;
|
|
906
|
+
const avgDurationMs = tasksWithDurationCount > 0 ? Math.round(totalDurationMs / tasksWithDurationCount) : 0;
|
|
907
|
+
const totalSeconds = Math.round(totalDurationMs / 1000);
|
|
908
|
+
const avgSeconds = Math.round(avgDurationMs / 1000);
|
|
909
|
+
const formattedTotal = formatDuration(totalDurationMs);
|
|
910
|
+
const formattedAvg = formatDuration(avgDurationMs);
|
|
911
|
+
const queue = await readQueueReport(projectRoot, config);
|
|
912
|
+
const approvalFlags = await collectApprovalFlagOutcomes(allSpecFolders);
|
|
913
|
+
const measuredTasks = await projectMeasuredTasks(allSpecFolders);
|
|
914
|
+
const sizes = {
|
|
915
|
+
scopeFileSeries: deriveScopeFileSeries(measuredTasks),
|
|
916
|
+
byAcceptanceLines: aggregateSizeBuckets(measuredTasks, 'acceptanceLines'),
|
|
917
|
+
};
|
|
918
|
+
const perSpec = {};
|
|
919
|
+
for (const [spec, value] of Object.entries(perSpecCost)) {
|
|
920
|
+
if (value > 0)
|
|
921
|
+
perSpec[spec] = value;
|
|
922
|
+
}
|
|
923
|
+
const sortedPerSpec = Object.fromEntries(Object.entries(perSpec).sort(([a], [b]) => a.localeCompare(b)));
|
|
924
|
+
return {
|
|
925
|
+
approvalFlags,
|
|
926
|
+
specs: {
|
|
927
|
+
total: totalSpecs,
|
|
928
|
+
active: activeSpecs,
|
|
929
|
+
archived: archivedSpecs,
|
|
930
|
+
},
|
|
931
|
+
now: {
|
|
932
|
+
total: totalTasks,
|
|
933
|
+
done: doneTasks,
|
|
934
|
+
verified: verifiedTasks,
|
|
935
|
+
manual: manualTasks,
|
|
936
|
+
dead: deadTasks,
|
|
937
|
+
regressed: regressedTasks,
|
|
938
|
+
running: runningTasks,
|
|
939
|
+
pending: pendingTasks,
|
|
940
|
+
unmarked: unmarkedTasks,
|
|
941
|
+
},
|
|
942
|
+
completionRate,
|
|
943
|
+
history: {
|
|
944
|
+
attempts: {
|
|
945
|
+
total: attemptsTotal,
|
|
946
|
+
byTask: attemptsByTask,
|
|
947
|
+
multipleAttempts,
|
|
948
|
+
},
|
|
949
|
+
deadByReason,
|
|
950
|
+
unexplainedReruns: {
|
|
951
|
+
total: unexplainedTotal,
|
|
952
|
+
byTask: unexplainedByTask,
|
|
953
|
+
},
|
|
954
|
+
verifyRuns: {
|
|
955
|
+
total: verifyTotal,
|
|
956
|
+
missingExitCode: verifyMissingExitCode,
|
|
957
|
+
byTask: verifyByTask,
|
|
958
|
+
},
|
|
959
|
+
preSpawnVerify: {
|
|
960
|
+
runs: preSpawnRuns,
|
|
961
|
+
mismatches: preSpawnMismatches,
|
|
962
|
+
mismatchedTasks: preSpawnMismatchedTasks,
|
|
963
|
+
missingPathRuns: preSpawnMissingPathRuns,
|
|
964
|
+
byStart: preSpawnByStart,
|
|
965
|
+
},
|
|
966
|
+
cost: {
|
|
967
|
+
total: totalCost,
|
|
968
|
+
perSpec: sortedPerSpec,
|
|
969
|
+
formattedTotal: formatReportedCost(totalCost, reportedCostAttempts),
|
|
970
|
+
provenance: 'harness-reported',
|
|
971
|
+
coverage: {
|
|
972
|
+
reportedAttempts: reportedCostAttempts,
|
|
973
|
+
totalAttempts: attemptsTotal,
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
retries,
|
|
977
|
+
rejections: {
|
|
978
|
+
total: rejectionTotal,
|
|
979
|
+
byPlannerModel: rejectionsByPlannerModel,
|
|
980
|
+
},
|
|
981
|
+
sizes,
|
|
982
|
+
scopeRegressions: {
|
|
983
|
+
detected: scopeDetected,
|
|
984
|
+
verificationPassedAtDetection: scopeVerificationPassed,
|
|
985
|
+
verificationFailedAtDetection: scopeVerificationFailed,
|
|
986
|
+
recertifiedByHuman: scopeRecertifiedByHuman,
|
|
987
|
+
requeuedForAgent: scopeRequeuedForAgent,
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
coverage: {
|
|
991
|
+
withEvents: withEventsCount,
|
|
992
|
+
withoutEvents: withoutEventsCount,
|
|
993
|
+
byChange: coverageByChange,
|
|
994
|
+
},
|
|
995
|
+
cycle: {
|
|
996
|
+
phases: cyclePhases,
|
|
997
|
+
byChange: cycleRows,
|
|
998
|
+
},
|
|
999
|
+
durations: {
|
|
1000
|
+
totalMs: totalDurationMs,
|
|
1001
|
+
totalSeconds,
|
|
1002
|
+
avgMs: avgDurationMs,
|
|
1003
|
+
avgSeconds,
|
|
1004
|
+
formattedTotal,
|
|
1005
|
+
formattedAvg,
|
|
1006
|
+
},
|
|
1007
|
+
tokens: {
|
|
1008
|
+
input: totalInput,
|
|
1009
|
+
cached_input: totalCachedInput,
|
|
1010
|
+
output: totalOutput,
|
|
1011
|
+
reasoning: totalReasoning,
|
|
1012
|
+
total: totalTokens,
|
|
1013
|
+
cacheSharePercent: totalInput + totalCachedInput > 0
|
|
1014
|
+
? Math.round((totalCachedInput / (totalInput + totalCachedInput)) * 1000) / 10
|
|
1015
|
+
: 0,
|
|
1016
|
+
},
|
|
1017
|
+
fileChanges: {
|
|
1018
|
+
totalChanges: totalFileChanges,
|
|
1019
|
+
uniqueFiles: Array.from(uniqueFiles).sort(),
|
|
1020
|
+
uniqueCount: uniqueFiles.size,
|
|
1021
|
+
},
|
|
1022
|
+
planning: {
|
|
1023
|
+
sessions: planningSessions,
|
|
1024
|
+
changesWithPlanningRecords,
|
|
1025
|
+
wallSeconds: planningWallSeconds,
|
|
1026
|
+
wallSecondsByChange: planningWallSecondsByChange,
|
|
1027
|
+
tokens: {
|
|
1028
|
+
input: planningInput ?? 0,
|
|
1029
|
+
output: planningOutput ?? 0,
|
|
1030
|
+
cached: planningCached ?? 0,
|
|
1031
|
+
reasoning: planningReasoning ?? 0,
|
|
1032
|
+
},
|
|
1033
|
+
cost: {
|
|
1034
|
+
total: planningCost,
|
|
1035
|
+
formattedTotal: formatReportedCost(planningCost, planningCostReportedSessions),
|
|
1036
|
+
provenance: 'harness-reported',
|
|
1037
|
+
},
|
|
1038
|
+
coverage: {
|
|
1039
|
+
reportedSessions: planningReportedSessions,
|
|
1040
|
+
totalSessions: planningSessions,
|
|
1041
|
+
},
|
|
1042
|
+
byChange: planningByChange,
|
|
1043
|
+
comparison: planningComparison,
|
|
1044
|
+
},
|
|
1045
|
+
queue,
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Alias for {@link getMetricsReport} matching the report-generation name used
|
|
1050
|
+
* by the metrics-and-reporting spec.
|
|
1051
|
+
*/
|
|
1052
|
+
export const generateReport = getMetricsReport;
|
|
1053
|
+
export const REPOSITORY_RECORD_MIN_MEASURED_TASKS = 5;
|
|
1054
|
+
export const REPOSITORY_RECORD_MAX_ARCHIVED_CHANGES = 20;
|
|
1055
|
+
export const REPOSITORY_RECORD_MAX_DEAD_OUTCOMES = 10;
|
|
1056
|
+
function numericPrefix(name) {
|
|
1057
|
+
const match = name.match(/^(\d+)/);
|
|
1058
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
1059
|
+
}
|
|
1060
|
+
/** Canonical archive folders, newest numeric first, hidden entries excluded. */
|
|
1061
|
+
async function listRecentArchiveFolders(archiveDir, limit) {
|
|
1062
|
+
let entries = [];
|
|
1063
|
+
try {
|
|
1064
|
+
entries = await fs.readdir(archiveDir);
|
|
1065
|
+
}
|
|
1066
|
+
catch {
|
|
1067
|
+
return [];
|
|
1068
|
+
}
|
|
1069
|
+
const folders = [];
|
|
1070
|
+
for (const entry of entries) {
|
|
1071
|
+
if (entry.startsWith('.') || entry.startsWith('_'))
|
|
1072
|
+
continue;
|
|
1073
|
+
const fullPath = path.join(archiveDir, entry);
|
|
1074
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
1075
|
+
if (stat?.isDirectory())
|
|
1076
|
+
folders.push(fullPath);
|
|
1077
|
+
}
|
|
1078
|
+
folders.sort((a, b) => {
|
|
1079
|
+
const nameA = path.basename(a);
|
|
1080
|
+
const nameB = path.basename(b);
|
|
1081
|
+
const numA = numericPrefix(nameA);
|
|
1082
|
+
const numB = numericPrefix(nameB);
|
|
1083
|
+
if (numA !== null && numB !== null) {
|
|
1084
|
+
if (numA !== numB)
|
|
1085
|
+
return numB - numA;
|
|
1086
|
+
return nameA.localeCompare(nameB);
|
|
1087
|
+
}
|
|
1088
|
+
if (numA !== null)
|
|
1089
|
+
return -1;
|
|
1090
|
+
if (numB !== null)
|
|
1091
|
+
return 1;
|
|
1092
|
+
return nameA.localeCompare(nameB);
|
|
1093
|
+
});
|
|
1094
|
+
return folders.slice(0, limit);
|
|
1095
|
+
}
|
|
1096
|
+
/** Every typed `dead` event in the window, in change/task/event order. */
|
|
1097
|
+
async function collectDeadOutcomes(folders) {
|
|
1098
|
+
const outcomes = [];
|
|
1099
|
+
for (const folderPath of folders) {
|
|
1100
|
+
const change = path.basename(folderPath);
|
|
1101
|
+
const tasksDir = path.join(folderPath, 'tasks');
|
|
1102
|
+
let taskFiles = [];
|
|
1103
|
+
try {
|
|
1104
|
+
taskFiles = (await fs.readdir(tasksDir))
|
|
1105
|
+
.filter((entry) => entry.endsWith('.md'))
|
|
1106
|
+
.sort(compareNumericPrefix);
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
for (const taskFile of taskFiles) {
|
|
1112
|
+
const taskNumber = taskFile.replace(/\.md$/, '');
|
|
1113
|
+
const eventContent = await fs
|
|
1114
|
+
.readFile(path.join(folderPath, '.run', 'events', `${taskNumber}.jsonl`), 'utf8')
|
|
1115
|
+
.catch(() => null);
|
|
1116
|
+
if (eventContent === null)
|
|
1117
|
+
continue;
|
|
1118
|
+
const metadata = await readTaskMetadata(folderPath, taskNumber);
|
|
1119
|
+
for (const event of parseEventLines(eventContent)) {
|
|
1120
|
+
if (event.type !== 'dead')
|
|
1121
|
+
continue;
|
|
1122
|
+
const rawReason = asData(event)?.reason;
|
|
1123
|
+
const reason = typeof rawReason === 'string' && rawReason.trim() ? rawReason.trim() : 'unknown';
|
|
1124
|
+
outcomes.push({ change, title: metadata.title, reason });
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return outcomes;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Shared bounded repository record: at most the 20 newest canonical archived
|
|
1132
|
+
* changes by numeric id, with measured outcomes and up to ten dead rows.
|
|
1133
|
+
*/
|
|
1134
|
+
export async function getRepositoryRecord(projectRoot, config = DEFAULT_CONFIG) {
|
|
1135
|
+
const archiveDir = getArchiveDir(config.paths.openspecRoot, projectRoot);
|
|
1136
|
+
const folders = await listRecentArchiveFolders(archiveDir, REPOSITORY_RECORD_MAX_ARCHIVED_CHANGES);
|
|
1137
|
+
const measured = await projectMeasuredTasks(folders);
|
|
1138
|
+
const deadOutcomes = await collectDeadOutcomes(folders);
|
|
1139
|
+
const passed = measured.filter((task) => task.firstAttemptPass).length;
|
|
1140
|
+
const durations = measured
|
|
1141
|
+
.map((task) => task.durationSeconds)
|
|
1142
|
+
.filter((value) => value !== null);
|
|
1143
|
+
// Scope-sized evidence uses exactly one resolver generation: resolver-2 when
|
|
1144
|
+
// the archive window has any, otherwise the labeled legacy fallback. Malformed
|
|
1145
|
+
// resolver versions never contribute a cross-generation comparison.
|
|
1146
|
+
const resolver2Tasks = measured.filter((task) => scopeFileGeneration(task) === 'resolver-2');
|
|
1147
|
+
const legacyTasks = measured.filter((task) => scopeFileGeneration(task) === 'legacy');
|
|
1148
|
+
const useResolver2 = resolver2Tasks.length > 0;
|
|
1149
|
+
const scopeTasks = useResolver2 ? resolver2Tasks : legacyTasks;
|
|
1150
|
+
const scopeFileResolver = scopeTasks.length > 0 ? (useResolver2 ? 'resolver-2' : 'legacy') : null;
|
|
1151
|
+
return {
|
|
1152
|
+
measuredTasks: measured.length,
|
|
1153
|
+
firstAttemptPassRate: measured.length > 0 ? round2(passed / measured.length) : 0,
|
|
1154
|
+
medianDurationSeconds: medianSeconds(durations),
|
|
1155
|
+
largestFirstAttemptPass: selectLargestFirstAttemptPass(scopeTasks),
|
|
1156
|
+
scopeFileResolver,
|
|
1157
|
+
deadOutcomes: deadOutcomes.slice(0, REPOSITORY_RECORD_MAX_DEAD_OUTCOMES),
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Body of the planning prompt's repository-record section. The caller owns the
|
|
1162
|
+
* heading. Fewer than five measured tasks print only the too-small sentence.
|
|
1163
|
+
*/
|
|
1164
|
+
export function formatRepositoryRecordBody(record) {
|
|
1165
|
+
if (record.measuredTasks < REPOSITORY_RECORD_MIN_MEASURED_TASKS) {
|
|
1166
|
+
return `This repository's measured record is too small (fewer than ${REPOSITORY_RECORD_MIN_MEASURED_TASKS} tasks).`;
|
|
1167
|
+
}
|
|
1168
|
+
const lines = [];
|
|
1169
|
+
lines.push(`First-attempt pass rate: ${record.firstAttemptPassRate}`);
|
|
1170
|
+
const largest = record.largestFirstAttemptPass;
|
|
1171
|
+
if (largest) {
|
|
1172
|
+
const scopeLabel = record.scopeFileResolver === 'legacy'
|
|
1173
|
+
? ' [legacy scope]'
|
|
1174
|
+
: record.scopeFileResolver === 'resolver-2'
|
|
1175
|
+
? ' [resolver 2 scope]'
|
|
1176
|
+
: '';
|
|
1177
|
+
lines.push(`Largest first-attempt pass: ${largest.change}/${largest.task} "${largest.title}" (scope files: ${largest.scopeFiles}, acceptance lines: ${largest.acceptanceLines})${scopeLabel}`);
|
|
1178
|
+
}
|
|
1179
|
+
else {
|
|
1180
|
+
lines.push('Largest first-attempt pass: unavailable');
|
|
1181
|
+
}
|
|
1182
|
+
lines.push(`Median task duration: ${record.medianDurationSeconds === null
|
|
1183
|
+
? 'unavailable'
|
|
1184
|
+
: formatDuration(record.medianDurationSeconds * 1000)}`);
|
|
1185
|
+
lines.push('Dead outcomes:');
|
|
1186
|
+
if (record.deadOutcomes.length === 0) {
|
|
1187
|
+
lines.push(' (none)');
|
|
1188
|
+
}
|
|
1189
|
+
else {
|
|
1190
|
+
for (const outcome of record.deadOutcomes) {
|
|
1191
|
+
lines.push(`- ${outcome.change}, ${outcome.title}, ${outcome.reason}`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return lines.join('\n');
|
|
1195
|
+
}
|
|
1196
|
+
/** A nullable measured value: `unavailable` when nothing reported it. */
|
|
1197
|
+
function formatOptionalValue(value) {
|
|
1198
|
+
return value === null ? 'unavailable' : String(value);
|
|
1199
|
+
}
|
|
1200
|
+
/** A nullable comparison cost: `not reported` when nothing on its side did. */
|
|
1201
|
+
function formatComparisonCost(cost) {
|
|
1202
|
+
return formatReportedCost(cost ?? 0, cost === null ? 0 : 1);
|
|
1203
|
+
}
|
|
1204
|
+
/** A nullable comparison token total: `not reported` when nothing on its side did. */
|
|
1205
|
+
function formatComparisonToken(value) {
|
|
1206
|
+
return value === null ? 'not reported' : String(value);
|
|
1207
|
+
}
|
|
1208
|
+
/** One `Planning by change:` row for a single change's economics. */
|
|
1209
|
+
function formatPlanningChangeLine(entry) {
|
|
1210
|
+
const tokens = entry.tokens;
|
|
1211
|
+
return [
|
|
1212
|
+
`sessions ${entry.sessions}`,
|
|
1213
|
+
`active ${formatOptionalValue(entry.activeMinutes)} min`,
|
|
1214
|
+
`tokens in ${formatOptionalValue(tokens.input)} out ${formatOptionalValue(tokens.output)} cache-read ${formatOptionalValue(tokens.cacheRead)} cache-write ${formatOptionalValue(tokens.cacheWrite)} reasoning ${formatOptionalValue(tokens.reasoning)}`,
|
|
1215
|
+
`cost ${formatComparisonCost(entry.cost)}`,
|
|
1216
|
+
`spec words ${formatOptionalValue(entry.specWords)}`,
|
|
1217
|
+
`changed lines ${formatOptionalValue(entry.changedLines)} (${formatOptionalValue(entry.specWordsPerChangedLine)} words/line)`,
|
|
1218
|
+
`last edit to approval ${formatOptionalValue(entry.minutesLastEditToApproval)} min`,
|
|
1219
|
+
].join(', ');
|
|
1220
|
+
}
|
|
1221
|
+
export function formatMetricsReport(report, config = DEFAULT_CONFIG) {
|
|
1222
|
+
const lines = [];
|
|
1223
|
+
lines.push('osq Delivery Metrics Report');
|
|
1224
|
+
lines.push('');
|
|
1225
|
+
lines.push('Specs Summary:');
|
|
1226
|
+
lines.push(` Total specs: ${report.specs.total} (${report.specs.active} active, ${report.specs.archived} archived)`);
|
|
1227
|
+
lines.push('');
|
|
1228
|
+
lines.push('Now:');
|
|
1229
|
+
lines.push(` Total tasks: ${report.now.total}`);
|
|
1230
|
+
lines.push(` Done: ${report.now.done}`);
|
|
1231
|
+
lines.push(` Verified done: ${report.now.verified}`);
|
|
1232
|
+
lines.push(` Manual done: ${report.now.manual}`);
|
|
1233
|
+
lines.push(` Dead: ${report.now.dead}`);
|
|
1234
|
+
lines.push(` Regressed: ${report.now.regressed}`);
|
|
1235
|
+
lines.push(` Running: ${report.now.running}`);
|
|
1236
|
+
lines.push(` Pending: ${report.now.pending}`);
|
|
1237
|
+
lines.push(` Unmarked: ${report.now.unmarked}`);
|
|
1238
|
+
lines.push(` Completion rate: ${report.completionRate}%`);
|
|
1239
|
+
lines.push('');
|
|
1240
|
+
lines.push('History:');
|
|
1241
|
+
lines.push(` Total attempts: ${report.history.attempts.total}`);
|
|
1242
|
+
lines.push(` Tasks with multiple attempts: ${report.history.attempts.multipleAttempts.length}`);
|
|
1243
|
+
lines.push(` Unexplained re-runs: ${report.history.unexplainedReruns.total}`);
|
|
1244
|
+
lines.push(` Verification runs: ${report.history.verifyRuns.total}`);
|
|
1245
|
+
lines.push(` Verification runs missing exit code: ${report.history.verifyRuns.missingExitCode}`);
|
|
1246
|
+
lines.push(` Pre-spawn verify mismatches: ${report.history.preSpawnVerify.mismatches} of ${report.history.preSpawnVerify.runs} runs`);
|
|
1247
|
+
lines.push(` Pre-spawn verify with missing paths: ${report.history.preSpawnVerify.missingPathRuns} of ${report.history.preSpawnVerify.runs} runs`);
|
|
1248
|
+
const preSpawnByStart = report.history.preSpawnVerify.byStart;
|
|
1249
|
+
lines.push(` Pre-spawn verify by declared start: red ${preSpawnByStart.red.passed} of ${preSpawnByStart.red.runs} passed, green ${preSpawnByStart.green.passed} of ${preSpawnByStart.green.runs} passed, any ${preSpawnByStart.any.passed} of ${preSpawnByStart.any.runs} passed`);
|
|
1250
|
+
lines.push(...formatRetryHistoryLines(report.history.retries));
|
|
1251
|
+
lines.push(` Harness-reported cost: ${report.history.cost.formattedTotal} (${report.history.cost.coverage.reportedAttempts} of ${report.history.cost.coverage.totalAttempts} attempts reported cost)`);
|
|
1252
|
+
lines.push(' Dead by reason:');
|
|
1253
|
+
const deadEntries = Object.entries(report.history.deadByReason).sort(([a], [b]) => a.localeCompare(b));
|
|
1254
|
+
if (deadEntries.length === 0) {
|
|
1255
|
+
lines.push(' (none)');
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
for (const [reason, count] of deadEntries) {
|
|
1259
|
+
lines.push(` ${reason}: ${count}`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
lines.push(` Rejections: ${report.history.rejections.total}`);
|
|
1263
|
+
lines.push(' Rejections by planner model:');
|
|
1264
|
+
const rejectionEntries = Object.entries(report.history.rejections.byPlannerModel).sort(([a], [b]) => a.localeCompare(b));
|
|
1265
|
+
if (rejectionEntries.length === 0) {
|
|
1266
|
+
lines.push(' (none)');
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
for (const [model, count] of rejectionEntries) {
|
|
1270
|
+
lines.push(` ${model}: ${count}`);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
lines.push(' Scope regressions:');
|
|
1274
|
+
lines.push(` Detected: ${report.history.scopeRegressions.detected}`);
|
|
1275
|
+
lines.push(` Verification passed at detection: ${report.history.scopeRegressions.verificationPassedAtDetection}`);
|
|
1276
|
+
lines.push(` Verification failed at detection: ${report.history.scopeRegressions.verificationFailedAtDetection}`);
|
|
1277
|
+
lines.push(` Recertified by human: ${report.history.scopeRegressions.recertifiedByHuman}`);
|
|
1278
|
+
lines.push(` Requeued for agent: ${report.history.scopeRegressions.requeuedForAgent}`);
|
|
1279
|
+
const scopeSeries = report.history.sizes.scopeFileSeries;
|
|
1280
|
+
const legacySeries = scopeSeries.find((entry) => entry.resolver === 'legacy');
|
|
1281
|
+
const resolver2Series = scopeSeries.find((entry) => entry.resolver === 'resolver-2');
|
|
1282
|
+
lines.push(' Size by scope files (legacy):');
|
|
1283
|
+
lines.push(...formatSizeBucketLines(legacySeries?.byScopeFiles ?? []));
|
|
1284
|
+
lines.push(' Size by scope files (resolver-2):');
|
|
1285
|
+
lines.push(...formatSizeBucketLines(resolver2Series?.byScopeFiles ?? []));
|
|
1286
|
+
lines.push(` Resolver 2 starts at: ${resolver2Series?.startsAtChange ?? 'unavailable'}`);
|
|
1287
|
+
lines.push(' Size by acceptance lines:');
|
|
1288
|
+
lines.push(...formatSizeBucketLines(report.history.sizes.byAcceptanceLines));
|
|
1289
|
+
const largestPass = selectHintSeries(scopeSeries)?.largestFirstAttemptPass ?? null;
|
|
1290
|
+
if (largestPass) {
|
|
1291
|
+
const matching = [];
|
|
1292
|
+
if (Math.abs(config.limits.maxScopeFiles - largestPass.scopeFiles) <= 1) {
|
|
1293
|
+
matching.push(`scope files ${largestPass.scopeFiles} is within 1 of configured maxScopeFiles ${config.limits.maxScopeFiles}`);
|
|
1294
|
+
}
|
|
1295
|
+
if (Math.abs(config.limits.maxAcceptanceLines - largestPass.acceptanceLines) <= 1) {
|
|
1296
|
+
matching.push(`acceptance lines ${largestPass.acceptanceLines} is within 1 of configured maxAcceptanceLines ${config.limits.maxAcceptanceLines}`);
|
|
1297
|
+
}
|
|
1298
|
+
if (matching.length > 0) {
|
|
1299
|
+
lines.push(` Size hint: largest first-attempt pass ${largestPass.change}/${largestPass.task} — ${matching.join('; ')}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
lines.push('');
|
|
1303
|
+
lines.push('Coverage:');
|
|
1304
|
+
lines.push(` Tasks with event files: ${report.coverage.withEvents}`);
|
|
1305
|
+
lines.push(` Tasks without event files: ${report.coverage.withoutEvents}`);
|
|
1306
|
+
lines.push('');
|
|
1307
|
+
lines.push('Planning:');
|
|
1308
|
+
lines.push(` Sessions: ${report.planning.sessions}`);
|
|
1309
|
+
lines.push(` Total wall time: ${formatDuration(report.planning.wallSeconds * 1000)}`);
|
|
1310
|
+
lines.push(' Wall time by change:');
|
|
1311
|
+
const wallEntries = Object.entries(report.planning.wallSecondsByChange).sort(([a], [b]) => a.localeCompare(b));
|
|
1312
|
+
if (wallEntries.length === 0) {
|
|
1313
|
+
lines.push(' (none)');
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
for (const [change, seconds] of wallEntries) {
|
|
1317
|
+
lines.push(` ${change}: ${formatDuration(seconds * 1000)}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
lines.push(` Input tokens: ${report.planning.tokens.input}`);
|
|
1321
|
+
lines.push(` Output tokens: ${report.planning.tokens.output}`);
|
|
1322
|
+
lines.push(` Cached tokens: ${report.planning.tokens.cached}`);
|
|
1323
|
+
lines.push(` Reasoning tokens: ${report.planning.tokens.reasoning}`);
|
|
1324
|
+
lines.push(` Harness-reported cost: ${report.planning.cost.formattedTotal}`);
|
|
1325
|
+
lines.push(` ${report.planning.coverage.reportedSessions} of ${report.planning.coverage.totalSessions} sessions reported usage`);
|
|
1326
|
+
lines.push(` ${report.planning.changesWithPlanningRecords} changes have a planning record`);
|
|
1327
|
+
const planningChangeEntries = Object.entries(report.planning.byChange).sort(([a], [b]) => a.localeCompare(b));
|
|
1328
|
+
// A repository with no planning at all prints only the totals above; the
|
|
1329
|
+
// per-change and comparison sections need at least one planning record.
|
|
1330
|
+
if (report.planning.sessions > 0 || planningChangeEntries.length > 0) {
|
|
1331
|
+
lines.push('');
|
|
1332
|
+
lines.push('Planning by change:');
|
|
1333
|
+
if (planningChangeEntries.length === 0) {
|
|
1334
|
+
lines.push(' (none)');
|
|
1335
|
+
}
|
|
1336
|
+
else {
|
|
1337
|
+
for (const [change, entry] of planningChangeEntries) {
|
|
1338
|
+
lines.push(` ${change}: ${formatPlanningChangeLine(entry)}`);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
lines.push('Planning vs execution:');
|
|
1342
|
+
const planningSide = report.planning.comparison.planning;
|
|
1343
|
+
const executionSide = report.planning.comparison.execution;
|
|
1344
|
+
lines.push(` Input tokens: planning ${formatComparisonToken(planningSide.input)}, execution ${formatComparisonToken(executionSide.input)}`);
|
|
1345
|
+
lines.push(` Output tokens: planning ${formatComparisonToken(planningSide.output)}, execution ${formatComparisonToken(executionSide.output)}`);
|
|
1346
|
+
lines.push(` Cached tokens: planning ${formatComparisonToken(planningSide.cached)}, execution ${formatComparisonToken(executionSide.cached)}`);
|
|
1347
|
+
lines.push(` Reasoning tokens: planning ${formatComparisonToken(planningSide.reasoning)}, execution ${formatComparisonToken(executionSide.reasoning)}`);
|
|
1348
|
+
lines.push(` Cost: planning ${formatComparisonCost(planningSide.cost)}, execution ${formatComparisonCost(executionSide.cost)}`);
|
|
1349
|
+
}
|
|
1350
|
+
lines.push('');
|
|
1351
|
+
lines.push('Approval flags:');
|
|
1352
|
+
lines.push(...formatApprovalFlagOutcomes(report.approvalFlags));
|
|
1353
|
+
lines.push('');
|
|
1354
|
+
lines.push('Cycle:');
|
|
1355
|
+
const cyclePhaseLines = [
|
|
1356
|
+
['Brief to approval', report.cycle.phases.briefToApproval],
|
|
1357
|
+
['Approval to first task', report.cycle.phases.approvalToFirstTask],
|
|
1358
|
+
['First task to archive', report.cycle.phases.firstTaskToArchive],
|
|
1359
|
+
['Total', report.cycle.phases.total],
|
|
1360
|
+
];
|
|
1361
|
+
for (const [label, phase] of cyclePhaseLines) {
|
|
1362
|
+
lines.push(` ${label}: total ${formatDuration(phase.totalSeconds * 1000)}, average ${formatDuration(phase.averageSeconds * 1000)} (${phase.coveredChanges} of ${phase.totalChanges} archived changes)`);
|
|
1363
|
+
}
|
|
1364
|
+
lines.push('');
|
|
1365
|
+
lines.push('Queue:');
|
|
1366
|
+
const queue = report.queue;
|
|
1367
|
+
if (!queue?.configured) {
|
|
1368
|
+
lines.push(' (not configured)');
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
lines.push(` Landed: ${queue.landed} of ${queue.total} items`);
|
|
1372
|
+
const queueCost = formatReportedCost(queue.planning.cost, queuePlanningCostReported(queue) ? 1 : 0);
|
|
1373
|
+
lines.push(` Planning sessions: ${queue.planning.sessions}, recorded cost: ${queueCost}`);
|
|
1374
|
+
lines.push(` Cost coverage: ${queue.planning.costCoverageComplete ? 'complete' : 'incomplete'}`);
|
|
1375
|
+
lines.push(' Covered item wall times:');
|
|
1376
|
+
const timedItems = queue.items.filter((item) => item.plannedToLandedSeconds !== null);
|
|
1377
|
+
if (timedItems.length === 0) {
|
|
1378
|
+
lines.push(' (none)');
|
|
1379
|
+
}
|
|
1380
|
+
else {
|
|
1381
|
+
for (const item of timedItems) {
|
|
1382
|
+
lines.push(` ${item.slug}: ${formatDuration(item.plannedToLandedSeconds * 1000)}`);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
lines.push(' Active failures:');
|
|
1386
|
+
if (queue.failures.length === 0) {
|
|
1387
|
+
lines.push(' (none)');
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
for (const failure of queue.failures) {
|
|
1391
|
+
lines.push(` ${failure.change} ${failure.target} (reason: ${failure.reason})`);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
lines.push(' Retained rejections:');
|
|
1395
|
+
if (queue.rejections.length === 0) {
|
|
1396
|
+
lines.push(' (none)');
|
|
1397
|
+
}
|
|
1398
|
+
else {
|
|
1399
|
+
for (const rejection of queue.rejections) {
|
|
1400
|
+
lines.push(` ${rejection.slug} ${rejection.change} (reason: ${rejection.reason}, at: ${rejection.timestamp ?? 'unavailable'})`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
lines.push('');
|
|
1405
|
+
lines.push('Execution Durations:');
|
|
1406
|
+
lines.push(` Total duration: ${report.durations.formattedTotal}`);
|
|
1407
|
+
lines.push(` Average duration: ${report.durations.formattedAvg}`);
|
|
1408
|
+
lines.push('');
|
|
1409
|
+
lines.push('Token Usage:');
|
|
1410
|
+
lines.push(` Input: ${report.tokens.input}`);
|
|
1411
|
+
lines.push(` Cached input: ${report.tokens.cached_input} (${report.tokens.cacheSharePercent}% cache share)`);
|
|
1412
|
+
lines.push(` Output: ${report.tokens.output}`);
|
|
1413
|
+
lines.push(` Reasoning: ${report.tokens.reasoning}`);
|
|
1414
|
+
lines.push(` Total tokens: ${report.tokens.total}`);
|
|
1415
|
+
lines.push('');
|
|
1416
|
+
lines.push('File Changes:');
|
|
1417
|
+
lines.push(` Total change events: ${report.fileChanges.totalChanges}`);
|
|
1418
|
+
lines.push(` Unique files modified: ${report.fileChanges.uniqueCount}`);
|
|
1419
|
+
return lines.join('\n');
|
|
1420
|
+
}
|
|
1421
|
+
//# sourceMappingURL=report.js.map
|