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
package/src/config/role-tools.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
22
22
|
// excluded. State-mutation safety is enforced separately by
|
|
23
23
|
// READ_ONLY_ROLES in role-permission.ts.
|
|
24
24
|
explorer: {
|
|
25
|
-
tools: ["read", "grep", "find", "ls", "glob", "bash"],
|
|
25
|
+
tools: ["read", "grep", "find", "ls", "glob", "bash", "ask"],
|
|
26
26
|
excludeTools: ["edit", "write", "web"],
|
|
27
27
|
},
|
|
28
28
|
|
|
@@ -39,14 +39,14 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
39
39
|
// planner to WRITE_ROLES would fire the gate before planning, breaking the
|
|
40
40
|
// default/implementation workflows.
|
|
41
41
|
planner: {
|
|
42
|
-
tools: ["read", "grep", "find", "ls", "glob"],
|
|
42
|
+
tools: ["read", "grep", "find", "ls", "glob", "ask"],
|
|
43
43
|
excludeTools: ["edit", "write", "bash", "web", "ask_question"],
|
|
44
44
|
},
|
|
45
45
|
|
|
46
46
|
// Critic - Read-only plan/design critique (F2: was missing from the map,
|
|
47
47
|
// so a custom critic agent had no tool-level read-only enforcement).
|
|
48
48
|
critic: {
|
|
49
|
-
tools: ["read", "grep", "find", "ls", "glob"],
|
|
49
|
+
tools: ["read", "grep", "find", "ls", "glob", "ask"],
|
|
50
50
|
excludeTools: ["edit", "write", "bash", "web"],
|
|
51
51
|
},
|
|
52
52
|
|
|
@@ -59,13 +59,13 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
59
59
|
|
|
60
60
|
// Reviewer - Read and review, no write
|
|
61
61
|
reviewer: {
|
|
62
|
-
tools: ["read", "grep", "find", "ls", "glob", "bash"],
|
|
62
|
+
tools: ["read", "grep", "find", "ls", "glob", "bash", "ask"],
|
|
63
63
|
excludeTools: ["edit", "write"],
|
|
64
64
|
},
|
|
65
65
|
|
|
66
66
|
// Writer - Documentation focused
|
|
67
67
|
writer: {
|
|
68
|
-
tools: ["read", "edit", "write", "ls"],
|
|
68
|
+
tools: ["read", "edit", "write", "ls", "ask"],
|
|
69
69
|
excludeTools: ["bash", "web", "ask_question"],
|
|
70
70
|
},
|
|
71
71
|
|
|
@@ -74,7 +74,7 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
74
74
|
// security-reviewer.md → "security-reviewer"). The underscore form never
|
|
75
75
|
// resolved at runtime (returned {}), silently dropping enforcement.
|
|
76
76
|
"security-reviewer": {
|
|
77
|
-
tools: ["read", "grep", "find"],
|
|
77
|
+
tools: ["read", "grep", "find", "ask"],
|
|
78
78
|
excludeTools: ["edit", "write", "bash", "web", "ask_question"],
|
|
79
79
|
},
|
|
80
80
|
|
|
@@ -84,15 +84,17 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
84
84
|
// agents/verifier.md). Tool-set keeps bash but excludes edit/write so source
|
|
85
85
|
// integrity is preserved during verification. Mirrors cold-verifier behavior.
|
|
86
86
|
verifier: {
|
|
87
|
-
tools: ["read", "grep", "find", "ls", "bash", "scratchpad"],
|
|
87
|
+
tools: ["read", "grep", "find", "ls", "bash", "scratchpad", "ask"],
|
|
88
88
|
excludeTools: ["edit", "write", "web"],
|
|
89
89
|
// Phase 1 scratchpad: multi-cell test/verify flows reuse parsed state.
|
|
90
90
|
scratchpad: true,
|
|
91
91
|
},
|
|
92
92
|
|
|
93
|
-
// Test Engineer - Can write tests (F1: hyphenated key)
|
|
93
|
+
// Test Engineer - Can write tests (F1: hyphenated key). Tool list mirrors
|
|
94
|
+
// agents/test-engineer.md frontmatter (T3 binding, commit c25dfbe2):
|
|
95
|
+
// search tools + delegate for spawning depth-1 helpers.
|
|
94
96
|
"test-engineer": {
|
|
95
|
-
tools: ["read", "edit", "write", "bash", "ls", "scratchpad"],
|
|
97
|
+
tools: ["read", "edit", "write", "bash", "ls", "glob", "grep", "find", "scratchpad", "ask", "delegate"],
|
|
96
98
|
excludeTools: ["web"],
|
|
97
99
|
// Phase 1 scratchpad: build/run test suites with state across cells.
|
|
98
100
|
scratchpad: true,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { collectSensitiveConfigPaths } from "../schema/sensitive-config-paths.ts";
|
|
2
|
+
import type { ConfigValidationResult, PiTeamsConfig } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Phase 5.1 (ADR docs/decisions/2026-08-15-schema-driven-sanitize.md): the
|
|
6
|
+
* drop-list is derived from `sensitive: true` marks in the config schema
|
|
7
|
+
* (see src/schema/sensitive-config-paths.ts) instead of being hardcoded.
|
|
8
|
+
*
|
|
9
|
+
* Legacy shape preserved verbatim from the pre-Phase-5.1 implementation:
|
|
10
|
+
* - warning format: `${projectPath}: project-level sensitive config '<dotted>' is ignored; set it in user config to trust it explicitly`
|
|
11
|
+
* - warning order: top-level keys first (executeWorkers, asyncByDefault,
|
|
12
|
+
* requireCleanWorktreeLeader), then sections runtime → autonomous →
|
|
13
|
+
* worktree → otlp → agents → tools; newly-marked sections (policy) follow
|
|
14
|
+
* in schema order. Within a section, keys follow schema property order.
|
|
15
|
+
* - empty-object collapse: a section whose defined keys were all dropped
|
|
16
|
+
* becomes `undefined`.
|
|
17
|
+
* - worktree/otlp redact via `{ ...section, key: undefined }` (the dropped
|
|
18
|
+
* key remains as an own `undefined` key) while other sections `delete` it —
|
|
19
|
+
* pinned by config-sanitize-merge.test.ts. runtime/autonomous are always
|
|
20
|
+
* replaced by a shallow copy even when nothing was dropped (legacy shape).
|
|
21
|
+
*
|
|
22
|
+
* Wave 1A (S19-1/S19-2/S19-5, F19-4/F19-7): two extensions on top of the
|
|
23
|
+
* unconditional sensitive walk —
|
|
24
|
+
* - new unconditional marks: `goalWrap` (whole subtree, terminal Record
|
|
25
|
+
* mark) and `autonomous.magicKeywords` are now user-only;
|
|
26
|
+
* - CONDITIONAL_PROJECT_DROPS (below): guard-tiering — the project tier may
|
|
27
|
+
* only TIGHTEN these fields, never loosen them. The legacy inline
|
|
28
|
+
* `requirePlanApproval === false` special-case is folded into that table
|
|
29
|
+
* (same warning output).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export function projectOverrideWarning(projectPath: string, dottedPath: string): string {
|
|
33
|
+
return `${projectPath}: project-level sensitive config '${dottedPath}' is ignored; set it in user config to trust it explicitly`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Wave 1A guard-tiering (S19-1/S19-5, F19-4/F19-7 — ADR
|
|
37
|
+
* docs/decisions/2026-08-15-schema-driven-sanitize.md, "Wave 1A remediation"):
|
|
38
|
+
* dotted path -> predicate over the project value. When the predicate holds
|
|
39
|
+
* (the value LOOSENS a guard or disables an availability control) the key is
|
|
40
|
+
* dropped with the standard warning; every other value survives untouched.
|
|
41
|
+
* Applied AFTER the unconditional sensitive walk.
|
|
42
|
+
*
|
|
43
|
+
* `limits.maxConcurrentWorkers` deliberately has NO entry: the schema already
|
|
44
|
+
* bounds it (Type.Integer minimum 1, reader-side sanity ceilings) and a project
|
|
45
|
+
* can only lower it — i.e. tighten — so no drop is needed (ADR Wave 1A).
|
|
46
|
+
*/
|
|
47
|
+
export const CONDITIONAL_PROJECT_DROPS: Record<string, (value: unknown) => boolean> = {
|
|
48
|
+
// Legacy exception (ADR Decision 3, folded in here): only `=== false`
|
|
49
|
+
// loosens plan approval; `true` tightens and survives.
|
|
50
|
+
"runtime.requirePlanApproval": (value) => value === false,
|
|
51
|
+
// S19-1: both guards are ACTIVE by default ("warn") — "off" silently
|
|
52
|
+
// disables them for every contributor who has not pinned the field in
|
|
53
|
+
// user config. "warn"/"block"/"fail" tighten or keep the guard and survive.
|
|
54
|
+
"runtime.completionMutationGuard": (value) => value === "off",
|
|
55
|
+
"runtime.effectivenessGuard": (value) => value === "off",
|
|
56
|
+
// S19-1: unbounded concurrency is a resource-exhaustion loosening.
|
|
57
|
+
"limits.allowUnboundedConcurrency": (value) => value === true,
|
|
58
|
+
// S19-1: disabling recovery / model scoping loosens reliability guards.
|
|
59
|
+
"reliability.autoRecover": (value) => value === false,
|
|
60
|
+
"reliability.scopeModels": (value) => value === false,
|
|
61
|
+
// F19-7 (availability-only): a project may ENABLE the broker, never
|
|
62
|
+
// disable it repo-wide. Other broker fields are unmarked (as-is).
|
|
63
|
+
"broker.enabled": (value) => value === false,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Legacy top-level warning order (kept byte-identical to the pre-5.1 code). */
|
|
67
|
+
const TOP_LEVEL_LEGACY_ORDER = ["executeWorkers", "asyncByDefault", "requireCleanWorktreeLeader"] as const;
|
|
68
|
+
|
|
69
|
+
/** Legacy section processing order; unlisted sections (e.g. policy) follow in schema order. */
|
|
70
|
+
const SECTION_LEGACY_ORDER = ["runtime", "autonomous", "worktree", "otlp", "agents", "tools"] as const;
|
|
71
|
+
|
|
72
|
+
/** Sections where the pre-5.1 code always replaced `sanitized.<section>` with a
|
|
73
|
+
* shallow copy, even when nothing was dropped (runtime/autonomous). */
|
|
74
|
+
const LEGACY_ALWAYS_COPY_SECTIONS = new Set<string>(["runtime", "autonomous"]);
|
|
75
|
+
|
|
76
|
+
/** Sections redacting via assignment (`key: undefined`, own key remains) instead
|
|
77
|
+
* of `delete` — legacy quirk pinned by config-sanitize-merge.test.ts. */
|
|
78
|
+
const ASSIGN_REDACT_SECTIONS = new Set<string>(["worktree", "otlp"]);
|
|
79
|
+
|
|
80
|
+
function groupPathsBySection(dottedPaths: string[]): Map<string, string[]> {
|
|
81
|
+
const groups = new Map<string, string[]>();
|
|
82
|
+
for (const dotted of dottedPaths) {
|
|
83
|
+
const separator = dotted.indexOf(".");
|
|
84
|
+
const section = separator === -1 ? "" : dotted.slice(0, separator);
|
|
85
|
+
const key = separator === -1 ? dotted : dotted.slice(separator + 1);
|
|
86
|
+
const bucket = groups.get(section);
|
|
87
|
+
if (bucket) bucket.push(key);
|
|
88
|
+
else groups.set(section, [key]);
|
|
89
|
+
}
|
|
90
|
+
return groups;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sectionProcessingOrder(sections: string[]): string[] {
|
|
94
|
+
return [...sections].sort((a, b) => {
|
|
95
|
+
const rankA = (SECTION_LEGACY_ORDER as readonly string[]).indexOf(a);
|
|
96
|
+
const rankB = (SECTION_LEGACY_ORDER as readonly string[]).indexOf(b);
|
|
97
|
+
// Unlisted sections (new sensitive marks) sort after legacy ones,
|
|
98
|
+
// preserving their schema-walk order (stable sort).
|
|
99
|
+
if (rankA === -1 && rankB === -1) return 0;
|
|
100
|
+
if (rankA === -1) return 1;
|
|
101
|
+
if (rankB === -1) return -1;
|
|
102
|
+
return rankA - rankB;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function sanitizeProjectConfig(projectPath: string, userConfig: PiTeamsConfig, config: PiTeamsConfig): ConfigValidationResult {
|
|
107
|
+
const sanitized: PiTeamsConfig = { ...config };
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
const groups = groupPathsBySection(collectSensitiveConfigPaths());
|
|
110
|
+
|
|
111
|
+
const dropTopLevel = (key: string): void => {
|
|
112
|
+
if ((config as Record<string, unknown>)[key] === undefined) return;
|
|
113
|
+
delete (sanitized as Record<string, unknown>)[key];
|
|
114
|
+
warnings.push(projectOverrideWarning(projectPath, key));
|
|
115
|
+
};
|
|
116
|
+
const topLevelKeys = groups.get("") ?? [];
|
|
117
|
+
for (const key of [...topLevelKeys].sort((a, b) => {
|
|
118
|
+
const rankA = (TOP_LEVEL_LEGACY_ORDER as readonly string[]).indexOf(a);
|
|
119
|
+
const rankB = (TOP_LEVEL_LEGACY_ORDER as readonly string[]).indexOf(b);
|
|
120
|
+
if (rankA === -1 && rankB === -1) return 0;
|
|
121
|
+
if (rankA === -1) return 1;
|
|
122
|
+
if (rankB === -1) return -1;
|
|
123
|
+
return rankA - rankB;
|
|
124
|
+
})) {
|
|
125
|
+
dropTopLevel(key);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sanitizedRecord = sanitized as Record<string, unknown>;
|
|
129
|
+
const configRecord = config as Record<string, unknown>;
|
|
130
|
+
for (const section of sectionProcessingOrder([...groups.keys()].filter((s) => s !== ""))) {
|
|
131
|
+
const sectionValue = configRecord[section];
|
|
132
|
+
if (sectionValue === undefined || typeof sectionValue !== "object") continue;
|
|
133
|
+
const sectionConfig = { ...(sectionValue as Record<string, unknown>) };
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const key of groups.get(section) ?? []) {
|
|
136
|
+
if (sectionConfig[key] === undefined) continue;
|
|
137
|
+
if (ASSIGN_REDACT_SECTIONS.has(section)) sectionConfig[key] = undefined;
|
|
138
|
+
else delete sectionConfig[key];
|
|
139
|
+
warnings.push(projectOverrideWarning(projectPath, `${section}.${key}`));
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
if (!changed && !LEGACY_ALWAYS_COPY_SECTIONS.has(section)) continue;
|
|
143
|
+
sanitizedRecord[section] = Object.values(sectionConfig).some((entry) => entry !== undefined) ? sectionConfig : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Wave 1A guard-tiering: conditional drops run AFTER the unconditional
|
|
147
|
+
// sensitive walk. Sections holding conditional keys that carry no sensitive
|
|
148
|
+
// marks (limits, reliability, broker) never appear in `groups`, so this pass
|
|
149
|
+
// reads the SANITIZED config (runtime is already a shallow copy there),
|
|
150
|
+
// shallow-copies before deleting (input stays unmutated), and applies the
|
|
151
|
+
// same empty-object-collapse convention. runtime.* conditional drops thus
|
|
152
|
+
// integrate with the existing runtime section copy into one coherent result.
|
|
153
|
+
const conditionalGroups = groupPathsBySection(Object.keys(CONDITIONAL_PROJECT_DROPS));
|
|
154
|
+
for (const section of sectionProcessingOrder([...conditionalGroups.keys()].filter((s) => s !== ""))) {
|
|
155
|
+
const sectionValue = sanitizedRecord[section];
|
|
156
|
+
if (sectionValue === undefined || typeof sectionValue !== "object") continue;
|
|
157
|
+
const sectionConfig = { ...(sectionValue as Record<string, unknown>) };
|
|
158
|
+
let changed = false;
|
|
159
|
+
for (const key of conditionalGroups.get(section) ?? []) {
|
|
160
|
+
if (!CONDITIONAL_PROJECT_DROPS[`${section}.${key}`](sectionConfig[key])) continue;
|
|
161
|
+
delete sectionConfig[key];
|
|
162
|
+
warnings.push(projectOverrideWarning(projectPath, `${section}.${key}`));
|
|
163
|
+
changed = true;
|
|
164
|
+
}
|
|
165
|
+
if (!changed) continue;
|
|
166
|
+
sanitizedRecord[section] = Object.values(sectionConfig).some((entry) => entry !== undefined) ? sectionConfig : undefined;
|
|
167
|
+
}
|
|
168
|
+
return { config: sanitized, warnings };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @internal — direct-test seam for Phase 2.2 extraction target (refactor-plan step 1.9c). */
|
|
172
|
+
export const __test__sanitizeProjectConfig = sanitizeProjectConfig;
|
package/src/config/types.ts
CHANGED
|
@@ -119,6 +119,10 @@ export interface CrewModelFallbackConfig {
|
|
|
119
119
|
export interface CrewControlConfig {
|
|
120
120
|
enabled?: boolean;
|
|
121
121
|
needsAttentionAfterMs?: number;
|
|
122
|
+
/** Consecutive tool-failure threshold before a worker is flagged. Default 3 (applied at read site). */
|
|
123
|
+
consecutiveFailureThreshold?: number;
|
|
124
|
+
/** Minutes after which a running worker is flagged long-running. Default 10 (applied at read site). */
|
|
125
|
+
longRunningMinutes?: number;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
export interface CrewWorktreeConfig {
|
|
@@ -139,8 +143,26 @@ export interface GoalWrapWorkflowConfig {
|
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
export interface CrewUiConfig {
|
|
142
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Where the crew dock (agents list) renders:
|
|
148
|
+
* - `aboveEditor` / `belowEditor`: pi's widget slots around the prompt;
|
|
149
|
+
* - `bottom`: inside the crew-vibes footer, BELOW the quota/meter lines
|
|
150
|
+
* (the very bottom of the screen). Falls back to `belowEditor` when
|
|
151
|
+
* crew-vibes is not active (no footer sink).
|
|
152
|
+
*/
|
|
153
|
+
widgetPlacement?: "aboveEditor" | "belowEditor" | "bottom";
|
|
143
154
|
widgetMaxLines?: number;
|
|
155
|
+
/**
|
|
156
|
+
* Per-agent row layout in the widget. `compact` is one width-budgeted line
|
|
157
|
+
* per agent; `detailed` keeps the two-line tree (name row + activity row).
|
|
158
|
+
*/
|
|
159
|
+
widgetRowStyle?: "compact" | "detailed";
|
|
160
|
+
/**
|
|
161
|
+
* Keyboard-navigable agent rows under the prompt (`↓` from an empty prompt).
|
|
162
|
+
* Requires owning pi's editor component, so it yields to any other extension
|
|
163
|
+
* that already installed a custom editor.
|
|
164
|
+
*/
|
|
165
|
+
inlinePanel?: boolean;
|
|
144
166
|
powerbar?: boolean;
|
|
145
167
|
dashboardPlacement?: "center" | "right";
|
|
146
168
|
dashboardWidth?: number;
|
|
@@ -282,6 +304,26 @@ export interface PiTeamsConfig {
|
|
|
282
304
|
* schema (`src/schema/config-schema.ts`).
|
|
283
305
|
*/
|
|
284
306
|
broker?: CrewBrokerConfig;
|
|
307
|
+
/**
|
|
308
|
+
* Governed nesting (ADR-5 docs/decisions/2026-08-17-governed-nesting.md).
|
|
309
|
+
* Worker→grandchild delegation via the `delegate` tool. Default is OFF
|
|
310
|
+
* (`enabled:false`) — fail-closed until WP-5 completes (B3 battery +
|
|
311
|
+
* security sign-off), then flipped to true. While false, `delegate`
|
|
312
|
+
* admission is rejected with a structured policy message and a
|
|
313
|
+
* `delegate.rejected` event (never silent).
|
|
314
|
+
*/
|
|
315
|
+
nesting?: CrewNestingConfig;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Governed-nesting config (ADR-5). */
|
|
319
|
+
export interface CrewNestingConfig {
|
|
320
|
+
/** Master switch. `false` (default) keeps the `delegate` surface dormant. */
|
|
321
|
+
enabled?: boolean;
|
|
322
|
+
/** Nested-slot budget override. Default max(1, floor(globalSem/2)). 1..64. */
|
|
323
|
+
maxSlots?: number;
|
|
324
|
+
/** Max crew depth for delegate-driven nesting. Default 2. 1..10 (mirrors
|
|
325
|
+
* the PI_CREW_MAX_DEPTH env clamp). */
|
|
326
|
+
maxDepth?: number;
|
|
285
327
|
}
|
|
286
328
|
|
|
287
329
|
/** CrewBroker config (Phase 0 inter-pi broker). */
|
|
@@ -294,6 +336,12 @@ export interface CrewBrokerConfig {
|
|
|
294
336
|
maxFrameBytes?: number;
|
|
295
337
|
/** Per-connection outbound queue cap. 32..4096 (default 256). */
|
|
296
338
|
outboundQueueCap?: number;
|
|
339
|
+
/** WP-2/R2 (ADR-0 docs/decisions/2026-08-17-waiting-producer-ask item 7):
|
|
340
|
+
* capability gate for the broker `wait.*` methods (waiting-producer park
|
|
341
|
+
* contract). DEFAULT FALSE — fail-closed until WP-2 completes; while
|
|
342
|
+
* false, wait.request/wait.resolve are rejected with a policy-disabled
|
|
343
|
+
* error plus a policy.action event in events.jsonl (never silent). */
|
|
344
|
+
waitMethodsEnabled?: boolean;
|
|
297
345
|
}
|
|
298
346
|
|
|
299
347
|
export interface LoadedPiTeamsConfig {
|
|
@@ -26,6 +26,21 @@ function isFinished(status: string): boolean {
|
|
|
26
26
|
return status === "completed" || status === "failed" || status === "cancelled" || status === "blocked";
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// R5-M2 (Round 5 MEDIUM-2): FIFO cap for the dedupe Set. Set preserves
|
|
30
|
+
// insertion order, so evicting .values().next().value drops the oldest-seen
|
|
31
|
+
// runId. 256 >> the ~20 runs polled per tick; eviction only matters for a
|
|
32
|
+
// long-lived host session that observes thousands of finished runs.
|
|
33
|
+
const MAX_SEEN_FINISHED_RUN_IDS = 256;
|
|
34
|
+
|
|
35
|
+
function addSeenFinishedRunId(state: AsyncNotifierState, runId: string): void {
|
|
36
|
+
state.seenFinishedRunIds.add(runId);
|
|
37
|
+
while (state.seenFinishedRunIds.size > MAX_SEEN_FINISHED_RUN_IDS) {
|
|
38
|
+
const oldest = state.seenFinishedRunIds.values().next().value;
|
|
39
|
+
if (oldest === undefined) break;
|
|
40
|
+
state.seenFinishedRunIds.delete(oldest);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export function isAsyncTerminalEvent(event: TeamEvent): boolean {
|
|
30
45
|
return event.type === "async.completed" || event.type === "async.failed" || event.type === "async.died";
|
|
31
46
|
}
|
|
@@ -129,11 +144,15 @@ export function startAsyncRunNotifier(
|
|
|
129
144
|
const sid = extractSessionId(ctx);
|
|
130
145
|
const ownsRun = (run: TeamRunManifest): boolean => !sid || !run.ownerSessionId || run.ownerSessionId === sid;
|
|
131
146
|
for (const run of listRuns(ctx.cwd).filter(ownsRun)) {
|
|
147
|
+
// R5-M2: the seed scans the FULL listRuns() index (newest-first); stop
|
|
148
|
+
// seeding once the Set is at capacity so a huge historical index cannot
|
|
149
|
+
// push the newest finished runIds out via FIFO eviction.
|
|
150
|
+
if (state.seenFinishedRunIds.size >= MAX_SEEN_FINISHED_RUN_IDS) break;
|
|
132
151
|
// Suppress only terminal runs that were already finished before this owner
|
|
133
152
|
// session (or before the previous session switch). Active runs must remain
|
|
134
153
|
// un-seen so completions during auto-compaction/session restart are delivered.
|
|
135
154
|
const updatedAtMs = timeMs(run.updatedAt) ?? 0;
|
|
136
|
-
if (isFinished(run.status) && updatedAtMs < staleBeforeMs) state
|
|
155
|
+
if (isFinished(run.status) && updatedAtMs < staleBeforeMs) addSeenFinishedRunId(state, run.runId);
|
|
137
156
|
}
|
|
138
157
|
let cachedRuns: TeamRunManifest[] | undefined;
|
|
139
158
|
state.interval = setInterval(() => {
|
|
@@ -147,7 +166,7 @@ export function startAsyncRunNotifier(
|
|
|
147
166
|
for (const run of cachedRuns) {
|
|
148
167
|
const current = markDeadAsyncRunIfNeeded(run) ?? run;
|
|
149
168
|
if (!isFinished(current.status) || state.seenFinishedRunIds.has(current.runId)) continue;
|
|
150
|
-
state
|
|
169
|
+
addSeenFinishedRunId(state, current.runId);
|
|
151
170
|
// Suppress notifications for INTERNAL goal-loop sub-runs.
|
|
152
171
|
// The outer goal-loop creates a synthetic 'goal-turn' workflow per turn
|
|
153
172
|
// (see buildTurnWorkflow in goal-loop-runner.ts). These runs are
|
|
@@ -182,6 +201,10 @@ export function startAsyncRunNotifier(
|
|
|
182
201
|
export function stopAsyncRunNotifier(state: AsyncNotifierState): void {
|
|
183
202
|
if (state.interval) clearInterval(state.interval);
|
|
184
203
|
state.interval = undefined;
|
|
204
|
+
// R5-M2: clear the dedupe Set on stop so entries do not persist across
|
|
205
|
+
// stop/start cycles; startAsyncRunNotifier re-seeds from listRuns() using
|
|
206
|
+
// lastStoppedAtMs, so runs finished before the stop stay suppressed.
|
|
207
|
+
state.seenFinishedRunIds.clear();
|
|
185
208
|
state.generation = (state.generation ?? 0) + 1;
|
|
186
209
|
state.lastStoppedAtMs = Date.now();
|
|
187
210
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { cleanupAllTrackedTempDirs } from "../runtime/model/pi-args.ts";
|
|
3
|
+
import { isSessionSwitchInFlight } from "../ui/inline-panel/view-session-store.ts";
|
|
3
4
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
5
|
|
|
5
6
|
// NOTE: globalProgressTracker import kept for documentation but not directly used
|
|
@@ -67,6 +68,18 @@ export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTermina
|
|
|
67
68
|
console.log("[pi-crew] Session shutdown - cleaning up resources");
|
|
68
69
|
|
|
69
70
|
try {
|
|
71
|
+
// Navigational switches (pi's own resume/new/fork) tear the session
|
|
72
|
+
// down on purpose — the user is only LOOKING at a different pi
|
|
73
|
+
// session; foreground team runs + their workers share this process
|
|
74
|
+
// and must keep running (P0: "jumping into any session stops the
|
|
75
|
+
// run" — exit 143 signature). Killing them here bypasses child-pi's
|
|
76
|
+
// abort flags, so the worker exit reads as an "unexpected
|
|
77
|
+
// code=143 signal=null" and the run is cancelled. Same contract as
|
|
78
|
+
// stopSessionBoundSubagents and the run-deadline caller-abort
|
|
79
|
+
// suppression; on REAL shutdowns (quit/reload) the flag is unset
|
|
80
|
+
// and this cleanup still drains the registry, as does the
|
|
81
|
+
// process-exit hook (tryRegisterSessionCleanup) when pi dies.
|
|
82
|
+
if (isSessionSwitchInFlight()) return;
|
|
70
83
|
// Kill all child-pi processes
|
|
71
84
|
await cleanupChildProcesses();
|
|
72
85
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
3
4
|
import { hasCrewFontFile, isWebTerminal } from "./font-detect.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -13,7 +14,7 @@ export const CAPACITY_STATUS_ID = "pi-crew-bar";
|
|
|
13
14
|
export const PROVIDER_STATUS_ID = "pi-crew-bar";
|
|
14
15
|
|
|
15
16
|
function resolveHome(): string {
|
|
16
|
-
return (
|
|
17
|
+
return getCrewEnv("PI_CREW_HOME")?.trim() || process.env.HOME || process.env.USERPROFILE || "";
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function configPath(): string {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { getFooterDockProvider } from "../../ui/dock-footer.ts";
|
|
3
4
|
import { requestRenderTarget } from "../../ui/pi-ui-compat.ts";
|
|
4
5
|
import { asCrewTheme, type CrewTheme } from "../../ui/theme-adapter.ts";
|
|
6
|
+
import { colorWidgetLine, renderLines } from "../../ui/widget/widget-renderer.ts";
|
|
5
7
|
import { truncateToWidth, visibleWidth } from "../../utils/visual.ts";
|
|
6
8
|
import type { CrewVibesConfig } from "./config.ts";
|
|
7
9
|
import type { ProviderUsage } from "./provider-usage.ts";
|
|
@@ -280,11 +282,28 @@ class CrewVibesFooter implements FooterComponent {
|
|
|
280
282
|
return [truncateToWidth(cap, width, "…"), this.rightAlign(quota, width)];
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
/** Dock lines registered by the crew widget (`widgetPlacement: "bottom"`):
|
|
286
|
+
* painted as the very last block of the footer, below the quota/meters.
|
|
287
|
+
* The provider returns RAW lines; we color them with this footer's theme
|
|
288
|
+
* (same per-line palette the widget would have used) and truncate to the
|
|
289
|
+
* real render width. */
|
|
290
|
+
private buildDockLines(width: number): string[] {
|
|
291
|
+
const provider = getFooterDockProvider();
|
|
292
|
+
if (!provider) return [];
|
|
293
|
+
const raw = provider(width);
|
|
294
|
+
if (raw.length === 0) return [];
|
|
295
|
+
return renderLines(
|
|
296
|
+
raw.map((line, index) => colorWidgetLine(line, index, this.theme)),
|
|
297
|
+
width,
|
|
298
|
+
).map((line) => truncateToWidth(line, width, "…"));
|
|
299
|
+
}
|
|
300
|
+
|
|
283
301
|
render(width: number): string[] {
|
|
284
302
|
const lines = [this.buildPwdLine(width), this.buildStatsLine(width)];
|
|
285
303
|
const statusLine = this.buildStatusLine(width);
|
|
286
304
|
if (statusLine) lines.push(statusLine);
|
|
287
305
|
lines.push(...this.buildMeterLines(width));
|
|
306
|
+
lines.push(...this.buildDockLines(width));
|
|
288
307
|
return lines;
|
|
289
308
|
}
|
|
290
309
|
}
|
|
@@ -19,6 +19,7 @@ function safeUiCall(scope: string, fn: () => void): void {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
import { setFooterDockSinkActive } from "../../ui/dock-footer.ts";
|
|
22
23
|
import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
|
|
23
24
|
import { intervalForSpeed } from "./figures.ts";
|
|
24
25
|
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
@@ -91,11 +92,18 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
91
92
|
function installFooter(ctx: ExtensionContext): void {
|
|
92
93
|
if (!ctx?.hasUI) return;
|
|
93
94
|
if (!metersActive()) {
|
|
94
|
-
safeUiCall("clear-footer", () =>
|
|
95
|
+
safeUiCall("clear-footer", () => {
|
|
96
|
+
// The crew dock can no longer render at the very bottom — the
|
|
97
|
+
// widget falls back to pi's belowEditor slot automatically.
|
|
98
|
+
setFooterDockSinkActive(false);
|
|
99
|
+
setFooter(ctx, undefined);
|
|
100
|
+
});
|
|
95
101
|
return;
|
|
96
102
|
}
|
|
97
103
|
safeUiCall("install-footer", () => {
|
|
98
104
|
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
105
|
+
// The footer can now host the crew dock below the quota lines.
|
|
106
|
+
setFooterDockSinkActive(true);
|
|
99
107
|
requestRender(ctx);
|
|
100
108
|
});
|
|
101
109
|
}
|
|
@@ -245,6 +253,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
245
253
|
stopFooterTimer();
|
|
246
254
|
stopCapacityTimer();
|
|
247
255
|
stopProviderTimer();
|
|
256
|
+
setFooterDockSinkActive(false);
|
|
248
257
|
setFooter(ctx, undefined);
|
|
249
258
|
clearVibesStatus(ctx);
|
|
250
259
|
return;
|
|
@@ -370,6 +379,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
370
379
|
stopFooterTimer();
|
|
371
380
|
stopCapacityTimer();
|
|
372
381
|
stopProviderTimer();
|
|
382
|
+
setFooterDockSinkActive(false);
|
|
373
383
|
setFooter(ctx, undefined);
|
|
374
384
|
clearVibesStatus(ctx);
|
|
375
385
|
});
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* for sequential agent chain execution based on ECC recommendations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
8
9
|
import * as fs from "node:fs";
|
|
10
|
+
import type { PlanRecord } from "../state/types.ts";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Tag → Agent chain mapping from ECC recommendations.
|
|
@@ -300,3 +302,133 @@ export async function orchestratePlan(options: OrchestrateOptions): Promise<{
|
|
|
300
302
|
|
|
301
303
|
return { steps, chain, overview };
|
|
302
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* T2/R4 (ADR-4 §6, producer 1): convert parsed orchestrate steps into a
|
|
308
|
+
* PlanRecord. Pure — persistence (appendPlanRevision) is the caller's job.
|
|
309
|
+
* Phases group steps by tag in encounter order; item ids reuse the stable
|
|
310
|
+
* stepId so revisions diff cleanly. taskIds stay EMPTY (single-writer rule:
|
|
311
|
+
* only the scheduler links tasks, ADR-4 §3).
|
|
312
|
+
*/
|
|
313
|
+
export function stepsToPlanRecord(steps: OrchestratedStep[], runId: string, opts: { title?: string } = {}): PlanRecord {
|
|
314
|
+
const phaseOrder: string[] = [];
|
|
315
|
+
const byTag = new Map<string, OrchestratedStep[]>();
|
|
316
|
+
for (const step of steps) {
|
|
317
|
+
if (!byTag.has(step.tag)) {
|
|
318
|
+
byTag.set(step.tag, []);
|
|
319
|
+
phaseOrder.push(step.tag);
|
|
320
|
+
}
|
|
321
|
+
(byTag.get(step.tag) as OrchestratedStep[]).push(step);
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
id: randomUUID(),
|
|
325
|
+
runId,
|
|
326
|
+
version: 1,
|
|
327
|
+
title: opts.title ?? "Orchestrated plan",
|
|
328
|
+
phases: phaseOrder.map((tag, i) => ({
|
|
329
|
+
id: `phase-${i + 1}-${tag}`,
|
|
330
|
+
title: byTag.get(tag)?.[0]?.heading ?? tag,
|
|
331
|
+
itemIds: (byTag.get(tag) ?? []).map((sqs) => sqs.stepId),
|
|
332
|
+
status: "pending",
|
|
333
|
+
})),
|
|
334
|
+
items: steps.map((sqs) => ({
|
|
335
|
+
id: sqs.stepId,
|
|
336
|
+
ref: sqs.stepId,
|
|
337
|
+
title: sqs.heading ?? sqs.prompt.slice(0, 80),
|
|
338
|
+
taskIds: [],
|
|
339
|
+
specIds: [],
|
|
340
|
+
acceptance: [],
|
|
341
|
+
status: "pending",
|
|
342
|
+
})),
|
|
343
|
+
createdAt: new Date().toISOString(),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* T2/R4 (ADR-4 §6, producer 3): parse a planner-role task output into a
|
|
349
|
+
* PlanRecord. Contract: one fenced/tagged block
|
|
350
|
+
*
|
|
351
|
+
* <plan>
|
|
352
|
+
* { "title": "...", "phases": [{ "title": "...", "items": [{"id": "...", "title": "...", "task": "...", "acceptance": ["..."]}] }] }
|
|
353
|
+
* </plan>
|
|
354
|
+
*
|
|
355
|
+
* The block content is JSON (code fences tolerated). Returns undefined when no
|
|
356
|
+
* block or malformed — callers treat that as "no plan record" (pre-v2 path).
|
|
357
|
+
* Item ids are namespaced `pi-<n>-<slug>` when the planner omits them, so ids
|
|
358
|
+
* stay stable across revisions for diff/carry-over.
|
|
359
|
+
*/
|
|
360
|
+
export function parsePlannerPlanOutput(text: string, runId: string, authorTaskId?: string): PlanRecord | undefined {
|
|
361
|
+
const blockMatch = text.match(/<plan>\s*([\s\S]*?)\s*<\/plan>/);
|
|
362
|
+
if (!blockMatch) return undefined;
|
|
363
|
+
let raw = blockMatch[1] as string;
|
|
364
|
+
// Security hardening (review S5): producer text is untrusted worker output —
|
|
365
|
+
// bound the parse input (deep nesting already fails safe via JSON.parse
|
|
366
|
+
// RangeError → catch → undefined; this caps the flat size too).
|
|
367
|
+
if (raw.length > 1_048_576) return undefined;
|
|
368
|
+
raw = raw.replace(/^```[a-z]*\n?/, "").replace(/\n?```$/, "");
|
|
369
|
+
let parsed: unknown;
|
|
370
|
+
try {
|
|
371
|
+
parsed = JSON.parse(raw);
|
|
372
|
+
} catch {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
376
|
+
const obj = parsed as { title?: unknown; phases?: unknown };
|
|
377
|
+
if (!Array.isArray(obj.phases) || obj.phases.length === 0) return undefined;
|
|
378
|
+
const phases: PlanRecord["phases"] = [];
|
|
379
|
+
const items: PlanRecord["items"] = [];
|
|
380
|
+
let n = 0;
|
|
381
|
+
for (const [phaseIndex, phaseRaw] of obj.phases.entries()) {
|
|
382
|
+
if (!phaseRaw || typeof phaseRaw !== "object" || Array.isArray(phaseRaw)) return undefined;
|
|
383
|
+
const phaseObj = phaseRaw as { title?: unknown; items?: unknown };
|
|
384
|
+
if (!Array.isArray(phaseObj.items) || phaseObj.items.length === 0) return undefined;
|
|
385
|
+
const phaseId = `phase-${phaseIndex + 1}`;
|
|
386
|
+
const itemIds: string[] = [];
|
|
387
|
+
for (const itemRaw of phaseObj.items) {
|
|
388
|
+
if (!itemRaw || typeof itemRaw !== "object" || Array.isArray(itemRaw)) return undefined;
|
|
389
|
+
const itemObj = itemRaw as { id?: unknown; title?: unknown; task?: unknown; acceptance?: unknown; ref?: unknown };
|
|
390
|
+
const taskText = typeof itemObj.task === "string" ? itemObj.task : undefined;
|
|
391
|
+
const title = typeof itemObj.title === "string" && itemObj.title.trim() ? itemObj.title.trim() : taskText?.slice(0, 80);
|
|
392
|
+
if (!title) return undefined;
|
|
393
|
+
n++;
|
|
394
|
+
const id = typeof itemObj.id === "string" && itemObj.id.trim() ? itemObj.id.trim() : `pi-${n}-${slugText(title)}`;
|
|
395
|
+
if (items.some((i) => i.id === id)) return undefined;
|
|
396
|
+
itemIds.push(id);
|
|
397
|
+
items.push({
|
|
398
|
+
id,
|
|
399
|
+
ref: typeof itemObj.ref === "string" ? itemObj.ref : undefined,
|
|
400
|
+
title,
|
|
401
|
+
taskIds: [],
|
|
402
|
+
specIds: [],
|
|
403
|
+
acceptance: Array.isArray(itemObj.acceptance) ? itemObj.acceptance.filter((a): a is string => typeof a === "string") : [],
|
|
404
|
+
status: "pending",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
phases.push({
|
|
408
|
+
id: phaseId,
|
|
409
|
+
title: typeof phaseObj.title === "string" && phaseObj.title.trim() ? phaseObj.title.trim() : `Phase ${phaseIndex + 1}`,
|
|
410
|
+
itemIds,
|
|
411
|
+
status: "pending",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
id: randomUUID(),
|
|
416
|
+
runId,
|
|
417
|
+
version: 1,
|
|
418
|
+
title: typeof obj.title === "string" && obj.title.trim() ? obj.title.trim() : "Planner plan",
|
|
419
|
+
phases,
|
|
420
|
+
items,
|
|
421
|
+
createdAt: new Date().toISOString(),
|
|
422
|
+
authorTaskId,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function slugText(value: string): string {
|
|
427
|
+
return (
|
|
428
|
+
value
|
|
429
|
+
.toLowerCase()
|
|
430
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
431
|
+
.replace(/^-+|-+$/g, "")
|
|
432
|
+
.slice(0, 32) || "item"
|
|
433
|
+
);
|
|
434
|
+
}
|