portable-agent-layer 0.88.0 → 0.89.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.
Files changed (88) hide show
  1. package/README.md +8 -5
  2. package/assets/agents/gemini-researcher.md +13 -0
  3. package/assets/agents/grok-researcher.md +13 -0
  4. package/assets/agents/multi-perspective-researcher.md +12 -0
  5. package/assets/agents/perplexity-researcher.md +13 -0
  6. package/assets/agents/skill-author.md +17 -0
  7. package/assets/agents/subagent-author.md +18 -1
  8. package/assets/skills/create-subagent/SKILL.md +3 -2
  9. package/assets/skills/create-subagent/authoring-guide.md +16 -0
  10. package/assets/templates/PAL/MEMORY_SYSTEM.md +1 -1
  11. package/assets/templates/PAL/README.md +6 -5
  12. package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +4 -1
  13. package/assets/templates/hooks.antigravity.json +42 -0
  14. package/assets/templates/settings.antigravity.json +44 -0
  15. package/package.json +1 -1
  16. package/src/cli/builtin-tools.ts +28 -23
  17. package/src/cli/doctor/agents.ts +64 -64
  18. package/src/cli/identity.ts +26 -20
  19. package/src/cli/index.ts +109 -312
  20. package/src/cli/knowledge.ts +160 -201
  21. package/src/cli/ledger-command.ts +69 -0
  22. package/src/cli/ledger.ts +20 -101
  23. package/src/cli/personal-context.ts +15 -12
  24. package/src/cli/rule.ts +29 -29
  25. package/src/cli/server.ts +33 -52
  26. package/src/cli/session-agent.ts +7 -9
  27. package/src/cli/skill.ts +73 -64
  28. package/src/cli/subagent.ts +51 -58
  29. package/src/cli/tree.ts +177 -0
  30. package/src/hooks/InvocationContext.ts +26 -0
  31. package/src/hooks/LedgerCommit.ts +4 -2
  32. package/src/hooks/LedgerSnapshot.ts +2 -0
  33. package/src/hooks/SecurityValidator.ts +2 -0
  34. package/src/hooks/StopOrchestrator.ts +12 -2
  35. package/src/hooks/UserPromptOrchestrator.ts +3 -15
  36. package/src/hooks/handlers/context-digests.ts +21 -21
  37. package/src/hooks/handlers/inject-retrieval.ts +7 -5
  38. package/src/hooks/handlers/readme-sync.ts +6 -2
  39. package/src/hooks/handlers/session-name.ts +3 -12
  40. package/src/hooks/lib/agent-definition.ts +6 -16
  41. package/src/hooks/lib/agent-registry.ts +88 -0
  42. package/src/hooks/lib/agent.ts +47 -23
  43. package/src/hooks/lib/antigravity-inference.ts +84 -0
  44. package/src/hooks/lib/antigravity-transcript.ts +129 -0
  45. package/src/hooks/lib/claim-check.ts +13 -2
  46. package/src/hooks/lib/claim-log.ts +5 -1
  47. package/src/hooks/lib/claude-md.ts +21 -1
  48. package/src/hooks/lib/context.ts +5 -1
  49. package/src/hooks/lib/hook-turn.ts +25 -2
  50. package/src/hooks/lib/inference.ts +77 -1
  51. package/src/hooks/lib/invocation-context.ts +99 -0
  52. package/src/hooks/lib/ledger-hook.ts +14 -6
  53. package/src/hooks/lib/models.ts +34 -17
  54. package/src/hooks/lib/paths.ts +39 -6
  55. package/src/hooks/lib/prompt-capture.ts +28 -0
  56. package/src/hooks/lib/prompt-text.ts +1 -0
  57. package/src/hooks/lib/readme-sync.ts +3 -31
  58. package/src/hooks/lib/security-gate.ts +10 -3
  59. package/src/hooks/lib/semi-static.ts +13 -3
  60. package/src/hooks/lib/session-context.ts +3 -1
  61. package/src/hooks/lib/session-names.ts +20 -4
  62. package/src/hooks/lib/stop.ts +1 -0
  63. package/src/hooks/lib/transcript.ts +6 -3
  64. package/src/targets/agent-render.ts +3 -3
  65. package/src/targets/antigravity/install.ts +68 -0
  66. package/src/targets/antigravity/uninstall.ts +66 -0
  67. package/src/targets/lib.ts +81 -64
  68. package/src/targets/opencode/install.ts +2 -0
  69. package/src/targets/opencode/permission.ts +39 -0
  70. package/src/targets/opencode/uninstall.ts +9 -8
  71. package/src/tools/agent/algorithm-reflect.ts +70 -62
  72. package/src/tools/agent/algorithm-synthesize.ts +39 -14
  73. package/src/tools/agent/analyze.ts +31 -36
  74. package/src/tools/agent/handoff-note.ts +51 -54
  75. package/src/tools/agent/interaction.ts +32 -28
  76. package/src/tools/agent/project.ts +300 -287
  77. package/src/tools/agent/relationship-note.ts +40 -36
  78. package/src/tools/agent/relationship-reflect.ts +23 -32
  79. package/src/tools/agent/synthesize.ts +26 -27
  80. package/src/tools/agent/thread.ts +42 -51
  81. package/src/tools/agent/wisdom-frame.ts +45 -38
  82. package/src/tools/control-room/server.ts +0 -2
  83. package/src/tools/control-room/ui/dist/assets/{index-BZMDzWFa.js → index-mFpXpiBj.js} +1 -1
  84. package/src/tools/control-room/ui/dist/index.html +1 -1
  85. package/src/tools/control-room/ui/screens/log.tsx +10 -1
  86. package/src/tools/lib/command.ts +275 -0
  87. package/src/tools/subagent-doctor.ts +74 -9
  88. package/src/tools/token-cost.ts +18 -17
@@ -3,26 +3,9 @@
3
3
  *
4
4
  * Thin presentation layer over src/tools/knowledge/{lib,graph}.ts. Owns
5
5
  * formatting + argv parsing only; all entity logic lives in the tools.
6
- *
7
- * Subcommands:
8
- * search <query> Substring search across title/tags/body
9
- * graph <slug> [--hops N] BFS traversal from a slug (default 2 hops)
10
- * stats Counts, hubs, isolated nodes
11
- * hubs Top 10 most-connected entities
12
- * find <tag> Entities tagged with <tag>
13
- * show <slug> Print one entity (frontmatter + body)
14
- * add <domain> <name> Create entity (interactive unless flags given)
15
- * --tags ai,research Comma-separated tags
16
- * --related slug:type Repeatable; type ∈ RELATION_TYPES
17
- * --quality 0-10
18
- * --status seedling|budding|evergreen
19
- * --type <subtype> Free-form sub-type (default by domain)
20
- * ls [domain] List entities, optionally by one domain
21
6
  */
22
7
 
23
8
  import { readFileSync } from "node:fs";
24
- import { parseArgs } from "node:util";
25
- import * as clack from "@clack/prompts";
26
9
  import { toPath } from "../hooks/lib/paths";
27
10
  import { buildGraph, resolveSlug, stats, traverse } from "../tools/knowledge/graph";
28
11
  import {
@@ -43,76 +26,111 @@ import {
43
26
  STATUSES,
44
27
  type Status,
45
28
  } from "../tools/knowledge/lib";
46
-
47
- // ── Dispatcher ─────────────────────────────────────────────────────
48
-
49
- export async function runKnowledge(args: string[]): Promise<number> {
50
- const [sub, ...rest] = args;
51
- switch (sub) {
52
- case "search":
53
- return cmdSearch(rest);
54
- case "graph":
55
- return cmdGraph(rest);
56
- case "stats":
57
- return cmdStats();
58
- case "hubs":
59
- return cmdHubs();
60
- case "find":
61
- return cmdFind(rest);
62
- case "show":
63
- return cmdShow(rest);
64
- case "add":
65
- return cmdAdd(rest);
66
- case "ls":
67
- return cmdLs(rest);
68
- case "ingest":
69
- return cmdIngest(rest);
70
- case undefined:
71
- case "help":
72
- case "--help":
73
- case "-h":
74
- showHelp();
75
- return 0;
76
- default:
77
- console.error(`Unknown subcommand: ${sub}\n`);
78
- showHelp();
79
- return 1;
80
- }
81
- }
82
-
83
- // ── Helpers ────────────────────────────────────────────────────────
84
-
85
- function showHelp(): void {
86
- console.log(`
87
- Usage:
88
- pal cli knowledge <subcommand> [args]
89
-
90
- Subcommands:
91
- search <query> Substring search across title, tags, body
92
- graph <slug> [--hops N] BFS traversal from a slug (default 2 hops)
93
- stats Counts, hubs, isolated nodes
94
- hubs Top 10 most-connected entities
95
- find <tag> Entities tagged with <tag>
96
- show <slug> Print one entity (frontmatter + body)
97
- add <domain> <name> Create entity (interactive unless flags given)
98
- --tags ai,research Comma-separated tags
99
- --related slug:type Typed relation (repeatable)
100
- --quality 0-10 Default 5
101
- --status seedling|budding|evergreen Default seedling
102
- --type <subtype> Free-form sub-type
103
- ls [domain] List entities (optionally by one domain)
104
- ingest [--file F] Upsert JSON from stdin (or --file)
105
- --source <id> Provenance tag (default "manual")
106
-
107
- Domains: People, Companies, Ideas, Research
108
- Relation types: ${RELATION_TYPES.join(", ")}
109
- `);
29
+ import { group, leaf, runCommand, UsageError } from "../tools/lib/command";
30
+
31
+ const VOCABULARY = `Domains: ${DOMAINS.join(", ")}
32
+ Relation types: ${RELATION_TYPES.join(", ")}`;
33
+
34
+ export const knowledgeCommand = group({
35
+ summary: "Query and manage the knowledge store",
36
+ commands: {
37
+ search: leaf({
38
+ summary: "Substring search across title, tags, body",
39
+ args: "<query>",
40
+ run: ({ positionals }) => cmdSearch(positionals[0]),
41
+ }),
42
+ graph: leaf({
43
+ summary: "BFS traversal from a slug (default 2 hops)",
44
+ args: "<slug>",
45
+ options: {
46
+ hops: { type: "string", value: "<n>", description: "Hops to follow (default 2)" },
47
+ },
48
+ run: ({ positionals, values }) => cmdGraph(positionals[0], parseHops(values.hops)),
49
+ }),
50
+ stats: leaf({ summary: "Counts, hubs, isolated nodes", run: cmdStats }),
51
+ hubs: leaf({ summary: "Top 10 most-connected entities", run: cmdHubs }),
52
+ find: leaf({
53
+ summary: "Entities tagged with <tag>",
54
+ args: "<tag>",
55
+ run: ({ positionals }) => cmdFind(positionals[0]),
56
+ }),
57
+ show: leaf({
58
+ summary: "Print one entity (frontmatter + body)",
59
+ args: "<slug>",
60
+ run: ({ positionals }) => cmdShow(positionals[0]),
61
+ }),
62
+ add: leaf({
63
+ summary: "Create an entity (interactive unless flags are given)",
64
+ args: "<domain> <name>",
65
+ options: {
66
+ tags: { type: "string", value: "<a,b>", description: "Comma-separated tags" },
67
+ related: {
68
+ type: "string",
69
+ multiple: true,
70
+ value: "<slug:type>",
71
+ description: "Typed relation (repeatable)",
72
+ },
73
+ quality: { type: "string", value: "<0-10>", description: "Default 5" },
74
+ status: {
75
+ type: "string",
76
+ value: `<${STATUSES.join("|")}>`,
77
+ description: "Default seedling",
78
+ },
79
+ type: { type: "string", value: "<subtype>", description: "Free-form sub-type" },
80
+ body: { type: "string", value: "<text>", description: "Markdown body" },
81
+ },
82
+ details: VOCABULARY,
83
+ run: ({ positionals, values }) =>
84
+ cmdAdd(
85
+ parseDomain(positionals[0]),
86
+ positionals[1],
87
+ parseAddFlags(values),
88
+ Object.keys(values).length > 0
89
+ ),
90
+ }),
91
+ ls: leaf({
92
+ summary: "List entities, optionally by one domain",
93
+ args: "[domain]",
94
+ details: VOCABULARY,
95
+ run: ({ positionals }) =>
96
+ cmdLs(positionals[0] === undefined ? undefined : parseDomain(positionals[0])),
97
+ }),
98
+ ingest: leaf({
99
+ summary: "Upsert people and companies from JSON on stdin (or --file)",
100
+ options: {
101
+ source: {
102
+ type: "string",
103
+ short: "s",
104
+ value: "<id>",
105
+ description: 'Provenance tag (default "manual")',
106
+ },
107
+ file: {
108
+ type: "string",
109
+ short: "f",
110
+ value: "<path>",
111
+ description: "Read the JSON from a file instead of stdin",
112
+ },
113
+ },
114
+ run: ({ values }) => cmdIngest(values.source ?? "manual", values.file),
115
+ }),
116
+ },
117
+ details: VOCABULARY,
118
+ });
119
+
120
+ export function runKnowledge(args: string[]): Promise<number> {
121
+ return runCommand(knowledgeCommand, args, ["pal", "cli", "knowledge"]);
110
122
  }
111
123
 
112
124
  function isDomain(s: string): s is Domain {
113
125
  return (DOMAINS as readonly string[]).includes(s);
114
126
  }
115
127
 
128
+ function parseDomain(value: string): Domain {
129
+ if (!isDomain(value))
130
+ throw new UsageError(`domain must be one of: ${DOMAINS.join(", ")}`);
131
+ return value;
132
+ }
133
+
116
134
  function isStatus(s: string): s is Status {
117
135
  return (STATUSES as readonly string[]).includes(s);
118
136
  }
@@ -171,12 +189,7 @@ function scoreEntity(entity: Entity, q: string): number {
171
189
  return score;
172
190
  }
173
191
 
174
- function cmdSearch(args: string[]): number {
175
- const q = args[0];
176
- if (!q) {
177
- console.error("Usage: pal cli knowledge search <query>");
178
- return 1;
179
- }
192
+ function cmdSearch(q: string): number {
180
193
  const hits: SearchHit[] = [];
181
194
  for (const e of list()) {
182
195
  const s = scoreEntity(e, q);
@@ -195,23 +208,15 @@ function cmdSearch(args: string[]): number {
195
208
 
196
209
  // ── graph ──────────────────────────────────────────────────────────
197
210
 
198
- function cmdGraph(args: string[]): number {
199
- const { values, positionals } = parseArgs({
200
- args,
201
- options: { hops: { type: "string" } },
202
- allowPositionals: true,
203
- strict: false,
204
- });
205
- const query = positionals[0];
206
- if (!query) {
207
- console.error("Usage: pal cli knowledge graph <slug> [--hops N]");
208
- return 1;
209
- }
210
- const hops = values.hops ? Number(values.hops) : 2;
211
+ function parseHops(value: string | undefined): number {
212
+ const hops = value === undefined ? 2 : Number(value);
211
213
  if (!Number.isInteger(hops) || hops < 1) {
212
- console.error("--hops must be a positive integer");
213
- return 1;
214
+ throw new UsageError("--hops must be a positive integer");
214
215
  }
216
+ return hops;
217
+ }
218
+
219
+ function cmdGraph(query: string, hops: number): number {
215
220
  const g = buildGraph();
216
221
  const slug = resolveSlug(g, query);
217
222
  if (!slug) {
@@ -302,12 +307,8 @@ function cmdHubs(): number {
302
307
 
303
308
  // ── find ───────────────────────────────────────────────────────────
304
309
 
305
- function cmdFind(args: string[]): number {
306
- const tag = args[0]?.toLowerCase();
307
- if (!tag) {
308
- console.error("Usage: pal cli knowledge find <tag>");
309
- return 1;
310
- }
310
+ function cmdFind(rawTag: string): number {
311
+ const tag = rawTag.toLowerCase();
311
312
  // Accept both the bare tag and the topic-prefixed form so users don't
312
313
  // need to know which kind a given concept was stored as.
313
314
  const prefixedTag = tag.startsWith("topic:") ? tag : `topic:${tag}`;
@@ -331,12 +332,7 @@ function cmdFind(args: string[]): number {
331
332
 
332
333
  // ── show ───────────────────────────────────────────────────────────
333
334
 
334
- function cmdShow(args: string[]): number {
335
- const query = args[0];
336
- if (!query) {
337
- console.error("Usage: pal cli knowledge show <slug>");
338
- return 1;
339
- }
335
+ function cmdShow(query: string): number {
340
336
  const g = buildGraph();
341
337
  const slug = resolveSlug(g, query);
342
338
  if (!slug) {
@@ -385,91 +381,74 @@ interface AddFlags {
385
381
  body?: string;
386
382
  }
387
383
 
384
+ interface AddFlagValues {
385
+ tags?: string;
386
+ related?: string[];
387
+ quality?: string;
388
+ status?: string;
389
+ type?: string;
390
+ body?: string;
391
+ }
392
+
388
393
  function parseRelatedFlag(value: string): Related {
389
394
  const [slug, type] = value.split(":");
390
395
  if (!slug || !type) {
391
- throw new Error(`--related must be slug:type, got "${value}"`);
396
+ throw new UsageError(`--related must be slug:type, got "${value}"`);
392
397
  }
393
398
  if (!isRelationType(type)) {
394
- throw new Error(
399
+ throw new UsageError(
395
400
  `--related type must be one of: ${RELATION_TYPES.join(", ")} (got "${type}")`
396
401
  );
397
402
  }
398
403
  return { slug, type };
399
404
  }
400
405
 
401
- function parseAddFlags(args: string[]): AddFlags {
402
- const { values } = parseArgs({
403
- args,
404
- options: {
405
- tags: { type: "string" },
406
- related: { type: "string", multiple: true },
407
- quality: { type: "string" },
408
- status: { type: "string" },
409
- type: { type: "string" },
410
- body: { type: "string" },
411
- },
412
- allowPositionals: true,
413
- strict: false,
414
- });
415
- const tags = values.tags
416
- ? String(values.tags)
417
- .split(",")
418
- .map((t) => t.trim())
419
- .filter(Boolean)
420
- : [];
421
- const related = ((values.related as string[] | undefined) ?? []).map(parseRelatedFlag);
422
- const quality = values.quality != null ? Number(values.quality) : undefined;
423
- if (quality != null && (!Number.isInteger(quality) || quality < 0 || quality > 10)) {
424
- throw new Error("--quality must be an integer 0-10");
425
- }
426
- const status = values.status as string | undefined;
427
- if (status && !isStatus(status)) {
428
- throw new Error(`--status must be one of: ${STATUSES.join(", ")}`);
406
+ function parseQuality(value: string | undefined): number | undefined {
407
+ if (value === undefined) return undefined;
408
+ const quality = Number(value);
409
+ if (!Number.isInteger(quality) || quality < 0 || quality > 10) {
410
+ throw new UsageError("--quality must be an integer 0-10");
429
411
  }
412
+ return quality;
413
+ }
414
+
415
+ function parseStatus(value: string | undefined): Status | undefined {
416
+ if (value === undefined) return undefined;
417
+ if (!isStatus(value))
418
+ throw new UsageError(`--status must be one of: ${STATUSES.join(", ")}`);
419
+ return value;
420
+ }
421
+
422
+ function parseAddFlags(values: AddFlagValues): AddFlags {
423
+ const tags = (values.tags ?? "")
424
+ .split(",")
425
+ .map((t) => t.trim())
426
+ .filter(Boolean);
430
427
  return {
431
428
  tags,
432
- related,
433
- quality,
434
- status: status as Status | undefined,
435
- type: values.type as string | undefined,
436
- body: values.body as string | undefined,
429
+ related: (values.related ?? []).map(parseRelatedFlag),
430
+ quality: parseQuality(values.quality),
431
+ status: parseStatus(values.status),
432
+ type: values.type,
433
+ body: values.body,
437
434
  };
438
435
  }
439
436
 
440
- async function cmdAdd(args: string[]): Promise<number> {
441
- // First two positionals: domain, name.
442
- const positional = args.filter((a) => !a.startsWith("--"));
443
- const domainStr = positional[0];
444
- const name = positional[1];
445
- if (!domainStr || !isDomain(domainStr)) {
446
- console.error(`Usage: pal cli knowledge add <${DOMAINS.join("|")}> <name> [flags]`);
447
- return 1;
448
- }
449
- if (!name) {
450
- console.error("Missing <name>");
451
- return 1;
452
- }
453
-
454
- const flagArgs = args.slice(args.indexOf(name) + 1);
455
- const hasAnyFlag = flagArgs.some((a) => a.startsWith("--"));
456
- let flags: AddFlags;
457
- try {
458
- flags = parseAddFlags(flagArgs);
459
- } catch (e) {
460
- console.error(e instanceof Error ? e.message : String(e));
461
- return 1;
462
- }
463
-
464
- // Interactive mode only when no flags and we have a TTY.
465
- if (!hasAnyFlag && process.stdin.isTTY) {
437
+ async function cmdAdd(
438
+ domain: Domain,
439
+ name: string,
440
+ given: AddFlags,
441
+ anyFlagGiven: boolean
442
+ ): Promise<number> {
443
+ let flags = given;
444
+ if (!anyFlagGiven && process.stdin.isTTY) {
466
445
  const enriched = await runInteractiveAdd(flags);
467
446
  if (enriched === null) return 1;
468
447
  flags = enriched;
469
448
  }
470
449
 
471
450
  const entity = getOrCreate({
472
- domain: domainStr,
451
+ domain,
473
452
  name,
474
453
  tags: flags.tags,
475
454
  related: flags.related,
@@ -483,6 +462,7 @@ async function cmdAdd(args: string[]): Promise<number> {
483
462
  }
484
463
 
485
464
  async function runInteractiveAdd(prefilled: AddFlags): Promise<AddFlags | null> {
465
+ const clack = await import("@clack/prompts");
486
466
  clack.intro("Add knowledge entry");
487
467
  const tagsInput = await clack.text({
488
468
  message: "Tags (comma-separated, optional):",
@@ -528,13 +508,7 @@ async function runInteractiveAdd(prefilled: AddFlags): Promise<AddFlags | null>
528
508
 
529
509
  // ── ls ─────────────────────────────────────────────────────────────
530
510
 
531
- function cmdLs(args: string[]): number {
532
- const domainArg = args[0];
533
- if (domainArg && !isDomain(domainArg)) {
534
- console.error(`Domain must be one of: ${DOMAINS.join(", ")}`);
535
- return 1;
536
- }
537
- const target = domainArg ? (domainArg as Domain) : undefined;
511
+ function cmdLs(target: Domain | undefined): number {
538
512
  const entries = list(target).sort(
539
513
  (a, b) => a.domain.localeCompare(b.domain) || a.slug.localeCompare(b.slug)
540
514
  );
@@ -559,25 +533,10 @@ async function readIngestInput(file: string | undefined): Promise<string | null>
559
533
  return await Bun.stdin.text();
560
534
  }
561
535
 
562
- async function cmdIngest(args: string[]): Promise<number> {
563
- const { values } = parseArgs({
564
- args,
565
- options: {
566
- source: { type: "string", short: "s", default: "manual" },
567
- file: { type: "string", short: "f" },
568
- },
569
- strict: true,
570
- });
571
-
572
- const sourceId = values.source ?? "manual";
573
- const raw = await readIngestInput(values.file);
574
-
536
+ async function cmdIngest(sourceId: string, file: string | undefined): Promise<number> {
537
+ const raw = await readIngestInput(file);
575
538
  if (raw === null || !raw.trim()) {
576
- console.error(
577
- "Usage: echo '<JSON>' | pal cli knowledge ingest --source <id>\n" +
578
- " or: pal cli knowledge ingest --file <path> --source <id>"
579
- );
580
- return 1;
539
+ throw new UsageError("pipe the JSON on stdin or pass --file <path>");
581
540
  }
582
541
 
583
542
  let data: IngestPayload;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The `pal cli ledger` command tree. The queries load only when one runs, so a
3
+ * missing ledger dependency cannot stop the rest of the CLI, the doctor included.
4
+ */
5
+
6
+ import { group, leaf } from "../tools/lib/command";
7
+
8
+ const JSON_OPTION = { type: "boolean", description: "Machine-readable output" } as const;
9
+
10
+ const FILTER_OPTIONS = {
11
+ project: {
12
+ type: "string",
13
+ value: "<slug>",
14
+ description: "Actions against a registered project",
15
+ },
16
+ since: {
17
+ type: "string",
18
+ value: "<7d|2026-09-01>",
19
+ description: "A duration back from now, or a date",
20
+ },
21
+ until: { type: "string", value: "<date>", description: "Upper bound on the timestamp" },
22
+ actor: { type: "string", value: "<id>", description: "Who caused it" },
23
+ machine: { type: "string", value: "<id>", description: "Which install wrote it" },
24
+ runtime: {
25
+ type: "string",
26
+ value: "<agent>",
27
+ description: "claude, cursor, codex, copilot, opencode",
28
+ },
29
+ outcome: {
30
+ type: "string",
31
+ value: "<applied|failed|denied>",
32
+ description: "What became of the action",
33
+ },
34
+ tool: { type: "string", value: "<Edit|Write>", description: "The tool that acted" },
35
+ target: {
36
+ type: "string",
37
+ value: "<substring>",
38
+ description: "Match anywhere in the recorded path",
39
+ },
40
+ limit: { type: "string", value: "<n>", description: "Keep the newest n matches" },
41
+ json: JSON_OPTION,
42
+ } as const;
43
+
44
+ export const ledgerCommand = group({
45
+ summary: "Query the action ledger",
46
+ details: `Examples:
47
+ pal cli ledger log --project portable-agent-layer --since 7d
48
+ pal cli ledger log --target memory/ --outcome applied
49
+ pal cli ledger stats --since 24h`,
50
+ commands: {
51
+ log: leaf({
52
+ summary: "Matching actions, oldest first",
53
+ options: FILTER_OPTIONS,
54
+ run: async ({ values }) => (await import("./ledger")).ledgerLog(values),
55
+ }),
56
+ show: leaf({
57
+ summary: "One action in full: change, target, standing",
58
+ args: "<id>",
59
+ options: { json: JSON_OPTION },
60
+ run: async ({ positionals, values }) =>
61
+ (await import("./ledger")).ledgerShow(positionals[0], values.json === true),
62
+ }),
63
+ stats: leaf({
64
+ summary: "Counts by outcome, runtime, actor, tool, target",
65
+ options: FILTER_OPTIONS,
66
+ run: async ({ values }) => (await import("./ledger")).ledgerStats(values),
67
+ }),
68
+ },
69
+ });