pi-crew 0.8.14 → 0.9.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +366 -0
  2. package/README.md +112 -2
  3. package/docs/FEATURE_INTAKE.md +1 -1
  4. package/docs/HARNESS.md +20 -19
  5. package/docs/PROJECT_REVIEW.md +132 -133
  6. package/docs/PROJECT_REVIEW_FIXES.md +130 -131
  7. package/docs/actions-reference.md +127 -121
  8. package/docs/architecture.md +1 -1
  9. package/docs/code-review-2026-05-11.md +134 -134
  10. package/docs/commands-reference.md +108 -106
  11. package/docs/comparison-pi-subagents-vs-pi-crew.md +105 -105
  12. package/docs/deep-review-report.md +1 -1
  13. package/docs/dynamic-workflows.md +90 -0
  14. package/docs/fixes/BATCH_A_H1_H2.md +17 -17
  15. package/docs/fixes/bug-007-async-notifier-stale-ctx.md +23 -23
  16. package/docs/followup-plan-2026-05-12.md +135 -135
  17. package/docs/followup-review-2026-05-12.md +86 -86
  18. package/docs/followup-review-round3-2026-05-12.md +123 -123
  19. package/docs/goals.md +59 -0
  20. package/docs/implementation-plan-top3.md +4 -4
  21. package/docs/issue-29-analysis.md +2 -2
  22. package/docs/oh-my-pi-research.md +154 -154
  23. package/docs/optimization-plan.md +2 -0
  24. package/docs/perf/baseline-2026-05.md +9 -9
  25. package/docs/perf/final-report-2026-05.md +2 -2
  26. package/docs/perf/sprint-1-report.md +2 -2
  27. package/docs/perf/sprint-2-report.md +1 -1
  28. package/docs/perf/upgrade-plan-2026-05.md +72 -72
  29. package/docs/pi-crew-bugs.md +230 -230
  30. package/docs/pi-crew-investigation-report.md +102 -102
  31. package/docs/pi-crew-test-round5.md +4 -4
  32. package/docs/runtime-analysis-child-vs-live.md +57 -57
  33. package/docs/runtime-migration-in-process-analysis.md +97 -97
  34. package/package.json +2 -4
  35. package/skills/orchestration/SKILL.md +11 -11
  36. package/src/agents/agent-config.ts +4 -0
  37. package/src/config/config.ts +39 -0
  38. package/src/config/types.ts +11 -0
  39. package/src/extension/action-suggestions.ts +2 -1
  40. package/src/extension/async-notifier.ts +10 -0
  41. package/src/extension/help.ts +14 -0
  42. package/src/extension/registration/commands.ts +27 -0
  43. package/src/extension/team-tool/destructive-gate.ts +1 -1
  44. package/src/extension/team-tool/goal-wrap.ts +288 -0
  45. package/src/extension/team-tool/goal.ts +405 -0
  46. package/src/extension/team-tool/run.ts +103 -4
  47. package/src/extension/team-tool/workflow-manage.ts +194 -0
  48. package/src/extension/team-tool.ts +20 -0
  49. package/src/hooks/types.ts +3 -1
  50. package/src/runtime/async-runner.ts +27 -2
  51. package/src/runtime/background-runner.ts +68 -19
  52. package/src/runtime/child-pi.ts +9 -1
  53. package/src/runtime/completion-guard.ts +1 -1
  54. package/src/runtime/dynamic-workflow-context.ts +450 -0
  55. package/src/runtime/dynamic-workflow-runner.ts +180 -0
  56. package/src/runtime/global-worker-cap.ts +96 -0
  57. package/src/runtime/goal-evaluator.ts +294 -0
  58. package/src/runtime/goal-loop-runner.ts +612 -0
  59. package/src/runtime/goal-state-store.ts +209 -0
  60. package/src/runtime/iteration-hooks.ts +2 -1
  61. package/src/runtime/pi-args.ts +10 -2
  62. package/src/runtime/post-checks.ts +2 -1
  63. package/src/runtime/result-extractor.ts +32 -0
  64. package/src/runtime/team-runner.ts +11 -1
  65. package/src/runtime/verification-gates.ts +88 -5
  66. package/src/runtime/verification-integrity.ts +110 -0
  67. package/src/runtime/verification-worktree.ts +136 -0
  68. package/src/runtime/workspace-lock.ts +448 -0
  69. package/src/schema/config-schema.ts +26 -0
  70. package/src/schema/team-tool-schema.ts +39 -4
  71. package/src/state/atomic-write.ts +9 -0
  72. package/src/state/contracts.ts +14 -0
  73. package/src/state/crew-init.ts +18 -5
  74. package/src/state/event-log.ts +7 -1
  75. package/src/state/state-store.ts +2 -0
  76. package/src/state/types.ts +82 -0
  77. package/src/state/worker-atomic-writer.ts +190 -0
  78. package/src/utils/env-allowlist.ts +30 -0
  79. package/src/utils/redaction.ts +104 -24
  80. package/src/utils/safe-paths.ts +55 -14
  81. package/src/workflows/discover-workflows.ts +25 -1
  82. package/src/workflows/workflow-config.ts +13 -0
  83. package/src/worktree/cleanup.ts +2 -1
  84. package/src/worktree/worktree-manager.ts +4 -3
  85. package/teams/parallel-research.team.md +1 -1
  86. package/workflows/examples/hello.dwf.ts +24 -0
@@ -6,6 +6,36 @@
6
6
  // Pattern for PEM private keys (possessive quantifier prevents backtracking)
7
7
  export const PEM_PRIVATE_KEY_PATTERN = /-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g;
8
8
 
9
+ // --- P1f (RFC §P1f / §6 STRIDE) — additional anchored, ReDoS-SAFE secret patterns. ---
10
+ // All patterns below are LINEAR-TIME: each uses a single bounded quantifier on a
11
+ // character class (fixed {N} or a plain +) with NO nested quantifiers and NO
12
+ // overlapping alternation. Boundaries are zero-width lookarounds on simple char
13
+ // classes, which are also linear. Do NOT introduce (a+)+-style nesting here.
14
+ //
15
+ // RESIDUAL (documented, Med-High per RFC §6): regex redaction is BEST-EFFORT
16
+ // against an *adversarial* worker that can encode/split/transform secrets
17
+ // (base64, line splits, novel formats, non-pattern env vars). This catches the
18
+ // common/accidental leak; it is NOT a boundary against a determined exfiltrator.
19
+ // Full mitigation ladder: (1) redaction here + at artifact-write; (2) Phase 1.5
20
+ // sanitized-env verification; (3) sandbox (deferred).
21
+
22
+ // JWT — three base64url segments separated by dots, distinctive "eyJ" headers.
23
+ // Linear: single + on [A-Za-z0-9_-] per segment, no nesting.
24
+ export const JWT_PATTERN = /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
25
+
26
+ // GitHub PAT (classic + fine-grained prefixes) — fixed 36-char base62 tail.
27
+ // Linear: fixed {36} count on a char class (constant time per match position).
28
+ export const GITHUB_PAT_PATTERN = /(?<![A-Za-z0-9_])gh[pousr]_[A-Za-z0-9]{36}(?![A-Za-z0-9])/g;
29
+
30
+ // AWS access key id — fixed 16-char uppercase-alphanumeric tail.
31
+ // Linear: fixed {16} count on a char class.
32
+ export const AWS_ACCESS_KEY_PATTERN = /(?<![A-Za-z0-9])AKIA[0-9A-Z]{16}(?![0-9A-Z])/g;
33
+
34
+ // Optional extras (RFC OQ13) — same ReDoS-safe shape (fixed counts / single +).
35
+ export const SLACK_TOKEN_PATTERN = /(?<![A-Za-z0-9_-])xox[baprs]-[A-Za-z0-9-]{10,}/g;
36
+ export const GOOGLE_API_KEY_PATTERN = /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])/g;
37
+ export const STRIPE_KEY_PATTERN = /(?<![A-Za-z0-9_])sk_live_[0-9a-zA-Z]{24}(?![0-9a-zA-Z])/g;
38
+
9
39
  // Linear-time secret key detection
10
40
  // IMPORTANT: This function must maintain linear-time guarantees.
11
41
  // The fast-path regex uses simple string alternatives with anchors only (no quantifiers),
@@ -18,23 +48,52 @@ export function isSecretKey(keyName: string): boolean {
18
48
  if (/^(token|apikey|api_key|password|secret|credential|authorization|privatekey|private_key)$/.test(lower)) {
19
49
  return true;
20
50
  }
21
- // Linear scan for prefix characters followed by keywords
51
+ // Linear scan for prefix characters followed by keywords.
52
+ // FIX (cold-review #1 of Phase 1): use `lower.startsWith(kw, i+1)` + `lower.charAt(...)` to
53
+ // avoid allocating substring+toLowerCase inside the O(n) loop (was O(n^2) — adversarial
54
+ // worker emitting `_`×N+`=` stalled 200KB→29s, 500KB→216s). Now O(n).
22
55
  const prefixes = "_.-";
23
56
  const keywords = ["token", "api", "key", "password", "passwd", "secret", "credential", "authorization", "private"];
24
-
57
+
25
58
  for (let i = 0; i < keyName.length; i++) {
26
59
  if (prefixes.includes(keyName[i])) {
27
- const remaining = keyName.substring(i + 1).toLowerCase();
28
60
  for (const kw of keywords) {
29
- if (remaining.startsWith(kw)) {
30
- const afterKw = remaining.substring(kw.length);
31
- if (afterKw === "" || prefixes.includes(afterKw[0]) || /[a-zA-Z0-9]/.test(afterKw[0])) {
61
+ if (lower.startsWith(kw, i + 1)) {
62
+ const afterCh = lower.charAt(i + 1 + kw.length);
63
+ if (afterCh === "" || prefixes.includes(afterCh) || /[a-zA-Z0-9]/.test(afterCh)) {
32
64
  return true;
33
65
  }
34
66
  }
35
67
  }
36
68
  }
37
69
  }
70
+ // FIX (P1f, surfaced by notification-sink test): also match camelCase
71
+ // boundaries (e.g. `apiToken`, `clientSecret`, `authKey`) — the separator
72
+ // scan above requires `_-.` between prefix and keyword and MISSES the very
73
+ // common camelCase pattern. Scan: a keyword matches if it appears with a
74
+ // word boundary (start of string, end of string, camelCase lowercase->upper
75
+ // transition, or one of `_-.` separators). Linear: one forward pass.
76
+ for (const kw of keywords) {
77
+ let from = 0;
78
+ while (true) {
79
+ const idx = lower.indexOf(kw, from);
80
+ if (idx === -1) break;
81
+ const before = idx === 0 ? "" : lower.charAt(idx - 1);
82
+ const afterIdx = idx + kw.length;
83
+ const afterCh = afterIdx >= lower.length ? "" : lower.charAt(afterIdx);
84
+ const atStart = idx === 0;
85
+ const atEnd = afterIdx === lower.length;
86
+ const camelBoundary = /[A-Z]/.test(keyName.charAt(afterIdx)); // lowercase->uppercase in original
87
+ const sepBoundary = prefixes.includes(before) || prefixes.includes(afterCh);
88
+ if (atStart || atEnd || camelBoundary || sepBoundary) {
89
+ // Require non-empty chars before/after to avoid matching `api` inside `capitalize`
90
+ const hasBefore = idx > 0;
91
+ const hasAfter = afterIdx < lower.length;
92
+ if (hasBefore || hasAfter) return true;
93
+ }
94
+ from = idx + 1;
95
+ }
96
+ }
38
97
  return false;
39
98
  }
40
99
 
@@ -124,9 +183,20 @@ export function redactSecretString(value: string): string {
124
183
  // Replace Authorization headers (non-Bearer format)
125
184
  result = redactAuthHeader(result);
126
185
 
127
- // Replace Bearer tokens
186
+ // Replace Bearer tokens (run before structured-token patterns so a
187
+ // "Bearer <jwt>" pair is collapsed first; bare tokens are caught below).
128
188
  result = redactBearerTokens(result);
129
189
 
190
+ // P1f: structured secret tokens (JWT / GitHub PAT / AWS keys + optional
191
+ // Slack/Google/Stripe). Best-effort vs adversarial workers (see note above).
192
+ result = result
193
+ .replace(JWT_PATTERN, "***")
194
+ .replace(GITHUB_PAT_PATTERN, "***")
195
+ .replace(AWS_ACCESS_KEY_PATTERN, "***")
196
+ .replace(SLACK_TOKEN_PATTERN, "***")
197
+ .replace(GOOGLE_API_KEY_PATTERN, "***")
198
+ .replace(STRIPE_KEY_PATTERN, "***");
199
+
130
200
  // Replace inline secrets: key=value or key:value patterns
131
201
  result = redactInlineSecrets(result);
132
202
 
@@ -134,51 +204,61 @@ export function redactSecretString(value: string): string {
134
204
  }
135
205
 
136
206
  // Linear-time inline secret redaction: token=xxx, api_key=xxx, etc.
207
+ // FIX (P1f): previously O(n^2) — after a non-secret alphanumeric run, the loop did
208
+ // i++ (advance 1 char) and re-scanned from i+1, so a long run was rescanned O(n)
209
+ // times = O(n^2). The P1f ReDoS test (300KB no-dot input) surfaced this pre-existing
210
+ // bug. Now advances past the whole run when it isn't a redactable secret -> O(n).
137
211
  function redactInlineSecrets(value: string): string {
138
212
  const result: string[] = [];
139
213
  let i = 0;
140
-
214
+
141
215
  while (i < value.length) {
142
- // Look for pattern: word_chars + = or : + non-whitespace_value
143
- // Check for secret key followed by = or :
216
+ // Collect a run of key characters (alphanumeric, underscore, hyphen).
144
217
  let j = i;
145
- let keyLen = 0;
146
-
147
- // Collect key characters (alphanumeric, underscore, hyphen)
148
218
  while (j < value.length && /[a-zA-Z0-9_-]/.test(value[j])) {
149
219
  j++;
150
- keyLen++;
151
220
  }
152
-
221
+ const keyLen = j - i;
222
+
223
+ let redacted = false;
153
224
  if (keyLen > 0 && j < value.length && (value[j] === '=' || value[j] === ':')) {
154
- const key = value.substring(i, i + keyLen);
155
-
225
+ const key = value.substring(i, j);
226
+
156
227
  // Check if this is a secret key
157
228
  if (isSecretKey(key)) {
158
229
  // Find the value (everything after = or : until space, comma, or end)
159
230
  const sep = value[j];
160
231
  let k = j + 1;
161
232
  let valLen = 0;
162
- while (k < value.length && valLen < 500 && value[k] !== ' ' && value[k] !== ',' && value[k] !== ';' && value[k] !== '"' && value[k] !== '"' && value[k] !== '\r' && value[k] !== '\n') {
233
+ while (k < value.length && valLen < 500 && value[k] !== ' ' && value[k] !== ',' && value[k] !== ';' && value[k] !== '"' && value[k] !== '\r' && value[k] !== '\n') {
163
234
  k++;
164
235
  valLen++;
165
236
  }
166
-
237
+
167
238
  // Only redact if there's actual content
168
239
  if (valLen > 0) {
169
240
  result.push(key);
170
241
  result.push(sep);
171
242
  result.push("***");
172
243
  i = k;
173
- continue;
244
+ redacted = true;
174
245
  }
175
246
  }
176
247
  }
177
-
178
- result.push(value[i]);
179
- i++;
248
+
249
+ if (!redacted) {
250
+ if (keyLen > 0) {
251
+ // Not a redactable secret — push the WHOLE run and advance past it (O(n)).
252
+ result.push(value.substring(i, j));
253
+ i = j;
254
+ } else {
255
+ // Single non-key character (space, punctuation, etc.)
256
+ result.push(value[i]);
257
+ i++;
258
+ }
259
+ }
180
260
  }
181
-
261
+
182
262
  return result.join("");
183
263
  }
184
264
 
@@ -16,32 +16,73 @@ export function resolveContainedPath(baseDir: string, targetPath: string): strin
16
16
  }
17
17
  const base = path.resolve(baseDir);
18
18
  const resolved = path.isAbsolute(targetPath) ? path.resolve(targetPath) : path.resolve(base, targetPath);
19
- // On Windows, paths are case-insensitive and short-name (8.3) aliases may
20
- // differ from long-name forms (e.g. C:\Users\RUNNER~1 vs C:\Users\runneradmin).
21
- // We normalize both paths to their canonical form by resolving through
22
- // realpathSync, walking up ancestors for non-existent paths.
23
- const baseNorm = process.platform === "win32" ? resolveWindowsCanonical(base) : base;
24
- const resolvedNorm = process.platform === "win32" ? resolveWindowsCanonical(resolved) : resolved;
19
+ // Normalize BOTH paths to canonical form on ALL platforms. This resolves
20
+ // symlinks so that a base and target that refer to the same physical dir
21
+ // via different paths (Windows 8.3 short-name; macOS /var /private/var)
22
+ // compare equal. Without this, a legitimately-contained target under an
23
+ // OS-managed symlink is wrongly rejected as "outside" the base.
24
+ const baseNorm = resolveCanonicalPath(base);
25
+ const resolvedNorm = resolveCanonicalPath(resolved);
25
26
  const relative = process.platform === "win32"
26
27
  ? path.relative(baseNorm.toLowerCase(), resolvedNorm.toLowerCase())
27
- : path.relative(base, resolved);
28
+ : path.relative(baseNorm, resolvedNorm);
28
29
  if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Path is outside ${baseDir}: ${targetPath}`);
29
30
  return resolved;
30
31
  }
31
32
 
33
+ /**
34
+ * Platform-agnostic canonical path resolution. Resolves symlinks (via
35
+ * realpathSync) to normalize paths so the same physical location always
36
+ * yields the same string regardless of how it was referenced.
37
+ *
38
+ * - Windows: delegates to resolveWindowsCanonical (canonical LONG-NAME form,
39
+ * resolving 8.3 short-name aliases like RUNNER~1 → runneradmin).
40
+ * - POSIX: uses realpathSync, which resolves system symlinks such as the
41
+ * macOS /var → /private/var mapping.
42
+ *
43
+ * For non-existent paths (write targets), walks up to the deepest existing
44
+ * ancestor and joins the remaining components, so the canonical prefix is
45
+ * still comparable.
46
+ */
47
+ function resolveCanonicalPath(p: string): string {
48
+ if (process.platform === "win32") return resolveWindowsCanonical(p);
49
+ try {
50
+ return fs.realpathSync(p);
51
+ } catch {
52
+ const parts: string[] = [];
53
+ let current = p;
54
+ while (current !== path.dirname(current)) {
55
+ try {
56
+ const real = fs.realpathSync(current);
57
+ let acc = real;
58
+ for (let i = parts.length - 1; i >= 0; i--) acc = path.join(acc, parts[i]);
59
+ return acc;
60
+ } catch { /* keep walking up */ }
61
+ parts.push(path.basename(current));
62
+ current = path.dirname(current);
63
+ }
64
+ return p;
65
+ }
66
+ }
67
+
32
68
  /**
33
69
  * On Windows, resolve a path to its canonical (long-name) form.
34
70
  * Walks up ancestors until finding one that exists, then joins back down.
35
71
  * This handles paths where intermediate directories don't exist yet but
36
72
  * their ancestors do (and may use short-name aliases).
73
+ *
74
+ * Uses fs.realpathSync.native (canonical long-name form) as the primary
75
+ * resolver, falling back to non-native realpathSync if .native fails.
37
76
  */
38
77
  function resolveWindowsCanonical(p: string): string {
39
78
  try {
40
- // Use regular realpathSync (not .native) to preserve the input path form.
41
- // On Windows CI, .native always returns long-name (runneradmin) while
42
- // non-native preserves short-name (RUNNER~1). Using non-native ensures
43
- // the returned form matches what os.tmpdir() and mkdtempSync produce.
44
- let real = fs.realpathSync(p);
79
+ // Use the NATIVE realpath to resolve to canonical LONG-NAME form.
80
+ // On Windows, fs.realpathSync.native resolves 8.3 short-name aliases
81
+ // (e.g. RUNNER~1) to long-name (runneradmin). This is essential for
82
+ // containment checks: base (from cwd, often long-name) and target
83
+ // (from os.tmpdir()/mkdtempSync, often short-name) must normalize to
84
+ // the SAME form or a contained target is wrongly rejected as "outside".
85
+ let real = fs.realpathSync.native(p);
45
86
  // Guard against NTFS internal paths (e.g. C:\$Extend\$Deleted)
46
87
  if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
47
88
  return real;
@@ -56,8 +97,8 @@ function resolveWindowsCanonical(p: string): string {
56
97
  let current = p;
57
98
  while (current !== path.dirname(current)) {
58
99
  try {
59
- // Use non-native to preserve input path form
60
- let real = fs.realpathSync(current);
100
+ let real: string;
101
+ try { real = fs.realpathSync.native(current); } catch { real = fs.realpathSync(current); }
61
102
  // Guard against NTFS internal paths
62
103
  if (real.includes("$Extend") || real.includes("$Deleted")) throw new Error("NTFS internal path");
63
104
  // Found existing ancestor — join with remaining parts in reverse order
@@ -120,11 +120,35 @@ function parseWorkflowFile(filePath: string, source: ResourceSource): WorkflowCo
120
120
 
121
121
  function readWorkflowDir(dir: string, source: ResourceSource): WorkflowConfig[] {
122
122
  if (!fs.existsSync(dir)) return [];
123
- return fs.readdirSync(dir)
123
+ const staticWorkflows = fs.readdirSync(dir)
124
124
  .filter((entry) => entry.endsWith(".workflow.md"))
125
125
  .map((entry) => parseWorkflowFile(path.join(dir, entry), source))
126
126
  .filter((workflow): workflow is WorkflowConfig => workflow !== undefined)
127
127
  .sort((a, b) => a.name.localeCompare(b.name));
128
+ // P2: also discover dynamic workflows (*.dwf.ts). A .dwf.ts's default export is a JS orchestrator.
129
+ const dynamicWorkflows = fs.readdirSync(dir)
130
+ .filter((entry) => entry.endsWith(".dwf.ts"))
131
+ .map((entry) => parseDynamicWorkflowFile(path.join(dir, entry), source))
132
+ .filter((workflow): workflow is WorkflowConfig => workflow !== undefined);
133
+ return [...staticWorkflows, ...dynamicWorkflows].sort((a, b) => a.name.localeCompare(b.name));
134
+ }
135
+
136
+ /** P2: a .dwf.ts is a dynamic workflow. Name = filename stem; script = the file itself. */
137
+ function parseDynamicWorkflowFile(filePath: string, source: ResourceSource): WorkflowConfig | undefined {
138
+ try {
139
+ const basename = path.basename(filePath, ".dwf.ts");
140
+ return {
141
+ name: basename,
142
+ description: `Dynamic workflow script (${basename}.dwf.ts).`,
143
+ source,
144
+ filePath,
145
+ steps: [],
146
+ runtime: "dynamic",
147
+ dynamicScript: filePath,
148
+ };
149
+ } catch {
150
+ return undefined;
151
+ }
128
152
  }
129
153
 
130
154
  export function discoverWorkflows(cwd: string): WorkflowDiscoveryResult {
@@ -39,4 +39,17 @@ export interface WorkflowConfig {
39
39
  filePath: string;
40
40
  steps: WorkflowStep[];
41
41
  maxConcurrency?: number;
42
+ /** P2 dynamic-workflow discriminator. Default "static" (the .workflow.md step-list model).
43
+ * "dynamic" = the workflow is a JS/TS script (.dwf.ts) run via dynamic-workflow-runner.
44
+ * Backward-compatible: absent = "static". */
45
+ runtime?: "static" | "dynamic";
46
+ /** For runtime:"dynamic" — relative/absolute path to the .dwf.ts script. Unused for static. */
47
+ dynamicScript?: string;
48
+ }
49
+
50
+ /** A dynamic workflow (runtime === "dynamic"). steps is empty — the script is the source of truth. */
51
+ export interface DynamicWorkflowConfig extends WorkflowConfig {
52
+ runtime: "dynamic";
53
+ dynamicScript: string;
54
+ steps: [];
42
55
  }
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
4
5
  import type { TeamRunManifest } from "../state/types.ts";
5
6
  import { writeArtifact } from "../state/artifact-store.ts";
6
7
  import { projectCrewRoot } from "../utils/paths.ts";
@@ -18,7 +19,7 @@ export interface WorktreeCleanupResult {
18
19
 
19
20
  // SECURITY: PI_* and PI_CREW_* wildcards removed — they could match secret vars like PI_PASSWORD.
20
21
  // Git operations do not need PI_CREW_* execution-control vars.
21
- const GIT_SAFE_ENV = { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", "USERPROFILE", "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "C", LC_ALL: "C" };
22
+ const GIT_SAFE_ENV = { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "C", LC_ALL: "C" };
22
23
 
23
24
  function sanitizeBranchPart(value: string): string {
24
25
  return value.toLowerCase().replace(/[^a-z0-9._/-]+/g, "-").replace(/^-+|-+$/g, "") || "task";
@@ -1,6 +1,7 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
4
5
  import { loadConfig } from "../config/config.ts";
5
6
  import { projectCrewRoot } from "../utils/paths.ts";
6
7
  import { DEFAULT_PATHS } from "../config/defaults.ts";
@@ -27,7 +28,7 @@ export interface WorktreeDiffStat {
27
28
  function git(cwd: string, args: string[]): string {
28
29
  // SECURITY: PI_* and PI_CREW_* wildcards removed — they could match secret vars like PI_PASSWORD.
29
30
  // Git operations do not need PI_CREW_* execution-control vars.
30
- return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", "USERPROFILE", "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8" }, windowsHide: true }).trim();
31
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...sanitizeEnvSecrets(process.env, { allowList: ["PATH", "HOME", "USER", ...WINDOWS_ESSENTIAL_ENV_VARS, "SHELL", "TERM", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MESSAGES", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "NVM_BIN", "NVM_DIR", "NODE_PATH", "GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL"] }), LANG: "en_US.UTF-8", LC_ALL: "en_US.UTF-8" }, windowsHide: true }).trim();
31
32
  }
32
33
 
33
34
  // Dots are removed from branch names since they are used in path construction,
@@ -182,7 +183,7 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
182
183
  timeout: cfg.setupHookTimeoutMs ?? 30_000,
183
184
  shell: false, // cmd.exe /c handles batch files safely
184
185
  env: sanitizeEnvSecrets(process.env, {
185
- allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL"],
186
+ allowList: ["PATH", "HOME", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL"],
186
187
  }),
187
188
  windowsHide: true,
188
189
  })
@@ -193,7 +194,7 @@ function runSetupHook(manifest: TeamRunManifest, task: TeamTaskState, repoRoot:
193
194
  timeout: cfg.setupHookTimeoutMs ?? 30_000,
194
195
  shell: false,
195
196
  env: sanitizeEnvSecrets(process.env, {
196
- allowList: ["PATH", "HOME", "USERPROFILE", "TEMP", "TMP", "TMPDIR", "LANG", "LC_ALL"],
197
+ allowList: ["PATH", "HOME", ...WINDOWS_ESSENTIAL_ENV_VARS, "TMPDIR", "LANG", "LC_ALL"],
197
198
  }),
198
199
  windowsHide: true,
199
200
  });
@@ -4,7 +4,7 @@ description: Parallel research team for multi-project/source audits
4
4
  workspaceMode: single
5
5
  defaultWorkflow: parallel-research
6
6
  maxConcurrency: 4
7
- triggers: đọc sâu, deep read, deep research, source audit, multiple projects, parallel research, pi-*
7
+ triggers: deep reading, deep read, deep research, source audit, multiple projects, parallel research, pi-*
8
8
  category: research
9
9
  cost: cheap
10
10
  ---
@@ -0,0 +1,24 @@
1
+ /**
2
+ * hello.dwf.ts — Minimal reference dynamic-workflow script (P2).
3
+ *
4
+ * Usage: place under `.crew/workflows/hello.dwf.ts`, then
5
+ * team action='run' workflow='hello' goal='Greet the user warmly.'
6
+ *
7
+ * Demonstrates the WorkflowCtx surface: one ctx.agent() call + ctx.setResult().
8
+ * See 07-PLAN.md v3 §3.1 and 00-SPEC.md §3.2.
9
+ */
10
+ import type { WorkflowCtx } from "../src/runtime/dynamic-workflow-context.ts";
11
+
12
+ export default async function (ctx: WorkflowCtx): Promise<void> {
13
+ const greeting = await ctx.agent({
14
+ role: "executor",
15
+ prompt: `Compose a single-line warm greeting. Context: ${ctx.goal ?? "(no goal)"}`,
16
+ maxTurns: 2,
17
+ });
18
+ // Only ctx.setResult() reaches the main context.
19
+ ctx.setResult("results/greeting.txt", { ok: greeting.ok, model: "executor" });
20
+ // Note: in this trivial example the artifact path is illustrative; production scripts
21
+ // would use ctx.agent()'s returned artifactPath. Here we just surface the agent's text
22
+ // via the summary (runDynamicWorkflow reads the final artifact or falls back).
23
+ void greeting;
24
+ }