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/loop-store.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
-
import { computeNextRun } from './loop-schedule.js';
|
|
6
|
-
let configuredFtownHome;
|
|
7
|
-
/** Inject the instance ".ftown home" once at startup (see module doc). */
|
|
8
|
-
export function configureLoopStoreHome(home) {
|
|
9
|
-
configuredFtownHome = home;
|
|
10
|
-
}
|
|
11
|
-
function ftownHome() {
|
|
12
|
-
return configuredFtownHome ?? join(homedir(), '.ftown');
|
|
13
|
-
}
|
|
14
|
-
function loopsPath() {
|
|
15
|
-
return join(ftownHome(), 'loops.json');
|
|
16
|
-
}
|
|
17
|
-
/** Tolerant loader: returns { loops: [] } on a missing OR corrupt file. Never throws. */
|
|
18
|
-
function loadLoops() {
|
|
19
|
-
try {
|
|
20
|
-
const path = loopsPath();
|
|
21
|
-
if (!existsSync(path))
|
|
22
|
-
return { loops: [] };
|
|
23
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
24
|
-
return { loops: Array.isArray(parsed.loops) ? parsed.loops : [] };
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return { loops: [] };
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function saveLoops(data) {
|
|
31
|
-
mkdirSync(ftownHome(), { recursive: true, mode: 0o700 });
|
|
32
|
-
const path = loopsPath();
|
|
33
|
-
const tmp = `${path}.tmp`;
|
|
34
|
-
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode: 0o600 });
|
|
35
|
-
renameSync(tmp, path); // atomic
|
|
36
|
-
}
|
|
37
|
-
/** Trim `group`; blank/whitespace-only collapses to undefined (field absent). */
|
|
38
|
-
function normalizeGroup(group) {
|
|
39
|
-
const trimmed = group?.trim();
|
|
40
|
-
return trimmed ? trimmed : undefined;
|
|
41
|
-
}
|
|
42
|
-
export function listLoops() {
|
|
43
|
-
return loadLoops().loops;
|
|
44
|
-
}
|
|
45
|
-
export function getLoop(id) {
|
|
46
|
-
return loadLoops().loops.find((loop) => loop.id === id);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Mint a Loop from a client draft: fresh id/timestamps, zeroed counters, and a
|
|
50
|
-
* nextRunAt computed from now — even when created disabled, so re-enabling has a
|
|
51
|
-
* target. Throws (via computeNextRun) on a malformed cron expression.
|
|
52
|
-
*/
|
|
53
|
-
export function createLoop(draft) {
|
|
54
|
-
const data = loadLoops();
|
|
55
|
-
const now = new Date();
|
|
56
|
-
const loop = {
|
|
57
|
-
...draft,
|
|
58
|
-
id: uuidv4(),
|
|
59
|
-
createdAt: now.toISOString(),
|
|
60
|
-
updatedAt: now.toISOString(),
|
|
61
|
-
nextRunAt: new Date(computeNextRun(draft.schedule, now.getTime())).toISOString(),
|
|
62
|
-
runCount: 0,
|
|
63
|
-
skipCount: 0,
|
|
64
|
-
};
|
|
65
|
-
const group = normalizeGroup(draft.group);
|
|
66
|
-
if (group)
|
|
67
|
-
loop.group = group;
|
|
68
|
-
else
|
|
69
|
-
delete loop.group;
|
|
70
|
-
data.loops.push(loop);
|
|
71
|
-
saveLoops(data);
|
|
72
|
-
return loop;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Merge `patch` over an existing loop. id + createdAt are immutable; updatedAt is
|
|
76
|
-
* bumped. A schedule change recomputes nextRunAt from now (the old target is
|
|
77
|
-
* stale). Returns null when no loop has that id.
|
|
78
|
-
*/
|
|
79
|
-
export function updateLoop(id, patch) {
|
|
80
|
-
const data = loadLoops();
|
|
81
|
-
const index = data.loops.findIndex((loop) => loop.id === id);
|
|
82
|
-
if (index === -1)
|
|
83
|
-
return null;
|
|
84
|
-
const existing = data.loops[index];
|
|
85
|
-
const updated = {
|
|
86
|
-
...existing,
|
|
87
|
-
...patch,
|
|
88
|
-
id: existing.id,
|
|
89
|
-
// A loop is owned by exactly one bridge (the one it is persisted on); its
|
|
90
|
-
// bridgeId is the RPC routing key. Never let a patch move it — a stray
|
|
91
|
-
// patch.bridgeId would otherwise desync the record from the routing guard.
|
|
92
|
-
bridgeId: existing.bridgeId,
|
|
93
|
-
createdAt: existing.createdAt,
|
|
94
|
-
updatedAt: new Date().toISOString(),
|
|
95
|
-
};
|
|
96
|
-
if (patch.schedule) {
|
|
97
|
-
updated.nextRunAt = new Date(computeNextRun(updated.schedule, Date.now())).toISOString();
|
|
98
|
-
}
|
|
99
|
-
if ('group' in patch) {
|
|
100
|
-
const group = normalizeGroup(patch.group);
|
|
101
|
-
if (group)
|
|
102
|
-
updated.group = group;
|
|
103
|
-
else
|
|
104
|
-
delete updated.group;
|
|
105
|
-
}
|
|
106
|
-
data.loops[index] = updated;
|
|
107
|
-
saveLoops(data);
|
|
108
|
-
return updated;
|
|
109
|
-
}
|
|
110
|
-
export function deleteLoop(id) {
|
|
111
|
-
const data = loadLoops();
|
|
112
|
-
const remaining = data.loops.filter((loop) => loop.id !== id);
|
|
113
|
-
if (remaining.length === data.loops.length)
|
|
114
|
-
return false;
|
|
115
|
-
data.loops = remaining;
|
|
116
|
-
saveLoops(data);
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
/** Insert a loop, or replace the existing one with the same id, in place. */
|
|
120
|
-
export function upsertLoop(loop) {
|
|
121
|
-
const data = loadLoops();
|
|
122
|
-
const index = data.loops.findIndex((existing) => existing.id === loop.id);
|
|
123
|
-
if (index === -1)
|
|
124
|
-
data.loops.push(loop);
|
|
125
|
-
else
|
|
126
|
-
data.loops[index] = loop;
|
|
127
|
-
saveLoops(data);
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* Atomically apply a scheduler-owned mutation: reload the loop FRESH from disk,
|
|
131
|
-
* apply `fn`, then save. Returns the merged loop, or null when the id no longer
|
|
132
|
-
* exists (deleted concurrently) — the caller then skips its publish so a deleted
|
|
133
|
-
* loop is never resurrected.
|
|
134
|
-
*
|
|
135
|
-
* The scheduler holds a detached Loop snapshot across long awaits (a 20s
|
|
136
|
-
* preflight, an in-process spawn). Writing that whole stale snapshot back would
|
|
137
|
-
* (a) resurrect a loop deleted mid-flight and (b) clobber a concurrent
|
|
138
|
-
* update_loop patch to user-owned fields. Because `fn` runs on the
|
|
139
|
-
* freshly-loaded record and touches only runtime fields, both hazards are gone:
|
|
140
|
-
* a delete wins (null), and an unrelated enabled/schedule/task patch survives.
|
|
141
|
-
*/
|
|
142
|
-
export function mutateLoopRuntime(id, fn) {
|
|
143
|
-
const data = loadLoops();
|
|
144
|
-
const index = data.loops.findIndex((loop) => loop.id === id);
|
|
145
|
-
if (index === -1)
|
|
146
|
-
return null;
|
|
147
|
-
const loop = data.loops[index];
|
|
148
|
-
fn(loop);
|
|
149
|
-
data.loops[index] = loop;
|
|
150
|
-
saveLoops(data);
|
|
151
|
-
return loop;
|
|
152
|
-
}
|
|
153
|
-
//# sourceMappingURL=loop-store.js.map
|
package/dist/loop-store.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loop-store.js","sourceRoot":"","sources":["../src/loop-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { LoopDraft, LoopSchedule } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
* Loop payload validation, extracted from index.ts so it is unit-testable
|
|
4
|
-
* without importing the CLI entrypoint (which calls program.parse() on load).
|
|
5
|
-
* Every validator returns an error string, or null when the input is valid.
|
|
6
|
-
*/
|
|
7
|
-
/** Derived from the harness registry — the harnesses with validForLoop. */
|
|
8
|
-
export declare const LOOP_HARNESSES: ReadonlySet<string>;
|
|
9
|
-
/** Validate a loop schedule (interval floor + cron parseability). */
|
|
10
|
-
export declare function validateSchedule(schedule: LoopSchedule | undefined): string | null;
|
|
11
|
-
export declare function validateRetention(retention: LoopDraft['retention'] | undefined): string | null;
|
|
12
|
-
/** Full-draft validation for create_loop. */
|
|
13
|
-
export declare function validateLoopDraft(draft: Partial<LoopDraft>): string | null;
|
|
14
|
-
/** Partial validation for update_loop — only the fields present in the patch. */
|
|
15
|
-
export declare function validateLoopPatch(patch: Partial<LoopDraft>): string | null;
|
package/dist/loop-validation.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { LOOP_HARNESS_TYPES } from './harness-registry.js';
|
|
2
|
-
import { computeNextRun } from './loop-schedule.js';
|
|
3
|
-
/**
|
|
4
|
-
* Loop payload validation, extracted from index.ts so it is unit-testable
|
|
5
|
-
* without importing the CLI entrypoint (which calls program.parse() on load).
|
|
6
|
-
* Every validator returns an error string, or null when the input is valid.
|
|
7
|
-
*/
|
|
8
|
-
/** Derived from the harness registry — the harnesses with validForLoop. */
|
|
9
|
-
export const LOOP_HARNESSES = new Set(LOOP_HARNESS_TYPES);
|
|
10
|
-
/** Validate a loop schedule (interval floor + cron parseability). */
|
|
11
|
-
export function validateSchedule(schedule) {
|
|
12
|
-
if (!schedule || typeof schedule !== 'object')
|
|
13
|
-
return 'schedule is required';
|
|
14
|
-
if (schedule.kind === 'interval') {
|
|
15
|
-
if (typeof schedule.everyMs !== 'number' || !Number.isFinite(schedule.everyMs) || schedule.everyMs < 1000) {
|
|
16
|
-
return 'interval everyMs must be a finite number >= 1000';
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
if (schedule.kind === 'cron') {
|
|
21
|
-
if (typeof schedule.expression !== 'string' || !schedule.expression.trim()) {
|
|
22
|
-
return 'cron expression is required';
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
computeNextRun(schedule, Date.now());
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
return `Invalid cron expression: ${schedule.expression}`;
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
return 'schedule.kind must be "interval" or "cron"';
|
|
33
|
-
}
|
|
34
|
-
export function validateRetention(retention) {
|
|
35
|
-
const value = retention?.autoClearAfterRuns;
|
|
36
|
-
const ok = value === null || (typeof value === 'number' && Number.isFinite(value) && value >= 0);
|
|
37
|
-
return retention && ok ? null : 'retention.autoClearAfterRuns must be null or a non-negative number';
|
|
38
|
-
}
|
|
39
|
-
/** Full-draft validation for create_loop. */
|
|
40
|
-
export function validateLoopDraft(draft) {
|
|
41
|
-
if (!draft || typeof draft !== 'object')
|
|
42
|
-
return 'Invalid loop payload';
|
|
43
|
-
// Required so a create can never slip past the bridge-routing guard and get
|
|
44
|
-
// duplicated across every connected bridge (create mints a fresh id).
|
|
45
|
-
if (typeof draft.bridgeId !== 'string' || !draft.bridgeId.trim())
|
|
46
|
-
return 'bridgeId is required';
|
|
47
|
-
if (typeof draft.name !== 'string' || !draft.name.trim())
|
|
48
|
-
return 'Loop name is required';
|
|
49
|
-
if (typeof draft.task !== 'string' || !draft.task.trim())
|
|
50
|
-
return 'Loop task is required';
|
|
51
|
-
if (typeof draft.harness !== 'string' || !LOOP_HARNESSES.has(draft.harness)) {
|
|
52
|
-
return `Invalid harness: ${String(draft.harness)}`;
|
|
53
|
-
}
|
|
54
|
-
if (draft.overlapPolicy !== 'skip' && draft.overlapPolicy !== 'allow') {
|
|
55
|
-
return 'overlapPolicy must be "skip" or "allow"';
|
|
56
|
-
}
|
|
57
|
-
if (typeof draft.enabled !== 'boolean')
|
|
58
|
-
return 'enabled must be a boolean';
|
|
59
|
-
const retentionError = validateRetention(draft.retention);
|
|
60
|
-
if (retentionError)
|
|
61
|
-
return retentionError;
|
|
62
|
-
return validateSchedule(draft.schedule);
|
|
63
|
-
}
|
|
64
|
-
/** Partial validation for update_loop — only the fields present in the patch. */
|
|
65
|
-
export function validateLoopPatch(patch) {
|
|
66
|
-
if (!patch || typeof patch !== 'object')
|
|
67
|
-
return 'Invalid patch';
|
|
68
|
-
if ('name' in patch && (typeof patch.name !== 'string' || !patch.name.trim())) {
|
|
69
|
-
return 'Loop name must be a non-empty string';
|
|
70
|
-
}
|
|
71
|
-
if ('task' in patch && (typeof patch.task !== 'string' || !patch.task.trim())) {
|
|
72
|
-
return 'Loop task must be a non-empty string';
|
|
73
|
-
}
|
|
74
|
-
if ('harness' in patch && (typeof patch.harness !== 'string' || !LOOP_HARNESSES.has(patch.harness))) {
|
|
75
|
-
return `Invalid harness: ${String(patch.harness)}`;
|
|
76
|
-
}
|
|
77
|
-
if ('overlapPolicy' in patch && patch.overlapPolicy !== 'skip' && patch.overlapPolicy !== 'allow') {
|
|
78
|
-
return 'overlapPolicy must be "skip" or "allow"';
|
|
79
|
-
}
|
|
80
|
-
if ('enabled' in patch && typeof patch.enabled !== 'boolean')
|
|
81
|
-
return 'enabled must be a boolean';
|
|
82
|
-
if ('retention' in patch) {
|
|
83
|
-
const retentionError = validateRetention(patch.retention);
|
|
84
|
-
if (retentionError)
|
|
85
|
-
return retentionError;
|
|
86
|
-
}
|
|
87
|
-
if ('schedule' in patch)
|
|
88
|
-
return validateSchedule(patch.schedule);
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
//# sourceMappingURL=loop-validation.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"loop-validation.js","sourceRoot":"","sources":["../src/loop-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAIpD;;;;GAIG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAwB,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAE/E,qEAAqE;AACrE,MAAM,UAAU,gBAAgB,CAAC,QAAkC;IACjE,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ;QAAE,OAAO,sBAAsB,CAAC;IAC7E,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QACjC,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;YAC1G,OAAO,kDAAkD,CAAC;QAC5D,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC7B,IAAI,OAAO,QAAQ,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3E,OAAO,6BAA6B,CAAC;QACvC,CAAC;QACD,IAAI,CAAC;YACH,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QACvC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,4BAA4B,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3D,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,4CAA4C,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,SAA6C;IAC7E,MAAM,KAAK,GAAG,SAAS,EAAE,kBAAkB,CAAC;IAC5C,MAAM,EAAE,GAAG,KAAK,KAAK,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;IACjG,OAAO,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,oEAAoE,CAAC;AACvG,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,sBAAsB,CAAC;IACvE,4EAA4E;IAC5E,sEAAsE;IACtE,IAAI,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE;QAAE,OAAO,sBAAsB,CAAC;IAChG,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,uBAAuB,CAAC;IACzF,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,uBAAuB,CAAC;IACzF,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QAC5E,OAAO,oBAAoB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,KAAK,CAAC,aAAa,KAAK,MAAM,IAAI,KAAK,CAAC,aAAa,KAAK,OAAO,EAAE,CAAC;QACtE,OAAO,yCAAyC,CAAC;IACnD,CAAC;IACD,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,2BAA2B,CAAC;IAC3E,MAAM,cAAc,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IAC1D,IAAI,cAAc;QAAE,OAAO,cAAc,CAAC;IAC1C,OAAO,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,iBAAiB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,eAAe,CAAC;IAChE,IAAI,MAAM,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9E,OAAO,sCAAsC,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9E,OAAO,sCAAsC,CAAC;IAChD,CAAC;IACD,IAAI,SAAS,IAAI,KAAK,IAAI,CAAC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;QACpG,OAAO,oBAAoB,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;IACrD,CAAC;IACD,IAAI,eAAe,IAAI,KAAK,IAAI,KAAK,CAAC,aAAa,KAAK,MAAM,IAAI,KAAK,CAAC,aAAa,KAAK,OAAO,EAAE,CAAC;QAClG,OAAO,yCAAyC,CAAC;IACnD,CAAC;IACD,IAAI,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,OAAO,2BAA2B,CAAC;IACjG,IAAI,WAAW,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,cAAc,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC1D,IAAI,cAAc;YAAE,OAAO,cAAc,CAAC;IAC5C,CAAC;IACD,IAAI,UAAU,IAAI,KAAK;QAAE,OAAO,gBAAgB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/skills/factory/SKILL.md
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
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. **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.
|
|
42
|
-
```bash
|
|
43
|
-
cp -r ~/.ftown/skills/factory/factory-template "$REPO/factory"
|
|
44
|
-
```
|
|
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.
|
|
52
|
-
4. **Gitignore + state dirs.** Append `.ffactory/` to `$REPO/.gitignore` (create the file
|
|
53
|
-
if absent). `mkdir -p "$REPO/.ffactory/tickets"`.
|
|
54
|
-
5. **Init the ticket db**, reading stage order and limits back out of the edited
|
|
55
|
-
`factory.yaml`:
|
|
56
|
-
```bash
|
|
57
|
-
fts init --db .ffactory/factory.db \
|
|
58
|
-
--stages groom,design,implement,review,qa,pr \
|
|
59
|
-
--bounce-limit 3 --claim-ttl-ms 1800000
|
|
60
|
-
```
|
|
61
|
-
(substitute the actual stage names in yaml order, `limits.bounce_limit`, and
|
|
62
|
-
`limits.claim_ttl_ms`).
|
|
63
|
-
6. **Register shared resources.** For every entry under `resources` in `factory.yaml`,
|
|
64
|
-
register its stable name and policy. For the default template:
|
|
65
|
-
```bash
|
|
66
|
-
fts register-resource --db .ffactory/factory.db \
|
|
67
|
-
--name staging --policy exclusive_only
|
|
68
|
-
```
|
|
69
|
-
Repeat once per configured resource. Workers acquire these leases FIFO; do not invent
|
|
70
|
-
resource names at runtime that were not declared in the factory definition.
|
|
71
|
-
7. **Register the loops, grouped under one factory label.** The `--group` flag folds all
|
|
72
|
-
loops together in the ftown dashboard and requires ftown bridge PR #29. If
|
|
73
|
-
`loop create` rejects `--group` as an unknown flag, tell the user their bridge is out
|
|
74
|
-
of date and needs updating — do not silently drop the flag and register ungrouped.
|
|
75
|
-
|
|
76
|
-
Dispatcher (interval loop, runs the Python dispatcher every tick):
|
|
77
|
-
```bash
|
|
78
|
-
~/.ftown/ftown-sessions loop create \
|
|
79
|
-
--name "<project>-dispatch" --every 30s --shell shell --workdir "$REPO" \
|
|
80
|
-
--group "Factory: <project>" --max-runtime 10m --retention 10 \
|
|
81
|
-
--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)'" \
|
|
82
|
-
--task "uv run --python 3.13 --with fticket,pyyaml python factory/bin/dispatch.py"
|
|
83
|
-
```
|
|
84
|
-
(`--every` from `limits.dispatch_every`. The preflight skips the tick — no shell
|
|
85
|
-
session — while no ticket is queued/claimed/in_progress/blocked, so an idle factory
|
|
86
|
-
costs nothing. A resting `rejected`/`dead_letter` ticket doesn't wake it: those belong
|
|
87
|
-
to explicit chaining and triage respectively.)
|
|
88
|
-
|
|
89
|
-
Triage (interval loop with a preflight skip guard — only spawns a session when there
|
|
90
|
-
is dead-letter/orphan work; requires ftown bridge >= 0.18.0 for cheap preflight skips —
|
|
91
|
-
on 0.17 the preflight still works but bloats run history since a skip still logs a
|
|
92
|
-
run):
|
|
93
|
-
```bash
|
|
94
|
-
~/.ftown/ftown-sessions loop create \
|
|
95
|
-
--name "<project>-triage" --every 10m --shell claude --model sonnet \
|
|
96
|
-
--workdir "$REPO" --group "Factory: <project>" --retention 10 \
|
|
97
|
-
--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)'" \
|
|
98
|
-
--task "FTS_DB=<abs path to .ffactory/factory.db> REPO_ROOT=<abs $REPO> OPERATOR_SESSION=<value or -> — Read and follow factory/skills/triage.md."
|
|
99
|
-
```
|
|
100
|
-
(`--every`/`--shell`/`--model` from `triage.every`/`triage.harness`/`triage.model`.)
|
|
101
|
-
The preflight exits 0 (work exists → run the session) when `fts triage --json` reports
|
|
102
|
-
any dead_letter or orphan tickets, and exits 1 (healthy → skip, no session spawned)
|
|
103
|
-
otherwise. The triage briefing must set the three task variables literally;
|
|
104
|
-
`triage.md` treats a missing variable as fatal.
|
|
105
|
-
|
|
106
|
-
Digest (daily cron loop, shift report mailed to the operator) — register this loop
|
|
107
|
-
ONLY when `operator_session` in `factory.yaml` is not `-`; skip it entirely otherwise,
|
|
108
|
-
since there is nowhere to mail the digest:
|
|
109
|
-
```bash
|
|
110
|
-
~/.ftown/ftown-sessions loop create \
|
|
111
|
-
--name "<project>-digest" --cron "0 9 * * *" --shell claude --model sonnet \
|
|
112
|
-
--workdir "$REPO" --group "Factory: <project>" --retention 5 \
|
|
113
|
-
--task "FTS_DB=<abs path to .ffactory/factory.db> REPO_ROOT=<abs $REPO> OPERATOR_SESSION=<operator> — Read and follow factory/skills/digest.md."
|
|
114
|
-
```
|
|
115
|
-
(`--cron`/`--shell`/`--model` from `digest.cron`/`digest.harness`/`digest.model`.)
|
|
116
|
-
8. **Wrap up.** Suggest the user commit `factory/` (not `.ffactory/`). Show them how to
|
|
117
|
-
create the first ticket:
|
|
118
|
-
```bash
|
|
119
|
-
mkdir -p .ffactory/tickets/1
|
|
120
|
-
# write .ffactory/tickets/1/request.md with the ask
|
|
121
|
-
fts create --db .ffactory/factory.db --title "<title>" --stage groom \
|
|
122
|
-
--folder .ffactory/tickets/1
|
|
123
|
-
```
|
|
124
|
-
and how to watch it: `fts serve --db .ffactory/factory.db --port 8377` (dashboard).
|
|
125
|
-
|
|
126
|
-
## resource leases for an existing factory
|
|
127
|
-
|
|
128
|
-
Existing factories are never overwritten by `factory init`. To introduce a new shared
|
|
129
|
-
resource safely, add its stable name/policy under `resources` in `factory/factory.yaml`,
|
|
130
|
-
then register it idempotently in the existing database:
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
fts register-resource --db .ffactory/factory.db \
|
|
134
|
-
--name staging --policy exclusive_only
|
|
135
|
-
fts resources --db .ffactory/factory.db --json
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Do not rename a resource while leases/waiters exist; register the new name, migrate users,
|
|
139
|
-
then retire the old name only after `fts resources` shows it idle.
|
|
140
|
-
|
|
141
|
-
## up / down
|
|
142
|
-
|
|
143
|
-
Resume or pause every registered loop by name. Loops don't expose lookup-by-name
|
|
144
|
-
directly, so filter `loops` output for the project's `--group` label (`Factory:
|
|
145
|
-
<project>`) to get ids first — this naturally covers whichever loops are actually
|
|
146
|
-
registered (dispatch + triage always; digest only if an operator is configured):
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
~/.ftown/ftown-sessions loops # find ids for all loops in group "Factory: <project>"
|
|
150
|
-
~/.ftown/ftown-sessions loop resume <loop-id> # up — repeat for each loop id found
|
|
151
|
-
~/.ftown/ftown-sessions loop pause <loop-id> # down — repeat for each loop id found
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## status
|
|
155
|
-
|
|
156
|
-
Report all of:
|
|
157
|
-
|
|
158
|
-
1. `fts board --db .ffactory/factory.db` — ticket counts by stage.
|
|
159
|
-
2. `fts queues --db .ffactory/factory.db` — per-stage claim queues.
|
|
160
|
-
3. `fts resources --db .ffactory/factory.db` — active leases and FIFO waitlists.
|
|
161
|
-
4. `fts doctor --db .ffactory/factory.db` — health checks (flag any failure first).
|
|
162
|
-
5. `~/.ftown/ftown-sessions loop runs <loop-id>` for every loop id registered under
|
|
163
|
-
`Factory: <project>` (dispatch, triage, and digest if present) — last few runs of
|
|
164
|
-
each, success/error. For triage specifically, also check `loop get <triage-id>` for
|
|
165
|
-
`lastSkipAt`/`lastSkipReason` — frequent skips are healthy (the preflight guard found
|
|
166
|
-
no dead-letter/orphan work).
|
|
167
|
-
6. Active workers: `~/.ftown/ftown-sessions list` filtered to names starting with
|
|
168
|
-
`<project>-t` (worker sessions the dispatcher spawned, distinct from the loop names
|
|
169
|
-
themselves which start with `<project>-dispatch`/`<project>-triage`/`<project>-digest`).
|
|
170
|
-
|
|
171
|
-
## run-now
|
|
172
|
-
|
|
173
|
-
Force an immediate dispatcher tick without waiting for the interval:
|
|
174
|
-
```bash
|
|
175
|
-
~/.ftown/ftown-sessions loop run <dispatch-loop-id>
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## teardown
|
|
179
|
-
|
|
180
|
-
Confirm with the user first — this is destructive to scheduling, distinct from deleting
|
|
181
|
-
data.
|
|
182
|
-
|
|
183
|
-
1. Pause then delete every loop registered under `Factory: <project>`: `loop pause <id>`
|
|
184
|
-
then `loop delete <id>` for dispatch, triage, and digest (if it was registered).
|
|
185
|
-
2. Remind the user: `.ffactory/` is disposable runtime state (db, tickets, worktrees) —
|
|
186
|
-
only delete it if they explicitly confirm, since it's the ticket history. `factory/`
|
|
187
|
-
is the checked-in definition — keep it (removing it is a separate, deliberate repo
|
|
188
|
-
change, not part of teardown).
|
|
189
|
-
|
|
190
|
-
## Troubleshooting
|
|
191
|
-
|
|
192
|
-
| Symptom | Cause / fix |
|
|
193
|
-
| --- | --- |
|
|
194
|
-
| `ftown-sessions list` errors at any step | Bridge is down — start the ftown bridge before retrying. |
|
|
195
|
-
| `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. |
|
|
196
|
-
| 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. |
|
|
197
|
-
| 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. |
|
|
198
|
-
| 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. |
|