ftown-bridge 0.18.2 → 0.19.1
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/agent-commands.d.ts +4 -0
- package/dist/agent-commands.js +27 -4
- package/dist/agent-commands.js.map +1 -1
- package/dist/create-ftown-session.js +2 -1
- package/dist/create-ftown-session.js.map +1 -1
- package/dist/ftown-sessions-cli.js +1 -1
- package/dist/ftown-sessions-cli.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loop-run-store.js +1 -0
- package/dist/loop-run-store.js.map +1 -1
- package/dist/loop-validation.js +1 -0
- package/dist/loop-validation.js.map +1 -1
- package/dist/session-store.d.ts +15 -1
- package/dist/session-store.js +63 -3
- package/dist/session-store.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/skills/factory/SKILL.md +167 -0
- package/skills/factory/factory-template/bin/dispatch.py +472 -0
- package/skills/factory/factory-template/factory.yaml +36 -0
- package/skills/factory/factory-template/skills/_protocol.md +85 -0
- package/skills/factory/factory-template/skills/design.md +259 -0
- package/skills/factory/factory-template/skills/digest.md +78 -0
- package/skills/factory/factory-template/skills/groom.md +243 -0
- package/skills/factory/factory-template/skills/implement.md +316 -0
- package/skills/factory/factory-template/skills/pr.md +220 -0
- package/skills/factory/factory-template/skills/qa.md +226 -0
- package/skills/factory/factory-template/skills/review.md +231 -0
- package/skills/factory/factory-template/skills/triage.md +157 -0
package/dist/session-store.d.ts
CHANGED
|
@@ -3,7 +3,10 @@ export declare class SessionStore {
|
|
|
3
3
|
private readonly sessionsDir;
|
|
4
4
|
private readonly archivePath;
|
|
5
5
|
private readonly writeLocks;
|
|
6
|
-
|
|
6
|
+
private readonly maxTerminalLogBytes;
|
|
7
|
+
constructor(dataDir: string, options?: {
|
|
8
|
+
maxTerminalLogBytes?: number;
|
|
9
|
+
});
|
|
7
10
|
/** Per-session data dir (session.json, terminal.log, inbox.jsonl). */
|
|
8
11
|
sessionDir(sessionId: string): string;
|
|
9
12
|
private sessionFilePath;
|
|
@@ -12,6 +15,17 @@ export declare class SessionStore {
|
|
|
12
15
|
loadSession(sessionId: string): Promise<Session | null>;
|
|
13
16
|
listSessions(): Promise<Session[]>;
|
|
14
17
|
appendTerminalData(sessionId: string, data: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Cap a terminal.log's on-disk size. Runs inside the per-session write lock,
|
|
20
|
+
* so it never races an append. The file is allowed to grow to 2x the retained
|
|
21
|
+
* size before being rewritten down to the tail — this amortises the rewrite
|
|
22
|
+
* cost while bounding disk use to at most 2x `maxTerminalLogBytes` per session.
|
|
23
|
+
* The partial first line of the retained tail is dropped so consumers never
|
|
24
|
+
* start mid-line (or mid-escape-sequence), and a one-line marker records that
|
|
25
|
+
* older output was discarded. Best-effort: any failure is swallowed so it can
|
|
26
|
+
* never poison the write-lock chain and stall future appends.
|
|
27
|
+
*/
|
|
28
|
+
private trimTerminalLogIfNeeded;
|
|
15
29
|
deleteSession(sessionId: string): Promise<void>;
|
|
16
30
|
/** Append a tombstone for a removed session to <dataDir>/archive.jsonl. */
|
|
17
31
|
archiveSession(session: Session): Promise<void>;
|
package/dist/session-store.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir, readdir, appendFile, rm, truncate } from 'node:fs/promises';
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, appendFile, rm, truncate, stat, open, rename } from 'node:fs/promises';
|
|
2
2
|
import { join, dirname } from 'node:path';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
+
/**
|
|
5
|
+
* Retained tail size for a session's terminal.log. A misbehaving child (e.g. a
|
|
6
|
+
* TUI stuck in a runaway repaint loop) can otherwise stream unbounded output
|
|
7
|
+
* and grow this file without limit — observed in the wild at 2.5 GB and still
|
|
8
|
+
* climbing, which also makes the full-file `loadTerminalLog` read exceed V8's
|
|
9
|
+
* max string length and throw. The on-disk file is only ever consumed as
|
|
10
|
+
* "recent output" (download/search); the live screen is driven by a separate
|
|
11
|
+
* in-memory emulator, so keeping just the tail is lossless for viewers.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MAX_TERMINAL_LOG_BYTES = 64 * 1024 * 1024;
|
|
14
|
+
const NEWLINE = 0x0a;
|
|
4
15
|
export class SessionStore {
|
|
5
16
|
sessionsDir;
|
|
6
17
|
archivePath;
|
|
7
18
|
writeLocks = new Map();
|
|
8
|
-
|
|
19
|
+
maxTerminalLogBytes;
|
|
20
|
+
constructor(dataDir, options = {}) {
|
|
9
21
|
this.sessionsDir = join(dataDir, 'sessions');
|
|
10
22
|
this.archivePath = join(dataDir, 'archive.jsonl');
|
|
23
|
+
const envCap = Number(process.env.FTOWN_MAX_TERMINAL_LOG_BYTES);
|
|
24
|
+
this.maxTerminalLogBytes =
|
|
25
|
+
options.maxTerminalLogBytes ??
|
|
26
|
+
(Number.isFinite(envCap) && envCap > 0 ? envCap : DEFAULT_MAX_TERMINAL_LOG_BYTES);
|
|
11
27
|
}
|
|
12
28
|
/** Per-session data dir (session.json, terminal.log, inbox.jsonl). */
|
|
13
29
|
sessionDir(sessionId) {
|
|
@@ -53,10 +69,54 @@ export class SessionStore {
|
|
|
53
69
|
await mkdir(dir, { recursive: true });
|
|
54
70
|
const filePath = this.terminalLogPath(sessionId);
|
|
55
71
|
const prevLock = this.writeLocks.get(sessionId) ?? Promise.resolve();
|
|
56
|
-
const newLock = prevLock
|
|
72
|
+
const newLock = prevLock
|
|
73
|
+
.then(() => appendFile(filePath, data, 'utf-8'))
|
|
74
|
+
.then(() => this.trimTerminalLogIfNeeded(filePath));
|
|
57
75
|
this.writeLocks.set(sessionId, newLock);
|
|
58
76
|
await newLock;
|
|
59
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Cap a terminal.log's on-disk size. Runs inside the per-session write lock,
|
|
80
|
+
* so it never races an append. The file is allowed to grow to 2x the retained
|
|
81
|
+
* size before being rewritten down to the tail — this amortises the rewrite
|
|
82
|
+
* cost while bounding disk use to at most 2x `maxTerminalLogBytes` per session.
|
|
83
|
+
* The partial first line of the retained tail is dropped so consumers never
|
|
84
|
+
* start mid-line (or mid-escape-sequence), and a one-line marker records that
|
|
85
|
+
* older output was discarded. Best-effort: any failure is swallowed so it can
|
|
86
|
+
* never poison the write-lock chain and stall future appends.
|
|
87
|
+
*/
|
|
88
|
+
async trimTerminalLogIfNeeded(filePath) {
|
|
89
|
+
try {
|
|
90
|
+
const { size } = await stat(filePath);
|
|
91
|
+
if (size <= this.maxTerminalLogBytes * 2) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const keep = this.maxTerminalLogBytes;
|
|
95
|
+
const fh = await open(filePath, 'r');
|
|
96
|
+
let tail;
|
|
97
|
+
try {
|
|
98
|
+
const buf = Buffer.alloc(keep);
|
|
99
|
+
const { bytesRead } = await fh.read(buf, 0, keep, size - keep);
|
|
100
|
+
tail = buf.subarray(0, bytesRead);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await fh.close();
|
|
104
|
+
}
|
|
105
|
+
// Drop the (almost certainly partial) first line so the retained log
|
|
106
|
+
// begins on a clean line boundary.
|
|
107
|
+
const nl = tail.indexOf(NEWLINE);
|
|
108
|
+
const body = nl >= 0 ? tail.subarray(nl + 1) : tail;
|
|
109
|
+
const marker = Buffer.from(`[ftown] terminal.log truncated — retaining last ${keep} bytes\r\n`, 'utf-8');
|
|
110
|
+
// Rewrite via a temp file + atomic rename so a concurrent full-file reader
|
|
111
|
+
// (loadTerminalLog runs outside the lock) never observes a partial file.
|
|
112
|
+
const tmpPath = `${filePath}.trim-${process.pid}.tmp`;
|
|
113
|
+
await writeFile(tmpPath, Buffer.concat([marker, body]));
|
|
114
|
+
await rename(tmpPath, filePath);
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
console.error(`[SessionStore] Failed to trim terminal log ${filePath}:`, err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
60
120
|
async deleteSession(sessionId) {
|
|
61
121
|
const dir = this.sessionDir(sessionId);
|
|
62
122
|
if (existsSync(dir)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ShellType = 'claude' | 'cursor' | 'codex' | 'shell' | 'zai' | 'kimi' | 'opencode' | 'deepseek' | 'fireworks';
|
|
1
|
+
export type ShellType = 'claude' | 'cursor' | 'codex' | 'shell' | 'zai' | 'kimi' | 'opencode' | 'deepseek' | 'fireworks' | 'grok';
|
|
2
2
|
export type SessionRuntime = 'tmux' | 'direct';
|
|
3
3
|
export interface Session {
|
|
4
4
|
id: string;
|
|
@@ -26,7 +26,7 @@ export type SessionStatus = 'pending' | 'running' | 'completed' | 'error';
|
|
|
26
26
|
export interface ArchivedSession extends Session {
|
|
27
27
|
removedAt: string;
|
|
28
28
|
}
|
|
29
|
-
export type LoopHarness = 'claude' | 'cursor' | 'codex' | 'opencode' | 'shell';
|
|
29
|
+
export type LoopHarness = 'claude' | 'cursor' | 'codex' | 'grok' | 'opencode' | 'shell';
|
|
30
30
|
export type LoopRunStatus = 'ok' | 'error' | 'running' | 'skipped';
|
|
31
31
|
export type LoopSchedule = {
|
|
32
32
|
kind: 'interval';
|
package/package.json
CHANGED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: factory
|
|
3
|
+
description: deploy and manage a per-project AI software factory (init/up/down/status/teardown) — FTS ticket pipeline + ftown loops. Trigger on "factory init", "deploy a factory", "factory status", "pause the factory".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- Vendored from the ffactory repo (skills/factory + factory-template); sync changes there first. -->
|
|
7
|
+
|
|
8
|
+
# Factory
|
|
9
|
+
|
|
10
|
+
You are a capable model helping a human deploy and operate a per-project AI software
|
|
11
|
+
factory: a `fticket` (`fts`) ticket pipeline driven by three ftown loops (a dispatcher
|
|
12
|
+
that spawns stage workers, a preflight-guarded interval triage sweep, and a daily digest
|
|
13
|
+
report). This skill is procedures, not narrative — follow the exact commands. It runs
|
|
14
|
+
inside the target project repo (`$REPO` = repo root, resolved via
|
|
15
|
+
`git rev-parse --show-toplevel`).
|
|
16
|
+
|
|
17
|
+
## Layout
|
|
18
|
+
|
|
19
|
+
- `factory/` — checked into the project repo, the factory's *definition*:
|
|
20
|
+
`factory.yaml` (stages, limits, triage config), `skills/*.md` (stage worker briefings +
|
|
21
|
+
`_protocol.md`), `bin/dispatch.py`.
|
|
22
|
+
- `.ffactory/` — gitignored, the factory's *runtime state*: `factory.db` (fts sqlite),
|
|
23
|
+
`tickets/<n>/` (per-ticket artifact folders), `worktrees/` (implement-stage checkouts),
|
|
24
|
+
`dispatch.cursor` (dispatcher bookmark). Disposable — deleting it loses history but not
|
|
25
|
+
the factory's definition.
|
|
26
|
+
|
|
27
|
+
## init
|
|
28
|
+
|
|
29
|
+
Idempotent. If `factory/` already exists in `$REPO`, stop and tell the user the factory is
|
|
30
|
+
already initialized (point them at `status`/`up`/`down` instead) — do not overwrite.
|
|
31
|
+
|
|
32
|
+
1. **Preconditions.** Confirm `$REPO` is a git repo. Run `~/.ftown/ftown-sessions list` —
|
|
33
|
+
if it errors, the bridge is down; stop and tell the user to start it first. Confirm
|
|
34
|
+
`uv` is on PATH.
|
|
35
|
+
2. **Install fticket.** `uv tool install fticket` (or `uv tool upgrade fticket` if already
|
|
36
|
+
installed). Verify with `fts --help`.
|
|
37
|
+
3. **Copy the template.**
|
|
38
|
+
```bash
|
|
39
|
+
cp -r ~/.ftown/skills/factory/factory-template "$REPO/factory"
|
|
40
|
+
```
|
|
41
|
+
`factory/bin` moves in as-is (no edits). Ask the user for: project name (default: the
|
|
42
|
+
repo directory's basename), an operator ftown session id (optional — default `-`, no
|
|
43
|
+
operator mail), and any stage routing tweaks (harness/model/max_workers per stage).
|
|
44
|
+
Edit `factory/factory.yaml` with their answers.
|
|
45
|
+
4. **Gitignore + state dirs.** Append `.ffactory/` to `$REPO/.gitignore` (create the file
|
|
46
|
+
if absent). `mkdir -p "$REPO/.ffactory/tickets"`.
|
|
47
|
+
5. **Init the ticket db**, reading stage order and limits back out of the edited
|
|
48
|
+
`factory.yaml`:
|
|
49
|
+
```bash
|
|
50
|
+
fts init --db .ffactory/factory.db \
|
|
51
|
+
--stages groom,design,implement,review,qa,pr \
|
|
52
|
+
--bounce-limit 3 --claim-ttl-ms 1800000
|
|
53
|
+
```
|
|
54
|
+
(substitute the actual stage names in yaml order, `limits.bounce_limit`, and
|
|
55
|
+
`limits.claim_ttl_ms`).
|
|
56
|
+
6. **Register the loops, grouped under one factory label.** The `--group` flag folds all
|
|
57
|
+
loops together in the ftown dashboard and requires ftown bridge PR #29. If
|
|
58
|
+
`loop create` rejects `--group` as an unknown flag, tell the user their bridge is out
|
|
59
|
+
of date and needs updating — do not silently drop the flag and register ungrouped.
|
|
60
|
+
|
|
61
|
+
Dispatcher (interval loop, runs the Python dispatcher every tick):
|
|
62
|
+
```bash
|
|
63
|
+
~/.ftown/ftown-sessions loop create \
|
|
64
|
+
--name "<project>-dispatch" --every 30s --shell shell --workdir "$REPO" \
|
|
65
|
+
--group "Factory: <project>" --max-runtime 10m --retention 10 \
|
|
66
|
+
--preflight "~/.local/bin/fts queues --db $REPO/.ffactory/factory.db --json | python3 -c 'import json,sys; qs=json.load(sys.stdin); sys.exit(0 if any(q.get(\"queued\",0)+q.get(\"claimed\",0)+q.get(\"in_progress\",0)+q.get(\"blocked\",0) for q in qs) else 1)'" \
|
|
67
|
+
--task "uv run --python 3.13 --with fticket,pyyaml python factory/bin/dispatch.py"
|
|
68
|
+
```
|
|
69
|
+
(`--every` from `limits.dispatch_every`. The preflight skips the tick — no shell
|
|
70
|
+
session — while no ticket is queued/claimed/in_progress/blocked, so an idle factory
|
|
71
|
+
costs nothing. A resting `rejected`/`dead_letter` ticket doesn't wake it: those belong
|
|
72
|
+
to explicit chaining and triage respectively.)
|
|
73
|
+
|
|
74
|
+
Triage (interval loop with a preflight skip guard — only spawns a session when there
|
|
75
|
+
is dead-letter/orphan work; requires ftown bridge >= 0.18.0 for cheap preflight skips —
|
|
76
|
+
on 0.17 the preflight still works but bloats run history since a skip still logs a
|
|
77
|
+
run):
|
|
78
|
+
```bash
|
|
79
|
+
~/.ftown/ftown-sessions loop create \
|
|
80
|
+
--name "<project>-triage" --every 10m --shell claude --model sonnet \
|
|
81
|
+
--workdir "$REPO" --group "Factory: <project>" --retention 10 \
|
|
82
|
+
--preflight "~/.local/bin/fts triage --db $REPO/.ffactory/factory.db --json | python3 -c 'import json,sys; d=json.load(sys.stdin); sys.exit(0 if d[\"dead_letter\"] or d[\"orphans\"] else 1)'" \
|
|
83
|
+
--task "FTS_DB=<abs path to .ffactory/factory.db> REPO_ROOT=<abs $REPO> OPERATOR_SESSION=<value or -> — Read and follow factory/skills/triage.md."
|
|
84
|
+
```
|
|
85
|
+
(`--every`/`--shell`/`--model` from `triage.every`/`triage.harness`/`triage.model`.)
|
|
86
|
+
The preflight exits 0 (work exists → run the session) when `fts triage --json` reports
|
|
87
|
+
any dead_letter or orphan tickets, and exits 1 (healthy → skip, no session spawned)
|
|
88
|
+
otherwise. The triage briefing must set the three task variables literally;
|
|
89
|
+
`triage.md` treats a missing variable as fatal.
|
|
90
|
+
|
|
91
|
+
Digest (daily cron loop, shift report mailed to the operator) — register this loop
|
|
92
|
+
ONLY when `operator_session` in `factory.yaml` is not `-`; skip it entirely otherwise,
|
|
93
|
+
since there is nowhere to mail the digest:
|
|
94
|
+
```bash
|
|
95
|
+
~/.ftown/ftown-sessions loop create \
|
|
96
|
+
--name "<project>-digest" --cron "0 9 * * *" --shell claude --model sonnet \
|
|
97
|
+
--workdir "$REPO" --group "Factory: <project>" --retention 5 \
|
|
98
|
+
--task "FTS_DB=<abs path to .ffactory/factory.db> REPO_ROOT=<abs $REPO> OPERATOR_SESSION=<operator> — Read and follow factory/skills/digest.md."
|
|
99
|
+
```
|
|
100
|
+
(`--cron`/`--shell`/`--model` from `digest.cron`/`digest.harness`/`digest.model`.)
|
|
101
|
+
7. **Wrap up.** Suggest the user commit `factory/` (not `.ffactory/`). Show them how to
|
|
102
|
+
create the first ticket:
|
|
103
|
+
```bash
|
|
104
|
+
mkdir -p .ffactory/tickets/1
|
|
105
|
+
# write .ffactory/tickets/1/request.md with the ask
|
|
106
|
+
fts create --db .ffactory/factory.db --title "<title>" --stage groom \
|
|
107
|
+
--folder .ffactory/tickets/1
|
|
108
|
+
```
|
|
109
|
+
and how to watch it: `fts serve --db .ffactory/factory.db --port 8377` (dashboard).
|
|
110
|
+
|
|
111
|
+
## up / down
|
|
112
|
+
|
|
113
|
+
Resume or pause every registered loop by name. Loops don't expose lookup-by-name
|
|
114
|
+
directly, so filter `loops` output for the project's `--group` label (`Factory:
|
|
115
|
+
<project>`) to get ids first — this naturally covers whichever loops are actually
|
|
116
|
+
registered (dispatch + triage always; digest only if an operator is configured):
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
~/.ftown/ftown-sessions loops # find ids for all loops in group "Factory: <project>"
|
|
120
|
+
~/.ftown/ftown-sessions loop resume <loop-id> # up — repeat for each loop id found
|
|
121
|
+
~/.ftown/ftown-sessions loop pause <loop-id> # down — repeat for each loop id found
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## status
|
|
125
|
+
|
|
126
|
+
Report all of:
|
|
127
|
+
|
|
128
|
+
1. `fts board --db .ffactory/factory.db` — ticket counts by stage.
|
|
129
|
+
2. `fts queues --db .ffactory/factory.db` — per-stage claim queues.
|
|
130
|
+
3. `fts doctor --db .ffactory/factory.db` — health checks (flag any failure first).
|
|
131
|
+
4. `~/.ftown/ftown-sessions loop runs <loop-id>` for every loop id registered under
|
|
132
|
+
`Factory: <project>` (dispatch, triage, and digest if present) — last few runs of
|
|
133
|
+
each, success/error. For triage specifically, also check `loop get <triage-id>` for
|
|
134
|
+
`lastSkipAt`/`lastSkipReason` — frequent skips are healthy (the preflight guard found
|
|
135
|
+
no dead-letter/orphan work).
|
|
136
|
+
5. Active workers: `~/.ftown/ftown-sessions list` filtered to names starting with
|
|
137
|
+
`<project>-t` (worker sessions the dispatcher spawned, distinct from the loop names
|
|
138
|
+
themselves which start with `<project>-dispatch`/`<project>-triage`/`<project>-digest`).
|
|
139
|
+
|
|
140
|
+
## run-now
|
|
141
|
+
|
|
142
|
+
Force an immediate dispatcher tick without waiting for the interval:
|
|
143
|
+
```bash
|
|
144
|
+
~/.ftown/ftown-sessions loop run <dispatch-loop-id>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## teardown
|
|
148
|
+
|
|
149
|
+
Confirm with the user first — this is destructive to scheduling, distinct from deleting
|
|
150
|
+
data.
|
|
151
|
+
|
|
152
|
+
1. Pause then delete every loop registered under `Factory: <project>`: `loop pause <id>`
|
|
153
|
+
then `loop delete <id>` for dispatch, triage, and digest (if it was registered).
|
|
154
|
+
2. Remind the user: `.ffactory/` is disposable runtime state (db, tickets, worktrees) —
|
|
155
|
+
only delete it if they explicitly confirm, since it's the ticket history. `factory/`
|
|
156
|
+
is the checked-in definition — keep it (removing it is a separate, deliberate repo
|
|
157
|
+
change, not part of teardown).
|
|
158
|
+
|
|
159
|
+
## Troubleshooting
|
|
160
|
+
|
|
161
|
+
| Symptom | Cause / fix |
|
|
162
|
+
| --- | --- |
|
|
163
|
+
| `ftown-sessions list` errors at any step | Bridge is down — start the ftown bridge before retrying. |
|
|
164
|
+
| `loop create --group` rejected as unknown flag | Bridge predates ftown PR #29 — tell the user to update the bridge; do not drop `--group` and register loops ungrouped. |
|
|
165
|
+
| Workers never spawn even though tickets are queued | Check `loop runs <dispatch-id>` for run status first, then the dispatcher's own stderr (surfaced in the run's session output) for `dispatch.py` errors — e.g. `max_sessions` cap reached, or a claim that keeps expiring. |
|
|
166
|
+
| A ticket looks stuck in one stage | `fts why --db .ffactory/factory.db <id>` — gives a stuck diagnosis (expired claim, bounce-limit hit, missing resource lease, etc.) without needing to read the raw db. |
|
|
167
|
+
| Triage never fires | Check `loop get <triage-id>` for `lastSkipAt`/`lastSkipReason` — preflight guard skipping is healthy when there are no dead-letter/orphan tickets; only worry if `fts triage --json` shows work but the loop still skips. |
|