@hassangameryt/freeflow 0.8.0 → 0.9.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { autoApproveGroup } from "./config.js";
|
|
2
|
+
import { renderTaskCall, renderTaskResult } from "./render.js";
|
|
3
|
+
import { confirmWithUser } from "../ask-user/ask.js";
|
|
4
|
+
export const TASK_MEMORY_TOOL = "task_memory";
|
|
5
|
+
const DESCRIPTION =
|
|
6
|
+
"Task memory: the Working Records (Track Work) this session works on, and their projects. Read the task-memory skill before your first proposal; Track Work decides what records say. Reading needs no permission: projects lists every project; list lists this repository's records, or a project's tasks; view reads a record's resume view or one entry by task ID; search finds lines in the focused task, its tree, or its project. Attention: focus switches to an attached record; detach stops working on one. Proposals, each confirmed by the user in a dialog before anything happens: attach a record; create a task in this repository's project and attach it; child or fanout (a child with its own Git worktree) of the focused task for a parallel session; project_new or project_link for this repository; import a repository record into a project; confirm a held record; env to change a task's directories; remove a task with its children, or project_remove a whole project, which moves their records to the trash. Only the user empties the trash, with /freeflow trash empty: when asked to, tell the user to run it. A decline is the user's answer: do not retry it or work around it.";
|
|
7
|
+
const ACTIONS = [
|
|
8
|
+
"projects",
|
|
9
|
+
"list",
|
|
10
|
+
"view",
|
|
11
|
+
"search",
|
|
12
|
+
"attach",
|
|
13
|
+
"focus",
|
|
14
|
+
"detach",
|
|
15
|
+
"create",
|
|
16
|
+
"child",
|
|
17
|
+
"fanout",
|
|
18
|
+
"project_new",
|
|
19
|
+
"project_link",
|
|
20
|
+
"import",
|
|
21
|
+
"confirm",
|
|
22
|
+
"env",
|
|
23
|
+
"remove",
|
|
24
|
+
"project_remove",
|
|
25
|
+
];
|
|
26
|
+
// Actions that change which records this session holds; the Runtime State follows them.
|
|
27
|
+
const CHANGES_STATE = new Set(["attach", "focus", "detach", "create", "import", "confirm", "remove", "project_remove"]);
|
|
28
|
+
/** Actions any profile may take: they only read. `env` reads too, unless it changes a directory. */
|
|
29
|
+
const READS = new Set(["projects", "list", "view", "search"]);
|
|
30
|
+
const reads = (params) =>
|
|
31
|
+
READS.has(params?.action) ||
|
|
32
|
+
(params?.action === "env" &&
|
|
33
|
+
!params?.primary?.trim?.() &&
|
|
34
|
+
!params?.add_dir?.trim?.() &&
|
|
35
|
+
!params?.remove_dir?.trim?.());
|
|
36
|
+
/** Registered once, declared only while task memory is effective (see applyTaskMemoryTools). */
|
|
37
|
+
export function registerTaskMemoryTool(pi, host) {
|
|
38
|
+
pi.registerTool({
|
|
39
|
+
name: TASK_MEMORY_TOOL,
|
|
40
|
+
label: "Task memory",
|
|
41
|
+
description: DESCRIPTION,
|
|
42
|
+
parameters: {
|
|
43
|
+
type: "object",
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {
|
|
46
|
+
action: { type: "string", enum: ACTIONS },
|
|
47
|
+
record: {
|
|
48
|
+
type: "string",
|
|
49
|
+
description: "A task ID such as my-project/task-003, or a record.md path; remove takes a task ID",
|
|
50
|
+
},
|
|
51
|
+
project: { type: "string", description: "list: a project's name; import: the project to move the record into" },
|
|
52
|
+
name: { type: "string", description: "project_new, project_link, project_remove: the project's name" },
|
|
53
|
+
worktrees: { type: "string", description: "project_new, project_link: where children's worktrees go" },
|
|
54
|
+
title: { type: "string", description: "create, child, fanout: the new task's title" },
|
|
55
|
+
goal: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description:
|
|
58
|
+
"create, child, fanout: its goal, one line; omitted, the title is the goal (the dialog shows it)",
|
|
59
|
+
},
|
|
60
|
+
entry: { type: "string", description: "view: one entry ID such as S-004" },
|
|
61
|
+
query: { type: "string", description: "search: text to find" },
|
|
62
|
+
scope: { type: "string", enum: ["task", "tree", "project"], description: "search: where to look" },
|
|
63
|
+
primary: { type: "string", description: "env: the task's new primary directory" },
|
|
64
|
+
add_dir: { type: "string", description: "env: a directory to add" },
|
|
65
|
+
remove_dir: { type: "string", description: "env: a directory to remove" },
|
|
66
|
+
},
|
|
67
|
+
required: ["action"],
|
|
68
|
+
},
|
|
69
|
+
// It asks the user, so scripts must not call it (Pi's guidance for such tools); called directly, its own
|
|
70
|
+
// transcript views apply, which a codemode script's nested call would bypass.
|
|
71
|
+
exposure: "model-only",
|
|
72
|
+
defaultActive: false,
|
|
73
|
+
executionMode: "sequential",
|
|
74
|
+
renderCall: (args, theme) => renderTaskCall(args, theme),
|
|
75
|
+
renderResult: (result, options, theme, context) => renderTaskResult(result, options, theme, context),
|
|
76
|
+
async execute(_id, params, _signal, _update, ctx) {
|
|
77
|
+
const refused = reads(params) ? undefined : host.refusal?.();
|
|
78
|
+
const group = reads(params) ? undefined : autoApproveGroup(params?.action);
|
|
79
|
+
const approved = group !== undefined && host.autoApproved?.(group) === true;
|
|
80
|
+
const result = refused ? { ok: false, text: refused } : await run(host, params, ctx, approved);
|
|
81
|
+
if (approved && result.ok)
|
|
82
|
+
result.text = `Auto-approved by the user's taskMemory.autoApprove setting (${group}); no dialog was shown.\n\n${result.text}`;
|
|
83
|
+
if (result.ok && CHANGES_STATE.has(params?.action)) host.changed(ctx);
|
|
84
|
+
let next;
|
|
85
|
+
if (result.ok && result.snapshot) {
|
|
86
|
+
const brief = await host.memory.brief(result.snapshot);
|
|
87
|
+
result.headline ??= `${params.action === "focus" ? "Focus on" : "Attached"} ${brief.label} · ${brief.state}`;
|
|
88
|
+
next = brief.next || undefined;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: result.text }],
|
|
92
|
+
details: {
|
|
93
|
+
action: params?.action,
|
|
94
|
+
...(result.snapshot ? { snapshot: result.snapshot } : {}),
|
|
95
|
+
...(result.headline ? { headline: result.headline } : {}),
|
|
96
|
+
...(next ? { next } : {}),
|
|
97
|
+
...(result.command ? { command: result.command } : {}),
|
|
98
|
+
...(result.id ? { id: result.id } : {}),
|
|
99
|
+
},
|
|
100
|
+
...(result.ok ? {} : { isError: true }),
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Ask the user before an action taken on their authority; without a UI the proposal cannot be confirmed. */
|
|
106
|
+
async function confirmProposal(ctx, title, message) {
|
|
107
|
+
if (!ctx.hasUI) return { ok: false, text: `${title} needs the user's confirmation, and no UI is available.` };
|
|
108
|
+
if (await confirmWithUser(ctx, title, message)) return undefined;
|
|
109
|
+
return { ok: false, headline: "Declined by the user", text: "The user declined. Do not retry or work around it." };
|
|
110
|
+
}
|
|
111
|
+
const show = (value) => (typeof value === "string" ? value.trim() : "");
|
|
112
|
+
async function run(host, params, ctx, approved = false) {
|
|
113
|
+
const { memory } = host;
|
|
114
|
+
// An action the user's auto-approve setting covers runs without asking.
|
|
115
|
+
const ask = (context, title, message) =>
|
|
116
|
+
approved ? Promise.resolve(undefined) : confirmProposal(context, title, message);
|
|
117
|
+
const cwd = ctx.cwd;
|
|
118
|
+
switch (params?.action) {
|
|
119
|
+
case "projects":
|
|
120
|
+
return memory.projects(cwd);
|
|
121
|
+
case "list":
|
|
122
|
+
return { ok: true, text: await memory.list(cwd, params.project) };
|
|
123
|
+
case "view":
|
|
124
|
+
return memory.view(params.record, params.entry, cwd);
|
|
125
|
+
case "search":
|
|
126
|
+
return memory.search(params.query, params.scope, cwd);
|
|
127
|
+
case "focus":
|
|
128
|
+
return memory.focus(params.record, cwd);
|
|
129
|
+
case "detach":
|
|
130
|
+
return memory.detach(params.record, cwd);
|
|
131
|
+
case "attach":
|
|
132
|
+
return (
|
|
133
|
+
(await ask(
|
|
134
|
+
ctx,
|
|
135
|
+
"Attach task record?",
|
|
136
|
+
`The agent proposes working on ${show(params.record)} in this session.`,
|
|
137
|
+
)) ?? memory.attach(params.record, cwd)
|
|
138
|
+
);
|
|
139
|
+
case "create": {
|
|
140
|
+
const declined = await ask(
|
|
141
|
+
ctx,
|
|
142
|
+
"Create task?",
|
|
143
|
+
`Create "${show(params.title)}" in this repository's project and attach it to this session? Goal: ${show(params.goal) || show(params.title)}`,
|
|
144
|
+
);
|
|
145
|
+
if (declined) return declined;
|
|
146
|
+
const created = await memory.create(params.title, params.goal, cwd);
|
|
147
|
+
if (!created.ok || !created.id) return created;
|
|
148
|
+
const attached = await memory.attach(created.id, cwd);
|
|
149
|
+
return attached.ok
|
|
150
|
+
? {
|
|
151
|
+
...attached,
|
|
152
|
+
headline: `Created and attached ${created.id} — ${show(params.title)}`,
|
|
153
|
+
text: `Created ${created.id} and attached it to this session.\n\n${attached.text}`,
|
|
154
|
+
}
|
|
155
|
+
: { ...attached, text: `Created ${created.id}, but it was not attached: ${attached.text}` };
|
|
156
|
+
}
|
|
157
|
+
case "child":
|
|
158
|
+
return (
|
|
159
|
+
(await ask(
|
|
160
|
+
ctx,
|
|
161
|
+
"Create child task?",
|
|
162
|
+
`Create "${show(params.title)}" as a child of the focused task, for a parallel session you start?`,
|
|
163
|
+
)) ?? memory.child(params.title, params.goal)
|
|
164
|
+
);
|
|
165
|
+
case "fanout":
|
|
166
|
+
return (
|
|
167
|
+
(await ask(
|
|
168
|
+
ctx,
|
|
169
|
+
"Fan out a child task?",
|
|
170
|
+
`Create child "${show(params.title)}" of the focused task with its own Git worktree and branch? You start its session in a new terminal.`,
|
|
171
|
+
)) ?? memory.fanout(params.title, params.goal)
|
|
172
|
+
);
|
|
173
|
+
case "project_new":
|
|
174
|
+
case "project_link": {
|
|
175
|
+
const operation = params.action === "project_new" ? "new" : "link";
|
|
176
|
+
const declined = await ask(
|
|
177
|
+
ctx,
|
|
178
|
+
operation === "new" ? "Create project?" : "Link repository?",
|
|
179
|
+
operation === "new"
|
|
180
|
+
? `Create project "${show(params.name)}" and link this repository (${cwd}) to it?${show(params.worktrees) ? ` Child worktrees go in ${show(params.worktrees)}.` : ""}`
|
|
181
|
+
: `Link this repository (${cwd}) to project "${show(params.name)}"?`,
|
|
182
|
+
);
|
|
183
|
+
return declined ?? memory.project(operation, params.name, params.worktrees, cwd);
|
|
184
|
+
}
|
|
185
|
+
case "import":
|
|
186
|
+
return (
|
|
187
|
+
(await ask(
|
|
188
|
+
ctx,
|
|
189
|
+
"Import record?",
|
|
190
|
+
`Move ${show(params.record)} into the ${show(params.project)} project's store, with a task ID?`,
|
|
191
|
+
)) ?? memory.importRecord(params.record, params.project, cwd)
|
|
192
|
+
);
|
|
193
|
+
case "confirm":
|
|
194
|
+
return (
|
|
195
|
+
(await ask(
|
|
196
|
+
ctx,
|
|
197
|
+
"Make held record writable?",
|
|
198
|
+
`Let this session write ${show(params.record) || "its held records"}, taking ownership where another session owns one?`,
|
|
199
|
+
)) ?? memory.confirm(params.record, cwd)
|
|
200
|
+
);
|
|
201
|
+
case "env": {
|
|
202
|
+
const change = { primary: show(params.primary), add: show(params.add_dir), remove: show(params.remove_dir) };
|
|
203
|
+
if (change.primary || change.add || change.remove) {
|
|
204
|
+
const declined = await ask(
|
|
205
|
+
ctx,
|
|
206
|
+
"Change task directories?",
|
|
207
|
+
`For ${show(params.record)}: ${[change.primary && `primary ${change.primary}`, change.add && `add ${change.add}`, change.remove && `remove ${change.remove}`].filter(Boolean).join(", ")}?`,
|
|
208
|
+
);
|
|
209
|
+
if (declined) return declined;
|
|
210
|
+
}
|
|
211
|
+
return memory.environment(params.record, change, cwd);
|
|
212
|
+
}
|
|
213
|
+
case "remove":
|
|
214
|
+
case "project_remove": {
|
|
215
|
+
// Removal always asks: no auto-approve setting covers it (Task 006).
|
|
216
|
+
const target = params.action === "remove" ? { task: params.record } : { project: params.name };
|
|
217
|
+
const { question, error } = await memory.removalQuestion(target);
|
|
218
|
+
if (!question) return { ok: false, text: error ?? "Nothing to remove." };
|
|
219
|
+
const title = params.action === "remove" ? "Remove task?" : "Remove project?";
|
|
220
|
+
return (await confirmProposal(ctx, title, question)) ?? memory.remove(target);
|
|
221
|
+
}
|
|
222
|
+
default:
|
|
223
|
+
return { ok: false, text: `action must be one of ${ACTIONS.join(", ")}.` };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Declare or withdraw task_memory when task memory's effective state changes, keeping other tools in place. */
|
|
227
|
+
export function applyTaskMemoryTools(pi, effective) {
|
|
228
|
+
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
229
|
+
const current = pi.getActiveTools();
|
|
230
|
+
const present = current.includes(TASK_MEMORY_TOOL);
|
|
231
|
+
if (effective && !present) pi.setActiveTools([...current, TASK_MEMORY_TOOL]);
|
|
232
|
+
if (!effective && present) pi.setActiveTools(current.filter((name) => name !== TASK_MEMORY_TOOL));
|
|
233
|
+
}
|
|
@@ -2,7 +2,7 @@ import { APPLY_PATCH_LARK_GRAMMAR } from "./grammar.js";
|
|
|
2
2
|
import { applyPatch } from "./plan.js";
|
|
3
3
|
export const APPLY_PATCH = "apply_patch";
|
|
4
4
|
const DESCRIPTION =
|
|
5
|
-
"Use
|
|
5
|
+
"Use apply_patch to change several files in one call, or to add, delete or move files together with other changes; to change one existing file, use edit. The 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). Copy context lines from your latest view of the file, enough for each hunk to match one place.";
|
|
6
6
|
const CODES = { add: "A", update: "M", move: "M", delete: "D" };
|
|
7
7
|
/** Codex's summary on success, so GPT models read the result they were trained on. */
|
|
8
8
|
export function resultText(result) {
|
|
@@ -3,14 +3,27 @@ import { createWriteStream } from "node:fs";
|
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
+
import { StringDecoder } from "node:string_decoder";
|
|
6
7
|
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
export const BASH_BACKGROUND = "bash_background";
|
|
9
|
+
export const MONITOR = "monitor";
|
|
8
10
|
export const STOP_BACKGROUND = "stop_background";
|
|
9
11
|
export const NOTICE_TYPE = "freeflow-background";
|
|
12
|
+
export const MONITOR_TYPE = "freeflow-monitor";
|
|
13
|
+
/** Background commands and monitors together. */
|
|
10
14
|
export const MAX_RUNNING = 16;
|
|
15
|
+
export const MONITOR_DEFAULT_MS = 5 * 60_000;
|
|
16
|
+
export const MONITOR_MAX_MS = 30 * 60_000;
|
|
17
|
+
/** Event notices a monitor may send per minute before it is stopped; each one can start a model turn. */
|
|
18
|
+
export const MONITOR_PER_MINUTE = 20;
|
|
19
|
+
/** Lines one event notice shows; the rest stay in the output file. */
|
|
20
|
+
export const MONITOR_MAX_LINES = 40;
|
|
21
|
+
const MAX_LINE_CHARS = 1_000;
|
|
11
22
|
/** Marks harness messages, which arrive on their own schedule, as not written by the user. */
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
import { freeflowNotice } from "../host/inbound.js";
|
|
24
|
+
export { freeflowNotice };
|
|
25
|
+
const newId = (prefix) => `${prefix}${randomBytes(6).readUIntBE(0, 6).toString(36).slice(-6).padStart(6, "0")}`;
|
|
26
|
+
const shellQuote = (text) => `'${text.replace(/'/g, `'\\''`)}'`;
|
|
14
27
|
function labelFor(command, description) {
|
|
15
28
|
const text = description?.trim() || command.trim().split("\n")[0];
|
|
16
29
|
return text.length > 80 ? `${text.slice(0, 77)}...` : text;
|
|
@@ -28,6 +41,10 @@ function close(stream, trailer) {
|
|
|
28
41
|
export class BackgroundJobs {
|
|
29
42
|
host;
|
|
30
43
|
jobs = new Map();
|
|
44
|
+
/** Launches between their check of MAX_RUNNING and their job's registration. */
|
|
45
|
+
starting = 0;
|
|
46
|
+
/** Changes when the session ends, so a launch that was still starting then does not run. */
|
|
47
|
+
generation = 0;
|
|
31
48
|
constructor(host) {
|
|
32
49
|
this.host = host;
|
|
33
50
|
}
|
|
@@ -35,39 +52,101 @@ export class BackgroundJobs {
|
|
|
35
52
|
return [...this.jobs.values()].filter((job) => job.status === "running");
|
|
36
53
|
}
|
|
37
54
|
async start(command, description, cwd, sessionId) {
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
return this.launch("command", command, description, cwd, sessionId);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Claude Code's Monitor: each line the command prints to stdout is an event for the model, batched by the host;
|
|
59
|
+
* stderr goes to the output file only. One notice when the command exits, expires, or is stopped for sending too
|
|
60
|
+
* much.
|
|
61
|
+
*/
|
|
62
|
+
async monitor(command, description, timeoutMs, cwd, sessionId) {
|
|
63
|
+
const ms = Math.min(Math.max(Math.round(timeoutMs) || MONITOR_DEFAULT_MS, 1_000), MONITOR_MAX_MS);
|
|
64
|
+
return this.launch("monitor", command, description, cwd, sessionId, ms);
|
|
65
|
+
}
|
|
66
|
+
async launch(kind, command, description, cwd, sessionId, timeoutMs) {
|
|
67
|
+
if (this.running().length + this.starting >= MAX_RUNNING)
|
|
40
68
|
throw new Error(`${MAX_RUNNING} background commands are running. Stop one with stop_background, then retry.`);
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
const prefix = kind === "monitor" ? "mon" : "bg";
|
|
70
|
+
let id = newId(prefix);
|
|
71
|
+
while (this.jobs.has(id)) id = newId(prefix);
|
|
72
|
+
const generation = this.generation;
|
|
43
73
|
const folder = join(tmpdir(), "freeflow", sessionId.replace(/[^A-Za-z0-9._-]/g, "_"), "background");
|
|
44
|
-
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
45
74
|
const outputPath = join(folder, `${id}.output`);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
// A monitor's stderr is appended to the same file by the shell, so both writers append.
|
|
76
|
+
let stream;
|
|
77
|
+
this.starting++;
|
|
78
|
+
try {
|
|
79
|
+
await mkdir(folder, { recursive: true, mode: 0o700 });
|
|
80
|
+
stream = createWriteStream(outputPath, { flags: kind === "monitor" ? "a" : "w", mode: 0o600 });
|
|
81
|
+
await new Promise((resolve, reject) => {
|
|
82
|
+
stream.once("open", () => resolve());
|
|
83
|
+
stream.once("error", reject);
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
this.starting--;
|
|
87
|
+
}
|
|
88
|
+
if (generation !== this.generation) {
|
|
89
|
+
await close(stream, "[not started: the session ended]\n");
|
|
90
|
+
throw new Error("The session ended before the command started.");
|
|
91
|
+
}
|
|
51
92
|
const controller = new AbortController();
|
|
52
93
|
let endsWithNewline = true;
|
|
53
94
|
const job = {
|
|
54
95
|
id,
|
|
96
|
+
kind,
|
|
55
97
|
label: labelFor(command, description),
|
|
56
98
|
outputPath,
|
|
57
99
|
controller,
|
|
58
100
|
status: "running",
|
|
101
|
+
events: 0,
|
|
59
102
|
done: Promise.resolve(),
|
|
60
103
|
};
|
|
61
|
-
|
|
104
|
+
let partial = "";
|
|
105
|
+
// One decoder per job, so a character split across two reads is decoded whole.
|
|
106
|
+
const decoder = new StringDecoder("utf8");
|
|
107
|
+
const event = (line) => {
|
|
108
|
+
job.events++;
|
|
109
|
+
const text = line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS)}... (line cut)` : line;
|
|
110
|
+
this.host.event(
|
|
111
|
+
id,
|
|
112
|
+
text,
|
|
113
|
+
(lines) => this.eventText(job, lines),
|
|
114
|
+
() => this.halt(job, "limit"),
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
const script = kind === "monitor" ? `{\n${command}\n} 2>>${shellQuote(outputPath)}` : command;
|
|
118
|
+
const run = createLocalBashOperations({ shellPath: this.host.shellPath() }).exec(script, cwd, {
|
|
62
119
|
onData: (data) => {
|
|
63
120
|
if (data.length === 0) return;
|
|
64
121
|
endsWithNewline = data[data.length - 1] === 10;
|
|
65
122
|
stream.write(data);
|
|
123
|
+
if (kind !== "monitor" || job.stopping) return;
|
|
124
|
+
const lines = (partial + decoder.write(data)).split("\n");
|
|
125
|
+
// An unfinished line keeps only what a terminal would show (the text after its last carriage return), so a
|
|
126
|
+
// progress bar redrawn with \r does not grow it without bound; the output file has everything.
|
|
127
|
+
partial = (lines.pop() ?? "")
|
|
128
|
+
.split("\r")
|
|
129
|
+
.at(-1)
|
|
130
|
+
.slice(-4 * MAX_LINE_CHARS);
|
|
131
|
+
for (const line of lines) if (line.trim() !== "") event(line.replace(/\r$/, ""));
|
|
66
132
|
},
|
|
67
133
|
signal: controller.signal,
|
|
68
134
|
});
|
|
135
|
+
const timer =
|
|
136
|
+
timeoutMs === undefined ? undefined : setTimeout(() => this.halt(job, "expired"), timeoutMs).unref?.();
|
|
137
|
+
const finish = () => {
|
|
138
|
+
if (timer) clearTimeout(timer);
|
|
139
|
+
job.ended = true;
|
|
140
|
+
if (kind !== "monitor") return;
|
|
141
|
+
partial += decoder.end();
|
|
142
|
+
if (partial.trim() !== "" && !job.stopping) event(partial);
|
|
143
|
+
partial = "";
|
|
144
|
+
if (job.stopping === "shutdown" && this.host.discardEvents) this.host.discardEvents(id);
|
|
145
|
+
else this.host.endEvents(id);
|
|
146
|
+
};
|
|
69
147
|
job.done = run.then(
|
|
70
148
|
async ({ exitCode }) => {
|
|
149
|
+
finish();
|
|
71
150
|
const code = exitCode ?? 1;
|
|
72
151
|
job.exitCode = code;
|
|
73
152
|
job.status = code === 0 ? "completed" : "failed";
|
|
@@ -75,10 +154,20 @@ export class BackgroundJobs {
|
|
|
75
154
|
this.notify(job);
|
|
76
155
|
},
|
|
77
156
|
async (error) => {
|
|
157
|
+
finish();
|
|
78
158
|
const lead = endsWithNewline ? "" : "\n";
|
|
79
159
|
if (job.stopping) {
|
|
80
160
|
job.status = "stopped";
|
|
81
|
-
|
|
161
|
+
const trailer =
|
|
162
|
+
job.stopping === "tool"
|
|
163
|
+
? "[stopped]"
|
|
164
|
+
: job.stopping === "shutdown"
|
|
165
|
+
? "[stopped at session end]"
|
|
166
|
+
: job.stopping === "expired"
|
|
167
|
+
? "[stopped: timeout]"
|
|
168
|
+
: "[stopped: too many events]";
|
|
169
|
+
await close(stream, `${lead}${trailer}\n`);
|
|
170
|
+
if (job.stopping === "expired" || job.stopping === "limit") this.notify(job);
|
|
82
171
|
return;
|
|
83
172
|
}
|
|
84
173
|
// The command could not run (for example, the working directory is gone).
|
|
@@ -89,7 +178,25 @@ export class BackgroundJobs {
|
|
|
89
178
|
},
|
|
90
179
|
);
|
|
91
180
|
this.jobs.set(id, job);
|
|
92
|
-
return { id, outputPath };
|
|
181
|
+
return { id, outputPath, ...(timeoutMs === undefined ? {} : { timeoutMs }) };
|
|
182
|
+
}
|
|
183
|
+
/** Stop a monitor on its own account: its timeout passed, or it sent more than its rate limit allows. */
|
|
184
|
+
halt(job, reason) {
|
|
185
|
+
if (job.status !== "running" || job.stopping || job.ended) return;
|
|
186
|
+
job.stopping = reason;
|
|
187
|
+
job.controller.abort();
|
|
188
|
+
}
|
|
189
|
+
eventText(job, lines) {
|
|
190
|
+
const shown = lines.slice(0, MONITOR_MAX_LINES);
|
|
191
|
+
const more = lines.length - shown.length;
|
|
192
|
+
return freeflowNotice(
|
|
193
|
+
"monitor",
|
|
194
|
+
[
|
|
195
|
+
`Monitor ${job.id} (${job.label}):`,
|
|
196
|
+
...shown,
|
|
197
|
+
...(more > 0 ? [`... ${more} more lines in ${job.outputPath}`] : []),
|
|
198
|
+
].join("\n"),
|
|
199
|
+
);
|
|
93
200
|
}
|
|
94
201
|
async stop(id) {
|
|
95
202
|
const job = this.jobs.get(id);
|
|
@@ -117,6 +224,7 @@ export class BackgroundJobs {
|
|
|
117
224
|
}
|
|
118
225
|
/** Session end: stop every running job, wait for the processes to exit, forget them. No notice is sent. */
|
|
119
226
|
async stopAll() {
|
|
227
|
+
this.generation++;
|
|
120
228
|
const running = this.running();
|
|
121
229
|
for (const job of running) {
|
|
122
230
|
job.stopping = "shutdown";
|
|
@@ -129,15 +237,45 @@ export class BackgroundJobs {
|
|
|
129
237
|
restate() {
|
|
130
238
|
const running = this.running();
|
|
131
239
|
if (running.length === 0) return;
|
|
132
|
-
const list = running
|
|
240
|
+
const list = running
|
|
241
|
+
.map((job) => `${job.kind === "monitor" ? "monitor " : ""}${job.id} (${job.label}), output ${job.outputPath}`)
|
|
242
|
+
.join("; ");
|
|
133
243
|
this.host.send(
|
|
134
|
-
|
|
244
|
+
freeflowNotice(
|
|
245
|
+
"background",
|
|
246
|
+
`Still running: ${list}. Do not start them again; stop one with stop_background before restarting it.`,
|
|
247
|
+
),
|
|
135
248
|
{ running: running.map((job) => ({ id: job.id, outputPath: job.outputPath })) },
|
|
136
249
|
false,
|
|
137
250
|
);
|
|
138
251
|
}
|
|
139
252
|
notify(job, runError) {
|
|
140
|
-
if (job.stopping) return;
|
|
253
|
+
if (job.stopping === "tool" || job.stopping === "shutdown") return;
|
|
254
|
+
const details = {
|
|
255
|
+
id: job.id,
|
|
256
|
+
...(job.kind === "monitor" ? { kind: job.kind } : {}),
|
|
257
|
+
status: job.status,
|
|
258
|
+
...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
|
|
259
|
+
...(job.kind === "monitor" ? { events: job.events } : {}),
|
|
260
|
+
outputPath: job.outputPath,
|
|
261
|
+
};
|
|
262
|
+
if (job.kind === "monitor") {
|
|
263
|
+
const events = `${job.events} ${job.events === 1 ? "event" : "events"}`;
|
|
264
|
+
const outcome =
|
|
265
|
+
job.stopping === "expired"
|
|
266
|
+
? `reached its timeout after ${events} and was stopped. Start it again to keep watching.`
|
|
267
|
+
: job.stopping === "limit"
|
|
268
|
+
? `was stopped after ${events}: it sent more than ${MONITOR_PER_MINUTE} notices in a minute. Filter its output to the lines you would act on, then start it again.`
|
|
269
|
+
: runError !== undefined
|
|
270
|
+
? `failed to run: ${runError}.`
|
|
271
|
+
: `exited with code ${job.exitCode} after ${events}.`;
|
|
272
|
+
this.host.send(
|
|
273
|
+
freeflowNotice("monitor", `Monitor ${job.id} (${job.label}) ${outcome} Output: ${job.outputPath}.`),
|
|
274
|
+
details,
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
141
279
|
const outcome =
|
|
142
280
|
runError !== undefined
|
|
143
281
|
? `failed to run: ${runError}`
|
|
@@ -145,23 +283,29 @@ export class BackgroundJobs {
|
|
|
145
283
|
? "completed"
|
|
146
284
|
: `failed with exit code ${job.exitCode}`;
|
|
147
285
|
this.host.send(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
id: job.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
outputPath: job.outputPath,
|
|
154
|
-
},
|
|
286
|
+
freeflowNotice(
|
|
287
|
+
"background",
|
|
288
|
+
`Background command ${job.id} (${job.label}) ${outcome}. Output: ${job.outputPath}.`,
|
|
289
|
+
),
|
|
290
|
+
details,
|
|
155
291
|
true,
|
|
156
292
|
);
|
|
157
293
|
}
|
|
158
294
|
}
|
|
159
295
|
const START_DESCRIPTION = [
|
|
160
|
-
"Run a shell command in the background
|
|
296
|
+
"Run a shell command in the background; it returns at once with an ID and an output file. Use it for servers, watchers, and builds, test runs or installs expected to take over two minutes; run shorter commands with bash.",
|
|
297
|
+
"You are notified once, when the command exits; if you are idle then, the notice starts a new turn. Meanwhile do other work, and read the output file only for output so far; do not poll it.",
|
|
161
298
|
"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.",
|
|
299
|
+
"Commands stop when the session ends; their output files stay readable.",
|
|
300
|
+
].join("\n\n");
|
|
301
|
+
const MONITOR_DESCRIPTION = [
|
|
302
|
+
"Use monitor to act on a running command's lines as they appear: errors in a log while you test, failures in a long test run. For one signal (a server is ready, a build finished), use bash_background with a command that exits when the condition holds.",
|
|
303
|
+
"Each line the command prints to stdout reaches you as a notice (lines within 200 ms join one notice); stderr goes only to the output file. You also get one notice when the command exits or when timeout_ms passes (default 5 minutes, at most 30); start it again to keep watching. A notice that arrives while you are idle starts a model turn, so make the command print only lines you would act on; one that prints too much is stopped.",
|
|
304
|
+
"Every stage of a pipe must flush each line (grep --line-buffered, awk with fflush()), or lines wait in a buffer; head cannot.",
|
|
305
|
+
"Silence is not success: match every outcome you would act on, failures as well as success, for example tail -f app.log | grep -E --line-buffered 'ready|ERROR|Traceback|FAILED'.",
|
|
162
306
|
].join("\n\n");
|
|
163
307
|
const STOP_DESCRIPTION =
|
|
164
|
-
"Stop a background command
|
|
308
|
+
"Stop a background command or monitor once neither you nor the user needs it, such as a server after the checks that used it. Returns after it has exited.";
|
|
165
309
|
/** Registered once, inactive until Tool Execution is effective (see tool-execution/tools.ts). */
|
|
166
310
|
export function registerBackgroundTools(pi, jobs, host) {
|
|
167
311
|
const guard = () => {
|
|
@@ -208,6 +352,54 @@ export function registerBackgroundTools(pi, jobs, host) {
|
|
|
208
352
|
};
|
|
209
353
|
},
|
|
210
354
|
});
|
|
355
|
+
pi.registerTool({
|
|
356
|
+
name: MONITOR,
|
|
357
|
+
label: MONITOR,
|
|
358
|
+
description: MONITOR_DESCRIPTION,
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
additionalProperties: false,
|
|
362
|
+
properties: {
|
|
363
|
+
command: { type: "string", description: "Shell command whose stdout lines are the events" },
|
|
364
|
+
description: { type: "string", description: "A few words saying what is watched; shown with every event" },
|
|
365
|
+
timeout_ms: {
|
|
366
|
+
type: "number",
|
|
367
|
+
minimum: 1_000,
|
|
368
|
+
maximum: MONITOR_MAX_MS,
|
|
369
|
+
description: `Stop watching after this long (default ${MONITOR_DEFAULT_MS}, at most ${MONITOR_MAX_MS})`,
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
required: ["command", "description"],
|
|
373
|
+
},
|
|
374
|
+
outputSchema: {
|
|
375
|
+
type: "object",
|
|
376
|
+
properties: { id: { type: "string" }, outputPath: { type: "string" }, timeoutMs: { type: "number" } },
|
|
377
|
+
required: ["id", "outputPath", "timeoutMs"],
|
|
378
|
+
},
|
|
379
|
+
exposure: "direct",
|
|
380
|
+
defaultActive: false,
|
|
381
|
+
async execute(_id, params, _signal, _u, ctx) {
|
|
382
|
+
guard();
|
|
383
|
+
const started = await jobs.monitor(
|
|
384
|
+
String(params?.command ?? ""),
|
|
385
|
+
String(params?.description ?? ""),
|
|
386
|
+
Number(params?.timeout_ms ?? MONITOR_DEFAULT_MS),
|
|
387
|
+
ctx.cwd,
|
|
388
|
+
ctx.sessionManager?.getSessionId?.() ?? "session",
|
|
389
|
+
);
|
|
390
|
+
const minutes = Math.round(((started.timeoutMs ?? MONITOR_DEFAULT_MS) / 60_000) * 10) / 10;
|
|
391
|
+
return {
|
|
392
|
+
content: [
|
|
393
|
+
{
|
|
394
|
+
type: "text",
|
|
395
|
+
text: `Monitor ${started.id} started. Each line it prints reaches you as a notice; it stops when the command exits, after ${minutes} min, or with stop_background. When you tell the user you are watching, say it stops after ${minutes} min unless restarted. Output: ${started.outputPath}.`,
|
|
396
|
+
},
|
|
397
|
+
],
|
|
398
|
+
details: started,
|
|
399
|
+
structuredContent: started,
|
|
400
|
+
};
|
|
401
|
+
},
|
|
402
|
+
});
|
|
211
403
|
pi.registerTool({
|
|
212
404
|
name: STOP_BACKGROUND,
|
|
213
405
|
label: STOP_BACKGROUND,
|
|
@@ -215,7 +407,7 @@ export function registerBackgroundTools(pi, jobs, host) {
|
|
|
215
407
|
parameters: {
|
|
216
408
|
type: "object",
|
|
217
409
|
additionalProperties: false,
|
|
218
|
-
properties: { id: { type: "string", description: "The ID bash_background returned" } },
|
|
410
|
+
properties: { id: { type: "string", description: "The ID bash_background or monitor returned" } },
|
|
219
411
|
required: ["id"],
|
|
220
412
|
},
|
|
221
413
|
outputSchema: {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export const DEFAULT_TOOL_EXECUTION_CONFIG = Object.freeze({
|
|
1
|
+
export const DEFAULT_TOOL_EXECUTION_CONFIG = Object.freeze({
|
|
2
|
+
enabled: true,
|
|
3
|
+
guidanceNotices: true,
|
|
4
|
+
});
|
|
5
|
+
/** The keys `toolExecution` acts on; the settings test checks each has a /freeflow settings row. */
|
|
6
|
+
export const TOOL_EXECUTION_KEYS = ["enabled", "guidanceNotices"];
|
|
2
7
|
// Settings of the retired v2 runtime (.deprecated/tool-execution-v2). Existing configuration files keep loading;
|
|
3
8
|
// the values no longer do anything.
|
|
4
9
|
const RETIRED_KEYS = ["capture", "programs", "workspace", "discovery", "adapters", "accounting"];
|
|
@@ -7,10 +12,12 @@ function record(value) {
|
|
|
7
12
|
}
|
|
8
13
|
export function validateToolExecutionConfig(value) {
|
|
9
14
|
if (!record(value)) return "toolExecution must be an object";
|
|
10
|
-
const unknown = Object.keys(value).find((key) => key
|
|
15
|
+
const unknown = Object.keys(value).find((key) => !TOOL_EXECUTION_KEYS.includes(key) && !RETIRED_KEYS.includes(key));
|
|
11
16
|
if (unknown) return `toolExecution has unsupported key: ${unknown}`;
|
|
12
17
|
if (value.enabled !== undefined && typeof value.enabled !== "boolean")
|
|
13
18
|
return "toolExecution.enabled must be a boolean";
|
|
19
|
+
if (value.guidanceNotices !== undefined && typeof value.guidanceNotices !== "boolean")
|
|
20
|
+
return "toolExecution.guidanceNotices must be a boolean";
|
|
14
21
|
return null;
|
|
15
22
|
}
|
|
16
23
|
export function resolveToolExecutionConfig(repository, local, freeflowEnabled) {
|
|
@@ -18,5 +25,9 @@ export function resolveToolExecutionConfig(repository, local, freeflowEnabled) {
|
|
|
18
25
|
const localTool = record(local.toolExecution) ? local.toolExecution : {};
|
|
19
26
|
const setting = Object.hasOwn(localTool, "enabled") ? localTool.enabled : repositoryTool.enabled;
|
|
20
27
|
const enabled = setting === undefined ? DEFAULT_TOOL_EXECUTION_CONFIG.enabled : setting === true;
|
|
21
|
-
|
|
28
|
+
const notices = Object.hasOwn(localTool, "guidanceNotices")
|
|
29
|
+
? localTool.guidanceNotices
|
|
30
|
+
: repositoryTool.guidanceNotices;
|
|
31
|
+
const guidanceNotices = notices === undefined ? DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices : notices === true;
|
|
32
|
+
return { enabled, guidanceNotices, effective: freeflowEnabled && enabled };
|
|
22
33
|
}
|