@markjaquith/agency 2.7.3 → 2.9.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 +71 -6
- package/cli.ts +52 -4
- package/fixtures/protocol/error.json +14 -0
- package/fixtures/protocol/success.json +7 -0
- package/index.ts +1 -0
- package/package.json +16 -1
- package/schemas/agency-envelope-v1.schema.json +38 -0
- package/skills/agency/SKILL.md +2 -0
- package/src/cli-parser.test.ts +2 -0
- package/src/cli-parser.ts +31 -1
- package/src/cli.test.ts +90 -1
- package/src/commands/init.test.ts +3 -8
- package/src/commands/integration.test.ts +51 -0
- package/src/commands/integration.ts +62 -0
- package/src/commands/read-only.test.ts +144 -0
- package/src/commands/validate.ts +7 -0
- package/src/commands/work.test.ts +3 -3
- package/src/protocol.test.ts +87 -0
- package/src/protocol.ts +211 -0
- package/src/services/IntegrationService.test.ts +137 -0
- package/src/services/IntegrationService.ts +136 -0
- package/src/services/WorkbaseService.test.ts +4 -118
- package/src/services/WorkbaseService.ts +0 -53
- package/src/services/WorktreeService.test.ts +2 -2
- package/src/test-utils.ts +12 -3
- package/src/utils/effect.test.ts +9 -3
- package/src/utils/effect.ts +4 -2
- package/src/workbase/AGENTS.md +2 -0
package/README.md
CHANGED
|
@@ -71,11 +71,12 @@ workbase/
|
|
|
71
71
|
backend/
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Agency
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
Agency keeps discovery and other observational commands read-only. Run
|
|
75
|
+
`agency integration status` to inspect `AGENTS.md` and
|
|
76
|
+
`.opencode/opencode.jsonc`, then `agency integration sync` to create missing
|
|
77
|
+
files or refresh checksum-safe managed files. The OpenCode config grants
|
|
78
|
+
external-directory access to task and epic references. Customized files are
|
|
79
|
+
reported but never overwritten.
|
|
79
80
|
|
|
80
81
|
Repository metadata comes directly from Git under `repos/{alias}`. Workbase
|
|
81
82
|
configuration may provide a custom writable-worktree creation command.
|
|
@@ -242,6 +243,8 @@ agency pr create refresh-copy
|
|
|
242
243
|
agency init [path] [--json]
|
|
243
244
|
agency workbase add <path> [--json]
|
|
244
245
|
agency workbase list [--json]
|
|
246
|
+
agency integration status [--json]
|
|
247
|
+
agency integration sync [--json]
|
|
245
248
|
agency repo add <alias> <remote> [--json]
|
|
246
249
|
agency repo link <alias> <path> [--json]
|
|
247
250
|
agency repo list [--json]
|
|
@@ -253,7 +256,8 @@ Registered workbases are stored in
|
|
|
253
256
|
repository. Alias names are then used by all documents and commands.
|
|
254
257
|
|
|
255
258
|
Commands that print Agency-owned results accept `--json`, including initialization,
|
|
256
|
-
repository mutations, entity creation/list/show,
|
|
259
|
+
integration inspection/sync, repository mutations, entity creation/list/show,
|
|
260
|
+
status, validation, and PR creation.
|
|
257
261
|
|
|
258
262
|
### Epics
|
|
259
263
|
|
|
@@ -298,6 +302,7 @@ agency task create <id> --multi-phase
|
|
|
298
302
|
### Noninteractive Use
|
|
299
303
|
|
|
300
304
|
Agency never prompts when `--no-input` is set or stdin/stderr are not TTYs.
|
|
305
|
+
`--json` also disables prompts and selectors, even when a TTY is available.
|
|
301
306
|
Commands with explicit inputs continue normally. `task new` fails immediately;
|
|
302
307
|
`work` requires an explicit directory, task ID, or `--epic` and must run from a
|
|
303
308
|
workbase; `validate` requires an explicit path or must run from a workbase.
|
|
@@ -404,6 +409,66 @@ unknown dependencies, and dependency cycles. YAML duplicate keys, anchors,
|
|
|
404
409
|
aliases, and custom tags are rejected. When path is omitted outside a workbase,
|
|
405
410
|
Agency prompts for a registered workbase.
|
|
406
411
|
|
|
412
|
+
## Machine Protocol
|
|
413
|
+
|
|
414
|
+
`--json` emits exactly one JSON value on stdout for success or failure. It takes
|
|
415
|
+
precedence over `--silent`; progress, warnings, and verbose diagnostics remain on
|
|
416
|
+
stderr. Version 1 success responses have this shape:
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{ "version": 1, "ok": true, "result": { "root": "/work/agency" } }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Failures exit nonzero and use the same versioned envelope:
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
{
|
|
426
|
+
"version": 1,
|
|
427
|
+
"ok": false,
|
|
428
|
+
"error": {
|
|
429
|
+
"code": "CLI_USAGE",
|
|
430
|
+
"message": "Unknown command 'unknown'.\n\nUsage: agency <command> [options]",
|
|
431
|
+
"fields": {
|
|
432
|
+
"detail": "Unknown command 'unknown'.",
|
|
433
|
+
"usage": "agency <command> [options]"
|
|
434
|
+
},
|
|
435
|
+
"retryable": false,
|
|
436
|
+
"remediation": "Correct the arguments using the usage value in error.fields."
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Every error contains a stable `code`, human-readable `message`, structured
|
|
442
|
+
`fields`, and `retryable`. `remediation` is included when Agency knows a specific
|
|
443
|
+
recovery action. Version 1 defines these codes:
|
|
444
|
+
|
|
445
|
+
| Code | Meaning |
|
|
446
|
+
| ------------------------- | -------------------------------------------------------- |
|
|
447
|
+
| `CLI_USAGE` | Invalid command, option, argument, or option combination |
|
|
448
|
+
| `WORKBASE_NOT_FOUND` | No workbase could be resolved |
|
|
449
|
+
| `WORKBASE_CONFIG_INVALID` | Invalid workbase configuration |
|
|
450
|
+
| `WORKBASE_REGISTRY_ERROR` | Invalid or inaccessible workbase registry |
|
|
451
|
+
| `FILE_NOT_FOUND` | A required path does not exist |
|
|
452
|
+
| `FILESYSTEM_ERROR` | A filesystem operation failed |
|
|
453
|
+
| `FRONTMATTER_INVALID` | A durable document has invalid frontmatter |
|
|
454
|
+
| `VALIDATION_FAILED` | Workbase validation reported issues |
|
|
455
|
+
| `REPOSITORY_ERROR` | Repository operation failed |
|
|
456
|
+
| `EPIC_ERROR` | Epic operation failed |
|
|
457
|
+
| `TASK_ERROR` | Task operation failed |
|
|
458
|
+
| `PHASE_ERROR` | Phase operation failed |
|
|
459
|
+
| `ARCHIVE_ERROR` | Archive operation failed |
|
|
460
|
+
| `WORKTREE_ERROR` | Worktree operation failed |
|
|
461
|
+
| `PULL_REQUEST_ERROR` | Pull request operation failed |
|
|
462
|
+
| `PROCESS_ERROR` | A child process failed and may be retried |
|
|
463
|
+
| `PROTOCOL_OUTPUT_ERROR` | A command violated the machine output contract |
|
|
464
|
+
| `COMMAND_FAILED` | An otherwise unclassified command failure |
|
|
465
|
+
|
|
466
|
+
The Effect schemas are exported from `@markjaquith/agency` and
|
|
467
|
+
`@markjaquith/agency/protocol`. The distributable JSON Schema is exported as
|
|
468
|
+
`@markjaquith/agency/schemas/agency-envelope-v1.json`. Representative payloads
|
|
469
|
+
are exported as `@markjaquith/agency/fixtures/protocol/success.json` and
|
|
470
|
+
`@markjaquith/agency/fixtures/protocol/error.json`.
|
|
471
|
+
|
|
407
472
|
## Agent Skill
|
|
408
473
|
|
|
409
474
|
`skills/agency/SKILL.md` contains an agent-oriented operating guide for Agency.
|
package/cli.ts
CHANGED
|
@@ -13,6 +13,10 @@ import { epic, help as epicHelp } from "./src/commands/epic"
|
|
|
13
13
|
import { phase, help as phaseHelp } from "./src/commands/phase"
|
|
14
14
|
import { archive, help as archiveHelp } from "./src/commands/archive"
|
|
15
15
|
import { workbase, help as workbaseHelp } from "./src/commands/workbase"
|
|
16
|
+
import {
|
|
17
|
+
integration,
|
|
18
|
+
help as integrationHelp,
|
|
19
|
+
} from "./src/commands/integration"
|
|
16
20
|
import type { Command } from "./src/types"
|
|
17
21
|
import { FileSystemService } from "./src/services/FileSystemService"
|
|
18
22
|
import { WorkbaseService } from "./src/services/WorkbaseService"
|
|
@@ -23,6 +27,13 @@ import { PhaseService } from "./src/services/PhaseService"
|
|
|
23
27
|
import { WorktreeService } from "./src/services/WorktreeService"
|
|
24
28
|
import { PullRequestService } from "./src/services/PullRequestService"
|
|
25
29
|
import { ArchiveService } from "./src/services/ArchiveService"
|
|
30
|
+
import { IntegrationService } from "./src/services/IntegrationService"
|
|
31
|
+
import {
|
|
32
|
+
collectCommandResult,
|
|
33
|
+
errorEnvelope,
|
|
34
|
+
successEnvelope,
|
|
35
|
+
writeEnvelope,
|
|
36
|
+
} from "./src/protocol"
|
|
26
37
|
|
|
27
38
|
// Create CLI layer with all services
|
|
28
39
|
const CliLayer = Layer.mergeAll(
|
|
@@ -35,6 +46,7 @@ const CliLayer = Layer.mergeAll(
|
|
|
35
46
|
WorktreeService.Default,
|
|
36
47
|
PullRequestService.Default,
|
|
37
48
|
ArchiveService.Default,
|
|
49
|
+
IntegrationService.Default,
|
|
38
50
|
)
|
|
39
51
|
|
|
40
52
|
/**
|
|
@@ -70,7 +82,7 @@ async function runCommand<E>(
|
|
|
70
82
|
Effect.either,
|
|
71
83
|
),
|
|
72
84
|
)
|
|
73
|
-
if (Either.isLeft(result)) throw
|
|
85
|
+
if (Either.isLeft(result)) throw result.left
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
// Read version from package.json
|
|
@@ -191,6 +203,22 @@ const commands: Record<string, Command> = {
|
|
|
191
203
|
)
|
|
192
204
|
},
|
|
193
205
|
},
|
|
206
|
+
integration: {
|
|
207
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
208
|
+
if (options.help) {
|
|
209
|
+
console.log(integrationHelp)
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
await runCommand(
|
|
213
|
+
integration({
|
|
214
|
+
subcommand: args[0],
|
|
215
|
+
json: options.json,
|
|
216
|
+
silent: options.silent,
|
|
217
|
+
verbose: options.verbose,
|
|
218
|
+
}),
|
|
219
|
+
)
|
|
220
|
+
},
|
|
221
|
+
},
|
|
194
222
|
repo: {
|
|
195
223
|
run: async (args: string[], options: Record<string, any>) => {
|
|
196
224
|
if (options.help) {
|
|
@@ -296,6 +324,7 @@ Usage: agency <command> [options]
|
|
|
296
324
|
Commands:
|
|
297
325
|
init [path] Initialize an Agency workbase
|
|
298
326
|
workbase <subcommand> Manage registered workbases
|
|
327
|
+
integration <command> Inspect or sync managed integration files
|
|
299
328
|
epic <subcommand> Manage epics
|
|
300
329
|
phase <subcommand> Manage task phases
|
|
301
330
|
archive <type> Archive a work item
|
|
@@ -323,13 +352,19 @@ For more information about a command, run:
|
|
|
323
352
|
`)
|
|
324
353
|
}
|
|
325
354
|
|
|
355
|
+
const machineMode = process.argv.slice(2).includes("--json")
|
|
356
|
+
|
|
326
357
|
try {
|
|
327
358
|
const args = process.argv.slice(2)
|
|
328
359
|
const { commandName, args: commandArgs, values } = parseCli(args)
|
|
329
360
|
|
|
330
361
|
// Handle global flags
|
|
331
362
|
if (values.version) {
|
|
332
|
-
|
|
363
|
+
if (machineMode) {
|
|
364
|
+
writeEnvelope(successEnvelope({ version: VERSION }))
|
|
365
|
+
} else {
|
|
366
|
+
console.log(`v${VERSION}`)
|
|
367
|
+
}
|
|
333
368
|
process.exit(0)
|
|
334
369
|
}
|
|
335
370
|
|
|
@@ -342,9 +377,22 @@ try {
|
|
|
342
377
|
|
|
343
378
|
const command = commands[commandName]!
|
|
344
379
|
const inputAllowed =
|
|
345
|
-
!values
|
|
346
|
-
|
|
380
|
+
!values.json &&
|
|
381
|
+
!values["no-input"] &&
|
|
382
|
+
Boolean(process.stdin.isTTY && process.stderr.isTTY)
|
|
383
|
+
if (values.json) {
|
|
384
|
+
const result = await collectCommandResult(() =>
|
|
385
|
+
command.run(commandArgs, { ...values, inputAllowed }),
|
|
386
|
+
)
|
|
387
|
+
writeEnvelope(successEnvelope(result))
|
|
388
|
+
} else {
|
|
389
|
+
await command.run(commandArgs, { ...values, inputAllowed })
|
|
390
|
+
}
|
|
347
391
|
} catch (error) {
|
|
392
|
+
if (machineMode) {
|
|
393
|
+
writeEnvelope(errorEnvelope(error))
|
|
394
|
+
process.exit(1)
|
|
395
|
+
}
|
|
348
396
|
if (error instanceof Error) {
|
|
349
397
|
let message = error.message
|
|
350
398
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"ok": false,
|
|
4
|
+
"error": {
|
|
5
|
+
"code": "CLI_USAGE",
|
|
6
|
+
"message": "Unknown command 'unknown'.\n\nUsage: agency <command> [options]",
|
|
7
|
+
"fields": {
|
|
8
|
+
"detail": "Unknown command 'unknown'.",
|
|
9
|
+
"usage": "agency <command> [options]"
|
|
10
|
+
},
|
|
11
|
+
"retryable": false,
|
|
12
|
+
"remediation": "Correct the arguments using the usage value in error.fields."
|
|
13
|
+
}
|
|
14
|
+
}
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"index.ts",
|
|
22
22
|
"cli.ts",
|
|
23
23
|
"src",
|
|
24
|
+
"schemas",
|
|
25
|
+
"fixtures/protocol",
|
|
24
26
|
"skills",
|
|
25
27
|
"README.md",
|
|
26
28
|
"LICENSE"
|
|
@@ -31,6 +33,19 @@
|
|
|
31
33
|
".": {
|
|
32
34
|
"types": "./index.ts",
|
|
33
35
|
"import": "./index.ts"
|
|
36
|
+
},
|
|
37
|
+
"./protocol": {
|
|
38
|
+
"types": "./src/protocol.ts",
|
|
39
|
+
"import": "./src/protocol.ts"
|
|
40
|
+
},
|
|
41
|
+
"./schemas/agency-envelope-v1.json": {
|
|
42
|
+
"default": "./schemas/agency-envelope-v1.schema.json"
|
|
43
|
+
},
|
|
44
|
+
"./fixtures/protocol/success.json": {
|
|
45
|
+
"default": "./fixtures/protocol/success.json"
|
|
46
|
+
},
|
|
47
|
+
"./fixtures/protocol/error.json": {
|
|
48
|
+
"default": "./fixtures/protocol/error.json"
|
|
34
49
|
}
|
|
35
50
|
},
|
|
36
51
|
"publishConfig": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/markjaquith/agency/schemas/agency-envelope-v1.schema.json",
|
|
4
|
+
"title": "Agency machine result envelope v1",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["version", "ok", "result"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"version": { "const": 1 },
|
|
12
|
+
"ok": { "const": true },
|
|
13
|
+
"result": true
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"type": "object",
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"required": ["version", "ok", "error"],
|
|
20
|
+
"properties": {
|
|
21
|
+
"version": { "const": 1 },
|
|
22
|
+
"ok": { "const": false },
|
|
23
|
+
"error": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"additionalProperties": false,
|
|
26
|
+
"required": ["code", "message", "fields", "retryable"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"code": { "type": "string" },
|
|
29
|
+
"message": { "type": "string" },
|
|
30
|
+
"fields": { "type": "object" },
|
|
31
|
+
"retryable": { "type": "boolean" },
|
|
32
|
+
"remediation": { "type": "string" }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
package/skills/agency/SKILL.md
CHANGED
|
@@ -39,6 +39,7 @@ From anywhere beneath a workbase, run:
|
|
|
39
39
|
```bash
|
|
40
40
|
agency status --json
|
|
41
41
|
agency validate --json
|
|
42
|
+
agency integration status --json
|
|
42
43
|
agency repo list --json
|
|
43
44
|
agency epic list --json
|
|
44
45
|
agency task list --json
|
|
@@ -56,6 +57,7 @@ If no workbase is found, do not initialize one without user intent. When asked:
|
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
59
|
agency init [path]
|
|
60
|
+
agency integration sync
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Register known workbases so `agency work` can select one when run elsewhere:
|
package/src/cli-parser.test.ts
CHANGED
|
@@ -82,6 +82,8 @@ describe("strict CLI parsing", () => {
|
|
|
82
82
|
[["init", "one", "two"], "agency init"],
|
|
83
83
|
[["workbase", "add", "one", "two"], "agency workbase add"],
|
|
84
84
|
[["workbase", "list", "extra"], "agency workbase list"],
|
|
85
|
+
[["integration", "status", "extra"], "agency integration status"],
|
|
86
|
+
[["integration", "sync", "extra"], "agency integration sync"],
|
|
85
87
|
[["repo", "add", "a", "b", "extra"], "agency repo add"],
|
|
86
88
|
[["repo", "link", "a", "b", "extra"], "agency repo link"],
|
|
87
89
|
[["repo", "list", "extra"], "agency repo list"],
|
package/src/cli-parser.ts
CHANGED
|
@@ -88,6 +88,24 @@ const commands = {
|
|
|
88
88
|
},
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
|
+
integration: {
|
|
92
|
+
usage: "agency integration <status|sync>",
|
|
93
|
+
options: outputOptions,
|
|
94
|
+
subcommands: {
|
|
95
|
+
status: {
|
|
96
|
+
usage: "agency integration status [--json]",
|
|
97
|
+
minArgs: 0,
|
|
98
|
+
maxArgs: 0,
|
|
99
|
+
options: ["json"],
|
|
100
|
+
},
|
|
101
|
+
sync: {
|
|
102
|
+
usage: "agency integration sync [--json]",
|
|
103
|
+
minArgs: 0,
|
|
104
|
+
maxArgs: 0,
|
|
105
|
+
options: ["json"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
91
109
|
repo: {
|
|
92
110
|
usage: "agency repo <add|link|list>",
|
|
93
111
|
options: outputOptions,
|
|
@@ -345,8 +363,20 @@ export interface ParsedCli {
|
|
|
345
363
|
>
|
|
346
364
|
}
|
|
347
365
|
|
|
366
|
+
class CliUsageError extends Error {
|
|
367
|
+
readonly _tag = "CliUsageError"
|
|
368
|
+
|
|
369
|
+
constructor(
|
|
370
|
+
readonly detail: string,
|
|
371
|
+
readonly usage: string,
|
|
372
|
+
) {
|
|
373
|
+
super(`${detail}\n\nUsage: ${usage}`)
|
|
374
|
+
this.name = "CliUsageError"
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
348
378
|
const usageError = (message: string, usage: string) =>
|
|
349
|
-
new
|
|
379
|
+
new CliUsageError(message, usage)
|
|
350
380
|
|
|
351
381
|
const optionLabel = (name: string) => `--${name}`
|
|
352
382
|
|
package/src/cli.test.ts
CHANGED
|
@@ -34,7 +34,9 @@ async function runCli(
|
|
|
34
34
|
function parseJson(result: CliResult) {
|
|
35
35
|
expect(result.exitCode).toBe(0)
|
|
36
36
|
expect(result.stderr).toBe("")
|
|
37
|
-
|
|
37
|
+
const envelope = JSON.parse(result.stdout)
|
|
38
|
+
expect(envelope).toMatchObject({ version: 1, ok: true })
|
|
39
|
+
return envelope.result
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
describe("CLI", () => {
|
|
@@ -79,6 +81,46 @@ describe("CLI", () => {
|
|
|
79
81
|
expect(taggedError.stderr).not.toContain("An error has occurred")
|
|
80
82
|
})
|
|
81
83
|
|
|
84
|
+
test("emits one versioned error envelope for usage and command failures", async () => {
|
|
85
|
+
const usage = await runCli(["unknown", "--json"])
|
|
86
|
+
expect(usage.exitCode).toBe(1)
|
|
87
|
+
expect(usage.stderr).toBe("")
|
|
88
|
+
expect(usage.stdout.trim().split("\n")).toHaveLength(1)
|
|
89
|
+
expect(JSON.parse(usage.stdout)).toEqual({
|
|
90
|
+
version: 1,
|
|
91
|
+
ok: false,
|
|
92
|
+
error: {
|
|
93
|
+
code: "CLI_USAGE",
|
|
94
|
+
message:
|
|
95
|
+
"Unknown command 'unknown'.\n\nUsage: agency <command> [options]",
|
|
96
|
+
fields: {
|
|
97
|
+
detail: "Unknown command 'unknown'.",
|
|
98
|
+
usage: "agency <command> [options]",
|
|
99
|
+
},
|
|
100
|
+
retryable: false,
|
|
101
|
+
remediation:
|
|
102
|
+
"Correct the arguments using the usage value in error.fields.",
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const cwd = await createTempDir()
|
|
107
|
+
tempDirs.push(cwd)
|
|
108
|
+
const commandFailure = await runCli(
|
|
109
|
+
["repo", "list", "--json", "--silent"],
|
|
110
|
+
cwd,
|
|
111
|
+
)
|
|
112
|
+
expect(commandFailure.exitCode).toBe(1)
|
|
113
|
+
expect(commandFailure.stderr).toBe("")
|
|
114
|
+
expect(JSON.parse(commandFailure.stdout)).toMatchObject({
|
|
115
|
+
version: 1,
|
|
116
|
+
ok: false,
|
|
117
|
+
error: {
|
|
118
|
+
code: "WORKBASE_NOT_FOUND",
|
|
119
|
+
retryable: false,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
82
124
|
test("rejects malformed input before running a command", async () => {
|
|
83
125
|
const parent = await createTempDir()
|
|
84
126
|
tempDirs.push(parent)
|
|
@@ -106,6 +148,7 @@ describe("CLI", () => {
|
|
|
106
148
|
for (const [command, usage] of [
|
|
107
149
|
["init", "Usage: agency init"],
|
|
108
150
|
["workbase", "Usage: agency workbase"],
|
|
151
|
+
["integration", "Usage: agency integration"],
|
|
109
152
|
["repo", "Usage: agency repo"],
|
|
110
153
|
["epic", "Usage: agency epic"],
|
|
111
154
|
["task", "Usage: agency task"],
|
|
@@ -136,6 +179,28 @@ describe("CLI", () => {
|
|
|
136
179
|
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
137
180
|
})
|
|
138
181
|
|
|
182
|
+
test("reports and synchronizes managed integration files", async () => {
|
|
183
|
+
const root = await createTempDir()
|
|
184
|
+
tempDirs.push(root)
|
|
185
|
+
expect((await runCli(["init", root])).exitCode).toBe(0)
|
|
186
|
+
|
|
187
|
+
const before = parseJson(
|
|
188
|
+
await runCli(["integration", "status", "--json"], root),
|
|
189
|
+
)
|
|
190
|
+
expect(before.files).toMatchObject([
|
|
191
|
+
{ name: "agents", state: "missing" },
|
|
192
|
+
{ name: "opencode", state: "missing" },
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
const synced = parseJson(
|
|
196
|
+
await runCli(["integration", "sync", "--json"], root),
|
|
197
|
+
)
|
|
198
|
+
expect(synced.files).toMatchObject([
|
|
199
|
+
{ name: "agents", state: "managed", changed: true },
|
|
200
|
+
{ name: "opencode", state: "managed", changed: true },
|
|
201
|
+
])
|
|
202
|
+
})
|
|
203
|
+
|
|
139
204
|
test("registers and lists workbases", async () => {
|
|
140
205
|
const parent = await createTempDir()
|
|
141
206
|
tempDirs.push(parent)
|
|
@@ -155,6 +220,30 @@ describe("CLI", () => {
|
|
|
155
220
|
).toEqual([await realpath(root)])
|
|
156
221
|
})
|
|
157
222
|
|
|
223
|
+
test("lets JSON override silent and disables interactive task input", async () => {
|
|
224
|
+
const root = await createTempDir()
|
|
225
|
+
tempDirs.push(root)
|
|
226
|
+
const result = await runCli(["init", root, "--json", "--silent"])
|
|
227
|
+
expect(parseJson(result)).toEqual({ root })
|
|
228
|
+
|
|
229
|
+
const interactive = await runCli(["task", "new", "--json"])
|
|
230
|
+
expect(interactive.exitCode).toBe(1)
|
|
231
|
+
expect(interactive.stderr).toBe("")
|
|
232
|
+
expect(JSON.parse(interactive.stdout)).toMatchObject({
|
|
233
|
+
version: 1,
|
|
234
|
+
ok: false,
|
|
235
|
+
error: { code: "COMMAND_FAILED", retryable: false },
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test("envelopes help and version output in machine mode", async () => {
|
|
240
|
+
const help = await runCli(["status", "--help", "--json"])
|
|
241
|
+
expect(parseJson(help)).toContain("Usage: agency status")
|
|
242
|
+
|
|
243
|
+
const version = await runCli(["status", "--version", "--json"])
|
|
244
|
+
expect(parseJson(version)).toEqual({ version: "0.0.0-development" })
|
|
245
|
+
})
|
|
246
|
+
|
|
158
247
|
test("runs a multi-phase domain workflow through subprocesses", async () => {
|
|
159
248
|
const parent = await createTempDir()
|
|
160
249
|
tempDirs.push(parent)
|
|
@@ -33,15 +33,10 @@ describe("init command", () => {
|
|
|
33
33
|
expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
|
|
34
34
|
"/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
|
|
35
35
|
)
|
|
36
|
-
expect(await Bun.file(join(root, "AGENTS.md")).
|
|
37
|
-
"# Agency Workbase",
|
|
38
|
-
)
|
|
39
|
-
expect(
|
|
40
|
-
await Bun.file(join(root, ".opencode/opencode.jsonc")).text(),
|
|
41
|
-
).toContain('"path": "../tasks"')
|
|
36
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
42
37
|
expect(
|
|
43
|
-
await Bun.file(join(root, ".opencode/opencode.jsonc")).
|
|
44
|
-
).
|
|
38
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
39
|
+
).toBe(false)
|
|
45
40
|
})
|
|
46
41
|
|
|
47
42
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
captureLogs,
|
|
5
|
+
cleanupTempDir,
|
|
6
|
+
createTempDir,
|
|
7
|
+
runTestEffect,
|
|
8
|
+
} from "../test-utils"
|
|
9
|
+
import { integration } from "./integration"
|
|
10
|
+
|
|
11
|
+
describe("integration command", () => {
|
|
12
|
+
let root: string
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
root = await createTempDir()
|
|
16
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(async () => cleanupTempDir(root))
|
|
20
|
+
|
|
21
|
+
test("reports integration status as JSON", async () => {
|
|
22
|
+
const logs = await captureLogs(() =>
|
|
23
|
+
runTestEffect(
|
|
24
|
+
integration({ subcommand: "status", cwd: root, json: true }),
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
expect(JSON.parse(logs[0]!)).toMatchObject({
|
|
29
|
+
root,
|
|
30
|
+
files: [
|
|
31
|
+
{ name: "agents", state: "missing" },
|
|
32
|
+
{ name: "opencode", state: "missing" },
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test("explicitly synchronizes integration files", async () => {
|
|
38
|
+
const logs = await captureLogs(() =>
|
|
39
|
+
runTestEffect(integration({ subcommand: "sync", cwd: root, json: true })),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(JSON.parse(logs[0]!).files).toMatchObject([
|
|
43
|
+
{ name: "agents", state: "managed", changed: true },
|
|
44
|
+
{ name: "opencode", state: "managed", changed: true },
|
|
45
|
+
])
|
|
46
|
+
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(true)
|
|
47
|
+
expect(
|
|
48
|
+
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
49
|
+
).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
+
import { IntegrationService } from "../services/IntegrationService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
|
|
6
|
+
interface IntegrationOptions extends BaseCommandOptions {
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly json?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const integration = (options: IntegrationOptions) =>
|
|
12
|
+
Effect.gen(function* () {
|
|
13
|
+
const service = yield* IntegrationService
|
|
14
|
+
const { log } = createLoggers(options)
|
|
15
|
+
const cwd = options.cwd ?? process.cwd()
|
|
16
|
+
|
|
17
|
+
switch (options.subcommand) {
|
|
18
|
+
case "status": {
|
|
19
|
+
const result = yield* service.status(cwd)
|
|
20
|
+
if (options.json) {
|
|
21
|
+
log(JSON.stringify(result, null, 2))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
for (const file of result.files) {
|
|
25
|
+
log(`${file.name}\t${file.state}\t${file.path}`)
|
|
26
|
+
}
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case "sync": {
|
|
31
|
+
const result = yield* service.sync(cwd)
|
|
32
|
+
if (options.json) {
|
|
33
|
+
log(JSON.stringify(result, null, 2))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
for (const file of result.files) {
|
|
37
|
+
log(
|
|
38
|
+
`${file.name}\t${file.changed ? "synced" : file.state}\t${file.path}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
default:
|
|
45
|
+
return yield* Effect.fail(
|
|
46
|
+
new Error("Subcommand is required. Available: status, sync"),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export const help = `
|
|
52
|
+
Usage: agency integration <subcommand>
|
|
53
|
+
|
|
54
|
+
Inspect or explicitly synchronize managed agent integration files.
|
|
55
|
+
|
|
56
|
+
Subcommands:
|
|
57
|
+
status Report managed, customized, missing, and drifted files
|
|
58
|
+
sync Create or update checksum-safe managed files
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--json Output results as JSON
|
|
62
|
+
`
|