@herbertgao/pi-subagents 0.18.5 → 0.22.0-local.1

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.
@@ -7,14 +7,15 @@
7
7
  *
8
8
  * ## Which typebox
9
9
  *
10
- * **`typebox`, not `@sinclair/typebox`.** They are different packages and both
11
- * are installed here. `@sinclair/typebox` (0.34) dispatches on a `Kind` symbol
12
- * that a schema arriving over the wire does not carry, so `Value.Check` throws
13
- * `Unknown type` on a plain JSON Schema — and `Type.Unsafe` does not help, it
14
- * stamps a `Kind` that is not registered. `typebox` v1 is a standards JSON
15
- * Schema validator and takes the schema as-is. It is also the package pi itself
16
- * types `ToolDefinition.parameters` against, so the same schema object serves
17
- * both roles with no conversion.
10
+ * **`typebox`, not `@sinclair/typebox`.** They are different packages.
11
+ * `@sinclair/typebox` (0.34) dispatches on a `Kind` symbol that a schema
12
+ * arriving over the wire does not carry, so `Value.Check` throws `Unknown type`
13
+ * on a plain JSON Schema — and `Type.Unsafe` does not help, it stamps a `Kind`
14
+ * that is not registered. `typebox` v1 is a standards JSON Schema validator and
15
+ * takes the schema as-is. It is also the package pi itself types
16
+ * `ToolDefinition.parameters` against and supplies to extensions at runtime
17
+ * (a peer dependency here), so the same schema object serves both roles with no
18
+ * conversion.
18
19
  *
19
20
  * ## Why we validate at all
20
21
  *
@@ -41,18 +41,36 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
41
41
  import { isAbsolute, join } from "node:path"
42
42
  import { getAgentDir } from "@earendil-works/pi-coding-agent"
43
43
  import { isSymlink, isUnsafeName, safeReadFile } from "../memory.js"
44
+ import {
45
+ isPackageResourceExcluded,
46
+ packageWorkflowDirs,
47
+ packageWorkflowFiles,
48
+ } from "../package-resources.js"
44
49
  import { hasMetaDeclaration } from "./meta.js"
45
50
  import { MAX_SCRIPT_LENGTH } from "./runtime.js"
46
51
 
47
52
  /** Extension a saved workflow file carries. */
48
53
  const WORKFLOW_EXTENSION = ".js"
49
54
 
50
- /** The roots a `name` is looked up in, highest priority first. */
55
+ /**
56
+ * The roots a `name` is looked up in, highest priority first.
57
+ *
58
+ * Directories declared by installed pi packages come last, so a project, shared
59
+ * workspace or personal script always wins the name. That is the same tier
60
+ * package-provided agents get, and the same tier pi gives package-provided
61
+ * skills — a package offers a workflow, it never takes a name away from you.
62
+ *
63
+ * The `packageWorkflows` gate and pi's project-trust answer are session state in
64
+ * package-resources.ts rather than parameters here, so a gate set in `/agents`
65
+ * reaches this without an options bag travelling through `resolveWorkflowScript`,
66
+ * `resolveWorkflowSource` and `readSavedWorkflow` purely to be consulted once.
67
+ */
51
68
  export function savedWorkflowRoots(cwd: string): string[] {
52
69
  return [
53
70
  join(cwd, ".pi", "workflows"),
54
71
  join(cwd, ".agents", "workflows"),
55
72
  join(getAgentDir(), "workflows"),
73
+ ...packageWorkflowDirs(cwd),
56
74
  ]
57
75
  }
58
76
 
@@ -82,6 +100,9 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
82
100
  for (const root of roots) {
83
101
  if (isSymlink(root)) continue // reject a symlinked root entirely, as skill-loader does
84
102
  const path = join(root, `${trimmed}${WORKFLOW_EXTENSION}`)
103
+ // A `!` entry in the declaring package's manifest. Only ever true for a path
104
+ // under a package root, so the local roots pay one set lookup for it.
105
+ if (isPackageResourceExcluded(path, cwd, "workflows")) continue
85
106
  const script = safeReadFile(path)
86
107
  if (script === undefined) continue
87
108
  // Found the file, so stop looking — a shadowing name that turns out not to
@@ -97,6 +118,25 @@ export function readSavedWorkflow(name: string, cwd: string): SavedWorkflow {
97
118
  return { ok: true, script, path }
98
119
  }
99
120
 
121
+ // Last: a package that declared an individual file rather than a directory.
122
+ // It carries no root to look a name up in, so it is matched by name here
123
+ // instead — after every root, which keeps package scripts lowest-precedence.
124
+ const declared = packageWorkflowFiles(cwd).get(trimmed)
125
+ if (declared !== undefined) {
126
+ const script = safeReadFile(declared)
127
+ if (script !== undefined) {
128
+ if (!hasMetaDeclaration(script)) {
129
+ return {
130
+ ok: false,
131
+ message:
132
+ `"${declared}" is not a workflow script — it has no \`export const meta = { name, description }\` ` +
133
+ "declaration. Nothing was run.",
134
+ }
135
+ }
136
+ return { ok: true, script, path: declared }
137
+ }
138
+ }
139
+
100
140
  const known = listSavedWorkflows(cwd)
101
141
  return {
102
142
  ok: false,
@@ -158,9 +198,15 @@ export function listSavedWorkflows(cwd: string): string[] {
158
198
  if (!entry.endsWith(WORKFLOW_EXTENSION)) continue
159
199
  const name = entry.slice(0, -WORKFLOW_EXTENSION.length)
160
200
  if (isUnsafeName(name)) continue
161
- if (isWorkflowFile(join(root, entry))) names.add(name)
201
+ const path = join(root, entry)
202
+ if (isPackageResourceExcluded(path, cwd, "workflows")) continue
203
+ if (isWorkflowFile(path)) names.add(name)
162
204
  }
163
205
  }
206
+ for (const [name, path] of packageWorkflowFiles(cwd)) {
207
+ if (isUnsafeName(name)) continue
208
+ if (isWorkflowFile(path)) names.add(name)
209
+ }
164
210
  return [...names].sort()
165
211
  }
166
212