@hassangameryt/freeflow 0.7.3 → 0.8.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +26 -31
- package/capabilities/cognitive-routing/SKILL.md +106 -161
- package/capabilities/cognitive-routing/references/both-mode.md +36 -0
- package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
- package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +239 -0
- package/command-surface.json +4 -0
- package/gemini-extension.json +1 -1
- package/hooks/shared/runtime-context.mjs +28 -30
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +22 -9
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
- package/pi-extension/dist/cognitive-routing/gate.js +241 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
- package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
- package/pi-extension/dist/cognitive-routing/session.js +221 -0
- package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
- package/pi-extension/dist/cognitive-routing/status.js +80 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
- package/pi-extension/dist/compaction/carry.js +191 -0
- package/pi-extension/dist/compaction/config.js +22 -0
- package/pi-extension/dist/compaction/controller.js +471 -0
- package/pi-extension/dist/compaction/harness.js +77 -0
- package/pi-extension/dist/compaction/results.js +158 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +77 -0
- package/pi-extension/dist/host/config.js +374 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +82 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
- package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
- package/pi-extension/dist/host/runtime-state.js +189 -0
- package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +101 -0
- package/pi-extension/dist/index.js +423 -173
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +44 -3
- package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
- package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
- package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
- package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +238 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +22 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +22 -75
- package/runtime/prompts/interaction-contract.md +15 -8
- package/runtime/prompts/tool-execution.md +38 -0
- package/runtime/prompts/working-method.md +108 -0
- package/skills/action-selection/SKILL.md +39 -47
- package/skills/action-selection/references/trajectory-stalls.md +2 -0
- package/skills/bypass/SKILL.md +2 -2
- package/skills/commit-work/SKILL.md +31 -83
- package/skills/commit-work/references/git-state-edges.md +1 -1
- package/skills/commit-work/references/simple-push.md +2 -0
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +31 -50
- package/skills/design-for-depth/SKILL.md +44 -53
- package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
- package/skills/design-for-depth/references/implementation-decisions.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +5 -36
- package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
- package/skills/diagnose-failure/SKILL.md +39 -24
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
- package/skills/discuss/SKILL.md +35 -11
- package/skills/discuss/references/discussion-continuity.md +7 -27
- package/skills/evaluate-skill/SKILL.md +49 -80
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
- package/skills/evaluate-skill/references/evaluation-design.md +14 -14
- package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
- package/skills/evaluate-skill/references/review-and-revision.md +3 -1
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
- package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
- package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
- package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
- package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
- package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
- package/skills/execute-work/SKILL.md +41 -9
- package/skills/execute-work/references/code-practices.md +2 -0
- package/skills/execute-work/references/execute-work-edges.md +11 -1
- package/skills/handoff/SKILL.md +1 -1
- package/skills/handoff/references/handoff-templates.md +1 -1
- package/skills/review-artifact/SKILL.md +42 -41
- package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +38 -32
- package/skills/review-work/references/adjudicate-work-review.md +1 -1
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/review-work/references/security-risk-lens.md +2 -4
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +7 -13
- package/skills/setup-freeflow/references/host-setup.md +3 -3
- package/skills/track-work/SKILL.md +113 -161
- package/skills/track-work/references/working-record-format.md +189 -350
- package/skills/track-work/scripts/lib/document.mjs +62 -6
- package/skills/track-work/scripts/lib/format.mjs +16 -1
- package/skills/track-work/scripts/lib/store.mjs +9 -8
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/working-record.mjs +34 -12
- package/skills/verify-work/SKILL.md +29 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
- package/skills/verify-work/references/integration-evidence.md +2 -0
- package/skills/verify-work/references/performance-evidence.md +3 -1
- package/skills/verify-work/references/test-design.md +3 -1
- package/skills/workflow/SKILL.md +65 -21
- package/skills/workflow/references/domain-skill-composition.md +5 -3
- package/skills/write-docs/SKILL.md +83 -0
- package/skills/write-docs/references/documentation-shapes.md +119 -0
- package/skills/write-docs/scripts/docs-check.mjs +139 -0
- package/skills/write-plan/SKILL.md +40 -40
- package/skills/write-plan/references/plan-shapes.md +1 -1
- package/skills/write-skill/SKILL.md +107 -147
- package/skills/write-skill/references/activation-boundaries.md +2 -0
- package/skills/write-skill/references/agent-first-instructions.md +22 -10
- package/skills/write-skill/references/progressive-disclosure.md +4 -2
- package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/skill-author.mjs +18 -2
- package/skills/write-spec/SKILL.md +34 -41
- package/skills/write-spec/references/artifact-standards.md +1 -1
- package/skills/write-spec/references/decision-records.md +2 -0
- package/skills/write-spec/references/spec-shapes.md +1 -1
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -378
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -314
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -754
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/provenance.js +0 -32
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -428
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -366
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -390
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
- package/pi-extension/src/provider-support/astra/history.ts +0 -145
- package/pi-extension/src/provider-support/astra/index.ts +0 -12
- package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
- package/pi-extension/src/provider-support/index.ts +0 -5
- package/pi-extension/src/runtime/request-history.ts +0 -243
- package/pi-extension/src/runtime/runtime-context.ts +0 -860
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -230
- package/pi-extension/src/session-sources/provenance.ts +0 -39
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -334
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1652
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cache-reuse/history.test.js +0 -117
- package/pi-extension/tests/cache-reuse/native.test.js +0 -91
- package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -191
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
- package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
- package/pi-extension/tests/provider-support/astra.test.js +0 -306
- package/pi-extension/tests/provider-support/cli.test.js +0 -92
- package/pi-extension/tests/provider-support/native.test.js +0 -183
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { resolveToolPath } from "../file-state.js";
|
|
5
|
+
import { closestMatch } from "../edit-messages.js";
|
|
6
|
+
import { parsePatch } from "./parser.js";
|
|
7
|
+
import { otherMatches, seekSequence } from "./match.js";
|
|
8
|
+
class PlanError extends Error {}
|
|
9
|
+
async function isFile(path) {
|
|
10
|
+
try {
|
|
11
|
+
return (await stat(path)).isFile();
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const MAX_SHOWN_LINES = 20;
|
|
17
|
+
function notFound(path, hunk, oldLines, lines) {
|
|
18
|
+
const subject = `${path} hunk ${hunk}`;
|
|
19
|
+
const match = closestMatch(lines.join("\n"), oldLines.join("\n"));
|
|
20
|
+
if (!match)
|
|
21
|
+
return new PlanError(
|
|
22
|
+
`${subject}: context not found and no similar lines exist in the current file. Read the region you meant, then retry.`,
|
|
23
|
+
);
|
|
24
|
+
return new PlanError(
|
|
25
|
+
[
|
|
26
|
+
`${subject}: context not found. Closest match, lines ${match.start}–${match.end} (current file contents):`,
|
|
27
|
+
...lines.slice(match.start - 1, Math.min(match.end, match.start - 1 + MAX_SHOWN_LINES)),
|
|
28
|
+
"If these are the lines you meant, retry with context copied exactly from them. If not, read the region you meant, then retry.",
|
|
29
|
+
].join("\n"),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
/** Split text as it was stored so it can be written back the same way: BOM, line ending, final newline. */
|
|
33
|
+
function splitText(raw) {
|
|
34
|
+
const bom = raw.startsWith("") ? "" : "";
|
|
35
|
+
const body = raw.slice(bom.length);
|
|
36
|
+
const firstLf = body.indexOf("\n");
|
|
37
|
+
const crlf = firstLf > 0 && body[firstLf - 1] === "\r";
|
|
38
|
+
const lf = body.replace(/\r\n/g, "\n");
|
|
39
|
+
const finalNewline = lf === "" || lf.endsWith("\n");
|
|
40
|
+
const lines = lf === "" ? [] : lf.split("\n");
|
|
41
|
+
if (lf.endsWith("\n")) lines.pop();
|
|
42
|
+
return { bom, crlf, finalNewline, lines };
|
|
43
|
+
}
|
|
44
|
+
function joinText(parts, lines) {
|
|
45
|
+
const text = lines.join("\n") + (parts.finalNewline && lines.length > 0 ? "\n" : "");
|
|
46
|
+
return parts.bom + (parts.crlf ? text.replace(/\n/g, "\r\n") : text);
|
|
47
|
+
}
|
|
48
|
+
/** Codex's compute_replacements, keeping the file's own bytes on context lines. */
|
|
49
|
+
function updateLines(path, lines, chunks, notes) {
|
|
50
|
+
const replacements = [];
|
|
51
|
+
let cursor = 0;
|
|
52
|
+
chunks.forEach((chunk, index) => {
|
|
53
|
+
const hunk = index + 1;
|
|
54
|
+
if (chunk.anchor !== undefined) {
|
|
55
|
+
const anchor = seekSequence(lines, [chunk.anchor], cursor, false);
|
|
56
|
+
if (!anchor)
|
|
57
|
+
throw new PlanError(
|
|
58
|
+
`${path} hunk ${hunk}: @@ line not found: '${chunk.anchor}'. Use a line that exists in the file after the previous hunk, or leave the @@ line empty.`,
|
|
59
|
+
);
|
|
60
|
+
cursor = anchor.index + 1;
|
|
61
|
+
}
|
|
62
|
+
if (chunk.oldLines.length === 0) {
|
|
63
|
+
replacements.push({ start: lines.length, length: 0, lines: [...chunk.newLines] });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
let pattern = chunk.oldLines;
|
|
67
|
+
let replacement = chunk.newLines;
|
|
68
|
+
let found = seekSequence(lines, pattern, cursor, chunk.endOfFile);
|
|
69
|
+
if (!found && pattern.at(-1) === "") {
|
|
70
|
+
// A trailing empty line stands for the file's final newline, which the line list does not hold.
|
|
71
|
+
pattern = pattern.slice(0, -1);
|
|
72
|
+
if (replacement.at(-1) === "") replacement = replacement.slice(0, -1);
|
|
73
|
+
found = seekSequence(lines, pattern, cursor, chunk.endOfFile);
|
|
74
|
+
}
|
|
75
|
+
if (!found) throw notFound(path, hunk, chunk.oldLines, lines);
|
|
76
|
+
const resolved = [...replacement];
|
|
77
|
+
for (const [oldIndex, newIndex] of chunk.context)
|
|
78
|
+
if (oldIndex < pattern.length && newIndex < resolved.length) resolved[newIndex] = lines[found.index + oldIndex];
|
|
79
|
+
const others = otherMatches(lines, pattern, found.index, found.pass);
|
|
80
|
+
if (others.length > 0)
|
|
81
|
+
notes.push(
|
|
82
|
+
`${path} hunk ${hunk}: its lines also match at line ${others.map((i) => i + 1).join(", ")}; it was applied at line ${found.index + 1}.`,
|
|
83
|
+
);
|
|
84
|
+
replacements.push({ start: found.index, length: pattern.length, lines: resolved });
|
|
85
|
+
cursor = found.index + pattern.length;
|
|
86
|
+
});
|
|
87
|
+
const next = [...lines];
|
|
88
|
+
for (const { start, length, lines: added } of [...replacements].sort((a, b) => b.start - a.start))
|
|
89
|
+
next.splice(start, length, ...added);
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
async function plan(hunks, cwd, files, notes) {
|
|
93
|
+
if (hunks.length === 0) throw new PlanError("The patch changes no files. Add at least one file section, then retry.");
|
|
94
|
+
// Later sections see earlier sections' results, as they would if applied one by one.
|
|
95
|
+
const virtual = new Map();
|
|
96
|
+
const current = async (abs) => {
|
|
97
|
+
if (virtual.has(abs)) return virtual.get(abs);
|
|
98
|
+
if (!(await isFile(abs))) return null;
|
|
99
|
+
const bytes = await readFile(abs);
|
|
100
|
+
const text = bytes.toString("utf8");
|
|
101
|
+
return Buffer.from(text, "utf8").equals(bytes) ? text : "\u0000not-utf8";
|
|
102
|
+
};
|
|
103
|
+
const steps = [];
|
|
104
|
+
for (const hunk of hunks) {
|
|
105
|
+
const abs = resolveToolPath(hunk.path, cwd);
|
|
106
|
+
const existing = await current(abs);
|
|
107
|
+
if (hunk.kind === "add") {
|
|
108
|
+
if (existing !== null && !virtual.has(abs) && !files.wasRead(hunk.path, cwd))
|
|
109
|
+
throw new PlanError(
|
|
110
|
+
`${hunk.path} exists and has not been read in this session. Read ${hunk.path}, then retry the patch.`,
|
|
111
|
+
);
|
|
112
|
+
virtual.set(abs, hunk.contents);
|
|
113
|
+
steps.push({ path: hunk.path, abs, operation: "add", content: hunk.contents });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (existing === null)
|
|
117
|
+
throw new PlanError(
|
|
118
|
+
hunk.kind === "delete"
|
|
119
|
+
? `${hunk.path}: file not found; nothing to delete.`
|
|
120
|
+
: `${hunk.path}: file not found. Use *** Add File: ${hunk.path} to create it.`,
|
|
121
|
+
);
|
|
122
|
+
if (existing === "\u0000not-utf8")
|
|
123
|
+
throw new PlanError(`${hunk.path}: not UTF-8 text; apply_patch edits text files only.`);
|
|
124
|
+
if (!virtual.has(abs) && (await files.changedSinceObserved(hunk.path, cwd)))
|
|
125
|
+
notes.push(`${hunk.path} changed since you read it; the patch applied to the current text.`);
|
|
126
|
+
if (hunk.kind === "delete") {
|
|
127
|
+
virtual.set(abs, null);
|
|
128
|
+
steps.push({ path: hunk.path, abs, operation: "delete" });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const parts = splitText(existing);
|
|
132
|
+
const content = joinText(parts, updateLines(hunk.path, parts.lines, hunk.chunks, notes));
|
|
133
|
+
if (hunk.moveTo !== undefined) {
|
|
134
|
+
const toAbs = resolveToolPath(hunk.moveTo, cwd);
|
|
135
|
+
if ((await current(toAbs)) !== null)
|
|
136
|
+
throw new PlanError(`${hunk.moveTo} already exists. Delete or rename it first, or move to another path.`);
|
|
137
|
+
virtual.set(abs, null);
|
|
138
|
+
virtual.set(toAbs, content);
|
|
139
|
+
steps.push({ path: hunk.path, abs, operation: "move", to: hunk.moveTo, toAbs, content });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
virtual.set(abs, content);
|
|
143
|
+
steps.push({ path: hunk.path, abs, operation: "update", content });
|
|
144
|
+
}
|
|
145
|
+
return steps;
|
|
146
|
+
}
|
|
147
|
+
async function withLocks(paths, fn) {
|
|
148
|
+
// Sorted, so two patches touching the same files never wait on each other in opposite order.
|
|
149
|
+
const [first, ...rest] = [...new Set(paths)].sort();
|
|
150
|
+
return first === undefined ? fn() : withFileMutationQueue(first, () => withLocks(rest, fn));
|
|
151
|
+
}
|
|
152
|
+
async function write(step) {
|
|
153
|
+
if (step.operation === "delete") return unlink(step.abs);
|
|
154
|
+
const target = step.toAbs ?? step.abs;
|
|
155
|
+
await mkdir(dirname(target), { recursive: true });
|
|
156
|
+
await writeFile(target, step.content, "utf8");
|
|
157
|
+
if (step.operation === "move") await unlink(step.abs);
|
|
158
|
+
}
|
|
159
|
+
/** Plan the whole patch against current files, then write it; a planning failure writes nothing. */
|
|
160
|
+
export async function applyPatch(text, cwd, files) {
|
|
161
|
+
const notes = [];
|
|
162
|
+
let hunks;
|
|
163
|
+
try {
|
|
164
|
+
hunks = parsePatch(text);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
167
|
+
return { status: "not_applied", files: [], notes, error: `Invalid patch: ${reason}. Fix the patch, then retry.` };
|
|
168
|
+
}
|
|
169
|
+
const paths = hunks.flatMap((hunk) => [
|
|
170
|
+
resolveToolPath(hunk.path, cwd),
|
|
171
|
+
...(hunk.kind === "update" && hunk.moveTo !== undefined ? [resolveToolPath(hunk.moveTo, cwd)] : []),
|
|
172
|
+
]);
|
|
173
|
+
return withLocks(paths, async () => {
|
|
174
|
+
let steps;
|
|
175
|
+
try {
|
|
176
|
+
steps = await plan(hunks, cwd, files, notes);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!(error instanceof PlanError)) throw error;
|
|
179
|
+
return { status: "not_applied", files: [], notes, error: error.message };
|
|
180
|
+
}
|
|
181
|
+
const changes = steps.map((step) => ({
|
|
182
|
+
path: step.path,
|
|
183
|
+
operation: step.operation,
|
|
184
|
+
...(step.to === undefined ? {} : { to: step.to }),
|
|
185
|
+
outcome: "not_written",
|
|
186
|
+
}));
|
|
187
|
+
for (const [index, step] of steps.entries()) {
|
|
188
|
+
try {
|
|
189
|
+
await write(step);
|
|
190
|
+
changes[index].outcome = "written";
|
|
191
|
+
} catch (error) {
|
|
192
|
+
changes[index].outcome = "failed";
|
|
193
|
+
changes[index].error = error instanceof Error ? error.message : String(error);
|
|
194
|
+
// A failed write may already have changed its target, so this is never reported as "not applied".
|
|
195
|
+
// Nothing after it is attempted.
|
|
196
|
+
return { status: "partial", files: changes, notes };
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { status: "applied", files: changes, notes };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { APPLY_PATCH_LARK_GRAMMAR } from "./grammar.js";
|
|
2
|
+
import { applyPatch } from "./plan.js";
|
|
3
|
+
export const APPLY_PATCH = "apply_patch";
|
|
4
|
+
const DESCRIPTION =
|
|
5
|
+
"Use the apply_patch tool to edit files. Its input is a patch: *** Begin Patch, then *** Add File:, *** Update File: (optionally followed by *** Move to:) or *** Delete File: sections, then *** End Patch. In update sections, @@ lines anchor a hunk and every other line starts with space (context), - (remove) or + (add). Include enough context lines for each hunk to match one place.";
|
|
6
|
+
const CODES = { add: "A", update: "M", move: "M", delete: "D" };
|
|
7
|
+
/** Codex's summary on success, so GPT models read the result they were trained on. */
|
|
8
|
+
export function resultText(result) {
|
|
9
|
+
const notes = result.notes;
|
|
10
|
+
if (result.status === "applied")
|
|
11
|
+
return [
|
|
12
|
+
"Success. Updated the following files:",
|
|
13
|
+
...result.files.map((file) => `${CODES[file.operation]} ${file.to ?? file.path}`),
|
|
14
|
+
...notes,
|
|
15
|
+
].join("\n");
|
|
16
|
+
if (result.status === "not_applied")
|
|
17
|
+
return [`Patch not applied; no files changed. ${result.error ?? ""}`.trim(), ...notes].join("\n");
|
|
18
|
+
const named = (outcome) =>
|
|
19
|
+
result.files.filter((file) => file.outcome === outcome).map((file) => file.to ?? file.path);
|
|
20
|
+
const failed = result.files.find((file) => file.outcome === "failed");
|
|
21
|
+
return [
|
|
22
|
+
`Patch partly applied. Written: ${named("written").join(", ") || "none"}. Not written: ${named("not_written").join(", ") || "none"}. Failed: ${failed?.to ?? failed?.path}: ${failed?.error}.`,
|
|
23
|
+
"Read the files named above before changing them again.",
|
|
24
|
+
...notes,
|
|
25
|
+
].join("\n");
|
|
26
|
+
}
|
|
27
|
+
const OUTPUT_SCHEMA = {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
status: { type: "string", enum: ["applied", "not_applied", "partial"] },
|
|
31
|
+
files: {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
path: { type: "string" },
|
|
37
|
+
operation: { type: "string", enum: ["add", "update", "delete", "move"] },
|
|
38
|
+
to: { type: "string" },
|
|
39
|
+
outcome: { type: "string", enum: ["written", "not_written", "failed"] },
|
|
40
|
+
error: { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
required: ["path", "operation", "outcome"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
required: ["status", "files"],
|
|
47
|
+
};
|
|
48
|
+
/** Registered once, inactive until Tool Execution is effective (see tool-execution/tools.ts). */
|
|
49
|
+
export function registerApplyPatch(pi, host) {
|
|
50
|
+
pi.registerTool({
|
|
51
|
+
name: APPLY_PATCH,
|
|
52
|
+
label: APPLY_PATCH,
|
|
53
|
+
description: DESCRIPTION,
|
|
54
|
+
parameters: {
|
|
55
|
+
type: "object",
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
properties: {
|
|
58
|
+
input: { type: "string", description: "The patch, from *** Begin Patch to *** End Patch." },
|
|
59
|
+
},
|
|
60
|
+
required: ["input"],
|
|
61
|
+
},
|
|
62
|
+
// GPT-5+ models on OpenAI endpoints receive Codex's freeform grammar tool; other models a one-string function.
|
|
63
|
+
constrainedSampling: { type: "grammar", variants: { openai_lark: APPLY_PATCH_LARK_GRAMMAR } },
|
|
64
|
+
outputSchema: OUTPUT_SCHEMA,
|
|
65
|
+
exposure: "direct",
|
|
66
|
+
defaultActive: false,
|
|
67
|
+
executionMode: "sequential",
|
|
68
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
69
|
+
if (!host.effective()) throw new Error("apply_patch is available only while Freeflow Tool Execution is on.");
|
|
70
|
+
const result = await applyPatch(String(params?.input ?? ""), ctx.cwd, await host.files(ctx));
|
|
71
|
+
const written = result.files.filter((file) => file.outcome === "written");
|
|
72
|
+
await host.written(
|
|
73
|
+
written.flatMap((file) => [file.path, ...(file.to ? [file.to] : [])]),
|
|
74
|
+
ctx,
|
|
75
|
+
);
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: resultText(result) }],
|
|
78
|
+
details: result,
|
|
79
|
+
structuredContent: { status: result.status, files: result.files },
|
|
80
|
+
...(result.status === "applied" ? {} : { isError: true }),
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
export const BASH_BACKGROUND = "bash_background";
|
|
8
|
+
export const STOP_BACKGROUND = "stop_background";
|
|
9
|
+
export const NOTICE_TYPE = "freeflow-background";
|
|
10
|
+
export const MAX_RUNNING = 16;
|
|
11
|
+
/** Marks harness messages, which arrive on their own schedule, as not written by the user. */
|
|
12
|
+
export const NOTICE_PREFIX = "[Freeflow notice, not from the user]";
|
|
13
|
+
const newId = () => `bg${randomBytes(6).readUIntBE(0, 6).toString(36).slice(-6).padStart(6, "0")}`;
|
|
14
|
+
function labelFor(command, description) {
|
|
15
|
+
const text = description?.trim() || command.trim().split("\n")[0];
|
|
16
|
+
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
17
|
+
}
|
|
18
|
+
function close(stream, trailer) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
stream.once("error", () => resolve());
|
|
21
|
+
stream.end(trailer, () => resolve());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Claude Code's background commands on Pi's shell backend: start and return at once, stream output to one file,
|
|
26
|
+
* notify the model when the command exits, stop on request or when the session ends.
|
|
27
|
+
*/
|
|
28
|
+
export class BackgroundJobs {
|
|
29
|
+
host;
|
|
30
|
+
jobs = new Map();
|
|
31
|
+
constructor(host) {
|
|
32
|
+
this.host = host;
|
|
33
|
+
}
|
|
34
|
+
running() {
|
|
35
|
+
return [...this.jobs.values()].filter((job) => job.status === "running");
|
|
36
|
+
}
|
|
37
|
+
async start(command, description, cwd, sessionId) {
|
|
38
|
+
const running = this.running();
|
|
39
|
+
if (running.length >= MAX_RUNNING)
|
|
40
|
+
throw new Error(`${MAX_RUNNING} background commands are running. Stop one with stop_background, then retry.`);
|
|
41
|
+
let id = newId();
|
|
42
|
+
while (this.jobs.has(id)) id = newId();
|
|
43
|
+
const folder = join(tmpdir(), "freeflow", sessionId.replace(/[^A-Za-z0-9._-]/g, "_"), "background");
|
|
44
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
45
|
+
const outputPath = join(folder, `${id}.output`);
|
|
46
|
+
const stream = createWriteStream(outputPath, { flags: "w", mode: 0o600 });
|
|
47
|
+
await new Promise((resolve, reject) => {
|
|
48
|
+
stream.once("open", () => resolve());
|
|
49
|
+
stream.once("error", reject);
|
|
50
|
+
});
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let endsWithNewline = true;
|
|
53
|
+
const job = {
|
|
54
|
+
id,
|
|
55
|
+
label: labelFor(command, description),
|
|
56
|
+
outputPath,
|
|
57
|
+
controller,
|
|
58
|
+
status: "running",
|
|
59
|
+
done: Promise.resolve(),
|
|
60
|
+
};
|
|
61
|
+
const run = createLocalBashOperations({ shellPath: this.host.shellPath() }).exec(command, cwd, {
|
|
62
|
+
onData: (data) => {
|
|
63
|
+
if (data.length === 0) return;
|
|
64
|
+
endsWithNewline = data[data.length - 1] === 10;
|
|
65
|
+
stream.write(data);
|
|
66
|
+
},
|
|
67
|
+
signal: controller.signal,
|
|
68
|
+
});
|
|
69
|
+
job.done = run.then(
|
|
70
|
+
async ({ exitCode }) => {
|
|
71
|
+
const code = exitCode ?? 1;
|
|
72
|
+
job.exitCode = code;
|
|
73
|
+
job.status = code === 0 ? "completed" : "failed";
|
|
74
|
+
await close(stream, `${endsWithNewline ? "" : "\n"}[exited with code ${code}]\n`);
|
|
75
|
+
this.notify(job);
|
|
76
|
+
},
|
|
77
|
+
async (error) => {
|
|
78
|
+
const lead = endsWithNewline ? "" : "\n";
|
|
79
|
+
if (job.stopping) {
|
|
80
|
+
job.status = "stopped";
|
|
81
|
+
await close(stream, `${lead}${job.stopping === "tool" ? "[stopped]" : "[stopped at session end]"}\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// The command could not run (for example, the working directory is gone).
|
|
85
|
+
job.status = "failed";
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
await close(stream, `${lead}[failed to run: ${message}]\n`);
|
|
88
|
+
this.notify(job, message);
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
this.jobs.set(id, job);
|
|
92
|
+
return { id, outputPath };
|
|
93
|
+
}
|
|
94
|
+
async stop(id) {
|
|
95
|
+
const job = this.jobs.get(id);
|
|
96
|
+
if (!job) {
|
|
97
|
+
const running = this.running().map((each) => each.id);
|
|
98
|
+
throw new Error(
|
|
99
|
+
`No background command ${id} in this session. Running: ${running.length > 0 ? running.join(", ") : "none"}.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
if (job.status === "running" && !job.stopping) {
|
|
103
|
+
job.stopping = "tool";
|
|
104
|
+
job.controller.abort();
|
|
105
|
+
}
|
|
106
|
+
await job.done;
|
|
107
|
+
if (job.stopping === "tool" && job.status === "stopped")
|
|
108
|
+
return { text: `Stopped ${id}.`, structured: { id, status: "stopped" } };
|
|
109
|
+
const exited =
|
|
110
|
+
job.exitCode === undefined
|
|
111
|
+
? `${id} already ${job.status === "stopped" ? "stopped" : "failed to run"}. Output: ${job.outputPath}.`
|
|
112
|
+
: `${id} already exited with code ${job.exitCode}. Output: ${job.outputPath}.`;
|
|
113
|
+
return {
|
|
114
|
+
text: exited,
|
|
115
|
+
structured: { id, status: job.status, ...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }) },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Session end: stop every running job, wait for the processes to exit, forget them. No notice is sent. */
|
|
119
|
+
async stopAll() {
|
|
120
|
+
const running = this.running();
|
|
121
|
+
for (const job of running) {
|
|
122
|
+
job.stopping = "shutdown";
|
|
123
|
+
job.controller.abort();
|
|
124
|
+
}
|
|
125
|
+
await Promise.all(running.map((job) => job.done));
|
|
126
|
+
this.jobs = new Map();
|
|
127
|
+
}
|
|
128
|
+
/** After compaction the model may no longer see which commands it started; restate the running ones once. */
|
|
129
|
+
restate() {
|
|
130
|
+
const running = this.running();
|
|
131
|
+
if (running.length === 0) return;
|
|
132
|
+
const list = running.map((job) => `${job.id} (${job.label}), output ${job.outputPath}`).join("; ");
|
|
133
|
+
this.host.send(
|
|
134
|
+
`${NOTICE_PREFIX} Still running: ${list}. Do not start them again; stop one with stop_background before restarting it.`,
|
|
135
|
+
{ running: running.map((job) => ({ id: job.id, outputPath: job.outputPath })) },
|
|
136
|
+
false,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
notify(job, runError) {
|
|
140
|
+
if (job.stopping) return;
|
|
141
|
+
const outcome =
|
|
142
|
+
runError !== undefined
|
|
143
|
+
? `failed to run: ${runError}`
|
|
144
|
+
: job.status === "completed"
|
|
145
|
+
? "completed"
|
|
146
|
+
: `failed with exit code ${job.exitCode}`;
|
|
147
|
+
this.host.send(
|
|
148
|
+
`${NOTICE_PREFIX} Background command ${job.id} (${job.label}) ${outcome}. Output: ${job.outputPath}.`,
|
|
149
|
+
{
|
|
150
|
+
id: job.id,
|
|
151
|
+
status: job.status,
|
|
152
|
+
...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
|
|
153
|
+
outputPath: job.outputPath,
|
|
154
|
+
},
|
|
155
|
+
true,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const START_DESCRIPTION = [
|
|
160
|
+
"Run a shell command in the background. Use it for servers, watchers and commands expected to run longer than two minutes. Returns an ID and an output file at once; you are notified once, when the command exits. Read the output file for output so far.",
|
|
161
|
+
"To be notified when something becomes ready, run a command that exits when it is: until grep -qE 'Listening|Error|Traceback' server.log; do sleep 1; done. Match the failure lines too: a check that waits only for success stays silent when the process crashes.",
|
|
162
|
+
].join("\n\n");
|
|
163
|
+
const STOP_DESCRIPTION =
|
|
164
|
+
"Stop a background command started with bash_background. Returns after the command has exited.";
|
|
165
|
+
/** Registered once, inactive until Tool Execution is effective (see tool-execution/tools.ts). */
|
|
166
|
+
export function registerBackgroundTools(pi, jobs, host) {
|
|
167
|
+
const guard = () => {
|
|
168
|
+
if (!host.effective())
|
|
169
|
+
throw new Error("Background commands are available only while Freeflow Tool Execution is on.");
|
|
170
|
+
};
|
|
171
|
+
pi.registerTool({
|
|
172
|
+
name: BASH_BACKGROUND,
|
|
173
|
+
label: BASH_BACKGROUND,
|
|
174
|
+
description: START_DESCRIPTION,
|
|
175
|
+
parameters: {
|
|
176
|
+
type: "object",
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
properties: {
|
|
179
|
+
command: { type: "string", description: "Shell command to run" },
|
|
180
|
+
description: { type: "string", description: "A few words saying what the command does" },
|
|
181
|
+
},
|
|
182
|
+
required: ["command"],
|
|
183
|
+
},
|
|
184
|
+
outputSchema: {
|
|
185
|
+
type: "object",
|
|
186
|
+
properties: { id: { type: "string" }, outputPath: { type: "string" } },
|
|
187
|
+
required: ["id", "outputPath"],
|
|
188
|
+
},
|
|
189
|
+
exposure: "direct",
|
|
190
|
+
defaultActive: false,
|
|
191
|
+
async execute(_id, params, _signal, _u, ctx) {
|
|
192
|
+
guard();
|
|
193
|
+
const { id, outputPath } = await jobs.start(
|
|
194
|
+
String(params?.command ?? ""),
|
|
195
|
+
params?.description,
|
|
196
|
+
ctx.cwd,
|
|
197
|
+
ctx.sessionManager?.getSessionId?.() ?? "session",
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
content: [
|
|
201
|
+
{
|
|
202
|
+
type: "text",
|
|
203
|
+
text: `Command running in background with ID ${id}. Output: ${outputPath}. You will be notified when it exits. Read that file for output so far.`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
details: { id, outputPath },
|
|
207
|
+
structuredContent: { id, outputPath },
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
pi.registerTool({
|
|
212
|
+
name: STOP_BACKGROUND,
|
|
213
|
+
label: STOP_BACKGROUND,
|
|
214
|
+
description: STOP_DESCRIPTION,
|
|
215
|
+
parameters: {
|
|
216
|
+
type: "object",
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
properties: { id: { type: "string", description: "The ID bash_background returned" } },
|
|
219
|
+
required: ["id"],
|
|
220
|
+
},
|
|
221
|
+
outputSchema: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
id: { type: "string" },
|
|
225
|
+
status: { type: "string", enum: ["stopped", "completed", "failed"] },
|
|
226
|
+
exitCode: { type: "number" },
|
|
227
|
+
},
|
|
228
|
+
required: ["id", "status"],
|
|
229
|
+
},
|
|
230
|
+
exposure: "direct",
|
|
231
|
+
defaultActive: false,
|
|
232
|
+
async execute(_id, params) {
|
|
233
|
+
guard();
|
|
234
|
+
const { text, structured } = await jobs.stop(String(params?.id ?? ""));
|
|
235
|
+
return { content: [{ type: "text", text }], details: structured, structuredContent: structured };
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Two shell habits that lose work once background commands exist: a trailing `&` leaves a process running after the
|
|
2
|
+
// call with no notice and no saved output, and a long foreground sleep holds the session while waiting on something.
|
|
3
|
+
/** Seconds at or above which a foreground sleep is refused. */
|
|
4
|
+
export const LONGEST_FOREGROUND_SLEEP_S = 10;
|
|
5
|
+
const UNIT_SECONDS = { "": 1, s: 1, m: 60, h: 3600, d: 86400 };
|
|
6
|
+
function sleepSeconds(command) {
|
|
7
|
+
let longest = 0;
|
|
8
|
+
// A sleep that starts a command: at the start, or after ; & | && || ( { then do.
|
|
9
|
+
for (const match of command.matchAll(/(?:^|[;&|({]|\bthen\b|\bdo\b)\s*sleep\s+(\d+(?:\.\d+)?)([smhd]?)\b/g))
|
|
10
|
+
longest = Math.max(longest, Number(match[1]) * UNIT_SECONDS[match[2]]);
|
|
11
|
+
return longest;
|
|
12
|
+
}
|
|
13
|
+
/** Why a bash command should run through bash_background instead, as one next action; undefined when it need not. */
|
|
14
|
+
export function backgroundRefusal(command) {
|
|
15
|
+
if (typeof command !== "string") return undefined;
|
|
16
|
+
const trimmed = command.replace(/\s+$/, "");
|
|
17
|
+
if (/(^|[^&])&$/.test(trimmed))
|
|
18
|
+
return "A command ending in & keeps running after this call, with no exit notice and no saved output. Run it with bash_background instead, without the &.";
|
|
19
|
+
const seconds = sleepSeconds(command);
|
|
20
|
+
if (seconds >= LONGEST_FOREGROUND_SLEEP_S)
|
|
21
|
+
return `A foreground sleep of ${seconds} s holds the session. To wait for a condition, run a loop that exits when it holds, such as until <check>; do sleep 1; done, with bash_background; you will be notified when it exits.`;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const DEFAULT_TOOL_EXECUTION_CONFIG = Object.freeze({ enabled: false });
|
|
2
|
+
// Settings of the retired v2 runtime (.deprecated/tool-execution-v2). Existing configuration files keep loading;
|
|
3
|
+
// the values no longer do anything.
|
|
4
|
+
const RETIRED_KEYS = ["capture", "programs", "workspace", "discovery", "adapters", "accounting"];
|
|
5
|
+
function record(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
export function validateToolExecutionConfig(value) {
|
|
9
|
+
if (!record(value)) return "toolExecution must be an object";
|
|
10
|
+
const unknown = Object.keys(value).find((key) => key !== "enabled" && !RETIRED_KEYS.includes(key));
|
|
11
|
+
if (unknown) return `toolExecution has unsupported key: ${unknown}`;
|
|
12
|
+
if (value.enabled !== undefined && typeof value.enabled !== "boolean")
|
|
13
|
+
return "toolExecution.enabled must be a boolean";
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
export function resolveToolExecutionConfig(repository, local, freeflowEnabled) {
|
|
17
|
+
const repositoryTool = record(repository.toolExecution) ? repository.toolExecution : {};
|
|
18
|
+
const localTool = record(local.toolExecution) ? local.toolExecution : {};
|
|
19
|
+
const setting = Object.hasOwn(localTool, "enabled") ? localTool.enabled : repositoryTool.enabled;
|
|
20
|
+
const enabled = setting === undefined ? DEFAULT_TOOL_EXECUTION_CONFIG.enabled : setting === true;
|
|
21
|
+
return { enabled, effective: freeflowEnabled && enabled };
|
|
22
|
+
}
|