ftown-bridge 0.19.39 → 0.19.40
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/dist/create-ftown-session.js +3 -8
- package/dist/create-ftown-session.js.map +1 -1
- package/dist/ftown-cli.d.ts +1 -0
- package/dist/ftown-cli.js +15 -7
- package/dist/ftown-cli.js.map +1 -1
- package/dist/ftown-env-cli.d.ts +1 -0
- package/dist/ftown-env-cli.js +15 -4
- package/dist/ftown-env-cli.js.map +1 -1
- package/dist/ftown-home.js +1 -1
- package/dist/ftown-sessions-cli.js +3 -3
- package/dist/harness-cli.js +3 -3
- package/dist/harness-installer.js +8 -75
- package/dist/harness-installer.js.map +1 -1
- package/dist/harness-registry.d.ts +2 -23
- package/dist/harness-registry.js +2 -16
- package/dist/harness-registry.js.map +1 -1
- package/dist/index.js +2 -8
- package/dist/index.js.map +1 -1
- package/dist/install-ftown-skill.d.ts +6 -0
- package/dist/install-ftown-skill.js +78 -3
- package/dist/install-ftown-skill.js.map +1 -1
- package/dist/mail-delivery.js +1 -1
- package/dist/mail-delivery.js.map +1 -1
- package/dist/provider-env-store.js +1 -1
- package/dist/provider-env-store.js.map +1 -1
- package/dist/retired-skill-assets.d.ts +3 -0
- package/dist/retired-skill-assets.js +11 -0
- package/dist/retired-skill-assets.js.map +1 -1
- package/package.json +2 -3
- package/skills/ftown/SKILL.md +54 -19
- package/skills/ftown/references/orchestrator.md +22 -99
- package/skills/ftown/references/sessions.md +32 -35
- package/dist/claude-trust.d.ts +0 -11
- package/dist/claude-trust.js +0 -47
- package/dist/claude-trust.js.map +0 -1
- package/dist/install-ftown-workflows-cli.d.ts +0 -3
- package/dist/install-ftown-workflows-cli.js +0 -33
- package/dist/install-ftown-workflows-cli.js.map +0 -1
- package/dist/workflow-runner-cli.d.ts +0 -2
- package/dist/workflow-runner-cli.js +0 -376
- package/dist/workflow-runner-cli.js.map +0 -1
- package/dist/workflow-runner.d.ts +0 -163
- package/dist/workflow-runner.js +0 -307
- package/dist/workflow-runner.js.map +0 -1
- package/skills/fticket/SKILL.md +0 -166
- package/skills/ftown/references/workflows.md +0 -470
- package/skills/ftown/scripts/example.flow.mjs +0 -170
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
# ftown-workflows
|
|
2
|
-
|
|
3
|
-
`ftown-workflows` is a **scripted orchestration engine** for real ftown sessions.
|
|
4
|
-
You write a `.mjs` script using a small API (`agent`, `parallel`, `pipeline`,
|
|
5
|
-
`phase`, `log`, `args`, `budget`) and the runner spawns real ftown sessions (claude /
|
|
6
|
-
cursor / codex), waits for each to write a result file, cleans up, and returns the
|
|
7
|
-
value — all deterministically and repeatably.
|
|
8
|
-
|
|
9
|
-
This complements the by-hand orchestrator playbook in
|
|
10
|
-
`references/orchestrator.md`. Use ftown-workflows when the work is scripted and
|
|
11
|
-
repeatable; use orchestrator guidance when you need to improvise or keep a
|
|
12
|
-
human in the loop.
|
|
13
|
-
|
|
14
|
-
A workflow runs deterministic control flow inside one explicitly started run.
|
|
15
|
-
|
|
16
|
-
## Operating Contract
|
|
17
|
-
|
|
18
|
-
Use ftown-workflows to encode deterministic multi-session control flow: fan-out,
|
|
19
|
-
verification, synthesis, loops, retries, and resumable handoffs. The workflow
|
|
20
|
-
script is where the structure lives: which workers run independently, which
|
|
21
|
-
results are verified, where a barrier is necessary, and what gets returned.
|
|
22
|
-
|
|
23
|
-
Do not infer workflow permission just because a task might benefit from
|
|
24
|
-
parallelism. Run a workflow only when the user explicitly asks for one, asks for
|
|
25
|
-
multi-agent orchestration, asks to fan out workers, names `ftown-workflows`, asks
|
|
26
|
-
to run a specific workflow, or invokes a skill/command whose instructions require
|
|
27
|
-
this skill. Otherwise, answer inline or describe the workflow you would run and
|
|
28
|
-
ask before spending the user's tokens.
|
|
29
|
-
|
|
30
|
-
If the user explicitly says this task **must use ftown-workflows**, a manual
|
|
31
|
-
simulation is not enough. Create a `.flow.mjs` script and run it with
|
|
32
|
-
`~/.ftown/ftown-workflows`.
|
|
33
|
-
|
|
34
|
-
## Scout First
|
|
35
|
-
|
|
36
|
-
Start with a cheap inline scout before writing the workflow script: list relevant
|
|
37
|
-
files, search call sites, scope the diff, read key modules, and identify whether
|
|
38
|
-
the task is understanding, design, review, research, migration, or greenfield
|
|
39
|
-
build. You do not need the final DAG before starting the task; you need the
|
|
40
|
-
work-list and shape before orchestration.
|
|
41
|
-
|
|
42
|
-
Common single-phase shapes:
|
|
43
|
-
|
|
44
|
-
| Intent | Shape |
|
|
45
|
-
| --- | --- |
|
|
46
|
-
| Understand | readers over subsystems -> structured map |
|
|
47
|
-
| Design | independent approaches -> judge panel -> scored synthesis |
|
|
48
|
-
| Review or audit | dimensions -> find -> adversarial verify -> synthesis |
|
|
49
|
-
| Research | search/read sweep -> deep read -> verify -> cited synthesis |
|
|
50
|
-
| Migrate | discover sites -> transform isolated slices -> verify |
|
|
51
|
-
| Greenfield build | scout stack -> contract-first prep -> modules -> compile/review |
|
|
52
|
-
|
|
53
|
-
For large work, run several small workflows in sequence instead of one giant
|
|
54
|
-
script. Read each result before deciding the next phase. A practical split is:
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
discovery-design.flow.mjs -> implementation-review.flow.mjs
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
The discovery/design workflow discovers constraints, compares approaches, writes
|
|
61
|
-
a durable handoff (`discovery-design.handoff.json`, `plan.json`, `specs/`, a
|
|
62
|
-
rubric), and stops. The implementation/review workflow consumes that handoff,
|
|
63
|
-
implements the work-list, integrates, verifies, and repairs against the rubric.
|
|
64
|
-
|
|
65
|
-
## Pipeline By Default
|
|
66
|
-
|
|
67
|
-
Default to `ctx.pipeline(...)` for multi-stage work. Each item should advance as
|
|
68
|
-
soon as its previous stage finishes; do not make fast items wait for the slowest
|
|
69
|
-
item unless the next stage genuinely needs all prior results at once.
|
|
70
|
-
|
|
71
|
-
A barrier with `ctx.parallel(...)` is correct when a later stage needs
|
|
72
|
-
cross-item context:
|
|
73
|
-
|
|
74
|
-
- deduping or merging all findings before expensive verification
|
|
75
|
-
- early exit when the full result set is empty
|
|
76
|
-
- comparing one finding against the other findings
|
|
77
|
-
|
|
78
|
-
A barrier is not justified by ordinary mapping/filtering, by conceptual phase
|
|
79
|
-
boundaries, or by code tidiness. Put per-item transforms inside a pipeline stage.
|
|
80
|
-
When unsure, pipeline.
|
|
81
|
-
|
|
82
|
-
Use explicit `phase` names in `ctx.agent(..., { phase })` inside pipelines and
|
|
83
|
-
parallel stages so progress groups are stable even when stages interleave.
|
|
84
|
-
|
|
85
|
-
## Quality Patterns
|
|
86
|
-
|
|
87
|
-
Pick and compose these patterns based on the user's request:
|
|
88
|
-
|
|
89
|
-
- **Adversarial verify:** for each claim/finding, spawn independent skeptics
|
|
90
|
-
asked to refute it. Keep a finding only if it survives the vote.
|
|
91
|
-
- **Perspective-diverse verify:** use distinct verifier lenses such as
|
|
92
|
-
correctness, security, performance, reproducibility, and UX instead of cloned
|
|
93
|
-
prompts.
|
|
94
|
-
- **Judge panel:** generate multiple independent solutions, have judges score
|
|
95
|
-
them, then synthesize from the winner while preserving useful ideas from
|
|
96
|
-
runners-up.
|
|
97
|
-
- **Loop-until-dry:** for unknown-size discovery, keep launching finder rounds
|
|
98
|
-
until a fixed number of consecutive rounds returns nothing new.
|
|
99
|
-
- **Multi-modal sweep:** search by different axes (file path, call graph,
|
|
100
|
-
content, timestamp, dependency, runtime behavior) and merge findings.
|
|
101
|
-
- **Completeness critic:** end with a worker that asks what was missed: unread
|
|
102
|
-
sources, unverified claims, uncovered modalities, or dropped work.
|
|
103
|
-
- **No silent caps:** if you cap coverage, sampling, retries, or result counts,
|
|
104
|
-
log what was skipped with `ctx.log()`.
|
|
105
|
-
|
|
106
|
-
Scale to the wording. "Find any bugs" can be a small finder set and one verifier.
|
|
107
|
-
"Thoroughly audit" or a large explicit budget should increase finder diversity,
|
|
108
|
-
verification votes, and loop-until-dry depth.
|
|
109
|
-
|
|
110
|
-
## Dependent Phases
|
|
111
|
-
|
|
112
|
-
`ctx.agent()` returns `null` instead of throwing when a worker times out, exits
|
|
113
|
-
without a result, exhausts budget, or writes `{ "ok": false }`. That is useful
|
|
114
|
-
for optional fan-out, but dangerous for dependent phases. Fail fast before
|
|
115
|
-
implementation, verification, or synthesis depends on a missing result.
|
|
116
|
-
|
|
117
|
-
Use this guard in workflow scripts:
|
|
118
|
-
|
|
119
|
-
```js
|
|
120
|
-
function requireAgentResult(value, label) {
|
|
121
|
-
if (value == null) {
|
|
122
|
-
throw new Error(`${label} failed; aborting dependent workflow phases`);
|
|
123
|
-
}
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Optional fan-out may filter failures with `.filter(Boolean)`. Required handoffs,
|
|
129
|
-
module implementations, verifiers, and final synthesis should use the guard.
|
|
130
|
-
|
|
131
|
-
## Greenfield Builds
|
|
132
|
-
|
|
133
|
-
When a workflow is building a whole app, game, service, library, or system from
|
|
134
|
-
scratch, the discovery/design phase should include contract-first prep before
|
|
135
|
-
implementation fan-out. Read and apply:
|
|
136
|
-
|
|
137
|
-
```text
|
|
138
|
-
~/.claude/skills/contract-first-prep/SKILL.md
|
|
139
|
-
~/.claude/skills/contract-first-prep/references/contract-guide.md
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
The prep worker should produce the parallel-safe handoff:
|
|
143
|
-
|
|
144
|
-
- minimal scaffold with a strict typecheck/build gate
|
|
145
|
-
- immutable type/interface contract for cross-module boundaries
|
|
146
|
-
- pure-data config plus shared low-level helpers
|
|
147
|
-
- disjoint module decomposition (`plan.json`) and per-module specs
|
|
148
|
-
|
|
149
|
-
The later implementation/review workflow treats that contract, config, and shared
|
|
150
|
-
helpers as frozen. Workers adapt their modules to the contract; only the
|
|
151
|
-
integrator performs broad wiring. If review finds a design flaw, launch a new
|
|
152
|
-
discovery/design workflow instead of letting implementers redesign in parallel.
|
|
153
|
-
|
|
154
|
-
Skip contract-first prep for small edits, single-file scripts, or established
|
|
155
|
-
codebases that already have their own architecture.
|
|
156
|
-
|
|
157
|
-
## Running a workflow
|
|
158
|
-
|
|
159
|
-
You must be **inside an ftown session** — `FTOWN_SESSION_ID` must be set.
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
~/.ftown/ftown-workflows run path/to/script.mjs
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
> **Caveat:** run ftown-workflows from a **top-level orchestrator session**. If the
|
|
166
|
-
> session running it is itself a child, the bridge flattens the tree so spawned
|
|
167
|
-
> workers become **siblings** of the orchestrator rather than its children. Results
|
|
168
|
-
> are file-based, so this does not affect correctness — only the dashboard topology.
|
|
169
|
-
|
|
170
|
-
Child/subagent sessions **can run workflows**. Do not refuse just because
|
|
171
|
-
`FTOWN_PARENT_SESSION_ID` is set or because the current session was spawned by
|
|
172
|
-
another agent. The only hard requirement is `FTOWN_SESSION_ID` plus a reachable
|
|
173
|
-
bridge. The parent/child caveat above is about dashboard topology, not
|
|
174
|
-
capability: results still flow through files under `~/.ftown/workflows/<run-id>/`.
|
|
175
|
-
|
|
176
|
-
Full options:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
~/.ftown/ftown-workflows run <script.mjs> \
|
|
180
|
-
[--args <json>] # parsed and available as ctx.args in the script
|
|
181
|
-
[--workdir <path>] # default working dir for spawned child sessions
|
|
182
|
-
[--shell claude|cursor|codex|pi|opencode|shell] # explicit override; default is current harness
|
|
183
|
-
[--concurrency <n>] # max simultaneous live sessions (default 4)
|
|
184
|
-
[--timeout <ms>] # per-agent timeout (default 1 800 000 = 30 min)
|
|
185
|
-
[--max-agents <n>] # hard budget cap on total spawns
|
|
186
|
-
[--run-id <id>] # resume a previous run (skip completed steps)
|
|
187
|
-
[--json] # print final result as raw JSON
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
The runner prints the **run directory** (`~/.ftown/workflows/<run-id>/`) at start.
|
|
191
|
-
For long runs, launch in the background and tail the run dir:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
~/.ftown/ftown-workflows run script.mjs --args '{"pr":42}' &
|
|
195
|
-
tail -f ~/.ftown/workflows/<run-id>/*.json
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
See the runnable template at `scripts/example.flow.mjs` in this skill directory.
|
|
199
|
-
|
|
200
|
-
## Script API
|
|
201
|
-
|
|
202
|
-
A workflow script is an ES module. Its **default export** (or named `run` export)
|
|
203
|
-
is an async function that receives a `WorkflowContext`:
|
|
204
|
-
|
|
205
|
-
```js
|
|
206
|
-
// my-workflow.mjs
|
|
207
|
-
export default async function (ctx) {
|
|
208
|
-
ctx.phase('Gather');
|
|
209
|
-
ctx.log(`args: ${JSON.stringify(ctx.args)}`);
|
|
210
|
-
|
|
211
|
-
const summary = await ctx.agent('Summarise the repo README', {
|
|
212
|
-
label: 'summarise',
|
|
213
|
-
workdir: '/path/to/repo',
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
ctx.log(`summary: ${summary}`);
|
|
217
|
-
return summary;
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### `ctx.agent(prompt, opts?)`
|
|
222
|
-
|
|
223
|
-
Spawns one real ftown session. Blocks until the session writes its result file,
|
|
224
|
-
then removes the session and returns the result.
|
|
225
|
-
|
|
226
|
-
- Without `schema`: returns a **string** (or `null` on failure/timeout). A string
|
|
227
|
-
`result` is returned as-is; a non-string `result` is returned as a JSON string.
|
|
228
|
-
- With `schema`: returns the child's `result` JSON value **as-is** (parsed from the
|
|
229
|
-
result file), or `null` on failure. The engine does **not** validate `result`
|
|
230
|
-
against the schema — the schema is embedded in the child's prompt as guidance only.
|
|
231
|
-
Treat conformance as best-effort and validate it yourself if you depend on it.
|
|
232
|
-
|
|
233
|
-
Returns `null` — never throws — for: timeout, session exits without a result,
|
|
234
|
-
`ok: false` in the result, budget exhausted.
|
|
235
|
-
|
|
236
|
-
Key options:
|
|
237
|
-
|
|
238
|
-
| option | default | meaning |
|
|
239
|
-
|---|---|---|
|
|
240
|
-
| `label` | `step-<n>` | step key used for the result file and resume |
|
|
241
|
-
| `phase` | — | progress grouping shown in logs |
|
|
242
|
-
| `schema` | — | JSON Schema embedded in the worker prompt; requests JSON result |
|
|
243
|
-
| `shell` | run-level default (current harness when unset) | `claude` / `cursor` / `codex` / `pi` / `opencode` / `shell` |
|
|
244
|
-
| `model` | — | model override passed to the session |
|
|
245
|
-
| `workdir` | run-level default | working directory for the child session |
|
|
246
|
-
| `timeoutMs` | 1 800 000 | wall-clock cap for this step |
|
|
247
|
-
| `pollIntervalMs` | 2000 | how often to check for the result file |
|
|
248
|
-
|
|
249
|
-
### `ctx.parallel(thunks)`
|
|
250
|
-
|
|
251
|
-
Run an array of thunks concurrently (barrier: waits for all). Respects the
|
|
252
|
-
run-level `--concurrency` cap. A thunk that errors → `null` entry; the call
|
|
253
|
-
never rejects.
|
|
254
|
-
|
|
255
|
-
```js
|
|
256
|
-
const reviews = await ctx.parallel(
|
|
257
|
-
files.map(f => () => ctx.agent(`Review ${f}`, { label: `review-${f}` }))
|
|
258
|
-
);
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
### `ctx.pipeline(items, ...stages)`
|
|
262
|
-
|
|
263
|
-
Thread each item through a sequence of stages independently (no barrier between
|
|
264
|
-
stages). A stage that throws drops that item to `null` and skips its remaining
|
|
265
|
-
stages.
|
|
266
|
-
|
|
267
|
-
```js
|
|
268
|
-
const results = await ctx.pipeline(
|
|
269
|
-
files,
|
|
270
|
-
async (file) => ctx.agent(`lint ${file}`, { label: `lint-${file}` }),
|
|
271
|
-
async (lintResult, file) => ctx.agent(`fix issues in ${file}: ${lintResult}`, { label: `fix-${file}` }),
|
|
272
|
-
);
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
### `ctx.phase(title)` / `ctx.log(message)`
|
|
276
|
-
|
|
277
|
-
Emit progress events to stderr. Use `phase` for major milestones, `log` for
|
|
278
|
-
detail lines.
|
|
279
|
-
|
|
280
|
-
### `ctx.args`
|
|
281
|
-
|
|
282
|
-
The value passed via `--args <json>` (parsed). `undefined` if not provided.
|
|
283
|
-
|
|
284
|
-
### `ctx.budget`
|
|
285
|
-
|
|
286
|
-
```js
|
|
287
|
-
ctx.budget.maxAgents // null = unbounded
|
|
288
|
-
ctx.budget.spent() // spawns so far (cached don't count)
|
|
289
|
-
ctx.budget.remaining() // maxAgents - spent(), or Infinity
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
## Result-file contract
|
|
293
|
-
|
|
294
|
-
Each child session receives a prompt that ends with a protocol block instructing
|
|
295
|
-
it to write its final result as JSON to a specific file path and then stop:
|
|
296
|
-
|
|
297
|
-
```json
|
|
298
|
-
{ "ok": true, "result": "...anything..." }
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
or on failure:
|
|
302
|
-
|
|
303
|
-
```json
|
|
304
|
-
{ "ok": false, "error": "reason" }
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
The engine polls the file every `pollIntervalMs` ms. A partial write (incomplete
|
|
308
|
-
JSON) is silently ignored until it is valid. The child session is removed (archived)
|
|
309
|
-
once the result is read, or on timeout/exit.
|
|
310
|
-
|
|
311
|
-
**You do not write this file yourself** — the child agent is instructed to do it.
|
|
312
|
-
The prompt injected by the engine tells the child agent exactly what to write.
|
|
313
|
-
|
|
314
|
-
## Script Patterns
|
|
315
|
-
|
|
316
|
-
Scripts are plain JavaScript modules. Avoid nondeterministic labels or control
|
|
317
|
-
flow (`Date.now()`, `Math.random()`, timestamp-derived labels) when you care
|
|
318
|
-
about resume, because cached results are matched by step order and label.
|
|
319
|
-
|
|
320
|
-
### Canonical Pipeline
|
|
321
|
-
|
|
322
|
-
Each item moves through review and verification independently. One file can be
|
|
323
|
-
verifying while another is still reviewing.
|
|
324
|
-
|
|
325
|
-
```js
|
|
326
|
-
export default async function (ctx) {
|
|
327
|
-
const results = await ctx.pipeline(
|
|
328
|
-
ctx.args.files,
|
|
329
|
-
(file) => ctx.agent(`Find bugs in ${file}`, {
|
|
330
|
-
label: `find-${file}`,
|
|
331
|
-
phase: 'Find',
|
|
332
|
-
schema: FINDINGS_SCHEMA,
|
|
333
|
-
}),
|
|
334
|
-
(review, file) => ctx.parallel(
|
|
335
|
-
(review?.findings ?? []).map((finding, i) => () => ctx.agent(
|
|
336
|
-
`Try to refute this finding:\n${JSON.stringify(finding)}`,
|
|
337
|
-
{ label: `verify-${file}-${i}`, phase: 'Verify', schema: VERDICT_SCHEMA },
|
|
338
|
-
).then((verdict) => ({ file, finding, verdict })))
|
|
339
|
-
),
|
|
340
|
-
);
|
|
341
|
-
|
|
342
|
-
return results.flat().filter(Boolean).filter((r) => r.verdict?.isReal);
|
|
343
|
-
}
|
|
344
|
-
```
|
|
345
|
-
|
|
346
|
-
### Correct Barrier
|
|
347
|
-
|
|
348
|
-
Use a barrier when deduplication or comparison needs every prior result.
|
|
349
|
-
|
|
350
|
-
```js
|
|
351
|
-
export default async function (ctx) {
|
|
352
|
-
ctx.phase('Find');
|
|
353
|
-
const reviews = await ctx.parallel(
|
|
354
|
-
ctx.args.dimensions.map((d) => () => ctx.agent(d.prompt, {
|
|
355
|
-
label: `find-${d.key}`,
|
|
356
|
-
schema: FINDINGS_SCHEMA,
|
|
357
|
-
})),
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
const allFindings = reviews.filter(Boolean).flatMap((r) => r.findings ?? []);
|
|
361
|
-
const deduped = dedupeByFileAndTitle(allFindings);
|
|
362
|
-
if (deduped.length === 0) return { confirmed: [] };
|
|
363
|
-
|
|
364
|
-
ctx.phase('Verify');
|
|
365
|
-
const verified = await ctx.parallel(
|
|
366
|
-
deduped.map((finding, i) => () => ctx.agent(
|
|
367
|
-
`Verify this deduped finding:\n${JSON.stringify(finding)}`,
|
|
368
|
-
{ label: `verify-${i}`, schema: VERDICT_SCHEMA },
|
|
369
|
-
).then((verdict) => ({ finding, verdict }))),
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
return { confirmed: verified.filter(Boolean).filter((r) => r.verdict?.isReal) };
|
|
373
|
-
}
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
### Loop Until Dry
|
|
377
|
-
|
|
378
|
-
Use this for unknown-size searches. Dedup against everything seen, including
|
|
379
|
-
rejected findings, so the loop converges.
|
|
380
|
-
|
|
381
|
-
```js
|
|
382
|
-
export default async function (ctx) {
|
|
383
|
-
const seen = new Set();
|
|
384
|
-
const confirmed = [];
|
|
385
|
-
let dryRounds = 0;
|
|
386
|
-
|
|
387
|
-
while (dryRounds < 2 && ctx.budget.remaining() > 0) {
|
|
388
|
-
ctx.phase(`Find round ${dryRounds + 1}`);
|
|
389
|
-
const found = await ctx.agent('Find more bugs not already covered.', {
|
|
390
|
-
label: `find-round-${confirmed.length}-${dryRounds}`,
|
|
391
|
-
schema: FINDINGS_SCHEMA,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const fresh = (found?.findings ?? []).filter((finding) => {
|
|
395
|
-
const key = `${finding.file}:${finding.title}`;
|
|
396
|
-
if (seen.has(key)) return false;
|
|
397
|
-
seen.add(key);
|
|
398
|
-
return true;
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
if (fresh.length === 0) {
|
|
402
|
-
dryRounds += 1;
|
|
403
|
-
ctx.log(`dry round ${dryRounds}/2`);
|
|
404
|
-
continue;
|
|
405
|
-
}
|
|
406
|
-
dryRounds = 0;
|
|
407
|
-
|
|
408
|
-
const judged = await ctx.parallel(
|
|
409
|
-
fresh.map((finding, i) => () => ctx.agent(
|
|
410
|
-
`Try to refute this finding:\n${JSON.stringify(finding)}`,
|
|
411
|
-
{ label: `judge-${seen.size}-${i}`, phase: 'Verify', schema: VERDICT_SCHEMA },
|
|
412
|
-
).then((verdict) => ({ finding, verdict }))),
|
|
413
|
-
);
|
|
414
|
-
confirmed.push(...judged.filter(Boolean).filter((r) => r.verdict?.isReal));
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
return { confirmed };
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Budget-Bounded Depth
|
|
422
|
-
|
|
423
|
-
Guard loops with a real cap. With no `--max-agents`, `ctx.budget.remaining()` is
|
|
424
|
-
`Infinity`, so add an explicit round limit or require a max-agent budget.
|
|
425
|
-
|
|
426
|
-
```js
|
|
427
|
-
export default async function (ctx) {
|
|
428
|
-
const rounds = ctx.budget.maxAgents == null ? 3 : ctx.budget.maxAgents;
|
|
429
|
-
const results = [];
|
|
430
|
-
|
|
431
|
-
for (let i = 0; i < rounds && ctx.budget.remaining() > 0; i += 1) {
|
|
432
|
-
const result = await ctx.agent(`Research angle ${i + 1}`, {
|
|
433
|
-
label: `research-${i + 1}`,
|
|
434
|
-
schema: RESEARCH_SCHEMA,
|
|
435
|
-
});
|
|
436
|
-
if (result) results.push(result);
|
|
437
|
-
ctx.log(`${i + 1}/${rounds} research rounds complete`);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return results;
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
## Resume
|
|
445
|
-
|
|
446
|
-
Every step is keyed by its `label` (or `step-<n>`). If a result file already
|
|
447
|
-
exists for a step, the engine returns the cached result without spawning a new
|
|
448
|
-
session. To resume a partial run:
|
|
449
|
-
|
|
450
|
-
```bash
|
|
451
|
-
~/.ftown/ftown-workflows run script.mjs --run-id <the-previous-run-id>
|
|
452
|
-
```
|
|
453
|
-
|
|
454
|
-
The run id and run directory are printed at startup.
|
|
455
|
-
|
|
456
|
-
## When to use this vs orchestrator guidance
|
|
457
|
-
|
|
458
|
-
| | orchestrator guidance | ftown-workflows |
|
|
459
|
-
|---|---|---|
|
|
460
|
-
| **style** | ad-hoc, by hand | scripted, deterministic |
|
|
461
|
-
| **human in loop** | yes — you direct workers via mail | no — script drives everything |
|
|
462
|
-
| **repeatability** | each run is improvised | same script, same steps |
|
|
463
|
-
| **resume** | manual | automatic via `--run-id` |
|
|
464
|
-
| **best for** | exploratory tasks, escalations, debugging | batch jobs, CI-style pipelines, fan-out reviews |
|
|
465
|
-
|
|
466
|
-
## If the CLI is missing
|
|
467
|
-
|
|
468
|
-
Start or restart **ftown-bridge** on this machine. It installs
|
|
469
|
-
`~/.ftown/ftown-workflows` and updates the unified skill under
|
|
470
|
-
`~/.ftown/skills/ftown/` (linked into ~/.agents/skills and ~/.claude/skills).
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* example.flow.mjs - template workflow: review files, verify each finding, synthesize.
|
|
3
|
-
*
|
|
4
|
-
* Run it inside an ftown session:
|
|
5
|
-
*
|
|
6
|
-
* ~/.ftown/ftown-workflows run example.flow.mjs \
|
|
7
|
-
* --args '{"files":["src/auth.ts","src/api.ts","src/db.ts"]}' \
|
|
8
|
-
* --workdir /path/to/your/repo
|
|
9
|
-
*
|
|
10
|
-
* Add --run-id <previous-id> to resume a partial run without re-running
|
|
11
|
-
* steps whose result files already exist.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const FINDINGS_SCHEMA = {
|
|
15
|
-
type: 'object',
|
|
16
|
-
required: ['findings'],
|
|
17
|
-
properties: {
|
|
18
|
-
findings: {
|
|
19
|
-
type: 'array',
|
|
20
|
-
items: {
|
|
21
|
-
type: 'object',
|
|
22
|
-
required: ['title', 'file', 'severity', 'evidence'],
|
|
23
|
-
properties: {
|
|
24
|
-
title: { type: 'string' },
|
|
25
|
-
file: { type: 'string' },
|
|
26
|
-
severity: { type: 'string' },
|
|
27
|
-
evidence: { type: 'string' },
|
|
28
|
-
recommendation: { type: 'string' },
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const VERDICT_SCHEMA = {
|
|
36
|
-
type: 'object',
|
|
37
|
-
required: ['isReal', 'reason'],
|
|
38
|
-
properties: {
|
|
39
|
-
isReal: { type: 'boolean' },
|
|
40
|
-
reason: { type: 'string' },
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const REPORT_SCHEMA = {
|
|
45
|
-
type: 'object',
|
|
46
|
-
required: ['summary', 'confirmed', 'rejected'],
|
|
47
|
-
properties: {
|
|
48
|
-
summary: { type: 'string' },
|
|
49
|
-
confirmed: { type: 'array', items: { type: 'string' } },
|
|
50
|
-
rejected: { type: 'array', items: { type: 'string' } },
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function requireAgentResult(value, label) {
|
|
55
|
-
if (value == null) {
|
|
56
|
-
throw new Error(`${label} failed; aborting dependent workflow phases`);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function stepKey(value) {
|
|
62
|
-
return String(value).replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '') || 'item';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function asFindings(value) {
|
|
66
|
-
return value && Array.isArray(value.findings) ? value.findings : [];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* @param {import('../../../src/workflow-runner.js').WorkflowContext} ctx
|
|
71
|
-
*/
|
|
72
|
-
export default async function (ctx) {
|
|
73
|
-
const files = Array.isArray(ctx.args?.files)
|
|
74
|
-
? ctx.args.files
|
|
75
|
-
: ['src/auth.ts', 'src/api.ts', 'src/db.ts'];
|
|
76
|
-
|
|
77
|
-
ctx.phase('Setup');
|
|
78
|
-
ctx.log(`Reviewing ${files.length} file(s): ${files.join(', ')}`);
|
|
79
|
-
ctx.log(`Budget: ${ctx.budget.maxAgents ?? 'unlimited'} agents`);
|
|
80
|
-
|
|
81
|
-
const reviewed = await ctx.pipeline(
|
|
82
|
-
files,
|
|
83
|
-
async (file) => {
|
|
84
|
-
const review = await ctx.agent(
|
|
85
|
-
`Review ${file} for correctness, security, and maintainability bugs.
|
|
86
|
-
Return only concrete findings with evidence. Do not include style preferences.`,
|
|
87
|
-
{
|
|
88
|
-
label: `review-${stepKey(file)}`,
|
|
89
|
-
phase: 'Review',
|
|
90
|
-
schema: FINDINGS_SCHEMA,
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
file,
|
|
96
|
-
findings: asFindings(requireAgentResult(review, `review ${file}`)),
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
async (review) => {
|
|
100
|
-
if (review.findings.length === 0) {
|
|
101
|
-
ctx.log(`No findings reported for ${review.file}`);
|
|
102
|
-
return { file: review.file, confirmed: [], rejected: [] };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const verified = await ctx.parallel(
|
|
106
|
-
review.findings.map((finding, index) => async () => {
|
|
107
|
-
const verdict = await ctx.agent(
|
|
108
|
-
`Try to refute this finding. Default to isReal=false if the evidence is weak,
|
|
109
|
-
not reproducible, or not actually caused by the code.
|
|
110
|
-
|
|
111
|
-
Finding:
|
|
112
|
-
${JSON.stringify(finding, null, 2)}`,
|
|
113
|
-
{
|
|
114
|
-
label: `verify-${stepKey(review.file)}-${index}`,
|
|
115
|
-
phase: 'Verify',
|
|
116
|
-
schema: VERDICT_SCHEMA,
|
|
117
|
-
},
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
finding,
|
|
122
|
-
verdict: requireAgentResult(verdict, `verify ${review.file} #${index + 1}`),
|
|
123
|
-
};
|
|
124
|
-
}),
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const kept = [];
|
|
128
|
-
const rejected = [];
|
|
129
|
-
for (const item of verified.filter(Boolean)) {
|
|
130
|
-
if (item.verdict.isReal === true) kept.push(item.finding);
|
|
131
|
-
else rejected.push({ finding: item.finding, reason: item.verdict.reason });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return { file: review.file, confirmed: kept, rejected };
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const completed = reviewed.filter(Boolean);
|
|
139
|
-
const confirmed = completed.flatMap((entry) => entry.confirmed);
|
|
140
|
-
const rejected = completed.flatMap((entry) => entry.rejected);
|
|
141
|
-
|
|
142
|
-
ctx.phase('Synthesis');
|
|
143
|
-
if (confirmed.length === 0) {
|
|
144
|
-
ctx.log('No confirmed findings survived verification');
|
|
145
|
-
return {
|
|
146
|
-
summary: 'No confirmed findings survived adversarial verification.',
|
|
147
|
-
confirmed: [],
|
|
148
|
-
rejected: rejected.map((entry) => entry.finding.title),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const report = await ctx.agent(
|
|
153
|
-
`Write a concise final code-review report from these verified findings.
|
|
154
|
-
Group by severity and include evidence. Mention rejected findings only if useful.
|
|
155
|
-
|
|
156
|
-
Confirmed:
|
|
157
|
-
${JSON.stringify(confirmed, null, 2)}
|
|
158
|
-
|
|
159
|
-
Rejected:
|
|
160
|
-
${JSON.stringify(rejected, null, 2)}`,
|
|
161
|
-
{
|
|
162
|
-
label: 'synthesis',
|
|
163
|
-
phase: 'Synthesis',
|
|
164
|
-
schema: REPORT_SCHEMA,
|
|
165
|
-
},
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
ctx.log(`Done. Budget used: ${ctx.budget.spent()} agent spawn(s).`);
|
|
169
|
-
return requireAgentResult(report, 'synthesis');
|
|
170
|
-
}
|