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.
Files changed (89) hide show
  1. package/dist/browser-access.js +2 -1
  2. package/dist/browser-access.js.map +1 -1
  3. package/dist/centrifugo-client.d.ts +1 -4
  4. package/dist/centrifugo-client.js +0 -29
  5. package/dist/centrifugo-client.js.map +1 -1
  6. package/dist/command-rpc.d.ts +1 -3
  7. package/dist/command-rpc.js +20 -74
  8. package/dist/command-rpc.js.map +1 -1
  9. package/dist/create-ftown-session.d.ts +0 -1
  10. package/dist/create-ftown-session.js +7 -13
  11. package/dist/create-ftown-session.js.map +1 -1
  12. package/dist/ftown-home.js +2 -2
  13. package/dist/ftown-sessions-cli.js +5 -340
  14. package/dist/ftown-sessions-cli.js.map +1 -1
  15. package/dist/harness-registry.d.ts +3 -22
  16. package/dist/harness-registry.js +3 -17
  17. package/dist/harness-registry.js.map +1 -1
  18. package/dist/index.js +10 -65
  19. package/dist/index.js.map +1 -1
  20. package/dist/install-ftown-skill.d.ts +5 -0
  21. package/dist/install-ftown-skill.js +91 -9
  22. package/dist/install-ftown-skill.js.map +1 -1
  23. package/dist/local-api-server.d.ts +1 -18
  24. package/dist/local-api-server.js +10 -108
  25. package/dist/local-api-server.js.map +1 -1
  26. package/dist/provider-env-store.js +1 -1
  27. package/dist/provider-env-store.js.map +1 -1
  28. package/dist/retired-automation.d.ts +14 -0
  29. package/dist/retired-automation.js +18 -0
  30. package/dist/retired-automation.js.map +1 -0
  31. package/dist/retired-skill-assets.d.ts +2 -0
  32. package/dist/retired-skill-assets.js +21 -0
  33. package/dist/retired-skill-assets.js.map +1 -0
  34. package/dist/session-controller.d.ts +2 -1
  35. package/dist/session-controller.js +5 -1
  36. package/dist/session-controller.js.map +1 -1
  37. package/dist/session-resurrection.d.ts +8 -2
  38. package/dist/session-resurrection.js +53 -17
  39. package/dist/session-resurrection.js.map +1 -1
  40. package/dist/solo/contract.d.ts +1 -1
  41. package/dist/solo/contract.js +2 -12
  42. package/dist/solo/contract.js.map +1 -1
  43. package/dist/solo/hub-manager.js +1 -1
  44. package/dist/terminal-pump.d.ts +1 -2
  45. package/dist/terminal-pump.js +1 -2
  46. package/dist/terminal-pump.js.map +1 -1
  47. package/dist/types.d.ts +4 -112
  48. package/dist/wire-types.d.ts +4 -48
  49. package/dist/wire-types.js +2 -0
  50. package/dist/wire-types.js.map +1 -1
  51. package/package.json +1 -2
  52. package/pi-extension/API.md +4 -12
  53. package/pi-extension/ftown.js +0 -138
  54. package/skills/ftown/SKILL.md +5 -27
  55. package/skills/ftown/agents/openai.yaml +2 -2
  56. package/skills/ftown/references/orchestrator.md +10 -65
  57. package/skills/ftown/references/sessions.md +3 -16
  58. package/skills/ftown/references/workflows.md +1 -3
  59. package/dist/loop-controller.d.ts +0 -87
  60. package/dist/loop-controller.js +0 -133
  61. package/dist/loop-controller.js.map +0 -1
  62. package/dist/loop-run-store.d.ts +0 -10
  63. package/dist/loop-run-store.js +0 -153
  64. package/dist/loop-run-store.js.map +0 -1
  65. package/dist/loop-schedule.d.ts +0 -18
  66. package/dist/loop-schedule.js +0 -35
  67. package/dist/loop-schedule.js.map +0 -1
  68. package/dist/loop-scheduler.d.ts +0 -148
  69. package/dist/loop-scheduler.js +0 -543
  70. package/dist/loop-scheduler.js.map +0 -1
  71. package/dist/loop-store.d.ts +0 -38
  72. package/dist/loop-store.js +0 -153
  73. package/dist/loop-store.js.map +0 -1
  74. package/dist/loop-validation.d.ts +0 -15
  75. package/dist/loop-validation.js +0 -91
  76. package/dist/loop-validation.js.map +0 -1
  77. package/skills/factory/SKILL.md +0 -198
  78. package/skills/factory/factory-template/bin/dispatch.py +0 -478
  79. package/skills/factory/factory-template/factory.yaml +0 -43
  80. package/skills/factory/factory-template/skills/_protocol.md +0 -105
  81. package/skills/factory/factory-template/skills/design.md +0 -260
  82. package/skills/factory/factory-template/skills/digest.md +0 -78
  83. package/skills/factory/factory-template/skills/groom.md +0 -243
  84. package/skills/factory/factory-template/skills/implement.md +0 -316
  85. package/skills/factory/factory-template/skills/pr.md +0 -221
  86. package/skills/factory/factory-template/skills/qa.md +0 -226
  87. package/skills/factory/factory-template/skills/review.md +0 -232
  88. package/skills/factory/factory-template/skills/triage.md +0 -157
  89. package/skills/ftown/references/loops.md +0 -93
@@ -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
@@ -1 +0,0 @@
1
- {"version":3,"file":"loop-store.js","sourceRoot":"","sources":["../src/loop-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACzF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAsBpD,IAAI,mBAAuC,CAAC;AAE5C,0EAA0E;AAC1E,MAAM,UAAU,sBAAsB,CAAC,IAAwB;IAC7D,mBAAmB,GAAG,IAAI,CAAC;AAC7B,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,mBAAmB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACzC,CAAC;AAED,yFAAyF;AACzF,SAAS,SAAS;IAChB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;QACzB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAuB,CAAC;QAC5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IACvB,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAe;IAChC,SAAS,CAAC,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1E,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS;AAClC,CAAC;AAED,iFAAiF;AACjF,SAAS,cAAc,CAAC,KAAyB;IAC/C,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,SAAS,EAAE,CAAC,KAAK,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,EAAU;IAChC,OAAO,SAAS,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,IAAI,GAAS;QACjB,GAAG,KAAK;QACR,EAAE,EAAE,MAAM,EAAE;QACZ,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;QAC5B,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;QAC5B,SAAS,EAAE,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE;QAChF,QAAQ,EAAE,CAAC;QACX,SAAS,EAAE,CAAC;KACb,CAAC;IACF,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC1C,IAAI,KAAK;QAAE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;;QACzB,OAAO,IAAI,CAAC,KAAK,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtB,SAAS,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,KAAyB;IAC9D,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAE9B,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACnC,MAAM,OAAO,GAAS;QACpB,GAAG,QAAQ;QACX,GAAG,KAAK;QACR,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,0EAA0E;QAC1E,uEAAuE;QACvE,2EAA2E;QAC3E,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAC;IACF,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;QACnB,OAAO,CAAC,SAAS,GAAG,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC3F,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;QACrB,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC1C,IAAI,KAAK;YAAE,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;;YAC5B,OAAO,OAAO,CAAC,KAAK,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC;IAC5B,SAAS,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EAAU;IACnC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,IAAI,SAAS,CAAC,MAAM,KAAK,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACzD,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;IACvB,SAAS,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,UAAU,CAAC,IAAU;IACnC,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC,CAAC;IAC1E,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;QACnC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,IAAI,CAAC,CAAC;AAClB,CAAC;AAOD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAU,EAAE,EAAsB;IAClE,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC/B,EAAE,CAAC,IAAI,CAAC,CAAC;IACT,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,IAAI,CAAC,CAAC;IAChB,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -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;
@@ -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":"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"}
@@ -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. |