pi-crew 0.9.40 → 0.9.42
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 +140 -0
- package/dist/build-meta.json +7089 -6728
- package/dist/index.mjs +45845 -45192
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/optimization-plan-2026-07.md +366 -0
- package/docs/phase4-triage.md +58 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/config/config.ts +19 -0
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +18 -5
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +2 -1
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +210 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +64 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/runtime/adaptive-plan.ts +8 -8
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi.ts +157 -71
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-loop-runner.ts +6 -2
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/heartbeat-watcher.ts +5 -0
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +12 -32
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +2 -2
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +0 -13
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +1 -1
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +3 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +3 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { errors } from "../errors.ts";
|
|
5
|
-
import { atomicWriteJson } from "../state/atomic-write.ts";
|
|
5
|
+
import { atomicWriteFile, atomicWriteJson } from "../state/atomic-write.ts";
|
|
6
6
|
import { saveRunManifest } from "../state/state-store.ts";
|
|
7
7
|
import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
8
8
|
import { recordFromTask, upsertCrewAgent } from "./crew-agent-records.ts";
|
|
@@ -152,29 +152,32 @@ function checkPidLiveness(
|
|
|
152
152
|
}
|
|
153
153
|
// If process is alive per kill(0), we're done.
|
|
154
154
|
if (liveness.alive) return { alive: true, detail: liveness.detail };
|
|
155
|
-
// Process is dead per kill(0).
|
|
155
|
+
// Process is dead per kill(0). ESRCH is AUTHORITATIVE — the PID does not exist.
|
|
156
|
+
// A frozen/zombie process still has a PID (kill(0) returns 0, or EPERM if we lack
|
|
157
|
+
// permission to signal it), so reaching here with alive=false means the process
|
|
158
|
+
// is genuinely gone. PID recycling is already handled above via the startTime
|
|
159
|
+
// TOCTOU check, so the heartbeat file must NOT override the dead verdict.
|
|
160
|
+
//
|
|
161
|
+
// HISTORY: a <5min-old heartbeat.json previously OVERRRODE this verdict (returned
|
|
162
|
+
// alive:true "process dead but heartbeat Xs old"), delaying repair by ~5min.
|
|
163
|
+
// Combined with the 60s reconcile cadence this produced a ~300-360s detection lag
|
|
164
|
+
// for dead workers (observed 353s in production). The heartbeat is now used only
|
|
165
|
+
// to enrich the detail string, never to override the authoritative kill(0) verdict.
|
|
166
|
+
let heartbeatNote = "";
|
|
156
167
|
if (stateRoot) {
|
|
157
|
-
const heartbeatPath = path.join(stateRoot, "heartbeat.json");
|
|
158
168
|
try {
|
|
169
|
+
const heartbeatPath = path.join(stateRoot, "heartbeat.json");
|
|
159
170
|
if (fs.existsSync(heartbeatPath)) {
|
|
160
171
|
const hb = JSON.parse(fs.readFileSync(heartbeatPath, "utf-8")) as { pid?: number; at?: number };
|
|
161
172
|
if (hb?.pid === pid && hb?.at) {
|
|
162
|
-
|
|
163
|
-
// Heartbeat written < 5 min ago → process was alive recently.
|
|
164
|
-
// Don't repair yet; let the next reconciliation cycle catch it.
|
|
165
|
-
if (ageMs < 5 * 60_000) {
|
|
166
|
-
return {
|
|
167
|
-
alive: true,
|
|
168
|
-
detail: `process dead but heartbeat ${Math.round(ageMs / 1000)}s old`,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
173
|
+
heartbeatNote = ` (heartbeat was ${Math.round((Date.now() - hb.at) / 1000)}s old)`;
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
} catch {
|
|
174
177
|
/* ignore — best-effort */
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
|
-
return { alive:
|
|
180
|
+
return { alive: false, detail: `${liveness.detail}${heartbeatNote}` };
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
/**
|
|
@@ -267,11 +270,6 @@ function buildStaleReconcileError(task: TeamTaskState, reason: string): Error {
|
|
|
267
270
|
return errors.runStale(reason, heartbeatAgeSeconds);
|
|
268
271
|
}
|
|
269
272
|
|
|
270
|
-
/** @deprecated use buildStaleReconcileError (returns a structured CrewError). Kept for any external callers. */
|
|
271
|
-
function formatStaleReconcileError(task: TeamTaskState, reason: string): string {
|
|
272
|
-
return buildStaleReconcileError(task, reason).message;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
273
|
function repairStaleRun(manifest: TeamRunManifest, tasks: TeamTaskState[], reason: string): TeamTaskState[] {
|
|
276
274
|
const now = new Date().toISOString();
|
|
277
275
|
const repairedTasks = tasks.map((task) => {
|
|
@@ -592,9 +590,13 @@ export function reconcileOrphanedTempWorkspaces(
|
|
|
592
590
|
if (!fs.existsSync(workspaceDir)) {
|
|
593
591
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
594
592
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
593
|
+
// Atomic exclusive-create: reserve the sentinel (fails with EEXIST if
|
|
594
|
+
// another cleanup is in progress), then fill it atomically. The brief
|
|
595
|
+
// empty-file window is safe — the sentinel "exists" (correctly
|
|
596
|
+
// signalling in-progress cleanup).
|
|
597
|
+
const sentinelFd = fs.openSync(sentinelPath, "wx");
|
|
598
|
+
fs.closeSync(sentinelFd);
|
|
599
|
+
atomicWriteFile(sentinelPath, JSON.stringify({ startedAt: now }));
|
|
598
600
|
} catch {
|
|
599
601
|
// Sentinel already exists (another cleanup in progress) — skip
|
|
600
602
|
canCleanup = false;
|
|
@@ -661,38 +663,6 @@ export function reconcileOrphanedTempWorkspaces(
|
|
|
661
663
|
} catch {
|
|
662
664
|
/* ignore if already gone */
|
|
663
665
|
}
|
|
664
|
-
} else if (canCleanup) {
|
|
665
|
-
// Sentinel already existed — another cleanup attempt is in progress.
|
|
666
|
-
// Perform a simplified TOCTOU check: if any manifest is running,
|
|
667
|
-
// do NOT remove the sentinel (let the first attempt finish its scan).
|
|
668
|
-
let hasRunningManifest = false;
|
|
669
|
-
if (fs.existsSync(stateRunsDir)) {
|
|
670
|
-
try {
|
|
671
|
-
for (const runDir of fs.readdirSync(stateRunsDir)) {
|
|
672
|
-
const manifestPath = path.join(stateRunsDir, runDir, "manifest.json");
|
|
673
|
-
if (!fs.existsSync(manifestPath)) continue;
|
|
674
|
-
try {
|
|
675
|
-
const m = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as TeamRunManifest;
|
|
676
|
-
if (m.status === "running") {
|
|
677
|
-
hasRunningManifest = true;
|
|
678
|
-
break;
|
|
679
|
-
}
|
|
680
|
-
} catch {
|
|
681
|
-
/* skip on parse error */
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
} catch {
|
|
685
|
-
/* skip if runs dir unreadable */
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
// Only clean up sentinel if no running manifests exist
|
|
689
|
-
if (!hasRunningManifest) {
|
|
690
|
-
try {
|
|
691
|
-
fs.unlinkSync(sentinelPath);
|
|
692
|
-
} catch {
|
|
693
|
-
/* ignore if already gone */
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
666
|
}
|
|
697
667
|
}
|
|
698
668
|
} catch {
|
|
@@ -439,13 +439,29 @@ export class SubagentManager {
|
|
|
439
439
|
private drainQueue(): void {
|
|
440
440
|
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
441
441
|
const next = this.queue.shift();
|
|
442
|
-
if (
|
|
442
|
+
if (next?.record.status !== "queued") continue;
|
|
443
443
|
this.start(next.record, next.options, next.runner, next.signal);
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
private async pollRunToTerminal(cwd: string, record: SubagentRecord): Promise<void> {
|
|
448
|
+
// Safety: max 30 minutes (1800 polls at 1s interval) to prevent infinite
|
|
449
|
+
// polling if manifest file is deleted or run state becomes unrecoverable.
|
|
450
|
+
const MAX_POLL_COUNT = 1800;
|
|
451
|
+
let pollCount = 0;
|
|
448
452
|
while (record.runId && (record.status === "running" || record.status === "blocked")) {
|
|
453
|
+
if (++pollCount > MAX_POLL_COUNT) {
|
|
454
|
+
logInternalError(
|
|
455
|
+
"subagent-manager.poll-timeout",
|
|
456
|
+
new Error(`pollRunToTerminal exceeded ${MAX_POLL_COUNT} polls for runId=${record.runId}`),
|
|
457
|
+
`id=${record.id}`,
|
|
458
|
+
);
|
|
459
|
+
record.status = "error";
|
|
460
|
+
record.error = `Poll timeout: run did not reach terminal state after ${MAX_POLL_COUNT} seconds`;
|
|
461
|
+
record.completedAt = Date.now();
|
|
462
|
+
savePersistedSubagentRecord(cwd, record);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
449
465
|
const loaded = loadRunManifestById(cwd, record.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
450
466
|
if (!loaded) {
|
|
451
467
|
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
@@ -488,7 +504,7 @@ export class SubagentManager {
|
|
|
488
504
|
private scheduleBlockedTerminalPoll(cwd: string, record: SubagentRecord): void {
|
|
489
505
|
const poll = (): void => {
|
|
490
506
|
const current = this.records.get(record.id);
|
|
491
|
-
if (
|
|
507
|
+
if (current?.status !== "blocked" || !current.runId) return;
|
|
492
508
|
const loaded = loadRunManifestById(cwd, current.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
493
509
|
if (
|
|
494
510
|
!loaded ||
|
|
@@ -524,7 +540,7 @@ export class SubagentManager {
|
|
|
524
540
|
const threshold = DEFAULT_SUBAGENT.stuckBlockedNotifyMs;
|
|
525
541
|
const fire = (): void => {
|
|
526
542
|
const current = this.records.get(record.id);
|
|
527
|
-
if (
|
|
543
|
+
if (current?.status !== "blocked" || !current.blockedAt || current.stuckNotified) return;
|
|
528
544
|
current.stuckNotified = true;
|
|
529
545
|
this.onEvent?.("subagent.stuck-blocked", {
|
|
530
546
|
event: "subagent.stuck-blocked",
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { DEFAULT_OUTPUT_CONTEXT } from "../config/defaults.ts";
|
|
4
4
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
5
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
5
6
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
6
7
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
7
8
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
@@ -155,7 +156,7 @@ export function teePathForArtifact(artifactsRoot: string, taskId: string, artifa
|
|
|
155
156
|
function writeTeeFile(fullOutputPath: string, content: string): boolean {
|
|
156
157
|
try {
|
|
157
158
|
fs.mkdirSync(path.dirname(fullOutputPath), { recursive: true });
|
|
158
|
-
|
|
159
|
+
atomicWriteFile(fullOutputPath, content);
|
|
159
160
|
return true;
|
|
160
161
|
} catch {
|
|
161
162
|
return false;
|
|
@@ -14,7 +14,7 @@ import { renderSuggestedFilesSection, runRetrievalCycle } from "./retrieval-orch
|
|
|
14
14
|
* filtering at the Pi level is a future optimisation (Phase 3.2+).
|
|
15
15
|
*/
|
|
16
16
|
export function toolGuidanceBlock(agent?: AgentConfig): string {
|
|
17
|
-
if (
|
|
17
|
+
if (agent?.loadMode !== "lean" || !agent.defaultTools?.length) return "";
|
|
18
18
|
return [
|
|
19
19
|
"# Tool Guidance",
|
|
20
20
|
`This role uses a focused tool set. Preferred tools: ${agent.defaultTools.join(", ")}.`,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MailboxMessage } from "../../state/mailbox.ts";
|
|
2
2
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
3
|
+
import { sanitizeTaskText } from "../task-packet.ts";
|
|
3
4
|
|
|
4
5
|
export interface RunProjectionSource {
|
|
5
6
|
kind: "events" | "mailbox" | "artifacts" | "ui_metadata" | "runtime_metadata";
|
|
@@ -64,7 +65,8 @@ export function transformRunContextBeforeWorkerStart(input: {
|
|
|
64
65
|
});
|
|
65
66
|
lines.push(`Pending messages (${messages.length}/${input.pendingMailbox.length}):`);
|
|
66
67
|
for (const msg of messages) {
|
|
67
|
-
|
|
68
|
+
const safeBody = sanitizeTaskText(msg.body.slice(0, 100));
|
|
69
|
+
lines.push(`- ${msg.kind ?? "message"}: ${safeBody}`);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -24,6 +24,14 @@ export function updateTask(tasks: TeamTaskState[], updated: TeamTaskState): Team
|
|
|
24
24
|
* committed first; retry with the fresh state. This is O(retry) under contention but
|
|
25
25
|
* converges in the normal single-writer case.
|
|
26
26
|
*
|
|
27
|
+
* ST-7: pass `skipCoalesce: true` when persisting a TERMINAL task status
|
|
28
|
+
* (completed/failed/cancelled/needs_attention/skipped). Without this the
|
|
29
|
+
* underlying `saveRunTasksCoalesced` would buffer the write for 50ms; a
|
|
30
|
+
* SIGKILL in that window loses the terminal update and crash recovery
|
|
31
|
+
* would see "running" in tasks.json while events.jsonl already shows
|
|
32
|
+
* "completed". For non-terminal transitions (heartbeat, progress) the
|
|
33
|
+
* default buffered write is fine and matches prior behavior.
|
|
34
|
+
*
|
|
27
35
|
* @param checkpointPhase - Optional checkpoint phase to include in the task state alongside the update.
|
|
28
36
|
*/
|
|
29
37
|
export function persistSingleTaskUpdate(
|
|
@@ -31,6 +39,7 @@ export function persistSingleTaskUpdate(
|
|
|
31
39
|
fallbackTasks: TeamTaskState[],
|
|
32
40
|
updated: TeamTaskState,
|
|
33
41
|
checkpointPhase?: TaskCheckpointState["phase"],
|
|
42
|
+
skipCoalesce: boolean = false,
|
|
34
43
|
): TeamTaskState[] {
|
|
35
44
|
const MAX_CAS_ATTEMPTS = 100;
|
|
36
45
|
let baseMtime = 0;
|
|
@@ -126,7 +135,10 @@ export function persistSingleTaskUpdate(
|
|
|
126
135
|
// non-coalesced writers; the flushPendingAtomicWrites() guard at
|
|
127
136
|
// the top of the retry loop ensures reads see any other coalesced
|
|
128
137
|
// writer's flushed-before-this-call state.
|
|
129
|
-
|
|
138
|
+
// ST-7: terminal transitions (skipCoalesce=true) bypass the 50ms
|
|
139
|
+
// coalesce window so a SIGKILL after the persist completes cannot
|
|
140
|
+
// leave tasks.json stale with a non-terminal status.
|
|
141
|
+
saveRunTasksCoalesced(manifest, merged, skipCoalesce);
|
|
130
142
|
} catch (err) {
|
|
131
143
|
logInternalError("persistSingleTaskUpdate", err, undefined, "error");
|
|
132
144
|
throw err;
|
|
@@ -15,11 +15,10 @@ import type {
|
|
|
15
15
|
OperationTerminalEvidence,
|
|
16
16
|
TeamRunManifest,
|
|
17
17
|
TeamTaskState,
|
|
18
|
-
UsageState,
|
|
19
18
|
VerificationEvidence,
|
|
20
19
|
} from "../state/types.ts";
|
|
21
20
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
22
|
-
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
21
|
+
import { resolveContainedPath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
23
22
|
import type { WorkflowStep } from "../workflows/workflow-config.ts";
|
|
24
23
|
import { captureWorktreeDiffAsync, captureWorktreeDiffStatAsync, prepareTaskWorkspaceAsync } from "../worktree/worktree-manager.ts";
|
|
25
24
|
import { reserveControlChannel } from "./agent-control.ts";
|
|
@@ -35,7 +34,7 @@ import {
|
|
|
35
34
|
recordFromTask,
|
|
36
35
|
upsertCrewAgent,
|
|
37
36
|
} from "./crew-agent-records.ts";
|
|
38
|
-
import type {
|
|
37
|
+
import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
|
|
39
38
|
import { crewHooks } from "./crew-hooks.ts";
|
|
40
39
|
import { bridgeEventFromJsonEvent, registerStreamBridge } from "./event-stream-bridge.ts";
|
|
41
40
|
import { createVerificationEvidence } from "./green-contract.ts";
|
|
@@ -87,7 +86,7 @@ registerYieldTool();
|
|
|
87
86
|
async function appendSteeringAsync(steeringDir: string, taskId: string, steers: string[]): Promise<void> {
|
|
88
87
|
try {
|
|
89
88
|
await fs.promises.mkdir(steeringDir, { recursive: true });
|
|
90
|
-
const steeringPath = `${
|
|
89
|
+
const steeringPath = resolveContainedPath(steeringDir, `${taskId}.jsonl`);
|
|
91
90
|
const lines = steers
|
|
92
91
|
.map(
|
|
93
92
|
(msg) =>
|
|
@@ -254,10 +253,18 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
254
253
|
if (input.step.preStepScript) {
|
|
255
254
|
const scriptTimeout = input.step.preStepTimeout ?? 30_000;
|
|
256
255
|
const scriptArgs = input.step.preStepArgs ?? [];
|
|
256
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
257
|
+
type: "hook.pre_step_started",
|
|
258
|
+
runId: manifest.runId,
|
|
259
|
+
taskId: task.id,
|
|
260
|
+
data: { script: input.step.preStepScript, argCount: scriptArgs.length, timeoutMs: scriptTimeout },
|
|
261
|
+
});
|
|
257
262
|
// SECURITY (M-1 fix, code-review 2026-06-23): use the project's safe-path
|
|
258
263
|
// primitive instead of a hand-rolled path.resolve + startsWith check.
|
|
259
264
|
// The lexical check passed a symlinked ancestor, letting execFileSync
|
|
260
265
|
// follow it and execute a script outside cwd. Throws on escape.
|
|
266
|
+
// Keep validation outside the optional-execution catch: preStepOptional
|
|
267
|
+
// must never bypass a path-containment failure.
|
|
261
268
|
resolveRealContainedPath(manifest.cwd, input.step.preStepScript);
|
|
262
269
|
try {
|
|
263
270
|
// LAZY: defer dynamic import of node:child_process to its call site.
|
|
@@ -268,9 +275,22 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
268
275
|
cwd: manifest.cwd,
|
|
269
276
|
maxBuffer: 1024 * 1024, // 1MB cap
|
|
270
277
|
});
|
|
278
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
279
|
+
type: "hook.pre_step_completed",
|
|
280
|
+
runId: manifest.runId,
|
|
281
|
+
taskId: task.id,
|
|
282
|
+
data: { script: input.step.preStepScript, outputBytes: Buffer.byteLength(preStepOutput, "utf8") },
|
|
283
|
+
});
|
|
271
284
|
} catch (err) {
|
|
272
285
|
const msg = err instanceof Error ? err.message : String(err);
|
|
273
286
|
const exitCode = (err as NodeJS.ErrnoException & { status?: number }).status;
|
|
287
|
+
appendEventFireAndForget(manifest.eventsPath, {
|
|
288
|
+
type: "hook.pre_step_failed",
|
|
289
|
+
runId: manifest.runId,
|
|
290
|
+
taskId: task.id,
|
|
291
|
+
message: `pre-step hook failed (exit ${exitCode ?? "?"})`,
|
|
292
|
+
data: { script: input.step.preStepScript, exitCode: exitCode ?? null, optional: input.step.preStepOptional === true },
|
|
293
|
+
});
|
|
274
294
|
// E1 (Round 15): structured CrewError with code E009 + help hint,
|
|
275
295
|
// instead of a raw Error. Surfaces the script path, exit code, and stderr.
|
|
276
296
|
// Round 21 (E4): if preStepOptional is set, a failing hook is NON-FATAL.
|
|
@@ -484,7 +504,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
484
504
|
runId: manifest.runId,
|
|
485
505
|
agentId: task.id,
|
|
486
506
|
artifactsRoot: manifest.artifactsRoot,
|
|
487
|
-
steeringFile: `${manifest.artifactsRoot}/steering
|
|
507
|
+
steeringFile: resolveContainedPath(`${manifest.artifactsRoot}/steering`, `${task.id}.jsonl`),
|
|
488
508
|
onSpawn: (pid) => {
|
|
489
509
|
try {
|
|
490
510
|
({ task, tasks } = checkpointTask(manifest, tasks, task, "child-spawned", pid));
|
|
@@ -1222,9 +1242,14 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
1222
1242
|
// stale manifest and overwrite this task's freshly-written artifacts, silently
|
|
1223
1243
|
// losing them. persistSingleTaskUpdate is re-entrance-safe (runLockHeldByUs guard),
|
|
1224
1244
|
// so nesting it inside this lock is a no-op re-acquire, not a deadlock.
|
|
1245
|
+
// ST-7: this is a terminal transition (task.status is completed/failed/needs_attention),
|
|
1246
|
+
// pass skipCoalesce=true so the terminal update is durable — a SIGKILL in the
|
|
1247
|
+
// 50ms coalesce window must NOT leave tasks.json showing the prior non-terminal
|
|
1248
|
+
// status, otherwise crash recovery would see inconsistency between tasks.json
|
|
1249
|
+
// (running) and events.jsonl (terminal).
|
|
1225
1250
|
tasks = await withRunLock(manifest, async () => {
|
|
1226
1251
|
await saveRunManifestAsync(manifest);
|
|
1227
|
-
return persistSingleTaskUpdate(manifest, tasks, task);
|
|
1252
|
+
return persistSingleTaskUpdate(manifest, tasks, task, undefined, true);
|
|
1228
1253
|
});
|
|
1229
1254
|
upsertCrewAgent(manifest, recordFromTask(manifest, task, runtimeKind));
|
|
1230
1255
|
// Execute task_result hook before emitting terminal event
|
|
@@ -9,6 +9,7 @@ import type { MetricRegistry } from "../observability/metric-registry.ts";
|
|
|
9
9
|
import { PluginRegistry } from "../plugins/plugin-registry.ts";
|
|
10
10
|
import { NextJsPlugin, VitePlugin, VitestPlugin } from "../plugins/plugins/index.ts";
|
|
11
11
|
import { hashArtifactContent as hashContent, writeArtifact } from "../state/artifact-store.ts";
|
|
12
|
+
import { atomicWriteFile } from "../state/atomic-write.ts";
|
|
12
13
|
import { appendEvent, appendEventAsync, appendEventBuffered, appendEventFireAndForget, flushEventLogBuffer } from "../state/event-log.ts";
|
|
13
14
|
import { HealthStore } from "../state/health-store.ts";
|
|
14
15
|
import { withRunLock } from "../state/locks.ts";
|
|
@@ -84,7 +85,7 @@ function startTeamRunHeartbeat(stateRoot: string, runId: string): () => void {
|
|
|
84
85
|
// captured manifest.updatedAt once at startup, making the value
|
|
85
86
|
// permanently stale throughout the run.
|
|
86
87
|
const now = new Date().toISOString();
|
|
87
|
-
|
|
88
|
+
atomicWriteFile(
|
|
88
89
|
heartbeatPath,
|
|
89
90
|
JSON.stringify({
|
|
90
91
|
pid: process.pid,
|
|
@@ -93,7 +94,7 @@ function startTeamRunHeartbeat(stateRoot: string, runId: string): () => void {
|
|
|
93
94
|
kind: "team-runner",
|
|
94
95
|
lastTaskUpdateAt: now,
|
|
95
96
|
}),
|
|
96
|
-
{
|
|
97
|
+
{ mode: 0o600 },
|
|
97
98
|
);
|
|
98
99
|
} catch {
|
|
99
100
|
// best-effort
|
|
@@ -13,7 +13,7 @@ import { spawn } from "node:child_process";
|
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { writeArtifact } from "../state/artifact-store.ts";
|
|
16
|
-
import type {
|
|
16
|
+
import type { GreenLevel, VerificationCommandResult, VerificationContract } from "../state/types.ts";
|
|
17
17
|
import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
|
|
18
18
|
import { sanitizeEnvSecrets } from "../utils/env-filter.ts";
|
|
19
19
|
import { redactSecretString } from "../utils/redaction.ts";
|
|
@@ -154,13 +154,61 @@ export const CARGO_RUST_GATES: Array<{
|
|
|
154
154
|
// `npm test > ~/.ssh/authorized_keys`) and used char-class semantics instead of
|
|
155
155
|
// a lookahead. Now `[<>](?![&\d])` blocks BOTH `<` and `>` file redirects while
|
|
156
156
|
// still permitting fd-duplication forms (`2>&1`, `1>&2`, `>&2`, `<&3`) via the
|
|
157
|
-
// negative lookahead (a `>`/`<` followed by `&` or a digit is allowed).
|
|
158
|
-
|
|
157
|
+
// negative lookahead (a `>`/`<` followed by `&` or a digit is allowed). A bare
|
|
158
|
+
// `&` is also blocked so an allowed gate cannot background itself and start a
|
|
159
|
+
// second command.
|
|
160
|
+
const DANGEROUS_SHELL_PATTERNS = /(?:;|&&|\|\||\$\(|`|\$\{|\$\w|\b(eval|exec)\b|[<>](?![&\d])|(?<![<>])&|[\r\n])/;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Verification gates intentionally execute project-controlled code, but they
|
|
164
|
+
* must enter through a known build/test runner. Interpreter eval modes and
|
|
165
|
+
* arbitrary utilities are excluded: `node -e`, `python -c`, `curl`, etc. can
|
|
166
|
+
* otherwise execute attacker-provided code without any shell metacharacters.
|
|
167
|
+
*
|
|
168
|
+
* Prefixes are token-bound and each side of a pipeline is checked separately,
|
|
169
|
+
* preventing an allowed prefix such as `npm test | node -e ...` from bypassing
|
|
170
|
+
* the restriction. Keep this list focused on common verification commands.
|
|
171
|
+
*/
|
|
172
|
+
const ALLOWED_GATE_COMMAND_PATTERNS: readonly RegExp[] = [
|
|
173
|
+
/^npm\s+(?:test(?:\s|$)|run(?:-script)?\s+[A-Za-z0-9_.:-]+(?:\s|$))/,
|
|
174
|
+
/^pnpm\s+(?:test(?:\s|$)|run\s+[A-Za-z0-9_.:-]+(?:\s|$))/,
|
|
175
|
+
/^yarn\s+(?:test(?:\s|$)|run\s+[A-Za-z0-9_.:-]+(?:\s|$))/,
|
|
176
|
+
/^npx\s+(?:--no-install\s+)?(?:biome|eslint|tsc)(?:\s|$)/,
|
|
177
|
+
/^(?:biome|eslint|tsc)(?:\s|$)/,
|
|
178
|
+
/^node\s+(?:(?:--experimental-strip-types|--no-warnings)\s+)*--test(?:\s|$)/,
|
|
179
|
+
/^python(?:3(?:\.\d+)*)?\s+-m\s+(?:pytest|unittest)(?:\s|$)/,
|
|
180
|
+
/^pytest(?:\s|$)/,
|
|
181
|
+
/^cargo\s+(?:build|check|clippy|fmt|test)(?:\s|$)/,
|
|
182
|
+
/^go\s+(?:build|test|vet)(?:\s|$)/,
|
|
183
|
+
/^bun\s+test(?:\s|$)/,
|
|
184
|
+
/^deno\s+(?:check|lint|test)(?:\s|$)/,
|
|
185
|
+
/^dotnet\s+(?:build|test)(?:\s|$)/,
|
|
186
|
+
/^(?:gradle|\.\/gradlew)\s+(?:build|check|test)(?:\s|$)/,
|
|
187
|
+
/^(?:mvn|\.\/mvnw)\s+(?:package|test|verify)(?:\s|$)/,
|
|
188
|
+
/^make\s+(?:build|check|lint|test)(?:\s|$)/,
|
|
189
|
+
/^(?:echo|false|printenv|printf|true)(?:\s|$)/,
|
|
190
|
+
/^(?:grep|head|rg|tail)(?:\s|$)/,
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const NODE_EVAL_MODE_PATTERN = /(?:^|\s)["']?(?:(?:-e|-p)(?!-)|--(?:eval|print)(?:=|\s|["']|$))/;
|
|
194
|
+
|
|
195
|
+
function validateAllowedGateCommands(normalized: string, original: string): void {
|
|
196
|
+
const pipelineCommands = normalized.split("|").map((part) => part.trim());
|
|
197
|
+
for (const pipelineCommand of pipelineCommands) {
|
|
198
|
+
if (/^node(?:\s|$)/.test(pipelineCommand) && NODE_EVAL_MODE_PATTERN.test(pipelineCommand)) {
|
|
199
|
+
throw new Error(`Security: verification gate command rejected (Node eval/print mode is not allowed): ${original}`);
|
|
200
|
+
}
|
|
201
|
+
if (!pipelineCommand || !ALLOWED_GATE_COMMAND_PATTERNS.some((pattern) => pattern.test(pipelineCommand))) {
|
|
202
|
+
throw new Error(`Security: verification gate command rejected (command is not allowlisted): ${original}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
159
206
|
|
|
160
207
|
/**
|
|
161
208
|
* Validate a verification gate command is safe to execute.
|
|
162
|
-
* Rejects commands with shell metacharacters that could enable injection
|
|
163
|
-
*
|
|
209
|
+
* Rejects commands with shell metacharacters that could enable injection and
|
|
210
|
+
* commands outside the verification-runner allowlist. Allows safe pipelines
|
|
211
|
+
* and stderr fd duplication (`2>&1`).
|
|
164
212
|
*/
|
|
165
213
|
/** @internal — exported for injection-guard unit testing (Round 25). */
|
|
166
214
|
export function __test__validateGateCommand(command: string): void {
|
|
@@ -187,6 +235,7 @@ function validateGateCommand(command: string): void {
|
|
|
187
235
|
if (DANGEROUS_SHELL_PATTERNS.test(normalized)) {
|
|
188
236
|
throw new Error(`Security: verification gate command rejected (dangerous shell pattern): ${command}`);
|
|
189
237
|
}
|
|
238
|
+
validateAllowedGateCommands(normalized, command);
|
|
190
239
|
}
|
|
191
240
|
|
|
192
241
|
async function executeCommand(
|
|
@@ -37,6 +37,7 @@ export const PiTeamsLimitsConfigSchema = Type.Object(
|
|
|
37
37
|
maxRetriesPerTask: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
38
38
|
maxTasksPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
39
39
|
heartbeatStaleMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
40
|
+
serializeOnPathOverlap: Type.Optional(Type.Boolean()),
|
|
40
41
|
},
|
|
41
42
|
{ additionalProperties: false },
|
|
42
43
|
);
|
|
@@ -59,6 +60,17 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
|
|
|
59
60
|
effectivenessGuard: Type.Optional(
|
|
60
61
|
Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
|
|
61
62
|
),
|
|
63
|
+
yield: Type.Optional(
|
|
64
|
+
Type.Object(
|
|
65
|
+
{
|
|
66
|
+
enabled: Type.Optional(Type.Boolean()),
|
|
67
|
+
maxReminders: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
68
|
+
reminderPrompt: Type.Optional(Type.String({ maxLength: 1000 })),
|
|
69
|
+
},
|
|
70
|
+
{ additionalProperties: false },
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
excludeContextBash: Type.Optional(Type.Boolean()),
|
|
62
74
|
isolationPolicy: Type.Optional(
|
|
63
75
|
Type.Object(
|
|
64
76
|
{
|
|
@@ -182,7 +194,7 @@ export const PiTeamsObservabilityConfigSchema = Type.Object(
|
|
|
182
194
|
{
|
|
183
195
|
enabled: Type.Optional(Type.Boolean()),
|
|
184
196
|
pollIntervalMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 60000 })),
|
|
185
|
-
metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum:
|
|
197
|
+
metricRetentionDays: Type.Optional(Type.Integer({ minimum: 1, maximum: 90 })),
|
|
186
198
|
},
|
|
187
199
|
{ additionalProperties: false },
|
|
188
200
|
);
|
|
@@ -205,6 +217,11 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
|
|
|
205
217
|
autoRecover: Type.Optional(Type.Boolean()),
|
|
206
218
|
deadletterThreshold: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
207
219
|
cleanupOrphanedTempDirs: Type.Optional(Type.Boolean()),
|
|
220
|
+
autoRepairIntervalMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
221
|
+
forcePreflight: Type.Optional(Type.Boolean()),
|
|
222
|
+
ambientStatusInjection: Type.Optional(Type.Boolean()),
|
|
223
|
+
perWriteValidation: Type.Optional(Type.Boolean()),
|
|
224
|
+
scopeModels: Type.Optional(Type.Boolean()),
|
|
208
225
|
},
|
|
209
226
|
{ additionalProperties: false },
|
|
210
227
|
);
|
|
@@ -235,6 +252,7 @@ export const PiTeamsUiConfigSchema = Type.Object(
|
|
|
235
252
|
dashboardLiveRefreshMs: Type.Optional(Type.Integer({ minimum: 250, maximum: 60000 })),
|
|
236
253
|
autoOpenDashboard: Type.Optional(Type.Boolean()),
|
|
237
254
|
autoOpenDashboardForForegroundRuns: Type.Optional(Type.Boolean()),
|
|
255
|
+
autoCloseDashboardMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
238
256
|
showModel: Type.Optional(Type.Boolean()),
|
|
239
257
|
showTokens: Type.Optional(Type.Boolean()),
|
|
240
258
|
showTools: Type.Optional(Type.Boolean()),
|
|
@@ -194,6 +194,8 @@ export const TeamToolParams = Type.Object({
|
|
|
194
194
|
description: "Path to a markdown plan document for orchestration.",
|
|
195
195
|
}),
|
|
196
196
|
),
|
|
197
|
+
subAction: Type.Optional(Type.String({ description: "Sub-action for schedule management (remove, disable, enable, update)." })),
|
|
198
|
+
jobId: Type.Optional(Type.String({ description: "Job ID for schedule management actions." })),
|
|
197
199
|
cron: Type.Optional(
|
|
198
200
|
Type.String({
|
|
199
201
|
description: "Cron expression for recurring scheduled runs (e.g., '0 9 * * MON').",
|
|
@@ -28,23 +28,34 @@ export interface SkillDescriptor {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* F6 (v0.7.9): discover skills from all
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
31
|
+
* F6 (v0.7.9): discover skills from all roots (matching pi-subagents' skill-loader
|
|
32
|
+
* so users authoring skills under either convention find them).
|
|
33
|
+
*
|
|
34
|
+
* IMPORTANT — inventory vs injection (SEC-003): this function returns EVERY
|
|
35
|
+
* skill found across roots (NO first-wins dedup) because it feeds the capability
|
|
36
|
+
* INVENTORY, which intentionally surfaces all skills (incl. project shadows) for
|
|
37
|
+
* transparency; `capability-inventory.ts` then marks project/user items that
|
|
38
|
+
* shadow a package/builtin one and re-sorts by id.
|
|
39
|
+
*
|
|
40
|
+
* The actual skill-content INJECTION path is separate: `skill-instructions.ts`
|
|
41
|
+
* `candidateSkillDirs` + `readSkillMarkdown` use first-wins with PACKAGE FIRST
|
|
42
|
+
* (SEC-003 fix, regression-tested in test/unit/skill-instructions.test.ts) so a
|
|
43
|
+
* project skill can NEVER override a trusted package skill's content in worker
|
|
44
|
+
* prompts. The package-first order below mirrors that injection path for
|
|
45
|
+
* consistency/defense-in-depth.
|
|
46
|
+
*
|
|
47
|
+
* Roots:
|
|
48
|
+
* 1. PACKAGE_SKILLS_DIR (bundled, trusted)
|
|
49
|
+
* 2. <cwd>/.pi/skills (project, Pi standard)
|
|
50
|
+
* 3. <cwd>/.agents/skills (project, Agent Skills spec — agentskills.io)
|
|
51
|
+
* 4. <cwd>/skills (project, legacy pi-crew convention)
|
|
52
|
+
* 5. <getAgentDir>/skills (user, Pi standard)
|
|
53
|
+
* 6. <homedir>/.agents/skills (user, Agent Skills spec)
|
|
54
|
+
* 7. <homedir>/.pi/skills (user, legacy Pi — pre-standard)
|
|
45
55
|
*/
|
|
46
56
|
function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescriptor["source"] }> {
|
|
47
57
|
return [
|
|
58
|
+
{ root: PACKAGE_SKILLS_DIR, source: "package" },
|
|
48
59
|
{ root: path.resolve(cwd, ".pi", "skills"), source: "project-pi" },
|
|
49
60
|
{
|
|
50
61
|
root: path.resolve(cwd, ".agents", "skills"),
|
|
@@ -57,7 +68,6 @@ function listSkillDirs(cwd: string): Array<{ root: string; source: SkillDescript
|
|
|
57
68
|
source: "user-agents",
|
|
58
69
|
},
|
|
59
70
|
{ root: path.join(os.homedir(), ".pi", "skills"), source: "user-pi" },
|
|
60
|
-
{ root: PACKAGE_SKILLS_DIR, source: "package" },
|
|
61
71
|
];
|
|
62
72
|
}
|
|
63
73
|
|
|
@@ -2,7 +2,6 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
6
5
|
import { userCrewRoot } from "../utils/paths.ts";
|
|
7
6
|
import { isSafePathId } from "../utils/safe-paths.ts";
|
|
8
7
|
import { sharedScanCache } from "../utils/scan-cache.ts";
|