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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
1
2
|
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "@sinclair/typebox";
|
|
3
4
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
@@ -28,14 +29,18 @@ import type { BatchBarrier } from "../../runtime/scheduling/batch-barrier.ts";
|
|
|
28
29
|
import {
|
|
29
30
|
readPersistedSubagentRecord,
|
|
30
31
|
type SubagentManager,
|
|
32
|
+
type SubagentRecord,
|
|
31
33
|
type SubagentSpawnOptions,
|
|
32
34
|
savePersistedSubagentRecord,
|
|
33
35
|
} from "../../runtime/subagent-manager.ts";
|
|
36
|
+
import { TEAM_TERMINAL_TASK_STATUSES } from "../../state/contracts.ts";
|
|
37
|
+
import { resolveEntryBySubagentId, upsertOwnershipEntry } from "../../state/stores/ownership-map.ts";
|
|
34
38
|
import { loadRunManifestById } from "../../state/stores/state-store.ts";
|
|
35
39
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
36
40
|
import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
|
|
37
41
|
import { agentToolRenderer, type ToolRenderContext } from "../../ui/tool-renderers/index.ts";
|
|
38
42
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
43
|
+
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
39
44
|
import {
|
|
40
45
|
__test__subagentSpawnParams,
|
|
41
46
|
formatSubagentRecord,
|
|
@@ -140,8 +145,55 @@ export function registerSubagentTools(
|
|
|
140
145
|
// by the Agent tool have proper session ownership for isolation.
|
|
141
146
|
const ctxWithSession = withSessionId(ctx);
|
|
142
147
|
spawnOptions.ownerSessionId = ctxWithSession.sessionId;
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
// WP-1/R1 (H6): the one-shot identity link. taskId is NOT knowable at
|
|
149
|
+
// spawn() time — handleTeamTool(action:"run") resolves only after the
|
|
150
|
+
// run is dispatched (scaffold/mock returns immediately; real runs at
|
|
151
|
+
// completion), and runId comes from the result details. So the link is
|
|
152
|
+
// written in the runner wrapper once the manifest resolves: the record
|
|
153
|
+
// gains taskId/depth, the persisted copy is refreshed, and the
|
|
154
|
+
// ownership-map entry (task ⇄ subagentId ⇄ artifactsDir) is upserted
|
|
155
|
+
// under the run lock. Best-effort: a failure logs but never throws
|
|
156
|
+
// into the spawn path. `spawnedRecord` is assigned right after spawn()
|
|
157
|
+
// returns; the runner's continuation runs on a later microtask, so the
|
|
158
|
+
// assignment is always visible when the identity-link block executes.
|
|
159
|
+
let spawnedRecord: SubagentRecord | undefined;
|
|
160
|
+
const runner = async (currentOptions: SubagentSpawnOptions, childSignal?: AbortSignal) => {
|
|
161
|
+
// WP-1/R1 (H6 fix, review round): mid-run steer requires the identity
|
|
162
|
+
// link AT DISPATCH TIME, not after completion. `handleTeamTool` awaits
|
|
163
|
+
// the full run; the only mid-run signal we have is `onRunStarted`
|
|
164
|
+
// (fired in run.ts with updatedManifest.runId as the run starts). Hook
|
|
165
|
+
// it so the subagentId leg of the ownership map lands while the worker
|
|
166
|
+
// is live — steer_subagent can then resolve taskId mid-run instead of
|
|
167
|
+
// saying "not linked". The completion-time block below remains as a
|
|
168
|
+
// fallback for runs that complete between dispatch and this hook.
|
|
169
|
+
const linkAtDispatch = (runId?: string): void => {
|
|
170
|
+
if (typeof runId !== "string" || !spawnedRecord || spawnedRecord.taskId) return;
|
|
171
|
+
try {
|
|
172
|
+
const loaded = loadRunManifestById(currentOptions.cwd, runId);
|
|
173
|
+
const taskId = loaded?.tasks[0]?.id;
|
|
174
|
+
if (taskId && loaded) {
|
|
175
|
+
spawnedRecord.taskId = taskId;
|
|
176
|
+
// runId too: the manager only back-fills record.runId from the
|
|
177
|
+
// awaited result (waitForRun — i.e. at COMPLETION for background
|
|
178
|
+
// runs). Linking it here keeps taskId+runId landing together at
|
|
179
|
+
// dispatch so mid-run consumers (steer) resolve both immediately.
|
|
180
|
+
spawnedRecord.runId = runId;
|
|
181
|
+
spawnedRecord.depth = currentOptions.depth ?? 0;
|
|
182
|
+
savePersistedSubagentRecord(currentOptions.cwd, spawnedRecord);
|
|
183
|
+
upsertOwnershipEntry(loaded.manifest, {
|
|
184
|
+
taskId,
|
|
185
|
+
runId,
|
|
186
|
+
subagentId: spawnedRecord.id,
|
|
187
|
+
artifactsDir: loaded.manifest.artifactsRoot,
|
|
188
|
+
depth: spawnedRecord.depth,
|
|
189
|
+
updatedAt: new Date().toISOString(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logInternalError("subagent-tools.identity-dispatch", err, `runId=${runId ?? "(unknown)"}`);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const result = await handleTeamTool(
|
|
145
197
|
{
|
|
146
198
|
action: "run",
|
|
147
199
|
agent: currentOptions.type,
|
|
@@ -153,6 +205,7 @@ export function registerSubagentTools(
|
|
|
153
205
|
} as TeamToolParamsValue,
|
|
154
206
|
{
|
|
155
207
|
...ctxWithSession,
|
|
208
|
+
onRunStarted: (runId: string) => linkAtDispatch(runId), // mid-run capture (FIX)
|
|
156
209
|
signal: childSignal,
|
|
157
210
|
...(options.startForegroundRun
|
|
158
211
|
? {
|
|
@@ -162,7 +215,64 @@ export function registerSubagentTools(
|
|
|
162
215
|
: {}),
|
|
163
216
|
},
|
|
164
217
|
);
|
|
218
|
+
// WP-1/R1 (H6): identity link — record taskId/depth + ownership entry
|
|
219
|
+
// once the run manifest resolves. Direct-agent one-shot runs have
|
|
220
|
+
// exactly one task (direct-agent workflow); resolve from the manifest
|
|
221
|
+
// rather than hardcoding the id. The ownership upsert is verified and
|
|
222
|
+
// retried: a concurrently-completing run holds the run lock during its
|
|
223
|
+
// terminal writes, so a single upsert can lose the lock race
|
|
224
|
+
// (acquireLockWithRetry throws on a fresh foreign lock) and the
|
|
225
|
+
// store swallows that best-effort. taskId is already on the record at
|
|
226
|
+
// that point, so steer still resolves — but the map's subagentId leg
|
|
227
|
+
// would be missing for widget/status attribution. Ride out the
|
|
228
|
+
// contention window, then verify; still never throw.
|
|
229
|
+
let runId: string | undefined;
|
|
230
|
+
try {
|
|
231
|
+
const detailsRunId = (result.details as { runId?: unknown } | undefined)?.runId;
|
|
232
|
+
runId = typeof detailsRunId === "string" ? detailsRunId : undefined;
|
|
233
|
+
if (typeof runId === "string" && spawnedRecord) {
|
|
234
|
+
const loaded = loadRunManifestById(currentOptions.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
235
|
+
const taskId = loaded?.tasks[0]?.id;
|
|
236
|
+
// P3 hardening (T1 review round 1): base the completion-time retry on
|
|
237
|
+
// the MAP LEG, not on record.taskId. A dispatch-time upsert
|
|
238
|
+
// (linkAtDispatch) that lost the lock race would leave the map's
|
|
239
|
+
// subagentId leg missing; the old `!spawnedRecord.taskId` guard then
|
|
240
|
+
// skipped the verify+retry entirely. Now: write record fields once
|
|
241
|
+
// (idempotent), then verify+retry the MAP until the subagentId leg
|
|
242
|
+
// is present or budget exhausted. Upsert is merge-idempotent, so
|
|
243
|
+
// double-writing is safe.
|
|
244
|
+
if (taskId && loaded) {
|
|
245
|
+
if (!spawnedRecord.taskId) {
|
|
246
|
+
spawnedRecord.taskId = taskId;
|
|
247
|
+
spawnedRecord.depth = currentOptions.depth ?? 0;
|
|
248
|
+
savePersistedSubagentRecord(currentOptions.cwd, spawnedRecord);
|
|
249
|
+
}
|
|
250
|
+
const entry = {
|
|
251
|
+
taskId,
|
|
252
|
+
runId,
|
|
253
|
+
subagentId: spawnedRecord.id,
|
|
254
|
+
artifactsDir: loaded.manifest.artifactsRoot,
|
|
255
|
+
depth: spawnedRecord.depth,
|
|
256
|
+
updatedAt: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
259
|
+
try {
|
|
260
|
+
upsertOwnershipEntry(loaded.manifest, entry);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
logInternalError("subagent-tools.identity-link", err, `taskId=${taskId}, attempt=${attempt}`);
|
|
263
|
+
}
|
|
264
|
+
if (resolveEntryBySubagentId(loaded.manifest, spawnedRecord.id)?.taskId === taskId) break;
|
|
265
|
+
if (attempt < 4) await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch (err) {
|
|
270
|
+
logInternalError("subagent-tools.identity-link", err, `runId=${runId ?? "(unknown)"}`);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
165
274
|
const record = subagentManager.spawn(spawnOptions, runner, spawnOptions.background ? undefined : signal);
|
|
275
|
+
spawnedRecord = record;
|
|
166
276
|
// Rule 1: register batch membership so completions can be coalesced.
|
|
167
277
|
if (spawnOptions.batchId && spawnOptions.background) {
|
|
168
278
|
options.batchBarrier?.register(spawnOptions.batchId, record.id, { description: record.description, type: record.type });
|
|
@@ -365,28 +475,127 @@ export function registerSubagentTools(
|
|
|
365
475
|
name: "steer_subagent",
|
|
366
476
|
label: "Steer Agent",
|
|
367
477
|
description:
|
|
368
|
-
"Send a steering note to a running pi-crew subagent.
|
|
478
|
+
"Send a steering note to a running pi-crew subagent. Resolves the subagent record → ownership map → appends artifacts/steering/<taskId>.jsonl, which the child worker polls at its next turn boundary. Use team action=steer (runId+taskId+message) for run-level steering, or team cancel for interruption.",
|
|
369
479
|
parameters: Type.Object({
|
|
370
480
|
agent_id: Type.String(),
|
|
371
481
|
message: Type.String(),
|
|
372
482
|
}) as never,
|
|
373
483
|
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
374
484
|
const p = params as { agent_id?: string; message?: string };
|
|
485
|
+
if (!p.message?.trim()) return subagentToolResult(t("agent.requiresPrompt"), {}, true);
|
|
375
486
|
const record = p.agent_id
|
|
376
487
|
? (subagentManager.getRecord(p.agent_id) ?? readPersistedSubagentRecord(ctx.cwd, p.agent_id))
|
|
377
488
|
: undefined;
|
|
378
489
|
if (!record) return subagentToolResult(t("result.notFound", { id: p.agent_id ?? "" }), {}, true);
|
|
490
|
+
// P2.3: Cross-session ownership check — refuse to steer a record owned by
|
|
491
|
+
// a different session. Legacy records (no ownerSessionId) still pass.
|
|
492
|
+
const currentSessionId = withSessionId(ctx).sessionId;
|
|
493
|
+
if (record.ownerSessionId && record.ownerSessionId !== currentSessionId) {
|
|
494
|
+
return subagentToolResult("Agent belongs to another session.", {}, true);
|
|
495
|
+
}
|
|
496
|
+
// Resolve the live run's taskId + artifactsRoot: prefer the record's
|
|
497
|
+
// taskId link (set by the Agent-tool spawn route), falling back to the
|
|
498
|
+
// per-run ownership map (resolve by subagentId) when the record predates
|
|
499
|
+
// the link.
|
|
500
|
+
let taskId = record.taskId;
|
|
501
|
+
let artifactsRoot: string | undefined;
|
|
502
|
+
let loaded: ReturnType<typeof loadRunManifestById> | undefined;
|
|
503
|
+
if (record.runId) {
|
|
504
|
+
loaded = loadRunManifestById(ctx.cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
505
|
+
if (loaded) {
|
|
506
|
+
artifactsRoot = loaded.manifest.artifactsRoot;
|
|
507
|
+
taskId ??= resolveEntryBySubagentId(loaded.manifest, record.id)?.taskId;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Back-compat: no taskId/link (legacy record or run never dispatched) →
|
|
511
|
+
// the existing "not linked" structured message, NO throw, NO file write.
|
|
512
|
+
if (!taskId || !artifactsRoot) {
|
|
513
|
+
return subagentToolResult(
|
|
514
|
+
[t("steer.unavailable"), record.runId ? t("steer.cancelHint", { runId: record.runId }) : undefined]
|
|
515
|
+
.filter((line): line is string => Boolean(line))
|
|
516
|
+
.join("\n"),
|
|
517
|
+
{
|
|
518
|
+
agentId: record.id,
|
|
519
|
+
runId: record.runId,
|
|
520
|
+
status: record.status,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
// Real steer — append to artifacts/steering/<taskId>.jsonl, same writer util
|
|
525
|
+
// + JSONL schema as `team steer` (handleSteer, team-tool.ts):
|
|
526
|
+
// {type:"steer", message, ts}. The child worker polls this file every
|
|
527
|
+
// 500ms and picks the message up at its next turn boundary.
|
|
528
|
+
// Security review (T1/WP-1 round 1, security 1-3): mirror handleSteer's
|
|
529
|
+
// hardening — (a) T-S1 terminal-task refusal + taskId-belongs-to-manifest
|
|
530
|
+
// validation; (b) cap steering-file growth (unbounded files force a
|
|
531
|
+
// Buffer.alloc(fileSize) on the next worker's first poll and re-deliver
|
|
532
|
+
// every line to each incarnation — see P2 replay finding); (c) no throw.
|
|
533
|
+
if (!loaded) return subagentToolResult("Run manifest unavailable for steer.", {}, true);
|
|
534
|
+
const manifestTask = loaded.tasks.find((t) => t.id === taskId);
|
|
535
|
+
if (!manifestTask) {
|
|
536
|
+
return subagentToolResult(
|
|
537
|
+
`Task '${taskId}' not found in the owning run; cannot steer.`,
|
|
538
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
539
|
+
true,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
if (TEAM_TERMINAL_TASK_STATUSES.has(manifestTask.status)) {
|
|
543
|
+
return subagentToolResult(
|
|
544
|
+
`Task '${taskId}' is ${manifestTask.status}; cannot steer.`,
|
|
545
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
546
|
+
true,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const steeringDir = `${artifactsRoot}/steering`;
|
|
551
|
+
fs.mkdirSync(steeringDir, { recursive: true });
|
|
552
|
+
const safeSteeringPath = resolveRealContainedPath(steeringDir, `${taskId}.jsonl`);
|
|
553
|
+
// Security 3: cap file growth — refuse when the file exceeds a byte
|
|
554
|
+
// threshold (drop-oldest semantics are not feasible for a plain JSONL
|
|
555
|
+
// append; refuse loudly instead of poisoning the next incarnation).
|
|
556
|
+
const MAX_STEERING_BYTES = 256 * 1024;
|
|
557
|
+
let existingBytes = 0;
|
|
558
|
+
try {
|
|
559
|
+
existingBytes = fs.statSync(safeSteeringPath).size;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
562
|
+
}
|
|
563
|
+
const line = JSON.stringify({ type: "steer", message: p.message, ts: new Date().toISOString() }) + "\n";
|
|
564
|
+
if (existingBytes + Buffer.byteLength(line) > MAX_STEERING_BYTES) {
|
|
565
|
+
return subagentToolResult(
|
|
566
|
+
`Steering file for task '${taskId}' has reached its size cap (${(MAX_STEERING_BYTES / 1024).toFixed(0)}KiB); steer refused.`,
|
|
567
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
568
|
+
true,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
fs.appendFileSync(safeSteeringPath, line);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
// B1 battery 2026-08-18 (F7): this catch previously logged at DEBUG
|
|
574
|
+
// level (invisible unless PI_TEAMS_DEBUG) and then STILL returned the
|
|
575
|
+
// success message — a live probe showed "Steer delivered to task …"
|
|
576
|
+
// while the steering file stayed 0 bytes, undiagnosable from the
|
|
577
|
+
// outside. Fail LOUD: error-level log + isError result.
|
|
578
|
+
logInternalError(
|
|
579
|
+
"subagent-tools.steer-write-failed",
|
|
580
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
581
|
+
`taskId=${taskId} runId=${record.runId} artifactsRoot=${artifactsRoot}`,
|
|
582
|
+
"error",
|
|
583
|
+
);
|
|
584
|
+
return subagentToolResult(
|
|
585
|
+
`Steer write failed for task '${taskId}': ${err instanceof Error ? err.message : String(err)} (see pi-crew error log)`,
|
|
586
|
+
{ agentId: record.id, runId: record.runId, taskId, status: record.status },
|
|
587
|
+
true,
|
|
588
|
+
);
|
|
589
|
+
}
|
|
379
590
|
return subagentToolResult(
|
|
380
591
|
[
|
|
381
592
|
t("steer.noted", { id: record.id }),
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
]
|
|
385
|
-
.filter((line): line is string => Boolean(line))
|
|
386
|
-
.join("\n"),
|
|
593
|
+
`Steer delivered to task '${taskId}'; it will be picked up at the next turn boundary.`,
|
|
594
|
+
].join("\n"),
|
|
387
595
|
{
|
|
388
596
|
agentId: record.id,
|
|
389
597
|
runId: record.runId,
|
|
598
|
+
taskId,
|
|
390
599
|
status: record.status,
|
|
391
600
|
},
|
|
392
601
|
);
|
|
@@ -410,7 +619,7 @@ export function registerSubagentTools(
|
|
|
410
619
|
...steerSubagentTool,
|
|
411
620
|
name: "crew_agent_steer",
|
|
412
621
|
label: "Steer Crew Agent",
|
|
413
|
-
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name.",
|
|
622
|
+
description: "Send a steering note to a pi-crew subagent using the conflict-safe tool name (same behavior as steer_subagent).",
|
|
414
623
|
};
|
|
415
624
|
const toolConfig = loadConfig(process.cwd()).config.tools;
|
|
416
625
|
const enableSteer = toolConfig?.enableSteer !== false;
|
|
@@ -13,6 +13,7 @@ import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
|
13
13
|
import { statusIcon, type ToolRenderContext, teamToolRenderer } from "../../ui/tool-renderers/index.ts";
|
|
14
14
|
import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
15
15
|
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
16
|
+
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
16
17
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
17
18
|
// Team tool handler — lazy-loaded because team-tool.ts imports many modules
|
|
18
19
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
@@ -174,7 +175,10 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
174
175
|
const controller = new AbortController();
|
|
175
176
|
const toolKey = Symbol();
|
|
176
177
|
deps.foregroundControllers.set(toolKey, controller);
|
|
177
|
-
|
|
178
|
+
// safe-abort: pi's tool-call abort can race a completed run whose
|
|
179
|
+
// spawned children already exited — Node's child_process signal
|
|
180
|
+
// listener throws AbortError out of abort() and kills the host.
|
|
181
|
+
const abort = (): void => safeAbort(controller, "team-tool.caller-abort");
|
|
178
182
|
signal?.addEventListener("abort", abort, { once: true });
|
|
179
183
|
const stopProgress = startTeamToolProgressBinder(onUpdate as OnUpdate | undefined);
|
|
180
184
|
try {
|
|
@@ -15,7 +15,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
15
15
|
import { loadConfig } from "../../config/config.ts";
|
|
16
16
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
17
17
|
import type { LiveRunSidebar as LiveRunSidebarType } from "../../ui/live-run-sidebar.ts";
|
|
18
|
-
import { requestRender, setExtensionWidget, showCustom } from "../../ui/pi-ui-compat.ts";
|
|
18
|
+
import { requestRender, setExtensionWidget, showCustom, toPiWidgetPlacement } from "../../ui/pi-ui-compat.ts";
|
|
19
19
|
import {
|
|
20
20
|
clearPiCrewPowerbar,
|
|
21
21
|
registerPiCrewPowerbarSegments,
|
|
@@ -76,10 +76,11 @@ export async function installLiveSidebar(ctx: ExtensionContext, runId: string, s
|
|
|
76
76
|
state.liveSidebarRunId = runId;
|
|
77
77
|
state.dashboardOpened = true;
|
|
78
78
|
const widgetPlacement = uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
|
|
79
|
-
|
|
80
|
-
setExtensionWidget(ctx, "pi-crew
|
|
79
|
+
const placement = toPiWidgetPlacement(widgetPlacement);
|
|
80
|
+
setExtensionWidget(ctx, "pi-crew", undefined, { placement });
|
|
81
|
+
setExtensionWidget(ctx, "pi-crew-active", undefined, { placement });
|
|
81
82
|
deps.widgetState.lastVisibility = "hidden";
|
|
82
|
-
deps.widgetState.lastPlacement =
|
|
83
|
+
deps.widgetState.lastPlacement = placement;
|
|
83
84
|
deps.widgetState.lastKey = "pi-crew-active";
|
|
84
85
|
deps.widgetState.model = undefined;
|
|
85
86
|
const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { getCrewEnv } from "../config/env-vars.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* HMAC-based origin signing for cross-extension RPC.
|
|
@@ -23,7 +24,7 @@ const SIGNATURE_VALIDITY_MS = 10 * 60 * 1000; // 10 min
|
|
|
23
24
|
|
|
24
25
|
/** Get the shared secret. Returns undefined if not configured. */
|
|
25
26
|
export function getRpcSecret(): string | undefined {
|
|
26
|
-
return
|
|
27
|
+
return getCrewEnv(SECRET_ENV_VAR);
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
/** Set the shared secret (for testing or programmatic setup). */
|
|
@@ -38,7 +39,8 @@ export function clearRpcSecret(): void {
|
|
|
38
39
|
|
|
39
40
|
/** Whether HMAC authentication is currently enabled. */
|
|
40
41
|
export function isHmacEnabled(): boolean {
|
|
41
|
-
|
|
42
|
+
const secret = getCrewEnv(SECRET_ENV_VAR);
|
|
43
|
+
return typeof secret === "string" && secret.length > 0;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// --- Types -------------------------------------------------------------------
|
|
@@ -142,7 +144,7 @@ export function withHmacVerification<P extends { requestId: string }>(
|
|
|
142
144
|
// Emit a soft info note (once per process) only when an actual RPC
|
|
143
145
|
// request is being processed — the prior registration-time warning
|
|
144
146
|
// was too noisy for users who never wire cross-extension RPC.
|
|
145
|
-
if (
|
|
147
|
+
if (getCrewEnv("PI_CREW_SUPPRESS_RPC_WARNING") !== "1") {
|
|
146
148
|
rpcHmacSoftInfoOnce();
|
|
147
149
|
}
|
|
148
150
|
return handler(params);
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
|
|
9
|
+
import { isTerminalTaskStatus } from "../../../state/contracts.ts";
|
|
9
10
|
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
10
11
|
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
11
|
-
import { saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
12
|
+
import { loadRunManifestById, saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
13
|
+
import { RUN_NOT_FOUND_HINT } from "../run-not-found.ts";
|
|
12
14
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
13
15
|
|
|
14
16
|
export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
@@ -31,28 +33,63 @@ export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
|
31
33
|
);
|
|
32
34
|
try {
|
|
33
35
|
return withRunLockSync(loaded.manifest, () => {
|
|
36
|
+
// R13-S1: fresh re-read INSIDE the lock (respond.ts:43 canonical pattern).
|
|
37
|
+
const fresh = loadRunManifestById(loaded.manifest.cwd, loaded.manifest.runId);
|
|
38
|
+
if (!fresh)
|
|
39
|
+
return result(
|
|
40
|
+
`Run '${loaded.manifest.runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
41
|
+
{
|
|
42
|
+
action: "api",
|
|
43
|
+
status: "error",
|
|
44
|
+
runId: loaded.manifest.runId,
|
|
45
|
+
},
|
|
46
|
+
true,
|
|
47
|
+
);
|
|
48
|
+
const freshTask = fresh.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
49
|
+
if (!freshTask)
|
|
50
|
+
return result(
|
|
51
|
+
`Task '${taskId}' not found in run '${fresh.manifest.runId}'.`,
|
|
52
|
+
{
|
|
53
|
+
action: "api",
|
|
54
|
+
status: "error",
|
|
55
|
+
runId: fresh.manifest.runId,
|
|
56
|
+
},
|
|
57
|
+
true,
|
|
58
|
+
);
|
|
59
|
+
// R13-S1: never resurrect a task that became terminal on disk — a completed/
|
|
60
|
+
// failed task must not be flipped back to running by a stale heartbeat.
|
|
61
|
+
if (isTerminalTaskStatus(freshTask.status))
|
|
62
|
+
return result(
|
|
63
|
+
`Cannot write heartbeat for task '${freshTask.id}': task is in terminal state '${freshTask.status}'.`,
|
|
64
|
+
{
|
|
65
|
+
action: "api",
|
|
66
|
+
status: "error",
|
|
67
|
+
runId: fresh.manifest.runId,
|
|
68
|
+
},
|
|
69
|
+
true,
|
|
70
|
+
);
|
|
34
71
|
const heartbeat = touchWorkerHeartbeat(
|
|
35
|
-
|
|
36
|
-
workerId:
|
|
72
|
+
freshTask.heartbeat ?? {
|
|
73
|
+
workerId: freshTask.id,
|
|
37
74
|
lastSeenAt: new Date().toISOString(),
|
|
38
75
|
},
|
|
39
76
|
{
|
|
40
77
|
alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined,
|
|
41
78
|
},
|
|
42
79
|
);
|
|
43
|
-
const tasks =
|
|
44
|
-
saveRunTasks(
|
|
45
|
-
appendEvent(
|
|
80
|
+
const tasks = fresh.tasks.map((item) => (item.id === freshTask.id ? { ...item, heartbeat } : item));
|
|
81
|
+
saveRunTasks(fresh.manifest, tasks);
|
|
82
|
+
appendEvent(fresh.manifest.eventsPath, {
|
|
46
83
|
type: "worker.heartbeat",
|
|
47
|
-
runId:
|
|
48
|
-
taskId:
|
|
84
|
+
runId: fresh.manifest.runId,
|
|
85
|
+
taskId: freshTask.id,
|
|
49
86
|
data: { ...heartbeat },
|
|
50
87
|
});
|
|
51
88
|
return result(JSON.stringify(heartbeat, null, 2), {
|
|
52
89
|
action: "api",
|
|
53
90
|
status: "ok",
|
|
54
|
-
runId:
|
|
55
|
-
artifactsRoot:
|
|
91
|
+
runId: fresh.manifest.runId,
|
|
92
|
+
artifactsRoot: fresh.manifest.artifactsRoot,
|
|
56
93
|
});
|
|
57
94
|
});
|
|
58
95
|
} catch (error) {
|
|
@@ -9,6 +9,7 @@ import { terminateLiveAgentsForRun } from "../../../runtime/live-session/live-ag
|
|
|
9
9
|
import { currentCrewRole, permissionForRole } from "../../../runtime/role-permission.ts";
|
|
10
10
|
import { withRunLock } from "../../../state/coordination/locks.ts";
|
|
11
11
|
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
12
|
+
import { getCurrentPlanRecord, setPlanApproval } from "../../../state/stores/plan-store.ts";
|
|
12
13
|
import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatus } from "../../../state/stores/state-store.ts";
|
|
13
14
|
import { logInternalError } from "../../../utils/internal-error.ts";
|
|
14
15
|
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
@@ -63,6 +64,10 @@ export const handleApprovePlan: ApiOperationHandler = async (hctx) => {
|
|
|
63
64
|
},
|
|
64
65
|
};
|
|
65
66
|
await saveRunManifestAsync(manifest);
|
|
67
|
+
// T2/R4 (ADR-4 §8): dual-write the record side — approval names the
|
|
68
|
+
// plan id+version. Pre-v2 runs without a PlanRecord skip silently.
|
|
69
|
+
const currentRecord = getCurrentPlanRecord(manifest);
|
|
70
|
+
if (currentRecord) setPlanApproval(manifest, { status: "approved", planVersion: currentRecord.version, by: "api" });
|
|
66
71
|
appendEvent(manifest.eventsPath, {
|
|
67
72
|
type: "plan.approved",
|
|
68
73
|
runId: manifest.runId,
|
|
@@ -140,6 +145,10 @@ export const handleCancelPlan: ApiOperationHandler = async (hctx) => {
|
|
|
140
145
|
},
|
|
141
146
|
};
|
|
142
147
|
await saveRunManifestAsync(manifest);
|
|
148
|
+
// T2/R4 (ADR-4 §8 vocabulary mapping): manifest gets "cancelled", the
|
|
149
|
+
// record side gets "rejected" (isPlanApprovalDenied accepts both).
|
|
150
|
+
const denyRecord = getCurrentPlanRecord(manifest);
|
|
151
|
+
if (denyRecord) setPlanApproval(manifest, { status: "rejected", planVersion: denyRecord.version, by: "api" });
|
|
143
152
|
saveRunTasks(manifest, tasks);
|
|
144
153
|
appendEvent(manifest.eventsPath, {
|
|
145
154
|
type: "plan.cancelled",
|