@kendoo.agentdesk/agentdesk 0.27.0 → 0.28.1

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 (47) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/bin/agentdesk.mjs +35 -45
  3. package/cli/agents.mjs +4 -256
  4. package/cli/bootstrap.mjs +40 -59
  5. package/cli/config.mjs +29 -4
  6. package/cli/daemon.mjs +72 -44
  7. package/cli/dotenv.mjs +96 -13
  8. package/cli/engine/agents/index.mjs +152 -0
  9. package/cli/engine/claude-auth.mjs +72 -0
  10. package/cli/engine/env.mjs +56 -0
  11. package/cli/engine/events.mjs +214 -0
  12. package/cli/engine/hooks.mjs +119 -0
  13. package/cli/engine/phases/EXECUTION.md +45 -0
  14. package/cli/engine/phases/INTAKE.md +34 -0
  15. package/cli/engine/phases/PLAN.md +26 -0
  16. package/cli/engine/phases/REVIEW.md +21 -0
  17. package/cli/engine/phases/SOLO.md +115 -0
  18. package/cli/engine/phases/SUMMARY.md +23 -0
  19. package/cli/engine/prompts.mjs +181 -0
  20. package/cli/engine/query.mjs +63 -0
  21. package/cli/engine/schemas.mjs +180 -0
  22. package/cli/engine/session.mjs +285 -0
  23. package/cli/engine/spawn.mjs +83 -0
  24. package/cli/engine/tracker/github.md +19 -0
  25. package/cli/engine/tracker/jira.md +23 -0
  26. package/cli/engine/tracker/linear.md +24 -0
  27. package/cli/engine/verdict.mjs +83 -0
  28. package/cli/init.mjs +290 -147
  29. package/cli/login.mjs +11 -3
  30. package/cli/phase-loop.mjs +78 -0
  31. package/cli/proc.mjs +131 -0
  32. package/cli/project-key.mjs +56 -0
  33. package/cli/prompt.mjs +9 -503
  34. package/cli/prompts.mjs +20 -1
  35. package/cli/security-check.mjs +1 -1
  36. package/cli/session-isolation.mjs +65 -9
  37. package/cli/session-sandbox.mjs +13 -1
  38. package/cli/setup-helpers.mjs +83 -36
  39. package/cli/team.mjs +41 -34
  40. package/cli/tracker-check.mjs +12 -2
  41. package/cli/tracker-project.mjs +93 -0
  42. package/cli/update-check.mjs +62 -0
  43. package/package.json +12 -3
  44. package/cli/orchestrator.mjs +0 -461
  45. package/cli/stream-parser.mjs +0 -216
  46. package/prompts/phased.md +0 -549
  47. package/prompts/team.md +0 -505
package/CHANGELOG.md CHANGED
@@ -8,6 +8,39 @@ All user-facing changes to AgentDesk. Each entry is tagged:
8
8
 
9
9
  Internal refactors, infrastructure changes, and architectural notes are not listed here.
10
10
 
11
+ ## [0.28.1] — 2026-09-17
12
+
13
+ ### Fixed
14
+ - `[CLI]` Jane's delegation to the team ran the delegate in the background by default, leaving its own tool calls with no live turn to get approved — every one was rejected, so a delegated agent could do nothing. Delegation now always runs synchronously.
15
+
16
+ ## [0.28.0] — 2026-09-17
17
+
18
+ ### Changed
19
+ - `[CLI]` Team sessions now run on a new engine built on the Claude Agent SDK. Each phase (INTAKE → PLAN → EXECUTION → REVIEW → SUMMARY) is its own Claude run; Jane leads with a single tool — delegation — and the engineering roles are real subagents with role-scoped tools. Jane can no longer touch code because the tools do not exist in her session. The dashboard's per-agent feed is now driven by real attribution instead of name-matching prose.
20
+ - `[CLI]` REVIEW fans out three independent reviewers (Sam, Bart, Vera) who read the diff themselves rather than the implementer's summary. The verdict is structured and validated; a review that ends without an explicit verdict is treated as *not approved*, never as approval.
21
+ - `[CLI]` Publishing (pushing or opening a PR) is blocked during EXECUTION until Sam's audit has run in that phase. This was previously a prose rule that did not hold.
22
+ - `[CLI]` Session memory (`.agentdesk/session-memory.md`) is now written by the engine from each phase's structured summary, so later phases never start with a missing or malformed handoff. Stale memory from an earlier run is archived instead of being fed into the next session.
23
+ - `[CLI]` Per-phase model overrides continue to apply; REVIEW and SUMMARY still default to Haiku. New project settings: `phaseMaxTurns` (per-phase turn cap) and `maxBudgetUsd` (hard spend cap for a session).
24
+ - `[CLI]` The CLI update check no longer blocks on minor releases, never blocks the daemon, and no longer runs for `--version`/`help`. `AGENTDESK_SKIP_UPDATE_CHECK=1` disables it.
25
+
26
+ ### Removed
27
+ - `[CLI]` `--single-process` / `--no-phased`. Every session runs the phased engine; the flags are accepted and ignored with a note.
28
+
29
+ ### Fixed
30
+ - `[CLI]` `agentdesk init` run from the directory that contains the clone (the parent of `./<repo>`) now uses that clone instead of stopping with "already exists and is not empty". When the directory holds something else, the message now says what it found: a checkout of another repo, or not a git checkout at all.
31
+ - `[CLI]` `agentdesk init` no longer leaves the Jira project / Linear team empty. In existing mode it copied the server's value without asking, and in new mode it only asked when the credential check succeeded, so "Save anyway" skipped the question. With no project bound, create-from-description sessions filed issues in whatever project the token could reach, and the final hint printed the agentdesk project id as if it were the tracker prefix. Init now always resolves the team/project (listing the tracker's projects when credentials verify, asking by hand otherwise), fills the one missing field in on agentdesk.live for existing projects, and the tracker check — in init and before every `agentdesk team` — fails with "Missing Jira project key — run 'agentdesk init'" instead of passing.
32
+ - `[CLI]` Settings pushes to agentdesk.live (init's registration push and the background heal-sync) were silently rejected with "Unknown fields: projectKey", so the server never learned the tracker binding the wizard had picked. The push payload is now reduced to the fields the settings API accepts.
33
+ - `[CLI]` Cancelling a session from the dashboard now actually stops it. Previously the daemon reported "stopped" while Claude kept editing and pushing, and a new session could start on the same working tree.
34
+ - `[CLI]` Sessions that crashed mid-way are no longer reported as complete; they end as a handoff with a resume file. Crash detection had been dead code since the first phase always wrote session memory.
35
+ - `[CLI]` Team sessions no longer die instantly on macOS. The strict sandbox profile was missing read access to the root directory and to the project's ancestor directories, which killed every Claude child process at startup (sessions ended after 0.1s with a misleading rate-limit handoff). The profile now grants those directory nodes plus Claude's own state files, while keeping ~/.ssh, ~/.aws, ~/Documents, and other projects' files blocked.
36
+ - `[CLI]` On macOS, sessions started by a user logged in through the browser reported "Not logged in" inside the sandbox: the strict profile blocked reads of the Keychain where Claude Code keeps its login. The profile now allows that, and also exposes an account profile directory when one is selected.
37
+ - `[CLI]` Running `agentdesk team` from inside an interactive Claude Code session no longer makes the child believe it is nested in that session (which also surfaced as "Not logged in").
38
+ - `[CLI]` `agentdesk init` no longer leaves the GitHub token in the clone's git remote URL, writes `.env` with owner-only permissions, adds `.env` to `.gitignore`, masks secret prompts, verifies a new token before saving it, and recognises repository names containing dots. Existing clones are repaired on the next `init`.
39
+ - `[CLI]` `agentdesk init` now reports a failed project registration instead of printing success, defaults the final confirmation to "no" when an access check failed, and offers to re-enter tracker credentials in existing-project mode.
40
+ - `[CLI]` Two projects mapped to the same tracker team no longer overwrite each other's settings on the server; the second gets a repo-qualified project id.
41
+ - `[CLI]` `agentdesk login` opens the browser on Windows.
42
+ - `[UI]` Saved tracker credentials could not be read back by sessions since late May; they now decrypt again and no data was lost.
43
+
11
44
  ## [0.27.0] — 2026-07-06
12
45
 
13
46
  ### Added
package/bin/agentdesk.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  import { readFileSync } from "fs";
6
6
  import { fileURLToPath } from "url";
7
7
  import { dirname, join } from "path";
8
+ import { classifyUpdate, shouldSkipCheck, updateAction } from "../cli/update-check.mjs";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = dirname(__filename);
@@ -13,37 +14,6 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")
13
14
  const args = process.argv.slice(2);
14
15
  const command = args[0];
15
16
 
16
- // Blocking update check — require update for major/minor bumps, warn for patches
17
- if (command !== "update") {
18
- try {
19
- const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) });
20
- if (res.ok) {
21
- const { version } = await res.json();
22
- if (version !== pkg.version) {
23
- const [curMajor, curMinor] = pkg.version.split(".").map(Number);
24
- const [latMajor, latMinor] = version.split(".").map(Number);
25
- const breaking = latMajor > curMajor || latMinor > curMinor;
26
-
27
- const dim = "\x1b[2m";
28
- const red = "\x1b[31m";
29
- const yellow = "\x1b[33m";
30
- const green = "\x1b[32m";
31
- const cyan = "\x1b[36m";
32
- const reset = "\x1b[0m";
33
-
34
- if (breaking) {
35
- console.log(`\n ${red}Update required:${reset} ${dim}${pkg.version}${reset} → ${green}${version}${reset}`);
36
- console.log(` Run: ${cyan}agentdesk update${reset}\n`);
37
- process.exit(1);
38
- } else {
39
- console.log(`\n ${yellow}Update available:${reset} ${dim}${pkg.version}${reset} → ${green}${version}${reset}`);
40
- console.log(` Run: ${cyan}agentdesk update${reset}\n`);
41
- }
42
- }
43
- }
44
- } catch {}
45
- }
46
-
47
17
  if (command === "--version" || command === "-v" || command === "version") {
48
18
  console.log(pkg.version);
49
19
  process.exit(0);
@@ -80,11 +50,13 @@ if (!command || command === "help" || command === "--help") {
80
50
  Options:
81
51
  --description, -d Task description or requirements
82
52
  --cwd Working directory (defaults to current)
83
- --single-process Run the whole team session in one Claude process on the EXECUTION model.
84
- Default is phased mode (INTAKE → PLAN → EXECUTION → REVIEW → SUMMARY),
85
- which honors per-phase model overrides from project settings.
86
53
  --child-strategy How to handle child tasks: "inline" (default) or "branch"
87
54
 
55
+ Sessions run five phases — INTAKE → PLAN → EXECUTION → REVIEW → SUMMARY — each as
56
+ its own Claude query. Jane leads with a single tool (Agent) and delegates to
57
+ role-scoped subagents; REVIEW fans out independent reviewers who read the diff cold.
58
+ Per-phase model overrides come from project settings (phaseModels).
59
+
88
60
 
89
61
  How it works:
90
62
  With a tracker (Linear, Jira, GitHub Issues):
@@ -96,7 +68,6 @@ if (!command || command === "help" || command === "--help") {
96
68
 
97
69
  Examples:
98
70
  agentdesk team KEN-517
99
- agentdesk team KEN-517 --single-process
100
71
  agentdesk team -d "Fix the checkout total calculation"
101
72
  agentdesk team -d "Add Google OAuth to the login page"
102
73
 
@@ -109,6 +80,30 @@ if (!command || command === "help" || command === "--help") {
109
80
  process.exit(0);
110
81
  }
111
82
 
83
+ // Update check. Runs after version/help so those never need the network, and
84
+ // only a MAJOR release blocks — and never for `daemon`, which runs unattended
85
+ // and must not refuse to start because a new version was published overnight.
86
+ // Policy lives in cli/update-check.mjs; AGENTDESK_SKIP_UPDATE_CHECK=1 disables.
87
+ if (!shouldSkipCheck(command)) {
88
+ try {
89
+ const res = await fetch(`https://registry.npmjs.org/${pkg.name}/latest`, { signal: AbortSignal.timeout(3000) });
90
+ if (res.ok) {
91
+ const { version } = await res.json();
92
+ const { action } = updateAction(command, classifyUpdate(pkg.version, version));
93
+ if (action !== "none") {
94
+ const dim = "\x1b[2m", red = "\x1b[31m", yellow = "\x1b[33m", green = "\x1b[32m", cyan = "\x1b[36m", reset = "\x1b[0m";
95
+ if (action === "block") {
96
+ console.log(`\n ${red}Update required:${reset} ${dim}${pkg.version}${reset} → ${green}${version}${reset} (major release)`);
97
+ console.log(` Run: ${cyan}agentdesk update${reset}\n`);
98
+ process.exit(1);
99
+ }
100
+ console.log(`\n ${yellow}Update available:${reset} ${dim}${pkg.version}${reset} → ${green}${version}${reset}`);
101
+ console.log(` Run: ${cyan}agentdesk update${reset}\n`);
102
+ }
103
+ }
104
+ } catch {}
105
+ }
106
+
112
107
  // Single-agent mode — agentdesk jane -d "...", agentdesk dennis TASK-123, etc.
113
108
  const AGENT_NAMES = ["jane", "dennis", "sam", "bart", "vera", "luna", "mark", "nora"];
114
109
  if (AGENT_NAMES.includes(command?.toLowerCase())) {
@@ -165,15 +160,12 @@ else if (command === "bootstrap") {
165
160
 
166
161
  else if (command === "team") {
167
162
  // Parse options first to find -d, --cwd, --child-strategy.
168
- // `phased` is the default each phase spawns its own Claude process so
169
- // per-phase model overrides from .agentdesk.json are honored. Pass
170
- // --single-process (alias --no-phased) to fall back to the legacy
171
- // single-process mode that runs everything on the EXECUTION model.
163
+ // Every team session runs the phased engine (INTAKE PLAN EXECUTION
164
+ // REVIEW SUMMARY) with real subagents; the old single-process mode is gone.
172
165
  let description = "";
173
166
  let cwd = process.cwd();
174
167
  let taskId = null;
175
168
  let childStrategy = null;
176
- let phased = true;
177
169
  const remaining = args.slice(1);
178
170
 
179
171
  for (let i = 0; i < remaining.length; i++) {
@@ -183,10 +175,8 @@ else if (command === "team") {
183
175
  cwd = remaining[++i];
184
176
  } else if (remaining[i] === "--child-strategy" && remaining[i + 1]) {
185
177
  childStrategy = remaining[++i];
186
- } else if (remaining[i] === "--phased") {
187
- phased = true;
188
- } else if (remaining[i] === "--single-process" || remaining[i] === "--no-phased") {
189
- phased = false;
178
+ } else if (remaining[i] === "--phased" || remaining[i] === "--single-process" || remaining[i] === "--no-phased") {
179
+ console.error(` Note: ${remaining[i]} is no longer needed — sessions always run the phased engine.`);
190
180
  } else if (!taskId && !remaining[i].startsWith("-")) {
191
181
  taskId = remaining[i];
192
182
  }
@@ -199,7 +189,7 @@ else if (command === "team") {
199
189
  }
200
190
 
201
191
  const { runTeam } = await import("../cli/team.mjs");
202
- const code = await runTeam(taskId, { description, cwd, childStrategy, phased });
192
+ const code = await runTeam(taskId, { description, cwd, childStrategy });
203
193
  process.exit(code);
204
194
  }
205
195
 
package/cli/agents.mjs CHANGED
@@ -5,7 +5,7 @@ export const BUILT_IN_AGENTS = {
5
5
  badge: "●● JANE ●●",
6
6
  role: "Product Analyst / Team Lead",
7
7
  description: "leads the session, clarifies requirements, coordinates the team, dictates tracker comments (Dennis executes), decomposes large tasks into subtasks",
8
- groundRules: "Jane is strictly non-technical and NEVER calls tools. She MUST NEVER: call Read, Write, Edit, Bash, Grep, Glob, or any other tool; read or reference code; mention file names, paths, function names, variable names, class names, imports, code snippets, terminal commands, error messages, or stack traces; write or modify code; execute tracker API calls herself. Her language is product-only: user stories, acceptance criteria, scope, priorities, stakeholder impact. When she needs technical information she asks Dennis or Sam (\"Dennis, can you check...\"). When tracker comments or status transitions are needed, Jane DICTATES the exact text and Dennis runs the command. If you are about to call a tool while the current speaker is Jane, STOP and switch to Dennis instead.",
8
+ groundRules: "Jane is strictly non-technical and NEVER calls tools. She MUST NEVER: call Read, Write, Edit, Bash, Grep, Glob, or any other tool; read or reference code; mention file names, paths, function names, variable names, class names, imports, code snippets, terminal commands, error messages, or stack traces; write or modify code; execute tracker API calls herself. Her language is product-only: user stories, acceptance criteria, scope, priorities, stakeholder impact. During INTAKE she evaluates task complexity and decomposes large features into subtasks (basic vs deferred). When she needs technical information she asks Dennis or Sam (\"Dennis, can you check...\"). When tracker comments or status transitions are needed, Jane DICTATES the exact text and Dennis runs the command. If you are about to call a tool while the current speaker is Jane, STOP and switch to Dennis instead.",
9
9
  planning: "Requirements: what we're building, acceptance criteria, scope. Flags UI tasks for Luna. (No code, no files, no tool calls.)",
10
10
  execution: {
11
11
  step: "Jane wraps up (dictates, Dennis executes)",
@@ -184,6 +184,7 @@ export function resolveTeam(config) {
184
184
  badge: `** ${custom.name.toUpperCase()} **`,
185
185
  role: custom.role || "Team Member",
186
186
  description: custom.role || "Custom team member",
187
+ tools: Array.isArray(custom.tools) ? custom.tools : undefined,
187
188
  groundRules: custom.when ? `${custom.name} is invoked: ${custom.when}. How to use: ${custom.how || "as needed"}.` : "",
188
189
  planning: `${custom.role || custom.name} Review`,
189
190
  execution: custom.when ? {
@@ -212,6 +213,8 @@ export function resolveTeam(config) {
212
213
  badge: entry.badge || `** ${entry.name.toUpperCase()} **`,
213
214
  role: entry.role || "Team Member",
214
215
  description: entry.description || entry.role || "Custom team member",
216
+ // Engine: explicit tool list for this agent's subagent definition.
217
+ tools: Array.isArray(entry.tools) ? entry.tools : undefined,
215
218
  groundRules: entry.groundRules || "",
216
219
  planning: entry.planning || `${entry.role || entry.name} Review`,
217
220
  execution: entry.execution || null,
@@ -235,258 +238,3 @@ export function resolveTeam(config) {
235
238
 
236
239
  return team;
237
240
  }
238
-
239
- /**
240
- * Return tracker-specific command snippets for execution steps.
241
- */
242
- function trackerCommands(tracker, config) {
243
- if (!tracker) return null;
244
-
245
- if (tracker === "linear") {
246
- return {
247
- postPrLink: [
248
- 'Post PR link on Linear using attachmentCreate:',
249
- '```',
250
- 'curl -s -X POST https://api.linear.app/graphql \\',
251
- ' -H "Authorization: $LINEAR_API_KEY" -H "Content-Type: application/json" \\',
252
- ' -d \'{"query":"mutation { attachmentCreate(input: { issueId: \\"$ISSUE_ID\\", title: \\"Pull Request\\", url: \\"$PR_URL\\" }) { success } }"}\'',
253
- '```',
254
- ].join('\n'),
255
- attachScreenshots: [
256
- 'Upload each screenshot via Linear fileUpload mutation, then post a comment with the image URLs:',
257
- '```bash',
258
- 'for f in screenshots/*.png; do',
259
- ' UPLOAD=$(curl -s -X POST https://api.linear.app/graphql \\',
260
- ' -H "Authorization: $LINEAR_API_KEY" -H "Content-Type: application/json" \\',
261
- ' -d \'{"query":"mutation { fileUpload(contentType: \\"image/png\\", filename: \\"\'$(basename "$f")\'\\", size: \'$(stat -f%z "$f")\') { uploadFile { uploadUrl assetUrl headers { key value } } } }"}\')',
262
- ' # Extract uploadUrl, assetUrl, headers from $UPLOAD — PUT the file to uploadUrl, then collect assetUrl',
263
- 'done',
264
- '# Post a comment with all image URLs as markdown images',
265
- '```',
266
- ].join('\n'),
267
- agentComment: [
268
- 'Post a comment on Linear with key findings:',
269
- '```',
270
- 'curl -s -X POST https://api.linear.app/graphql \\',
271
- ' -H "Authorization: $LINEAR_API_KEY" -H "Content-Type: application/json" \\',
272
- ' -d \'{"query":"mutation { commentCreate(input: { issueId: \\"$ISSUE_ID\\", body: \\"<COMMENT>\\" }) { success } }"}\'',
273
- '```',
274
- ].join('\n'),
275
- transitionToReview: [
276
- 'Transition to "In Review" on Linear:',
277
- '```',
278
- 'curl -s -X POST https://api.linear.app/graphql \\',
279
- ' -H "Authorization: $LINEAR_API_KEY" -H "Content-Type: application/json" \\',
280
- ' -d \'{"query":"mutation { issueUpdate(id: \\"$ISSUE_ID\\", input: { stateId: \\"$IN_REVIEW_STATE_ID\\" }) { success } }"}\'',
281
- '```',
282
- 'Find the "In Review" state ID from workflowStates first.',
283
- ].join('\n'),
284
- postSummary: [
285
- 'Post final summary comment on Linear (use commentCreate mutation above).',
286
- ].join('\n'),
287
- };
288
- }
289
-
290
- const baseUrl = config?.jira?.baseUrl || '$JIRA_BASE_URL';
291
- if (tracker === "jira") {
292
- return {
293
- postPrLink: [
294
- 'Post PR link on Jira as a remote link:',
295
- '```bash',
296
- `curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H "Content-Type: application/json" \\`,
297
- ` "${baseUrl}/rest/api/3/issue/$TASK_ID/remotelink" \\`,
298
- ` -d '{"object":{"url":"$PR_URL","title":"Pull Request"}}'`,
299
- '```',
300
- ].join('\n'),
301
- attachScreenshots: [
302
- 'Upload each screenshot as a Jira attachment:',
303
- '```bash',
304
- `for f in screenshots/*.png; do`,
305
- ` curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" \\`,
306
- ` -H "X-Atlassian-Token: no-check" \\`,
307
- ` -F "file=@$f" \\`,
308
- ` "${baseUrl}/rest/api/3/issue/$TASK_ID/attachments"`,
309
- `done`,
310
- '```',
311
- ].join('\n'),
312
- agentComment: [
313
- 'Post a comment on Jira with key findings. IMPORTANT: Any URLs in the comment MUST use ADF inlineCard nodes so they render as clickable links:',
314
- '```bash',
315
- `curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H "Content-Type: application/json" \\`,
316
- ` "${baseUrl}/rest/api/3/issue/$TASK_ID/comment" \\`,
317
- ` -d '<ADF_BODY>'`,
318
- '```',
319
- 'For plain text: `{"type":"text","text":"..."}`',
320
- 'For clickable URLs: `{"type":"inlineCard","attrs":{"url":"https://..."}}`',
321
- 'Example body with a link:',
322
- '```json',
323
- '{"body":{"type":"doc","version":1,"content":[{"type":"paragraph","content":[{"type":"text","text":"Session: "},{"type":"inlineCard","attrs":{"url":"https://agentdesk.live/sessions/..."}}]}]}}',
324
- '```',
325
- ].join('\n'),
326
- transitionToReview: [
327
- 'Transition task to "In Review" on Jira:',
328
- '```bash',
329
- `# First get available transitions:`,
330
- `curl -s -u "$JIRA_EMAIL:$JIRA_API_TOKEN" "${baseUrl}/rest/api/3/issue/$TASK_ID/transitions"`,
331
- `# Then transition (find the "In Review" transition ID):`,
332
- `curl -s -X POST -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H "Content-Type: application/json" \\`,
333
- ` "${baseUrl}/rest/api/3/issue/$TASK_ID/transitions" \\`,
334
- ` -d '{"transition":{"id":"<TRANSITION_ID>"}}'`,
335
- '```',
336
- ].join('\n'),
337
- postSummary: [
338
- `Post final summary comment on Jira (use comment endpoint above).`,
339
- ].join('\n'),
340
- };
341
- }
342
-
343
- if (tracker === "github") {
344
- return {
345
- postPrLink: [
346
- 'Reference the issue in the PR body ("Closes #$TASK_ID") and post a comment:',
347
- '```bash',
348
- 'gh issue comment $TASK_ID --body "PR created: $PR_URL"',
349
- '```',
350
- ].join('\n'),
351
- attachScreenshots: [
352
- 'Upload screenshots and post them in a GitHub issue comment:',
353
- '```bash',
354
- 'BODY=""',
355
- 'for f in screenshots/*.png; do',
356
- ' BASE64=$(base64 < "$f")',
357
- ' BODY="$BODY![$(basename $f)](data:image/png;base64,$BASE64)\\n"',
358
- 'done',
359
- 'gh issue comment $TASK_ID --body "$BODY"',
360
- '```',
361
- ].join('\n'),
362
- agentComment: [
363
- 'Post a comment on GitHub with key findings:',
364
- '```bash',
365
- 'gh issue comment $TASK_ID --body "<COMMENT>"',
366
- '```',
367
- ].join('\n'),
368
- transitionToReview: [
369
- 'Add "in review" label on GitHub:',
370
- '```bash',
371
- 'gh issue edit $TASK_ID --remove-label "in progress" --add-label "in review" 2>/dev/null || true',
372
- '```',
373
- ].join('\n'),
374
- postSummary: [
375
- 'Post final summary comment on GitHub:',
376
- '```bash',
377
- 'gh issue comment $TASK_ID --body "<SUMMARY>"',
378
- '```',
379
- ].join('\n'),
380
- };
381
- }
382
-
383
- return null;
384
- }
385
-
386
- /**
387
- * Generate the dynamic parts of the prompt from the team.
388
- * @param {Array} team - resolved team array
389
- * @param {Object} opts - optional { tracker, config } for tracker-specific execution steps
390
- */
391
- export function generateTeamPrompt(team, opts = {}) {
392
- const count = team.length;
393
- const agentWord = count === 1 ? "one AI agent" : `${count} AI agents`;
394
-
395
- const sections = {};
396
-
397
- // Agent list
398
- sections.agentList = team.map(a =>
399
- `- ${a.name} (${a.role}) — ${a.description}`
400
- ).join("\n");
401
-
402
- // Speaking order
403
- sections.speakingOrder = team.map((a, i) =>
404
- `${i + 1}. ${a.badge}`
405
- ).join("\n");
406
-
407
- // Ground rules
408
- const rules = [];
409
- rules.push(`1. Each agent speaks prefixed with their badge (e.g., "${team[0].badge} ...").`);
410
- rules.push(`2. ALL text output MUST be prefixed with the acting agent's badge.`);
411
- rules.push(`3. Raise concerns when you see problems — don't rubber-stamp.`);
412
- rules.push(`4. **No announcement without observation.** Before any agent claims "done", "deployed", "fixed", "passes", "works", "ready", "now shows X", or "should work" — run the check that would falsify it: execute the command and read the output, fetch the live endpoint and grep the response, load the page and inspect the DOM, verify the deployed artifact contains the new code. "The code looks right" / "the CI is green" / "the DB has the row" are intermediate signals, NOT proof of user-visible outcome. If the observation is out of reach (auth, browser state, external system), say so plainly and ask the user to confirm — don't paper over with confident language. If a fix didn't land on the first try, stop and re-diagnose from scratch: repeated near-misses mean the hypothesis is wrong, not that one more patch will do it.`);
413
- let ruleNum = 5;
414
- for (const a of team) {
415
- if (a.groundRules) {
416
- rules.push(`${ruleNum}. ${a.groundRules}`);
417
- ruleNum++;
418
- }
419
- }
420
- sections.groundRules = rules.join("\n");
421
-
422
- // Code principles
423
- const principles = team
424
- .filter(a => a.codePrinciple)
425
- .map(a => `- ${a.name}: ${a.codePrinciple}`);
426
- sections.codePrinciples = principles.length > 0 ? principles.join("\n") : "";
427
-
428
- // Planning presentations
429
- sections.planningOrder = team.map(a =>
430
- `${a.badge} ${a.planning}`
431
- ).join("\n");
432
-
433
- // Execution steps — inject tracker-specific commands
434
- const cmds = trackerCommands(opts.tracker, opts.config);
435
- const execAgents = team
436
- .filter(a => a.execution)
437
- .sort((a, b) => a.execution.order - b.execution.order);
438
- let stepNum = 1;
439
- const execSteps = [];
440
- for (const a of execAgents) {
441
- let tasks = [...a.execution.tasks];
442
-
443
- if (cmds) {
444
- // Bart: expand PR link + tracker comment tasks with actual commands
445
- if (a.name === "Bart") {
446
- tasks = tasks.map(t => {
447
- if (/PR link/i.test(t) || /Post PR.*tracker/i.test(t)) {
448
- return `Push and create PR.\n ${cmds.postPrLink}`;
449
- }
450
- if (/screenshot.*tracker/i.test(t)) {
451
- return `Upload screenshots to tracker, then post a comment.\n ${cmds.attachScreenshots}\n ${cmds.agentComment.replace('<COMMENT>', 'Screenshots attached — see attachments above.')}`;
452
- }
453
- return t;
454
- });
455
- }
456
-
457
- // Jane: expand transition + summary tasks
458
- if (a.name === "Jane") {
459
- tasks = tasks.map(t => {
460
- if (/transition.*in review/i.test(t) || /move.*in review/i.test(t)) {
461
- return `Transition task to "In Review".\n ${cmds.transitionToReview}`;
462
- }
463
- if (/summary.*tracker/i.test(t) || /final summary/i.test(t)) {
464
- return `Post final summary comment on tracker.\n ${cmds.postSummary}`;
465
- }
466
- return t;
467
- });
468
- }
469
-
470
- // Dennis & Sam: expand tracker comment
471
- if (a.name === "Dennis" || a.name === "Sam") {
472
- const hasTrackerTask = tasks.some(t => /tracker/i.test(t) || /comment/i.test(t));
473
- if (!hasTrackerTask) {
474
- const findingsLabel = a.name === "Dennis" ? "Files changed, technical decisions" : "Architecture concerns or clean audit";
475
- tasks.push(`Post comment on tracker with key findings (${findingsLabel}).\n ${cmds.agentComment}`);
476
- }
477
- }
478
- }
479
-
480
- const taskList = tasks.map((t, i) => `${i + 1}. ${t}`).join("\n");
481
- execSteps.push(`### Step ${stepNum} — ${a.name} ${a.execution.step.replace(/^[A-Za-z]+ /, "")}:\n${taskList}`);
482
- stepNum++;
483
- }
484
- sections.executionSteps = execSteps.join("\n\n");
485
-
486
- // Count and phrasing
487
- sections.count = count;
488
- sections.agentWord = agentWord;
489
- sections.names = team.map(a => a.name);
490
-
491
- return sections;
492
- }
package/cli/bootstrap.mjs CHANGED
@@ -10,24 +10,30 @@
10
10
  // This module also exports reusable building blocks so `agentdesk init` can
11
11
  // short-circuit to the bootstrap flow when it detects a fresh clone.
12
12
 
13
- import { existsSync, readFileSync, writeFileSync } from "fs";
13
+ import { writeFileSync } from "fs";
14
14
  import { join } from "path";
15
- import { execSync } from "child_process";
16
15
  import { loadConfig } from "./config.mjs";
17
16
  import { getStoredApiKey } from "./login.mjs";
18
17
  import { checkTrackerPermissions, resolveCredentialsFromEnv } from "./tracker-check.mjs";
19
- import { assertPushable, PreflightError } from "./session-preflight.mjs";
20
- import { select, promptRequired } from "./prompts.mjs";
18
+ import { assertPushable, PreflightError, getGitRemoteUrl } from "./session-preflight.mjs";
19
+ import { select, promptRequired, promptSecret } from "./prompts.mjs";
20
+ import { loadDotEnv, saveEnvVar } from "./dotenv.mjs";
21
+ import {
22
+ fetchGitHubLogin,
23
+ parseGitHubRef,
24
+ printAccessChecks,
25
+ readLocalConfig,
26
+ runAccessChecks,
27
+ } from "./setup-helpers.mjs";
21
28
 
22
29
  const SERVER = process.env.AGENTDESK_SERVER || "https://agentdesk.live";
23
30
 
31
+ // Shares parseGitHubRef with init so both entry points agree on what counts
32
+ // as a GitHub remote (the private regex here rejected repo names with dots).
24
33
  function detectGitRemote(dir) {
25
- try {
26
- const url = execSync("git remote get-url origin", { cwd: dir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
27
- const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/);
28
- if (m) return `${m[1]}/${m[2]}`;
29
- } catch {}
30
- return null;
34
+ const url = getGitRemoteUrl(dir);
35
+ const parsed = url ? parseGitHubRef(url) : null;
36
+ return parsed ? `${parsed.owner}/${parsed.repo}` : null;
31
37
  }
32
38
 
33
39
  async function fetchProjects(apiKey) {
@@ -162,37 +168,6 @@ export function writeProjectConfig(cwd, projectKey) {
162
168
  writeFileSync(join(cwd, ".agentdesk.json"), JSON.stringify({ projectKey }, null, 2) + "\n");
163
169
  }
164
170
 
165
- function saveEnvVar(dir, key, value) {
166
- const envPath = join(dir, ".env");
167
- let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
168
- const re = new RegExp(`^${key}=.*$`, "m");
169
- if (re.test(content)) {
170
- content = content.replace(re, `${key}=${value}`);
171
- } else {
172
- content += `${content && !content.endsWith("\n") ? "\n" : ""}${key}=${value}\n`;
173
- }
174
- writeFileSync(envPath, content);
175
- }
176
-
177
- function loadDotEnvLocal(dir) {
178
- const envPath = join(dir, ".env");
179
- const out = {};
180
- if (!existsSync(envPath)) return out;
181
- for (const line of readFileSync(envPath, "utf-8").split("\n")) {
182
- const trimmed = line.trim();
183
- if (!trimmed || trimmed.startsWith("#")) continue;
184
- const eq = trimmed.indexOf("=");
185
- if (eq !== -1) out[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
186
- }
187
- return out;
188
- }
189
-
190
- function readLocalConfig(cwd) {
191
- const path = join(cwd, ".agentdesk.json");
192
- if (!existsSync(path)) return null;
193
- try { return JSON.parse(readFileSync(path, "utf-8")); } catch { return null; }
194
- }
195
-
196
171
  // Compact step list — the init wizard carries the full narrative; bootstrap
197
172
  // assumes the user has already seen it on their first machine.
198
173
  function printTrackerHint(tracker) {
@@ -211,7 +186,7 @@ async function promptTrackerCreds(cwd, tracker, existing) {
211
186
  if (!creds.LINEAR_API_KEY) {
212
187
  console.log("");
213
188
  printTrackerHint("linear");
214
- const v = await promptRequired("Linear API key");
189
+ const v = await promptSecret("Linear API key");
215
190
  saveEnvVar(cwd, "LINEAR_API_KEY", v);
216
191
  creds.LINEAR_API_KEY = v;
217
192
  console.log(" ✓ Saved LINEAR_API_KEY to .env");
@@ -227,7 +202,7 @@ async function promptTrackerCreds(cwd, tracker, existing) {
227
202
  }
228
203
  if (!creds.JIRA_API_TOKEN) {
229
204
  if (creds.JIRA_EMAIL) console.log("");
230
- const v = await promptRequired("Jira API token");
205
+ const v = await promptSecret("Jira API token");
231
206
  saveEnvVar(cwd, "JIRA_API_TOKEN", v);
232
207
  creds.JIRA_API_TOKEN = v;
233
208
  console.log(" ✓ Saved JIRA_API_TOKEN to .env");
@@ -239,7 +214,7 @@ async function promptTrackerCreds(cwd, tracker, existing) {
239
214
  async function promptGitHubToken(cwd) {
240
215
  console.log("");
241
216
  printTrackerHint("github");
242
- const v = await promptRequired("GitHub token");
217
+ const v = await promptSecret("GitHub token");
243
218
  saveEnvVar(cwd, "GITHUB_TOKEN", v);
244
219
  console.log(" ✓ Saved GITHUB_TOKEN to .env");
245
220
  return v;
@@ -250,7 +225,9 @@ async function promptGitHubToken(cwd) {
250
225
  // are missing or broken, prompt for them, validate. Writes to `.env` only.
251
226
  // Returns { trackerOk, githubOk } for the caller to surface.
252
227
  export async function gapScan({ cwd, apiKey, projectKey }) {
253
- const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true });
228
+ // readOnly: bootstrap's contract is "writes to .env only". loadConfig's
229
+ // default behaviour rewrote .agentdesk.json and pushed to the server.
230
+ const config = await loadConfig(cwd, { apiKey, serverUrl: SERVER, projectName: projectKey, silent: true, readOnly: true });
254
231
  const tracker = config.tracker || null;
255
232
  const repo = config.github?.repo || null;
256
233
  const login = config.github?.login || null;
@@ -262,7 +239,7 @@ export async function gapScan({ cwd, apiKey, projectKey }) {
262
239
  if (repo) console.log(` Repo: ${repo}${login ? ` (@${login})` : ""}`);
263
240
  console.log("");
264
241
 
265
- let creds = resolveCredentialsFromEnv(loadDotEnvLocal(cwd));
242
+ let creds = resolveCredentialsFromEnv(loadDotEnv(cwd));
266
243
  let trackerOk = !tracker;
267
244
  let githubOk = false;
268
245
  let attempts = 0;
@@ -309,16 +286,22 @@ export async function gapScan({ cwd, apiKey, projectKey }) {
309
286
  if (!creds.GITHUB_TOKEN) creds.GITHUB_TOKEN = await promptGitHubToken(cwd);
310
287
  try {
311
288
  assertPushable({ cwd, creds: { GITHUB_TOKEN: creds.GITHUB_TOKEN }, projectName: projectKey });
312
- try {
313
- const env = { ...process.env, GH_TOKEN: creds.GITHUB_TOKEN };
314
- const who = execSync("gh api user --jq .login", { env, stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" }).trim();
315
- if (who && login && who.toLowerCase() !== login.toLowerCase()) {
316
- console.log(` ⚠ GitHub token authenticates as @${who}, not @${login} (from .agentdesk.json). Agents will post as @${who}.`);
317
- } else if (who) {
318
- console.log(` ✓ GitHub verified as @${who}`);
319
- }
320
- } catch {
321
- console.log(" ✓ GITHUB_TOKEN present (couldn't verify via gh — install gh CLI for identity check)");
289
+ // REST call, same as init. This used to shell out to `gh`, so a
290
+ // machine without the gh CLI got no identity check at all, and an
291
+ // invalid token was printed as " present" and accepted. An
292
+ // unverifiable token now goes through the same retry/skip prompt as a
293
+ // missing one.
294
+ const who = await fetchGitHubLogin(creds.GITHUB_TOKEN);
295
+ if (!who) {
296
+ throw new PreflightError(
297
+ "GITHUB_TOKEN did not authenticate with GitHub (invalid token, or offline)",
298
+ { code: "INVALID_GITHUB_TOKEN" }
299
+ );
300
+ }
301
+ if (login && who.toLowerCase() !== login.toLowerCase()) {
302
+ console.log(` ⚠ GitHub token authenticates as @${who}, not @${login} (from .agentdesk.json). Agents will post as @${who}.`);
303
+ } else {
304
+ console.log(` ✓ GitHub verified as @${who}`);
322
305
  }
323
306
  githubOk = true;
324
307
  } catch (err) {
@@ -363,7 +346,7 @@ export async function runBootstrap(cwd = process.cwd()) {
363
346
  process.exit(1);
364
347
  }
365
348
 
366
- let localConfig = readLocalConfig(cwd);
349
+ const localConfig = readLocalConfig(cwd);
367
350
  let projectKey = localConfig?.projectKey || null;
368
351
 
369
352
  if (!projectKey) {
@@ -383,8 +366,6 @@ export async function runBootstrap(cwd = process.cwd()) {
383
366
  // Final access-checks summary — same checklist the new-project wizard
384
367
  // prints at the end of its own flow, so both entry points finish
385
368
  // consistently.
386
- const { loadDotEnv } = await import("./setup-helpers.mjs");
387
- const { runAccessChecks, printAccessChecks } = await import("./setup-helpers.mjs");
388
369
  const creds = loadDotEnv(cwd);
389
370
  const results = await runAccessChecks({ apiKey, serverUrl: SERVER, cwd, config, creds });
390
371
  printAccessChecks(results, { tracker: config?.tracker });