@markjaquith/agency 2.48.1 → 2.49.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.
@@ -746,6 +746,24 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
746
746
  }
747
747
  }
748
748
 
749
+ const validateCompletion = (
750
+ record: DocumentRecord<TaskData | PhaseData>,
751
+ ) => {
752
+ if (!("repo" in record.data) || !record.data.completion) return
753
+ if (record.data.status !== "done") {
754
+ issue(record.path, "Non-PR completion requires status 'done'")
755
+ }
756
+ if (record.data.pr !== null) {
757
+ issue(
758
+ record.path,
759
+ "Non-PR completion cannot have a recorded pull request",
760
+ )
761
+ }
762
+ if (record.data.claim?.state === "active") {
763
+ issue(record.path, "Completed work cannot have an active claim")
764
+ }
765
+ }
766
+
749
767
  for (const epic of epics.values()) {
750
768
  validateRepositories(epic)
751
769
  const ids = new Set(epic.data.tasks.map((task) => task.id))
@@ -776,6 +794,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
776
794
  for (const task of tasks.values()) {
777
795
  validateRepositories(task)
778
796
  validateBranchOwnership(task)
797
+ validateCompletion(task)
779
798
  if (task.data.epic) {
780
799
  const parent = epics.get(task.data.epic)
781
800
  if (!parent) {
@@ -828,6 +847,7 @@ export class WorkbaseService extends Effect.Service<WorkbaseService>()(
828
847
  for (const phase of phases.values()) {
829
848
  validateRepositories(phase)
830
849
  validateBranchOwnership(phase)
850
+ validateCompletion(phase)
831
851
  }
832
852
 
833
853
  issues.sort((a, b) =>
@@ -43,7 +43,8 @@ reason to edit `agency.json` or `repos/` by hand.
43
43
  Require explicit user intent before initializing a workbase; changing repository
44
44
  aliases or applying repository setup or workbase sync changes; launching another
45
45
  agent from an active agent session; creating a pull request; archiving, restoring,
46
- dropping, or reopening work; or using `--force` to override readiness.
46
+ dropping, reopening, or completing work without a pull request; or using `--force`
47
+ to override readiness.
47
48
 
48
49
  ## Safety
49
50
 
@@ -80,7 +81,9 @@ release the claim with the current document revision.
80
81
  An execution unit remains `working` after implementation is committed and while
81
82
  its pull request is open. It becomes `done` only after its authoritative pull
82
83
  request is merged and Agency reconciles that state. Do not mark committed or
83
- review-ready work `done` manually.
84
+ review-ready work `done` manually. A genuine investigation, operational action,
85
+ or no-change result may complete without a pull request only with explicit user
86
+ intent, `--no-pull-request`, and a durable outcome summary.
84
87
 
85
88
  At each closeout trigger (creating or updating a PR, marking it ready, completing
86
89
  a refinement loop, or pausing or handing off completed implementation work):
@@ -90,6 +93,8 @@ a refinement loop, or pausing or handing off completed implementation work):
90
93
  keep the execution unit `working` through review and merge.
91
94
  - After merge, run `agency sync --apply` to reconcile the execution unit to
92
95
  `done`.
96
+ - For an approved non-PR outcome, finish an active claim or update unclaimed
97
+ status with `--no-pull-request --summary <text>` and optional supporting URL.
93
98
  - Refresh durable delivery context in `TASK.md` or `PHASE.md`, including recorded
94
99
  PR state, current head, diff summary, and verification results after later
95
100
  pushes when those details are maintained there.
@@ -0,0 +1,28 @@
1
+ import type { CompletionRecord } from "./schemas"
2
+
3
+ export interface NonPrCompletionInput {
4
+ readonly summary: string
5
+ readonly evidenceUrl?: string
6
+ }
7
+
8
+ export const buildNonPrCompletion = (
9
+ input: NonPrCompletionInput,
10
+ now: Date,
11
+ ): { readonly value: CompletionRecord } | { readonly error: string } => {
12
+ const summary = input.summary.trim()
13
+ if (!summary) return { error: "Non-PR completion summary must not be empty" }
14
+
15
+ const evidenceUrl = input.evidenceUrl?.trim()
16
+ if (evidenceUrl && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(evidenceUrl)) {
17
+ return { error: `Invalid completion evidence URL: ${evidenceUrl}` }
18
+ }
19
+
20
+ return {
21
+ value: {
22
+ mode: "non-pr",
23
+ completedAt: now.toISOString(),
24
+ summary,
25
+ ...(evidenceUrl ? { evidenceUrl } : {}),
26
+ },
27
+ }
28
+ }
@@ -75,6 +75,13 @@ export const PullRequestRecord = Schema.Struct({
75
75
  mergeable: Schema.optional(Schema.NullOr(Schema.Boolean)),
76
76
  })
77
77
 
78
+ export const CompletionRecord = Schema.Struct({
79
+ mode: Schema.Literal("non-pr"),
80
+ completedAt: IsoTimestamp,
81
+ summary: NonEmptyString,
82
+ evidenceUrl: Schema.optional(Url),
83
+ })
84
+
78
85
  const DeliveryProvider = Schema.Struct({
79
86
  provider: EntityId,
80
87
  remote: Schema.optional(NonEmptyString),
@@ -141,6 +148,7 @@ const ExecutionUnit = {
141
148
  pr: Schema.NullOr(Schema.Union(GitHubPullRequestUrl, PullRequestRecord)),
142
149
  status: Schema.optionalWith(WorkStatus, { default: () => "open" as const }),
143
150
  claim: Schema.optional(ClaimRecord),
151
+ completion: Schema.optional(CompletionRecord),
144
152
  }
145
153
 
146
154
  export const EpicFrontmatter = Schema.Struct({
@@ -187,6 +195,7 @@ export type RepositoryDeclaration = Schema.Schema.Type<
187
195
  export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
188
196
  export type ClaimRecord = Schema.Schema.Type<typeof ClaimRecord>
189
197
  export type PullRequestRecord = Schema.Schema.Type<typeof PullRequestRecord>
198
+ export type CompletionRecord = Schema.Schema.Type<typeof CompletionRecord>
190
199
  export type EpicFrontmatter = Schema.Schema.Type<typeof EpicFrontmatter>
191
200
  export type TaskFrontmatter = Schema.Schema.Type<typeof TaskFrontmatter>
192
201
  export type PhaseFrontmatter = Schema.Schema.Type<typeof PhaseFrontmatter>