@markjaquith/agency 2.61.1 → 2.61.3
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 +6 -0
- package/package.json +1 -1
- package/src/commands/context.test.ts +14 -0
- package/src/commands/sync.test.ts +39 -1
- package/src/commands/sync.ts +29 -4
- package/src/services/ContextService.ts +10 -1
- package/src/services/IntegrationService.test.ts +1 -0
- package/src/services/SyncService.test.ts +46 -2
- package/src/services/SyncService.ts +20 -7
- package/src/workbase/AGENTS.md +3 -2
package/README.md
CHANGED
|
@@ -527,6 +527,12 @@ and reference authority, local checkout and resolved-commit state, recorded PR
|
|
|
527
527
|
state, and validation warnings. Only `done` satisfies a dependency; `dropped` is
|
|
528
528
|
terminal but remains a blocker.
|
|
529
529
|
|
|
530
|
+
`authority.writable` identifies the writable repository checkout, while
|
|
531
|
+
`authority.documents.writable` lists the absolute paths of Agency documents the
|
|
532
|
+
target may maintain. A single-phase task lists its `TASK.md`; a phase lists its
|
|
533
|
+
owning `TASK.md` and active `PHASE.md`; orchestration targets list none. Use
|
|
534
|
+
Agency commands rather than direct edits for structural frontmatter mutations.
|
|
535
|
+
|
|
530
536
|
Complete output is the default for entity targets. Pass `--compact` explicitly
|
|
531
537
|
to omit document prose and low-level Git details while retaining identity,
|
|
532
538
|
hashes, authority, paths, graph state, materialization state, and validation
|
package/package.json
CHANGED
|
@@ -208,6 +208,10 @@ Phase prose.
|
|
|
208
208
|
expect(result.documents.task.body).toContain("Task prose.")
|
|
209
209
|
expect(result.documents.phase.body).toContain("Phase prose.")
|
|
210
210
|
expect(result.documents.phase.sha256).toMatch(/^[a-f0-9]{64}$/)
|
|
211
|
+
expect(result.authority.documents.writable).toEqual([
|
|
212
|
+
join(root, "tasks/agent-contract/TASK.md"),
|
|
213
|
+
join(root, "tasks/agent-contract/phases/context-command/PHASE.md"),
|
|
214
|
+
])
|
|
211
215
|
expect(result.workspace.writable.branchCommit).toMatch(/^[a-f0-9]{40}$/)
|
|
212
216
|
expect(result.workspace.writable.baseCommit).toMatch(/^[a-f0-9]{40}$/)
|
|
213
217
|
expect(result.workspace.references[0].resolvedCommit).toMatch(
|
|
@@ -232,6 +236,10 @@ Phase prose.
|
|
|
232
236
|
registered: true,
|
|
233
237
|
})
|
|
234
238
|
expect(result.authority.writable.checkoutPath).toContain("code/agency")
|
|
239
|
+
expect(result.authority.documents.writable).toEqual([
|
|
240
|
+
join(root, "tasks/agent-contract/TASK.md"),
|
|
241
|
+
join(root, "tasks/agent-contract/phases/context-command/PHASE.md"),
|
|
242
|
+
])
|
|
235
243
|
})
|
|
236
244
|
|
|
237
245
|
test("reports dependency and validation blockers deterministically", async () => {
|
|
@@ -275,6 +283,12 @@ status: dropped
|
|
|
275
283
|
taskId: "agent-contract",
|
|
276
284
|
})
|
|
277
285
|
expect(task.graph.parent).toEqual({ kind: "epic", id: "contract" })
|
|
286
|
+
expect(task.authority.documents.writable).toEqual([])
|
|
287
|
+
|
|
288
|
+
const executionTask = await readContext(root, "foundations")
|
|
289
|
+
expect(executionTask.authority.documents.writable).toEqual([
|
|
290
|
+
join(root, "tasks/foundations/TASK.md"),
|
|
291
|
+
])
|
|
278
292
|
|
|
279
293
|
const workbase = await readContext(root, ".")
|
|
280
294
|
expect(workbase).toMatchObject({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { chmod, mkdir } from "node:fs/promises"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { TaskService } from "../services/TaskService"
|
|
6
6
|
import {
|
|
@@ -99,6 +99,44 @@ describe("sync command", () => {
|
|
|
99
99
|
})
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
+
test("groups repeated human-readable warnings by affected target", async () => {
|
|
103
|
+
await runTestEffect(
|
|
104
|
+
TaskService.pipe(
|
|
105
|
+
Effect.flatMap((service) =>
|
|
106
|
+
service.create(
|
|
107
|
+
{
|
|
108
|
+
id: "second",
|
|
109
|
+
ticketUrl: null,
|
|
110
|
+
repo: "agency",
|
|
111
|
+
branch: "task/second",
|
|
112
|
+
base: "main",
|
|
113
|
+
},
|
|
114
|
+
root,
|
|
115
|
+
),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
const originalPath = process.env.PATH
|
|
120
|
+
const bin = join(root, "bin")
|
|
121
|
+
await mkdir(bin)
|
|
122
|
+
await Bun.write(
|
|
123
|
+
join(bin, "gh"),
|
|
124
|
+
'#!/bin/sh\necho "provider unavailable" >&2\nexit 1\n',
|
|
125
|
+
)
|
|
126
|
+
await chmod(join(bin, "gh"), 0o755)
|
|
127
|
+
process.env.PATH = `${bin}:${originalPath}`
|
|
128
|
+
try {
|
|
129
|
+
const logs = await captureLogs(() =>
|
|
130
|
+
runTestEffect(sync({ cwd: root, dryRun: true })),
|
|
131
|
+
)
|
|
132
|
+
expect(
|
|
133
|
+
logs.filter((line) => line.includes("provider unavailable")),
|
|
134
|
+
).toEqual(["Warning 'task:example', 'task:second': provider unavailable"])
|
|
135
|
+
} finally {
|
|
136
|
+
process.env.PATH = originalPath
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
102
140
|
test("reports human-readable progress without polluting JSON output", async () => {
|
|
103
141
|
const updates: string[] = []
|
|
104
142
|
const progress: Progress = {
|
package/src/commands/sync.ts
CHANGED
|
@@ -8,6 +8,27 @@ interface SyncCommandOptions extends BaseCommandOptions {
|
|
|
8
8
|
readonly dryRun?: boolean
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface Notice {
|
|
12
|
+
readonly kind: string
|
|
13
|
+
readonly target: string
|
|
14
|
+
readonly message: string
|
|
15
|
+
readonly action?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const groupedNotices = <T extends Notice>(notices: readonly T[]) => {
|
|
19
|
+
const groups = new Map<string, { notice: T; targets: string[] }>()
|
|
20
|
+
for (const notice of notices) {
|
|
21
|
+
const key = JSON.stringify([notice.kind, notice.message, notice.action])
|
|
22
|
+
const group = groups.get(key)
|
|
23
|
+
if (group) group.targets.push(notice.target)
|
|
24
|
+
else groups.set(key, { notice, targets: [notice.target] })
|
|
25
|
+
}
|
|
26
|
+
return groups.values()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const formatTargets = (targets: readonly string[]) =>
|
|
30
|
+
targets.map((target) => `'${target}'`).join(", ")
|
|
31
|
+
|
|
11
32
|
export const sync = (
|
|
12
33
|
options: SyncCommandOptions = {},
|
|
13
34
|
progress: Progress = createProgress({
|
|
@@ -62,9 +83,11 @@ export const sync = (
|
|
|
62
83
|
`${change.status === "applied" ? "Applied" : "Planned"} ${change.kind} '${change.target}': ${change.message}`,
|
|
63
84
|
)
|
|
64
85
|
}
|
|
65
|
-
for (const warning of
|
|
86
|
+
for (const { notice: warning, targets } of groupedNotices(
|
|
87
|
+
result.warnings,
|
|
88
|
+
)) {
|
|
66
89
|
log(
|
|
67
|
-
`Warning
|
|
90
|
+
`Warning ${formatTargets(targets)}: ${warning.message}${warning.action ? `. ${warning.action}` : ""}`,
|
|
68
91
|
)
|
|
69
92
|
}
|
|
70
93
|
for (const issue of result.repositories.unresolved) {
|
|
@@ -72,9 +95,11 @@ export const sync = (
|
|
|
72
95
|
`Unresolved repository '${issue.alias}': ${issue.message}. ${issue.action}`,
|
|
73
96
|
)
|
|
74
97
|
}
|
|
75
|
-
for (const issue of
|
|
98
|
+
for (const { notice: issue, targets } of groupedNotices(
|
|
99
|
+
result.unresolved,
|
|
100
|
+
)) {
|
|
76
101
|
log(
|
|
77
|
-
`Unresolved
|
|
102
|
+
`Unresolved ${formatTargets(targets)}: ${issue.message}${issue.action ? `. ${issue.action}` : ""}`,
|
|
78
103
|
)
|
|
79
104
|
}
|
|
80
105
|
if (
|
|
@@ -784,6 +784,15 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
784
784
|
: null
|
|
785
785
|
const reviewData =
|
|
786
786
|
task?.data && "review" in task.data ? task.data.review : null
|
|
787
|
+
const writableDocuments = reviewData
|
|
788
|
+
? task
|
|
789
|
+
? [task.path]
|
|
790
|
+
: []
|
|
791
|
+
: executionData && task
|
|
792
|
+
? phase
|
|
793
|
+
? [task.path, phase.path]
|
|
794
|
+
: [task.path]
|
|
795
|
+
: []
|
|
787
796
|
const references: readonly RepositoryReference[] = reviewData
|
|
788
797
|
? [{ repo: reviewData.repo, ref: reviewData.commit }]
|
|
789
798
|
: executionData
|
|
@@ -1078,7 +1087,7 @@ export class ContextService extends Effect.Service<ContextService>()(
|
|
|
1078
1087
|
checkoutPath: reference.checkoutPath,
|
|
1079
1088
|
})),
|
|
1080
1089
|
documents: {
|
|
1081
|
-
writable:
|
|
1090
|
+
writable: writableDocuments,
|
|
1082
1091
|
},
|
|
1083
1092
|
},
|
|
1084
1093
|
workspace: options.compact
|
|
@@ -418,6 +418,7 @@ describe("IntegrationService", () => {
|
|
|
418
418
|
expect(body).toContain("agency next --json")
|
|
419
419
|
expect(body).toContain("agency <command> --help")
|
|
420
420
|
expect(body).toContain("authority.writable.checkoutPath")
|
|
421
|
+
expect(body).toContain("authority.documents.writable")
|
|
421
422
|
expect(body).toContain("Only `done` satisfies a dependency")
|
|
422
423
|
expect(body).toContain("Require explicit user intent")
|
|
423
424
|
expect(body).toContain(
|
|
@@ -64,14 +64,17 @@ case "$*" in
|
|
|
64
64
|
*mergeable*) ;;
|
|
65
65
|
*) echo "mergeable field was not requested" >&2; exit 2 ;;
|
|
66
66
|
esac
|
|
67
|
+
case "$*" in
|
|
68
|
+
*baseRepository*) echo "unsupported baseRepository field was requested" >&2; exit 3 ;;
|
|
69
|
+
esac
|
|
67
70
|
if [ "$2" = "view" ]; then
|
|
68
71
|
cat <<'JSON'
|
|
69
|
-
{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"
|
|
72
|
+
{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}
|
|
70
73
|
JSON
|
|
71
74
|
exit 0
|
|
72
75
|
fi
|
|
73
76
|
cat <<'JSON'
|
|
74
|
-
[{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"
|
|
77
|
+
[{"number":42,"state":"MERGED","title":"Ship","isDraft":false,"headRefName":"feat/example","baseRefName":"main","headRepository":{"nameWithOwner":"example/agency"},"url":"https://github.com/example/agency/pull/42","mergedAt":"2100-01-01T00:00:00Z","mergeCommit":{"oid":"abc"},"mergeable":"MERGEABLE"}]
|
|
75
78
|
JSON
|
|
76
79
|
`,
|
|
77
80
|
)
|
|
@@ -172,6 +175,7 @@ pr: null
|
|
|
172
175
|
)
|
|
173
176
|
const invocation = await Bun.file(capture).text()
|
|
174
177
|
expect(invocation).toContain("--repo\n")
|
|
178
|
+
expect(invocation).not.toContain("baseRepository")
|
|
175
179
|
expect(invocation).toContain("GIT_DIR=")
|
|
176
180
|
expect(invocation).toContain(".jj")
|
|
177
181
|
|
|
@@ -1013,4 +1017,44 @@ exit 9
|
|
|
1013
1017
|
),
|
|
1014
1018
|
).toEqual([])
|
|
1015
1019
|
})
|
|
1020
|
+
|
|
1021
|
+
test("keeps pull request query failures concise", async () => {
|
|
1022
|
+
await runTestEffect(
|
|
1023
|
+
TaskService.pipe(
|
|
1024
|
+
Effect.flatMap((service) =>
|
|
1025
|
+
service.create(
|
|
1026
|
+
{
|
|
1027
|
+
id: "unavailable",
|
|
1028
|
+
ticketUrl: null,
|
|
1029
|
+
repo: "agency",
|
|
1030
|
+
branch: "feat/unavailable",
|
|
1031
|
+
base: "main",
|
|
1032
|
+
},
|
|
1033
|
+
root,
|
|
1034
|
+
),
|
|
1035
|
+
),
|
|
1036
|
+
),
|
|
1037
|
+
)
|
|
1038
|
+
await Bun.write(
|
|
1039
|
+
join(root, "bin", "gh"),
|
|
1040
|
+
`#!/bin/sh
|
|
1041
|
+
echo "Unknown JSON field: unsupported" >&2
|
|
1042
|
+
echo "Available fields:" >&2
|
|
1043
|
+
echo " additions" >&2
|
|
1044
|
+
exit 1
|
|
1045
|
+
`,
|
|
1046
|
+
)
|
|
1047
|
+
await chmod(join(root, "bin", "gh"), 0o755)
|
|
1048
|
+
|
|
1049
|
+
const result = await runTestEffect(
|
|
1050
|
+
SyncService.pipe(
|
|
1051
|
+
Effect.flatMap((service) => service.reconcile({ cwd: root })),
|
|
1052
|
+
),
|
|
1053
|
+
)
|
|
1054
|
+
expect(result.warnings).toContainEqual({
|
|
1055
|
+
kind: "pr-discovery-unavailable",
|
|
1056
|
+
target: "task:unavailable",
|
|
1057
|
+
message: "Unknown JSON field: unsupported",
|
|
1058
|
+
})
|
|
1059
|
+
})
|
|
1016
1060
|
})
|
|
@@ -153,6 +153,15 @@ const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
|
153
153
|
const originRef = (ref: string) =>
|
|
154
154
|
ref.replace(/^refs\/remotes\/origin\//, "").replace(/^origin\//, "")
|
|
155
155
|
|
|
156
|
+
const GITHUB_PR_FIELDS =
|
|
157
|
+
"number,state,title,isDraft,headRefName,baseRefName,headRepository,url,mergedAt,mergeCommit,mergeable"
|
|
158
|
+
|
|
159
|
+
const commandErrorSummary = (stderr: string, fallback: string) =>
|
|
160
|
+
stderr
|
|
161
|
+
.split("\n")
|
|
162
|
+
.map((line) => line.trim())
|
|
163
|
+
.find(Boolean) ?? fallback
|
|
164
|
+
|
|
156
165
|
export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
157
166
|
sync: () => ({
|
|
158
167
|
reconcile: (
|
|
@@ -349,7 +358,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
349
358
|
"view",
|
|
350
359
|
existing.url,
|
|
351
360
|
"--json",
|
|
352
|
-
|
|
361
|
+
GITHUB_PR_FIELDS,
|
|
353
362
|
],
|
|
354
363
|
{
|
|
355
364
|
cwd: repositoryPath,
|
|
@@ -372,7 +381,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
372
381
|
"--state",
|
|
373
382
|
"all",
|
|
374
383
|
"--json",
|
|
375
|
-
|
|
384
|
+
GITHUB_PR_FIELDS,
|
|
376
385
|
],
|
|
377
386
|
{
|
|
378
387
|
cwd: repositoryPath,
|
|
@@ -824,8 +833,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
824
833
|
warnings.push({
|
|
825
834
|
kind: existing ? "pr-unavailable" : "pr-discovery-unavailable",
|
|
826
835
|
target: record.key,
|
|
827
|
-
message:
|
|
828
|
-
queried.stderr
|
|
836
|
+
message: commandErrorSummary(
|
|
837
|
+
queried.stderr,
|
|
838
|
+
"Could not query delivery provider",
|
|
839
|
+
),
|
|
829
840
|
})
|
|
830
841
|
}
|
|
831
842
|
} else if (existing) {
|
|
@@ -858,7 +869,7 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
858
869
|
warnings.push({
|
|
859
870
|
kind: "pr-unavailable",
|
|
860
871
|
target: record.key,
|
|
861
|
-
message: `Could not inspect ${existing.url}: ${viewed.stderr
|
|
872
|
+
message: `Could not inspect ${existing.url}: ${commandErrorSummary(viewed.stderr, "GitHub query failed")}`,
|
|
862
873
|
})
|
|
863
874
|
}
|
|
864
875
|
} else {
|
|
@@ -901,8 +912,10 @@ export class SyncService extends Effect.Service<SyncService>()("SyncService", {
|
|
|
901
912
|
warnings.push({
|
|
902
913
|
kind: "pr-discovery-unavailable",
|
|
903
914
|
target: record.key,
|
|
904
|
-
message:
|
|
905
|
-
listed.stderr
|
|
915
|
+
message: commandErrorSummary(
|
|
916
|
+
listed.stderr,
|
|
917
|
+
"Could not discover pull requests",
|
|
918
|
+
),
|
|
906
919
|
})
|
|
907
920
|
}
|
|
908
921
|
}
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -48,10 +48,11 @@ agency validate --json
|
|
|
48
48
|
- A single-phase task or phase is an execution unit with one writable `repo` and
|
|
49
49
|
optional read-only `repos`. Only `done` satisfies a dependency; `dropped` is
|
|
50
50
|
terminal but leaves dependents blocked.
|
|
51
|
-
- For an execution unit, write
|
|
51
|
+
- For an execution unit, write repository content only at
|
|
52
52
|
`authority.writable.checkoutPath`. Every `authority.references` checkout is
|
|
53
53
|
read-only, even if filesystem permissions allow writes.
|
|
54
|
-
-
|
|
54
|
+
- Maintain only the Agency documents listed in `authority.documents.writable`:
|
|
55
|
+
keep task-wide decisions in `TASK.md` and phase-specific delivery context in
|
|
55
56
|
`PHASE.md`. Use Agency commands for structural frontmatter mutations.
|
|
56
57
|
|
|
57
58
|
## Consent Boundaries
|