@defrex/autobuild 0.11.0 → 0.13.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 +3 -1
- package/SPEC.md +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +15 -1
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- 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 +172 -35
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- 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 -3
- 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 +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -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 +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- 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 +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* validated.
|
|
13
13
|
*/
|
|
14
14
|
import { z } from 'zod'
|
|
15
|
+
import { normalizeServiceURL } from '../connection'
|
|
15
16
|
import { prImageHostSchema } from '../ontology'
|
|
16
17
|
import { SANDBOX_FORBIDDEN_ENV } from '../ports/workspace/operator-sandbox'
|
|
17
18
|
import {
|
|
@@ -60,14 +61,22 @@ function stepSection<T, C>(
|
|
|
60
61
|
label: string,
|
|
61
62
|
stepsSchema: z.ZodType<string[]>,
|
|
62
63
|
tableSchema: z.ZodType<T>,
|
|
63
|
-
compose: (steps: string[], stepConfigs: Record<string, T
|
|
64
|
+
compose: (steps: string[], stepConfigs: Record<string, T>, afterReconcile: boolean) => C,
|
|
65
|
+
allowAfterReconcile = false,
|
|
64
66
|
) {
|
|
65
67
|
return z
|
|
66
68
|
.unknown()
|
|
67
69
|
.transform((input, ctx): C => {
|
|
68
70
|
const stepConfigs: Record<string, T> = Object.create(null)
|
|
69
71
|
let steps: string[] = []
|
|
72
|
+
let afterReconcile = true
|
|
70
73
|
for (const [key, raw] of ownEntries(input, label, ctx)) {
|
|
74
|
+
if (key === 'afterReconcile' && allowAfterReconcile) {
|
|
75
|
+
const parsed = z.boolean().safeParse(raw)
|
|
76
|
+
if (parsed.success) afterReconcile = parsed.data
|
|
77
|
+
else forwardIssues(parsed.error.issues, ctx, [key])
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
71
80
|
if (key === 'steps') {
|
|
72
81
|
const parsed = stepsSchema.safeParse(raw)
|
|
73
82
|
if (parsed.success) steps = parsed.data
|
|
@@ -77,7 +86,7 @@ function stepSection<T, C>(
|
|
|
77
86
|
const table = parseEntry(tableSchema, raw, key, ctx)
|
|
78
87
|
if (table.ok) defineEntry(stepConfigs, key, table.value)
|
|
79
88
|
}
|
|
80
|
-
return compose(steps, stepConfigs)
|
|
89
|
+
return compose(steps, stepConfigs, afterReconcile)
|
|
81
90
|
})
|
|
82
91
|
.prefault({})
|
|
83
92
|
}
|
|
@@ -367,6 +376,7 @@ export const verifyPathGlobSchema = z.string().superRefine((pattern, ctx) => {
|
|
|
367
376
|
})
|
|
368
377
|
|
|
369
378
|
const verifyApplicabilityShape = {
|
|
379
|
+
afterReconcile: z.boolean().optional(),
|
|
370
380
|
/** Any-match positive path selectors; absent means unconditional. */
|
|
371
381
|
paths: z
|
|
372
382
|
.array(verifyPathGlobSchema)
|
|
@@ -399,6 +409,7 @@ export const verifyStepConfigSchema = z.discriminatedUnion('kind', [
|
|
|
399
409
|
export type VerifyStepConfig = z.infer<typeof verifyStepConfigSchema>
|
|
400
410
|
|
|
401
411
|
export interface VerifyConfig {
|
|
412
|
+
afterReconcile: boolean
|
|
402
413
|
/** Step order — each entry names a `verify:<step>` phase (§15.2.1). */
|
|
403
414
|
steps: string[]
|
|
404
415
|
/** The [verify.<step>] tables, keyed by step name. */
|
|
@@ -406,8 +417,8 @@ export interface VerifyConfig {
|
|
|
406
417
|
}
|
|
407
418
|
|
|
408
419
|
/**
|
|
409
|
-
* [verify] mixes
|
|
410
|
-
* subtables, so it cannot be a strictObject;
|
|
420
|
+
* [verify] mixes known scalars (`steps`, `afterReconcile`) with per-step
|
|
421
|
+
* subtables, so it cannot be a strictObject; every other key is
|
|
411
422
|
* validated as a step table — nothing passes through loosely, and no step name
|
|
412
423
|
* is lost to the prototype chain (see `stepSection`).
|
|
413
424
|
*/
|
|
@@ -415,7 +426,8 @@ export const verifySectionSchema = stepSection(
|
|
|
415
426
|
'[verify]',
|
|
416
427
|
z.array(z.string().min(1)).default([]),
|
|
417
428
|
verifyStepConfigSchema,
|
|
418
|
-
(steps, stepConfigs): VerifyConfig => ({ steps, stepConfigs }),
|
|
429
|
+
(steps, stepConfigs, afterReconcile): VerifyConfig => ({ steps, stepConfigs, afterReconcile }),
|
|
430
|
+
true,
|
|
419
431
|
)
|
|
420
432
|
|
|
421
433
|
// ── [finalize.<step>] ────────────────────────────────────────────────────────
|
|
@@ -500,6 +512,8 @@ export const policySchema = z.strictObject({
|
|
|
500
512
|
/** Same-finding survival threshold before auto-escalate (§10, §15.4). */
|
|
501
513
|
stallRounds: z.number().int().positive().default(3),
|
|
502
514
|
maxVerifyAttempts: z.number().int().positive().default(3),
|
|
515
|
+
/** Consecutive dispatch failures before human intervention is required. */
|
|
516
|
+
maxDispatchAttempts: z.number().int().positive().default(3),
|
|
503
517
|
/** Consecutive workspace setup failures before human intervention is required. */
|
|
504
518
|
maxSetupAttempts: z.number().int().positive().default(3),
|
|
505
519
|
/** Consecutive provider lifecycle failures before recovery requires a human retry. */
|
|
@@ -521,7 +535,8 @@ export type PolicyConfig = z.infer<typeof policySchema>
|
|
|
521
535
|
// API key comes from the LINEAR_API_KEY environment variable, never from this
|
|
522
536
|
// file.
|
|
523
537
|
|
|
524
|
-
export const
|
|
538
|
+
export const ticketsInputSchema = z.strictObject({
|
|
539
|
+
url: z.string().optional(),
|
|
525
540
|
/** Builtin or plugin-registered TicketSource name. Registry membership is
|
|
526
541
|
* validated after configured plugins load. */
|
|
527
542
|
source: z
|
|
@@ -555,7 +570,8 @@ export const ticketsSchema = z.strictObject({
|
|
|
555
570
|
.refine(
|
|
556
571
|
(s) => s.trim().length > 0,
|
|
557
572
|
'[tickets].readyState must not be blank — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
|
|
558
|
-
)
|
|
573
|
+
)
|
|
574
|
+
.optional(),
|
|
559
575
|
/**
|
|
560
576
|
* Narrows readiness to tickets assigned to one Linear user — a third
|
|
561
577
|
* conjunctive gate beside readyState and readyLabels. `"me"` is the owner of
|
|
@@ -573,15 +589,33 @@ export const ticketsSchema = z.strictObject({
|
|
|
573
589
|
/** Backend team key (e.g. "ENG") — required for linear and hosted. */
|
|
574
590
|
teamKey: z.string().min(1).optional(),
|
|
575
591
|
/** Workflow state claim() moves an issue to (§12); Linear and hosted. */
|
|
576
|
-
claimedState: z
|
|
592
|
+
claimedState: z
|
|
593
|
+
.string()
|
|
594
|
+
.min(1)
|
|
595
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
596
|
+
.optional(),
|
|
597
|
+
/** Hosted project completion state; defaults to done. */
|
|
598
|
+
doneState: z
|
|
599
|
+
.string()
|
|
600
|
+
.min(1)
|
|
601
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
602
|
+
.optional(),
|
|
577
603
|
/** State create() files new tickets into. Absent = the provider's default
|
|
578
604
|
* (Linear: the team's default state, e.g. Backlog; file: Triage). */
|
|
579
|
-
createState: z
|
|
605
|
+
createState: z
|
|
606
|
+
.string()
|
|
607
|
+
.min(1)
|
|
608
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
609
|
+
.optional(),
|
|
580
610
|
/** State the dispatcher hands tickets back to for human triage — spec-gate
|
|
581
611
|
* bounces (§6.3), aborted builds, closed-unmerged PRs. Absent = the
|
|
582
612
|
* provider's default (Linear: Backlog; file: Triage) — resolved by
|
|
583
613
|
* defaultTriageState in src/processes/dispatcher.ts. */
|
|
584
|
-
triageState: z
|
|
614
|
+
triageState: z
|
|
615
|
+
.string()
|
|
616
|
+
.min(1)
|
|
617
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
618
|
+
.optional(),
|
|
585
619
|
/** State harvest files its synthesized proposals into (§12). Absent = the
|
|
586
620
|
* triage state, which keeps the constitutional grooming gate: a proposal
|
|
587
621
|
* waits for a human before it can be dispatched. Setting it to readyState
|
|
@@ -590,7 +624,11 @@ export const ticketsSchema = z.strictObject({
|
|
|
590
624
|
* does not also redirect spec-gate bounces, aborts, and closed-unmerged PRs
|
|
591
625
|
* into the ready state, where a bounce would re-dispatch itself. Resolved by
|
|
592
626
|
* defaultProposalState in src/processes/dispatcher.ts. */
|
|
593
|
-
proposalState: z
|
|
627
|
+
proposalState: z
|
|
628
|
+
.string()
|
|
629
|
+
.min(1)
|
|
630
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank')
|
|
631
|
+
.optional(),
|
|
594
632
|
/** Directory of state dirs (`triage/ ready/ doing/ done/`) holding `<id>.md`
|
|
595
633
|
* ticket files; optional — defaults to `.autobuild/tickets`, resolved
|
|
596
634
|
* relative to the repo. Kept schema-optional (not `.default()`) so the
|
|
@@ -598,6 +636,36 @@ export const ticketsSchema = z.strictObject({
|
|
|
598
636
|
* still tell a defaulted dir from an explicit one. */
|
|
599
637
|
dir: z.string().min(1).optional(),
|
|
600
638
|
})
|
|
639
|
+
export const ticketsSchema = ticketsInputSchema
|
|
640
|
+
.superRefine((tickets, ctx) => {
|
|
641
|
+
if (tickets.source === 'hosted' && tickets.url !== undefined) {
|
|
642
|
+
try {
|
|
643
|
+
normalizeServiceURL(tickets.url, 'tickets.url')
|
|
644
|
+
} catch (error) {
|
|
645
|
+
ctx.addIssue({ code: 'custom', path: ['url'], message: (error as Error).message })
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (tickets.source !== 'hosted' && tickets.readyState === undefined)
|
|
649
|
+
ctx.addIssue({
|
|
650
|
+
code: 'custom',
|
|
651
|
+
path: ['readyState'],
|
|
652
|
+
message:
|
|
653
|
+
'[tickets].readyState is required — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
|
|
654
|
+
})
|
|
655
|
+
if (['file', 'linear'].includes(tickets.source) && tickets.url !== undefined)
|
|
656
|
+
ctx.addIssue({
|
|
657
|
+
code: 'custom',
|
|
658
|
+
path: ['url'],
|
|
659
|
+
message: '[tickets].url applies only to source = "hosted"',
|
|
660
|
+
})
|
|
661
|
+
})
|
|
662
|
+
.transform((tickets) => ({
|
|
663
|
+
...tickets,
|
|
664
|
+
readyState: tickets.readyState ?? 'ready',
|
|
665
|
+
...(tickets.source === 'hosted' && tickets.url !== undefined
|
|
666
|
+
? { url: normalizeServiceURL(tickets.url, 'tickets.url') }
|
|
667
|
+
: {}),
|
|
668
|
+
}))
|
|
601
669
|
export type TicketsConfig = z.infer<typeof ticketsSchema>
|
|
602
670
|
|
|
603
671
|
// ── [orchestrator] ──────────────────────────────────────────────────────────
|
|
@@ -725,7 +793,18 @@ export type OrchestratorConfig = z.infer<typeof orchestratorSchema>
|
|
|
725
793
|
|
|
726
794
|
// ── Whole file ───────────────────────────────────────────────────────────────
|
|
727
795
|
|
|
796
|
+
export const storeSchema = z.strictObject({
|
|
797
|
+
url: z.string().transform((value, ctx) => {
|
|
798
|
+
try {
|
|
799
|
+
return normalizeServiceURL(value)
|
|
800
|
+
} catch (error) {
|
|
801
|
+
ctx.addIssue({ code: 'custom', message: (error as Error).message })
|
|
802
|
+
return z.NEVER
|
|
803
|
+
}
|
|
804
|
+
}),
|
|
805
|
+
})
|
|
728
806
|
const configRootSchema = z.strictObject({
|
|
807
|
+
store: storeSchema.optional(),
|
|
729
808
|
/** Base branch builds cut from and target (§16.1). */
|
|
730
809
|
baseBranch: z.string().min(1).default('main'),
|
|
731
810
|
/** Concurrent builds for this repository (§16.1). */
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { normalizeGitRemoteUrl } from './kernel/origin'
|
|
2
|
+
/** Public device-approval protocol. Secrets travel only in capability POST bodies. */
|
|
3
|
+
export type ConnectionStatus = 'pending' | 'approved' | 'denied' | 'expired'
|
|
4
|
+
export interface ConnectionProposal {
|
|
5
|
+
repo: string
|
|
6
|
+
credentialId: string
|
|
7
|
+
digest: string
|
|
8
|
+
}
|
|
9
|
+
export interface ConnectionOutcome {
|
|
10
|
+
repo: string
|
|
11
|
+
credentialId: string
|
|
12
|
+
status: ConnectionStatus
|
|
13
|
+
}
|
|
14
|
+
export interface ConnectionStarted extends ConnectionOutcome {
|
|
15
|
+
id: string
|
|
16
|
+
capability: string
|
|
17
|
+
code: string
|
|
18
|
+
verificationURL: string
|
|
19
|
+
interval: number
|
|
20
|
+
expiresAt: string
|
|
21
|
+
}
|
|
22
|
+
export function normalizeServiceURL(raw: string, setting = 'store.url'): string {
|
|
23
|
+
let url: URL
|
|
24
|
+
try {
|
|
25
|
+
url = new URL(raw)
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error(`${setting} must be an absolute HTTPS service URL`)
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
!raw.trim() ||
|
|
31
|
+
raw !== raw.trim() ||
|
|
32
|
+
url.username ||
|
|
33
|
+
url.password ||
|
|
34
|
+
url.search ||
|
|
35
|
+
url.hash ||
|
|
36
|
+
!(
|
|
37
|
+
url.protocol === 'https:' ||
|
|
38
|
+
(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
39
|
+
)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${setting} must be HTTPS (HTTP is allowed on loopback), without credentials, query or fragment`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
return url.href.replace(/\/+$/, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeConnectionRepository(raw: string): string {
|
|
49
|
+
const value = raw.trim()
|
|
50
|
+
// SSH's conventional git user is transport syntax; URL passwords and HTTPS
|
|
51
|
+
// userinfo are never part of an identity (normalization would discard them).
|
|
52
|
+
if (/[\s\\?#]/.test(value) || !value || (/^[^/]*:[^/]*@/.test(value) && !value.includes('://')))
|
|
53
|
+
throw new Error('invalid repository identity')
|
|
54
|
+
if (value.includes('://')) {
|
|
55
|
+
let input: URL
|
|
56
|
+
try {
|
|
57
|
+
input = new URL(value)
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error('invalid repository identity')
|
|
60
|
+
}
|
|
61
|
+
if (input.password || (input.username && !['ssh:', 'git+ssh:'].includes(input.protocol))) {
|
|
62
|
+
throw new Error('repository identity must not contain credentials')
|
|
63
|
+
}
|
|
64
|
+
if (input.search || input.hash) throw new Error('invalid repository identity')
|
|
65
|
+
}
|
|
66
|
+
const normalized = normalizeGitRemoteUrl(value)
|
|
67
|
+
let url: URL
|
|
68
|
+
try {
|
|
69
|
+
url = new URL(normalized)
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error('invalid repository identity')
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
url.protocol !== 'https:' ||
|
|
75
|
+
!url.hostname ||
|
|
76
|
+
url.username ||
|
|
77
|
+
url.password ||
|
|
78
|
+
url.search ||
|
|
79
|
+
url.hash ||
|
|
80
|
+
url.pathname === '/' ||
|
|
81
|
+
/%|\\/.test(url.pathname)
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('repository identity must be a normalized HTTPS repository origin')
|
|
84
|
+
}
|
|
85
|
+
return normalized
|
|
86
|
+
}
|
|
@@ -51,6 +51,11 @@ export const allowedActorKinds: Record<EventType, readonly ActorKind[]> = {
|
|
|
51
51
|
'execution.ended': ['dispatcher'],
|
|
52
52
|
'infrastructure.failed': ['dispatcher'],
|
|
53
53
|
'infrastructure.cleanup-attempted': ['dispatcher'],
|
|
54
|
+
'pr-summary.requested': ['kernel'],
|
|
55
|
+
'pr-summary.attempted': ['kernel'],
|
|
56
|
+
'pr-summary.result': ['kernel'],
|
|
57
|
+
'pr-summary.settled': ['kernel'],
|
|
58
|
+
'finalize.step-started': ['kernel'],
|
|
54
59
|
'publication.requested': ['agent', 'kernel'],
|
|
55
60
|
// Only the dispatcher releases workspaces and settles publications (§15.7);
|
|
56
61
|
// the kernel never destroys one, so it never records a loss.
|
|
@@ -88,6 +93,7 @@ export const allowedActorKinds: Record<EventType, readonly ActorKind[]> = {
|
|
|
88
93
|
'code-review.started': ['kernel'],
|
|
89
94
|
'code-review.verdict': ['agent'],
|
|
90
95
|
|
|
96
|
+
'verify.reconcile-policy-captured': ['kernel'],
|
|
91
97
|
'verify.started': ['kernel'],
|
|
92
98
|
'verify.completed': ['kernel', 'agent'],
|
|
93
99
|
'finalize.started': ['kernel'],
|
|
@@ -177,10 +183,13 @@ export function validateEventWrite(input: {
|
|
|
177
183
|
const escalation = payloadResult.data as EventPayload<'escalation.raised'>
|
|
178
184
|
if (
|
|
179
185
|
actor.kind === 'dispatcher' &&
|
|
180
|
-
(escalation.source !== 'policy' ||
|
|
186
|
+
(escalation.source !== 'policy' ||
|
|
187
|
+
(escalation.policyCause !== 'infrastructure-failure-limit' &&
|
|
188
|
+
escalation.policyCause !== 'dispatch-failure-limit' &&
|
|
189
|
+
escalation.policyCause !== 'execution-no-progress'))
|
|
181
190
|
) {
|
|
182
191
|
throw new EventValidationError(
|
|
183
|
-
'invalid dispatcher escalation: only policy/infrastructure-failure-limit may be raised outside the kernel',
|
|
192
|
+
'invalid dispatcher escalation: only policy/infrastructure-failure-limit or policy/dispatch-failure-limit or policy/execution-no-progress may be raised outside the kernel',
|
|
184
193
|
)
|
|
185
194
|
}
|
|
186
195
|
if (escalation.source === 'policy' && escalation.policyCause === undefined) {
|
|
@@ -172,6 +172,7 @@ const verifyCompletedPayloadSchema = z.union([
|
|
|
172
172
|
z.strictObject({
|
|
173
173
|
...verifyCompletionBase,
|
|
174
174
|
outcome: z.literal('skipped'),
|
|
175
|
+
skipCause: z.literal('post-reconcile-policy').optional(),
|
|
175
176
|
reason: z.string().trim().min(1, 'a skipped verification requires a non-blank reason'),
|
|
176
177
|
}),
|
|
177
178
|
]),
|
|
@@ -195,6 +196,12 @@ const finalizeStepCompletedPayloadSchema = z.discriminatedUnion('ok', [
|
|
|
195
196
|
/** One ticket asset revision a build froze (SPEC §6.3): the identity and
|
|
196
197
|
* shape of what `listTicketAssets` returned at the pinning moment. */
|
|
197
198
|
export const pinnedAssetSchema = z.strictObject({
|
|
199
|
+
snapshot: z
|
|
200
|
+
.strictObject({
|
|
201
|
+
kind: z.string().regex(/^ticket-snapshot-[a-f0-9-]+$/),
|
|
202
|
+
revision: z.number().int().nonnegative(),
|
|
203
|
+
})
|
|
204
|
+
.optional(),
|
|
198
205
|
kind: z.string().min(1),
|
|
199
206
|
name: z.string().min(1),
|
|
200
207
|
revision: z.number().int().nonnegative(),
|
|
@@ -336,6 +343,26 @@ export const eventPayloadSchemas = {
|
|
|
336
343
|
})
|
|
337
344
|
.optional(),
|
|
338
345
|
}),
|
|
346
|
+
/** Optional summary publication identities and durable write budgets. */
|
|
347
|
+
'pr-summary.requested': z.strictObject({
|
|
348
|
+
occurrence: z.number().int().positive(),
|
|
349
|
+
boundary: z.number().int().positive(),
|
|
350
|
+
}),
|
|
351
|
+
'pr-summary.attempted': z.strictObject({
|
|
352
|
+
occurrence: z.number().int().positive(),
|
|
353
|
+
boundary: z.number().int().positive(),
|
|
354
|
+
pr: z.number().int().positive(),
|
|
355
|
+
}),
|
|
356
|
+
'pr-summary.result': z.strictObject({
|
|
357
|
+
attempt: z.number().int().positive(),
|
|
358
|
+
outcome: z.enum(['success', 'failure', 'rate-limited']),
|
|
359
|
+
eligibleAfter: z.string().datetime().optional(),
|
|
360
|
+
}),
|
|
361
|
+
'pr-summary.settled': z.strictObject({
|
|
362
|
+
request: z.number().int().positive(),
|
|
363
|
+
disposition: z.enum(['attempted', 'skipped', 'deferred']),
|
|
364
|
+
}),
|
|
365
|
+
'finalize.step-started': z.strictObject({ step: z.string().min(1) }),
|
|
339
366
|
/** Remote phases deposit their output and park before trusted publication.
|
|
340
367
|
* The dispatcher settles the request only after the VM session and execution
|
|
341
368
|
* lease are gone, then appends the ordinary phase completion fact. */
|
|
@@ -515,6 +542,27 @@ export const eventPayloadSchemas = {
|
|
|
515
542
|
* starts. */
|
|
516
543
|
feedback: feedbackSchema.optional(),
|
|
517
544
|
}),
|
|
545
|
+
'verify.reconcile-policy-captured': z.strictObject({
|
|
546
|
+
reconcileSeq: z.number().int().positive(),
|
|
547
|
+
steps: z
|
|
548
|
+
.array(
|
|
549
|
+
z.strictObject({
|
|
550
|
+
step: z.string().min(1),
|
|
551
|
+
enabled: z.boolean(),
|
|
552
|
+
source: z.string().min(1),
|
|
553
|
+
}),
|
|
554
|
+
)
|
|
555
|
+
.superRefine((steps, ctx) => {
|
|
556
|
+
if (new Set(steps.map((s) => s.step)).size !== steps.length)
|
|
557
|
+
ctx.addIssue({ code: 'custom', message: 'duplicate policy step' })
|
|
558
|
+
for (const s of steps)
|
|
559
|
+
if (
|
|
560
|
+
s.source !== 'verify.afterReconcile' &&
|
|
561
|
+
s.source !== `verify.${s.step}.afterReconcile`
|
|
562
|
+
)
|
|
563
|
+
ctx.addIssue({ code: 'custom', message: 'invalid policy source' })
|
|
564
|
+
}),
|
|
565
|
+
}),
|
|
518
566
|
'verify.completed': verifyCompletedPayloadSchema,
|
|
519
567
|
'finalize.started': empty,
|
|
520
568
|
'finalize.completed': z.strictObject({
|
|
@@ -630,6 +678,7 @@ export interface NormalizedVerifyCompletion {
|
|
|
630
678
|
outcome: VerifyOutcome
|
|
631
679
|
report?: ArtifactRef
|
|
632
680
|
reason?: string
|
|
681
|
+
skipCause?: 'post-reconcile-policy'
|
|
633
682
|
}
|
|
634
683
|
|
|
635
684
|
/**
|
|
@@ -653,6 +702,7 @@ export function normalizeVerifyCompletion(
|
|
|
653
702
|
attempt: payload.attempt,
|
|
654
703
|
outcome: 'skipped',
|
|
655
704
|
reason: payload.reason,
|
|
705
|
+
...(payload.skipCause !== undefined ? { skipCause: payload.skipCause } : {}),
|
|
656
706
|
}
|
|
657
707
|
}
|
|
658
708
|
return {
|
|
@@ -90,6 +90,7 @@ const tickCountersSchema = z.strictObject({
|
|
|
90
90
|
const restartRequiredConfigPathSchema = z.enum([
|
|
91
91
|
'forge',
|
|
92
92
|
'plugins',
|
|
93
|
+
'store',
|
|
93
94
|
'workspace.provider',
|
|
94
95
|
'workspace.config',
|
|
95
96
|
'tickets.source',
|
|
@@ -154,6 +155,16 @@ export const harvestEventPayloadSchemas = {
|
|
|
154
155
|
* command supervising the guest runner. Written as soon as the launch
|
|
155
156
|
* succeeds so a later process can re-observe the execution without
|
|
156
157
|
* process memory. */
|
|
158
|
+
'harvest.execution.authorized': z.strictObject({
|
|
159
|
+
execution: z.string().min(1),
|
|
160
|
+
run: z.string().min(1),
|
|
161
|
+
launch: z.enum(['started', 'resumed']),
|
|
162
|
+
leaseHolder: z.string().min(1),
|
|
163
|
+
}),
|
|
164
|
+
'harvest.execution.ended': z.strictObject({
|
|
165
|
+
execution: z.string().min(1),
|
|
166
|
+
run: z.string().min(1),
|
|
167
|
+
}),
|
|
157
168
|
'harvest.execution.started': z.strictObject({
|
|
158
169
|
execution: z.string().min(1),
|
|
159
170
|
provider: z.string().min(1),
|
|
@@ -557,6 +568,8 @@ const allowedActorKinds: Record<RepositoryEventType, readonly ActorKind[]> = {
|
|
|
557
568
|
'harvest.recovery-requested': ['kernel'],
|
|
558
569
|
'harvest.recovery-exhausted': ['kernel'],
|
|
559
570
|
'harvest.started': ['dispatcher', 'kernel'],
|
|
571
|
+
'harvest.execution.authorized': ['dispatcher'],
|
|
572
|
+
'harvest.execution.ended': ['dispatcher'],
|
|
560
573
|
'harvest.execution.started': ['dispatcher'],
|
|
561
574
|
'harvest.execution.released': ['dispatcher'],
|
|
562
575
|
'harvest.step.started': ['kernel'],
|
|
@@ -57,7 +57,13 @@ import type {
|
|
|
57
57
|
import { GitWorktreeProvider, spawnExec } from '../ports/workspace/git-worktree'
|
|
58
58
|
import { InProcessBuildExecution } from '../ports/workspace/in-process-build-execution'
|
|
59
59
|
import { BuildRunner, LeaseHeldError, SetupFailureError } from '../processes/build-runner'
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
diagnosticArtifact,
|
|
62
|
+
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
63
|
+
effectiveBuildConfigContent,
|
|
64
|
+
parseEffectiveBuildConfig,
|
|
65
|
+
} from '../processes/build-execution-state'
|
|
66
|
+
import { composeBuildConfig } from '../config/live'
|
|
61
67
|
import { Dispatcher, type LaunchRunnerResult } from '../processes/dispatcher'
|
|
62
68
|
import {
|
|
63
69
|
getHarvestStatus,
|
|
@@ -120,6 +126,7 @@ export const CONFORMING_BODY = [
|
|
|
120
126
|
export function readyTicket(id: string, over: Partial<Omit<Ticket, 'ref'>> = {}): Ticket {
|
|
121
127
|
const title = over.title ?? 'Add rate limiting'
|
|
122
128
|
return {
|
|
129
|
+
revision: 'fixture',
|
|
123
130
|
ref: { source: 'fake', id, title },
|
|
124
131
|
title,
|
|
125
132
|
body: over.body ?? CONFORMING_BODY,
|
|
@@ -398,6 +405,10 @@ export async function makeHarness(opts: {
|
|
|
398
405
|
* CONFIG_TOML, so every existing scenario is untouched; a scenario proving
|
|
399
406
|
* two-axis routing (§9) supplies its own `[roles.default]`/phase roles. */
|
|
400
407
|
configToml?: string
|
|
408
|
+
/** Force a routing disagreement while retaining the runner's branch config. */
|
|
409
|
+
dispatcherConfig?: (config: Config) => Config
|
|
410
|
+
pinnedConfig?: boolean
|
|
411
|
+
getConfig?: () => Config
|
|
401
412
|
/** Forge gate existence is independent from current PR mergeability. */
|
|
402
413
|
gatePresence?: 'present' | 'absent'
|
|
403
414
|
/** Optional adapter-level runtime composition for focused integration tests.
|
|
@@ -602,6 +613,20 @@ export async function makeHarness(opts: {
|
|
|
602
613
|
runner: new BuildRunner({
|
|
603
614
|
store: activeStore.scopeBuild(slug),
|
|
604
615
|
config,
|
|
616
|
+
...(opts.pinnedConfig
|
|
617
|
+
? {
|
|
618
|
+
getConfig: async () => {
|
|
619
|
+
const artifact = await activeStore.getArtifact(
|
|
620
|
+
slug,
|
|
621
|
+
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
622
|
+
)
|
|
623
|
+
return composeBuildConfig(
|
|
624
|
+
artifact === null ? config : parseEffectiveBuildConfig(artifact),
|
|
625
|
+
opts.getConfig?.() ?? config,
|
|
626
|
+
)
|
|
627
|
+
},
|
|
628
|
+
}
|
|
629
|
+
: {}),
|
|
605
630
|
// Two-axis registry (§9): every runtime is backed by the SAME scripted
|
|
606
631
|
// runner instance, so the `s_1…s_N` session numbering scenarios rely on
|
|
607
632
|
// is preserved regardless of which runtime a role selects. `scripted`
|
|
@@ -629,6 +654,15 @@ export async function makeHarness(opts: {
|
|
|
629
654
|
}
|
|
630
655
|
}
|
|
631
656
|
const launchRunner = async (slug: string): Promise<LaunchRunnerResult> => {
|
|
657
|
+
if (
|
|
658
|
+
opts.pinnedConfig &&
|
|
659
|
+
(await activeStore.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)) === null
|
|
660
|
+
) {
|
|
661
|
+
await activeStore.putArtifact(slug, {
|
|
662
|
+
kind: BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
663
|
+
content: effectiveBuildConfigContent(config),
|
|
664
|
+
})
|
|
665
|
+
}
|
|
632
666
|
launched.push({ slug, ...(await makeRunner(slug)) })
|
|
633
667
|
return 'scheduled'
|
|
634
668
|
}
|
|
@@ -638,10 +672,11 @@ export async function makeHarness(opts: {
|
|
|
638
672
|
tickets: ticketSource,
|
|
639
673
|
workspaces,
|
|
640
674
|
forge: selectedForge,
|
|
641
|
-
config,
|
|
675
|
+
config: opts.dispatcherConfig?.(structuredClone(config)) ?? config,
|
|
642
676
|
repo: origin,
|
|
643
677
|
exec: spawnExec,
|
|
644
678
|
launchRunner,
|
|
679
|
+
...(opts.getConfig ? { getConfig: opts.getConfig } : {}),
|
|
645
680
|
ids,
|
|
646
681
|
clock,
|
|
647
682
|
})
|