diff --git a/.github/workflows/plugin-scanner.yml b/.github/workflows/plugin-scanner.yml index d2a9e6cf..a0418446 100644 --- a/.github/workflows/plugin-scanner.yml +++ b/.github/workflows/plugin-scanner.yml @@ -22,5 +22,14 @@ jobs: with: plugin_dir: "." mode: scan + format: json + output: plugin-security-report.json + pr_comment: "off" min_score: 80 fail_on_severity: high + - name: Show complete scanner findings + if: always() + run: | + if [ -f plugin-security-report.json ]; then + cat plugin-security-report.json + fi diff --git a/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md b/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md index 22631344..a20ca01a 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/claude-taskflow/plugin/skills/taskflow/SKILL.md @@ -147,9 +147,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -572,7 +572,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -624,8 +624,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/claude-taskflow/plugin/skills/taskflow/configuration.md b/packages/claude-taskflow/plugin/skills/taskflow/configuration.md index eac25e01..74743d40 100644 --- a/packages/claude-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/claude-taskflow/plugin/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -268,8 +268,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -431,7 +431,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -493,7 +493,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -512,7 +512,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -551,22 +551,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md b/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md index 491fc8c2..9aeda667 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/codex-taskflow/plugin/skills/taskflow/SKILL.md @@ -142,9 +142,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -567,7 +567,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -619,8 +619,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/codex-taskflow/plugin/skills/taskflow/configuration.md b/packages/codex-taskflow/plugin/skills/taskflow/configuration.md index 402d7546..51e663ca 100644 --- a/packages/codex-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/codex-taskflow/plugin/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -268,8 +268,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -428,7 +428,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -490,7 +490,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -509,7 +509,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -548,22 +548,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md b/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md index 38d3c223..80ece90b 100644 --- a/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/grok-taskflow/plugin/skills/taskflow/SKILL.md @@ -152,9 +152,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -577,7 +577,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -629,8 +629,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/grok-taskflow/plugin/skills/taskflow/configuration.md b/packages/grok-taskflow/plugin/skills/taskflow/configuration.md index f3f10ed7..c219fd35 100644 --- a/packages/grok-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/grok-taskflow/plugin/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -268,8 +268,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -438,7 +438,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -500,7 +500,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -519,7 +519,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -558,22 +558,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/hermes-taskflow/plugin/skills/taskflow/SKILL.md b/packages/hermes-taskflow/plugin/skills/taskflow/SKILL.md index cc03cc2f..94dd29c6 100644 --- a/packages/hermes-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/hermes-taskflow/plugin/skills/taskflow/SKILL.md @@ -147,9 +147,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -572,7 +572,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -624,8 +624,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/hermes-taskflow/plugin/skills/taskflow/configuration.md b/packages/hermes-taskflow/plugin/skills/taskflow/configuration.md index 1d9045e4..ee7c0db6 100644 --- a/packages/hermes-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/hermes-taskflow/plugin/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -268,8 +268,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -435,7 +435,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -497,7 +497,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -516,7 +516,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -555,22 +555,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md b/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md index 14dae09c..4f3ad21d 100644 --- a/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md +++ b/packages/opencode-taskflow/plugin/skills/taskflow/SKILL.md @@ -143,9 +143,9 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -568,7 +568,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -620,8 +620,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md b/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md index fe4c2044..2d593b51 100644 --- a/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md +++ b/packages/opencode-taskflow/plugin/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -268,8 +268,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -428,7 +428,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -490,7 +490,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -509,7 +509,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -548,22 +548,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/pi-taskflow/skills/taskflow/SKILL.md b/packages/pi-taskflow/skills/taskflow/SKILL.md index c55ea753..620c7e0e 100644 --- a/packages/pi-taskflow/skills/taskflow/SKILL.md +++ b/packages/pi-taskflow/skills/taskflow/SKILL.md @@ -122,11 +122,11 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** (typically in the OS tmp dir) and point every call at it with `defineFile`: ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: { "action": "verify", "defineFile": "/tmp/audit.json" } // zero tokens { "action": "compile", "defineFile": "/tmp/audit.json" } // diagram + report -{ "action": "run", "defineFile": "/tmp/audit.json", "args": { … } } +{ "action": "run", "defineFile": "/tmp/audit.json", "args": { ... } } ``` The file can be raw JSON **or** a Markdown doc with a fenced ```json block @@ -543,7 +543,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -586,8 +586,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true } diff --git a/packages/pi-taskflow/skills/taskflow/configuration.md b/packages/pi-taskflow/skills/taskflow/configuration.md index 231cac2f..ea43c96e 100644 --- a/packages/pi-taskflow/skills/taskflow/configuration.md +++ b/packages/pi-taskflow/skills/taskflow/configuration.md @@ -49,7 +49,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -66,11 +66,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -86,7 +86,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -274,8 +274,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -302,7 +302,7 @@ Notes: sandbox profile, while the other hosts use their own permission contracts. Omit it to request the host's default capability policy. - Each phase runs as an isolated process: - `pi --mode json -p --no-session [--model …] [--thinking …] [--tools …] [--append-system-prompt ] "Task: …"`. + `pi --mode json -p --no-session [--model ...] [--thinking ...] [--tools ...] [--append-system-prompt ] "Task: ..."`. For Codex, OpenCode, Grok, or Hermes, an operator can intentionally pass additional task-specific environment variables by listing their names in the @@ -440,7 +440,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -504,7 +504,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -523,7 +523,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -562,22 +562,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/packages/pi-taskflow/src/runner.ts b/packages/pi-taskflow/src/runner.ts index dc0d1cf5..d5544a20 100644 --- a/packages/pi-taskflow/src/runner.ts +++ b/packages/pi-taskflow/src/runner.ts @@ -455,6 +455,9 @@ export async function runAgentTask( args.push(`Task: ${task}`); const invocation = getPiInvocation(args); const childEnv = { ...process.env, ...ctxEnv }; + // The owning run wins over an inherited parent marker. Direct callers + // without an identity keep the existing environment behavior. + if (opts.runId !== undefined) childEnv.PI_SUBAGENT_RUN_ID = opts.runId; // A child agent is not a host principal. Never let it inherit the // operator's resolve-only bridge opt-in and mint equivalent authority. delete childEnv[CWD_BRIDGE_MODE_ENV]; diff --git a/packages/pi-taskflow/test/run-identity.test.ts b/packages/pi-taskflow/test/run-identity.test.ts new file mode 100644 index 00000000..4d4c4cfa --- /dev/null +++ b/packages/pi-taskflow/test/run-identity.test.ts @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { test } from "node:test"; +import type { AgentConfig } from "taskflow-core"; +import { createPiSubagentRunner, runAgentTask } from "../src/runner.ts"; + +test("Pi run identity: owning run overrides ambient identity and omitted identity preserves direct-call behavior", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "tf-pi-run-id-")); + const capture = path.join(dir, "capture.json"); + const probe = path.join(dir, "probe.mjs"); + fs.writeFileSync(probe, `#!${process.execPath}\n` + + `import { writeFileSync } from "node:fs";\n` + + `writeFileSync(${JSON.stringify(capture)}, JSON.stringify({\n` + + ` runId: process.env.PI_SUBAGENT_RUN_ID ?? null,\n` + + ` devloopsRunId: process.env.DEVLOOPS_RUN_ID ?? null,\n` + + ` ctxDir: process.env.PI_TASKFLOW_CTX_DIR ?? null,\n` + + ` nodeId: process.env.PI_TASKFLOW_NODE_ID ?? null,\n` + + ` argv: process.argv.slice(2),\n` + + `}));\n` + + `const emit = x => process.stdout.write(JSON.stringify(x) + "\\n");\n` + + `emit({ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "ok" }], stopReason: "stop" } });\n` + + `emit({ type: "agent_end", willRetry: false });\n` + + `emit({ type: "agent_settled" });\n`, { mode: 0o755 }); + const envKeys = ["PI_TASKFLOW_PI_BIN", "PI_SUBAGENT_RUN_ID", "DEVLOOPS_RUN_ID", "PI_TASKFLOW_CTX_DIR", "PI_TASKFLOW_NODE_ID"]; + const previous = new Map(envKeys.map((key) => [key, process.env[key]])); + const agents: AgentConfig[] = [{ name: "probe", description: "probe", systemPrompt: "", source: "user", filePath: "" }]; + const readCapture = () => JSON.parse(fs.readFileSync(capture, "utf8")) as { + runId: string | null; devloopsRunId: string | null; ctxDir: string | null; nodeId: string | null; argv: string[]; + }; + try { + for (const key of envKeys) delete process.env[key]; + process.env.PI_TASKFLOW_PI_BIN = probe; + assert.equal((await runAgentTask(dir, agents, "probe", "direct", {})).exitCode, 0); + assert.equal(readCapture().runId, null, "No identity is fabricated for direct callers"); + + assert.equal((await runAgentTask(dir, agents, "probe", "owned", { runId: "taskflow-owned" })).exitCode, 0); + assert.equal(readCapture().runId, "taskflow-owned"); + assert.equal(process.env.PI_SUBAGENT_RUN_ID, undefined, "Parent environment must not be mutated"); + assert.equal(readCapture().devloopsRunId, null, "No downstream-specific marker is fabricated"); + assert.ok(readCapture().argv.includes("--no-extensions"), "Run identity does not weaken child isolation"); + + process.env.PI_SUBAGENT_RUN_ID = "parent-run"; + process.env.DEVLOOPS_RUN_ID = "downstream-owned"; + assert.equal((await runAgentTask(dir, agents, "probe", "inherited", {})).exitCode, 0); + assert.equal(readCapture().runId, "parent-run", "Omitted identity preserves ambient behavior"); + + const result = await createPiSubagentRunner().runTask(dir, agents, "probe", "owned-with-context", { + runId: "current-taskflow-run", ctxDir: dir, nodeId: "work", + }); + assert.equal(result.exitCode, 0); + const child = readCapture(); + assert.equal(child.runId, "current-taskflow-run", "Owning identity replaces stale inherited Pi marker"); + assert.equal(child.devloopsRunId, "downstream-owned", "Dev-loops retains ownership and precedence of its neutral marker"); + assert.equal(child.ctxDir, dir); + assert.equal(child.nodeId, "work"); + assert.ok(child.argv.includes("--extension")); + assert.equal(process.env.PI_SUBAGENT_RUN_ID, "parent-run"); + } finally { + for (const [key, value] of previous) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + fs.rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/taskflow-core/src/exec/step-kinds.ts b/packages/taskflow-core/src/exec/step-kinds.ts index 1135a690..9d4dbf5d 100644 --- a/packages/taskflow-core/src/exec/step-kinds.ts +++ b/packages/taskflow-core/src/exec/step-kinds.ts @@ -146,6 +146,7 @@ async function runAgentCall( agentName, effectiveTask, { + runId: ctx.state.runId, model: phase.model, thinking: phase.thinking, tools: phase.tools, diff --git a/packages/taskflow-core/src/exec/step.ts b/packages/taskflow-core/src/exec/step.ts index 9b118f19..760d5851 100644 --- a/packages/taskflow-core/src/exec/step.ts +++ b/packages/taskflow-core/src/exec/step.ts @@ -351,6 +351,7 @@ async function runOneAgent( agentName, effectiveTask, { + runId: ctx.state.runId, model: phase.model, thinking: phase.thinking, tools: phase.tools, diff --git a/packages/taskflow-core/src/host/runner-types.ts b/packages/taskflow-core/src/host/runner-types.ts index 091839d1..000bae1e 100644 --- a/packages/taskflow-core/src/host/runner-types.ts +++ b/packages/taskflow-core/src/host/runner-types.ts @@ -87,6 +87,9 @@ export interface LiveUpdate { /** Per-run knobs the engine passes to whichever host runner executes the task. */ export interface RunOptions { + /** Owning Taskflow state's run identity, supplied by the runtime rather than + * flow input. Hosts may expose it to their children; direct callers may omit it. */ + runId?: string; model?: string; thinking?: string; tools?: string[]; diff --git a/packages/taskflow-core/src/runtime.ts b/packages/taskflow-core/src/runtime.ts index fab15cb0..2f816b88 100644 --- a/packages/taskflow-core/src/runtime.ts +++ b/packages/taskflow-core/src/runtime.ts @@ -1715,6 +1715,7 @@ async function executePhaseInner( } opts?.promptCalls?.push(task); const runOptions = { + runId: state.runId, model: phase.model, thinking: phase.thinking, tools: phase.tools, diff --git a/packages/taskflow-core/test/run-identity.test.ts b/packages/taskflow-core/test/run-identity.test.ts new file mode 100644 index 00000000..9265767f --- /dev/null +++ b/packages/taskflow-core/test/run-identity.test.ts @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { test } from "node:test"; +import type { AgentConfig } from "../src/agents.ts"; +import { canUseEventKernel } from "../src/exec/driver.ts"; +import type { RunOptions } from "../src/host/runner-types.ts"; +import { executeTaskflow, type RuntimeDeps } from "../src/runtime.ts"; +import type { Phase, Taskflow } from "../src/schema.ts"; +import { loadRun, saveRun, type RunState } from "../src/store.ts"; +import { emptyUsage } from "../src/usage.ts"; + +const AGENTS: AgentConfig[] = [{ name: "a", description: "test", systemPrompt: "", source: "user", filePath: "" }]; + +function persistedState(cwd: string, def: Taskflow, runId: string): RunState { + saveRun({ runId, flowName: def.name, def, args: { runId: "argument-controlled" }, status: "running", phases: {}, createdAt: Date.now(), updatedAt: Date.now(), cwd }); + const state = loadRun(cwd, runId); + assert.ok(state); + return state; +} + +const cases: Array<{ name: string; phase: Phase; calls: number }> = [ + { name: "agent", phase: { id: "work", type: "agent", agent: "a", task: "agent" }, calls: 1 }, + { name: "map", phase: { id: "work", type: "map", agent: "a", task: "map {item}", over: '["x","y"]' }, calls: 2 }, + { name: "parallel", phase: { id: "work", type: "parallel", branches: [{ agent: "a", task: "left" }, { agent: "a", task: "right" }] }, calls: 2 }, + { name: "reduce", phase: { id: "work", type: "reduce", agent: "a", task: "reduce" }, calls: 1 }, + { name: "gate", phase: { id: "work", type: "gate", agent: "a", task: "gate" }, calls: 1 }, + { name: "loop", phase: { id: "work", type: "loop", agent: "a", task: "loop", until: "{loop.iteration}>=2", maxIterations: 2, convergence: false }, calls: 2 }, + { name: "tournament", phase: { id: "work", type: "tournament", agent: "a", task: "variant", variants: 2, judge: "judge", mode: "best" }, calls: 3 }, +]; + +for (const eventKernel of [false, true]) { + const engine = eventKernel ? "kernel" : "imperative"; + for (const { name, phase, calls: expectedCalls } of cases) { + test(`run identity: ${engine} ${name} calls receive persisted state identity`, async (t) => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "tf-run-id-")); + t.after(() => fs.rmSync(cwd, { recursive: true, force: true })); + const def: Taskflow = Object.assign({ name: `identity-${name}`, phases: [Object.assign({ ...phase, final: true }, { runId: "phase-controlled" })] }, { runId: "flow-controlled" }); + const runId = `persisted-${engine}-${name}`; + const state = persistedState(cwd, def, runId); + assert.equal(canUseEventKernel(def), true, "Kernel fixture must not silently fall back"); + const calls: Array<{ opts: RunOptions; stack: string }> = []; + const runTask: RuntimeDeps["runTask"] = async (_cwd, _agents, agent, task, opts) => { + calls.push({ opts, stack: new Error().stack ?? "" }); + const output = name === "gate" ? "VERDICT: PASS" : name === "tournament" ? "WINNER: 1" : `ok-${calls.length}`; + return { agent, task, exitCode: 0, output, stderr: "", usage: emptyUsage(), stopReason: "end" }; + }; + const result = await executeTaskflow(state, { cwd, agents: AGENTS, eventKernel, persist: saveRun, runTask }); + assert.equal(result.ok, true, result.finalOutput); + assert.equal(calls.length, expectedCalls); + for (const call of calls) { + assert.equal(call.opts.runId, runId, "Neither flow, phase nor argument data may override the owning state"); + if (eventKernel) { + const source = ["agent", "map", "parallel"].includes(name) ? "exec/step.ts" : "exec/step-kinds.ts"; + assert.ok(call.stack.replaceAll("\\", "/").includes(source), `Expected real kernel path ${source}`); + } + } + assert.equal(loadRun(cwd, runId)?.runId, runId); + }); + } + + test(`run identity: ${engine} nested flow uses its immediate owning state`, async (t) => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "tf-nested-run-id-")); + t.after(() => fs.rmSync(cwd, { recursive: true, force: true })); + const child: Taskflow = { name: "child", phases: [ + { id: "one", type: "agent", agent: "a", task: "child-one" }, + { id: "two", type: "reduce", agent: "a", task: "child-two", from: ["one"], dependsOn: ["one"], final: true }, + ] }; + const def: Taskflow = { name: "parent", phases: [ + { id: "root", type: "agent", agent: "a", task: "parent" }, + { id: "nested", type: "flow", use: "child", dependsOn: ["root"], final: true }, + ] }; + const runId = `persisted-${engine}-parent`; + const state = persistedState(cwd, def, runId); + const calls: Array<{ task: string; runId?: string }> = []; + const runTask: RuntimeDeps["runTask"] = async (_cwd, _agents, agent, task, opts) => { + calls.push({ task, runId: opts.runId }); + return { agent, task, exitCode: 0, output: "ok", stderr: "", usage: emptyUsage(), stopReason: "end" }; + }; + const result = await executeTaskflow(state, { cwd, agents: AGENTS, eventKernel, persist: saveRun, runTask, loadFlow: (name) => name === "child" ? child : undefined }); + assert.equal(result.ok, true, JSON.stringify(result.state)); + assert.equal(calls.length, 3); + assert.equal(calls[0].runId, runId); + assert.ok(calls[1].runId); + assert.notEqual(calls[1].runId, runId, "Nested execution creates a distinct owning state"); + assert.equal(calls[1].runId, calls[2].runId, "Both child handlers share that state identity"); + if (eventKernel) assert.equal(calls[1].runId, `${runId}-n-child`); + else assert.match(calls[1].runId, /^child-/); + }); +} diff --git a/skills-src/taskflow/configuration.md b/skills-src/taskflow/configuration.md index 9eb49146..b9a66844 100644 --- a/skills-src/taskflow/configuration.md +++ b/skills-src/taskflow/configuration.md @@ -47,7 +47,7 @@ Top-level keys of the taskflow definition object. | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. | | `scriptCwd` | `invocation`\|`flow` | `invocation` | Default cwd policy for `script` phases. `flow` requires trusted saved-flow/`defineFile` provenance; explicit phase `cwd` wins, and inherited cwd-bridge boundaries still constrain the resolved source directory. `taskFile` is the same class of load-time trusted source (see §2). | | `args` | record | `{}` | Declared invocation arguments. See §3. | -| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | +| `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", ...}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). | | `phases` | array | — | **Required.** The phase DAG. See §2. | | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. | @@ -64,11 +64,11 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. "id": "audit", // required, unique — referenced via {steps.audit.output} "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent) "agent": "analyst", // agent name to run this phase - "task": "Audit {item.route}…", + "task": "Audit {item.route}...", "dependsOn": ["discover"],// DAG edges "over": "{steps.discover.json}", // [map] array to fan out over "as": "item", // [map] loop var name (default: item) - "branches": [ /* … */ ], // [parallel|race] static task list + "branches": [ /* ... */ ], // [parallel|race] static task list "from": ["audit"], // [reduce] phase ids to aggregate "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow "expandMode": "nested", // [expand] nested | graft @@ -84,7 +84,7 @@ Keys of each object in `phases[]`. Some only apply to specific `type`s. | Key | Applies to | Default | Notes | |-----|-----------|---------|-------| -| `id` | all | — | **Required, unique.** Used in `{steps.…}`. | +| `id` | all | — | **Required, unique.** Used in `{steps....}`. | | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). | | `agent` | all | first available | Agent name; resolved from the scoped pool. | | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types unless `taskFile` is set. | @@ -277,8 +277,8 @@ There are **two independent concurrency limits**: "concurrency": 6, // ≤6 sibling phases run at once "phases": [ { "id": "scan", "type": "map", "over": "{steps.list.json}", - "concurrency": 3, // …but this map only fans out 3 at a time - "task": "…", "dependsOn": ["list"] } + "concurrency": 3, // ...but this map only fans out 3 at a time + "task": "...", "dependsOn": ["list"] } ] } ``` @@ -306,7 +306,7 @@ Notes: Omit it to request the host's default capability policy. - Each phase runs as an isolated process: - `pi --mode json -p --no-session [--model …] [--thinking …] [--tools …] [--append-system-prompt ] "Task: …"`. + `pi --mode json -p --no-session [--model ...] [--thinking ...] [--tools ...] [--append-system-prompt ] "Task: ..."`. - Each phase runs as an isolated `codex exec --json` session. A model id that @@ -549,7 +549,7 @@ fingerprint folds **“did the world change?”** signals into that key, so an external change becomes a cache **miss** even when the task text is identical. Each entry is one of: -| Entry | Becomes a miss when… | Resolves to | +| Entry | Becomes a miss when... | Resolves to | |-------|----------------------|-------------| | `git:HEAD` / `git:` | the commit moves | the resolved SHA (30s timeout → ``; no git → ``) | | `glob:` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) | @@ -615,7 +615,7 @@ Each entry is one of: ```jsonc { "id": "review", "type": "gate", "agent": "reviewer", "model": "claude-opus-4", "thinking": "high", - "task": "…\nVERDICT:", "dependsOn": ["audit"] } + "task": "...\nVERDICT:", "dependsOn": ["audit"] } ``` **Sandbox a phase to read-only in a subdirectory:** @@ -634,7 +634,7 @@ Each entry is one of: **Project-only agents:** ```jsonc -{ "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] } +{ "name": "ci-audit", "agentScope": "project", "phases": [ /* ... */ ] } ``` --- @@ -673,22 +673,22 @@ symlinks, and commit atomically; `--emit both` preflights both destinations. **Authoring notes (kinds ↔ runes)** -Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow +Import: `import { flow, agent, map, ... } from "taskflow-dsl"`. Runes erase to Taskflow JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry). | JSON `type` | DSL rune(s) | Notes | |-------------|-------------|--------| | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` | -| `parallel` | `parallel([agent…])` | waits for all branches | -| `map` | `map(source, item => agent…)` | `over` + `as` | +| `parallel` | `parallel([agent...])` | waits for all branches | +| `map` | `map(source, item => agent...)` | `over` + `as` | | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` | -| `reduce` | `reduce([…], () => agent…)` | `from` | +| `reduce` | `reduce([...], () => agent...)` | `from` | | `approval` | `approval({ request })` | | | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def | -| `loop` | `loop({ task, until?, … })` | | -| `tournament` | `tournament({ branches/variants, judge, … })` | | +| `loop` | `loop({ task, until?, ... })` | | +| `tournament` | `tournament({ branches/variants, judge, ... })` | | | `script` | `script(run, opts?)` | string or argv array | -| `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | +| `race` | `race([agent...], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted | | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` | - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`. diff --git a/skills-src/taskflow/core.md b/skills-src/taskflow/core.md index 1a1a929c..6e8fc728 100644 --- a/skills-src/taskflow/core.md +++ b/skills-src/taskflow/core.md @@ -128,18 +128,18 @@ For a non-trivial flow you'll iterate on, **write the definition to a file** ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: { "action": "verify", "defineFile": "/tmp/audit.json" } // zero tokens { "action": "compile", "defineFile": "/tmp/audit.json" } // diagram + report -{ "action": "run", "defineFile": "/tmp/audit.json", "args": { … } } +{ "action": "run", "defineFile": "/tmp/audit.json", "args": { ... } } ``` ```jsonc -// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[…]} object) +// 1. write /tmp/audit.json with the `write` tool (a full {name, phases:[...]} object) // 2. verify, iterate, run — all reference the SAME file by path: -{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { … } } } // zero tokens: bind + plan + budget bound +{ "name": "taskflow_plan", "arguments": { "defineFile": "/tmp/audit.json", "args": { ... } } } // zero tokens: bind + plan + budget bound { "name": "taskflow_verify", "arguments": { "defineFile": "/tmp/audit.json" } } // zero tokens { "name": "taskflow_compile", "arguments": { "defineFile": "/tmp/audit.json" } } // diagram { "name": "taskflow_lint", "arguments": { "defineFile": "/tmp/audit.json" } } // script-lint + custom verifiers @@ -569,7 +569,7 @@ Each effect: **Phase output is the payload.** With one declared `fs.write`, the phase's output becomes the staged file content (see the example: `process.stdout.write` = the report). With several `fs.write` effects, the phase must emit JSON -mapping each effect id to its content (`{ "report": "…", "backup": "…" }`). +mapping each effect id to its content (`{ "report": "...", "backup": "..." }`). Commit promotes each file atomically; a later failure restores every admitted file to its durable pre-state, and a direct write by the agent/script to a **declared final path** is detected and restored — only the resource @@ -623,8 +623,8 @@ more than comparing every approach. { "id": "quick", "type": "race", "branches": [ - { "task": "Answer with a short heuristic…", "agent": "executor" }, - { "task": "Answer with a thorough search…", "agent": "researcher" } + { "task": "Answer with a short heuristic...", "agent": "executor" }, + { "task": "Answer with a thorough search...", "agent": "researcher" } ], "final": true }