@defrex/autobuild 0.12.0 → 0.14.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 +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { AbEvent } from '../events/catalog'
|
|
2
|
+
import { DISPATCHER } from '../events/envelope'
|
|
3
|
+
import type { EventType } from '../events/payloads'
|
|
4
|
+
import type { IdSource } from '../ids'
|
|
5
|
+
import type { Config } from '../config/schema'
|
|
6
|
+
import { type Decision, decideNextFromFacts } from '../kernel/engine'
|
|
7
|
+
import { defineReducer } from '../kernel/incremental'
|
|
8
|
+
import type { RepoViewStore } from './repo-view'
|
|
9
|
+
|
|
10
|
+
/** Every new event must explicitly choose whether it advances build work. */
|
|
11
|
+
export const executionProgress = {
|
|
12
|
+
'build.created': 'progress',
|
|
13
|
+
'build.completed': 'progress',
|
|
14
|
+
'runner.attached': 'bookkeeping',
|
|
15
|
+
'runner.setup-failed': 'failure',
|
|
16
|
+
'workspace.provisioned': 'bookkeeping',
|
|
17
|
+
'workspace.provision-started': 'bookkeeping',
|
|
18
|
+
'workspace.released': 'bookkeeping',
|
|
19
|
+
'execution.started': 'bookkeeping',
|
|
20
|
+
'execution.ended': 'bookkeeping',
|
|
21
|
+
'infrastructure.failed': 'failure',
|
|
22
|
+
'infrastructure.cleanup-attempted': 'bookkeeping',
|
|
23
|
+
'pr-summary.requested': 'progress',
|
|
24
|
+
'pr-summary.attempted': 'progress',
|
|
25
|
+
'pr-summary.result': 'progress',
|
|
26
|
+
'pr-summary.settled': 'progress',
|
|
27
|
+
'finalize.step-started': 'progress',
|
|
28
|
+
'publication.requested': 'progress',
|
|
29
|
+
'publication.lost': 'progress',
|
|
30
|
+
'abort.remote-branch-deleted': 'progress',
|
|
31
|
+
'abort.local-branch-deleted': 'progress',
|
|
32
|
+
'abort.ticket-returned': 'progress',
|
|
33
|
+
'dispatch.failed': 'failure',
|
|
34
|
+
'dispatch.comment-posted': 'bookkeeping',
|
|
35
|
+
'build.pause-requested': 'progress',
|
|
36
|
+
'build.resume-requested': 'progress',
|
|
37
|
+
'build.abort-requested': 'progress',
|
|
38
|
+
'build.discard-requested': 'progress',
|
|
39
|
+
'build.auto-merge-requested': 'progress',
|
|
40
|
+
'build.auto-merge-cancelled': 'progress',
|
|
41
|
+
'build.auto-merge-default-observed': 'bookkeeping',
|
|
42
|
+
'build.paused': 'progress',
|
|
43
|
+
'build.resumed': 'progress',
|
|
44
|
+
'build.aborted': 'progress',
|
|
45
|
+
'spec.imported': 'progress',
|
|
46
|
+
'spec.authored': 'progress',
|
|
47
|
+
'spec.revised': 'progress',
|
|
48
|
+
'session.started': 'progress',
|
|
49
|
+
'session.ended': 'progress',
|
|
50
|
+
'plan.started': 'progress',
|
|
51
|
+
'plan.completed': 'progress',
|
|
52
|
+
'plan-review.started': 'progress',
|
|
53
|
+
'plan-review.verdict': 'progress',
|
|
54
|
+
'implement.started': 'progress',
|
|
55
|
+
'implement.completed': 'progress',
|
|
56
|
+
'code-review.started': 'progress',
|
|
57
|
+
'code-review.verdict': 'progress',
|
|
58
|
+
'verify.reconcile-policy-captured': 'progress',
|
|
59
|
+
'verify.started': 'progress',
|
|
60
|
+
'verify.completed': 'progress',
|
|
61
|
+
'finalize.started': 'progress',
|
|
62
|
+
'finalize.completed': 'progress',
|
|
63
|
+
'finalize.step-completed': 'progress',
|
|
64
|
+
'pr-attachment.designated': 'progress',
|
|
65
|
+
'pr-attachment.hosted': 'progress',
|
|
66
|
+
'pr-attachment.reclaimed': 'progress',
|
|
67
|
+
'pr-attachment.reclaim-failed': 'progress',
|
|
68
|
+
'pr.auto-merge-enabled': 'progress',
|
|
69
|
+
'pr.auto-merge-disabled': 'progress',
|
|
70
|
+
'pr.merged': 'progress',
|
|
71
|
+
'pr.closed': 'progress',
|
|
72
|
+
'pr.conflicted': 'progress',
|
|
73
|
+
'reconcile.progress-checked': 'progress',
|
|
74
|
+
'reconcile.started': 'progress',
|
|
75
|
+
'reconcile.completed': 'progress',
|
|
76
|
+
'observation.recorded': 'progress',
|
|
77
|
+
'escalation.raised': 'progress',
|
|
78
|
+
'escalation.answered': 'progress',
|
|
79
|
+
'phase.failed': 'failure',
|
|
80
|
+
} satisfies Record<EventType, 'bookkeeping' | 'failure' | 'progress'>
|
|
81
|
+
|
|
82
|
+
interface Accumulator {
|
|
83
|
+
active: { instance: string; seq: number; eligible: boolean } | null
|
|
84
|
+
count: number
|
|
85
|
+
open: string[]
|
|
86
|
+
latest: { instance: string; seq: number } | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const executionNoProgressReducer = defineReducer({
|
|
90
|
+
version: 1,
|
|
91
|
+
initial: (): Accumulator => ({ active: null, count: 0, open: [], latest: null }),
|
|
92
|
+
fold(acc, events: readonly AbEvent[]) {
|
|
93
|
+
for (const event of events) {
|
|
94
|
+
if (event.type === 'execution.started') {
|
|
95
|
+
// An abandoned start breaks the consecutive completed-execution streak.
|
|
96
|
+
if (acc.active !== null) acc.count = 0
|
|
97
|
+
acc.active = { instance: event.payload.instance, seq: event.seq, eligible: true }
|
|
98
|
+
} else if (event.type === 'execution.ended') {
|
|
99
|
+
if (acc.active?.instance !== event.payload.instance) continue
|
|
100
|
+
const clean = event.payload.outcome === 'completed' && event.payload.exitCode === 0
|
|
101
|
+
acc.count = clean && acc.active.eligible ? acc.count + 1 : 0
|
|
102
|
+
acc.latest = { instance: event.payload.instance, seq: event.seq }
|
|
103
|
+
acc.active = null
|
|
104
|
+
} else if (
|
|
105
|
+
event.type === 'escalation.raised' &&
|
|
106
|
+
event.payload.source === 'policy' &&
|
|
107
|
+
event.payload.policyCause === 'execution-no-progress'
|
|
108
|
+
) {
|
|
109
|
+
if (!acc.open.includes(event.payload.id)) acc.open.push(event.payload.id)
|
|
110
|
+
} else if (event.type === 'escalation.answered') {
|
|
111
|
+
if (!acc.open.includes(event.payload.id)) continue
|
|
112
|
+
acc.open = acc.open.filter((id) => id !== event.payload.id)
|
|
113
|
+
acc.count = 0
|
|
114
|
+
acc.active = null
|
|
115
|
+
} else if (executionProgress[event.type] !== 'bookkeeping') {
|
|
116
|
+
acc.count = 0
|
|
117
|
+
if (acc.active !== null) acc.active.eligible = false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
finish: (acc) => ({ ...acc, open: [...acc.open] }),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Describe routing without copying commands or phase feedback into the question. */
|
|
125
|
+
function expectedAction(decision: Decision): string {
|
|
126
|
+
if (decision.kind === 'run-phase') return `${decision.kind} ${decision.phase}@${decision.round}`
|
|
127
|
+
if ('step' in decision)
|
|
128
|
+
return `${decision.kind} ${decision.step} (attempt ${'attempt' in decision ? decision.attempt : 1})`
|
|
129
|
+
if (decision.kind === 'raise-escalation') return `${decision.kind} ${decision.phase}`
|
|
130
|
+
return decision.kind
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Repair the third-completion/raise crash gap without provisioning execution four. */
|
|
134
|
+
export async function ensureExecutionProgress(
|
|
135
|
+
store: Pick<RepoViewStore, 'buildFacts' | 'appendIfCurrent' | 'refreshBuild'>,
|
|
136
|
+
ids: IdSource,
|
|
137
|
+
slug: string,
|
|
138
|
+
config: Config,
|
|
139
|
+
decide?: (
|
|
140
|
+
state: Parameters<typeof decideNextFromFacts>[0],
|
|
141
|
+
log: Parameters<typeof decideNextFromFacts>[1],
|
|
142
|
+
) => Promise<Decision | undefined>,
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
for (;;) {
|
|
145
|
+
await store.refreshBuild(slug)
|
|
146
|
+
const facts = await store.buildFacts(slug)
|
|
147
|
+
const decision = decide
|
|
148
|
+
? await decide(facts.state, facts.log)
|
|
149
|
+
: decideNextFromFacts(facts.state, facts.log, config)
|
|
150
|
+
if (decision === undefined) return false
|
|
151
|
+
// Commands must still be acknowledged even while this guard is parked.
|
|
152
|
+
if (decision.kind === 'acknowledge') return true
|
|
153
|
+
const streak = facts.executionNoProgress
|
|
154
|
+
if (streak.open.length > 0) return false
|
|
155
|
+
// Callers own wait routing, including publication-recovery launches. A
|
|
156
|
+
// legitimate wait is not dispatcher/runner disagreement.
|
|
157
|
+
if (decision.kind === 'wait') return true
|
|
158
|
+
if (streak.count < 3) return true
|
|
159
|
+
const raised = await store.appendIfCurrent(slug, facts.state.lastSeq, {
|
|
160
|
+
actor: DISPATCHER,
|
|
161
|
+
type: 'escalation.raised',
|
|
162
|
+
payload: {
|
|
163
|
+
id: ids('esc'),
|
|
164
|
+
phase: 'setup',
|
|
165
|
+
source: 'policy',
|
|
166
|
+
policyCause: 'execution-no-progress',
|
|
167
|
+
question: `${streak.count} consecutive executions ended normally without build progress. Dispatcher expected ${expectedAction(decision)}. Inspect the dispatcher/runner disagreement, then answer with guidance or retry.`,
|
|
168
|
+
refs:
|
|
169
|
+
streak.latest === null
|
|
170
|
+
? []
|
|
171
|
+
: [`execution:${streak.latest.instance}`, `seq:${streak.latest.seq}`],
|
|
172
|
+
},
|
|
173
|
+
})
|
|
174
|
+
if (raised !== null) return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ticketGrantRequest } from '../ports/tickets/grants'
|
|
1
2
|
/**
|
|
2
3
|
* Crash-safe repository-scoped harvest workflow: scan -> synthesize <-> review
|
|
3
4
|
* -> file. It reuses `converge` for adversarial review semantics while every
|
|
@@ -56,6 +57,20 @@ export async function runHarvestChild(
|
|
|
56
57
|
workspacePath: string = process.cwd(),
|
|
57
58
|
): Promise<void> {
|
|
58
59
|
const fullStore: BuildStore = openStore(input.storeRef, env.AB_TOKEN)
|
|
60
|
+
const ticketAuthority =
|
|
61
|
+
env.AB_TICKET_URL && env.AB_TICKET_TOKEN
|
|
62
|
+
? { url: env.AB_TICKET_URL, token: env.AB_TICKET_TOKEN }
|
|
63
|
+
: undefined
|
|
64
|
+
const renewTickets = ticketAuthority
|
|
65
|
+
? setInterval(() => {
|
|
66
|
+
void ticketGrantRequest(ticketAuthority.url, ticketAuthority.token, {
|
|
67
|
+
action: 'renew',
|
|
68
|
+
repo: input.repo,
|
|
69
|
+
}).catch(() => {
|
|
70
|
+
// Expiry denies subsequent ticket operations; never regain project authority.
|
|
71
|
+
})
|
|
72
|
+
}, 60_000)
|
|
73
|
+
: undefined
|
|
59
74
|
try {
|
|
60
75
|
const config = await loadConfig(join(workspacePath, 'autobuild.toml'))
|
|
61
76
|
// Full build-child composition parity, in the same order: plugins load
|
|
@@ -80,7 +95,7 @@ export async function runHarvestChild(
|
|
|
80
95
|
const { targetRepo, localStateRoot } = guestTicketSourceArgs(input, workspacePath)
|
|
81
96
|
const tickets = await createTicketSource(
|
|
82
97
|
config.tickets,
|
|
83
|
-
env,
|
|
98
|
+
{ ...env, AB_HARVEST: input.preparedRun?.run ?? input.instance },
|
|
84
99
|
targetRepo,
|
|
85
100
|
localStateRoot,
|
|
86
101
|
plugins,
|
|
@@ -103,6 +118,8 @@ export async function runHarvestChild(
|
|
|
103
118
|
...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
|
|
104
119
|
...(input.environment !== undefined ? { environment: input.environment } : {}),
|
|
105
120
|
sessionEnv: {
|
|
121
|
+
...(env.AB_TICKET_URL ? { AB_TICKET_URL: env.AB_TICKET_URL } : {}),
|
|
122
|
+
...(env.AB_TICKET_TOKEN ? { AB_TICKET_TOKEN: env.AB_TICKET_TOKEN } : {}),
|
|
106
123
|
AB_STORE: input.storeRef,
|
|
107
124
|
...(env.AB_TOKEN !== undefined && env.AB_TOKEN !== '' ? { AB_TOKEN: env.AB_TOKEN } : {}),
|
|
108
125
|
},
|
|
@@ -112,10 +129,19 @@ export async function runHarvestChild(
|
|
|
112
129
|
})
|
|
113
130
|
await runner.run()
|
|
114
131
|
} finally {
|
|
132
|
+
if (renewTickets) clearInterval(renewTickets)
|
|
115
133
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
if (ticketAuthority)
|
|
135
|
+
await ticketGrantRequest(ticketAuthority.url, ticketAuthority.token, {
|
|
136
|
+
action: 'revoke',
|
|
137
|
+
repo: input.repo,
|
|
138
|
+
})
|
|
139
|
+
} finally {
|
|
140
|
+
try {
|
|
141
|
+
await fullStore.close()
|
|
142
|
+
} catch {
|
|
143
|
+
// Process teardown cannot change the already-durable runner outcome.
|
|
144
|
+
}
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RepositoryReadFacts } from '../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* Durable harvest execution state: the repository-journal projection that
|
|
3
4
|
* hosted-harvest supervision (AUT-305) reads instead of process memory. The
|
|
@@ -88,22 +89,27 @@ export interface HarvestExecutionClassificationInput {
|
|
|
88
89
|
* `held`; anything else is `idle`. The result feeds the same counters a
|
|
89
90
|
* locally run harvest reports. */
|
|
90
91
|
export function classifyHarvestOutcome(
|
|
91
|
-
events: readonly RepositoryEvent[],
|
|
92
|
+
events: readonly RepositoryEvent[] | RepositoryReadFacts,
|
|
92
93
|
input: HarvestExecutionClassificationInput,
|
|
93
94
|
): HarvestRunnerResult {
|
|
94
|
-
const
|
|
95
|
-
const
|
|
95
|
+
const facts = !Array.isArray(events) ? (events as RepositoryReadFacts) : undefined
|
|
96
|
+
const raw = Array.isArray(events) ? events : []
|
|
97
|
+
const state = facts?.harvest ?? reduceHarvest(raw)
|
|
98
|
+
const started = raw.find(
|
|
96
99
|
(event) =>
|
|
97
100
|
event.type === 'harvest.execution.started' && event.seq === input.executionStartedSeq,
|
|
98
101
|
)
|
|
102
|
+
const execution =
|
|
103
|
+
facts?.harvestExecutionStarts[String(input.executionStartedSeq)] ??
|
|
104
|
+
(started?.type === 'harvest.execution.started' ? started.payload.execution : undefined)
|
|
99
105
|
const authorization =
|
|
100
|
-
|
|
101
|
-
?
|
|
106
|
+
execution === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: (facts?.harvestAuthorizations[JSON.stringify(execution)] ??
|
|
109
|
+
raw.find(
|
|
102
110
|
(event) =>
|
|
103
|
-
event.type === 'harvest.execution.authorized' &&
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
-
: undefined
|
|
111
|
+
event.type === 'harvest.execution.authorized' && event.payload.execution === execution,
|
|
112
|
+
))
|
|
107
113
|
const selected =
|
|
108
114
|
authorization?.type === 'harvest.execution.authorized' ? authorization.payload : undefined
|
|
109
115
|
const latestRun = selected
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readRepositoryFacts } from '../store/reduced-reads'
|
|
2
|
+
import { ticketIdentity } from '../ports/tickets/identity'
|
|
3
|
+
import { issueTicketGrant } from '../ports/tickets/grants'
|
|
1
4
|
import { delegateCredential } from '../store/remote/credentials'
|
|
2
5
|
/**
|
|
3
6
|
* Crash-safe repository-scoped harvest workflow:
|
|
@@ -20,7 +23,6 @@ import {
|
|
|
20
23
|
decideHarvestControl,
|
|
21
24
|
openHarvestRun,
|
|
22
25
|
proposalArtifactForRound,
|
|
23
|
-
reduceHarvest,
|
|
24
26
|
type HarvestRunState,
|
|
25
27
|
} from '../kernel/harvest'
|
|
26
28
|
import type { ArtifactRef, Feedback, Verdict } from '../ontology'
|
|
@@ -237,28 +239,23 @@ export class HarvestRunner {
|
|
|
237
239
|
// Bounded read (AUT-489): reduceHarvest and the verdict/failure filters
|
|
238
240
|
// below consume durable types only, so the subset is replay-equivalent.
|
|
239
241
|
if (this.deps.preparedRun) {
|
|
240
|
-
const events = await store
|
|
241
|
-
const authorization = events.
|
|
242
|
-
(event) =>
|
|
243
|
-
event.type === 'harvest.execution.authorized' &&
|
|
244
|
-
event.payload.execution === instance &&
|
|
245
|
-
event.payload.run === this.deps.preparedRun!.run,
|
|
246
|
-
)
|
|
242
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
243
|
+
const authorization = events.harvestAuthorizations[JSON.stringify(instance)]
|
|
247
244
|
if (authorization?.type !== 'harvest.execution.authorized')
|
|
248
245
|
throw new Error('prepared harvest execution unavailable')
|
|
249
|
-
run =
|
|
246
|
+
run = events.harvest.runs.find((run) => run.run === this.deps.preparedRun!.run)
|
|
250
247
|
initial = authorization.payload.launch
|
|
251
248
|
} else {
|
|
252
|
-
let events = await store
|
|
253
|
-
let state =
|
|
249
|
+
let events = (await readRepositoryFacts(store, repo)).value
|
|
250
|
+
let state = events.harvest
|
|
254
251
|
run = actionableHarvestRun(state)
|
|
255
252
|
|
|
256
253
|
// Settle durable operator control before scanning or resuming work. The
|
|
257
254
|
// selected run comes from the full journal, so a shadowed failure or
|
|
258
255
|
// exhaustion barrier is recovered/reported before any later open run.
|
|
259
256
|
await this.controlBoundary(run?.run)
|
|
260
|
-
events = await store
|
|
261
|
-
state =
|
|
257
|
+
events = (await readRepositoryFacts(store, repo)).value
|
|
258
|
+
state = events.harvest
|
|
262
259
|
run = openHarvestRun(state)
|
|
263
260
|
|
|
264
261
|
if (!run) {
|
|
@@ -272,7 +269,7 @@ export class HarvestRunner {
|
|
|
272
269
|
// Bounded read (AUT-489): the gate's journal input is consumed only
|
|
273
270
|
// through reduceHarvest, so the subset is replay-equivalent here too.
|
|
274
271
|
const [gateEvents, digests] = await Promise.all([
|
|
275
|
-
store.
|
|
272
|
+
readRepositoryFacts(store, repo).then((result) => result.value.harvest),
|
|
276
273
|
store.getRepoBuildDigests(repo),
|
|
277
274
|
])
|
|
278
275
|
const pressure = evaluateHarvestPressureFromDigests({
|
|
@@ -336,8 +333,8 @@ export class HarvestRunner {
|
|
|
336
333
|
}),
|
|
337
334
|
)
|
|
338
335
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
339
|
-
events = await store
|
|
340
|
-
run =
|
|
336
|
+
events = (await readRepositoryFacts(store, repo)).value
|
|
337
|
+
run = events.harvest.runs.find((candidate) => candidate.run === runId)
|
|
341
338
|
if (!run) throw new Error(`harvest.started did not reduce run "${runId}"`)
|
|
342
339
|
// A request racing the claim parks this exact run; its snapshot stays
|
|
343
340
|
// open and is resumed rather than scanned again.
|
|
@@ -501,14 +498,8 @@ export class HarvestRunner {
|
|
|
501
498
|
)
|
|
502
499
|
// Bounded read (AUT-489): only harvest.review.verdict facts are
|
|
503
500
|
// filtered here, and those are durable.
|
|
504
|
-
const event = (await this.deps.store
|
|
505
|
-
(
|
|
506
|
-
candidate,
|
|
507
|
-
): candidate is Extract<RepositoryEvent, { type: 'harvest.review.verdict' }> =>
|
|
508
|
-
candidate.type === 'harvest.review.verdict' &&
|
|
509
|
-
candidate.payload.run === run.run &&
|
|
510
|
-
candidate.payload.round === round,
|
|
511
|
-
)
|
|
501
|
+
const event = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
502
|
+
.harvestVerdicts[JSON.stringify([run.run, round])]?.first
|
|
512
503
|
if (!event) throw new Error('review reducer/event mismatch')
|
|
513
504
|
await this.controlBoundary(run.run)
|
|
514
505
|
return verdictFromEvent(event)
|
|
@@ -570,15 +561,8 @@ export class HarvestRunner {
|
|
|
570
561
|
})
|
|
571
562
|
// Bounded read (AUT-489): only harvest.review.verdict facts are filtered
|
|
572
563
|
// here, and those are durable.
|
|
573
|
-
const events = await this.deps.store
|
|
574
|
-
const verdict = [
|
|
575
|
-
.reverse()
|
|
576
|
-
.find(
|
|
577
|
-
(event): event is Extract<RepositoryEvent, { type: 'harvest.review.verdict' }> =>
|
|
578
|
-
event.type === 'harvest.review.verdict' &&
|
|
579
|
-
event.payload.run === run.run &&
|
|
580
|
-
event.payload.round === round,
|
|
581
|
-
)
|
|
564
|
+
const events = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
565
|
+
const verdict = events.harvestVerdicts[JSON.stringify([run.run, round])]?.latest
|
|
582
566
|
if (!verdict) throw new Error(`review@${round} terminal is missing`)
|
|
583
567
|
await this.completeStep(
|
|
584
568
|
run.run,
|
|
@@ -622,22 +606,16 @@ export class HarvestRunner {
|
|
|
622
606
|
await this.ensureLease()
|
|
623
607
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
624
608
|
// consume durable types only.
|
|
625
|
-
const events = await store
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
event.payload.step === spec.step &&
|
|
631
|
-
event.payload.round === spec.round,
|
|
632
|
-
)
|
|
633
|
-
const failures = matchingFailures.length
|
|
634
|
-
const latestFailure = matchingFailures.at(-1)
|
|
635
|
-
const current = reduceHarvest(events).runs.find((candidate) => candidate.run === spec.run)
|
|
609
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
610
|
+
const failureFacts = events.harvestFailures[JSON.stringify([spec.run, spec.step, spec.round])]
|
|
611
|
+
const failures = failureFacts?.count ?? 0
|
|
612
|
+
const latestFailure = failureFacts?.latest
|
|
613
|
+
const current = events.harvest.runs.find((candidate) => candidate.run === spec.run)
|
|
636
614
|
const resumedAfterTerminalFailure =
|
|
637
615
|
latestFailure?.payload.willRetry === false &&
|
|
638
616
|
current?.status === 'running' &&
|
|
639
617
|
current.failure === undefined &&
|
|
640
|
-
events.
|
|
618
|
+
events.latestHarvestResumedSeq > latestFailure.seq
|
|
641
619
|
if (failures >= this.maxSessionAttempts && !resumedAfterTerminalFailure) {
|
|
642
620
|
throw new SessionFailure(`${spec.step}@${spec.round} exhausted retries`)
|
|
643
621
|
}
|
|
@@ -882,17 +860,11 @@ export class HarvestRunner {
|
|
|
882
860
|
await this.ensureLease()
|
|
883
861
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
884
862
|
// consume durable types only.
|
|
885
|
-
const events = await store
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
event.payload.step === spec.step &&
|
|
891
|
-
event.payload.round === spec.round,
|
|
892
|
-
)
|
|
893
|
-
const failures = matchingFailures.length
|
|
894
|
-
const latestFailure = matchingFailures.at(-1)
|
|
895
|
-
const current = reduceHarvest(events).runs.find((candidate) => candidate.run === spec.run)
|
|
863
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
864
|
+
const failureFacts = events.harvestFailures[JSON.stringify([spec.run, spec.step, spec.round])]
|
|
865
|
+
const failures = failureFacts?.count ?? 0
|
|
866
|
+
const latestFailure = failureFacts?.latest
|
|
867
|
+
const current = events.harvest.runs.find((candidate) => candidate.run === spec.run)
|
|
896
868
|
// A stopping failure may already have consumed the ordinary retry budget.
|
|
897
869
|
// harvest.resumed deliberately clears that reduced stop and grants one real
|
|
898
870
|
// re-entry without deleting history or resetting attempt numbers. The next
|
|
@@ -901,7 +873,7 @@ export class HarvestRunner {
|
|
|
901
873
|
latestFailure?.payload.willRetry === false &&
|
|
902
874
|
current?.status === 'running' &&
|
|
903
875
|
current.failure === undefined &&
|
|
904
|
-
events.
|
|
876
|
+
events.latestHarvestResumedSeq > latestFailure.seq
|
|
905
877
|
if (failures >= this.maxSessionAttempts && !resumedAfterTerminalFailure) {
|
|
906
878
|
throw new SessionFailure(`${spec.step}@${spec.round} exhausted retries`)
|
|
907
879
|
}
|
|
@@ -1176,9 +1148,16 @@ export class HarvestRunner {
|
|
|
1176
1148
|
)
|
|
1177
1149
|
const packet = await loadScanPacket(store, repo, current.scan)
|
|
1178
1150
|
const observations = packet.observations
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
)
|
|
1151
|
+
const identity = ticketIdentity(tickets)
|
|
1152
|
+
const knownLedger = new Map<string, (typeof packet.ledger)[number]>()
|
|
1153
|
+
for (const entry of packet.ledger) {
|
|
1154
|
+
const current =
|
|
1155
|
+
entry.ticket.source === tickets.name ? await tickets.get(entry.ticket.id) : null
|
|
1156
|
+
knownLedger.set(await identity(entry.ticket), {
|
|
1157
|
+
...entry,
|
|
1158
|
+
ticket: current?.ref ?? entry.ticket,
|
|
1159
|
+
})
|
|
1160
|
+
}
|
|
1182
1161
|
const alreadyFiled = new Map(current.filed.map((entry) => [entry.proposalKey, entry]))
|
|
1183
1162
|
const reservations = new Map(current.reservations.map((entry) => [entry.proposalKey, entry.id]))
|
|
1184
1163
|
const dispositions: HarvestDisposition[] = []
|
|
@@ -1258,7 +1237,7 @@ export class HarvestRunner {
|
|
|
1258
1237
|
...(filed.blockers !== undefined ? { blockers: filed.blockers } : {}),
|
|
1259
1238
|
})
|
|
1260
1239
|
} else if (proposal.action === 'join') {
|
|
1261
|
-
const known = knownLedger.get(
|
|
1240
|
+
const known = knownLedger.get(await identity(proposal.ticket))
|
|
1262
1241
|
if (!known) {
|
|
1263
1242
|
throw new Error(
|
|
1264
1243
|
`approved join target ${proposal.ticket.source}:${proposal.ticket.id} is not in the ledger context`,
|
|
@@ -1357,8 +1336,8 @@ export class HarvestRunner {
|
|
|
1357
1336
|
await this.ensureLease()
|
|
1358
1337
|
// Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
|
|
1359
1338
|
// consume durable types only.
|
|
1360
|
-
const events = await this.deps.store
|
|
1361
|
-
const state =
|
|
1339
|
+
const events = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value
|
|
1340
|
+
const state = events.harvest
|
|
1362
1341
|
const current = state.runs.find((candidate) => candidate.run === run)
|
|
1363
1342
|
if (current?.status !== 'running') return
|
|
1364
1343
|
const open = [...current.steps]
|
|
@@ -1366,13 +1345,7 @@ export class HarvestRunner {
|
|
|
1366
1345
|
.find((occurrence) => occurrence.completedSeq === undefined)
|
|
1367
1346
|
const step = open?.step ?? 'file'
|
|
1368
1347
|
const round = open?.round
|
|
1369
|
-
const prior = events.
|
|
1370
|
-
(event) =>
|
|
1371
|
-
event.type === 'harvest.failed' &&
|
|
1372
|
-
event.payload.run === run &&
|
|
1373
|
-
event.payload.step === step &&
|
|
1374
|
-
event.payload.round === round,
|
|
1375
|
-
).length
|
|
1348
|
+
const prior = events.harvestFailures[JSON.stringify([run, step, round])]?.count ?? 0
|
|
1376
1349
|
const attempt = prior + 1
|
|
1377
1350
|
const willRetry = attempt < this.maxSessionAttempts
|
|
1378
1351
|
const message = errorMessage(error)
|
|
@@ -1412,7 +1385,7 @@ export class HarvestRunner {
|
|
|
1412
1385
|
if (this.deps.preparedRun) return
|
|
1413
1386
|
await this.ensureLease()
|
|
1414
1387
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1415
|
-
const state =
|
|
1388
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1416
1389
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1417
1390
|
if (decision.kind !== 'exhaust-recovery' || decision.run !== run) return
|
|
1418
1391
|
await this.finalizeRecoveryExhaustion(decision.run, decision.attempts, decision.limit)
|
|
@@ -1438,7 +1411,7 @@ export class HarvestRunner {
|
|
|
1438
1411
|
|
|
1439
1412
|
await this.ensureLease()
|
|
1440
1413
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1441
|
-
const state =
|
|
1414
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1442
1415
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1443
1416
|
if (
|
|
1444
1417
|
decision.kind !== 'exhaust-recovery' ||
|
|
@@ -1475,7 +1448,7 @@ export class HarvestRunner {
|
|
|
1475
1448
|
while (true) {
|
|
1476
1449
|
await this.ensureLease()
|
|
1477
1450
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1478
|
-
const state =
|
|
1451
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1479
1452
|
const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
|
|
1480
1453
|
if (decision.kind === 'proceed') return
|
|
1481
1454
|
if (this.deps.preparedRun) throw new HarvestParkedSignal(run)
|
|
@@ -1520,7 +1493,7 @@ export class HarvestRunner {
|
|
|
1520
1493
|
|
|
1521
1494
|
private async refreshRun(run: string): Promise<HarvestRunState> {
|
|
1522
1495
|
// Bounded read (AUT-489): reduceHarvest consumes durable types only.
|
|
1523
|
-
const state =
|
|
1496
|
+
const state = (await readRepositoryFacts(this.deps.store, this.deps.repo)).value.harvest
|
|
1524
1497
|
const found = state.runs.find((candidate) => candidate.run === run)
|
|
1525
1498
|
if (!found) throw new Error(`unknown harvest run "${run}"`)
|
|
1526
1499
|
return found
|
|
@@ -1638,9 +1611,20 @@ export class HarvestRunner {
|
|
|
1638
1611
|
session,
|
|
1639
1612
|
})
|
|
1640
1613
|
: undefined
|
|
1614
|
+
const ticketGrant =
|
|
1615
|
+
parent?.AB_TICKET_URL && parent.AB_TICKET_TOKEN
|
|
1616
|
+
? await issueTicketGrant(
|
|
1617
|
+
parent.AB_TICKET_URL,
|
|
1618
|
+
parent.AB_TICKET_TOKEN,
|
|
1619
|
+
this.deps.repo,
|
|
1620
|
+
this.deps.instance,
|
|
1621
|
+
'phase',
|
|
1622
|
+
)
|
|
1623
|
+
: undefined
|
|
1641
1624
|
return {
|
|
1642
1625
|
AB_STORE: 'local',
|
|
1643
1626
|
...this.deps.sessionEnv,
|
|
1627
|
+
...(ticketGrant ? { AB_TICKET_TOKEN: ticketGrant.token } : {}),
|
|
1644
1628
|
...(token ? { AB_TOKEN: token } : {}),
|
|
1645
1629
|
AB_REPO: this.deps.repo,
|
|
1646
1630
|
AB_HARVEST: run,
|