@yycholla/pi-dynamic-workflows 3.3.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 (179) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +325 -0
  3. package/assets/readme/hero.png +0 -0
  4. package/assets/readme/hero.svg +70 -0
  5. package/assets/readme/package-cover.png +0 -0
  6. package/assets/readme/package-cover.svg +66 -0
  7. package/assets/readme/workflow.png +0 -0
  8. package/assets/readme/workflow.svg +77 -0
  9. package/dist/accept-workflow-guidance.d.ts +14 -0
  10. package/dist/accept-workflow-guidance.js +53 -0
  11. package/dist/adversarial-review.d.ts +30 -0
  12. package/dist/adversarial-review.js +107 -0
  13. package/dist/agent-history.d.ts +20 -0
  14. package/dist/agent-history.js +122 -0
  15. package/dist/agent-registry.d.ts +83 -0
  16. package/dist/agent-registry.js +190 -0
  17. package/dist/agent.d.ts +364 -0
  18. package/dist/agent.js +714 -0
  19. package/dist/builtin-commands.d.ts +19 -0
  20. package/dist/builtin-commands.js +251 -0
  21. package/dist/builtin-workflows.d.ts +45 -0
  22. package/dist/builtin-workflows.js +121 -0
  23. package/dist/code-review.d.ts +26 -0
  24. package/dist/code-review.js +181 -0
  25. package/dist/config.d.ts +37 -0
  26. package/dist/config.js +44 -0
  27. package/dist/deep-research.d.ts +30 -0
  28. package/dist/deep-research.js +124 -0
  29. package/dist/display.d.ts +134 -0
  30. package/dist/display.js +248 -0
  31. package/dist/effort-command.d.ts +28 -0
  32. package/dist/effort-command.js +68 -0
  33. package/dist/enums.d.ts +69 -0
  34. package/dist/enums.js +78 -0
  35. package/dist/errors.d.ts +113 -0
  36. package/dist/errors.js +140 -0
  37. package/dist/extension-reload.d.ts +37 -0
  38. package/dist/extension-reload.js +78 -0
  39. package/dist/fs-persistence.d.ts +63 -0
  40. package/dist/fs-persistence.js +102 -0
  41. package/dist/index.d.ts +57 -0
  42. package/dist/index.js +35 -0
  43. package/dist/logger.d.ts +21 -0
  44. package/dist/logger.js +66 -0
  45. package/dist/model-routing.d.ts +30 -0
  46. package/dist/model-routing.js +50 -0
  47. package/dist/model-spec.d.ts +29 -0
  48. package/dist/model-spec.js +252 -0
  49. package/dist/model-tier-config.d.ts +133 -0
  50. package/dist/model-tier-config.js +249 -0
  51. package/dist/run-persistence.d.ts +180 -0
  52. package/dist/run-persistence.js +294 -0
  53. package/dist/saved-commands.d.ts +28 -0
  54. package/dist/saved-commands.js +100 -0
  55. package/dist/shared-store.d.ts +98 -0
  56. package/dist/shared-store.js +212 -0
  57. package/dist/structured-output.d.ts +19 -0
  58. package/dist/structured-output.js +30 -0
  59. package/dist/task-panel.d.ts +61 -0
  60. package/dist/task-panel.js +422 -0
  61. package/dist/usage-limit-scheduler.d.ts +145 -0
  62. package/dist/usage-limit-scheduler.js +368 -0
  63. package/dist/web-tools.d.ts +20 -0
  64. package/dist/web-tools.js +120 -0
  65. package/dist/workflow-authoring-coverage.d.ts +70 -0
  66. package/dist/workflow-authoring-coverage.js +421 -0
  67. package/dist/workflow-authoring-reference.d.ts +20 -0
  68. package/dist/workflow-authoring-reference.js +156 -0
  69. package/dist/workflow-capability-contract.d.ts +131 -0
  70. package/dist/workflow-capability-contract.js +604 -0
  71. package/dist/workflow-commands.d.ts +18 -0
  72. package/dist/workflow-commands.js +260 -0
  73. package/dist/workflow-comprehension.d.ts +133 -0
  74. package/dist/workflow-comprehension.js +1321 -0
  75. package/dist/workflow-context-measurement.d.ts +72 -0
  76. package/dist/workflow-context-measurement.js +213 -0
  77. package/dist/workflow-control-tool.d.ts +30 -0
  78. package/dist/workflow-control-tool.js +176 -0
  79. package/dist/workflow-delivery-choice.d.ts +20 -0
  80. package/dist/workflow-delivery-choice.js +48 -0
  81. package/dist/workflow-editor.d.ts +93 -0
  82. package/dist/workflow-editor.js +363 -0
  83. package/dist/workflow-manager.d.ts +492 -0
  84. package/dist/workflow-manager.js +1124 -0
  85. package/dist/workflow-paths.d.ts +22 -0
  86. package/dist/workflow-paths.js +46 -0
  87. package/dist/workflow-release-gate.d.ts +39 -0
  88. package/dist/workflow-release-gate.js +309 -0
  89. package/dist/workflow-saved.d.ts +38 -0
  90. package/dist/workflow-saved.js +126 -0
  91. package/dist/workflow-settings.d.ts +70 -0
  92. package/dist/workflow-settings.js +131 -0
  93. package/dist/workflow-tool.d.ts +71 -0
  94. package/dist/workflow-tool.js +367 -0
  95. package/dist/workflow-ui.d.ts +182 -0
  96. package/dist/workflow-ui.js +1587 -0
  97. package/dist/workflow.d.ts +333 -0
  98. package/dist/workflow.js +1151 -0
  99. package/dist/workflows-models-command.d.ts +31 -0
  100. package/dist/workflows-models-command.js +156 -0
  101. package/dist/worktree.d.ts +25 -0
  102. package/dist/worktree.js +61 -0
  103. package/extensions/workflow.ts +151 -0
  104. package/package.json +104 -0
  105. package/skills/workflow-authoring/SKILL.md +30 -0
  106. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  107. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  108. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  109. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  110. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  111. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  112. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  113. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  114. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  115. package/skills/workflow-authoring/examples/tournament.js +81 -0
  116. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  117. package/skills/workflow-authoring/references/capabilities.md +41 -0
  118. package/skills/workflow-authoring/references/capability-details.md +357 -0
  119. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  120. package/skills/workflow-authoring/references/debugging.md +27 -0
  121. package/skills/workflow-authoring/references/focused-recipes.md +11 -0
  122. package/skills/workflow-authoring/references/helpers.md +9 -0
  123. package/skills/workflow-authoring/references/lifecycle.md +33 -0
  124. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  125. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  126. package/skills/workflow-authoring/references/registry-ownership.md +15 -0
  127. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  128. package/skills/workflow-authoring/references/review.md +43 -0
  129. package/skills/workflow-authoring/references/runtime.md +27 -0
  130. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  131. package/skills/workflow-authoring/references/versions.md +13 -0
  132. package/skills/workflow-patterns/SKILL.md +51 -0
  133. package/src/accept-workflow-guidance.ts +71 -0
  134. package/src/adversarial-review.ts +120 -0
  135. package/src/agent-history.ts +157 -0
  136. package/src/agent-registry.ts +221 -0
  137. package/src/agent.ts +929 -0
  138. package/src/builtin-commands.ts +286 -0
  139. package/src/builtin-workflows.ts +155 -0
  140. package/src/code-review.ts +183 -0
  141. package/src/config.ts +55 -0
  142. package/src/deep-research.ts +135 -0
  143. package/src/display.ts +367 -0
  144. package/src/effort-command.ts +87 -0
  145. package/src/enums.ts +77 -0
  146. package/src/errors.ts +199 -0
  147. package/src/extension-reload.ts +100 -0
  148. package/src/fs-persistence.ts +124 -0
  149. package/src/index.ts +176 -0
  150. package/src/logger.ts +88 -0
  151. package/src/model-routing.ts +73 -0
  152. package/src/model-spec.ts +309 -0
  153. package/src/model-tier-config.ts +296 -0
  154. package/src/run-persistence.ts +484 -0
  155. package/src/saved-commands.ts +115 -0
  156. package/src/shared-store.ts +228 -0
  157. package/src/structured-output.ts +47 -0
  158. package/src/task-panel.ts +490 -0
  159. package/src/usage-limit-scheduler.ts +432 -0
  160. package/src/web-tools.ts +124 -0
  161. package/src/workflow-authoring-coverage.ts +486 -0
  162. package/src/workflow-authoring-reference.ts +186 -0
  163. package/src/workflow-capability-contract.ts +806 -0
  164. package/src/workflow-commands.ts +287 -0
  165. package/src/workflow-comprehension.ts +1673 -0
  166. package/src/workflow-context-measurement.ts +262 -0
  167. package/src/workflow-control-tool.ts +238 -0
  168. package/src/workflow-delivery-choice.ts +69 -0
  169. package/src/workflow-editor.ts +444 -0
  170. package/src/workflow-manager.ts +1405 -0
  171. package/src/workflow-paths.ts +63 -0
  172. package/src/workflow-release-gate.ts +529 -0
  173. package/src/workflow-saved.ts +180 -0
  174. package/src/workflow-settings.ts +194 -0
  175. package/src/workflow-tool.ts +464 -0
  176. package/src/workflow-ui.ts +1789 -0
  177. package/src/workflow.ts +1615 -0
  178. package/src/workflows-models-command.ts +211 -0
  179. package/src/worktree.ts +76 -0
@@ -0,0 +1,53 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { WORKFLOW_AUTHORING_FROZEN_FILES } from "./workflow-authoring-coverage.js";
5
+ const COVERAGE_MANIFEST_PATH = "src/workflow-authoring-coverage.ts";
6
+ function sha256(source) {
7
+ return createHash("sha256").update(source).digest("hex");
8
+ }
9
+ function manifestEntry(path, hash) {
10
+ return ` path: ${JSON.stringify(path)},\n sha256: ${JSON.stringify(hash)},`;
11
+ }
12
+ /**
13
+ * Accepts reviewed changes to explicitly named frozen workflow-authoring files.
14
+ *
15
+ * Throws before writing when no path is supplied, a path is not frozen, a file
16
+ * is missing, or the coverage manifest no longer contains the expected entry.
17
+ */
18
+ export function acceptWorkflowGuidance(root, requestedPaths) {
19
+ if (requestedPaths.length === 0) {
20
+ throw new Error("Pass at least one frozen workflow-authoring path to accept.");
21
+ }
22
+ const frozenByPath = new Map(WORKFLOW_AUTHORING_FROZEN_FILES.map((entry) => [entry.path, entry]));
23
+ const paths = [...new Set(requestedPaths.map((path) => path.replace(/^\.\//, "")))];
24
+ const entries = paths.map((path) => {
25
+ const frozen = frozenByPath.get(path);
26
+ if (frozen === undefined) {
27
+ throw new Error(`${path} is not a frozen workflow-authoring guidance file.`);
28
+ }
29
+ const absolute = join(root, path);
30
+ if (!existsSync(absolute)) {
31
+ throw new Error(`Frozen workflow-authoring guidance file is missing: ${path}.`);
32
+ }
33
+ return {
34
+ path,
35
+ previousSha256: frozen.sha256,
36
+ sha256: sha256(readFileSync(absolute, "utf8")),
37
+ };
38
+ });
39
+ const manifestPath = join(root, COVERAGE_MANIFEST_PATH);
40
+ const originalManifest = readFileSync(manifestPath, "utf8");
41
+ let nextManifest = originalManifest;
42
+ for (const entry of entries) {
43
+ const previous = manifestEntry(entry.path, entry.previousSha256);
44
+ if (!nextManifest.includes(previous)) {
45
+ throw new Error(`Coverage manifest does not contain the expected frozen entry for ${entry.path}.`);
46
+ }
47
+ nextManifest = nextManifest.replace(previous, manifestEntry(entry.path, entry.sha256));
48
+ }
49
+ if (nextManifest !== originalManifest) {
50
+ writeFileSync(manifestPath, nextManifest);
51
+ }
52
+ return entries.map((entry) => ({ ...entry, changed: entry.previousSha256 !== entry.sha256 }));
53
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Adversarial review mode for workflows.
3
+ * Agents cross-check each other's findings for higher quality results.
4
+ */
5
+ export interface AdversarialReviewConfig {
6
+ /** Number of independent reviewers per finding. */
7
+ reviewerCount: number;
8
+ /** Whether to filter out findings that don't survive cross-checking. */
9
+ filterContested: boolean;
10
+ /** Minimum agreement threshold (0-1). */
11
+ agreementThreshold: number;
12
+ }
13
+ /**
14
+ * Generate an adversarial-review workflow. The script is static and reads its
15
+ * inputs from `args` (task/reviewers/threshold) — no string interpolation.
16
+ *
17
+ * Each finding is judged independently by N reviewers who are told to REFUTE it;
18
+ * a finding survives only when the share of reviewers calling it real meets the
19
+ * agreement threshold.
20
+ */
21
+ export declare function generateAdversarialReviewWorkflow(): string;
22
+ /**
23
+ * Generate a multi-perspective analysis workflow.
24
+ *
25
+ * `topic` and each `perspectives` entry are user-supplied strings baked
26
+ * directly into the generated script's source, so every one is embedded via
27
+ * JSON.stringify — a proper JS string literal that can't be broken out of by
28
+ * a quote, backslash, or backtick in the value.
29
+ */
30
+ export declare function generateMultiPerspectiveWorkflow(topic: string, perspectives: string[]): string;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Adversarial review mode for workflows.
3
+ * Agents cross-check each other's findings for higher quality results.
4
+ */
5
+ /**
6
+ * Generate an adversarial-review workflow. The script is static and reads its
7
+ * inputs from `args` (task/reviewers/threshold) — no string interpolation.
8
+ *
9
+ * Each finding is judged independently by N reviewers who are told to REFUTE it;
10
+ * a finding survives only when the share of reviewers calling it real meets the
11
+ * agreement threshold.
12
+ */
13
+ export function generateAdversarialReviewWorkflow() {
14
+ return `export const meta = {
15
+ name: 'adversarial_review',
16
+ description: 'Adversarial review: findings cross-checked by independent skeptics',
17
+ phases: [
18
+ { title: 'Investigate' },
19
+ { title: 'Refute' },
20
+ { title: 'Consensus' },
21
+ ],
22
+ }
23
+
24
+ const task = (args && args.task) || ''
25
+ const reviewers = (args && args.reviewers) || 2
26
+ const threshold = (args && args.threshold) || 0.5
27
+
28
+ phase('Investigate')
29
+ const investigation = await agent(
30
+ 'Investigate the following and list concrete, individually-checkable findings:\\n' + task,
31
+ { label: 'investigate', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }
32
+ )
33
+ const findings = investigation.findings || []
34
+
35
+ phase('Refute')
36
+ const judged = await parallel(findings.map((f, i) => () =>
37
+ parallel(Array.from({ length: reviewers }, (_, r) => () =>
38
+ agent(
39
+ 'You are a skeptical reviewer. Try to REFUTE this finding for the task below. ' +
40
+ 'Default to real=false when uncertain. Investigate with the available tools if needed.\\n\\n' +
41
+ 'TASK: ' + task + '\\nFINDING: ' + f,
42
+ { label: 'refute ' + (i + 1) + '.' + (r + 1), schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } }
43
+ )
44
+ )).then((votes) => {
45
+ const valid = votes.filter(Boolean)
46
+ const realCount = valid.filter((v) => v && v.real).length
47
+ const ratio = valid.length ? realCount / valid.length : 0
48
+ return { finding: f, realVotes: realCount, totalVotes: valid.length, survives: ratio >= threshold }
49
+ })
50
+ ))
51
+
52
+ const survivors = judged.filter((j) => j && j.survives)
53
+
54
+ phase('Consensus')
55
+ const report = await agent(
56
+ 'Write a final review report. Include ONLY the findings that survived adversarial review (listed below), ' +
57
+ 'each with a short justification. Note how many were discarded.\\n\\n' +
58
+ 'SURVIVING FINDINGS JSON:\\n' + JSON.stringify(survivors),
59
+ { label: 'consensus' }
60
+ )
61
+
62
+ return { total: findings.length, survivors, report }`;
63
+ }
64
+ /**
65
+ * Generate a multi-perspective analysis workflow.
66
+ *
67
+ * `topic` and each `perspectives` entry are user-supplied strings baked
68
+ * directly into the generated script's source, so every one is embedded via
69
+ * JSON.stringify — a proper JS string literal that can't be broken out of by
70
+ * a quote, backslash, or backtick in the value.
71
+ */
72
+ export function generateMultiPerspectiveWorkflow(topic, perspectives) {
73
+ const perspectiveAgents = perspectives
74
+ .map((p, i) => {
75
+ const label = p
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9]+/g, "-")
78
+ .replace(/^-+|-+$/g, "")
79
+ .slice(0, 20) || `perspective-${i + 1}`;
80
+ return ` () => agent(${JSON.stringify(`Analyze from ${p} perspective: `)} + topic, { label: ${JSON.stringify(label)} }),`;
81
+ })
82
+ .join("\n");
83
+ return `export const meta = {
84
+ name: 'multi_perspective_analysis',
85
+ description: ${JSON.stringify(`Analyze from ${perspectives.length} different perspectives`)},
86
+ phases: [
87
+ { title: 'Perspective Analysis' },
88
+ { title: 'Synthesis' },
89
+ ],
90
+ };
91
+
92
+ phase('Perspective Analysis');
93
+ const topic = ${JSON.stringify(topic)};
94
+ const analyses = await parallel([
95
+ ${perspectiveAgents}
96
+ ]);
97
+
98
+ phase('Synthesis');
99
+ const synthesis = await agent(
100
+ 'Synthesize these different perspectives into a balanced analysis:\\n' +
101
+ 'Analyses: ' + JSON.stringify(analyses) + '\\n' +
102
+ 'Topic: ' + topic,
103
+ { label: 'synthesizer' }
104
+ );
105
+
106
+ return { analyses, synthesis };`;
107
+ }
@@ -0,0 +1,20 @@
1
+ export type AgentHistoryRole = "user" | "assistant" | "tool";
2
+ export type AgentHistoryKind = "text" | "toolCall" | "toolResult" | "error";
3
+ export interface AgentHistoryEntry {
4
+ role: AgentHistoryRole;
5
+ kind: AgentHistoryKind;
6
+ text: string;
7
+ toolName?: string;
8
+ /** Source path for file-oriented tool calls rendered specially by the pager. */
9
+ path?: string;
10
+ /** Pi's display-oriented edit diff, preserved from EditToolDetails. */
11
+ diff?: string;
12
+ isError?: boolean;
13
+ timestamp?: number;
14
+ }
15
+ export interface AgentHistoryOptions {
16
+ maxEntries?: number;
17
+ maxTextChars?: number;
18
+ maxTotalChars?: number;
19
+ }
20
+ export declare function compactAgentHistory(messages: unknown[], options?: AgentHistoryOptions): AgentHistoryEntry[];
@@ -0,0 +1,122 @@
1
+ const DEFAULT_MAX_ENTRIES = 40;
2
+ const DEFAULT_MAX_TEXT_CHARS = 2000;
3
+ const DEFAULT_MAX_TOTAL_CHARS = 20000;
4
+ export function compactAgentHistory(messages, options = {}) {
5
+ const maxEntries = positiveInt(options.maxEntries, DEFAULT_MAX_ENTRIES);
6
+ const maxTextChars = positiveInt(options.maxTextChars, DEFAULT_MAX_TEXT_CHARS);
7
+ const maxTotalChars = positiveInt(options.maxTotalChars, DEFAULT_MAX_TOTAL_CHARS);
8
+ const entries = [];
9
+ for (const raw of messages) {
10
+ const message = asRecord(raw);
11
+ if (!message)
12
+ continue;
13
+ const role = message.role;
14
+ const timestamp = typeof message.timestamp === "number" ? message.timestamp : undefined;
15
+ if (role === "user") {
16
+ const text = textFromContent(message.content);
17
+ if (text.trim())
18
+ entries.push({ role: "user", kind: "text", text, timestamp });
19
+ continue;
20
+ }
21
+ if (role === "assistant") {
22
+ for (const part of Array.isArray(message.content) ? message.content : []) {
23
+ const block = asRecord(part);
24
+ if (!block || typeof block.type !== "string")
25
+ continue;
26
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
27
+ entries.push({ role: "assistant", kind: "text", text: block.text, timestamp });
28
+ }
29
+ else if (block.type === "toolCall" && typeof block.name === "string") {
30
+ const args = asRecord(block.arguments);
31
+ const filePath = (block.name === "write" || block.name === "edit") && typeof args?.path === "string" ? args.path : undefined;
32
+ const writeContent = block.name === "write" && filePath && typeof args?.content === "string" ? args.content : undefined;
33
+ entries.push({
34
+ role: "assistant",
35
+ kind: "toolCall",
36
+ toolName: block.name,
37
+ // A write's JSON envelope is both noisy and likely to be truncated
38
+ // into invalid JSON. Preserve its source directly so the pager can
39
+ // render it as code. Edit calls retain their path so the pager can
40
+ // pair the compact call header with the result's native Pi diff.
41
+ text: writeContent ?? stringifyCompact(block.arguments ?? {}),
42
+ path: filePath,
43
+ timestamp,
44
+ });
45
+ }
46
+ }
47
+ if (typeof message.errorMessage === "string" && message.errorMessage.trim()) {
48
+ entries.push({ role: "assistant", kind: "error", text: message.errorMessage, isError: true, timestamp });
49
+ }
50
+ continue;
51
+ }
52
+ if (role === "toolResult") {
53
+ const toolName = typeof message.toolName === "string" ? message.toolName : undefined;
54
+ const text = textFromContent(message.content) || "(no text output)";
55
+ const details = asRecord(message.details);
56
+ const diff = toolName === "edit" && typeof details?.diff === "string" ? details.diff : undefined;
57
+ entries.push({
58
+ role: "tool",
59
+ kind: message.isError ? "error" : "toolResult",
60
+ toolName,
61
+ text,
62
+ diff,
63
+ isError: Boolean(message.isError),
64
+ timestamp,
65
+ });
66
+ }
67
+ }
68
+ return fitEntries(entries, maxEntries, maxTextChars, maxTotalChars);
69
+ }
70
+ function fitEntries(entries, maxEntries, maxTextChars, maxTotalChars) {
71
+ const fitted = [];
72
+ let total = 0;
73
+ for (const entry of entries.slice(-maxEntries).reverse()) {
74
+ const remaining = maxTotalChars - total;
75
+ if (remaining <= 0)
76
+ break;
77
+ // Treat an edit diff as the entry's primary display payload. Keeping it
78
+ // within the same per-entry and total bounds prevents EditToolDetails from
79
+ // bypassing history compaction with a large changed file.
80
+ let entryBudget = Math.min(maxTextChars, remaining);
81
+ const diff = entry.diff ? truncateText(entry.diff, entryBudget) : undefined;
82
+ entryBudget -= diff?.length ?? 0;
83
+ const text = truncateText(entry.text, entryBudget);
84
+ fitted.unshift({ ...entry, text, diff });
85
+ total += text.length + (diff?.length ?? 0);
86
+ }
87
+ return fitted;
88
+ }
89
+ function textFromContent(content) {
90
+ if (typeof content === "string")
91
+ return content;
92
+ if (!Array.isArray(content))
93
+ return "";
94
+ return content
95
+ .map((part) => {
96
+ const block = asRecord(part);
97
+ return block?.type === "text" && typeof block.text === "string" ? block.text : "";
98
+ })
99
+ .filter(Boolean)
100
+ .join("");
101
+ }
102
+ function stringifyCompact(value) {
103
+ try {
104
+ return JSON.stringify(value);
105
+ }
106
+ catch {
107
+ return String(value);
108
+ }
109
+ }
110
+ function truncateText(text, maxChars) {
111
+ if (text.length <= maxChars)
112
+ return text;
113
+ if (maxChars <= 20)
114
+ return text.slice(0, maxChars);
115
+ return `${text.slice(0, maxChars - 20)}... [truncated]`;
116
+ }
117
+ function positiveInt(value, fallback) {
118
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
119
+ }
120
+ function asRecord(value) {
121
+ return value && typeof value === "object" ? value : undefined;
122
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Named workflow subagent definitions ("agentType" registry).
3
+ *
4
+ * A workflow script can route an agent() call to a reusable, named definition:
5
+ *
6
+ * agent("audit this dir", { agentType: "security-auditor" })
7
+ *
8
+ * Definitions live as Markdown files under `.pi/agents/*.md` (project, cwd-relative)
9
+ * and `~/.pi/agent/agents/*.md` (user — `getAgentDir() + "agents"`, honoring the
10
+ * `PI_CODING_AGENT_DIR` override), matching pi-coding-agent's own built-in agent
11
+ * discovery convention. The legacy `~/.pi/agents/*.md` location is still scanned as
12
+ * a deprecated fallback (with a one-time warning) so users who followed this repo's
13
+ * earlier docs are not silently broken; the new location wins on a name collision.
14
+ * Frontmatter binds the subagent's tools, model, and a body prompt; project
15
+ * definitions win over both user-level locations on a name collision. This mirrors
16
+ * Claude Code's `.claude/agents` registry: agentType is a real binding of
17
+ * tools+model+system-prompt, not a prose hint.
18
+ *
19
+ * Bound today: `tools` (allowlist), `disallowedTools` (denylist), `model`,
20
+ * and the markdown body (`prompt`). Parsed-but-ignored for now (documented): `mcp`, `skills`, `background`.
21
+ * Wired: `isolation` ("worktree") → createWorktree() in workflow.ts.
22
+ */
23
+ export interface AgentDefinition {
24
+ /** Stable identity used as the `agentType` value. */
25
+ name: string;
26
+ /** One-line summary (for discoverability in the tool guideline). */
27
+ description?: string;
28
+ /** Allowlist of coding-tool names the subagent may use. Undefined = all. */
29
+ tools?: string[];
30
+ /** Denylist of coding-tool names, applied after the allowlist. */
31
+ disallowedTools?: string[];
32
+ /** Model spec (`provider/modelId` or bare id) for this subagent. */
33
+ model?: string;
34
+ /** Isolation mode. When "worktree", agents using this type run in a git worktree. */
35
+ isolation?: "worktree";
36
+ /** Markdown body, prepended to the subagent's task as role guidance. */
37
+ prompt: string;
38
+ /** Where the definition was loaded from (project wins over user). */
39
+ source: "project" | "user";
40
+ }
41
+ export type AgentRegistry = Map<string, AgentDefinition>;
42
+ /**
43
+ * Parse one agent-definition markdown file. Returns null only when there is no
44
+ * usable content (no name derivable and an empty body).
45
+ */
46
+ export declare function parseAgentDefinition(content: string, source: "project" | "user", fileName: string): AgentDefinition | null;
47
+ /**
48
+ * Load the agent registry once for a run. Scans the project dir, then the
49
+ * user dir, then — as a deprecated fallback — the legacy user dir; the FIRST
50
+ * definition for a name wins (project > user > legacy user, then filename
51
+ * order), so a name collision is resolved deterministically and silently.
52
+ *
53
+ * When a definition is only found at the legacy location (not shadowed by
54
+ * the new user dir), a single deprecation warning is logged for this call
55
+ * telling the user to move their files — not one warning per legacy file.
56
+ *
57
+ * `opts` overrides the scanned directories (used by tests).
58
+ */
59
+ export declare function loadAgentRegistry(cwd: string, opts?: {
60
+ projectDir?: string;
61
+ userDir?: string;
62
+ legacyUserDir?: string;
63
+ }): AgentRegistry;
64
+ /** Resolve an agentType name to its definition, or undefined if not registered. */
65
+ export declare function resolveAgentType(name: string | undefined, registry: AgentRegistry): AgentDefinition | undefined;
66
+ /**
67
+ * Apply a definition's tool policy to a tool list: keep only allowlisted names
68
+ * (when an allowlist is given), then drop any denylisted names. Generic over any
69
+ * object with a `name` so it is unit-testable without real ToolDefinitions.
70
+ */
71
+ export declare function applyToolPolicy<T extends {
72
+ name: string;
73
+ }>(tools: T[], allow?: string[], deny?: string[]): T[];
74
+ /**
75
+ * A stable identity string for a resolved definition, folded into the resume
76
+ * call-hash so editing an agent `.md` invalidates that call's cached result.
77
+ */
78
+ export declare function agentDefinitionKey(def: AgentDefinition | undefined): string | null;
79
+ /** List registered agent types for discoverability in the tool guideline. */
80
+ export declare function listAgentTypes(registry: AgentRegistry): Array<{
81
+ name: string;
82
+ description?: string;
83
+ }>;
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Named workflow subagent definitions ("agentType" registry).
3
+ *
4
+ * A workflow script can route an agent() call to a reusable, named definition:
5
+ *
6
+ * agent("audit this dir", { agentType: "security-auditor" })
7
+ *
8
+ * Definitions live as Markdown files under `.pi/agents/*.md` (project, cwd-relative)
9
+ * and `~/.pi/agent/agents/*.md` (user — `getAgentDir() + "agents"`, honoring the
10
+ * `PI_CODING_AGENT_DIR` override), matching pi-coding-agent's own built-in agent
11
+ * discovery convention. The legacy `~/.pi/agents/*.md` location is still scanned as
12
+ * a deprecated fallback (with a one-time warning) so users who followed this repo's
13
+ * earlier docs are not silently broken; the new location wins on a name collision.
14
+ * Frontmatter binds the subagent's tools, model, and a body prompt; project
15
+ * definitions win over both user-level locations on a name collision. This mirrors
16
+ * Claude Code's `.claude/agents` registry: agentType is a real binding of
17
+ * tools+model+system-prompt, not a prose hint.
18
+ *
19
+ * Bound today: `tools` (allowlist), `disallowedTools` (denylist), `model`,
20
+ * and the markdown body (`prompt`). Parsed-but-ignored for now (documented): `mcp`, `skills`, `background`.
21
+ * Wired: `isolation` ("worktree") → createWorktree() in workflow.ts.
22
+ */
23
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { basename, join } from "node:path";
26
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
27
+ import { AGENTS_DIR } from "./config.js";
28
+ function toStringArray(value) {
29
+ if (value == null)
30
+ return undefined;
31
+ // YAML list form: ["read", "grep"]
32
+ if (Array.isArray(value)) {
33
+ const arr = value.filter((v) => typeof v === "string" && v.trim().length > 0).map((v) => v.trim());
34
+ return arr.length ? arr : undefined;
35
+ }
36
+ // Comma-separated string form: "read, grep, find" — the form pi-coding-agent's
37
+ // parseFrontmatter returns and the form the official subagent example uses.
38
+ if (typeof value === "string" && value.trim().length > 0) {
39
+ const arr = value
40
+ .split(",")
41
+ .map((s) => s.trim())
42
+ .filter(Boolean);
43
+ return arr.length ? arr : undefined;
44
+ }
45
+ return undefined;
46
+ }
47
+ /**
48
+ * Parse one agent-definition markdown file. Returns null only when there is no
49
+ * usable content (no name derivable and an empty body).
50
+ */
51
+ export function parseAgentDefinition(content, source, fileName) {
52
+ let parsed;
53
+ try {
54
+ parsed = parseFrontmatter(content);
55
+ }
56
+ catch {
57
+ // Malformed frontmatter: treat the whole file as a body, name from filename.
58
+ parsed = { frontmatter: {}, body: content };
59
+ }
60
+ const fm = parsed.frontmatter;
61
+ const fmName = typeof fm.name === "string" ? fm.name.trim() : "";
62
+ const name = fmName || basename(fileName).replace(/\.md$/i, "").trim();
63
+ const prompt = parsed.body.trim();
64
+ if (!name && !prompt)
65
+ return null;
66
+ return {
67
+ name,
68
+ description: typeof fm.description === "string" ? fm.description.trim() || undefined : undefined,
69
+ tools: toStringArray(fm.tools),
70
+ disallowedTools: toStringArray(fm.disallowedTools),
71
+ model: typeof fm.model === "string" ? fm.model.trim() || undefined : undefined,
72
+ isolation: typeof fm.isolation === "string" && fm.isolation.toLowerCase().trim() === "worktree" ? "worktree" : undefined,
73
+ prompt,
74
+ source,
75
+ };
76
+ }
77
+ function readDefsFromDir(dir, source) {
78
+ if (!existsSync(dir))
79
+ return [];
80
+ let files;
81
+ try {
82
+ files = readdirSync(dir).filter((f) => f.toLowerCase().endsWith(".md"));
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ const defs = [];
88
+ for (const file of files.sort()) {
89
+ try {
90
+ const def = parseAgentDefinition(readFileSync(join(dir, file), "utf-8"), source, file);
91
+ if (def)
92
+ defs.push(def);
93
+ }
94
+ catch {
95
+ // Skip unreadable/invalid files; never let one bad file break the registry.
96
+ }
97
+ }
98
+ return defs;
99
+ }
100
+ /**
101
+ * Load the agent registry once for a run. Scans the project dir, then the
102
+ * user dir, then — as a deprecated fallback — the legacy user dir; the FIRST
103
+ * definition for a name wins (project > user > legacy user, then filename
104
+ * order), so a name collision is resolved deterministically and silently.
105
+ *
106
+ * When a definition is only found at the legacy location (not shadowed by
107
+ * the new user dir), a single deprecation warning is logged for this call
108
+ * telling the user to move their files — not one warning per legacy file.
109
+ *
110
+ * `opts` overrides the scanned directories (used by tests).
111
+ */
112
+ export function loadAgentRegistry(cwd, opts) {
113
+ const projectDir = opts?.projectDir ?? join(cwd, AGENTS_DIR);
114
+ // User-level definitions live under the agent dir (e.g. ~/.pi/agent/agents/),
115
+ // matching the convention used by pi-coding-agent's built-in agent discovery
116
+ // and the official subagent extension example. Reading getAgentDir() also
117
+ // honors the PI_CODING_AGENT_DIR env override.
118
+ const userDir = opts?.userDir ?? join(getAgentDir(), "agents");
119
+ // Deprecated: this repo's docs used to point users at ~/.pi/agents/ before
120
+ // pi-coding-agent's convention (~/.pi/agent/agents/) was known. Keep scanning
121
+ // it as a fallback so those files don't silently stop resolving.
122
+ const legacyUserDir = opts?.legacyUserDir ?? join(homedir(), AGENTS_DIR);
123
+ const registry = new Map();
124
+ for (const def of readDefsFromDir(projectDir, "project")) {
125
+ if (def.name && !registry.has(def.name))
126
+ registry.set(def.name, def);
127
+ }
128
+ if (userDir !== projectDir) {
129
+ for (const def of readDefsFromDir(userDir, "user")) {
130
+ if (def.name && !registry.has(def.name))
131
+ registry.set(def.name, def);
132
+ }
133
+ }
134
+ if (legacyUserDir !== projectDir && legacyUserDir !== userDir) {
135
+ let warnedLegacy = false;
136
+ for (const def of readDefsFromDir(legacyUserDir, "user")) {
137
+ if (def.name && !registry.has(def.name)) {
138
+ registry.set(def.name, def);
139
+ if (!warnedLegacy) {
140
+ console.warn(`[agent-registry] Loaded agent definition(s) from the deprecated location "${legacyUserDir}". ` +
141
+ `Move them to "${userDir}" — the old location may stop being read in a future release.`);
142
+ warnedLegacy = true;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return registry;
148
+ }
149
+ /** Resolve an agentType name to its definition, or undefined if not registered. */
150
+ export function resolveAgentType(name, registry) {
151
+ if (!name)
152
+ return undefined;
153
+ return registry.get(name);
154
+ }
155
+ /**
156
+ * Apply a definition's tool policy to a tool list: keep only allowlisted names
157
+ * (when an allowlist is given), then drop any denylisted names. Generic over any
158
+ * object with a `name` so it is unit-testable without real ToolDefinitions.
159
+ */
160
+ export function applyToolPolicy(tools, allow, deny) {
161
+ let out = tools;
162
+ if (allow?.length) {
163
+ const allowSet = new Set(allow);
164
+ out = out.filter((t) => allowSet.has(t.name));
165
+ }
166
+ if (deny?.length) {
167
+ const denySet = new Set(deny);
168
+ out = out.filter((t) => !denySet.has(t.name));
169
+ }
170
+ return out;
171
+ }
172
+ /**
173
+ * A stable identity string for a resolved definition, folded into the resume
174
+ * call-hash so editing an agent `.md` invalidates that call's cached result.
175
+ */
176
+ export function agentDefinitionKey(def) {
177
+ if (!def)
178
+ return null;
179
+ return JSON.stringify({
180
+ tools: def.tools ?? null,
181
+ disallowedTools: def.disallowedTools ?? null,
182
+ model: def.model ?? null,
183
+ isolation: def.isolation ?? null,
184
+ prompt: def.prompt,
185
+ });
186
+ }
187
+ /** List registered agent types for discoverability in the tool guideline. */
188
+ export function listAgentTypes(registry) {
189
+ return [...registry.values()].map((d) => ({ name: d.name, description: d.description }));
190
+ }