@defrex/autobuild 0.9.0 → 0.10.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 +10 -0
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +24 -3
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +26 -7
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/tickets/SKILL.md +57 -5
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reducer snapshots: a cache beside the event log, never an authority.
|
|
3
|
+
*
|
|
4
|
+
* For one scope (a build log or a repository journal) and one registered
|
|
5
|
+
* reducer, the store may keep that reducer's serialized accumulator as of a
|
|
6
|
+
* cursor, so a process with no hot memory loads it, reads only the events
|
|
7
|
+
* newer than the cursor, and advances — instead of replaying the whole log.
|
|
8
|
+
*
|
|
9
|
+
* The log always regenerates a snapshot. Every consumer treats a missing,
|
|
10
|
+
* version-mismatched, ahead-of-log, malformed or unreadable snapshot as a miss
|
|
11
|
+
* and replays; deleting every snapshot changes no observable output.
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod'
|
|
14
|
+
import type { IncrementalReducer } from '../kernel/incremental'
|
|
15
|
+
import type { BuildStore } from './types'
|
|
16
|
+
|
|
17
|
+
export type SnapshotScope = { kind: 'build'; slug: string } | { kind: 'repo'; repo: string }
|
|
18
|
+
|
|
19
|
+
export interface ReducerSnapshot {
|
|
20
|
+
/** The reducer version that wrote `state`; a reader on another version
|
|
21
|
+
* ignores the snapshot. */
|
|
22
|
+
version: number
|
|
23
|
+
/** The greatest event seq folded into `state`. */
|
|
24
|
+
cursor: number
|
|
25
|
+
/** The reducer's JSON accumulator as of `cursor`. */
|
|
26
|
+
state: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const reducerSnapshotWireSchema = z.object({
|
|
30
|
+
version: z.number().int().min(1),
|
|
31
|
+
cursor: z.number().int().min(0),
|
|
32
|
+
state: z.unknown(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/** The registry name a snapshot is keyed by: a short identifier, safe in a URL
|
|
36
|
+
* path segment and a SQL key. */
|
|
37
|
+
export const SNAPSHOT_REDUCER_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/
|
|
38
|
+
|
|
39
|
+
export function snapshotScopeKey(scope: SnapshotScope): string {
|
|
40
|
+
return scope.kind === 'build' ? `build:${scope.slug}` : `repo:${scope.repo}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether a put replaces the stored row. The stored cursor never decreases:
|
|
44
|
+
* a higher cursor wins whatever the versions, and an upgraded reducer replaces
|
|
45
|
+
* an older shape at an unchanged cursor. */
|
|
46
|
+
export function snapshotSupersedes(
|
|
47
|
+
next: Pick<ReducerSnapshot, 'version' | 'cursor'>,
|
|
48
|
+
stored: Pick<ReducerSnapshot, 'version' | 'cursor'> | undefined,
|
|
49
|
+
): boolean {
|
|
50
|
+
if (stored === undefined) return true
|
|
51
|
+
if (next.cursor !== stored.cursor) return next.cursor > stored.cursor
|
|
52
|
+
return next.version > stored.version
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type SnapshotStore = Pick<BuildStore, 'getReducerSnapshot' | 'putReducerSnapshot'>
|
|
56
|
+
|
|
57
|
+
/** Read a snapshot, degrading every failure to a miss. */
|
|
58
|
+
export async function loadReducerSnapshot(
|
|
59
|
+
store: Partial<SnapshotStore>,
|
|
60
|
+
scope: SnapshotScope,
|
|
61
|
+
name: string,
|
|
62
|
+
reducer: Pick<IncrementalReducer<unknown, unknown, unknown>, 'version'>,
|
|
63
|
+
): Promise<ReducerSnapshot | null> {
|
|
64
|
+
if (store.getReducerSnapshot === undefined) return null
|
|
65
|
+
try {
|
|
66
|
+
const found = await store.getReducerSnapshot(scope, name, reducer.version)
|
|
67
|
+
if (found === null || found.version !== reducer.version) return null
|
|
68
|
+
if (!Number.isInteger(found.cursor) || found.cursor < 0) return null
|
|
69
|
+
return found
|
|
70
|
+
} catch {
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Write a snapshot best-effort; a failing store never reaches the caller. */
|
|
76
|
+
export async function persistReducerSnapshot(
|
|
77
|
+
store: Partial<SnapshotStore>,
|
|
78
|
+
scope: SnapshotScope,
|
|
79
|
+
name: string,
|
|
80
|
+
snapshot: ReducerSnapshot,
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
if (store.putReducerSnapshot === undefined) return false
|
|
83
|
+
try {
|
|
84
|
+
return await store.putReducerSnapshot(scope, name, snapshot)
|
|
85
|
+
} catch {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AbEvent } from '../events/catalog'
|
|
12
12
|
import type { PinnedAsset } from '../events/payloads'
|
|
13
|
+
import { defineReducer } from '../kernel/incremental'
|
|
13
14
|
import { contentHash, type BlobStore, type BuildStore } from './types'
|
|
14
15
|
|
|
15
16
|
export type { PinnedAsset }
|
|
@@ -379,19 +380,40 @@ export async function samplePinnedAssets(
|
|
|
379
380
|
return (await store.listTicketAssets(repo, ticketId)).map(pinnedAssetOf)
|
|
380
381
|
}
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
export const PINNED_ASSETS_REDUCER_VERSION = 1
|
|
384
|
+
|
|
385
|
+
/** The asset sets recorded by every `build.created`/`spec.revised` pin event
|
|
386
|
+
* that carries one, in array order. Each pin event replaces the set, so the
|
|
387
|
+
* newest decides the live pin; all of them answer "was this revision ever
|
|
388
|
+
* named". `pinnedRevision` is the query over the finished state. */
|
|
389
|
+
export const pinnedAssetsReducer = defineReducer<
|
|
390
|
+
{ pins: PinnedAsset[][] },
|
|
391
|
+
AbEvent,
|
|
392
|
+
{ pins: PinnedAsset[][] }
|
|
393
|
+
>({
|
|
394
|
+
version: PINNED_ASSETS_REDUCER_VERSION,
|
|
395
|
+
initial: () => ({ pins: [] }),
|
|
396
|
+
fold(acc, events) {
|
|
397
|
+
for (const event of events) {
|
|
398
|
+
if (event.type !== 'build.created' && event.type !== 'spec.revised') continue
|
|
399
|
+
const assets = event.payload.assets
|
|
400
|
+
if (assets === undefined) continue
|
|
401
|
+
acc.pins.push(assets)
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
finish: (acc) => ({ pins: acc.pins.slice() }),
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
/** Latest-pinned revision of `kind/name` over a reduced pin state, or (with
|
|
408
|
+
* `rev`) the revision when any pin named it. */
|
|
409
|
+
export function pinnedRevision(
|
|
410
|
+
state: { pins: readonly (readonly PinnedAsset[])[] },
|
|
386
411
|
kind: string,
|
|
387
412
|
name: string,
|
|
388
413
|
rev?: number,
|
|
389
414
|
): number | undefined {
|
|
390
415
|
let latest: number | undefined
|
|
391
|
-
for (const
|
|
392
|
-
if (event.type !== 'build.created' && event.type !== 'spec.revised') continue
|
|
393
|
-
const assets = event.payload.assets
|
|
394
|
-
if (assets === undefined) continue
|
|
416
|
+
for (const assets of state.pins) {
|
|
395
417
|
if (rev !== undefined) {
|
|
396
418
|
if (assets.some((a) => a.kind === kind && a.name === name && a.revision === rev)) return rev
|
|
397
419
|
continue
|
|
@@ -403,6 +425,17 @@ export function findPinnedRevision(
|
|
|
403
425
|
return latest
|
|
404
426
|
}
|
|
405
427
|
|
|
428
|
+
/** Latest-pinned revision of `kind/name` as the build's events record it, or
|
|
429
|
+
* (with `rev`) the revision when any pin in the log named it. */
|
|
430
|
+
export function findPinnedRevision(
|
|
431
|
+
events: AbEvent[],
|
|
432
|
+
kind: string,
|
|
433
|
+
name: string,
|
|
434
|
+
rev?: number,
|
|
435
|
+
): number | undefined {
|
|
436
|
+
return pinnedRevision(pinnedAssetsReducer.reduce(events), kind, name, rev)
|
|
437
|
+
}
|
|
438
|
+
|
|
406
439
|
/** `BuildStore.getPinnedTicketAsset` for any adapter: read the build's own
|
|
407
440
|
* events to find the pinned revision, then fetch exactly that revision from the
|
|
408
441
|
* ticket's assets. Earlier revisions stay retrievable after a replace or a
|
|
@@ -26,6 +26,8 @@ import type {
|
|
|
26
26
|
} from '../events/sessions'
|
|
27
27
|
export type { SessionEventEnvelope }
|
|
28
28
|
import type { TicketRef } from '../ontology'
|
|
29
|
+
import type { ReducerSnapshot, SnapshotScope } from './snapshots'
|
|
30
|
+
export type { ReducerSnapshot, SnapshotScope }
|
|
29
31
|
import type {
|
|
30
32
|
TicketAsset,
|
|
31
33
|
TicketAssetInput,
|
|
@@ -115,6 +117,12 @@ export interface BuildDigest {
|
|
|
115
117
|
* when any of its merges is strictly after the oldest unclaimed
|
|
116
118
|
* observation). */
|
|
117
119
|
merged?: string
|
|
120
|
+
/** A `pr-attachment.hosted` has no later matching `pr-attachment.reclaimed`
|
|
121
|
+
* (the `pendingPrAttachmentReclaims` rule). Absent otherwise. */
|
|
122
|
+
reclaimPending?: true
|
|
123
|
+
/** The latest `execution.started` has no later `execution.ended` with the
|
|
124
|
+
* same `instance` (the `openExecution` rule). Absent otherwise. */
|
|
125
|
+
executionOpen?: true
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
export interface RepositoryRecord {
|
|
@@ -365,6 +373,36 @@ export interface BuildStore {
|
|
|
365
373
|
* replay yields (`projectRepositoryStateEvents` is the normative oracle).
|
|
366
374
|
* Unknown repo rejects like `getRepoEvents`. */
|
|
367
375
|
getRepoStateEvents(repo: string): Promise<RepositoryEvent[]>
|
|
376
|
+
/** The journal's true high-water mark: the greatest assigned seq of any
|
|
377
|
+
* event type, 0 for an empty journal. Returns no event rows — the cursor a
|
|
378
|
+
* long-lived reader (the dispatcher's repository view) needs to resume with
|
|
379
|
+
* `getRepoEvents(repo, cursor)` even when the bounded subset excludes the
|
|
380
|
+
* journal's trailing events. Unknown repo rejects like `getRepoEvents`;
|
|
381
|
+
* never an `ensureRepo` side effect. */
|
|
382
|
+
getRepoHighWater(repo: string): Promise<number>
|
|
383
|
+
/** The stored reducer snapshot for a scope (a build log or a repository
|
|
384
|
+
* journal), a cache beside the log and never an authority. `null` when
|
|
385
|
+
* absent, when the stored version differs from `version`, when the stored
|
|
386
|
+
* cursor is ahead of the scope's log tail, or when the scope is unknown (no
|
|
387
|
+
* `ensureRepo` side effect, no rejection). A store without snapshot storage
|
|
388
|
+
* answers `null`. */
|
|
389
|
+
getReducerSnapshot(
|
|
390
|
+
scope: SnapshotScope,
|
|
391
|
+
reducer: string,
|
|
392
|
+
version: number,
|
|
393
|
+
): Promise<ReducerSnapshot | null>
|
|
394
|
+
/** Upsert a reducer snapshot; the stored cursor never decreases. Writes only
|
|
395
|
+
* when no row exists, when `cursor` is above the stored one (whatever the
|
|
396
|
+
* versions — a mismatched row is replaced as soon as any writer advances
|
|
397
|
+
* past it), or when the cursor is equal and `version` is higher. Everything
|
|
398
|
+
* else, and any cursor beyond the log tail, is a no-op. Atomic, so
|
|
399
|
+
* concurrent writers cannot undo each other's progress. Resolves `true`
|
|
400
|
+
* when the row was written. */
|
|
401
|
+
putReducerSnapshot(
|
|
402
|
+
scope: SnapshotScope,
|
|
403
|
+
reducer: string,
|
|
404
|
+
snapshot: ReducerSnapshot,
|
|
405
|
+
): Promise<boolean>
|
|
368
406
|
putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta>
|
|
369
407
|
getRepoArtifact(repo: string, kind: string, rev?: number): Promise<RepositoryArtifact | null>
|
|
370
408
|
listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]>
|
package/skills/guide/SKILL.md
CHANGED
|
@@ -732,7 +732,8 @@ state eligible.
|
|
|
732
732
|
|---|---|---|---|
|
|
733
733
|
| `source` | — | **required**, nonblank builtin or plugin registration name | Which provider backs all ticket operations. Unknown names report every available builtin and plugin source. |
|
|
734
734
|
| `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
|
|
735
|
-
| `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file
|
|
735
|
+
| `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
|
|
736
|
+
| `readyAssignee` | — | optional; `"me"` or an email; `linear` only | Also require the ticket to be assigned to that Linear user (`me` = owner of `LINEAR_API_KEY`); unassigned tickets are never ready. |
|
|
736
737
|
| `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
|
|
737
738
|
| `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
|
|
738
739
|
| `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
|
|
@@ -772,11 +773,16 @@ Cross-field rules, each an **error**:
|
|
|
772
773
|
janitor completion. Every source-agnostic `ab ticket` command loads and uses
|
|
773
774
|
the same configured registration.
|
|
774
775
|
|
|
775
|
-
The file tracker is **directory-per-state**: `<dir>/<state>/<id>.md
|
|
776
|
-
|
|
776
|
+
The file tracker is **directory-per-state**: `<dir>/<state>/<id>.md`, where
|
|
777
|
+
every directory directly under the root is a state: the four lifecycle
|
|
778
|
+
directories `triage/ ready/ doing/ done/` (always present, created on first use)
|
|
779
|
+
plus any others the repository adds, such as `icebox/`. A state exists when its
|
|
780
|
+
directory exists; state names match directories case-insensitively, and a name
|
|
781
|
+
with no directory is an error listing those that exist. The directory *is* the
|
|
782
|
+
state, so a transition or
|
|
777
783
|
claim is a rename — frontmatter carries no `state`/`claimedBy`, and a ticket
|
|
778
784
|
body survives byte-exactly because a move never rewrites the file. A malformed
|
|
779
|
-
record in any
|
|
785
|
+
record in any state directory is diagnosed with its path and validation
|
|
780
786
|
problem, left untouched, and excluded from listing/claim so unrelated valid
|
|
781
787
|
ready work can continue. `ab ticket list` sends those diagnostics to stderr and
|
|
782
788
|
keeps JSON stdout as one bare valid-ticket array. Tracker-wide safety checks
|
|
@@ -784,8 +790,8 @@ remain fatal: the same id in two state dirs is a loud error naming both paths,
|
|
|
784
790
|
as are root-level ticket files with no state and filesystem failures. When
|
|
785
791
|
`dir` is defaulted, the backlog writes its own `.gitignore` of `*`, so git never sees it; an
|
|
786
792
|
explicit `dir` is the user's and is left alone. Agents and operators drive it
|
|
787
|
-
through the source-agnostic `ab ticket` commands rather than running `mv`
|
|
788
|
-
hand.
|
|
793
|
+
through the source-agnostic `ab ticket` commands rather than running `mv` or
|
|
794
|
+
`mkdir` by hand; `ab ticket state create` makes a new state.
|
|
789
795
|
|
|
790
796
|
**Secrets never live in this file.** `LINEAR_API_KEY` and plugin-declared
|
|
791
797
|
`requiredEnv` credentials are environment variables (a local `.env` works).
|
|
@@ -1063,6 +1069,19 @@ with Linear and the file tracker, without provider-specific API/MCP calls:
|
|
|
1063
1069
|
labeled metadata and the body verbatim, so it can read a stored spec back.
|
|
1064
1070
|
- `ab ticket move <id> <state> [--json]` transitions one ticket and reports its
|
|
1065
1071
|
post-transition value.
|
|
1072
|
+
- `ab ticket states [--json]` lists every state of the configured source:
|
|
1073
|
+
lifecycle states first (triage, ready, doing, done), then custom states
|
|
1074
|
+
alphabetically. Each row has the state name, the roles it plays under the
|
|
1075
|
+
current config (create, ready, claimed, triage, proposal, done, or none), its
|
|
1076
|
+
ticket count, and its purpose when set. `--json` emits one bare array of
|
|
1077
|
+
`{name, roles, tickets, about?}`.
|
|
1078
|
+
- `ab ticket state create <name> [--about <text>] [--json]` makes a state, for
|
|
1079
|
+
example `icebox`. It is idempotent: an existing state is reported as already
|
|
1080
|
+
existing, and `--about` (one line) records or replaces its purpose. A name is
|
|
1081
|
+
one to forty lowercase letters, digits, and hyphens starting with a letter,
|
|
1082
|
+
and may not match an existing state case-insensitively. There is no rename or
|
|
1083
|
+
delete. Only the file source supports `states` and `state create`; other
|
|
1084
|
+
sources exit nonzero naming the source and the unsupported operation.
|
|
1066
1085
|
|
|
1067
1086
|
State names and ids are source-local. For block and unblock, the first id is the
|
|
1068
1087
|
ticket being changed and the comma-separated second operand names its blockers.
|
|
@@ -1849,7 +1868,7 @@ default, when you need to know what this repo's version says).
|
|
|
1849
1868
|
| Skill | Place in the lifecycle | Purpose |
|
|
1850
1869
|
|---|---|---|
|
|
1851
1870
|
| `ab-spec` | Before a build exists | Design a feature spec-first through conversation, or flesh out a ticket to the spec standard. The human-interactive surface; takes a ticket, not a build slug. **Model-invocable.** |
|
|
1852
|
-
| `ab-tickets` | Before a build exists | Drive this repo's local file tracker: create a ticket, report the backlog, groom or move one between `triage/ ready/ doing/ done
|
|
1871
|
+
| `ab-tickets` | Before a build exists | Drive this repo's local file tracker: create a ticket, report the backlog, groom or move one between the lifecycle directories (`triage/ ready/ doing/ done/`) and any repository-defined states, including the icebox. The agent-facing surface on the tracker — use it instead of `mv`. **Model-invocable.** |
|
|
1853
1872
|
| `ab-guide` | Outside the pipeline | This skill: reference for the lifecycle, config surface, setup/upgrade behavior, and the installed skills. Its editable `references/setup.md` guides the init handoff. **Model-invocable.** |
|
|
1854
1873
|
| `ab-harvest` | harvest `synthesize` step | Continue the producer across review rounds: cluster the claimed structured observations and author typed spec-standard create/join/suppress proposals. Runner-only. |
|
|
1855
1874
|
| `ab-harvest-review` | harvest `review` step | Fresh adversarial reviewer for proposal coverage, semantic dedup, spec quality, and evidence; returns `approve`/`revise`/`escalate`. Runner-only. |
|
|
@@ -297,6 +297,8 @@ it to exist. An unknown build returns `404 not-found`.
|
|
|
297
297
|
| `appendIfCurrent` | `POST /builds/{slug}/events/conditional` | `{"expectedSeq": nonnegative integer, "event": event write}` | appended: `201` + build event envelope; stream advanced: `200 null` |
|
|
298
298
|
| `getEvents` | `GET /builds/{slug}/events?since={n}&wait={s}` | optional `since` query value, parsed below; absence defaults to `0`. Optional `wait` in whole seconds, parsed per the event-wait rules below | `200` + envelopes whose `seq` is strictly greater than parsed `since`, in increasing sequence order |
|
|
299
299
|
| `appendWithArtifacts` | `POST /builds/{slug}/deposits` | atomic deposit request | `201` + `{event, artifacts}` |
|
|
300
|
+
| `getReducerSnapshot` | `GET /builds/{slug}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, or ahead of the build log's tail; an unknown build is `404`, which the shipped client also maps to `null` |
|
|
301
|
+
| `putReducerSnapshot` | `PUT /builds/{slug}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the log's tail answer `written: false` |
|
|
300
302
|
| `putArtifact` | `POST /builds/{slug}/artifacts` | artifact input | `201` + build artifact metadata |
|
|
301
303
|
| `getArtifact` | `GET /builds/{slug}/artifacts?kind={kind}&rev={n}` | `kind` is required and nonempty; optional `rev` is parsed below | `200` + artifact read; an absent `rev` parameter selects the latest revision; a missing kind/revision is `200 null` |
|
|
302
304
|
| `listArtifacts` | `GET /builds/{slug}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
|
|
@@ -336,6 +338,32 @@ part of this protocol. Retention is enforced server-side inside the store at
|
|
|
336
338
|
deposit time (bounded pruning of dispatcher run/config artifact revisions),
|
|
337
339
|
not through any endpoint.
|
|
338
340
|
|
|
341
|
+
### Reducer snapshots
|
|
342
|
+
|
|
343
|
+
A reducer snapshot is one registered reducer's serialized state as of a log
|
|
344
|
+
cursor, kept per scope (one build log or one repository journal) and reducer
|
|
345
|
+
name. It is a cache the log regenerates, never an authority: a process with no
|
|
346
|
+
hot memory loads it, reads only the events newer than its cursor, advances the
|
|
347
|
+
reducer, and writes the advanced state back, and deleting every snapshot changes
|
|
348
|
+
no observable output. The routes above and in section 4 are additive and carry
|
|
349
|
+
no protocol-version bump. The store, not the client, enforces the invariants:
|
|
350
|
+
|
|
351
|
+
- the stored cursor never decreases, whatever the versions of the competing
|
|
352
|
+
writers; a write succeeds only when no row exists, when its cursor is above the
|
|
353
|
+
stored one, or when its cursor equals the stored one and its version is higher
|
|
354
|
+
(an upgraded reducer replacing the old shape at an unchanged tail);
|
|
355
|
+
- a read answers `null` for a row whose version differs from the requested one,
|
|
356
|
+
and for one whose cursor is beyond the scope's log tail; a write beyond the
|
|
357
|
+
tail is refused;
|
|
358
|
+
- an unknown build or repository answers `null` (read) or `written: false`
|
|
359
|
+
(write) and never creates the scope.
|
|
360
|
+
|
|
361
|
+
A client treats `404` and `405` from either route as "no snapshot", so a client
|
|
362
|
+
newer than its store replays the log with no error, and a client older than its
|
|
363
|
+
store never calls the routes. Authentication and transport failures still
|
|
364
|
+
surface normally. A build token may touch only its own build's snapshots and a
|
|
365
|
+
repository token only its own repository's, exactly as for events.
|
|
366
|
+
|
|
339
367
|
### The event-read bounded wait
|
|
340
368
|
|
|
341
369
|
Both event reads (`getEvents` and, symmetrically, the `getRepoEvents` read of
|
|
@@ -382,6 +410,8 @@ without a journal record, or a repository without builds.
|
|
|
382
410
|
| `appendRepo` | `POST /repos/{repo}/events` | event write | `201` + repository event envelope |
|
|
383
411
|
| `getRepoEvents` | `GET /repos/{repo}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + envelopes with `seq >` parsed `since`, in increasing sequence order |
|
|
384
412
|
| `getRepoStateEvents` | `GET /repos/{repo}/state-events` | none | `200` + the bounded repository-journal subset (AUT-489): every durable event type (`harvest.*`, `orchestrator.sandbox.*`, the three dispatcher setting types) plus, when the journal has one, the tail from the latest `dispatcher.run-started`, in increasing sequence order; served inside the repository-existence gate like `GET events`, so an unknown repository is `404` |
|
|
413
|
+
| `getReducerSnapshot` (repo scope) | `GET /repos/{repo}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, ahead of the journal's tail, or the repository has no journal record; an unknown repository is `404`, which the shipped client also maps to `null` |
|
|
414
|
+
| `putReducerSnapshot` (repo scope) | `PUT /repos/{repo}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the journal's tail answer `written: false` |
|
|
385
415
|
| `appendRepoWithArtifacts` | `POST /repos/{repo}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using repository shapes |
|
|
386
416
|
| `putRepoArtifact` | `POST /repos/{repo}/artifacts` | artifact input | `201` + repository artifact metadata |
|
|
387
417
|
| `getRepoArtifact` | `GET /repos/{repo}/artifacts?kind={kind}&rev={n}` | required nonempty `kind`; optional `rev` is parsed as in section 3 | `200` + artifact read; latest only when the `rev` parameter is absent; missing kind/revision is `200 null` |
|
|
@@ -834,6 +864,11 @@ resource access; an explicit resource whose id is `"*"` is not admin.
|
|
|
834
864
|
| repo | no | no | no | exact id only | yes | exact id only | exact id only | exact repo only | no |
|
|
835
865
|
| session | no | no | no | no | no | no | no | no | exact id only |
|
|
836
866
|
|
|
867
|
+
The snapshot routes (`/builds/{slug}/snapshots/{reducer}` and
|
|
868
|
+
`/repos/{repo}/snapshots/{reducer}`) follow the one-matching-build and
|
|
869
|
+
one-matching-repo columns: a build token reads and writes only its own build's
|
|
870
|
+
snapshots, a repo token only its own repository's.
|
|
871
|
+
|
|
837
872
|
A valid token used for the wrong resource receives `403 auth`. Resource scope
|
|
838
873
|
gates all operations, including reads, artifact operations, and leases. A
|
|
839
874
|
session resource token's authority is exactly its one session — it cannot
|
package/skills/tickets/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tickets
|
|
3
|
-
description: Work this repo's local ticket tracker — create or edit a ticket, amend blockedBy dependencies, report the backlog, move work between
|
|
3
|
+
description: Work this repo's local ticket tracker — create or edit a ticket, amend blockedBy dependencies, report the backlog, triage the inbox, move work between the lifecycle directories and any repository-defined states such as the icebox, or answer "what's the status of ticket X". Use whenever the user asks about tickets, blockers, the backlog, or wants something queued for autobuild to build.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /tickets
|
|
@@ -12,7 +12,11 @@ content and blocker relationships. The default file source needs no secret.
|
|
|
12
12
|
## Where it lives
|
|
13
13
|
|
|
14
14
|
`.autobuild/tickets/` by default (if `autobuild.toml` has a `[tickets] dir`,
|
|
15
|
-
that directory instead), holding
|
|
15
|
+
that directory instead), holding the four lifecycle state directories plus any others this repo adds.
|
|
16
|
+
`triage/`, `ready/`, `doing/`, and `done/` are the lifecycle directories the
|
|
17
|
+
dispatcher uses. Any other directory is a repository-defined state: the
|
|
18
|
+
dispatcher never moves tickets into it and never dispatches from it unless
|
|
19
|
+
`autobuild.toml` names it (for example `readyState` or `proposalState`).
|
|
16
20
|
|
|
17
21
|
```
|
|
18
22
|
.autobuild/tickets/
|
|
@@ -20,6 +24,7 @@ that directory instead), holding exactly four state directories:
|
|
|
20
24
|
ready/ # groomed; file lifecycle-state gate satisfied
|
|
21
25
|
doing/ # claimed; a build is running
|
|
22
26
|
done/ # merged
|
|
27
|
+
icebox/ # example of a repo-added state, present only once created
|
|
23
28
|
```
|
|
24
29
|
|
|
25
30
|
**A ticket's state is the directory it is in.** There is no `state` field
|
|
@@ -71,13 +76,17 @@ retry, and adding validates the blocker exists and is not the ticket itself.
|
|
|
71
76
|
## Report the backlog
|
|
72
77
|
|
|
73
78
|
```
|
|
79
|
+
ab ticket states # every state, lifecycle and custom: role, count, purpose
|
|
74
80
|
ls .autobuild/tickets/ready # groomed candidates in the ready lifecycle state
|
|
75
81
|
ls .autobuild/tickets/doing # what's building right now
|
|
76
82
|
```
|
|
77
83
|
|
|
78
84
|
The filename is the id. For a title, read the file's frontmatter. To report
|
|
79
|
-
the whole backlog,
|
|
80
|
-
|
|
85
|
+
the whole backlog, cover every state — lifecycle and custom — via
|
|
86
|
+
`ab ticket states` or by listing every directory under the tracker root, so the
|
|
87
|
+
report includes `icebox/` and any other category. `ab ticket list` without
|
|
88
|
+
filters shows only the ready state, regardless of custom states, so it is not a
|
|
89
|
+
backlog report.
|
|
81
90
|
|
|
82
91
|
## Groom / transition a ticket
|
|
83
92
|
|
|
@@ -113,6 +122,49 @@ bounces.
|
|
|
113
122
|
**Don't hand-move anything out of `doing/`** — a build owns it. If you need to
|
|
114
123
|
stop a build, that's `ab` (or a human), not `mv`.
|
|
115
124
|
|
|
125
|
+
## Icebox
|
|
126
|
+
|
|
127
|
+
`icebox/` is work deliberately deferred, groomed or not, that nothing
|
|
128
|
+
dispatches from. The first time it is needed, create it:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
ab ticket state create icebox --about "Deferred on purpose; nothing dispatches from here"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Move a ticket in with `ab ticket move file-3 icebox`. To revive one, move it to
|
|
135
|
+
`triage/` when it needs regrooming, or to `ready/` when its spec still holds.
|
|
136
|
+
|
|
137
|
+
## Triage the backlog
|
|
138
|
+
|
|
139
|
+
To run a pass over `triage/`:
|
|
140
|
+
|
|
141
|
+
1. Run `ab ticket states` to see which destinations exist.
|
|
142
|
+
2. For each ticket in `triage/`, read it with `ab ticket show <id>` and judge
|
|
143
|
+
whether its body meets the
|
|
144
|
+
[spec standard](../ab-guide/references/spec-standard.md).
|
|
145
|
+
3. Move it to `ready/` if it conforms and should be built; to `icebox/` if it
|
|
146
|
+
should wait; to another existing category if one fits; otherwise leave it in
|
|
147
|
+
`triage/` and tell the user why.
|
|
148
|
+
4. Report every move to the user in the session — id, title, destination, and
|
|
149
|
+
reason. A move leaves no comment on the ticket, so the report is the only
|
|
150
|
+
record of why.
|
|
151
|
+
|
|
152
|
+
## Inventing categories
|
|
153
|
+
|
|
154
|
+
Run `ab ticket states` before creating anything. Reuse an existing state whose
|
|
155
|
+
purpose fits. Create a new one only for a recurring reason that no existing
|
|
156
|
+
state covers, with a lowercase hyphenated noun name and a one-line `--about`
|
|
157
|
+
purpose. `icebox` is the first worked example; `rejected` is a second:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
ab ticket state create rejected --about "Considered and declined; kept for the record"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Never invent a second ready, doing, or done state, never move anything out of
|
|
164
|
+
`doing/`, and never create states for a build's internal progress — the
|
|
165
|
+
lifecycle belongs to the dispatcher. Remember that `ab ticket list` without
|
|
166
|
+
filters still shows only the ready state, however many custom states exist.
|
|
167
|
+
|
|
116
168
|
## Rules
|
|
117
169
|
|
|
118
170
|
- Never edit frontmatter to change state — the directory is the state.
|
|
@@ -132,7 +184,7 @@ listing under default config. That does not make `ready/` sufficient;
|
|
|
132
184
|
unresolved blockers are an independent dispatch gate.
|
|
133
185
|
|
|
134
186
|
No Linear lifecycle UI. Directory-based lifecycle operations (`ls`, `mv`, and
|
|
135
|
-
the
|
|
187
|
+
the state directories) are file-tracker-only. If this repo's
|
|
136
188
|
`autobuild.toml` sets `[tickets] source = "linear"`, do not apply that lifecycle
|
|
137
189
|
guidance: state and labels live in Linear. The source-agnostic `ab ticket block`
|
|
138
190
|
and `ab ticket unblock` commands still edit blocker relationships through the
|