@onepointfour-packs/flaredeck-agent 0.1.0

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 (148) hide show
  1. package/LICENSE.md +14 -0
  2. package/README.md +102 -0
  3. package/dist/agent.d.ts +104 -0
  4. package/dist/agent.js +329 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/approval.d.ts +72 -0
  7. package/dist/approval.js +161 -0
  8. package/dist/approval.js.map +1 -0
  9. package/dist/approvals.d.ts +2 -0
  10. package/dist/approvals.js +148 -0
  11. package/dist/approvals.js.map +1 -0
  12. package/dist/auth.d.ts +79 -0
  13. package/dist/auth.js +181 -0
  14. package/dist/auth.js.map +1 -0
  15. package/dist/automode.d.ts +33 -0
  16. package/dist/automode.js +69 -0
  17. package/dist/automode.js.map +1 -0
  18. package/dist/branches.d.ts +25 -0
  19. package/dist/branches.js +81 -0
  20. package/dist/branches.js.map +1 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +12 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/codesearch.d.ts +27 -0
  25. package/dist/codesearch.js +111 -0
  26. package/dist/codesearch.js.map +1 -0
  27. package/dist/concurrency.d.ts +14 -0
  28. package/dist/concurrency.js +52 -0
  29. package/dist/concurrency.js.map +1 -0
  30. package/dist/config.d.ts +106 -0
  31. package/dist/config.js +119 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/engine-antigravity.d.ts +23 -0
  34. package/dist/engine-antigravity.js +357 -0
  35. package/dist/engine-antigravity.js.map +1 -0
  36. package/dist/engine-claude.d.ts +18 -0
  37. package/dist/engine-claude.js +64 -0
  38. package/dist/engine-claude.js.map +1 -0
  39. package/dist/engine.d.ts +122 -0
  40. package/dist/engine.js +36 -0
  41. package/dist/engine.js.map +1 -0
  42. package/dist/env.d.ts +1 -0
  43. package/dist/env.js +10 -0
  44. package/dist/env.js.map +1 -0
  45. package/dist/environment.d.ts +39 -0
  46. package/dist/environment.js +100 -0
  47. package/dist/environment.js.map +1 -0
  48. package/dist/flaredeck-check.d.ts +21 -0
  49. package/dist/flaredeck-check.js +12 -0
  50. package/dist/flaredeck-check.js.map +1 -0
  51. package/dist/flaredeck-proxy.d.ts +60 -0
  52. package/dist/flaredeck-proxy.js +186 -0
  53. package/dist/flaredeck-proxy.js.map +1 -0
  54. package/dist/flaredeck.d.ts +132 -0
  55. package/dist/flaredeck.js +106 -0
  56. package/dist/flaredeck.js.map +1 -0
  57. package/dist/gemini.d.ts +21 -0
  58. package/dist/gemini.js +59 -0
  59. package/dist/gemini.js.map +1 -0
  60. package/dist/gitguard.d.ts +2 -0
  61. package/dist/gitguard.js +90 -0
  62. package/dist/gitguard.js.map +1 -0
  63. package/dist/github.d.ts +38 -0
  64. package/dist/github.js +153 -0
  65. package/dist/github.js.map +1 -0
  66. package/dist/githubauth.d.ts +21 -0
  67. package/dist/githubauth.js +90 -0
  68. package/dist/githubauth.js.map +1 -0
  69. package/dist/intake.d.ts +124 -0
  70. package/dist/intake.js +383 -0
  71. package/dist/intake.js.map +1 -0
  72. package/dist/jam.d.ts +45 -0
  73. package/dist/jam.js +385 -0
  74. package/dist/jam.js.map +1 -0
  75. package/dist/jev.d.ts +31 -0
  76. package/dist/jev.js +75 -0
  77. package/dist/jev.js.map +1 -0
  78. package/dist/jevguard.d.ts +28 -0
  79. package/dist/jevguard.js +213 -0
  80. package/dist/jevguard.js.map +1 -0
  81. package/dist/mailbox.d.ts +22 -0
  82. package/dist/mailbox.js +51 -0
  83. package/dist/mailbox.js.map +1 -0
  84. package/dist/main.d.ts +2 -0
  85. package/dist/main.js +764 -0
  86. package/dist/main.js.map +1 -0
  87. package/dist/markdown.d.ts +5 -0
  88. package/dist/markdown.js +81 -0
  89. package/dist/markdown.js.map +1 -0
  90. package/dist/oauth.d.ts +30 -0
  91. package/dist/oauth.js +216 -0
  92. package/dist/oauth.js.map +1 -0
  93. package/dist/permissions.d.ts +43 -0
  94. package/dist/permissions.js +57 -0
  95. package/dist/permissions.js.map +1 -0
  96. package/dist/preflight.d.ts +71 -0
  97. package/dist/preflight.js +116 -0
  98. package/dist/preflight.js.map +1 -0
  99. package/dist/providers.d.ts +26 -0
  100. package/dist/providers.js +27 -0
  101. package/dist/providers.js.map +1 -0
  102. package/dist/repopicker.d.ts +3 -0
  103. package/dist/repopicker.js +111 -0
  104. package/dist/repopicker.js.map +1 -0
  105. package/dist/runs.d.ts +59 -0
  106. package/dist/runs.js +211 -0
  107. package/dist/runs.js.map +1 -0
  108. package/dist/sandbox.d.ts +13 -0
  109. package/dist/sandbox.js +110 -0
  110. package/dist/sandbox.js.map +1 -0
  111. package/dist/schedules.d.ts +11 -0
  112. package/dist/schedules.js +85 -0
  113. package/dist/schedules.js.map +1 -0
  114. package/dist/secretguard.d.ts +4 -0
  115. package/dist/secretguard.js +115 -0
  116. package/dist/secretguard.js.map +1 -0
  117. package/dist/secrets.d.ts +27 -0
  118. package/dist/secrets.js +51 -0
  119. package/dist/secrets.js.map +1 -0
  120. package/dist/serve.d.ts +17 -0
  121. package/dist/serve.js +1206 -0
  122. package/dist/serve.js.map +1 -0
  123. package/dist/status.d.ts +73 -0
  124. package/dist/status.js +98 -0
  125. package/dist/status.js.map +1 -0
  126. package/dist/stream.d.ts +91 -0
  127. package/dist/stream.js +103 -0
  128. package/dist/stream.js.map +1 -0
  129. package/dist/streamtoken.d.ts +2 -0
  130. package/dist/streamtoken.js +30 -0
  131. package/dist/streamtoken.js.map +1 -0
  132. package/dist/team.d.ts +61 -0
  133. package/dist/team.js +136 -0
  134. package/dist/team.js.map +1 -0
  135. package/dist/ui.d.ts +110 -0
  136. package/dist/ui.js +348 -0
  137. package/dist/ui.js.map +1 -0
  138. package/dist/updater.d.ts +15 -0
  139. package/dist/updater.js +178 -0
  140. package/dist/updater.js.map +1 -0
  141. package/dist/workspace.d.ts +115 -0
  142. package/dist/workspace.js +247 -0
  143. package/dist/workspace.js.map +1 -0
  144. package/dist/worktree.d.ts +42 -0
  145. package/dist/worktree.js +140 -0
  146. package/dist/worktree.js.map +1 -0
  147. package/package.json +52 -0
  148. package/python/sidecar.py +178 -0
@@ -0,0 +1,140 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ const git = (args, cwd) => new Promise((resolve) => {
5
+ const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
6
+ let out = '';
7
+ const cap = (d) => { if (out.length < 4000)
8
+ out += String(d); };
9
+ child.stdout.on('data', cap);
10
+ child.stderr.on('data', cap);
11
+ child.on('close', (c) => resolve({ code: c ?? 1, out: out.trim() }));
12
+ child.on('error', (e) => resolve({ code: 1, out: String(e) }));
13
+ });
14
+ /** `BIR-0282` → `agent/bir-0282`. Stable, so re-work on a card returns to the same branch. */
15
+ const branchFor = (card) => `agent/${card.toLowerCase().replace(/[^a-z0-9._-]+/g, '-')}`;
16
+ export async function ensureWorktree(repo, card, onStep,
17
+ /**
18
+ * Where work is cut FROM, which is not always the repo's default branch. Some projects have agents
19
+ * branch from `dev` — the lead owns it, agents take the latest and hand a feature branch back.
20
+ * Cutting from the default instead would base every card on the wrong history, and the merge
21
+ * would look like a much bigger change than it is.
22
+ */
23
+ base) {
24
+ if (!existsSync(join(repo, '.git')))
25
+ return null; // not a git repo; work in place
26
+ const branch = branchFor(card);
27
+ // Beside the repo, not inside it: a worktree within the working tree confuses status, ignore
28
+ // rules and every tool that walks the directory.
29
+ const path = join(repo, '..', `.worktrees-${basenameOf(repo)}`, branch.replace(/\//g, '-'));
30
+ if (existsSync(path)) {
31
+ onStep(`reusing worktree ${branch}`);
32
+ return { path, branch, created: false };
33
+ }
34
+ // Branch from the default branch as the REMOTE has it, so an agent does not inherit whatever
35
+ // half-finished state a local checkout happens to be in. A repo with no remote is a normal
36
+ // case though — a scratch project, a fresh init — so fall back to the local branch rather
37
+ // than giving up on isolation entirely, which is the part that matters.
38
+ const fallback = await defaultBranch(repo);
39
+ const from = base ?? fallback;
40
+ await git(['fetch', '--quiet', 'origin', from], repo);
41
+ const exists = async (ref) => (await git(['rev-parse', '--verify', ref], repo)).code === 0;
42
+ let start = (await exists(`origin/${from}`)) ? `origin/${from}` : (await exists(from)) ? from : '';
43
+ if (!start) {
44
+ // A declared base that does not exist is a configuration mistake, and it used to be absorbed
45
+ // silently: the branch fell through to HEAD, meaning whatever this checkout happened to be
46
+ // sitting on — someone else's half-finished branch as easily as anything. The card then looked
47
+ // normal and its diff was against the wrong history. Say it, and fall back to the repo's own
48
+ // default rather than to an accident.
49
+ onStep(`base "${from}" is not on origin or local — falling back to ${fallback}`);
50
+ start = (await exists(`origin/${fallback}`)) ? `origin/${fallback}` : (await exists(fallback)) ? fallback : 'HEAD';
51
+ }
52
+ const made = await git(['worktree', 'add', '-B', branch, path, start], repo);
53
+ if (made.code !== 0) {
54
+ onStep(`could not make a worktree: ${made.out.split('\n').slice(-1)[0]}`);
55
+ return null;
56
+ }
57
+ // Make `origin` fetch-only IN THIS WORKTREE.
58
+ //
59
+ // Up to here, "do not publish from the shell" was a PreToolUse hook reading command text, and
60
+ // it says itself that nothing reading strings can be airtight: `git -C`, an alias, a line
61
+ // inside a script. What made that a real hole rather than a theoretical one is the machine —
62
+ // Xcode ships a system gitconfig with `credential.helper = osxkeychain`, and `gh` is logged in
63
+ // with `repo` and `workflow` scope, so any push that got past the regex would quietly find a
64
+ // working credential and succeed.
65
+ //
66
+ // A dead pushurl closes it without reading anything: `git push`, `git push origin`, `git push
67
+ // -f origin main` all fail immediately in this worktree, however they are spelled and whatever
68
+ // wrapper they are spelled inside. Fetch is untouched, so the agent can still stay current
69
+ // with the base branch, which is work it legitimately does. And push_branch is unaffected
70
+ // because it never pushes to `origin`: it builds a one-shot authenticated URL of its own.
71
+ //
72
+ // This is not the fence either — branch protection on the remote is. It is the difference
73
+ // between a door that is asked not to be opened and one with no handle on this side.
74
+ const pushBlocked = await git(['config', '--local', 'remote.origin.pushurl', 'no-push/publish-with-the-push_branch-tool'], path);
75
+ if (pushBlocked.code !== 0) {
76
+ // Said out loud rather than swallowed: the hook still refuses shell pushes, but this
77
+ // worktree is one layer thinner than the others and that is worth knowing.
78
+ onStep(`could not make origin fetch-only here — shell pushes are blocked by the guard only`);
79
+ }
80
+ onStep(`worktree ${branch} from ${start}`);
81
+ return { path, branch, created: true, from: start };
82
+ }
83
+ async function defaultBranch(repo) {
84
+ const head = await git(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], repo);
85
+ if (head.code === 0 && head.out)
86
+ return head.out.replace(/^origin\//, '');
87
+ for (const guess of ['main', 'master', 'dev']) {
88
+ if ((await git(['rev-parse', '--verify', `origin/${guess}`], repo)).code === 0)
89
+ return guess;
90
+ }
91
+ return 'main';
92
+ }
93
+ const basenameOf = (p) => p.replace(/\/+$/, '').split('/').pop() || 'repo';
94
+ /** Told to the agent so it knows where it is and what it is on. */
95
+ export function worktreeBrief(tree) {
96
+ if (!tree)
97
+ return '';
98
+ return (`\n\nYou are working in ${tree.path} on branch ${tree.branch}` +
99
+ `${tree.from ? `, branched from ${tree.from}` : ''} — a checkout of your own. Commit here as ` +
100
+ `you go, then publish with push_branch. Do not touch any other checkout on this machine: ` +
101
+ `someone may be using it.`);
102
+ }
103
+ /**
104
+ * Remove a card's worktree once the card is finished with.
105
+ *
106
+ * Worktrees accumulate: one repository here had fourteen, most of them long-done cards, each a
107
+ * full checkout on disk. But a worktree is also the ONLY copy of anything an agent has committed
108
+ * and not published, so this refuses far more readily than it deletes.
109
+ *
110
+ * It removes only when there is nothing to lose: no uncommitted changes, no untracked files, and
111
+ * no commit that is not already on the remote. Anything else is left exactly where it is, and the
112
+ * reason is returned so a person can be told rather than left to find an orphan later.
113
+ */
114
+ export async function releaseWorktree(repo, card, onStep) {
115
+ const branch = branchFor(card);
116
+ const path = join(repo, '..', `.worktrees-${basenameOf(repo)}`, branch.replace(/\//g, '-'));
117
+ if (!existsSync(path))
118
+ return { removed: false, why: 'there was no worktree' };
119
+ const dirty = await git(['status', '--porcelain'], path);
120
+ if (dirty.code !== 0)
121
+ return { removed: false, why: `could not read its status (${dirty.out.split('\n')[0]})` };
122
+ if (dirty.out.trim())
123
+ return { removed: false, why: 'it has uncommitted changes' };
124
+ // Commits that exist here and nowhere else. `--remotes` covers a branch pushed under any name,
125
+ // so a published branch counts as safe however push_branch named it.
126
+ const unpushed = await git(['log', '--oneline', '--not', '--remotes'], path);
127
+ if (unpushed.code === 0 && unpushed.out.trim()) {
128
+ const n = unpushed.out.trim().split('\n').length;
129
+ return { removed: false, why: `it has ${n} commit${n === 1 ? '' : 's'} that are not on the remote` };
130
+ }
131
+ const gone = await git(['worktree', 'remove', '--force', path], repo);
132
+ if (gone.code !== 0)
133
+ return { removed: false, why: `git would not remove it (${gone.out.split('\n')[0]})` };
134
+ // The branch too — it pointed at the same published commits, and leaving hundreds of agent/*
135
+ // branches behind makes a real branch list unreadable.
136
+ await git(['branch', '-D', branch], repo);
137
+ onStep(`released worktree ${branch}`);
138
+ return { removed: true, why: 'nothing was left on it' };
139
+ }
140
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../src/worktree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAgBjC,MAAM,GAAG,GAAG,CAAC,IAAc,EAAE,GAAW,EAA0C,EAAE,CAClF,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;IACtB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAC7E,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,EAAE,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI;QAAE,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;IACrE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACjE,CAAC,CAAC,CAAC;AAEL,8FAA8F;AAC9F,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,SAAS,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC;AAEjG,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAAY,EACZ,IAAY,EACZ,MAA8B;AAC9B;;;;;GAKG;AACH,IAAa;IAEb,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,gCAAgC;IAElF,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,6FAA6F;IAC7F,iDAAiD;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IAE5F,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,MAAM,CAAC,oBAAoB,MAAM,EAAE,CAAC,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1C,CAAC;IAED,6FAA6F;IAC7F,2FAA2F;IAC3F,0FAA0F;IAC1F,wEAAwE;IACxE,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,IAAI,QAAQ,CAAC;IAC9B,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC;IACnG,IAAI,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACnG,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,6FAA6F;QAC7F,2FAA2F;QAC3F,+FAA+F;QAC/F,6FAA6F;QAC7F,sCAAsC;QACtC,MAAM,CAAC,SAAS,IAAI,iDAAiD,QAAQ,EAAE,CAAC,CAAC;QACjF,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC;IACrH,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7E,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,MAAM,CAAC,8BAA8B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IACD,6CAA6C;IAC7C,EAAE;IACF,8FAA8F;IAC9F,0FAA0F;IAC1F,6FAA6F;IAC7F,+FAA+F;IAC/F,6FAA6F;IAC7F,kCAAkC;IAClC,EAAE;IACF,8FAA8F;IAC9F,+FAA+F;IAC/F,2FAA2F;IAC3F,0FAA0F;IAC1F,0FAA0F;IAC1F,EAAE;IACF,0FAA0F;IAC1F,qFAAqF;IACrF,MAAM,WAAW,GAAG,MAAM,GAAG,CAC3B,CAAC,QAAQ,EAAE,SAAS,EAAE,uBAAuB,EAAE,2CAA2C,CAAC,EAC3F,IAAI,CACL,CAAC;IACF,IAAI,WAAW,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC3B,qFAAqF;QACrF,2EAA2E;QAC3E,MAAM,CAAC,oFAAoF,CAAC,CAAC;IAC/F,CAAC;IAED,MAAM,CAAC,YAAY,MAAM,SAAS,KAAK,EAAE,CAAC,CAAC;IAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACtD,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,IAAY;IACvC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,cAAc,EAAE,SAAS,EAAE,SAAS,EAAE,0BAA0B,CAAC,EAAE,IAAI,CAAC,CAAC;IACjG,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;IAC1E,KAAK,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,UAAU,KAAK,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IAC/F,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,MAAM,CAAC;AAEnF,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,IAAqB;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,OAAO,CACL,0BAA0B,IAAI,CAAC,IAAI,cAAc,IAAI,CAAC,MAAM,EAAE;QAC9D,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,4CAA4C;QAC9F,0FAA0F;QAC1F,0BAA0B,CAC3B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAY,EACZ,IAAY,EACZ,MAA8B;IAE9B,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5F,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,uBAAuB,EAAE,CAAC;IAE/E,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,IAAI,CAAC,CAAC;IACzD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,8BAA8B,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAChH,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,4BAA4B,EAAE,CAAC;IAEnF,+FAA+F;IAC/F,qEAAqE;IACrE,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC;IAC7E,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;QAC/C,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC;QACjD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,6BAA6B,EAAE,CAAC;IACvG,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,CAAC;IACtE,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,4BAA4B,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IAC5G,6FAA6F;IAC7F,uDAAuD;IACvD,MAAM,GAAG,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IAC1C,MAAM,CAAC,qBAAqB,MAAM,EAAE,CAAC,CAAC;IACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,wBAAwB,EAAE,CAAC;AAC1D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@onepointfour-packs/flaredeck-agent",
3
+ "version": "0.1.0",
4
+ "description": "Run a FlareDeck agent on your own machine: it picks up cards assigned to it, works each one in a checkout of its own, and reports back on the card.",
5
+ "type": "module",
6
+ "bin": {
7
+ "flaredeck-agent": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "python",
12
+ "LICENSE.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "dev": "tsx src/cli.ts",
20
+ "typecheck": "tsc --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@anthropic-ai/claude-agent-sdk": "^0.3.283",
24
+ "ink": "^7.1.1",
25
+ "ink-text-input": "^6.0.0",
26
+ "react": "^19.3.0",
27
+ "yaml": "^2.9.1",
28
+ "zod": "^4.6.5"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.2",
32
+ "@types/react": "^19.3.0",
33
+ "tsx": "^4.19.2",
34
+ "typescript": "^5.7.2"
35
+ },
36
+ "license": "SEE LICENSE IN LICENSE.md",
37
+ "homepage": "https://app.flaredeck.app",
38
+ "bugs": {
39
+ "url": "https://app.flaredeck.app"
40
+ },
41
+ "author": "Phiresky",
42
+ "keywords": [
43
+ "flaredeck",
44
+ "agent",
45
+ "kanban",
46
+ "mcp",
47
+ "claude",
48
+ "gemini",
49
+ "automation",
50
+ "coding-agent"
51
+ ]
52
+ }
@@ -0,0 +1,178 @@
1
+ """The Gemini half of the harness.
2
+
3
+ The harness is TypeScript; the Antigravity SDK is Python with a compiled runtime binary, so this
4
+ runs as a child process and speaks JSON Lines over stdin/stdout. It is deliberately thin: it owns
5
+ no policy and makes no decisions. Every tool call is referred back to Node over localhost, so
6
+ gitGuard, secretGuard and the Jev gate keep ONE implementation rather than being rewritten here
7
+ and drifting from the originals.
8
+
9
+ Node → here {"type":"run","brief":...,"cwd":...,"resume":...,"model":...,"guard_url":...}
10
+ {"type":"steer","text":...}
11
+ {"type":"stop"}
12
+ here → Node {"type":"event","kind":"text"|"tool"|"todo"|"usage"|"done"|"ready", ...}
13
+ {"type":"fatal","message":...}
14
+
15
+ The event names are the harness's own `AgentEvent` union, not Antigravity's, because that union
16
+ was written for its consumers and already has no vendor in it.
17
+ """
18
+ import asyncio
19
+ import json
20
+ import os
21
+ import sys
22
+ import urllib.request
23
+
24
+ from contextlib import AsyncExitStack
25
+
26
+ from google.antigravity import Agent, LocalAgentConfig
27
+ from google.antigravity.hooks import policy, post_tool_call, pre_tool_call_decide
28
+ from google.antigravity.types import HookResult, McpStreamableHttpServer
29
+
30
+ GUARD_URL = ""
31
+
32
+
33
+ def emit(obj: dict) -> None:
34
+ """One JSON object per line, flushed — Node is reading this as a stream, not a file."""
35
+ sys.stdout.write(json.dumps(obj) + "\n")
36
+ sys.stdout.flush()
37
+
38
+
39
+ def ask_node(path: str, payload: dict) -> dict:
40
+ """Put the decision where the policy already lives. A guard that cannot be reached DENIES:
41
+ an agent that keeps working because its supervisor is unreachable is the failure this
42
+ harness exists to prevent."""
43
+ try:
44
+ req = urllib.request.Request(
45
+ f"{GUARD_URL}{path}",
46
+ data=json.dumps(payload).encode(),
47
+ headers={"Content-Type": "application/json"},
48
+ )
49
+ with urllib.request.urlopen(req, timeout=120) as r:
50
+ return json.loads(r.read().decode())
51
+ except Exception as e: # noqa: BLE001 - any failure here must be closed, not loud
52
+ return {"allow": False, "message": f"The harness could not be reached to approve this ({e})."}
53
+
54
+
55
+ @pre_tool_call_decide
56
+ async def gate(data):
57
+ """Antigravity's shell tool is `run_command` and its argument is `CommandLine` — NOT
58
+ `command`. Reading the wrong key yields an empty string, which every guard would wave
59
+ through, so the name is asserted rather than assumed."""
60
+ args = dict(getattr(data, "args", {}) or {})
61
+ verdict = ask_node("/tool-call", {
62
+ "name": str(getattr(data, "name", "")),
63
+ # Which server a tool came from: the harness pre-approves FlareDeck's READ tools the same
64
+ # way it does for the other engine, and cannot tell them apart without this.
65
+ "server": str(getattr(data, "server_name", "") or ""),
66
+ "args": args,
67
+ })
68
+ if not verdict.get("allow", False):
69
+ return HookResult(allow=False, message=verdict.get("message", "Refused by the harness."))
70
+ modified = verdict.get("modified_args")
71
+ if modified:
72
+ return HookResult(allow=True, modified_args=modified)
73
+ return HookResult(allow=True)
74
+
75
+
76
+ @post_tool_call
77
+ async def seen(data):
78
+ """PostToolCallHook is an InspectHook: it can SEE a result and cannot change it. So this
79
+ reports a secret that came back rather than redacting it — the harness decides what to do
80
+ about an engine that cannot redact, and says so out loud rather than pretending."""
81
+ result = getattr(data, "result", None)
82
+ if result is not None:
83
+ ask_node("/tool-result", {"name": str(getattr(data, "name", "")), "result": str(result)[:20000]})
84
+ return None
85
+
86
+
87
+ async def main() -> None:
88
+ global GUARD_URL
89
+ first = json.loads(sys.stdin.readline())
90
+ if first.get("type") != "run":
91
+ emit({"type": "fatal", "message": f"expected a run message, got {first.get('type')!r}"})
92
+ return
93
+ GUARD_URL = first["guard_url"]
94
+
95
+ kwargs = {
96
+ "system_instructions": first.get("system_instructions") or "",
97
+ "policies": [policy.allow_all()], # the gate above is the policy; this just defers to it
98
+ "hooks": [gate, seen],
99
+ "workspaces": [first["cwd"]],
100
+ }
101
+ # FlareDeck, reached over HTTP with the HARNESS's own token — so the model's tools act as the
102
+ # same account the harness listens as. Two identities for one agent is a thing that has gone
103
+ # wrong here before and is hard to see once it has.
104
+ mcp = [
105
+ McpStreamableHttpServer(name=m["name"], url=m["url"], headers=m.get("headers") or {})
106
+ for m in (first.get("mcp") or [])
107
+ ]
108
+ if mcp:
109
+ kwargs["mcp_servers"] = mcp
110
+ if os.environ.get("GOOGLE_GENAI_USE_VERTEXAI"):
111
+ kwargs.update(vertex=True, project=os.environ["GOOGLE_CLOUD_PROJECT"],
112
+ location=os.environ.get("GOOGLE_CLOUD_LOCATION", "us-central1"))
113
+ else:
114
+ kwargs["api_key"] = os.environ["GEMINI_API_KEY"]
115
+ if first.get("model"):
116
+ kwargs["model"] = first["model"]
117
+ if first.get("resume"):
118
+ kwargs["conversation_id"] = first["resume"]
119
+
120
+ # A conversation lives in the Antigravity runtime, which dies with this process — so a session
121
+ # id the harness stored before a restart names something that no longer exists, and asking for
122
+ # it fails the whole card: "Failed to create agent: conversation ... not found". The card is
123
+ # not lost by that. Its whole thread is on FlareDeck and it is perfectly workable from a fresh
124
+ # conversation, so an unusable resume is dropped rather than raised.
125
+ #
126
+ # Both the construction AND the entering are covered, because the runtime is only reached on
127
+ # entry — that is where the failure actually surfaces.
128
+ stack = AsyncExitStack()
129
+ try:
130
+ agent = await stack.enter_async_context(Agent(LocalAgentConfig(**kwargs)))
131
+ except Exception as e: # noqa: BLE001
132
+ if not kwargs.get("conversation_id") or "not found" not in str(e).lower():
133
+ raise
134
+ gone = str(kwargs.pop("conversation_id"))[:8]
135
+ await stack.aclose()
136
+ # Said out loud: an agent that silently forgot a conversation would re-read the card and
137
+ # quietly redo work it had already reported.
138
+ emit({"type": "event", "kind": "text",
139
+ "text": f"The earlier session for this card is gone ({gone}…), so this starts fresh from the card and its comments."})
140
+ stack = AsyncExitStack()
141
+ agent = await stack.enter_async_context(Agent(LocalAgentConfig(**kwargs)))
142
+
143
+ async with stack:
144
+ # "connected", not "configured", and it is not a guess: a server that fails to load raises
145
+ # during Agent construction ("MCP load failed for flaredeck: ..."), so reaching this line
146
+ # means every server in `mcp` is really attached to THIS session. The harness refuses to
147
+ # work a card otherwise, and it is right to — the tool names come from the harness's own
148
+ # discovery a moment earlier, because the SDK gives no way to enumerate a session's tools.
149
+ emit({"type": "event", "kind": "ready", "tools": first.get("tool_names") or [],
150
+ "mcp": [{"name": m.name, "status": "connected"} for m in mcp],
151
+ "apiKeySource": "vertex" if kwargs.get("vertex") else "gemini-api-key",
152
+ "permissionMode": "harness-gated",
153
+ "sessionId": str(getattr(agent, "conversation_id", "") or "")})
154
+ response = await agent.chat(first["brief"])
155
+ text = await response.text()
156
+ if text:
157
+ emit({"type": "event", "kind": "text", "text": text})
158
+ # The session id only exists AFTER the first turn — conversation_id is None until then —
159
+ # so the ready event above could never carry it, and the harness, finding no session,
160
+ # skipped the handover entirely: "no session to write a handover from". That handover is
161
+ # where an agent writes its notes and closes the card, so the work half ran and the
162
+ # report half silently never did. Sending ready again with the real id is enough: the
163
+ # harness stores a session id from any ready that has one.
164
+ session_id = str(getattr(agent, "conversation_id", "") or "")
165
+ if session_id:
166
+ emit({"type": "event", "kind": "ready", "tools": first.get("tool_names") or [],
167
+ "mcp": [{"name": m.name, "status": "connected"} for m in mcp],
168
+ "apiKeySource": "vertex" if kwargs.get("vertex") else "gemini-api-key",
169
+ "permissionMode": "harness-gated", "sessionId": session_id})
170
+ emit({"type": "event", "kind": "done", "ok": True, "result": text or ""})
171
+
172
+
173
+ if __name__ == "__main__":
174
+ try:
175
+ asyncio.run(main())
176
+ except Exception as e: # noqa: BLE001
177
+ emit({"type": "fatal", "message": str(e)})
178
+ sys.exit(1)