ftown-bridge 0.19.10 → 0.19.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ftown-bridge",
3
- "version": "0.19.10",
3
+ "version": "0.19.12",
4
4
  "description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -361,14 +361,35 @@ export function registerFtownPiExtension(pi, options = {}) {
361
361
  });
362
362
 
363
363
  async function runSessions(params) {
364
- requireOperation(params, ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep']);
365
- if (!['list', 'archive'].includes(params.operation)) requireString(params, 'target');
364
+ requireOperation(params, ['list', 'children', 'archive', 'get', 'usage', 'running', 'screen', 'grep']);
365
+ if (!['list', 'children', 'archive'].includes(params.operation)) requireString(params, 'target');
366
366
  if (params.operation === 'grep') requireString(params, 'pattern');
367
367
  if (params.operation === 'archive') {
368
368
  return requestJson('/api/archive', { method: 'GET' });
369
369
  }
370
370
  const sessions = await listSessions();
371
- if (params.operation === 'list') return { sessions };
371
+ const filterByPattern = (candidates) => {
372
+ if (!params.pattern) return candidates;
373
+ let pattern;
374
+ try {
375
+ pattern = new RegExp(params.pattern, 'i');
376
+ } catch (error) {
377
+ throw new Error(`Invalid session pattern: ${error instanceof Error ? error.message : String(error)}`);
378
+ }
379
+ return candidates.filter((session) =>
380
+ Object.values(session).some((value) => typeof value === 'string' && pattern.test(value)));
381
+ };
382
+ if (params.operation === 'children') {
383
+ const parentSessionId = params.target
384
+ ? resolveSession(sessions, params.target).id
385
+ : ftownSessionId;
386
+ if (!parentSessionId) throw new Error('children requires target outside an ftown session');
387
+ const children = sessions.filter((session) => session.parentSessionId === parentSessionId);
388
+ return { sessions: filterByPattern(children) };
389
+ }
390
+ if (params.operation === 'list') {
391
+ return { sessions: filterByPattern(sessions) };
392
+ }
372
393
  const session = resolveSession(sessions, params.target);
373
394
  if (params.operation === 'get') {
374
395
  return requestJson(`/api/sessions/${encodeURIComponent(session.id)}`, { method: 'GET' });
@@ -400,7 +421,10 @@ export function registerFtownPiExtension(pi, options = {}) {
400
421
  });
401
422
  }
402
423
 
403
- const targetProperty = { type: 'string', description: 'Session id or unique exact name.' };
424
+ const targetProperty = {
425
+ type: 'string',
426
+ description: 'Session id or unique exact name; optional parent for children (defaults to current session).',
427
+ };
404
428
  const pageProperties = {
405
429
  offset: { type: 'integer', minimum: 0 },
406
430
  limit: { type: 'integer', minimum: 1, maximum: 1000 },
@@ -409,16 +433,19 @@ export function registerFtownPiExtension(pi, options = {}) {
409
433
  pi.registerTool({
410
434
  name: 'ftown_sessions',
411
435
  label: 'ftown sessions',
412
- description: 'List or inspect ftown sessions, running state, archive, token usage, terminal screen, and terminal log matches.',
436
+ description: 'List, filter, or inspect ftown sessions, including children, running state, archive, token usage, terminal screen, and terminal log matches.',
413
437
  parameters: {
414
438
  type: 'object',
415
439
  properties: {
416
440
  operation: {
417
441
  type: 'string',
418
- enum: ['list', 'archive', 'get', 'usage', 'running', 'screen', 'grep'],
442
+ enum: ['list', 'children', 'archive', 'get', 'usage', 'running', 'screen', 'grep'],
419
443
  },
420
444
  target: targetProperty,
421
- pattern: { type: 'string', minLength: 1 },
445
+ pattern: {
446
+ type: 'string', minLength: 1,
447
+ description: 'Case-insensitive regex for list/children filtering; required for terminal grep.',
448
+ },
422
449
  ...pageProperties,
423
450
  context: { type: 'integer', minimum: 0, maximum: 10 },
424
451
  },
@@ -34,15 +34,21 @@ already initialized (point them at `status`/`up`/`down` instead) — do not over
34
34
  `uv` is on PATH.
35
35
  2. **Install fticket.** `uv tool install fticket` (or `uv tool upgrade fticket` if already
36
36
  installed). Verify with `fts --help`.
37
- 3. **Copy the template.**
37
+ 3. **Resolve routing and copy the template.** If the deployment prompt names the
38
+ initiating harness/model, use those values. Otherwise, when `FTOWN_SESSION_ID` is
39
+ set, run `~/.ftown/ftown-sessions get "$FTOWN_SESSION_ID"` and read this session's
40
+ `shellType` and optional `model`. If neither source identifies the harness, ask the
41
+ user instead of assuming one.
38
42
  ```bash
39
43
  cp -r ~/.ftown/skills/factory/factory-template "$REPO/factory"
40
44
  ```
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), any stage routing tweaks (harness/model/max_workers per stage), and
44
- the shared resources workers must lease (default `staging`, exclusive-only).
45
- Edit `factory/factory.yaml` with their answers.
45
+ `factory/bin` moves in as-is (no edits). Set every stage plus `triage` and `digest` to
46
+ the initiating harness/model. When the initiator has no explicit model, remove each
47
+ template `model` value so the harness default is used. Only retain or add a different
48
+ route when the user explicitly requested that override. Also apply the project name
49
+ (default: the repo directory's basename), operator ftown session id (optional —
50
+ default `-`, no operator mail), and shared resources (default `staging`,
51
+ exclusive-only). Edit `factory/factory.yaml` with these values.
46
52
  4. **Gitignore + state dirs.** Append `.ffactory/` to `$REPO/.gitignore` (create the file
47
53
  if absent). `mkdir -p "$REPO/.ffactory/tickets"`.
48
54
  5. **Init the ticket db**, reading stage order and limits back out of the edited
@@ -272,7 +272,11 @@ def capacity_spawn(
272
272
  db_path: Path,
273
273
  dry_run: bool,
274
274
  ) -> tuple[list[int], int, int, int]:
275
- """Claim + spawn up to capacity, stages in yaml order.
275
+ """Claim + spawn up to capacity, later stages first (pipeline drain).
276
+
277
+ Reversing yaml stage order prefers tickets already moving through the pipeline over
278
+ admitting new work at the first stage. With max_sessions=1, one ticket therefore
279
+ runs end-to-end before the dispatcher starts the next one.
276
280
 
277
281
  Returns (spawned_ids, rejected_input, expiry_capped, active_total). Guards run just
278
282
  after the claim (fts.claim picks the ticket, so preflight can't run earlier); a guard
@@ -297,7 +301,7 @@ def capacity_spawn(
297
301
  except FTSError as exc:
298
302
  print(f"dead_letter failed for ticket {ticket_id}: {exc}", file=sys.stderr)
299
303
 
300
- for stage in cfg.stages:
304
+ for stage in reversed(cfg.stages):
301
305
  free = min(stage.max_workers - active_of(stage.name), budget)
302
306
  if free <= 0:
303
307
  continue
@@ -5,7 +5,7 @@ project: my-project # used to namespace ftown loops and worker names
5
5
  operator_session: "-" # ftown session id for digests/worker mail; "-" = none
6
6
 
7
7
  limits:
8
- max_sessions: 6 # global concurrent worker cap for this factory
8
+ max_sessions: 6 # global cap; set 1 for strict one-ticket end-to-end flow
9
9
  bounce_limit: 3 # rejections before auto dead-letter (fts config)
10
10
  claim_ttl_ms: 1800000 # default claim TTL (30m); stages may override
11
11
  max_ticket_age_h: 48 # dispatcher dead-letters tickets older than this
@@ -17,6 +17,7 @@ resources:
17
17
  - name: staging
18
18
  policy: exclusive_only # both | exclusive_only | shared_only
19
19
 
20
+ # Dispatcher claims later stages first, draining work before admitting new tickets.
20
21
  # Harness per stage: claude | codex | cursor | zai | kimi | deepseek | fireworks
21
22
  # next: where advance() sends the ticket on success (- = terminal)
22
23
  # bounce: where reject() sends it (- = rejection not allowed)