@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
/** Typed agent/operator surface for repository-scoped harvest workflows. Agent
|
|
2
|
+
* stdout is never interpreted: proposal sets and review verdicts land through
|
|
3
|
+
* this module as validated repository events and versioned artifacts. */
|
|
4
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import {
|
|
8
|
+
isHarvestEvent,
|
|
9
|
+
type HarvestEvent,
|
|
10
|
+
type RepositoryEvent,
|
|
11
|
+
type RepositoryEventEnvelope,
|
|
12
|
+
} from '../events/repository'
|
|
13
|
+
import { agentActor } from '../events/envelope'
|
|
14
|
+
import {
|
|
15
|
+
harvestProposalSetSchema,
|
|
16
|
+
type HarvestProposalSet,
|
|
17
|
+
type HarvestScanPacket,
|
|
18
|
+
} from '../harvest/schema'
|
|
19
|
+
import type { IdSource } from '../ids'
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
22
|
+
openHarvestRuns,
|
|
23
|
+
parkedHarvestRuns,
|
|
24
|
+
proposalArtifactForRound,
|
|
25
|
+
reduceHarvest,
|
|
26
|
+
unresolvedHarvestAttentionRuns,
|
|
27
|
+
type HarvestPendingCommand,
|
|
28
|
+
type HarvestRunState,
|
|
29
|
+
} from '../kernel/harvest'
|
|
30
|
+
import { findingDraftSchema, type Finding } from '../ontology'
|
|
31
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
32
|
+
import {
|
|
33
|
+
HARVEST_PROPOSALS_ARTIFACT,
|
|
34
|
+
HARVEST_REVIEW_ARTIFACT,
|
|
35
|
+
loadScanPacket,
|
|
36
|
+
validateProposalCoverage,
|
|
37
|
+
} from '../processes/harvest'
|
|
38
|
+
import type { BuildStore } from '../store/types'
|
|
39
|
+
import type { HarvestCliEnv } from './env'
|
|
40
|
+
import { withSessionlessStore, type StoreOpener } from './store-opening'
|
|
41
|
+
|
|
42
|
+
export interface HarvestCliDeps {
|
|
43
|
+
store: BuildStore
|
|
44
|
+
env: HarvestCliEnv
|
|
45
|
+
workspacePath: string
|
|
46
|
+
ids: IdSource
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface HarvestContextManifest {
|
|
50
|
+
repo: string
|
|
51
|
+
run: string
|
|
52
|
+
phase: 'synthesize' | 'review'
|
|
53
|
+
round: number
|
|
54
|
+
required: string[]
|
|
55
|
+
allowedTerminal: 'submit' | 'verdict'
|
|
56
|
+
materialized: string[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function wipeAbDir(dir: string): Promise<void> {
|
|
60
|
+
await rm(dir, { recursive: true, force: true })
|
|
61
|
+
await mkdir(dir, { recursive: true })
|
|
62
|
+
await writeFile(join(dir, '.gitignore'), '*\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function writeJson(path: string, value: unknown): Promise<void> {
|
|
66
|
+
await mkdir(dirname(path), { recursive: true })
|
|
67
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runForEnv(events: RepositoryEvent[], env: HarvestCliEnv): HarvestRunState {
|
|
71
|
+
const run = reduceHarvest(events).runs.find((candidate) => candidate.run === env.run)
|
|
72
|
+
if (!run) throw new Error(`unknown harvest run "${env.run}" in repo "${env.repo}"`)
|
|
73
|
+
if (run.status !== 'running') {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`harvest run "${env.run}" is already ${run.status} — its agent session may not deposit another terminal`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return run
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertNoSessionTerminal(events: RepositoryEvent[], env: HarvestCliEnv): void {
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
if (
|
|
84
|
+
event.type === 'harvest.proposals.submitted' &&
|
|
85
|
+
env.phase === 'synthesize' &&
|
|
86
|
+
event.payload.run === env.run &&
|
|
87
|
+
event.payload.round === env.round
|
|
88
|
+
) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`second harvest terminal rejected: proposals for ${env.run} synthesize@${env.round} already landed at repo seq ${event.seq}`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
if (
|
|
94
|
+
event.type === 'harvest.review.verdict' &&
|
|
95
|
+
env.phase === 'review' &&
|
|
96
|
+
event.payload.run === env.run &&
|
|
97
|
+
event.payload.round === env.round
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`second harvest terminal rejected: verdict for ${env.run} review@${env.round} already landed at repo seq ${event.seq}`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if (event.type === 'harvest.session.ended' && event.payload.session === env.session) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`harvest terminal rejected: session "${env.session}" already ended at repo seq ${event.seq}`,
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readArtifactJson(
|
|
112
|
+
store: BuildStore,
|
|
113
|
+
repo: string,
|
|
114
|
+
kind: string,
|
|
115
|
+
rev: number,
|
|
116
|
+
): Promise<unknown> {
|
|
117
|
+
const artifact = await store.getRepoArtifact(repo, kind, rev)
|
|
118
|
+
if (!artifact) throw new Error(`missing repository artifact ${kind}@${rev}`)
|
|
119
|
+
return JSON.parse(new TextDecoder().decode(artifact.content))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function buildHarvestContext(deps: HarvestCliDeps): Promise<HarvestContextManifest> {
|
|
123
|
+
const { store, env, workspacePath } = deps
|
|
124
|
+
const events = await store.getRepoEvents(env.repo)
|
|
125
|
+
const run = runForEnv(events, env)
|
|
126
|
+
const packet = await loadScanPacket(store, env.repo, run.scan)
|
|
127
|
+
const abDir = join(workspacePath, '.ab')
|
|
128
|
+
await wipeAbDir(abDir)
|
|
129
|
+
|
|
130
|
+
const materialized: string[] = []
|
|
131
|
+
const materialize = async (relative: string, value: unknown): Promise<void> => {
|
|
132
|
+
await writeJson(join(abDir, relative), value)
|
|
133
|
+
materialized.push(relative)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await materialize('observations.json', packet.observations)
|
|
137
|
+
await materialize('originating-tickets.json', packet.originatingTickets)
|
|
138
|
+
await materialize('ledger.json', packet.ledger)
|
|
139
|
+
|
|
140
|
+
const proposal = proposalArtifactForRound(run, env.round)
|
|
141
|
+
if (proposal !== undefined) {
|
|
142
|
+
await materialize(
|
|
143
|
+
'proposals.json',
|
|
144
|
+
await readArtifactJson(store, env.repo, proposal.kind, proposal.rev),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const priorReviews = run.reviews
|
|
149
|
+
.filter((review) => review.round < env.round)
|
|
150
|
+
.sort((a, b) => a.round - b.round)
|
|
151
|
+
for (const review of priorReviews) {
|
|
152
|
+
if (review.findings.length > 0) {
|
|
153
|
+
await materialize(`history/findings-r${review.round}.json`, review.findings)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (env.phase === 'synthesize' && env.round > 1) {
|
|
157
|
+
const previous = [...run.reviews]
|
|
158
|
+
.reverse()
|
|
159
|
+
.find((review) => review.round === env.round - 1 && review.verdict === 'revise')
|
|
160
|
+
if (previous !== undefined) {
|
|
161
|
+
await materialize('findings.json', previous.findings)
|
|
162
|
+
const previousProposal = proposalArtifactForRound(run, env.round - 1)
|
|
163
|
+
if (previousProposal !== undefined && proposal === undefined) {
|
|
164
|
+
await materialize(
|
|
165
|
+
'proposals.json',
|
|
166
|
+
await readArtifactJson(store, env.repo, previousProposal.kind, previousProposal.rev),
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const manifest: HarvestContextManifest = {
|
|
173
|
+
repo: env.repo,
|
|
174
|
+
run: env.run,
|
|
175
|
+
phase: env.phase,
|
|
176
|
+
round: env.round,
|
|
177
|
+
required: env.phase === 'synthesize' ? [HARVEST_PROPOSALS_ARTIFACT] : [HARVEST_REVIEW_ARTIFACT],
|
|
178
|
+
allowedTerminal: env.phase === 'synthesize' ? 'submit' : 'verdict',
|
|
179
|
+
materialized,
|
|
180
|
+
}
|
|
181
|
+
await writeJson(join(abDir, 'context.json'), manifest)
|
|
182
|
+
return manifest
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function readJsonFile(path: string): Promise<unknown> {
|
|
186
|
+
const file = Bun.file(path)
|
|
187
|
+
if (!(await file.exists())) throw new Error(`file not found: ${path}`)
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(await file.text())
|
|
190
|
+
} catch (error) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function validateJoinTargets(set: HarvestProposalSet, packet: HarvestScanPacket): void {
|
|
198
|
+
const allowed = new Set(packet.ledger.map((entry) => `${entry.ticket.source}:${entry.ticket.id}`))
|
|
199
|
+
for (const proposal of set.proposals) {
|
|
200
|
+
if (proposal.action !== 'join') continue
|
|
201
|
+
const key = `${proposal.ticket.source}:${proposal.ticket.id}`
|
|
202
|
+
if (!allowed.has(key)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`join target ${key} was not supplied in the harvest ledger context — agents may only join a known prior proposal`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function submitHarvestProposals(
|
|
211
|
+
deps: HarvestCliDeps,
|
|
212
|
+
file: string,
|
|
213
|
+
): Promise<RepositoryEventEnvelope<'harvest.proposals.submitted'>> {
|
|
214
|
+
const { store, env } = deps
|
|
215
|
+
if (env.phase !== 'synthesize') {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`'ab harvest submit' is only valid in synthesize@<round>; current phase is ${env.phase}@${env.round}`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
const events = await store.getRepoEvents(env.repo)
|
|
221
|
+
const run = runForEnv(events, env)
|
|
222
|
+
assertNoSessionTerminal(events, env)
|
|
223
|
+
|
|
224
|
+
const parsed = harvestProposalSetSchema.safeParse(await readJsonFile(file))
|
|
225
|
+
if (!parsed.success) {
|
|
226
|
+
throw new Error(`${file} does not match the harvest proposal schema: ${parsed.error.message}`)
|
|
227
|
+
}
|
|
228
|
+
const coverage = validateProposalCoverage(parsed.data, run.observations)
|
|
229
|
+
if (!coverage.ok) {
|
|
230
|
+
throw new Error(
|
|
231
|
+
`harvest proposals must cover every claimed observation exactly once: ${coverage.errors.join('; ')}`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
const packet = await loadScanPacket(store, env.repo, run.scan)
|
|
235
|
+
validateJoinTargets(parsed.data, packet)
|
|
236
|
+
|
|
237
|
+
const { event } = await store.appendRepoWithArtifacts(
|
|
238
|
+
env.repo,
|
|
239
|
+
[
|
|
240
|
+
{
|
|
241
|
+
kind: HARVEST_PROPOSALS_ARTIFACT,
|
|
242
|
+
content: `${JSON.stringify(parsed.data, null, 2)}\n`,
|
|
243
|
+
metadata: { run: env.run, round: env.round },
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
(deposited) => ({
|
|
247
|
+
actor: agentActor('harvest', env.session),
|
|
248
|
+
type: 'harvest.proposals.submitted',
|
|
249
|
+
payload: {
|
|
250
|
+
run: env.run,
|
|
251
|
+
round: env.round,
|
|
252
|
+
artifact: {
|
|
253
|
+
kind: deposited[0]!.kind,
|
|
254
|
+
rev: deposited[0]!.revision,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
)
|
|
259
|
+
return event
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const FINDINGS_SHAPE = z.array(findingDraftSchema)
|
|
263
|
+
|
|
264
|
+
async function parseHarvestFindings(
|
|
265
|
+
deps: HarvestCliDeps,
|
|
266
|
+
file: string,
|
|
267
|
+
run: HarvestRunState,
|
|
268
|
+
): Promise<Finding[]> {
|
|
269
|
+
const parsed = FINDINGS_SHAPE.safeParse(await readJsonFile(file))
|
|
270
|
+
if (!parsed.success) {
|
|
271
|
+
throw new Error(`${file} does not match the finding schema: ${parsed.error.message}`)
|
|
272
|
+
}
|
|
273
|
+
const known = new Set(
|
|
274
|
+
run.reviews
|
|
275
|
+
.filter((review) => review.round < deps.env.round)
|
|
276
|
+
.flatMap((review) => review.findings.map((finding) => finding.id)),
|
|
277
|
+
)
|
|
278
|
+
for (const finding of parsed.data) {
|
|
279
|
+
for (const id of finding.persists) {
|
|
280
|
+
if (!known.has(id)) {
|
|
281
|
+
throw new Error(`persists id "${id}" is not a finding from an earlier harvest review round`)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return parsed.data.map((finding) => ({ ...finding, id: deps.ids('hf') }))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface HarvestVerdictOpts {
|
|
289
|
+
verdict: string
|
|
290
|
+
notes: string
|
|
291
|
+
findings?: string
|
|
292
|
+
reason?: string
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function submitHarvestVerdict(
|
|
296
|
+
deps: HarvestCliDeps,
|
|
297
|
+
opts: HarvestVerdictOpts,
|
|
298
|
+
): Promise<RepositoryEventEnvelope<'harvest.review.verdict'>> {
|
|
299
|
+
const { store, env } = deps
|
|
300
|
+
if (env.phase !== 'review') {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`'ab harvest verdict' is only valid in review@<round>; current phase is ${env.phase}@${env.round}`,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
if (!['approve', 'revise', 'escalate'].includes(opts.verdict)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`harvest verdict "${opts.verdict}" is invalid — expected approve | revise | escalate`,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
const events = await store.getRepoEvents(env.repo)
|
|
311
|
+
const run = runForEnv(events, env)
|
|
312
|
+
assertNoSessionTerminal(events, env)
|
|
313
|
+
if (proposalArtifactForRound(run, env.round) === undefined) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`review@${env.round} has no proposal artifact to review — synthesize must submit first`,
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const notesFile = Bun.file(opts.notes)
|
|
320
|
+
if (!(await notesFile.exists())) throw new Error(`--notes file not found: ${opts.notes}`)
|
|
321
|
+
const notes = await notesFile.text()
|
|
322
|
+
let findings: Finding[] = []
|
|
323
|
+
if (opts.verdict === 'revise') {
|
|
324
|
+
if (opts.findings === undefined) {
|
|
325
|
+
throw new Error(`'ab harvest verdict revise' requires --findings <json>`)
|
|
326
|
+
}
|
|
327
|
+
findings = await parseHarvestFindings(deps, opts.findings, run)
|
|
328
|
+
if (findings.length === 0) {
|
|
329
|
+
throw new Error(`'ab harvest verdict revise' requires at least one finding`)
|
|
330
|
+
}
|
|
331
|
+
} else if (opts.findings !== undefined) {
|
|
332
|
+
throw new Error(`--findings is only valid with the revise verdict`)
|
|
333
|
+
}
|
|
334
|
+
const reason = opts.reason?.trim()
|
|
335
|
+
if (opts.verdict === 'escalate' && !reason) {
|
|
336
|
+
throw new Error(`'ab harvest verdict escalate' requires --reason <text>`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { event } = await store.appendRepoWithArtifacts(
|
|
340
|
+
env.repo,
|
|
341
|
+
[
|
|
342
|
+
{
|
|
343
|
+
kind: HARVEST_REVIEW_ARTIFACT,
|
|
344
|
+
content: notes,
|
|
345
|
+
metadata: {
|
|
346
|
+
run: env.run,
|
|
347
|
+
round: env.round,
|
|
348
|
+
verdict: opts.verdict,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
(deposited) => ({
|
|
353
|
+
actor: agentActor('harvest-review', env.session),
|
|
354
|
+
type: 'harvest.review.verdict',
|
|
355
|
+
payload: {
|
|
356
|
+
run: env.run,
|
|
357
|
+
round: env.round,
|
|
358
|
+
verdict: opts.verdict as 'approve' | 'revise' | 'escalate',
|
|
359
|
+
findings,
|
|
360
|
+
artifact: {
|
|
361
|
+
kind: deposited[0]!.kind,
|
|
362
|
+
rev: deposited[0]!.revision,
|
|
363
|
+
},
|
|
364
|
+
...(reason ? { reason } : {}),
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
)
|
|
368
|
+
return event
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export interface HarvestRecoveryStatus {
|
|
372
|
+
/** True only for an ordinary infrastructure stop the dispatcher may reopen. */
|
|
373
|
+
recoverable: boolean
|
|
374
|
+
/** Completed, escalated, and recovery-exhausted runs are genuinely terminal. */
|
|
375
|
+
finished: boolean
|
|
376
|
+
automatic: {
|
|
377
|
+
attempts: number
|
|
378
|
+
limit: number
|
|
379
|
+
exhausted: boolean
|
|
380
|
+
}
|
|
381
|
+
stopped?: {
|
|
382
|
+
step: NonNullable<HarvestRunState['failure']>['step']
|
|
383
|
+
round?: number
|
|
384
|
+
}
|
|
385
|
+
attention: {
|
|
386
|
+
required: boolean
|
|
387
|
+
acknowledged: boolean
|
|
388
|
+
}
|
|
389
|
+
pending: {
|
|
390
|
+
observations: Array<{ build: string; seq: number }>
|
|
391
|
+
proposalKeys: string[]
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export interface HarvestFiledStatusView {
|
|
396
|
+
proposalKey: string
|
|
397
|
+
ticket: { source: string; id: string }
|
|
398
|
+
blockers?: { declared: string[]; derived: string[] }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export interface HarvestRunStatusView {
|
|
402
|
+
run: string
|
|
403
|
+
status: HarvestRunState['status']
|
|
404
|
+
startedSeq: number
|
|
405
|
+
startedAt: string
|
|
406
|
+
observations: number
|
|
407
|
+
trigger?: HarvestRunState['trigger']
|
|
408
|
+
steps: HarvestRunState['steps']
|
|
409
|
+
rounds: number
|
|
410
|
+
filed: HarvestFiledStatusView[]
|
|
411
|
+
escalation?: HarvestRunState['escalation']
|
|
412
|
+
failure?: HarvestRunState['failure']
|
|
413
|
+
recovery: HarvestRecoveryStatus
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface HarvestStatusView {
|
|
417
|
+
repo: string
|
|
418
|
+
run?: string
|
|
419
|
+
/** Repository control takes display precedence while paused; runStatus keeps
|
|
420
|
+
* the primary run lifecycle queryable without making pause terminal. */
|
|
421
|
+
status: 'idle' | 'paused' | HarvestRunState['status']
|
|
422
|
+
runStatus?: HarvestRunState['status']
|
|
423
|
+
paused: boolean
|
|
424
|
+
pausedSeq?: number
|
|
425
|
+
pausedAt?: string
|
|
426
|
+
pendingCommands: HarvestPendingCommand[]
|
|
427
|
+
/** Every unresolved failed run plus all open runs and the latest historical
|
|
428
|
+
* summary, in durable start order and without duplicate entries. */
|
|
429
|
+
runs: HarvestRunStatusView[]
|
|
430
|
+
/** Backward-compatible primary-run fields. `runs` is authoritative when
|
|
431
|
+
* more than one workflow needs to be reported. */
|
|
432
|
+
observations: number
|
|
433
|
+
trigger?: HarvestRunState['trigger']
|
|
434
|
+
steps: HarvestRunState['steps']
|
|
435
|
+
rounds: number
|
|
436
|
+
filed: HarvestFiledStatusView[]
|
|
437
|
+
escalation?: HarvestRunState['escalation']
|
|
438
|
+
failure?: HarvestRunState['failure']
|
|
439
|
+
recovery: HarvestRecoveryStatus
|
|
440
|
+
events?: HarvestEvent[]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function projectRecovery(run: HarvestRunState | undefined): HarvestRecoveryStatus {
|
|
444
|
+
const exhaustion = run?.recoveryExhaustion
|
|
445
|
+
const attempts = run?.recoveryRequests.length ?? 0
|
|
446
|
+
const limit =
|
|
447
|
+
exhaustion?.limit ??
|
|
448
|
+
run?.recoveryRequests.at(-1)?.limit ??
|
|
449
|
+
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS
|
|
450
|
+
const stopped = run?.failure
|
|
451
|
+
return {
|
|
452
|
+
recoverable: run?.status === 'failed' && exhaustion === undefined,
|
|
453
|
+
finished:
|
|
454
|
+
run?.status === 'completed' || run?.status === 'escalated' || exhaustion !== undefined,
|
|
455
|
+
automatic: {
|
|
456
|
+
attempts,
|
|
457
|
+
limit,
|
|
458
|
+
exhausted: exhaustion !== undefined,
|
|
459
|
+
},
|
|
460
|
+
...(stopped !== undefined
|
|
461
|
+
? {
|
|
462
|
+
stopped: {
|
|
463
|
+
step: stopped.step,
|
|
464
|
+
...(stopped.round !== undefined ? { round: stopped.round } : {}),
|
|
465
|
+
},
|
|
466
|
+
}
|
|
467
|
+
: {}),
|
|
468
|
+
attention: {
|
|
469
|
+
required: exhaustion !== undefined && exhaustion.attentionAcknowledgedSeq === undefined,
|
|
470
|
+
acknowledged: exhaustion?.attentionAcknowledgedSeq !== undefined,
|
|
471
|
+
},
|
|
472
|
+
pending: {
|
|
473
|
+
// Before give-up the immutable claimed snapshot is the exact retained
|
|
474
|
+
// work. Exhaustion classifies it atomically, after which only the
|
|
475
|
+
// selectively released subset remains pending.
|
|
476
|
+
observations: structuredClone(
|
|
477
|
+
exhaustion?.releasedObservations ?? (run?.status === 'failed' ? run.observations : []),
|
|
478
|
+
),
|
|
479
|
+
proposalKeys: exhaustion?.pendingProposals.map((proposal) => proposal.proposalKey) ?? [],
|
|
480
|
+
},
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function projectHarvestRunStatus(run: HarvestRunState): HarvestRunStatusView {
|
|
485
|
+
return {
|
|
486
|
+
run: run.run,
|
|
487
|
+
status: run.status,
|
|
488
|
+
startedSeq: run.startedSeq,
|
|
489
|
+
startedAt: run.startedAt,
|
|
490
|
+
observations: run.observations.length,
|
|
491
|
+
...(run.trigger !== undefined ? { trigger: run.trigger } : {}),
|
|
492
|
+
steps: run.steps,
|
|
493
|
+
rounds: Math.max(0, ...run.reviews.map((review) => review.round)),
|
|
494
|
+
filed: run.filed.map((entry) => ({
|
|
495
|
+
proposalKey: entry.proposalKey,
|
|
496
|
+
ticket: { source: entry.ticket.source, id: entry.ticket.id },
|
|
497
|
+
...(entry.blockers !== undefined ? { blockers: structuredClone(entry.blockers) } : {}),
|
|
498
|
+
})),
|
|
499
|
+
...(run.escalation !== undefined ? { escalation: run.escalation } : {}),
|
|
500
|
+
...(run.failure !== undefined ? { failure: run.failure } : {}),
|
|
501
|
+
recovery: projectRecovery(run),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function projectHarvestStatus(
|
|
506
|
+
repo: string,
|
|
507
|
+
events: RepositoryEvent[],
|
|
508
|
+
newestEvents?: number,
|
|
509
|
+
): HarvestStatusView {
|
|
510
|
+
const state = reduceHarvest(events)
|
|
511
|
+
const history =
|
|
512
|
+
newestEvents === undefined ? undefined : events.filter(isHarvestEvent).slice(-newestEvents)
|
|
513
|
+
const unresolved = [...parkedHarvestRuns(state), ...unresolvedHarvestAttentionRuns(state)]
|
|
514
|
+
const open = openHarvestRuns(state)
|
|
515
|
+
const included = new Set(
|
|
516
|
+
[...unresolved, ...open, ...(state.latest ? [state.latest] : [])].map((run) => run.run),
|
|
517
|
+
)
|
|
518
|
+
const runs = state.runs.filter((run) => included.has(run.run)).map(projectHarvestRunStatus)
|
|
519
|
+
const unresolvedIds = new Set(unresolved.map((run) => run.run))
|
|
520
|
+
const primary =
|
|
521
|
+
runs.find((run) => unresolvedIds.has(run.run)) ??
|
|
522
|
+
runs.find((run) => run.status === 'running') ??
|
|
523
|
+
runs.at(-1)
|
|
524
|
+
|
|
525
|
+
const control = {
|
|
526
|
+
paused: state.paused,
|
|
527
|
+
...(state.pausedSeq !== undefined ? { pausedSeq: state.pausedSeq } : {}),
|
|
528
|
+
...(state.pausedAt !== undefined ? { pausedAt: state.pausedAt } : {}),
|
|
529
|
+
pendingCommands: state.pendingCommands,
|
|
530
|
+
runs,
|
|
531
|
+
...(history !== undefined ? { events: history } : {}),
|
|
532
|
+
}
|
|
533
|
+
if (primary === undefined) {
|
|
534
|
+
return {
|
|
535
|
+
repo,
|
|
536
|
+
status: state.paused ? 'paused' : 'idle',
|
|
537
|
+
...control,
|
|
538
|
+
observations: 0,
|
|
539
|
+
steps: [],
|
|
540
|
+
rounds: 0,
|
|
541
|
+
filed: [],
|
|
542
|
+
recovery: projectRecovery(undefined),
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const { status: runStatus, startedSeq: _startedSeq, startedAt: _startedAt, ...summary } = primary
|
|
547
|
+
return {
|
|
548
|
+
repo,
|
|
549
|
+
status: state.paused ? 'paused' : runStatus,
|
|
550
|
+
runStatus,
|
|
551
|
+
...control,
|
|
552
|
+
...summary,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderHarvestRunStatus(run: HarvestRunStatusView, paused: boolean): string[] {
|
|
557
|
+
const displayStatus = paused ? 'paused' : run.status
|
|
558
|
+
const lines = [
|
|
559
|
+
`harvest ${run.run} — ${displayStatus}${paused ? ` (run ${run.status})` : ''}`,
|
|
560
|
+
`observations: ${run.observations}`,
|
|
561
|
+
...(run.trigger !== undefined ? [`trigger: ${run.trigger}`] : []),
|
|
562
|
+
`review rounds: ${run.rounds}`,
|
|
563
|
+
'steps:',
|
|
564
|
+
]
|
|
565
|
+
for (const step of run.steps) {
|
|
566
|
+
lines.push(
|
|
567
|
+
` ${step.step}${step.round !== undefined ? ` r${step.round}` : ''}: ` +
|
|
568
|
+
`${step.outcome ?? (step.completedSeq === undefined ? 'running' : 'done')}`,
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
if (run.recovery.stopped !== undefined) {
|
|
572
|
+
lines.push(
|
|
573
|
+
`stopped at: ${run.recovery.stopped.step}${
|
|
574
|
+
run.recovery.stopped.round !== undefined ? ` r${run.recovery.stopped.round}` : ''
|
|
575
|
+
}`,
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
if (
|
|
579
|
+
run.recovery.recoverable ||
|
|
580
|
+
run.recovery.automatic.attempts > 0 ||
|
|
581
|
+
run.recovery.automatic.exhausted
|
|
582
|
+
) {
|
|
583
|
+
lines.push(
|
|
584
|
+
`automatic recovery: ${run.recovery.automatic.attempts}/${run.recovery.automatic.limit}${
|
|
585
|
+
run.recovery.automatic.exhausted
|
|
586
|
+
? ' exhausted'
|
|
587
|
+
: run.recovery.recoverable
|
|
588
|
+
? ' available'
|
|
589
|
+
: ' resumed'
|
|
590
|
+
}`,
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
const observations = run.recovery.pending.observations.map(
|
|
594
|
+
(occurrence) => `${occurrence.build}:${occurrence.seq}`,
|
|
595
|
+
)
|
|
596
|
+
const proposals = run.recovery.pending.proposalKeys
|
|
597
|
+
if (observations.length > 0 || proposals.length > 0) {
|
|
598
|
+
lines.push(
|
|
599
|
+
`pending: ${observations.length} observation${observations.length === 1 ? '' : 's'}` +
|
|
600
|
+
`${observations.length > 0 ? ` (${observations.join(', ')})` : ''}` +
|
|
601
|
+
`${proposals.length > 0 ? `; proposals ${proposals.join(', ')}` : ''}`,
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
if (run.recovery.automatic.exhausted) {
|
|
605
|
+
lines.push(
|
|
606
|
+
run.recovery.attention.required
|
|
607
|
+
? 'attention: human acknowledgement required'
|
|
608
|
+
: 'attention: acknowledged',
|
|
609
|
+
)
|
|
610
|
+
}
|
|
611
|
+
if (run.escalation) lines.push(`escalation: ${run.escalation.reason}`)
|
|
612
|
+
if (run.failure) lines.push(`failure: ${run.failure.error}`)
|
|
613
|
+
if (run.filed.length > 0) {
|
|
614
|
+
lines.push('filed:')
|
|
615
|
+
for (const entry of run.filed) {
|
|
616
|
+
lines.push(` ${entry.proposalKey} -> ${entry.ticket.source}:${entry.ticket.id}`)
|
|
617
|
+
if (entry.blockers !== undefined) {
|
|
618
|
+
lines.push(
|
|
619
|
+
` declared blockers: ${entry.blockers.declared.join(', ') || 'none'}`,
|
|
620
|
+
` originating blockers: ${entry.blockers.derived.join(', ') || 'none'}`,
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return lines
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function renderHarvestStatus(view: HarvestStatusView): string[] {
|
|
629
|
+
if (view.runs.length === 0) {
|
|
630
|
+
const lines = [
|
|
631
|
+
`harvest ${view.repo}: ${view.status === 'paused' ? 'paused' : 'idle'} (no runs)`,
|
|
632
|
+
]
|
|
633
|
+
if (view.pendingCommands.length > 0) {
|
|
634
|
+
lines.push(
|
|
635
|
+
`control pending: ${view.pendingCommands.map((command) => command.command).join(', ')}`,
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
if (view.events !== undefined) {
|
|
639
|
+
lines.push('events:')
|
|
640
|
+
for (const event of view.events) {
|
|
641
|
+
lines.push(` ${event.seq} ${event.ts} ${event.type}`)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return lines
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const lines: string[] = []
|
|
648
|
+
for (const run of view.runs) {
|
|
649
|
+
if (lines.length > 0) lines.push('')
|
|
650
|
+
lines.push(...renderHarvestRunStatus(run, view.paused))
|
|
651
|
+
}
|
|
652
|
+
if (view.pendingCommands.length > 0) {
|
|
653
|
+
lines.splice(
|
|
654
|
+
1,
|
|
655
|
+
0,
|
|
656
|
+
`control pending: ${view.pendingCommands.map((command) => command.command).join(', ')}`,
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
if (view.events !== undefined) {
|
|
660
|
+
lines.push('events:')
|
|
661
|
+
for (const event of view.events) {
|
|
662
|
+
lines.push(` ${event.seq} ${event.ts} ${event.type}`)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return lines
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export interface HarvestStatusOpts {
|
|
669
|
+
/** Current directory; normalized to the main checkout before journal reads. */
|
|
670
|
+
repo: string
|
|
671
|
+
env: Record<string, string | undefined>
|
|
672
|
+
exec: Exec
|
|
673
|
+
stdout: (line: string) => void
|
|
674
|
+
storeRef?: string
|
|
675
|
+
json?: boolean
|
|
676
|
+
events?: number
|
|
677
|
+
/** Injectable store seam for command tests. */
|
|
678
|
+
openStore?: StoreOpener
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
|
|
682
|
+
await withSessionlessStore(
|
|
683
|
+
{
|
|
684
|
+
targetRepo: opts.repo,
|
|
685
|
+
env: opts.env,
|
|
686
|
+
exec: opts.exec,
|
|
687
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
688
|
+
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
689
|
+
},
|
|
690
|
+
async ({ store, repo }) => {
|
|
691
|
+
const record = await store.getRepo(repo)
|
|
692
|
+
const events = record === null ? [] : await store.getRepoEvents(repo)
|
|
693
|
+
const view = projectHarvestStatus(repo, events, opts.events)
|
|
694
|
+
if (opts.json === true) opts.stdout(JSON.stringify(view, null, 2))
|
|
695
|
+
else for (const line of renderHarvestStatus(view)) opts.stdout(line)
|
|
696
|
+
},
|
|
697
|
+
)
|
|
698
|
+
}
|