ftown-bridge 0.19.37 → 0.19.39
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/browser-access.js +2 -1
- package/dist/browser-access.js.map +1 -1
- package/dist/centrifugo-client.d.ts +1 -4
- package/dist/centrifugo-client.js +0 -29
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/command-rpc.d.ts +1 -3
- package/dist/command-rpc.js +20 -74
- package/dist/command-rpc.js.map +1 -1
- package/dist/create-ftown-session.d.ts +0 -1
- package/dist/create-ftown-session.js +7 -13
- package/dist/create-ftown-session.js.map +1 -1
- package/dist/ftown-home.js +2 -2
- package/dist/ftown-sessions-cli.js +5 -340
- package/dist/ftown-sessions-cli.js.map +1 -1
- package/dist/harness-registry.d.ts +3 -22
- package/dist/harness-registry.js +3 -17
- package/dist/harness-registry.js.map +1 -1
- package/dist/index.js +10 -65
- package/dist/index.js.map +1 -1
- package/dist/install-ftown-skill.d.ts +5 -0
- package/dist/install-ftown-skill.js +91 -9
- package/dist/install-ftown-skill.js.map +1 -1
- package/dist/local-api-server.d.ts +1 -18
- package/dist/local-api-server.js +10 -108
- package/dist/local-api-server.js.map +1 -1
- package/dist/provider-env-store.js +1 -1
- package/dist/provider-env-store.js.map +1 -1
- package/dist/retired-automation.d.ts +14 -0
- package/dist/retired-automation.js +18 -0
- package/dist/retired-automation.js.map +1 -0
- package/dist/retired-skill-assets.d.ts +2 -0
- package/dist/retired-skill-assets.js +21 -0
- package/dist/retired-skill-assets.js.map +1 -0
- package/dist/session-controller.d.ts +2 -1
- package/dist/session-controller.js +5 -1
- package/dist/session-controller.js.map +1 -1
- package/dist/session-resurrection.d.ts +8 -2
- package/dist/session-resurrection.js +53 -17
- package/dist/session-resurrection.js.map +1 -1
- package/dist/solo/contract.d.ts +1 -1
- package/dist/solo/contract.js +2 -12
- package/dist/solo/contract.js.map +1 -1
- package/dist/solo/hub-manager.js +1 -1
- package/dist/terminal-pump.d.ts +1 -2
- package/dist/terminal-pump.js +1 -2
- package/dist/terminal-pump.js.map +1 -1
- package/dist/types.d.ts +4 -112
- package/dist/wire-types.d.ts +4 -48
- package/dist/wire-types.js +2 -0
- package/dist/wire-types.js.map +1 -1
- package/package.json +1 -2
- package/pi-extension/API.md +4 -12
- package/pi-extension/ftown.js +0 -138
- package/skills/ftown/SKILL.md +5 -27
- package/skills/ftown/agents/openai.yaml +2 -2
- package/skills/ftown/references/orchestrator.md +10 -65
- package/skills/ftown/references/sessions.md +3 -16
- package/skills/ftown/references/workflows.md +1 -3
- package/dist/loop-controller.d.ts +0 -87
- package/dist/loop-controller.js +0 -133
- package/dist/loop-controller.js.map +0 -1
- package/dist/loop-run-store.d.ts +0 -10
- package/dist/loop-run-store.js +0 -153
- package/dist/loop-run-store.js.map +0 -1
- package/dist/loop-schedule.d.ts +0 -18
- package/dist/loop-schedule.js +0 -35
- package/dist/loop-schedule.js.map +0 -1
- package/dist/loop-scheduler.d.ts +0 -148
- package/dist/loop-scheduler.js +0 -543
- package/dist/loop-scheduler.js.map +0 -1
- package/dist/loop-store.d.ts +0 -38
- package/dist/loop-store.js +0 -153
- package/dist/loop-store.js.map +0 -1
- package/dist/loop-validation.d.ts +0 -15
- package/dist/loop-validation.js +0 -91
- package/dist/loop-validation.js.map +0 -1
- package/skills/factory/SKILL.md +0 -198
- package/skills/factory/factory-template/bin/dispatch.py +0 -478
- package/skills/factory/factory-template/factory.yaml +0 -43
- package/skills/factory/factory-template/skills/_protocol.md +0 -105
- package/skills/factory/factory-template/skills/design.md +0 -260
- package/skills/factory/factory-template/skills/digest.md +0 -78
- package/skills/factory/factory-template/skills/groom.md +0 -243
- package/skills/factory/factory-template/skills/implement.md +0 -316
- package/skills/factory/factory-template/skills/pr.md +0 -221
- package/skills/factory/factory-template/skills/qa.md +0 -226
- package/skills/factory/factory-template/skills/review.md +0 -232
- package/skills/factory/factory-template/skills/triage.md +0 -157
- package/skills/ftown/references/loops.md +0 -93
package/dist/wire-types.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface Session {
|
|
|
43
43
|
shellType?: string;
|
|
44
44
|
model?: string;
|
|
45
45
|
parentSessionId?: string;
|
|
46
|
+
/** Historical automation provenance; new sessions cannot set it. */
|
|
47
|
+
loopId?: string;
|
|
46
48
|
usage?: SessionUsage;
|
|
47
49
|
}
|
|
48
50
|
export type MailType = 'message' | 'task' | 'result' | 'escalation';
|
|
@@ -60,54 +62,8 @@ export interface MailMessage {
|
|
|
60
62
|
deliveredAt?: string;
|
|
61
63
|
deliveredVia?: string;
|
|
62
64
|
}
|
|
63
|
-
export
|
|
64
|
-
export
|
|
65
|
-
kind: 'interval';
|
|
66
|
-
everyMs: number;
|
|
67
|
-
} | {
|
|
68
|
-
kind: 'cron';
|
|
69
|
-
expression: string;
|
|
70
|
-
tz?: string;
|
|
71
|
-
};
|
|
72
|
-
/** Body for POST /api/loops and PATCH /api/loops/:id (as a Partial). */
|
|
73
|
-
export interface LoopDraft {
|
|
74
|
-
name: string;
|
|
75
|
-
schedule: LoopSchedule;
|
|
76
|
-
harness: LoopHarness;
|
|
77
|
-
workdir?: string;
|
|
78
|
-
task: string;
|
|
79
|
-
model?: string;
|
|
80
|
-
enabled: boolean;
|
|
81
|
-
overlapPolicy: 'skip' | 'allow';
|
|
82
|
-
retention: {
|
|
83
|
-
autoClearAfterRuns: number | null;
|
|
84
|
-
};
|
|
85
|
-
preflight?: {
|
|
86
|
-
command: string;
|
|
87
|
-
timeoutMs?: number;
|
|
88
|
-
};
|
|
89
|
-
postflight?: {
|
|
90
|
-
command: string;
|
|
91
|
-
timeoutMs?: number;
|
|
92
|
-
runOnSkip?: boolean;
|
|
93
|
-
};
|
|
94
|
-
maxRuntimeMs?: number;
|
|
95
|
-
group?: string;
|
|
96
|
-
}
|
|
97
|
-
/** Loop shape as returned by GET /api/loops and /api/loops/:id. */
|
|
98
|
-
export interface LoopInfo extends LoopDraft {
|
|
99
|
-
id: string;
|
|
100
|
-
bridgeId: string;
|
|
101
|
-
createdAt: string;
|
|
102
|
-
updatedAt: string;
|
|
103
|
-
lastRunAt?: string;
|
|
104
|
-
nextRunAt?: string;
|
|
105
|
-
lastStatus?: 'ok' | 'error' | 'running' | 'skipped';
|
|
106
|
-
lastSessionId?: string;
|
|
107
|
-
runCount: number;
|
|
108
|
-
skipCount: number;
|
|
109
|
-
runNowRequested?: boolean;
|
|
110
|
-
}
|
|
65
|
+
export declare const AUTOMATION_REMOVED_CODE: "automation_removed";
|
|
66
|
+
export declare const AUTOMATION_REMOVED_MESSAGE = "Recurring loops and factory automation have been removed";
|
|
111
67
|
/**
|
|
112
68
|
* Keystrokes appended after `send`/`keys` text to submit it, keyed by shell type.
|
|
113
69
|
*
|
package/dist/wire-types.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// are the shapes as seen over the wire by those CLIs — not necessarily identical to the
|
|
8
8
|
// richer internal types in ./types.ts.
|
|
9
9
|
export const MAIL_TYPES = ['message', 'task', 'result', 'escalation'];
|
|
10
|
+
export const AUTOMATION_REMOVED_CODE = 'automation_removed';
|
|
11
|
+
export const AUTOMATION_REMOVED_MESSAGE = 'Recurring loops and factory automation have been removed';
|
|
10
12
|
/**
|
|
11
13
|
* Keystrokes appended after `send`/`keys` text to submit it, keyed by shell type.
|
|
12
14
|
*
|
package/dist/wire-types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-types.js","sourceRoot":"","sources":["../src/wire-types.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,mFAAmF;AACnF,6FAA6F;AAC7F,EAAE;AACF,mFAAmF;AACnF,sFAAsF;AACtF,wFAAwF;AACxF,uCAAuC;
|
|
1
|
+
{"version":3,"file":"wire-types.js","sourceRoot":"","sources":["../src/wire-types.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,mFAAmF;AACnF,6FAA6F;AAC7F,EAAE;AACF,mFAAmF;AACnF,sFAAsF;AACtF,wFAAwF;AACxF,uCAAuC;AAwDvC,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;AAgB3F,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAA6B,CAAC;AACrE,MAAM,CAAC,MAAM,0BAA0B,GAAG,0DAA0D,CAAC;AAErG;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,UAAmB;IAC9C,OAAO,IAAI,CAAC;AACd,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB,CAAC,CAAc;IAC9C,OAAO,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;AACpE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ftown-bridge",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.39",
|
|
4
4
|
"description": "CLI bridge for ftown — generic PTY-over-Centrifugo relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,7 +54,6 @@
|
|
|
54
54
|
"@xterm/headless": "^6.0.0",
|
|
55
55
|
"centrifuge": "^5.2.2",
|
|
56
56
|
"commander": "^13.1.0",
|
|
57
|
-
"cron-parser": "^4.9.0",
|
|
58
57
|
"express": "^5.3.0",
|
|
59
58
|
"express-rate-limit": "^8.7.1",
|
|
60
59
|
"jsonwebtoken": "^9.0.3",
|
package/pi-extension/API.md
CHANGED
|
@@ -8,7 +8,6 @@ The bundled Pi extension exposes ftown's authenticated local bridge API as a sma
|
|
|
8
8
|
| --- | --- | --- | --- |
|
|
9
9
|
| Session | Opaque UUID, or a unique exact name for lookup | Optional parent session; owns inbox, usage, screen, and log | Create, inspect, rename, reparent, stop, remove, revive |
|
|
10
10
|
| Mail message | Assigned by the session inbox | Sent from one session to another; optional thread | Send, peek/read, delivered |
|
|
11
|
-
| Loop | Opaque ID, or a unique exact name for lookup | Owns loop runs | Create, inspect, update, run, delete |
|
|
12
11
|
|
|
13
12
|
All tools talk only to the local bridge selected by `FTOWN_HOOK_PORT` or `~/.ftown/bridge.json`. The extension supplies that bridge's bearer token and never sends credentials to the model.
|
|
14
13
|
|
|
@@ -31,15 +30,8 @@ All tools talk only to the local bridge selected by `FTOWN_HOOK_PORT` or `~/.fto
|
|
|
31
30
|
| `ftown_session_manage` | `reparent` | `target`, `parent` | Updated session | Yes |
|
|
32
31
|
| `ftown_session_manage` | `remove` | `target` | Removal acknowledgement | Yes |
|
|
33
32
|
| `ftown_session_manage` | `revive` | `target` | Recreated session and resume state | Yes |
|
|
34
|
-
| `ftown_loops` | `list` | None | `{ loops }` | No |
|
|
35
|
-
| `ftown_loops` | `get` | `target` | `{ loop }` | No |
|
|
36
|
-
| `ftown_loops` | `create` | `name`, `task`, `schedule` | `{ loop }` | Yes |
|
|
37
|
-
| `ftown_loops` | `update` | `target` and changed fields | `{ loop }` | Yes |
|
|
38
|
-
| `ftown_loops` | `delete` | `target` | Removal acknowledgement | Yes |
|
|
39
|
-
| `ftown_loops` | `runs` | `target` | Loop run history | No |
|
|
40
|
-
| `ftown_loops` | `run_now` | `target` | Requested loop run | Yes |
|
|
41
33
|
|
|
42
|
-
`target` accepts an opaque ID or a unique exact name. Mail additionally accepts `parent`. Reparenting with `parent: null` clears the parent. Session
|
|
34
|
+
`target` accepts an opaque ID or a unique exact name. Mail additionally accepts `parent`. Reparenting with `parent: null` clears the parent. Session creation intentionally accepts structured fields only; arbitrary session commands and environment variables are not part of the model-facing contract.
|
|
43
35
|
|
|
44
36
|
The extension also registers `/ftown-mail read [--peek]`, `/ftown-mail send <session> <message>`, and `/ftown-sessions` for interactive use.
|
|
45
37
|
|
|
@@ -57,11 +49,11 @@ into the terminal.
|
|
|
57
49
|
|
|
58
50
|
The local bridge bearer token authorizes access to the current ftown user's bridge resources. Session name resolution is performed against that same authenticated bridge. The model can inspect a terminal screen or search its captured log, but it cannot inject raw terminal keystrokes, resize terminals, clear terminal history, or call hook/conversation-resolution internals through these tools.
|
|
59
51
|
|
|
60
|
-
Mutation executions are deduplicated in the running extension by `(tool name, Pi tool-call ID)`, including concurrent retries. Failed attempts are not cached. This prevents a retried model tool call from duplicating mail, sessions, management actions
|
|
52
|
+
Mutation executions are deduplicated in the running extension by `(tool name, Pi tool-call ID)`, including concurrent retries. Failed attempts are not cached. This prevents a retried model tool call from duplicating mail, sessions, or management actions; it is not a durable idempotency key across Pi process restarts.
|
|
61
53
|
|
|
62
54
|
## Pagination and errors
|
|
63
55
|
|
|
64
|
-
Inbox reads accept `limit` from 1 to 100. Screen and log operations accept zero-based `offset`; screen `limit` is 1 to 1,000, log `limit` is 1 to 1,000, and grep context is 0 to 10 lines. Session
|
|
56
|
+
Inbox reads accept `limit` from 1 to 100. Screen and log operations accept zero-based `offset`; screen `limit` is 1 to 1,000, log `limit` is 1 to 1,000, and grep context is 0 to 10 lines. Session collection endpoints currently return the bridge's complete local collection and inherit its unpaginated behavior.
|
|
65
57
|
|
|
66
58
|
Transport failures, missing/ambiguous names, unavailable parent context, validation failures, and bridge `{ "error": string }` responses become Pi tool errors. No response body, token, stack trace, or upstream URL is exposed to the model.
|
|
67
59
|
|
|
@@ -71,4 +63,4 @@ The tool schemas ship with `ftown-bridge` and follow its semantic version. Addin
|
|
|
71
63
|
|
|
72
64
|
## Consumer walkthrough
|
|
73
65
|
|
|
74
|
-
An agent can call `ftown_sessions.list`, use the returned session ID with `ftown_sessions.get` or `usage`, and then send context with `ftown_mail.send`. To delegate, it can create a child with `ftown_session_create`, retain the returned ID, inspect its output, and later rename, reparent, stop, remove, or revive it.
|
|
66
|
+
An agent can call `ftown_sessions.list`, use the returned session ID with `ftown_sessions.get` or `usage`, and then send context with `ftown_mail.send`. To delegate, it can create a child with `ftown_session_create`, retain the returned ID, inspect its output, and later rename, reparent, stop, remove, or revive it.
|
package/pi-extension/ftown.js
CHANGED
|
@@ -638,144 +638,6 @@ export function registerFtownPiExtension(pi, options = {}) {
|
|
|
638
638
|
},
|
|
639
639
|
});
|
|
640
640
|
|
|
641
|
-
async function listLoops() {
|
|
642
|
-
const payload = await requestJson('/api/loops', { method: 'GET' });
|
|
643
|
-
return Array.isArray(payload?.loops) ? payload.loops : [];
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function resolveLoop(loops, target) {
|
|
647
|
-
const normalized = target?.trim();
|
|
648
|
-
if (!normalized) throw new Error('A loop id or name is required');
|
|
649
|
-
const byId = loops.find((loop) => loop.id === normalized);
|
|
650
|
-
if (byId) return byId;
|
|
651
|
-
const byName = loops.filter((loop) => loop.name === normalized);
|
|
652
|
-
if (byName.length === 1) return byName[0];
|
|
653
|
-
if (byName.length > 1) throw new Error(`Multiple loops are named "${normalized}"; use an id`);
|
|
654
|
-
throw new Error(`Loop not found: ${normalized}`);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const loopScheduleProperty = {
|
|
658
|
-
type: 'object',
|
|
659
|
-
properties: {
|
|
660
|
-
kind: { type: 'string', enum: ['interval', 'cron'] },
|
|
661
|
-
everyMs: { type: 'integer', minimum: 1000 },
|
|
662
|
-
expression: { type: 'string', minLength: 1 },
|
|
663
|
-
tz: { type: 'string', minLength: 1 },
|
|
664
|
-
},
|
|
665
|
-
required: ['kind'],
|
|
666
|
-
additionalProperties: false,
|
|
667
|
-
};
|
|
668
|
-
const loopDraftProperties = {
|
|
669
|
-
name: { type: 'string', minLength: 1 },
|
|
670
|
-
task: { type: 'string', minLength: 1 },
|
|
671
|
-
schedule: loopScheduleProperty,
|
|
672
|
-
shell: { type: 'string', enum: ['claude', 'cursor', 'codex', 'grok', 'pi', 'kimi-code', 'opencode', 'shell'] },
|
|
673
|
-
workdir: { type: 'string' },
|
|
674
|
-
model: { type: 'string' },
|
|
675
|
-
enabled: { type: 'boolean' },
|
|
676
|
-
overlapPolicy: { type: 'string', enum: ['skip', 'allow'] },
|
|
677
|
-
retention: { type: ['integer', 'null'], minimum: 0 },
|
|
678
|
-
maxRuntimeMs: { type: 'integer', minimum: 1000 },
|
|
679
|
-
group: { type: 'string' },
|
|
680
|
-
};
|
|
681
|
-
const loopDraftFields = Object.keys(loopDraftProperties);
|
|
682
|
-
|
|
683
|
-
function validateLoopSchedule(schedule) {
|
|
684
|
-
requireProperty({ schedule }, 'schedule');
|
|
685
|
-
if (schedule === null || typeof schedule !== 'object') throw new Error('schedule is required');
|
|
686
|
-
requireOperation({ operation: schedule.kind }, ['interval', 'cron']);
|
|
687
|
-
if (schedule.kind === 'interval') requireProperty(schedule, 'everyMs', 'schedule.everyMs');
|
|
688
|
-
if (schedule.kind === 'cron') requireString(schedule, 'expression', 'schedule.expression');
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
function validateLoopParams(params) {
|
|
692
|
-
requireOperation(params, ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update']);
|
|
693
|
-
if (['get', 'runs', 'run_now', 'delete', 'update'].includes(params.operation)) {
|
|
694
|
-
requireString(params, 'target');
|
|
695
|
-
}
|
|
696
|
-
if (params.operation === 'create') {
|
|
697
|
-
requireString(params, 'name');
|
|
698
|
-
requireString(params, 'task');
|
|
699
|
-
requireProperty(params, 'schedule');
|
|
700
|
-
}
|
|
701
|
-
if (params.operation === 'update' && !loopDraftFields.some((field) => hasOwn(params, field))) {
|
|
702
|
-
throw new Error('At least one field to update is required');
|
|
703
|
-
}
|
|
704
|
-
if (hasOwn(params, 'schedule')) validateLoopSchedule(params.schedule);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function loopBody(params, create = false) {
|
|
708
|
-
const body = {};
|
|
709
|
-
for (const field of ['name', 'task', 'schedule', 'workdir', 'model', 'enabled', 'overlapPolicy', 'maxRuntimeMs', 'group']) {
|
|
710
|
-
if (Object.prototype.hasOwnProperty.call(params, field)) body[field] = params[field];
|
|
711
|
-
}
|
|
712
|
-
if (Object.prototype.hasOwnProperty.call(params, 'shell')) body.harness = params.shell;
|
|
713
|
-
if (Object.prototype.hasOwnProperty.call(params, 'retention')) {
|
|
714
|
-
body.retention = { autoClearAfterRuns: params.retention };
|
|
715
|
-
}
|
|
716
|
-
if (create) {
|
|
717
|
-
body.harness ??= 'pi';
|
|
718
|
-
body.enabled ??= true;
|
|
719
|
-
body.overlapPolicy ??= 'skip';
|
|
720
|
-
body.retention ??= { autoClearAfterRuns: 10 };
|
|
721
|
-
}
|
|
722
|
-
return body;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
pi.registerTool({
|
|
726
|
-
name: 'ftown_loops',
|
|
727
|
-
label: 'ftown loops',
|
|
728
|
-
description: 'List, inspect, create, update, delete, or run scheduled ftown loops.',
|
|
729
|
-
parameters: {
|
|
730
|
-
type: 'object',
|
|
731
|
-
properties: {
|
|
732
|
-
operation: {
|
|
733
|
-
type: 'string',
|
|
734
|
-
enum: ['list', 'get', 'runs', 'run_now', 'delete', 'create', 'update'],
|
|
735
|
-
},
|
|
736
|
-
target: { type: 'string' },
|
|
737
|
-
...loopDraftProperties,
|
|
738
|
-
},
|
|
739
|
-
required: ['operation'],
|
|
740
|
-
additionalProperties: false,
|
|
741
|
-
},
|
|
742
|
-
async execute(toolCallId, params) {
|
|
743
|
-
try {
|
|
744
|
-
validateLoopParams(params);
|
|
745
|
-
if (params.operation === 'create') {
|
|
746
|
-
return await executeOnce('ftown_loops', toolCallId, async () =>
|
|
747
|
-
toolResult(await requestJson('/api/loops', {
|
|
748
|
-
method: 'POST', body: JSON.stringify(loopBody(params, true)),
|
|
749
|
-
})));
|
|
750
|
-
}
|
|
751
|
-
const loops = await listLoops();
|
|
752
|
-
if (params.operation === 'list') return toolResult({ loops });
|
|
753
|
-
const loop = resolveLoop(loops, params.target);
|
|
754
|
-
const loopPath = `/api/loops/${encodeURIComponent(loop.id)}`;
|
|
755
|
-
if (params.operation === 'get') {
|
|
756
|
-
return toolResult(await requestJson(loopPath, { method: 'GET' }));
|
|
757
|
-
}
|
|
758
|
-
if (params.operation === 'runs') {
|
|
759
|
-
return toolResult(await requestJson(`${loopPath}/runs`, { method: 'GET' }));
|
|
760
|
-
}
|
|
761
|
-
const mutate = async () => {
|
|
762
|
-
if (params.operation === 'run_now') {
|
|
763
|
-
return toolResult(await requestJson(`${loopPath}/run-now`, { method: 'POST' }));
|
|
764
|
-
}
|
|
765
|
-
if (params.operation === 'delete') {
|
|
766
|
-
return toolResult(await requestJson(loopPath, { method: 'DELETE' }));
|
|
767
|
-
}
|
|
768
|
-
return toolResult(await requestJson(loopPath, {
|
|
769
|
-
method: 'PATCH', body: JSON.stringify(loopBody(params)),
|
|
770
|
-
}));
|
|
771
|
-
};
|
|
772
|
-
return await executeOnce('ftown_loops', toolCallId, mutate);
|
|
773
|
-
} catch (error) {
|
|
774
|
-
return toolError(error);
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
|
-
});
|
|
778
|
-
|
|
779
641
|
async function deliverMail(wait = 0, signal) {
|
|
780
642
|
const messages = await drainMail(wait, signal);
|
|
781
643
|
if (signal?.aborted || messages.length === 0) return 0;
|
package/skills/ftown/SKILL.md
CHANGED
|
@@ -2,31 +2,23 @@
|
|
|
2
2
|
name: ftown
|
|
3
3
|
description: >-
|
|
4
4
|
Control ftown from inside agent sessions: create/list/read/drive sibling
|
|
5
|
-
sessions, send ftown mail,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
loops, recurring agent runs, orchestration, fan-out, or ftown-workflows.
|
|
5
|
+
sessions, send ftown mail, and coordinate worker agent sessions. Use when a task
|
|
6
|
+
mentions ftown, ftown sessions, bridge sessions, child/sibling agents, mail/inbox,
|
|
7
|
+
orchestration, or fan-out.
|
|
9
8
|
---
|
|
10
9
|
|
|
11
10
|
# ftown
|
|
12
11
|
|
|
13
12
|
Use the local bridge CLIs installed under `~/.ftown`. They read
|
|
14
13
|
`~/.ftown/bridge.json`; anyone who can read that file can control the bridge's
|
|
15
|
-
sessions
|
|
14
|
+
sessions.
|
|
16
15
|
|
|
17
16
|
## Pick The Reference
|
|
18
17
|
|
|
19
18
|
- Session control, mail, terminal output, archive/revive, or local API:
|
|
20
19
|
read `references/sessions.md`.
|
|
21
|
-
- Recurring scheduled work, cron/interval loops, loop run history:
|
|
22
|
-
read `references/loops.md`.
|
|
23
20
|
- Ad-hoc multi-agent coordination from an orchestrator session:
|
|
24
21
|
read `references/orchestrator.md`.
|
|
25
|
-
- Ticket-backed multi-agent coordination, durable status/context, dependencies, and
|
|
26
|
-
resource leases: read the installed `fticket` skill. Prefer FTS over direct messages
|
|
27
|
-
whenever an existing `FTS_DB`/ticket is available.
|
|
28
|
-
- Scripted, resumable, deterministic multi-session fan-out:
|
|
29
|
-
read `references/workflows.md`.
|
|
30
22
|
|
|
31
23
|
## Quick Commands
|
|
32
24
|
|
|
@@ -36,20 +28,6 @@ sessions and loops.
|
|
|
36
28
|
~/.ftown/ftown-sessions create --prompt "Run tests" --workdir "$PWD" --parent
|
|
37
29
|
~/.ftown/ftown-sessions screen <session-id> --limit 200
|
|
38
30
|
~/.ftown/ftown-sessions tell <session-id> --type task "continue with the API client"
|
|
39
|
-
|
|
40
|
-
# Loops
|
|
41
|
-
~/.ftown/ftown-sessions loops
|
|
42
|
-
~/.ftown/ftown-sessions loop create --name repo-watch --every 30m --shell codex --workdir "$PWD" --task "Review recent changes"
|
|
43
|
-
~/.ftown/ftown-sessions loop run <loop-id>
|
|
44
|
-
|
|
45
|
-
# Workflows
|
|
46
|
-
~/.ftown/ftown-workflows run path/to/script.mjs --workdir "$PWD"
|
|
47
|
-
|
|
48
|
-
# FTS-backed coordination (when FTS_DB/TICKET_ID are supplied)
|
|
49
|
-
fts show --db "$FTS_DB" "$TICKET_ID" --json
|
|
50
|
-
fts resources --db "$FTS_DB" --json
|
|
51
31
|
```
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
control flow inside one run. Prefer orchestrator guidance for ad-hoc
|
|
55
|
-
human-in-the-loop multi-agent coordination.
|
|
33
|
+
Use orchestrator guidance for ad-hoc multi-agent coordination.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "ftown"
|
|
3
|
-
short_description: "Control ftown sessions,
|
|
4
|
-
default_prompt: "Use ftown to manage sessions
|
|
3
|
+
short_description: "Control ftown sessions, mail, and orchestration."
|
|
4
|
+
default_prompt: "Use ftown to manage sessions and mail."
|
|
@@ -6,37 +6,6 @@ can coordinate worker agent sessions on this machine using the
|
|
|
6
6
|
|
|
7
7
|
See `references/sessions.md` for the full session CLI reference.
|
|
8
8
|
|
|
9
|
-
## Coordination priority: FTS first
|
|
10
|
-
|
|
11
|
-
FTS (`fticket`) is the primary coordination plane whenever the task supplies an
|
|
12
|
-
`FTS_DB`/`TICKET_ID` or the repository already has `.ffactory/factory.db`. Read the
|
|
13
|
-
installed `fticket` skill before operating it.
|
|
14
|
-
|
|
15
|
-
- **Context:** read `fts show --db "$FTS_DB" "$TICKET_ID" --json` and the ticket's
|
|
16
|
-
immutable artifact folder. Write durable handoffs there instead of sending them in chat.
|
|
17
|
-
- **Status:** use claims, lifecycle transitions, renewals, notes, `fts board`, and
|
|
18
|
-
`fts events`; these are the shared source of truth for every worker and orchestrator.
|
|
19
|
-
- **Resources:** register named shared surfaces once, then require workers to acquire an
|
|
20
|
-
exclusive/shared lease before use and release it afterward. A waitlisted lease means
|
|
21
|
-
the worker must not touch that resource; the FTS scheduler promotes it FIFO.
|
|
22
|
-
- **Dependencies:** express ordering with ticket dependencies rather than messages such as
|
|
23
|
-
"wait until worker A is done."
|
|
24
|
-
|
|
25
|
-
Do not initialize a new FTS database merely because one is absent unless creating that
|
|
26
|
-
control plane is within the user's requested scope. When no FTS database/ticket exists,
|
|
27
|
-
or when a fenced worker cannot write to FTS, ftown mail is the fallback. Use mail for
|
|
28
|
-
urgent escalation that needs to wake a human/agent; do not duplicate routine context and
|
|
29
|
-
status in both systems.
|
|
30
|
-
|
|
31
|
-
Useful inspection commands:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
fts board --db "$FTS_DB"
|
|
35
|
-
fts show --db "$FTS_DB" "$TICKET_ID" --json
|
|
36
|
-
fts resources --db "$FTS_DB" --json
|
|
37
|
-
fts events --db "$FTS_DB" --after <cursor>
|
|
38
|
-
```
|
|
39
|
-
|
|
40
9
|
## Spawning workers
|
|
41
10
|
|
|
42
11
|
Omit `--shell` to inherit the orchestrator's current harness. Use `--shell` only
|
|
@@ -53,10 +22,9 @@ parent to `$FTOWN_SESSION_ID`.
|
|
|
53
22
|
--prompt "Review the auth module and summarize findings"
|
|
54
23
|
```
|
|
55
24
|
|
|
56
|
-
Returns JSON containing `session.id` — save it to inspect the worker later.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`--parent` are also auto-briefed about the FTS-first policy and mail fallback.
|
|
25
|
+
Returns JSON containing `session.id` — save it to inspect the worker later.
|
|
26
|
+
Workers spawned with `--parent` are auto-briefed about reporting results and
|
|
27
|
+
escalations through mail, then removing themselves when finished.
|
|
60
28
|
Provider-flavored workers require a bridge-host token configured with
|
|
61
29
|
`ftown env set <flavor> <token>`; the bridge supplies their endpoint/model
|
|
62
30
|
environment when it spawns them.
|
|
@@ -74,12 +42,10 @@ environment when it spawns them.
|
|
|
74
42
|
~/.ftown/ftown-sessions running <session-id>
|
|
75
43
|
```
|
|
76
44
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
investigate a worker whose ticket/claim has gone quiet. When operating without FTS, end
|
|
80
|
-
your turn and let fallback mail wake you instead of running a polling loop.
|
|
45
|
+
Use `screen` / `grep` to investigate a worker that has gone quiet. When waiting for
|
|
46
|
+
results, end your turn and let mail wake you instead of running a polling loop.
|
|
81
47
|
|
|
82
|
-
## Messaging
|
|
48
|
+
## Messaging (mail)
|
|
83
49
|
|
|
84
50
|
Each session has an inbox. Claude and codex receive mail automatically at turn
|
|
85
51
|
boundaries via hooks. Pi's bundled extension maintains a long-poll and wakes idle
|
|
@@ -101,15 +67,14 @@ ftown-harness mail send <child> --thread <id> "use the v2 endpoint"
|
|
|
101
67
|
ftown-harness mail read
|
|
102
68
|
```
|
|
103
69
|
|
|
104
|
-
|
|
105
|
-
`--type escalation` when blocked).
|
|
106
|
-
when a fenced/unavailable database prevents recording the outcome. Fan-out still works via
|
|
70
|
+
Children report back with `mail send --parent --type result` (or
|
|
71
|
+
`--type escalation` when blocked). Fan-out works via
|
|
107
72
|
`~/.ftown/ftown-sessions tell --children "pause and report status"` (now mail
|
|
108
73
|
under the hood; one of `--parent | --children | --siblings`).
|
|
109
74
|
|
|
110
75
|
If a worker is idle and must be woken right now, terminal keystroke injection
|
|
111
76
|
(`ftown-harness send <id> "..." -s` or `tell --keys`) is the **last resort** —
|
|
112
|
-
prefer
|
|
77
|
+
prefer mail, which arrives at the next turn boundary.
|
|
113
78
|
|
|
114
79
|
## Cleanup
|
|
115
80
|
|
|
@@ -126,30 +91,10 @@ Child briefings normally make workers report first and then remove themselves. E
|
|
|
126
91
|
ask a worker to stay open when you need follow-up interaction; otherwise, clean up any
|
|
127
92
|
worker that exits without honoring the self-removal instruction.
|
|
128
93
|
|
|
129
|
-
## Recurring work
|
|
130
|
-
|
|
131
|
-
For unattended recurring work, create a scheduled loop instead of keeping an
|
|
132
|
-
orchestrator alive to poll or sleep. See `references/loops.md` for the full loop
|
|
133
|
-
workflow:
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
~/.ftown/ftown-sessions loop create \
|
|
137
|
-
--name repo-watch \
|
|
138
|
-
--every 30m \
|
|
139
|
-
--shell codex \
|
|
140
|
-
--workdir /path/to/repo \
|
|
141
|
-
--task "Inspect recent changes, run the focused checks, and report issues"
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Loop runs show as normal ftown sessions grouped under their loop in the
|
|
145
|
-
dashboard. Use `~/.ftown/ftown-sessions loop runs <loop-id>` to inspect run
|
|
146
|
-
sessions from the CLI.
|
|
147
|
-
|
|
148
94
|
## Practical guidance
|
|
149
95
|
|
|
150
96
|
- **Parallelize** independent tasks: spawn multiple workers before waiting for any.
|
|
151
|
-
- **
|
|
152
|
-
for diagnosis and mail only as fallback.
|
|
97
|
+
- **Follow worker results**: read incoming mail and use `screen` / `grep` for diagnosis.
|
|
153
98
|
- **Name workers clearly**: `--name` shows in `list` output and as the sender
|
|
154
99
|
name on incoming mail.
|
|
155
100
|
- **Check `list` first**: see what's already running before spawning duplicates.
|
|
@@ -51,12 +51,6 @@ top-level `~/.ftown/ftown` dispatcher.
|
|
|
51
51
|
~/.ftown/ftown-sessions revive <session-id>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
## Scheduled loops
|
|
55
|
-
|
|
56
|
-
Loop runs are normal sessions tagged with `loopId`. For loop creation,
|
|
57
|
-
schedule syntax, manual runs, pause/resume, and run history, read
|
|
58
|
-
`references/loops.md`.
|
|
59
|
-
|
|
60
54
|
## Messaging (mail)
|
|
61
55
|
|
|
62
56
|
`tell` posts to the target session's **inbox**. Mail is delivered into the
|
|
@@ -137,11 +131,9 @@ Spawned ftown sessions receive:
|
|
|
137
131
|
|
|
138
132
|
Agent children (any `--shell` except `shell`) spawned with a parent also get an
|
|
139
133
|
automatic one-paragraph briefing prepended to their first input: it states their
|
|
140
|
-
name/id and parent name/id.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
it explains how to report via `ftown-harness mail send --parent --type result`.
|
|
144
|
-
After recording or reporting the outcome, the child is instructed to remove itself as
|
|
134
|
+
name/id and parent name/id. It explains how to report via
|
|
135
|
+
`ftown-harness mail send --parent --type result` (or `--type escalation` when blocked).
|
|
136
|
+
After reporting the outcome, the child is instructed to remove itself as
|
|
145
137
|
its final command unless the user explicitly asked it to stay open. The creator's
|
|
146
138
|
`--prompt` follows after a `Task:` line.
|
|
147
139
|
|
|
@@ -171,11 +163,6 @@ The CLI wraps the loopback API. Raw access if needed:
|
|
|
171
163
|
| DELETE | `/api/sessions/:id` | Remove (tombstone-archived) |
|
|
172
164
|
| GET | `/api/archive` | List removed-session tombstones |
|
|
173
165
|
| POST | `/api/sessions/:id/revive` | Recreate a removed session (new id) |
|
|
174
|
-
| GET | `/api/loops` | List scheduled loops |
|
|
175
|
-
| POST | `/api/loops` | Create a scheduled loop on this bridge |
|
|
176
|
-
| GET/PATCH/DELETE | `/api/loops/:id` | Inspect, update, or delete a loop |
|
|
177
|
-
| POST | `/api/loops/:id/run-now` | Request an immediate loop run; returns 404 `reason: not_found` if the loop no longer exists |
|
|
178
|
-
| GET | `/api/loops/:id/runs` | List run sessions for a loop |
|
|
179
166
|
|
|
180
167
|
To move a session under a different parent, PATCH it with the new parent's id:
|
|
181
168
|
|
|
@@ -11,9 +11,7 @@ This complements the by-hand orchestrator playbook in
|
|
|
11
11
|
repeatable; use orchestrator guidance when you need to improvise or keep a
|
|
12
12
|
human in the loop.
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
loop ...`. Scheduled loops create recurring ftown sessions over time. A workflow
|
|
16
|
-
loop is control flow inside one deterministic workflow run.
|
|
14
|
+
A workflow runs deterministic control flow inside one explicitly started run.
|
|
17
15
|
|
|
18
16
|
## Operating Contract
|
|
19
17
|
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Loop, LoopDraft, LoopRunRecord, Session } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Transport-agnostic loop operations, defined ONCE and shared by the two
|
|
4
|
-
* dispatch surfaces (the Centrifugo RPC switch in index.ts and the local HTTP
|
|
5
|
-
* router in local-api-server.ts). Methods take already-parsed typed input and
|
|
6
|
-
* return typed results/errors; the adapters own all wire concerns (command
|
|
7
|
-
* envelopes, HTTP statuses, response payload shaping).
|
|
8
|
-
*
|
|
9
|
-
* Error-propagation contract (matches the pre-refactor adapters byte for byte):
|
|
10
|
-
* - `create`/`update` catch store/publish failures and return `code: 'failed'`
|
|
11
|
-
* (the HTTP adapter mapped those to 400).
|
|
12
|
-
* - `delete`/`runNow`/`runs` let unexpected failures THROW (the RPC adapter's
|
|
13
|
-
* outer catch and the HTTP router's 500 handler mapped those before).
|
|
14
|
-
*/
|
|
15
|
-
export type LoopErrorCode = 'invalid' | 'not_found' | 'failed';
|
|
16
|
-
export interface LoopControllerError {
|
|
17
|
-
ok: false;
|
|
18
|
-
code: LoopErrorCode;
|
|
19
|
-
message: string;
|
|
20
|
-
}
|
|
21
|
-
export type LoopControllerResult<T> = ({
|
|
22
|
-
ok: true;
|
|
23
|
-
} & T) | LoopControllerError;
|
|
24
|
-
/** Outcome of a manual fire request. `fired: false` is NOT an error at the
|
|
25
|
-
* controller level — the RPC adapter reports it as a successful response with
|
|
26
|
-
* a reason, while the HTTP adapter maps `not_found` to a 404. */
|
|
27
|
-
export type RunLoopNowOutcome = {
|
|
28
|
-
fired: true;
|
|
29
|
-
loop: Loop;
|
|
30
|
-
} | {
|
|
31
|
-
fired: false;
|
|
32
|
-
reason: 'not_found' | 'overlap';
|
|
33
|
-
};
|
|
34
|
-
/** Injectable persistence surface (defaults to the real file-backed module). */
|
|
35
|
-
export interface LoopStoreApi {
|
|
36
|
-
createLoop(draft: LoopDraft): Loop;
|
|
37
|
-
getLoop(id: string): Loop | undefined;
|
|
38
|
-
listLoops(): Loop[];
|
|
39
|
-
updateLoop(id: string, patch: Partial<LoopDraft>): Loop | null;
|
|
40
|
-
deleteLoop(id: string): boolean;
|
|
41
|
-
mutateLoopRuntime(id: string, fn: (loop: Loop) => void): Loop | null;
|
|
42
|
-
}
|
|
43
|
-
export interface LoopRunStoreApi {
|
|
44
|
-
deleteLoopRunRecords(loopId: string): void;
|
|
45
|
-
listLoopRunRecordsWithFallback(loopId: string, sessions: Session[], loadLog?: (sessionId: string) => Promise<string>): Promise<LoopRunRecord[]>;
|
|
46
|
-
}
|
|
47
|
-
export interface LoopControllerDeps {
|
|
48
|
-
/** Loops are always owned by the bridge they were created on; every draft's
|
|
49
|
-
* bridgeId is forced to this value. */
|
|
50
|
-
bridgeId: string;
|
|
51
|
-
scheduler: {
|
|
52
|
-
kick(): void;
|
|
53
|
-
onLoopDeleted(loop: Loop): void;
|
|
54
|
-
};
|
|
55
|
-
/** runner.isRunning — used by the run-now overlap guard. */
|
|
56
|
-
isSessionRunning(sessionId: string): boolean;
|
|
57
|
-
publishLoopUpdate(loop: Loop): Promise<void>;
|
|
58
|
-
publishLoopRemoved(loopId: string): Promise<void>;
|
|
59
|
-
/** Wire-safe (env-stripped) session list, for the loop-runs fallback. */
|
|
60
|
-
listWireSessions(): Promise<Session[]>;
|
|
61
|
-
loadTerminalLog(sessionId: string): Promise<string>;
|
|
62
|
-
loopStore?: LoopStoreApi;
|
|
63
|
-
loopRunStore?: LoopRunStoreApi;
|
|
64
|
-
}
|
|
65
|
-
export declare class LoopController {
|
|
66
|
-
private readonly deps;
|
|
67
|
-
private readonly loopStore;
|
|
68
|
-
private readonly loopRunStore;
|
|
69
|
-
constructor(deps: LoopControllerDeps);
|
|
70
|
-
/** Create a loop owned by this bridge. bridgeId is forced to THIS bridge —
|
|
71
|
-
* a loop is always owned by its runner, whatever the caller sent. */
|
|
72
|
-
create(input: Partial<LoopDraft>): Promise<LoopControllerResult<{
|
|
73
|
-
loop: Loop;
|
|
74
|
-
}>>;
|
|
75
|
-
list(): Loop[];
|
|
76
|
-
get(loopId: string): LoopControllerResult<{
|
|
77
|
-
loop: Loop;
|
|
78
|
-
}>;
|
|
79
|
-
update(loopId: string, patch: Partial<LoopDraft>): Promise<LoopControllerResult<{
|
|
80
|
-
loop: Loop;
|
|
81
|
-
}>>;
|
|
82
|
-
delete(loopId: string): Promise<{
|
|
83
|
-
removed: boolean;
|
|
84
|
-
}>;
|
|
85
|
-
runNow(loopId: string): Promise<RunLoopNowOutcome>;
|
|
86
|
-
runs(loopId: string): Promise<LoopRunRecord[]>;
|
|
87
|
-
}
|