@hcflabs/pi-subagent 27.10.8 → 28.0.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.
@@ -33,6 +33,11 @@ const AccessSchema = StringEnum(DIRECT_ACCESS, { description: ACCESS_GUIDANCE })
33
33
  const DryRunSchema = Type.Boolean({ description: "Optional. When true, run every launch validation and return the resolved plan without starting workers, saving state, or reserving the checkout." });
34
34
  const ContinueFromSchema = Type.String({ pattern: "^D[1-9][0-9]*\\.[1-9][0-9]*$", description: "Optional direct worker reference such as D7.1. Continues that finished worker's Pi session in a new tab with this task; role must equal the recorded Role." });
35
35
 
36
+ const RepoSchema = Type.String({
37
+ minLength: 1,
38
+ description: "Optional Repository target: a child Git repository as a path relative to the Pi working directory, such as team/service. The worker runs there, and admission reserves that checkout. A task value overrides the request value. Omit it to use the Pi working directory. Use subagent_repos to list targets.",
39
+ });
40
+
36
41
  const PathsSchema = Type.Array(Type.String({ minLength: 1 }), {
37
42
  minItems: 1,
38
43
  maxItems: 32,
@@ -46,6 +51,7 @@ export const DelegationSchema = Type.Object({
46
51
  access: AccessSchema,
47
52
  paths: Type.Optional(PathsSchema),
48
53
  continueFrom: Type.Optional(ContinueFromSchema),
54
+ repo: Type.Optional(RepoSchema),
49
55
  kind: Type.Optional(DirectKindSchema),
50
56
  model: Type.Optional(ModelSchema),
51
57
  modelClass: Type.Optional(ModelClassSchema),
@@ -59,6 +65,7 @@ export const DirectWorkflowSchema = Type.Object({
59
65
  access: Type.Optional(AccessSchema),
60
66
  paths: Type.Optional(PathsSchema),
61
67
  continueFrom: Type.Optional(ContinueFromSchema),
68
+ repo: Type.Optional(RepoSchema),
62
69
  kind: Type.Optional(DirectKindSchema),
63
70
  model: Type.Optional(ModelSchema),
64
71
  modelClass: Type.Optional(ModelClassSchema),
@@ -114,8 +121,18 @@ function normalizePaths(value: Static<typeof DelegationSchema>, path: string): s
114
121
  });
115
122
  }
116
123
 
117
- function normalizeDelegation(value: Static<typeof DelegationSchema>, path: string): Delegation {
124
+ /** Check the selector shape here. Admission and launch resolve it against the Pi working directory. */
125
+ function normalizeRepo(value: string, path: string): string {
126
+ const normalized = text(value, path);
127
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(normalized) || normalized.startsWith("/") || normalized.split(/[\\/]/).includes("..")) {
128
+ throw new Error(`${path} must be a relative path without control characters or .. components.`);
129
+ }
130
+ return normalized;
131
+ }
132
+
133
+ function normalizeDelegation(value: Static<typeof DelegationSchema>, path: string, requestRepo?: string): Delegation {
118
134
  const paths = normalizePaths(value, path);
135
+ const repo = value.repo === undefined ? requestRepo : normalizeRepo(value.repo, `${path}.repo`);
119
136
  return {
120
137
  role: text(value.role, `${path}.role`),
121
138
  name: normalizeTaskName(value.name, `${path}.name`),
@@ -123,6 +140,7 @@ function normalizeDelegation(value: Static<typeof DelegationSchema>, path: strin
123
140
  access: value.access,
124
141
  ...(paths === undefined ? {} : { paths }),
125
142
  ...(value.continueFrom === undefined ? {} : { continueFrom: value.continueFrom }),
143
+ ...(repo === undefined ? {} : { repo }),
126
144
  kind: "text",
127
145
  ...(value.model === undefined ? {} : { model: text(value.model, `${path}.model`) }),
128
146
  ...(value.modelClass === undefined ? {} : { modelClass: value.modelClass }),
@@ -164,9 +182,10 @@ export function parseWorkflow(value: unknown): ParsedWorkflow {
164
182
  if (!mode) throw new Error("direct workflow must select exactly one of single, tasks, or chain.");
165
183
  const input = value as DirectInput;
166
184
  if (mode === "single" && !hasDelegation(input)) throw new Error("direct workflow requires role, name, task, and access.");
185
+ const repo = input.repo === undefined ? undefined : normalizeRepo(input.repo, "workflow.repo");
167
186
  const workflow: ParsedWorkflow = mode === "single" ? { mode, delegations: [normalizeDelegation(input as Static<typeof DelegationSchema>, "workflow")] }
168
- : mode === "parallel" ? { mode, delegations: input.tasks!.map((item, index) => normalizeDelegation(item, `tasks[${index}]`)) }
169
- : { mode, delegations: input.chain!.map((item, index) => normalizeDelegation(item, `chain[${index}]`)) };
187
+ : mode === "parallel" ? { mode, delegations: input.tasks!.map((item, index) => normalizeDelegation(item, `tasks[${index}]`, repo)) }
188
+ : { mode, delegations: input.chain!.map((item, index) => normalizeDelegation(item, `chain[${index}]`, repo)) };
170
189
  const continued = workflow.delegations.flatMap(({ continueFrom }) => continueFrom === undefined ? [] : [continueFrom]);
171
190
  if (new Set(continued).size !== continued.length) throw new Error("A direct request may continue each worker at most once.");
172
191
  return workflow;
@@ -175,6 +194,9 @@ export function parseWorkflow(value: unknown): ParsedWorkflow {
175
194
  /** A direct request is a writer request when any task declares write access. */
176
195
  export const writesCheckout = (workflow: ParsedWorkflow): boolean => workflow.delegations.some(({ access }) => access === "write");
177
196
 
197
+ /** The Repository target of each task. `undefined` means the Pi working directory. */
198
+ export const workflowRepos = (workflow: ParsedWorkflow): Array<string | undefined> => workflow.delegations.map(({ repo }) => repo);
199
+
178
200
  /** `dryRun` is a tool-call option, not part of the direct workflow or the durable isolated request. */
179
201
  export function parseDelegateTask(value: unknown): ParsedDelegateTask {
180
202
  let dryRun = false;
@@ -185,6 +207,7 @@ export function parseDelegateTask(value: unknown): ParsedDelegateTask {
185
207
  value = rest;
186
208
  }
187
209
  if (value && typeof value === "object" && !Array.isArray(value) && (value as { mode?: unknown }).mode === "isolated") {
210
+ if (Object.hasOwn(value, "repo")) throw new Error("mode isolated does not accept repo yet. Start Pi in the repository, or use mode direct.");
188
211
  return { mode: "isolated", request: parseExecuteRequest(value), dryRun };
189
212
  }
190
213
  return { mode: "direct", workflow: parseWorkflow(value), dryRun };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcflabs/pi-subagent",
3
- "version": "27.10.8",
3
+ "version": "28.0.0",
4
4
  "description": "Delegate bounded direct work or durable checked isolated task graphs to Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -74,6 +74,7 @@
74
74
  "@hcflabs/pi-herdr": "^0.5.0",
75
75
  "@hcflabs/pi-multi-codex": "^3.0.0",
76
76
  "@hcflabs/pi-process": "^0.2.0",
77
+ "@hcflabs/pi-repo": "^0.1.0",
77
78
  "@hcflabs/pi-task-models": "^9.0.0",
78
79
  "proper-lockfile": "^4.1.2"
79
80
  },
@@ -30,7 +30,7 @@ To build on a finished direct worker, add `continueFrom` with its reference (suc
30
30
 
31
31
  # Isolated requests
32
32
 
33
- Give the request a unique ID and goal. Give each task a unique ID, kind, Role, model class, bounded requirements, deliverable, `dependsOn`, and `contextFrom`. Changesets require focused task checks, and a graph containing changesets requires final checks. The root must have a `package.json` test script; `pnpm test` is added to final checks if missing and runs against the combined tip before Main promotion. Text context may name only completed text tasks, preserves declared order, and implies a scheduling dependency; do not repeat an edge in `dependsOn` and `contextFrom`.
33
+ Give the request a unique ID and goal. Give each task a unique ID, kind, Role, model class, bounded requirements, deliverable, `dependsOn`, and `contextFrom`. Changesets require focused task checks, and a graph containing changesets requires final checks. Admission adds the repository's Full suite to final checks if missing: the `repoSuites` config entry, `pnpm test` for a root `package.json` test script, or `make test` for a Makefile `test` target. The suite runs against the combined tip before Main promotion. If no suite exists, tell the user to add one or configure `repoSuites`. Text context may name only completed text tasks, preserves declared order, and implies a scheduling dependency; do not repeat an edge in `dependsOn` and `contextFrom`.
34
34
 
35
35
  Every changeset becomes ready automatically after preliminary checks pass. While a locally owned changeset task is actively working, the user may open `/subagent` to queue bounded same-worker revisions or withdraw all still-pending instructions into the native editor and submit one replacement. Cancellation after withdrawal leaves the originals removed; already-claimed instructions cannot be recalled. Each revision must produce a new clean commit and rerun preliminary checks. If a worker's turn ends with uncommitted changes, the runner sends it one commit reminder per attempt; this uses no correction, and a worktree still dirty afterwards needs attention. When no queued revision remains, the runner seals and durably records the exact ready candidate without waiting for sibling workers. There is no guaranteed post-completion editing window, and late follow-ups fail visibly.
36
36