pi-crew 0.9.68 → 0.10.2
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.
- package/CHANGELOG.md +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
import { type Static, type TSchema, Type } from "@sinclair/typebox";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { PiTeamsAutonomyProfileSchema, PiTeamsConfigSchema } from "../schema/config-schema.ts";
|
|
4
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
5
|
+
import { suggestConfigKey } from "./suggestions.ts";
|
|
6
|
+
import type {
|
|
7
|
+
AgentOverrideConfig,
|
|
8
|
+
ConfigValidationResult,
|
|
9
|
+
CrewAgentsConfig,
|
|
10
|
+
CrewBrokerConfig,
|
|
11
|
+
CrewControlConfig,
|
|
12
|
+
CrewLimitsConfig,
|
|
13
|
+
CrewNestingConfig,
|
|
14
|
+
CrewNotificationsConfig,
|
|
15
|
+
CrewObservabilityConfig,
|
|
16
|
+
CrewOtlpConfig,
|
|
17
|
+
CrewPolicyConfig,
|
|
18
|
+
CrewReliabilityConfig,
|
|
19
|
+
CrewRetryPolicyConfig,
|
|
20
|
+
CrewRuntimeConfig,
|
|
21
|
+
CrewTelemetryConfig,
|
|
22
|
+
CrewToolsConfig,
|
|
23
|
+
CrewUiConfig,
|
|
24
|
+
CrewWorktreeConfig,
|
|
25
|
+
GoalWrapWorkflowConfig,
|
|
26
|
+
PiTeamsAutonomousConfig,
|
|
27
|
+
PiTeamsAutonomyProfile,
|
|
28
|
+
PiTeamsConfig,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
|
|
31
|
+
function errorPathFromValidation(error: unknown): string {
|
|
32
|
+
if (error && typeof error === "object") {
|
|
33
|
+
if (typeof (error as { path?: unknown }).path === "string") return (error as { path: string }).path;
|
|
34
|
+
if (typeof (error as { instancePath?: unknown }).instancePath === "string") return (error as { instancePath: string }).instancePath;
|
|
35
|
+
if (
|
|
36
|
+
typeof (error as { keyword?: unknown }).keyword === "string" &&
|
|
37
|
+
typeof (error as { schemaPath?: unknown }).schemaPath === "string"
|
|
38
|
+
)
|
|
39
|
+
return (error as { schemaPath: string }).schemaPath;
|
|
40
|
+
}
|
|
41
|
+
return "config";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Known top-level config keys from the schema — used for fuzzy suggestions. */
|
|
45
|
+
const KNOWN_TOP_LEVEL_KEYS = Object.keys(PiTeamsConfigSchema.properties ?? {}) as string[];
|
|
46
|
+
|
|
47
|
+
function validateConfigWithWarnings(raw: unknown): string[] {
|
|
48
|
+
if (!Value.Check(PiTeamsConfigSchema, raw)) {
|
|
49
|
+
return [...Value.Errors(PiTeamsConfigSchema, raw)].map((error) => {
|
|
50
|
+
const path = errorPathFromValidation(error);
|
|
51
|
+
const message = (error as { message?: unknown }).message ?? "invalid value";
|
|
52
|
+
// Enhance "additionalProperties" errors with fuzzy suggestions
|
|
53
|
+
if ((error as { keyword?: unknown }).keyword === "additionalProperties") {
|
|
54
|
+
const offendingKey = path.split("/").pop() ?? path;
|
|
55
|
+
const suggestion = suggestConfigKey(offendingKey, KNOWN_TOP_LEVEL_KEYS);
|
|
56
|
+
if (suggestion) return `${path}: ${message} (did you mean '${suggestion}'?)`;
|
|
57
|
+
}
|
|
58
|
+
return `${path}: ${message}`;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const LIMIT_CEILINGS = {
|
|
65
|
+
maxConcurrentWorkers: 1024,
|
|
66
|
+
maxTaskDepth: 100,
|
|
67
|
+
maxChildrenPerTask: 1000,
|
|
68
|
+
maxRunMinutes: 1440,
|
|
69
|
+
maxRetriesPerTask: 100,
|
|
70
|
+
maxTasksPerRun: 10_000,
|
|
71
|
+
heartbeatStaleMs: 24 * 60 * 60 * 1000,
|
|
72
|
+
runtimeMaxTurns: 10_000,
|
|
73
|
+
runtimeGraceTurns: 1_000,
|
|
74
|
+
// RT-NEW-1: taskTimeoutMs is in MILLISECONDS — it must NOT reuse runtimeMaxTurns
|
|
75
|
+
// (10_000 turns), which capped the effective timeout at 10s and silently disabled
|
|
76
|
+
// any larger value (e.g. 300_000 = 5min) via parsePositiveInteger returning undefined.
|
|
77
|
+
runtimeTaskTimeoutMs: 24 * 60 * 60 * 1000,
|
|
78
|
+
nestingMaxSlots: 64,
|
|
79
|
+
nestingMaxDepth: 10,
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keys that could allow prototype pollution if merged into plain objects.
|
|
84
|
+
* NOTE: This set is comprehensive for ES2023 and earlier. When upgrading JavaScript
|
|
85
|
+
* versions, verify whether new dangerous Object.prototype properties have been added
|
|
86
|
+
* that could enable prototype pollution attacks.
|
|
87
|
+
*/
|
|
88
|
+
export const DANGEROUS_OBJECT_KEYS = new Set([
|
|
89
|
+
"__proto__",
|
|
90
|
+
"constructor",
|
|
91
|
+
"prototype",
|
|
92
|
+
"hasOwnProperty",
|
|
93
|
+
"toString",
|
|
94
|
+
"valueOf",
|
|
95
|
+
"isPrototypeOf",
|
|
96
|
+
"propertyIsEnumerable",
|
|
97
|
+
"toLocaleString",
|
|
98
|
+
"__defineGetter__",
|
|
99
|
+
"__defineSetter__",
|
|
100
|
+
"__lookupGetter__",
|
|
101
|
+
"__lookupSetter__",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Strips dangerous Object.prototype keys from an object.
|
|
106
|
+
* Returns a new object built with Object.create(null) to prevent
|
|
107
|
+
* prototype pollution attacks.
|
|
108
|
+
*/
|
|
109
|
+
function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
110
|
+
const sanitized: Record<string, unknown> = Object.create(null);
|
|
111
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
112
|
+
// Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
|
|
113
|
+
const lowerKey = key.toLowerCase();
|
|
114
|
+
if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) continue;
|
|
115
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
116
|
+
sanitized[key] = sanitizeObject(value as Record<string, unknown>);
|
|
117
|
+
} else {
|
|
118
|
+
sanitized[key] = value;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return sanitized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
125
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
126
|
+
// Defensive: create a sanitized copy to prevent prototype pollution.
|
|
127
|
+
// Uses Object.create(null) so the result has no prototype chain.
|
|
128
|
+
// WARNING: The returned object has no prototype methods (no hasOwnProperty,
|
|
129
|
+
// toString, etc.). Use Object.hasOwn(obj, key) or
|
|
130
|
+
// Object.prototype.hasOwnProperty.call(obj, key) for property checks.
|
|
131
|
+
return sanitizeObject(value as Record<string, unknown>);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseWithSchema<T extends TSchema>(schema: T, value: unknown, context?: string): Static<T> | undefined {
|
|
135
|
+
if (!Value.Check(schema, value)) {
|
|
136
|
+
if (context) {
|
|
137
|
+
logInternalError("config.parseWithSchema", undefined, `${context}: schema validation failed`);
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
return Value.Decode(schema, value);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseIntegerInRange(value: unknown, minimum = 1, maximum = Number.MAX_SAFE_INTEGER): number | undefined {
|
|
145
|
+
return parseWithSchema(Type.Integer({ minimum, maximum }), value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
|
|
149
|
+
return parseIntegerInRange(value, 1, max);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
|
|
153
|
+
return parseWithSchema(PiTeamsAutonomyProfileSchema, value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseStringList(value: unknown): string[] | undefined {
|
|
157
|
+
const items = parseWithSchema(Type.Array(Type.String()), value);
|
|
158
|
+
if (!items || items.length === 0) return undefined;
|
|
159
|
+
const normalized = items.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
160
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
164
|
+
if (value === false) return false;
|
|
165
|
+
if (typeof value === "string") return value.trim() === "" ? [] : parseStringList(value.split(","));
|
|
166
|
+
return parseStringList(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function effectiveAutonomousConfig(
|
|
170
|
+
config: PiTeamsAutonomousConfig | undefined,
|
|
171
|
+
): Required<Pick<PiTeamsAutonomousConfig, "profile" | "enabled" | "injectPolicy" | "preferAsyncForLongTasks" | "allowWorktreeSuggestion">> &
|
|
172
|
+
Pick<PiTeamsAutonomousConfig, "magicKeywords"> {
|
|
173
|
+
const profile = config?.enabled === false ? "manual" : (config?.profile ?? "suggested");
|
|
174
|
+
const profileDefaults: Record<
|
|
175
|
+
PiTeamsAutonomyProfile,
|
|
176
|
+
{
|
|
177
|
+
enabled: boolean;
|
|
178
|
+
injectPolicy: boolean;
|
|
179
|
+
preferAsyncForLongTasks: boolean;
|
|
180
|
+
allowWorktreeSuggestion: boolean;
|
|
181
|
+
}
|
|
182
|
+
> = {
|
|
183
|
+
manual: {
|
|
184
|
+
enabled: false,
|
|
185
|
+
injectPolicy: false,
|
|
186
|
+
preferAsyncForLongTasks: false,
|
|
187
|
+
allowWorktreeSuggestion: false,
|
|
188
|
+
},
|
|
189
|
+
suggested: {
|
|
190
|
+
enabled: true,
|
|
191
|
+
injectPolicy: true,
|
|
192
|
+
preferAsyncForLongTasks: false,
|
|
193
|
+
allowWorktreeSuggestion: true,
|
|
194
|
+
},
|
|
195
|
+
assisted: {
|
|
196
|
+
enabled: true,
|
|
197
|
+
injectPolicy: true,
|
|
198
|
+
preferAsyncForLongTasks: true,
|
|
199
|
+
allowWorktreeSuggestion: true,
|
|
200
|
+
},
|
|
201
|
+
aggressive: {
|
|
202
|
+
enabled: true,
|
|
203
|
+
injectPolicy: true,
|
|
204
|
+
preferAsyncForLongTasks: true,
|
|
205
|
+
allowWorktreeSuggestion: true,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const defaults = profileDefaults[profile];
|
|
209
|
+
return {
|
|
210
|
+
profile,
|
|
211
|
+
enabled: config?.enabled ?? defaults.enabled,
|
|
212
|
+
injectPolicy: config?.injectPolicy ?? defaults.injectPolicy,
|
|
213
|
+
preferAsyncForLongTasks: config?.preferAsyncForLongTasks ?? defaults.preferAsyncForLongTasks,
|
|
214
|
+
allowWorktreeSuggestion: config?.allowWorktreeSuggestion ?? defaults.allowWorktreeSuggestion,
|
|
215
|
+
magicKeywords: config?.magicKeywords,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function parseStringArrayRecord(value: unknown): Record<string, string[]> | undefined {
|
|
220
|
+
const record = parseWithSchema(Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String())), value);
|
|
221
|
+
if (!record) return undefined;
|
|
222
|
+
const result: Record<string, string[]> = {};
|
|
223
|
+
for (const [key, rawValues] of Object.entries(record)) {
|
|
224
|
+
const parsed = parseStringList(rawValues);
|
|
225
|
+
if (parsed && parsed.length > 0) result[key] = parsed;
|
|
226
|
+
}
|
|
227
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefined {
|
|
231
|
+
const obj = asRecord(value);
|
|
232
|
+
if (!obj) return undefined;
|
|
233
|
+
const config: PiTeamsAutonomousConfig = {
|
|
234
|
+
profile: parseProfile(obj.profile),
|
|
235
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
236
|
+
injectPolicy: parseWithSchema(Type.Boolean(), obj.injectPolicy),
|
|
237
|
+
preferAsyncForLongTasks: parseWithSchema(Type.Boolean(), obj.preferAsyncForLongTasks),
|
|
238
|
+
allowWorktreeSuggestion: parseWithSchema(Type.Boolean(), obj.allowWorktreeSuggestion),
|
|
239
|
+
magicKeywords: parseStringArrayRecord(obj.magicKeywords),
|
|
240
|
+
};
|
|
241
|
+
return Object.values(config).some((entry) => entry !== undefined) ? config : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
|
|
245
|
+
const obj = asRecord(value);
|
|
246
|
+
if (!obj) return undefined;
|
|
247
|
+
const limits: CrewLimitsConfig = {
|
|
248
|
+
maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
|
|
249
|
+
allowUnboundedConcurrency: parseWithSchema(Type.Boolean(), obj.allowUnboundedConcurrency),
|
|
250
|
+
maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
|
|
251
|
+
maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
|
|
252
|
+
maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
|
|
253
|
+
maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
|
|
254
|
+
maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
|
|
255
|
+
heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
|
|
256
|
+
serializeOnPathOverlap: parseWithSchema(Type.Boolean(), obj.serializeOnPathOverlap),
|
|
257
|
+
};
|
|
258
|
+
return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parseNestingConfig(value: unknown): CrewNestingConfig | undefined {
|
|
262
|
+
const obj = asRecord(value);
|
|
263
|
+
if (!obj) return undefined;
|
|
264
|
+
const nesting: CrewNestingConfig = {
|
|
265
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
266
|
+
maxSlots: parsePositiveInteger(obj.maxSlots, LIMIT_CEILINGS.nestingMaxSlots),
|
|
267
|
+
maxDepth: parsePositiveInteger(obj.maxDepth, LIMIT_CEILINGS.nestingMaxDepth),
|
|
268
|
+
};
|
|
269
|
+
return Object.values(nesting).some((entry) => entry !== undefined) ? nesting : undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolicy"] | undefined {
|
|
273
|
+
const obj = asRecord(value);
|
|
274
|
+
if (!obj) return undefined;
|
|
275
|
+
const isolatedRoles = parseStringList(obj.isolatedRoles);
|
|
276
|
+
const defaultRuntime = parseWithSchema(Type.Union([Type.Literal("live-session"), Type.Literal("child-process")]), obj.defaultRuntime);
|
|
277
|
+
if (isolatedRoles === undefined && defaultRuntime === undefined) return undefined;
|
|
278
|
+
return {
|
|
279
|
+
...(isolatedRoles !== undefined ? { isolatedRoles } : {}),
|
|
280
|
+
...(defaultRuntime !== undefined ? { defaultRuntime } : {}),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* F19-1 (Round 19 parity): runtime.modelFallback was declared in types.ts and the
|
|
286
|
+
* schema (PiTeamsModelFallbackConfigSchema) but parseRuntimeConfig never emitted
|
|
287
|
+
* it, so user config was silently dropped. Mirrors the schema field-for-field.
|
|
288
|
+
*/
|
|
289
|
+
function parseModelFallbackConfig(value: unknown): CrewRuntimeConfig["modelFallback"] {
|
|
290
|
+
const obj = asRecord(value);
|
|
291
|
+
if (!obj) return undefined;
|
|
292
|
+
const modelFallback: NonNullable<CrewRuntimeConfig["modelFallback"]> = {
|
|
293
|
+
maxAutoFallbacks: parseWithSchema(Type.Integer({ minimum: 0 }), obj.maxAutoFallbacks),
|
|
294
|
+
order: parseWithSchema(Type.Union([Type.Literal("parentFirst"), Type.Literal("asIs")]), obj.order),
|
|
295
|
+
requireCredentials: parseWithSchema(Type.Boolean(), obj.requireCredentials),
|
|
296
|
+
quotaAwareOrdering: parseWithSchema(Type.Boolean(), obj.quotaAwareOrdering),
|
|
297
|
+
defaultSubagentModel: parseWithSchema(Type.String({ minLength: 1 }), obj.defaultSubagentModel),
|
|
298
|
+
};
|
|
299
|
+
return Object.values(modelFallback).some((entry) => entry !== undefined) ? modelFallback : undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
303
|
+
const obj = asRecord(value);
|
|
304
|
+
if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
|
|
305
|
+
const runtime: CrewRuntimeConfig = {
|
|
306
|
+
mode: parseWithSchema(
|
|
307
|
+
Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
|
|
308
|
+
obj.mode,
|
|
309
|
+
),
|
|
310
|
+
preferLiveSession: parseWithSchema(Type.Boolean(), obj.preferLiveSession),
|
|
311
|
+
allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
|
|
312
|
+
maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
|
|
313
|
+
graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
|
|
314
|
+
taskTimeoutMs: parsePositiveInteger(obj.taskTimeoutMs, LIMIT_CEILINGS.runtimeTaskTimeoutMs),
|
|
315
|
+
inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext) ?? true,
|
|
316
|
+
promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
|
|
317
|
+
groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
|
|
318
|
+
groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
|
|
319
|
+
requirePlanApproval: parseWithSchema(Type.Boolean(), obj.requirePlanApproval),
|
|
320
|
+
completionMutationGuard: parseWithSchema(
|
|
321
|
+
Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("fail")]),
|
|
322
|
+
obj.completionMutationGuard,
|
|
323
|
+
),
|
|
324
|
+
effectivenessGuard: parseWithSchema(
|
|
325
|
+
Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
|
|
326
|
+
obj.effectivenessGuard,
|
|
327
|
+
),
|
|
328
|
+
yield: (() => {
|
|
329
|
+
const y = asRecord(obj.yield);
|
|
330
|
+
if (!y) return undefined;
|
|
331
|
+
const parsed: NonNullable<CrewRuntimeConfig["yield"]> = {
|
|
332
|
+
enabled: parseWithSchema(Type.Boolean(), y.enabled),
|
|
333
|
+
maxReminders: parseWithSchema(Type.Integer({ minimum: 0 }), y.maxReminders),
|
|
334
|
+
reminderPrompt: parseWithSchema(Type.String({ maxLength: 1000 }), y.reminderPrompt),
|
|
335
|
+
};
|
|
336
|
+
return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
|
|
337
|
+
})(),
|
|
338
|
+
excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
|
|
339
|
+
agentExtensions: parseStringList(obj.agentExtensions),
|
|
340
|
+
isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
|
|
341
|
+
modelFallback: parseModelFallbackConfig(obj.modelFallback),
|
|
342
|
+
};
|
|
343
|
+
return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseControlConfig(value: unknown): CrewControlConfig | undefined {
|
|
347
|
+
const obj = asRecord(value);
|
|
348
|
+
if (!obj) return undefined;
|
|
349
|
+
const control: CrewControlConfig = {
|
|
350
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
351
|
+
needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
|
|
352
|
+
// F19-3 (Round 19 parity): read at agent-control.ts with defaults 3/10 —
|
|
353
|
+
// emit undefined when unset so the read-site defaults stay authoritative.
|
|
354
|
+
consecutiveFailureThreshold: parsePositiveInteger(obj.consecutiveFailureThreshold),
|
|
355
|
+
longRunningMinutes: parsePositiveInteger(obj.longRunningMinutes),
|
|
356
|
+
};
|
|
357
|
+
return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Phase 0 broker parser. Returns `undefined` only when input is not an object;
|
|
362
|
+
* otherwise returns the broker config (with only defined fields populated)
|
|
363
|
+
* so the caller can layer defaults on top.
|
|
364
|
+
*/
|
|
365
|
+
function parseBrokerConfig(value: unknown): CrewBrokerConfig | undefined {
|
|
366
|
+
const obj = asRecord(value);
|
|
367
|
+
if (!obj) return undefined;
|
|
368
|
+
// Use the exact schema bounds (4..32 / 1024..1048576 / 32..4096). The
|
|
369
|
+
// previous version used parsePositiveInteger(value, default) which clamps
|
|
370
|
+
// the UPPER bound to the default — effectively pathHashLen was capped
|
|
371
|
+
// at 8, much narrower than the schema advertises.
|
|
372
|
+
const broker: CrewBrokerConfig = {
|
|
373
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
374
|
+
pathHashLen: parseIntegerInRange(obj.pathHashLen, 4, 32),
|
|
375
|
+
maxFrameBytes: parseIntegerInRange(obj.maxFrameBytes, 1024, 1_048_576),
|
|
376
|
+
outboundQueueCap: parseIntegerInRange(obj.outboundQueueCap, 32, 4096),
|
|
377
|
+
// WP-2/R2 fix (B1 battery 2026-08-18): the hand-rolled whitelist here
|
|
378
|
+
// missed this field — config-schema.ts + DEFAULT_BROKER + the CrewBroker
|
|
379
|
+
// ctor all honored it, but parseConfig silently DROPPED it from every
|
|
380
|
+
// config file, so `broker.waitMethodsEnabled: true` in a workspace
|
|
381
|
+
// config never reached the broker (live ask rejected policy-disabled).
|
|
382
|
+
waitMethodsEnabled: parseWithSchema(Type.Boolean(), obj.waitMethodsEnabled),
|
|
383
|
+
};
|
|
384
|
+
return Object.values(broker).some((entry) => entry !== undefined) ? broker : undefined;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
|
|
388
|
+
const obj = asRecord(value);
|
|
389
|
+
if (!obj) return undefined;
|
|
390
|
+
const rawSetupHook = parseWithSchema(Type.String(), obj.setupHook);
|
|
391
|
+
const setupHook = rawSetupHook?.trim();
|
|
392
|
+
const worktree: CrewWorktreeConfig = {
|
|
393
|
+
setupHook: setupHook ? setupHook : undefined,
|
|
394
|
+
setupHookTimeoutMs: parsePositiveInteger(obj.setupHookTimeoutMs, 300_000),
|
|
395
|
+
linkNodeModules: parseWithSchema(Type.Boolean(), obj.linkNodeModules),
|
|
396
|
+
// C6: seedPaths was declared in the type + schema but never parsed here, so
|
|
397
|
+
// loadedConfig.config.worktree?.seedPaths was always undefined -> the global
|
|
398
|
+
// worktree seed overlay (worktree-manager.ts) silently never applied.
|
|
399
|
+
seedPaths: parseStringList(obj.seedPaths),
|
|
400
|
+
};
|
|
401
|
+
return Object.values(worktree).some((entry) => entry !== undefined) ? worktree : undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Parse goalWrap config (RFC v0.5 vision: apply goal completion-guarantee to builtins). */
|
|
405
|
+
function parseGoalWrapConfig(value: unknown): Record<string, GoalWrapWorkflowConfig> | undefined {
|
|
406
|
+
const obj = asRecord(value);
|
|
407
|
+
if (!obj) return undefined;
|
|
408
|
+
const result: Record<string, GoalWrapWorkflowConfig> = {};
|
|
409
|
+
let hasAny = false;
|
|
410
|
+
for (const [workflowName, entry] of Object.entries(obj)) {
|
|
411
|
+
const entryObj = asRecord(entry);
|
|
412
|
+
if (!entryObj) continue;
|
|
413
|
+
const parsed: GoalWrapWorkflowConfig = {
|
|
414
|
+
enabled: parseWithSchema(Type.Boolean(), entryObj.enabled),
|
|
415
|
+
maxTurns: parseWithSchema(Type.Integer({ minimum: 1, maximum: 50 }), entryObj.maxTurns),
|
|
416
|
+
evaluatorModel: parseWithSchema(Type.String({ minLength: 1 }), entryObj.evaluatorModel),
|
|
417
|
+
budgetTotal: parseWithSchema(Type.Integer({ minimum: 1000 }), entryObj.budgetTotal),
|
|
418
|
+
budgetUnlimited: parseWithSchema(Type.Boolean(), entryObj.budgetUnlimited),
|
|
419
|
+
};
|
|
420
|
+
// Parse verification sub-object.
|
|
421
|
+
const verObj = asRecord(entryObj.verification);
|
|
422
|
+
if (verObj) {
|
|
423
|
+
const commands = Array.isArray(verObj.commands)
|
|
424
|
+
? verObj.commands.filter((c): c is string => typeof c === "string" && c.length > 0)
|
|
425
|
+
: undefined;
|
|
426
|
+
const mode = verObj.mode === "text-only" ? ("text-only" as const) : undefined;
|
|
427
|
+
if (commands || mode) {
|
|
428
|
+
parsed.verification = {
|
|
429
|
+
...(commands ? { commands } : { commands: [] }),
|
|
430
|
+
...(mode ? { mode } : {}),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (Object.values(parsed).some((v) => v !== undefined)) {
|
|
435
|
+
result[workflowName] = parsed;
|
|
436
|
+
hasAny = true;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return hasAny ? result : undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
443
|
+
const obj = asRecord(value);
|
|
444
|
+
if (!obj) return undefined;
|
|
445
|
+
const override: AgentOverrideConfig = {
|
|
446
|
+
disabled: parseWithSchema(Type.Boolean(), obj.disabled),
|
|
447
|
+
model: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.model),
|
|
448
|
+
fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
|
|
449
|
+
thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
|
|
450
|
+
tools: parseStringArrayOrFalse(obj.tools),
|
|
451
|
+
skills: parseStringArrayOrFalse(obj.skills),
|
|
452
|
+
};
|
|
453
|
+
return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
457
|
+
const obj = asRecord(value);
|
|
458
|
+
if (!obj) return undefined;
|
|
459
|
+
const rawWidgetPlacement = parseWithSchema(
|
|
460
|
+
Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor"), Type.Literal("bottom")]),
|
|
461
|
+
obj.widgetPlacement,
|
|
462
|
+
);
|
|
463
|
+
const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
|
|
464
|
+
const rawRowStyle = parseWithSchema(Type.Union([Type.Literal("compact"), Type.Literal("detailed")]), obj.widgetRowStyle);
|
|
465
|
+
const ui: CrewUiConfig = {
|
|
466
|
+
widgetPlacement: rawWidgetPlacement,
|
|
467
|
+
widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
|
|
468
|
+
widgetRowStyle: rawRowStyle,
|
|
469
|
+
inlinePanel: parseWithSchema(Type.Boolean(), obj.inlinePanel),
|
|
470
|
+
powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
|
|
471
|
+
dashboardPlacement: rawDashboardPlacement,
|
|
472
|
+
dashboardWidth: parseIntegerInRange(obj.dashboardWidth, 32, 120),
|
|
473
|
+
dashboardLiveRefreshMs: parseIntegerInRange(obj.dashboardLiveRefreshMs, 250, 60_000),
|
|
474
|
+
autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
|
|
475
|
+
autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
|
|
476
|
+
autoCloseDashboardMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoCloseDashboardMs),
|
|
477
|
+
showModel: parseWithSchema(Type.Boolean(), obj.showModel),
|
|
478
|
+
showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
|
|
479
|
+
showTools: parseWithSchema(Type.Boolean(), obj.showTools),
|
|
480
|
+
transcriptTailBytes: parseIntegerInRange(obj.transcriptTailBytes, 1024, 50 * 1024 * 1024),
|
|
481
|
+
mascotStyle: parseWithSchema(Type.Union([Type.Literal("cat"), Type.Literal("armin")]), obj.mascotStyle),
|
|
482
|
+
mascotEffect: parseWithSchema(
|
|
483
|
+
Type.Union([
|
|
484
|
+
Type.Literal("random"),
|
|
485
|
+
Type.Literal("none"),
|
|
486
|
+
Type.Literal("typewriter"),
|
|
487
|
+
Type.Literal("scanline"),
|
|
488
|
+
Type.Literal("rain"),
|
|
489
|
+
Type.Literal("fade"),
|
|
490
|
+
Type.Literal("crt"),
|
|
491
|
+
Type.Literal("glitch"),
|
|
492
|
+
Type.Literal("dissolve"),
|
|
493
|
+
]),
|
|
494
|
+
obj.mascotEffect,
|
|
495
|
+
),
|
|
496
|
+
};
|
|
497
|
+
return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
|
|
501
|
+
const obj = asRecord(value);
|
|
502
|
+
if (!obj) return undefined;
|
|
503
|
+
const overrides: Record<string, AgentOverrideConfig> = {};
|
|
504
|
+
if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
|
|
505
|
+
for (const [name, rawOverride] of Object.entries(obj.overrides as Record<string, unknown>)) {
|
|
506
|
+
const parsed = parseAgentOverride(rawOverride);
|
|
507
|
+
if (parsed && name.trim()) overrides[name.trim()] = parsed;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const agents: CrewAgentsConfig = {
|
|
511
|
+
disableBuiltins: parseWithSchema(Type.Boolean(), obj.disableBuiltins),
|
|
512
|
+
overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
|
|
513
|
+
};
|
|
514
|
+
return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function parseToolsConfig(value: unknown): CrewToolsConfig | undefined {
|
|
518
|
+
const obj = asRecord(value);
|
|
519
|
+
if (!obj) return undefined;
|
|
520
|
+
const tools: CrewToolsConfig = {
|
|
521
|
+
enableClaudeStyleAliases: parseWithSchema(Type.Boolean(), obj.enableClaudeStyleAliases),
|
|
522
|
+
enableSteer: parseWithSchema(Type.Boolean(), obj.enableSteer),
|
|
523
|
+
terminateOnForeground: parseWithSchema(Type.Boolean(), obj.terminateOnForeground),
|
|
524
|
+
};
|
|
525
|
+
return Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseTelemetryConfig(value: unknown): CrewTelemetryConfig | undefined {
|
|
529
|
+
const obj = asRecord(value);
|
|
530
|
+
if (!obj) return undefined;
|
|
531
|
+
const telemetry: CrewTelemetryConfig = {
|
|
532
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
533
|
+
};
|
|
534
|
+
return Object.values(telemetry).some((entry) => entry !== undefined) ? telemetry : undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parsePolicyConfig(value: unknown): CrewPolicyConfig | undefined {
|
|
538
|
+
const obj = asRecord(value);
|
|
539
|
+
if (!obj) return undefined;
|
|
540
|
+
const policy: CrewPolicyConfig = {
|
|
541
|
+
requireIntentForDestructiveActions: parseWithSchema(Type.Boolean(), obj.requireIntentForDestructiveActions),
|
|
542
|
+
disabledCapabilities: parseWithSchema(Type.Array(Type.String()), obj.disabledCapabilities),
|
|
543
|
+
};
|
|
544
|
+
return Object.values(policy).some((entry) => entry !== undefined) ? policy : undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function parseNotificationsConfig(value: unknown): CrewNotificationsConfig | undefined {
|
|
548
|
+
const obj = asRecord(value);
|
|
549
|
+
if (!obj) return undefined;
|
|
550
|
+
const notifications: CrewNotificationsConfig = {
|
|
551
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
552
|
+
severityFilter: parseWithSchema(
|
|
553
|
+
Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")])),
|
|
554
|
+
obj.severityFilter,
|
|
555
|
+
),
|
|
556
|
+
// F19-5 (Wave 1A): minimum 1000 aligns with PiTeamsNotificationsConfigSchema
|
|
557
|
+
// (config-schema.ts:208); values <1000 now parse to undefined like other
|
|
558
|
+
// bound violations (schema has no upper bound, so the old parser-only 24h
|
|
559
|
+
// ceiling is gone — schema wins).
|
|
560
|
+
dedupWindowMs: parseWithSchema(Type.Integer({ minimum: 1000 }), obj.dedupWindowMs),
|
|
561
|
+
batchWindowMs: parseWithSchema(Type.Integer({ minimum: 0, maximum: 60_000 }), obj.batchWindowMs),
|
|
562
|
+
quietHours: parseWithSchema(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" }), obj.quietHours),
|
|
563
|
+
sinkRetentionDays: parsePositiveInteger(obj.sinkRetentionDays, 90),
|
|
564
|
+
};
|
|
565
|
+
return Object.values(notifications).some((entry) => entry !== undefined) ? notifications : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function parseObservabilityConfig(value: unknown): CrewObservabilityConfig | undefined {
|
|
569
|
+
const obj = asRecord(value);
|
|
570
|
+
if (!obj) return undefined;
|
|
571
|
+
const observability: CrewObservabilityConfig = {
|
|
572
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
573
|
+
pollIntervalMs: parseWithSchema(Type.Integer({ minimum: 1000, maximum: 60_000 }), obj.pollIntervalMs),
|
|
574
|
+
// F19-5 (Wave 1A): 365 -> 90 aligns the parser ceiling with
|
|
575
|
+
// PiTeamsObservabilityConfigSchema max 90 (config-schema.ts:220).
|
|
576
|
+
metricRetentionDays: parsePositiveInteger(obj.metricRetentionDays, 90),
|
|
577
|
+
};
|
|
578
|
+
return Object.values(observability).some((entry) => entry !== undefined) ? observability : undefined;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefined {
|
|
582
|
+
const obj = asRecord(value);
|
|
583
|
+
if (!obj) return undefined;
|
|
584
|
+
const retryObj = asRecord(obj.retryPolicy);
|
|
585
|
+
const retryPolicy: CrewRetryPolicyConfig | undefined = retryObj
|
|
586
|
+
? {
|
|
587
|
+
maxAttempts: parsePositiveInteger(retryObj.maxAttempts, 10),
|
|
588
|
+
backoffMs: parseWithSchema(Type.Integer({ minimum: 100, maximum: 60_000 }), retryObj.backoffMs),
|
|
589
|
+
jitterRatio: parseWithSchema(Type.Number({ minimum: 0, maximum: 1 }), retryObj.jitterRatio),
|
|
590
|
+
exponentialFactor: parseWithSchema(Type.Number({ minimum: 1, maximum: 5 }), retryObj.exponentialFactor),
|
|
591
|
+
retryableErrors: parseStringList(retryObj.retryableErrors),
|
|
592
|
+
maxTotalSpawns: parsePositiveInteger(retryObj.maxTotalSpawns),
|
|
593
|
+
}
|
|
594
|
+
: undefined;
|
|
595
|
+
const reliability: CrewReliabilityConfig = {
|
|
596
|
+
autoRetry: parseWithSchema(Type.Boolean(), obj.autoRetry),
|
|
597
|
+
retryPolicy: retryPolicy && Object.values(retryPolicy).some((entry) => entry !== undefined) ? retryPolicy : undefined,
|
|
598
|
+
autoRecover: parseWithSchema(Type.Boolean(), obj.autoRecover),
|
|
599
|
+
deadletterThreshold: parsePositiveInteger(obj.deadletterThreshold),
|
|
600
|
+
cleanupOrphanedTempDirs: parseWithSchema(Type.Boolean(), obj.cleanupOrphanedTempDirs),
|
|
601
|
+
autoRepairIntervalMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoRepairIntervalMs),
|
|
602
|
+
forcePreflight: parseWithSchema(Type.Boolean(), obj.forcePreflight),
|
|
603
|
+
ambientStatusInjection: parseWithSchema(Type.Boolean(), obj.ambientStatusInjection),
|
|
604
|
+
perWriteValidation: parseWithSchema(Type.Boolean(), obj.perWriteValidation),
|
|
605
|
+
scopeModels: parseWithSchema(Type.Boolean(), obj.scopeModels),
|
|
606
|
+
};
|
|
607
|
+
return Object.values(reliability).some((entry) => entry !== undefined) ? reliability : undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
|
|
611
|
+
const obj = asRecord(value);
|
|
612
|
+
if (!obj) return undefined;
|
|
613
|
+
const headers: Record<string, string> = Object.create(null);
|
|
614
|
+
const rawHeaders = asRecord(obj.headers);
|
|
615
|
+
if (rawHeaders)
|
|
616
|
+
for (const [key, entry] of Object.entries(rawHeaders)) {
|
|
617
|
+
if (typeof entry !== "string") continue;
|
|
618
|
+
// Prevent prototype pollution via dangerous Object.prototype keys.
|
|
619
|
+
// Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
|
|
620
|
+
const lowerKey = key.toLowerCase();
|
|
621
|
+
if (
|
|
622
|
+
lowerKey === "__proto__" ||
|
|
623
|
+
lowerKey === "constructor" ||
|
|
624
|
+
lowerKey === "prototype" ||
|
|
625
|
+
lowerKey === "hasownproperty" ||
|
|
626
|
+
lowerKey === "tostring" ||
|
|
627
|
+
lowerKey === "valueof" ||
|
|
628
|
+
lowerKey === "isprototypeof" ||
|
|
629
|
+
lowerKey === "propertyisenumerable" ||
|
|
630
|
+
lowerKey === "tolocalestring" ||
|
|
631
|
+
lowerKey === "__definegetter__" ||
|
|
632
|
+
lowerKey === "__definesetter__" ||
|
|
633
|
+
lowerKey === "__lookupgetter__" ||
|
|
634
|
+
lowerKey === "__lookupsetter__"
|
|
635
|
+
)
|
|
636
|
+
continue;
|
|
637
|
+
// Validate key format: must start with letter, then alphanumeric/hyphen/underscore.
|
|
638
|
+
// Blocks CRLF, NUL, spaces, shell metacharacters in header keys.
|
|
639
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(key)) continue;
|
|
640
|
+
headers[key] = entry;
|
|
641
|
+
}
|
|
642
|
+
const otlp: CrewOtlpConfig = {
|
|
643
|
+
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
644
|
+
// F19-5 (Wave 1A): ^https?:// pattern matches PiTeamsOtlpConfigSchema
|
|
645
|
+
// (config-schema.ts:258-265); non-http(s) endpoints parse to undefined.
|
|
646
|
+
endpoint: parseWithSchema(Type.String({ minLength: 1, pattern: "^https?://" }), obj.endpoint),
|
|
647
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
648
|
+
intervalMs: parseWithSchema(Type.Integer({ minimum: 5000 }), obj.intervalMs),
|
|
649
|
+
};
|
|
650
|
+
return Object.values(otlp).some((entry) => entry !== undefined) ? otlp : undefined;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function parseConfig(raw: unknown): PiTeamsConfig {
|
|
654
|
+
const obj = asRecord(raw);
|
|
655
|
+
if (!obj) return {};
|
|
656
|
+
return {
|
|
657
|
+
asyncByDefault: parseWithSchema(Type.Boolean(), obj.asyncByDefault),
|
|
658
|
+
executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
|
|
659
|
+
notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
|
|
660
|
+
requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
|
|
661
|
+
ignoreMethod: parseWithSchema(Type.Union([Type.Literal("gitignore"), Type.Literal("exclude")]), obj.ignoreMethod),
|
|
662
|
+
autonomous: parseAutonomousConfig(obj.autonomous),
|
|
663
|
+
limits: parseLimitsConfig(obj.limits),
|
|
664
|
+
nesting: parseNestingConfig(obj.nesting),
|
|
665
|
+
runtime: parseRuntimeConfig(obj.runtime),
|
|
666
|
+
control: parseControlConfig(obj.control),
|
|
667
|
+
worktree: parseWorktreeConfig(obj.worktree),
|
|
668
|
+
goalWrap: parseGoalWrapConfig(obj.goalWrap),
|
|
669
|
+
agents: parseAgentsConfig(obj.agents),
|
|
670
|
+
tools: parseToolsConfig(obj.tools),
|
|
671
|
+
telemetry: parseTelemetryConfig(obj.telemetry),
|
|
672
|
+
policy: parsePolicyConfig(obj.policy),
|
|
673
|
+
notifications: parseNotificationsConfig(obj.notifications),
|
|
674
|
+
observability: parseObservabilityConfig(obj.observability),
|
|
675
|
+
reliability: parseReliabilityConfig(obj.reliability),
|
|
676
|
+
otlp: parseOtlpConfig(obj.otlp),
|
|
677
|
+
ui: parseUiConfig(obj.ui),
|
|
678
|
+
broker: parseBrokerConfig(obj.broker),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function parseConfigWithWarnings(raw: unknown): ConfigValidationResult {
|
|
683
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: {}, warnings: [] };
|
|
684
|
+
const parsed = parseConfig(raw);
|
|
685
|
+
const warnings = validateConfigWithWarnings(raw as Record<string, unknown>);
|
|
686
|
+
return { config: parsed, warnings };
|
|
687
|
+
}
|