pi-dag-workflow 0.0.0-stage → 0.7.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 (108) hide show
  1. package/CHANGELOG.md +454 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -2
  4. package/README.zh-CN.md +212 -0
  5. package/docs/COMMUNICATION.md +129 -0
  6. package/docs/RELEASING.md +66 -0
  7. package/package.json +70 -4
  8. package/src/agents/commands/child.ts +148 -0
  9. package/src/agents/commands/register.ts +404 -0
  10. package/src/agents/domain/context.ts +183 -0
  11. package/src/agents/domain/diagnostics.ts +32 -0
  12. package/src/agents/domain/owner.ts +448 -0
  13. package/src/agents/domain/profiles.ts +216 -0
  14. package/src/agents/domain/runtime.ts +603 -0
  15. package/src/agents/domain/schema.ts +15 -0
  16. package/src/agents/index.ts +5 -0
  17. package/src/agents/io/contracts.ts +50 -0
  18. package/src/agents/io/host.ts +16 -0
  19. package/src/agents/io/notices.ts +27 -0
  20. package/src/agents/io/persistence.ts +210 -0
  21. package/src/agents/io/process.ts +100 -0
  22. package/src/agents/io/profile-file.ts +16 -0
  23. package/src/agents/io/skills.ts +46 -0
  24. package/src/goal/commands/register.ts +235 -0
  25. package/src/goal/domain/failure.ts +14 -0
  26. package/src/goal/domain/owner.ts +575 -0
  27. package/src/goal/domain/policy.ts +8 -0
  28. package/src/goal/domain/prompt.ts +57 -0
  29. package/src/goal/domain/state.ts +176 -0
  30. package/src/goal/index.ts +5 -0
  31. package/src/goal/io/host.ts +8 -0
  32. package/src/plan/commands/register.ts +101 -0
  33. package/src/plan/domain/owner.ts +51 -0
  34. package/src/plan/domain/policy.ts +27 -0
  35. package/src/plan/domain/state.ts +15 -0
  36. package/src/plan/index.ts +13 -0
  37. package/src/plan/io/records.ts +3 -0
  38. package/src/retry/commands/menu.ts +158 -0
  39. package/src/retry/commands/register.ts +26 -0
  40. package/src/retry/domain/owner.ts +418 -0
  41. package/src/retry/domain/state.ts +71 -0
  42. package/src/retry/index.ts +5 -0
  43. package/src/retry/io/host.ts +15 -0
  44. package/src/shared/agent-view.ts +18 -0
  45. package/src/shared/completion.ts +111 -0
  46. package/src/shared/config-file.ts +106 -0
  47. package/src/shared/config.ts +112 -0
  48. package/src/shared/dag/cache.ts +26 -0
  49. package/src/shared/dag/graph.ts +170 -0
  50. package/src/shared/dag/layout.ts +130 -0
  51. package/src/shared/goal-view.ts +2 -0
  52. package/src/shared/i18n.ts +62 -0
  53. package/src/shared/message-types.ts +12 -0
  54. package/src/shared/messages.en.ts +1290 -0
  55. package/src/shared/passive-message.ts +50 -0
  56. package/src/shared/pi-cli.ts +29 -0
  57. package/src/shared/profile-launch.ts +110 -0
  58. package/src/shared/state-types.ts +5 -0
  59. package/src/shared/task-schema.ts +39 -0
  60. package/src/shared/task-types.ts +25 -0
  61. package/src/shared/terminal.ts +19 -0
  62. package/src/shared/tool-info.ts +8 -0
  63. package/src/shared/ui/filter.ts +144 -0
  64. package/src/shared/ui/menu.ts +447 -0
  65. package/src/shared/ui/panel.ts +768 -0
  66. package/src/shared/ui/presentation.ts +624 -0
  67. package/src/shared/ui/render-cache.ts +63 -0
  68. package/src/shared/ui/render.ts +641 -0
  69. package/src/shared/ui/symbols.ts +136 -0
  70. package/src/team/commands/bootstrap.ts +52 -0
  71. package/src/team/commands/controls.ts +91 -0
  72. package/src/team/commands/history.ts +100 -0
  73. package/src/team/commands/menu.ts +455 -0
  74. package/src/team/commands/register.ts +1321 -0
  75. package/src/team/domain/inbox.ts +178 -0
  76. package/src/team/domain/launcher.ts +355 -0
  77. package/src/team/domain/lifecycle.ts +34 -0
  78. package/src/team/domain/teams.ts +161 -0
  79. package/src/team/index.ts +4 -0
  80. package/src/team/io/inbox.ts +255 -0
  81. package/src/team/io/launcher.ts +270 -0
  82. package/src/team/io/lock.ts +19 -0
  83. package/src/team/io/registry.ts +477 -0
  84. package/src/team/io/runtime.ts +353 -0
  85. package/src/team/io/session-activity.ts +185 -0
  86. package/src/team/io/session-delete.ts +98 -0
  87. package/src/team/io/session-history.ts +153 -0
  88. package/src/team/io/session-tail.ts +132 -0
  89. package/src/team/io/sid-lock.ts +115 -0
  90. package/src/team/io/sid-session.ts +98 -0
  91. package/src/todos/commands/checkpoints.ts +31 -0
  92. package/src/todos/commands/register.ts +395 -0
  93. package/src/todos/domain/checkpoint-state.ts +32 -0
  94. package/src/todos/domain/checkpoint-summary.ts +146 -0
  95. package/src/todos/domain/cleanup.ts +25 -0
  96. package/src/todos/domain/owner.ts +109 -0
  97. package/src/todos/domain/presets.ts +192 -0
  98. package/src/todos/domain/state.ts +287 -0
  99. package/src/todos/index.ts +15 -0
  100. package/src/todos/io/records.ts +8 -0
  101. package/src/ui/index.ts +4 -0
  102. package/src/workflow/commands/connect.ts +81 -0
  103. package/src/workflow/commands/view.ts +121 -0
  104. package/src/workflow/domain/features.ts +5 -0
  105. package/src/workflow/domain/view-owner.ts +18 -0
  106. package/src/workflow/domain/view-state.ts +12 -0
  107. package/src/workflow/index.ts +225 -0
  108. package/src/workflow/io/session.ts +48 -0
@@ -0,0 +1,216 @@
1
+ import { saveProfileText } from "../io/profile-file.ts";
2
+ import { readConfigText } from "../../shared/config-file.ts";
3
+
4
+ import { CORE_TOOLS, CHILD_TOOLS } from '../io/contracts.ts';
5
+ export { CORE_TOOLS, CHILD_TOOLS } from '../io/contracts.ts';
6
+ const OPTIONAL_TOOLS = new Set<string>(CHILD_TOOLS.filter((name) => !(CORE_TOOLS as readonly string[]).includes(name)));
7
+
8
+ export function validateToolNames(value: unknown, allowed?: readonly string[]): string[] {
9
+ const boundary = allowed === undefined ? '' : `Allowed: ${allowed.join(', ')}; discover with subagent_inspect tools:true`;
10
+ if (!Array.isArray(value) || value.some((name) => typeof name !== 'string')) throw new Error(`tools must be a list of supported tool names. ${boundary}`);
11
+ const tools = (value as string[]).map(name => name.trim());
12
+ for (const name of tools) if (!name || /[\s,\x00-\x1f]/.test(name) || allowed !== undefined && !allowed.includes(name)) throw new Error(`Child tool is not trusted or supported: ${name}. ${boundary}`);
13
+ return [...new Set(tools)];
14
+ }
15
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
16
+ import type { ThinkingLevel, ModelRef, Profile, ResolvedProfile } from '../io/contracts.ts';
17
+ export type { ThinkingLevel, ModelRef, Profile, ResolvedProfile } from '../io/contracts.ts';
18
+ interface ProfileRegistry { find(provider: string, id: string): unknown }
19
+ /** Lenient read of declared extension paths: used to trust extension tools before full validation. */
20
+ export function profileExtensionPaths(text: string): string[] {
21
+ try {
22
+ const config = JSON.parse(text) as { profiles?: unknown } | null;
23
+ if (!config || typeof config !== 'object' || !Array.isArray(config.profiles)) return [];
24
+ const paths: string[] = [];
25
+ for (const item of config.profiles) {
26
+ if (!item || typeof item !== 'object') continue;
27
+ const entries = (item as { childExtensions?: unknown }).childExtensions;
28
+ if (Array.isArray(entries)) for (const entry of entries) if (typeof entry === 'string' && entry.trim()) paths.push(entry.trim());
29
+ }
30
+ return [...new Set(paths)];
31
+ } catch { return []; }
32
+ }
33
+ /** Read the profile file once and return every declared extension path, ignoring invalid entries. */
34
+ export async function readProfileExtensionPaths(path?: string): Promise<string[]> {
35
+ if (!path) return [];
36
+ let text: string;
37
+ try { text = await readConfigText(path, 65536, 'Profile configuration exceeds 64 KiB'); }
38
+ catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
39
+ return profileExtensionPaths(text);
40
+ }
41
+ interface ProfileStoreOptions { path?: string; registry: ProfileRegistry; trustedTools?: readonly string[]; getTools?(): readonly string[] }
42
+
43
+ function object(value: unknown): Record<string, unknown> {
44
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected a configuration object");
45
+ return value as Record<string, unknown>;
46
+ }
47
+ function exactKeys(value: Record<string, unknown>, keys: string[]): void {
48
+ const unknown = Object.keys(value).find((key) => !keys.includes(key));
49
+ if (unknown) throw new Error(`Unknown profile field: ${unknown}`);
50
+ }
51
+ function nonempty(value: unknown, field: string): string {
52
+ if (typeof value !== "string" || !value.trim() || value.length > 200 || /[\r\n\0]/.test(value)) throw new Error(`Invalid ${field}`);
53
+ return value;
54
+ }
55
+
56
+ const MAX_SKILLS = 8;
57
+ const MAX_SKILL_PATH = 2048;
58
+ /** Any `scheme:` prefix; a single Windows drive letter is filtered out before this is applied. */
59
+ const URL_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
60
+ const WINDOWS_DRIVE = /^[a-zA-Z]:[\\/]/;
61
+
62
+ /** Configuration only. Job/task state belongs to the parent Pi branch. */
63
+ export class ProfileStore {
64
+ private profiles = new Map<string, Profile>();
65
+ private readonly options: ProfileStoreOptions;
66
+ constructor(options: ProfileStoreOptions) {
67
+ this.options = options;
68
+ for (const tool of options.trustedTools ?? []) if (!OPTIONAL_TOOLS.has(tool)) throw new Error(`Unsupported trusted child tool: ${tool}`);
69
+ }
70
+ private model(value: unknown): ModelRef {
71
+ const record = object(value);
72
+ exactKeys(record, ["provider", "id"]);
73
+ const model = { provider: nonempty(record.provider, "model provider"), id: nonempty(record.id, "model id") };
74
+ if (!this.options.registry.find(model.provider, model.id)) throw new Error(`Unknown model: ${model.provider}/${model.id} (exact registry name required)`);
75
+ return model;
76
+ }
77
+ /** One explicitly listed local Markdown skill file. Syntax only: paths are not resolved or read here. */
78
+ private skill(value: unknown): string {
79
+ if (typeof value !== "string") throw new Error("Skill paths must be text");
80
+ const path = value.trim();
81
+ if (!path) throw new Error("Skill path must contain text");
82
+ if (path.length > MAX_SKILL_PATH) throw new Error(`Skill path must be at most ${MAX_SKILL_PATH} characters`);
83
+ if (/[\p{Cc}\p{Bidi_Control}\u2028\u2029]/u.test(value)) throw new Error("Skill path must not contain control characters");
84
+ if (URL_SCHEME.test(path) && !WINDOWS_DRIVE.test(path)) throw new Error('Skill must be a local Markdown file path, not a URL');
85
+ return path;
86
+ }
87
+ /**
88
+ * Bounded list of local skill files, in the order the user listed them. Absolute, `~/` and
89
+ * working-directory-relative paths are all kept verbatim; existence is deliberately not checked
90
+ * here so a profile written in one working directory still resolves correctly in another.
91
+ */
92
+ validateSkills(value: unknown): string[] {
93
+ if (!Array.isArray(value) || value.length > MAX_SKILLS || value.some((skill) => typeof skill !== "string")) throw new Error(`skills must be a list of at most ${MAX_SKILLS} local skill file paths`);
94
+ return [...new Set(value.map((skill) => this.skill(skill)))];
95
+ }
96
+ validateTools(value: unknown): string[] {
97
+ return validateToolNames(value, this.allowedTools());
98
+ }
99
+ /**
100
+ * Local extension files the children of this profile may load. Syntax only; the paths are resolved
101
+ * against the configuration directory when the profile is dispatched (same basis as skills).
102
+ */
103
+ validateChildExtensions(value: unknown): string[] {
104
+ if (!Array.isArray(value) || value.length > MAX_SKILLS || value.some(entry => typeof entry !== "string")) throw new Error(`childExtensions must be a list of at most ${MAX_SKILLS} local extension file paths`);
105
+ return [...new Set((value as string[]).map(entry => {
106
+ const path = entry.trim();
107
+ if (!path) throw new Error('Child extension path must contain text');
108
+ if (path.length > MAX_SKILL_PATH) throw new Error(`Child extension path must be at most ${MAX_SKILL_PATH} characters`);
109
+ if (/[\p{Cc}\p{Bidi_Control}\u2028\u2029]/u.test(entry)) throw new Error('Child extension path must not contain control characters');
110
+ if (URL_SCHEME.test(path) && !WINDOWS_DRIVE.test(path)) throw new Error('Child extension must be a local file path, not a URL');
111
+ return path;
112
+ }))];
113
+ }
114
+ allowedTools(): readonly string[] { return this.options.getTools?.() ?? [...CORE_TOOLS, ...(this.options.trustedTools ?? [])]; }
115
+ /**
116
+ * Free-form guidance for the children this profile dispatches. It travels to the child process
117
+ * through the environment and arrives there as one passive custom message (never a system prompt
118
+ * section), so it is bounded and normalized before it ever leaves this store.
119
+ */
120
+ instructions(value: unknown): string {
121
+ if (typeof value !== "string") throw new Error("instructions must be text");
122
+ const text = value.replace(/\r\n?/g, "\n").trim();
123
+ if (!text) throw new Error("instructions must contain text");
124
+ if (text.length > 2000) throw new Error("instructions must be at most 2000 characters");
125
+ return text;
126
+ }
127
+ private validate(value: unknown): Profile {
128
+ const record = object(value);
129
+ exactKeys(record, ["name", "model", "thinking", "tools", "skills", "instructions", "childExtensions", "isolate"]);
130
+ const name = nonempty(record.name, "profile name");
131
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,47}$/.test(name) || name === "inherit") throw new Error("Profile name must be 1–48 letters, digits, '_' or '-' (inherit is reserved)");
132
+ if (record.thinking !== undefined && !THINKING_LEVELS.includes(record.thinking as ThinkingLevel)) throw new Error("Invalid thinking level");
133
+ if (record.isolate !== undefined && typeof record.isolate !== "boolean") throw new Error("Profile isolate must be true or false");
134
+ const model = record.model === undefined ? undefined : this.model(record.model);
135
+ if (model && record.thinking !== undefined && record.thinking !== 'off' && (this.options.registry.find(model.provider, model.id) as { reasoning?: boolean } | undefined)?.reasoning === false) throw new Error('Selected model does not support thinking; use off');
136
+ return {
137
+ name,
138
+ ...(model === undefined ? {} : { model }),
139
+ ...(record.thinking === undefined ? {} : { thinking: record.thinking as ThinkingLevel }),
140
+ ...(record.tools === undefined ? {} : { tools: this.validateTools(record.tools) }),
141
+ ...(record.skills === undefined ? {} : { skills: this.validateSkills(record.skills) }),
142
+ ...(record.instructions === undefined ? {} : { instructions: this.instructions(record.instructions) }),
143
+ ...(record.childExtensions === undefined ? {} : { childExtensions: this.validateChildExtensions(record.childExtensions) }),
144
+ ...(record.isolate === undefined ? {} : { isolate: record.isolate as boolean }),
145
+ };
146
+ }
147
+ /** Validate an unsaved UI draft using exactly the persistence rules, without changing the store. */
148
+ validateDraft(profile: Profile): Profile { return this.validate(profile); }
149
+ list(): Profile[] { return structuredClone([...this.profiles.values()]); }
150
+ get(name: string): Profile | undefined { const value = this.profiles.get(name); return value ? structuredClone(value) : undefined; }
151
+ set(profile: Profile): Profile {
152
+ const value = this.validate(profile);
153
+ if (!this.profiles.has(value.name) && this.profiles.size >= 64) throw new Error('profiles must contain at most 64 profiles');
154
+ this.profiles.set(value.name, value); return structuredClone(value);
155
+ }
156
+ delete(name: string): boolean { return this.profiles.delete(name); }
157
+ resolve(
158
+ name?: string,
159
+ inheritedModel?: ModelRef,
160
+ toolsOverride?: string[],
161
+ inherited?: { thinking?: ThinkingLevel | undefined; tools?: readonly string[] | undefined },
162
+ overrides?: { model?: ModelRef; thinking?: ThinkingLevel; skills?: string[] },
163
+ previous?: ResolvedProfile,
164
+ ): ResolvedProfile {
165
+ const profile = name && name !== 'inherit' ? this.get(name) : undefined;
166
+ if (name && name !== 'inherit' && !profile) throw new Error(`Unknown profile: ${name}`);
167
+ // An omitted profile resumes the snapshot; only explicit inherit resets it to the parent.
168
+ const base = name === 'inherit' ? undefined : previous;
169
+ const selected = overrides?.model ?? profile?.model ?? base?.model ?? inheritedModel;
170
+ if (!selected) throw new Error("Select a parent model or configure a named profile model before spawning");
171
+ const model = this.model(selected);
172
+ const reasoning = (this.options.registry.find(model.provider, model.id) as { reasoning?: boolean } | undefined)?.reasoning;
173
+ const explicitThinking = overrides?.thinking ?? profile?.thinking;
174
+ let thinking = explicitThinking ?? base?.thinking ?? inherited?.thinking ?? 'off';
175
+ if (!THINKING_LEVELS.includes(thinking)) throw new Error('Invalid thinking level');
176
+ if (thinking !== 'off' && reasoning === false) {
177
+ if (explicitThinking !== undefined) throw new Error('Selected model does not support thinking; use off');
178
+ thinking = 'off';
179
+ }
180
+ const allowed = new Set<string>(this.allowedTools());
181
+ const inheritedAllowed = (tool: string) => allowed.has(tool) && (CHILD_TOOLS as readonly string[]).includes(tool);
182
+ const inheritedTools = inherited?.tools === undefined ? [...CORE_TOOLS] : inherited.tools.filter(inheritedAllowed);
183
+ const ignored = toolsOverride === undefined && profile?.tools === undefined && base?.tools === undefined ? [...new Set((inherited?.tools ?? []).filter((name) => !inheritedAllowed(name)))] : [];
184
+ const skills = overrides?.skills ?? profile?.skills ?? base?.skills;
185
+ const instructions = profile?.instructions ?? base?.instructions;
186
+ const childExtensions = profile?.childExtensions ?? base?.childExtensions;
187
+ return {
188
+ name: profile?.name ?? base?.name ?? 'inherit', model, thinking,
189
+ tools: this.validateTools(toolsOverride ?? profile?.tools ?? base?.tools ?? [...new Set(inheritedTools)]),
190
+ ...(skills === undefined ? {} : { skills: this.validateSkills(skills) }),
191
+ ...(instructions === undefined ? {} : { instructions }),
192
+ ...(childExtensions === undefined ? {} : { childExtensions: this.validateChildExtensions(childExtensions) }),
193
+ ...(ignored.length ? { ignoredInheritedTools: ignored } : {}),
194
+ };
195
+ }
196
+ async load(): Promise<void> {
197
+ if (!this.options.path) return;
198
+ let text: string;
199
+ try { text = await readConfigText(this.options.path, 65536, 'Profile configuration exceeds 64 KiB'); }
200
+ catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { this.profiles.clear(); return; } throw error; }
201
+ const config = object(JSON.parse(text));
202
+ exactKeys(config, ["profiles"]);
203
+ if (!Array.isArray(config.profiles) || config.profiles.length > 64) throw new Error("profiles must contain at most 64 profiles");
204
+ const validated = config.profiles.map((item) => this.validate(item));
205
+ if (new Set(validated.map((item) => item.name)).size !== validated.length) throw new Error("Duplicate profile name");
206
+ this.profiles = new Map(validated.map((item) => [item.name, item]));
207
+ }
208
+ async save(): Promise<void> {
209
+ const path = this.options.path;
210
+ if (!path) throw new Error("No profile configuration path configured");
211
+ const profiles = this.list().map((item) => this.validate(item));
212
+ const source = `${JSON.stringify({ profiles }, null, 2)}\n`;
213
+ if (Buffer.byteLength(source) > 65536) throw new Error('Profile configuration exceeds 64 KiB');
214
+ await saveProfileText(path, source);
215
+ }
216
+ }