@markjaquith/agency 2.69.1 → 2.71.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 +29 -16
- package/cli-main.ts +74 -3
- package/package.json +5 -1
- package/pi-extensions/agency.test.ts +117 -0
- package/pi-extensions/agency.ts +141 -0
- package/scripts/install-pi-extension.ts +25 -0
- package/src/cli-parser.ts +11 -0
- package/src/cli.test.ts +40 -2
- package/src/commands/init.test.ts +3 -6
- package/src/commands/integration.test.ts +2 -20
- package/src/commands/integration.ts +3 -5
- package/src/services/IntegrationService.test.ts +24 -156
- package/src/services/IntegrationService.ts +31 -52
- package/src/usage-log.test.ts +94 -0
- package/src/usage-log.ts +143 -0
- package/src/workbase/pi-extension-file.ts +0 -158
package/README.md
CHANGED
|
@@ -21,6 +21,21 @@ bun install -g @markjaquith/agency
|
|
|
21
21
|
|
|
22
22
|
For development, run `bun link` from this repository.
|
|
23
23
|
|
|
24
|
+
## Local Usage Logging
|
|
25
|
+
|
|
26
|
+
Agency records privacy-safe CLI usage events locally so command journeys,
|
|
27
|
+
failures, and flag adoption can be analyzed. Events are stored in SQLite at
|
|
28
|
+
`$XDG_STATE_HOME/agency/usage.sqlite3` (or
|
|
29
|
+
`~/.local/state/agency/usage.sqlite3`) and retained for 90 days by default.
|
|
30
|
+
Each event contains the normalized command path, flag names, timing, outcome,
|
|
31
|
+
Agency version, and ordered `AGENCY_SESSION_ID` correlation. Raw arguments,
|
|
32
|
+
flag values, free-form input, and the current directory are never recorded.
|
|
33
|
+
|
|
34
|
+
Export events as JSON Lines with `agency usage export`. Set
|
|
35
|
+
`AGENCY_NO_USAGE_LOG=1` to opt out, `AGENCY_USAGE_RETENTION_DAYS` to change
|
|
36
|
+
retention, or `AGENCY_USAGE_DB` to select a different database path. Logging is
|
|
37
|
+
best effort and never changes command output or exit behavior.
|
|
38
|
+
|
|
24
39
|
## Core Model
|
|
25
40
|
|
|
26
41
|
- A **workbase** is the root containing durable documents and local repository
|
|
@@ -56,8 +71,6 @@ workbase/
|
|
|
56
71
|
tui.jsonc # managed TUI plugin registration
|
|
57
72
|
plugins/agency-repository-skills.ts # managed workbase access and checkout skills
|
|
58
73
|
tui/agency-debug.ts # managed /agency-debug TUI diagnostic
|
|
59
|
-
.pi/
|
|
60
|
-
extensions/agency-workbase.ts # managed workbase context and checkout skills
|
|
61
74
|
agency.json # tracked config and portable repository declarations
|
|
62
75
|
repos/ # ignored local materializations
|
|
63
76
|
frontend/ # bare Git repository or symlink
|
|
@@ -86,8 +99,8 @@ workbase/
|
|
|
86
99
|
|
|
87
100
|
Agency keeps discovery and other observational commands read-only. Run
|
|
88
101
|
`agency integration status` to inspect `.agency/AGENTS.md`, the managed
|
|
89
|
-
OpenCode configuration and plugins,
|
|
90
|
-
|
|
102
|
+
OpenCode configuration and plugins, then `agency integration sync` to create
|
|
103
|
+
missing files or refresh checksum-safe
|
|
91
104
|
managed files. Customized files are reported but never overwritten. Sync also
|
|
92
105
|
removes checksum-valid retired managed artifacts while
|
|
93
106
|
preserving customized files at their former paths. The root
|
|
@@ -128,15 +141,15 @@ The plugin grants whole-workbase access dynamically, while the portable
|
|
|
128
141
|
reference advertises that context to agents. Bash and Agency operations must
|
|
129
142
|
still follow the write authority reported by `agency context`.
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
workbase, and exposes skills from
|
|
137
|
-
`.
|
|
138
|
-
|
|
139
|
-
read-only.
|
|
144
|
+
Agency's package lifecycle installs one Pi extension at
|
|
145
|
+
`~/.pi/agent/extensions/agency.ts`. Pi loads it globally, so it works from
|
|
146
|
+
workbase roots and nested epic, task, phase, or checkout directories without
|
|
147
|
+
project trust or an explicit `--extension` flag. Outside an Agency workbase it
|
|
148
|
+
registers no resources and changes no prompt. Inside one, it loads the managed
|
|
149
|
+
Agency instructions, advertises the complete workbase, and exposes skills from
|
|
150
|
+
the writable checkout's `.claude/skills`, `.agents/skills`,
|
|
151
|
+
`.opencode/{skill,skills}`, and `.pi/skills` directories. `agency context`
|
|
152
|
+
remains the authority for writes; reference checkouts remain read-only.
|
|
140
153
|
|
|
141
154
|
Repository aliases, the version-control backend, and canonical fetch remotes are
|
|
142
155
|
declared in tracked `agency.json`; local clones and symlinks remain ignored under
|
|
@@ -434,11 +447,11 @@ working directory so the workbase `AGENTS.md` and managed OpenCode config are
|
|
|
434
447
|
discovered normally.
|
|
435
448
|
Agency's managed OpenCode plugin grants the active workbase external-directory
|
|
436
449
|
access and adds existing checkout-local `.claude/skills`, `.agents/skills`, and
|
|
437
|
-
`.opencode/{skill,skills}` directories to `skills.paths`. The
|
|
450
|
+
`.opencode/{skill,skills}` directories to `skills.paths`. The global Pi
|
|
438
451
|
extension provides equivalent whole-workbase context and additionally discovers
|
|
439
452
|
checkout-local `.pi/skills` through Pi's `resources_discover` lifecycle.
|
|
440
|
-
`agency work` supplies the checkout directly; plain OpenCode
|
|
441
|
-
|
|
453
|
+
`agency work` supplies the checkout directly; plain OpenCode and Pi launches
|
|
454
|
+
resolve a materialized execution-unit
|
|
442
455
|
checkout through `agency context`. A multi-phase
|
|
443
456
|
task root has no single checkout, so launch from its phase directory when using
|
|
444
457
|
plain OpenCode or Pi. Other checkout-local configuration is not composed.
|
package/cli-main.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
successEnvelope,
|
|
68
68
|
writeEnvelope,
|
|
69
69
|
} from "./src/protocol"
|
|
70
|
+
import { exportUsageEvents, recordUsageEvent } from "./src/usage-log"
|
|
70
71
|
|
|
71
72
|
// Create CLI layer with all services
|
|
72
73
|
const CliLayer = Layer.mergeAll(
|
|
@@ -176,6 +177,12 @@ const VERSION = packageJson.version
|
|
|
176
177
|
|
|
177
178
|
// Define commands
|
|
178
179
|
const commands: Record<string, Command> = {
|
|
180
|
+
usage: {
|
|
181
|
+
run: async () => {
|
|
182
|
+
for (const event of await exportUsageEvents())
|
|
183
|
+
console.log(JSON.stringify(event))
|
|
184
|
+
},
|
|
185
|
+
},
|
|
179
186
|
act: {
|
|
180
187
|
run: async (args: string[], options: Record<string, any>) => {
|
|
181
188
|
if (options.help) return console.log(actHelp)
|
|
@@ -785,6 +792,7 @@ Commands:
|
|
|
785
792
|
init [path] Initialize an Agency workbase
|
|
786
793
|
workbase <subcommand> Manage registered workbases
|
|
787
794
|
integration <command> Inspect or sync managed integration files
|
|
795
|
+
usage export Export local usage events as JSON Lines
|
|
788
796
|
epic <subcommand> Manage epics
|
|
789
797
|
phase <subcommand> Manage task phases
|
|
790
798
|
claim <task> [phase] Claim an execution unit
|
|
@@ -830,9 +838,30 @@ const machineMode = process.argv
|
|
|
830
838
|
.slice(2)
|
|
831
839
|
.some((argument) => argument === "--json" || argument === "--jsonl")
|
|
832
840
|
|
|
841
|
+
const invocationStartedAt = performance.now()
|
|
842
|
+
const rawArguments = process.argv.slice(2)
|
|
843
|
+
let usageCommandPath = "invalid"
|
|
844
|
+
let usageFlagNames = rawArguments
|
|
845
|
+
.filter((argument) => argument.startsWith("--"))
|
|
846
|
+
.map((argument) => argument.slice(2).split("=", 1)[0]!)
|
|
847
|
+
|
|
833
848
|
try {
|
|
834
|
-
const
|
|
835
|
-
|
|
849
|
+
const {
|
|
850
|
+
commandName,
|
|
851
|
+
args: commandArgs,
|
|
852
|
+
passthrough,
|
|
853
|
+
values,
|
|
854
|
+
} = parseCli(rawArguments)
|
|
855
|
+
usageCommandPath =
|
|
856
|
+
[commandName, commandArgs[0]]
|
|
857
|
+
.filter(
|
|
858
|
+
(part): part is string =>
|
|
859
|
+
typeof part === "string" && part.length > 0 && !part.startsWith("-"),
|
|
860
|
+
)
|
|
861
|
+
.join("/") || "root"
|
|
862
|
+
usageFlagNames = Object.entries(values)
|
|
863
|
+
.filter(([, value]) => value !== undefined && value !== false)
|
|
864
|
+
.map(([name]) => name)
|
|
836
865
|
|
|
837
866
|
// Handle global flags
|
|
838
867
|
if (values.version) {
|
|
@@ -841,6 +870,16 @@ try {
|
|
|
841
870
|
} else {
|
|
842
871
|
console.log(`v${VERSION}`)
|
|
843
872
|
}
|
|
873
|
+
await recordUsageEvent(
|
|
874
|
+
{
|
|
875
|
+
commandPath: "version",
|
|
876
|
+
flagNames: usageFlagNames,
|
|
877
|
+
durationMs: performance.now() - invocationStartedAt,
|
|
878
|
+
outcome: "success",
|
|
879
|
+
exitStatus: 0,
|
|
880
|
+
},
|
|
881
|
+
VERSION,
|
|
882
|
+
)
|
|
844
883
|
process.exit(0)
|
|
845
884
|
}
|
|
846
885
|
|
|
@@ -848,7 +887,18 @@ try {
|
|
|
848
887
|
// Show help if no command
|
|
849
888
|
if (!commandName) {
|
|
850
889
|
showMainHelp()
|
|
851
|
-
|
|
890
|
+
const exitStatus = values.help ? 0 : 1
|
|
891
|
+
await recordUsageEvent(
|
|
892
|
+
{
|
|
893
|
+
commandPath: "help",
|
|
894
|
+
flagNames: usageFlagNames,
|
|
895
|
+
durationMs: performance.now() - invocationStartedAt,
|
|
896
|
+
outcome: exitStatus === 0 ? "success" : "failure",
|
|
897
|
+
exitStatus,
|
|
898
|
+
},
|
|
899
|
+
VERSION,
|
|
900
|
+
)
|
|
901
|
+
process.exit(exitStatus)
|
|
852
902
|
}
|
|
853
903
|
|
|
854
904
|
const command = commands[commandName]!
|
|
@@ -880,7 +930,28 @@ try {
|
|
|
880
930
|
passthrough,
|
|
881
931
|
})
|
|
882
932
|
}
|
|
933
|
+
const exitStatus = Number(process.exitCode ?? 0)
|
|
934
|
+
await recordUsageEvent(
|
|
935
|
+
{
|
|
936
|
+
commandPath: usageCommandPath,
|
|
937
|
+
flagNames: usageFlagNames,
|
|
938
|
+
durationMs: performance.now() - invocationStartedAt,
|
|
939
|
+
outcome: exitStatus === 0 ? "success" : "failure",
|
|
940
|
+
exitStatus,
|
|
941
|
+
},
|
|
942
|
+
VERSION,
|
|
943
|
+
)
|
|
883
944
|
} catch (error) {
|
|
945
|
+
await recordUsageEvent(
|
|
946
|
+
{
|
|
947
|
+
commandPath: usageCommandPath,
|
|
948
|
+
flagNames: usageFlagNames,
|
|
949
|
+
durationMs: performance.now() - invocationStartedAt,
|
|
950
|
+
outcome: "failure",
|
|
951
|
+
exitStatus: 1,
|
|
952
|
+
},
|
|
953
|
+
VERSION,
|
|
954
|
+
)
|
|
884
955
|
if (machineMode) {
|
|
885
956
|
writeEnvelope(errorEnvelope(error))
|
|
886
957
|
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.71.0",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"cli.ts",
|
|
23
23
|
"cli-main.ts",
|
|
24
24
|
"src",
|
|
25
|
+
"pi-extensions",
|
|
26
|
+
"scripts/install-pi-extension.ts",
|
|
25
27
|
"schemas",
|
|
26
28
|
"fixtures/protocol",
|
|
27
29
|
"README.md",
|
|
@@ -61,6 +63,8 @@
|
|
|
61
63
|
"tag": "latest"
|
|
62
64
|
},
|
|
63
65
|
"scripts": {
|
|
66
|
+
"postinstall": "bun scripts/install-pi-extension.ts install",
|
|
67
|
+
"preuninstall": "bun scripts/install-pi-extension.ts uninstall",
|
|
64
68
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
|
65
69
|
"test": "find src \\( -name '*.test.ts' -o -name '*.test.tsx' \\) -print0 | xargs -0 -n 1 -P 4 bun test",
|
|
66
70
|
"test:opencode": "AGENCY_TEST_OPENCODE=1 bun test src/cli.test.ts --test-name-pattern 'provides effective whole-workbase OpenCode access'",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { cleanupTempDir, createTempDir } from "../src/test-utils"
|
|
5
|
+
import agencyExtension from "./agency"
|
|
6
|
+
|
|
7
|
+
describe("global Pi extension", () => {
|
|
8
|
+
let root: string
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
root = await createTempDir()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(async () => cleanupTempDir(root))
|
|
15
|
+
|
|
16
|
+
const load = (response?: object) => {
|
|
17
|
+
const handlers = new Map<string, (...args: any[]) => any>()
|
|
18
|
+
const pi = {
|
|
19
|
+
exec: async () => ({
|
|
20
|
+
code: response ? 0 : 1,
|
|
21
|
+
stdout: response ? JSON.stringify(response) : "",
|
|
22
|
+
stderr: "",
|
|
23
|
+
}),
|
|
24
|
+
on: (name: string, handler: (...args: any[]) => any) => {
|
|
25
|
+
handlers.set(name, handler)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
agencyExtension(pi as never)
|
|
29
|
+
return handlers
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("no-ops outside Agency workbases without invoking the CLI", async () => {
|
|
33
|
+
let executions = 0
|
|
34
|
+
const handlers = new Map<string, (...args: any[]) => any>()
|
|
35
|
+
agencyExtension({
|
|
36
|
+
exec: async () => {
|
|
37
|
+
executions += 1
|
|
38
|
+
return { code: 1, stdout: "", stderr: "" }
|
|
39
|
+
},
|
|
40
|
+
on: (name: string, handler: (...args: any[]) => any) => {
|
|
41
|
+
handlers.set(name, handler)
|
|
42
|
+
},
|
|
43
|
+
} as never)
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
await handlers.get("resources_discover")?.({ cwd: root }),
|
|
47
|
+
).toBeUndefined()
|
|
48
|
+
expect(
|
|
49
|
+
await handlers.get("before_agent_start")?.(
|
|
50
|
+
{ systemPrompt: "Base" },
|
|
51
|
+
{ cwd: root },
|
|
52
|
+
),
|
|
53
|
+
).toBeUndefined()
|
|
54
|
+
expect(executions).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("uses CLI context from nested directories for skills and authority", async () => {
|
|
58
|
+
const task = join(root, "tasks", "example")
|
|
59
|
+
const nested = join(task, "code", "agency", "src")
|
|
60
|
+
const checkout = join(task, "code", "agency")
|
|
61
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
62
|
+
await mkdir(join(root, ".agency"), { recursive: true })
|
|
63
|
+
await Bun.write(
|
|
64
|
+
join(root, ".agency", "AGENTS.md"),
|
|
65
|
+
"# Managed instructions\n",
|
|
66
|
+
)
|
|
67
|
+
await mkdir(join(checkout, ".agents", "skills"), { recursive: true })
|
|
68
|
+
await mkdir(nested, { recursive: true })
|
|
69
|
+
|
|
70
|
+
const handlers = load({
|
|
71
|
+
ok: true,
|
|
72
|
+
result: {
|
|
73
|
+
workbase: { root },
|
|
74
|
+
target: {
|
|
75
|
+
kind: "task",
|
|
76
|
+
taskId: "example",
|
|
77
|
+
path: join(task, "TASK.md"),
|
|
78
|
+
},
|
|
79
|
+
authority: { mode: "execution", writable: { checkoutPath: checkout } },
|
|
80
|
+
documents: { task: { data: { status: "working" } } },
|
|
81
|
+
validation: { valid: true },
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
const resources = await handlers.get("resources_discover")?.({
|
|
85
|
+
cwd: nested,
|
|
86
|
+
})
|
|
87
|
+
expect(resources.skillPaths).toEqual([join(checkout, ".agents", "skills")])
|
|
88
|
+
|
|
89
|
+
const prompt = await handlers.get("before_agent_start")?.(
|
|
90
|
+
{ systemPrompt: "Base" },
|
|
91
|
+
{ cwd: nested },
|
|
92
|
+
)
|
|
93
|
+
expect(prompt.systemPrompt).toContain("# Managed instructions")
|
|
94
|
+
expect(prompt.systemPrompt).toContain(
|
|
95
|
+
`${checkout} as the default implementation directory`,
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("rejects CLI context for a different workbase", async () => {
|
|
100
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
101
|
+
const handlers = load({
|
|
102
|
+
ok: true,
|
|
103
|
+
result: {
|
|
104
|
+
workbase: { root: join(root, "other") },
|
|
105
|
+
target: { kind: "epic", epicId: "example" },
|
|
106
|
+
validation: { valid: true },
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(
|
|
111
|
+
await handlers.get("before_agent_start")?.(
|
|
112
|
+
{ systemPrompt: "Base" },
|
|
113
|
+
{ cwd: root },
|
|
114
|
+
),
|
|
115
|
+
).toBeUndefined()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
|
|
4
|
+
type ExtensionAPI = {
|
|
5
|
+
exec: (
|
|
6
|
+
command: string,
|
|
7
|
+
args: string[],
|
|
8
|
+
options: { timeout: number },
|
|
9
|
+
) => Promise<{ code: number; stdout: string }>
|
|
10
|
+
on: (
|
|
11
|
+
name: "resources_discover" | "before_agent_start",
|
|
12
|
+
handler: (...args: any[]) => unknown,
|
|
13
|
+
) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type AgencyContext = {
|
|
17
|
+
root?: string
|
|
18
|
+
checkout?: string
|
|
19
|
+
target?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const contextTarget = (result: Record<string, any>): string | undefined => {
|
|
23
|
+
const target = result.target
|
|
24
|
+
if (target?.kind === "epic") return `epic:${target.epicId}`
|
|
25
|
+
if (target?.kind === "phase") {
|
|
26
|
+
return `execution-unit:phase/${target.taskId}/${target.phaseId}`
|
|
27
|
+
}
|
|
28
|
+
if (target?.kind === "task") {
|
|
29
|
+
return result.authority?.mode === "execution"
|
|
30
|
+
? `execution-unit:task/${target.taskId}`
|
|
31
|
+
: `task:${target.taskId}`
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const discoverWorkbase = (directory: string) => {
|
|
36
|
+
let current = resolve(directory)
|
|
37
|
+
while (true) {
|
|
38
|
+
if (existsSync(join(current, "agency.json"))) return current
|
|
39
|
+
const parent = dirname(current)
|
|
40
|
+
if (parent === current) return
|
|
41
|
+
current = parent
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const agencyContext = async (
|
|
46
|
+
pi: ExtensionAPI,
|
|
47
|
+
directory: string,
|
|
48
|
+
): Promise<AgencyContext | undefined> => {
|
|
49
|
+
const root = discoverWorkbase(directory)
|
|
50
|
+
if (!root) return
|
|
51
|
+
|
|
52
|
+
const command = await pi.exec(
|
|
53
|
+
"agency",
|
|
54
|
+
["context", directory, "--compact", "--json"],
|
|
55
|
+
{ timeout: 5000 },
|
|
56
|
+
)
|
|
57
|
+
if (command.code !== 0) return
|
|
58
|
+
|
|
59
|
+
const envelope = JSON.parse(command.stdout)
|
|
60
|
+
if (envelope.ok !== true) return
|
|
61
|
+
const result = envelope.result ?? {}
|
|
62
|
+
const target = contextTarget(result)
|
|
63
|
+
const status =
|
|
64
|
+
result.documents?.phase?.data?.status ??
|
|
65
|
+
result.documents?.task?.data?.status
|
|
66
|
+
if (
|
|
67
|
+
result.validation?.valid !== true ||
|
|
68
|
+
result.workbase?.root !== root ||
|
|
69
|
+
!target ||
|
|
70
|
+
(target.startsWith("execution-unit:") &&
|
|
71
|
+
(!result.authority?.writable?.checkoutPath || status !== "working"))
|
|
72
|
+
)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
root,
|
|
77
|
+
checkout: result.authority?.writable?.checkoutPath,
|
|
78
|
+
target,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default function agencyExtension(pi: ExtensionAPI) {
|
|
83
|
+
const contexts = new Map<string, Promise<AgencyContext | undefined>>()
|
|
84
|
+
const runtimeContext = (directory: string) => {
|
|
85
|
+
const key = resolve(directory)
|
|
86
|
+
let context = contexts.get(key)
|
|
87
|
+
if (!context) {
|
|
88
|
+
context = agencyContext(pi, key).catch(() => undefined)
|
|
89
|
+
contexts.set(key, context)
|
|
90
|
+
}
|
|
91
|
+
return context
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pi.on("resources_discover", async (event: { cwd: string }) => {
|
|
95
|
+
const context = await runtimeContext(event.cwd)
|
|
96
|
+
if (!context?.checkout) return
|
|
97
|
+
|
|
98
|
+
const skillPaths = [
|
|
99
|
+
join(context.checkout, ".claude", "skills"),
|
|
100
|
+
join(context.checkout, ".agents", "skills"),
|
|
101
|
+
join(context.checkout, ".opencode", "skill"),
|
|
102
|
+
join(context.checkout, ".opencode", "skills"),
|
|
103
|
+
join(context.checkout, ".pi", "skills"),
|
|
104
|
+
].filter(existsSync)
|
|
105
|
+
if (skillPaths.length > 0) return { skillPaths: [...new Set(skillPaths)] }
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
pi.on(
|
|
109
|
+
"before_agent_start",
|
|
110
|
+
async (event: { systemPrompt: string }, ctx: { cwd: string }) => {
|
|
111
|
+
const context = await runtimeContext(ctx.cwd)
|
|
112
|
+
if (!context?.root) return
|
|
113
|
+
|
|
114
|
+
const instructionsPath = join(context.root, ".agency", "AGENTS.md")
|
|
115
|
+
const instructions = existsSync(instructionsPath)
|
|
116
|
+
? readFileSync(instructionsPath, "utf8").trim()
|
|
117
|
+
: undefined
|
|
118
|
+
const activeTarget = process.env.AGENCY_TARGET
|
|
119
|
+
const worker =
|
|
120
|
+
activeTarget && context.target === activeTarget
|
|
121
|
+
? `Agency verified this Pi session as the active worker for ${activeTarget}. Perform the assigned work directly. Do not invoke agency work for this target or launch a replacement worker.`
|
|
122
|
+
: undefined
|
|
123
|
+
const access = `The complete Agency workbase is available at ${context.root}. Use absolute paths under that root when workbase context is needed. Agency context remains the authority for writes.`
|
|
124
|
+
const implementation = context.checkout
|
|
125
|
+
? `Pi remains rooted in the task or phase directory for Agency instructions and context. Treat ${context.checkout} as the default implementation directory for source reads, edits, repository status, builds, tests, formatting, and other repository-local commands. Run Agency lifecycle and context commands from the task or phase directory. Any reference checkouts reported by Agency context are read-only.`
|
|
126
|
+
: undefined
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
systemPrompt: [
|
|
130
|
+
event.systemPrompt,
|
|
131
|
+
instructions,
|
|
132
|
+
access,
|
|
133
|
+
worker,
|
|
134
|
+
implementation,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.join("\n\n"),
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cp, mkdir, rm } from "node:fs/promises"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
|
|
5
|
+
export const piExtensionPath = (home = homedir()) =>
|
|
6
|
+
join(home, ".pi", "agent", "extensions", "agency.ts")
|
|
7
|
+
|
|
8
|
+
export const installPiExtension = async (
|
|
9
|
+
source = join(import.meta.dir, "..", "pi-extensions", "agency.ts"),
|
|
10
|
+
destination = piExtensionPath(),
|
|
11
|
+
) => {
|
|
12
|
+
await mkdir(dirname(destination), { recursive: true })
|
|
13
|
+
await cp(source, destination)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const uninstallPiExtension = async (destination = piExtensionPath()) => {
|
|
17
|
+
await rm(destination, { force: true })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (import.meta.main) {
|
|
21
|
+
const command = process.argv[2] ?? "install"
|
|
22
|
+
if (command === "install") await installPiExtension()
|
|
23
|
+
else if (command === "uninstall") await uninstallPiExtension()
|
|
24
|
+
else throw new Error(`Unknown Pi extension lifecycle command: ${command}`)
|
|
25
|
+
}
|
package/src/cli-parser.ts
CHANGED
|
@@ -136,6 +136,17 @@ const nonPrCompletionOptions = {
|
|
|
136
136
|
} satisfies OptionConfig
|
|
137
137
|
|
|
138
138
|
const commands = {
|
|
139
|
+
usage: {
|
|
140
|
+
usage: "agency usage export",
|
|
141
|
+
options: commonOptions,
|
|
142
|
+
subcommands: {
|
|
143
|
+
export: {
|
|
144
|
+
usage: "agency usage export",
|
|
145
|
+
minArgs: 0,
|
|
146
|
+
maxArgs: 0,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
139
150
|
init: {
|
|
140
151
|
usage: "agency init [path] [--json]",
|
|
141
152
|
options: outputOptions,
|
package/src/cli.test.ts
CHANGED
|
@@ -36,6 +36,8 @@ async function runCli(
|
|
|
36
36
|
env: {
|
|
37
37
|
...process.env,
|
|
38
38
|
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
39
|
+
XDG_STATE_HOME: isolatedConfigHome,
|
|
40
|
+
AGENCY_NO_USAGE_LOG: "1",
|
|
39
41
|
...env,
|
|
40
42
|
},
|
|
41
43
|
stdout: "pipe",
|
|
@@ -170,6 +172,44 @@ describe("CLI", () => {
|
|
|
170
172
|
expect(taggedError.stderr).not.toContain("An error has occurred")
|
|
171
173
|
})
|
|
172
174
|
|
|
175
|
+
test("records successful and failed invocations without argument values", async () => {
|
|
176
|
+
const state = await createTempDir()
|
|
177
|
+
tempDirs.push(state)
|
|
178
|
+
const env = {
|
|
179
|
+
XDG_STATE_HOME: state,
|
|
180
|
+
AGENCY_SESSION_ID: "cli-session",
|
|
181
|
+
AGENCY_NO_USAGE_LOG: "0",
|
|
182
|
+
}
|
|
183
|
+
expect((await runCli(["--version"], projectRoot, env)).exitCode).toBe(0)
|
|
184
|
+
expect(
|
|
185
|
+
(await runCli(["unknown", "--cwd", "/private/value"], projectRoot, env))
|
|
186
|
+
.exitCode,
|
|
187
|
+
).toBe(1)
|
|
188
|
+
|
|
189
|
+
const exported = await runCli(["usage", "export"], projectRoot, env)
|
|
190
|
+
expect(exported).toMatchObject({ exitCode: 0, stderr: "" })
|
|
191
|
+
const events = exported.stdout
|
|
192
|
+
.trim()
|
|
193
|
+
.split("\n")
|
|
194
|
+
.map((line) => JSON.parse(line))
|
|
195
|
+
expect(events).toEqual([
|
|
196
|
+
expect.objectContaining({
|
|
197
|
+
sessionId: "cli-session",
|
|
198
|
+
sessionSequence: 1,
|
|
199
|
+
commandPath: "version",
|
|
200
|
+
flagNames: ["version"],
|
|
201
|
+
outcome: "success",
|
|
202
|
+
}),
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
sessionSequence: 2,
|
|
205
|
+
commandPath: "invalid",
|
|
206
|
+
flagNames: ["cwd"],
|
|
207
|
+
outcome: "failure",
|
|
208
|
+
}),
|
|
209
|
+
])
|
|
210
|
+
expect(exported.stdout).not.toContain("/private/value")
|
|
211
|
+
})
|
|
212
|
+
|
|
173
213
|
test("coordinates claims through revision-guarded machine commands", async () => {
|
|
174
214
|
const root = await createTempDir()
|
|
175
215
|
tempDirs.push(root)
|
|
@@ -642,7 +682,6 @@ exit 23
|
|
|
642
682
|
{ name: "opencode-plugin", state: "managed" },
|
|
643
683
|
{ name: "opencode-tui", state: "managed" },
|
|
644
684
|
{ name: "opencode-tui-plugin", state: "managed" },
|
|
645
|
-
{ name: "pi-extension", state: "managed" },
|
|
646
685
|
])
|
|
647
686
|
|
|
648
687
|
const synced = parseJson(
|
|
@@ -654,7 +693,6 @@ exit 23
|
|
|
654
693
|
{ name: "opencode-plugin", state: "managed", changed: false },
|
|
655
694
|
{ name: "opencode-tui", state: "managed", changed: false },
|
|
656
695
|
{ name: "opencode-tui-plugin", state: "managed", changed: false },
|
|
657
|
-
{ name: "pi-extension", state: "managed", changed: false },
|
|
658
696
|
])
|
|
659
697
|
})
|
|
660
698
|
|
|
@@ -89,12 +89,9 @@ describe("init command", () => {
|
|
|
89
89
|
expect(tuiPlugin).toContain('slashName: "agency-debug"')
|
|
90
90
|
expect(tuiPlugin).toContain("api.ui.toast")
|
|
91
91
|
expect(tuiPlugin).not.toContain("chat.message")
|
|
92
|
-
|
|
93
|
-
join(root, ".pi/extensions/agency-workbase.ts"),
|
|
94
|
-
).
|
|
95
|
-
expect(piExtension).toContain('pi.on("resources_discover"')
|
|
96
|
-
expect(piExtension).toContain('pi.on("before_agent_start"')
|
|
97
|
-
expect(piExtension).toContain("AGENCY_WRITABLE_CHECKOUT")
|
|
92
|
+
expect(
|
|
93
|
+
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).exists(),
|
|
94
|
+
).toBe(false)
|
|
98
95
|
})
|
|
99
96
|
|
|
100
97
|
test("preserves existing gitignore entries", async () => {
|
|
@@ -51,11 +51,6 @@ describe("integration command", () => {
|
|
|
51
51
|
state: "missing",
|
|
52
52
|
remediation: expect.stringContaining("integration sync"),
|
|
53
53
|
},
|
|
54
|
-
{
|
|
55
|
-
name: "pi-extension",
|
|
56
|
-
state: "missing",
|
|
57
|
-
remediation: expect.stringContaining("integration sync"),
|
|
58
|
-
},
|
|
59
54
|
],
|
|
60
55
|
})
|
|
61
56
|
})
|
|
@@ -104,12 +99,7 @@ OpenCode TUI config: missing
|
|
|
104
99
|
OpenCode /agency-debug: missing
|
|
105
100
|
Path: .opencode/tui/agency-debug.ts
|
|
106
101
|
The managed OpenCode TUI diagnostic companion needs synchronization.
|
|
107
|
-
Action: Run 'agency integration sync' to install /agency-debug
|
|
108
|
-
|
|
109
|
-
Pi workbase extension: missing
|
|
110
|
-
Path: .pi/extensions/agency-workbase.ts
|
|
111
|
-
The managed Pi workbase extension needs synchronization.
|
|
112
|
-
Action: Run 'agency integration sync' to provide workbase context and expose writable-checkout skills in Pi.`)
|
|
102
|
+
Action: Run 'agency integration sync' to install /agency-debug.`)
|
|
113
103
|
})
|
|
114
104
|
|
|
115
105
|
test("explicitly synchronizes integration files", async () => {
|
|
@@ -123,7 +113,6 @@ Pi workbase extension: missing
|
|
|
123
113
|
{ name: "opencode-plugin", state: "managed", changed: true },
|
|
124
114
|
{ name: "opencode-tui", state: "managed", changed: true },
|
|
125
115
|
{ name: "opencode-tui-plugin", state: "managed", changed: true },
|
|
126
|
-
{ name: "pi-extension", state: "managed", changed: true },
|
|
127
116
|
])
|
|
128
117
|
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
129
118
|
expect(await Bun.file(join(root, ".agency/AGENTS.md")).exists()).toBe(true)
|
|
@@ -144,9 +133,6 @@ Pi workbase extension: missing
|
|
|
144
133
|
expect(
|
|
145
134
|
await Bun.file(join(root, ".opencode/tui/agency-debug.ts")).exists(),
|
|
146
135
|
).toBe(true)
|
|
147
|
-
expect(
|
|
148
|
-
await Bun.file(join(root, ".pi/extensions/agency-workbase.ts")).exists(),
|
|
149
|
-
).toBe(true)
|
|
150
136
|
})
|
|
151
137
|
|
|
152
138
|
test("formats integration sync results for people", async () => {
|
|
@@ -174,10 +160,6 @@ OpenCode TUI config: synced
|
|
|
174
160
|
|
|
175
161
|
OpenCode /agency-debug: synced
|
|
176
162
|
Path: .opencode/tui/agency-debug.ts
|
|
177
|
-
Agency's managed OpenCode TUI diagnostic companion is current
|
|
178
|
-
|
|
179
|
-
Pi workbase extension: synced
|
|
180
|
-
Path: .pi/extensions/agency-workbase.ts
|
|
181
|
-
Agency's managed Pi extension provides whole-workbase context and exposes writable-checkout skills.`)
|
|
163
|
+
Agency's managed OpenCode TUI diagnostic companion is current.`)
|
|
182
164
|
})
|
|
183
165
|
})
|