@developerz.ai/aitm 0.0.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/agent-config/agent-config-detector.d.ts +15 -0
  4. package/dist/agent-config/agent-config-detector.js +56 -0
  5. package/dist/agent-config/agent-config-detector.js.map +1 -0
  6. package/dist/cli/args.d.ts +37 -0
  7. package/dist/cli/args.js +238 -0
  8. package/dist/cli/args.js.map +1 -0
  9. package/dist/cli/cli.d.ts +15 -0
  10. package/dist/cli/cli.js +113 -0
  11. package/dist/cli/cli.js.map +1 -0
  12. package/dist/cli/commands.d.ts +83 -0
  13. package/dist/cli/commands.js +521 -0
  14. package/dist/cli/commands.js.map +1 -0
  15. package/dist/compaction/compactor.d.ts +20 -0
  16. package/dist/compaction/compactor.js +75 -0
  17. package/dist/compaction/compactor.js.map +1 -0
  18. package/dist/config/config-loader.d.ts +25 -0
  19. package/dist/config/config-loader.js +275 -0
  20. package/dist/config/config-loader.js.map +1 -0
  21. package/dist/config/config-writer.d.ts +14 -0
  22. package/dist/config/config-writer.js +178 -0
  23. package/dist/config/config-writer.js.map +1 -0
  24. package/dist/config/schema.d.ts +85 -0
  25. package/dist/config/schema.js +38 -0
  26. package/dist/config/schema.js.map +1 -0
  27. package/dist/credentials/credentials.d.ts +15 -0
  28. package/dist/credentials/credentials.js +58 -0
  29. package/dist/credentials/credentials.js.map +1 -0
  30. package/dist/credentials/defaults.d.ts +2 -0
  31. package/dist/credentials/defaults.js +21 -0
  32. package/dist/credentials/defaults.js.map +1 -0
  33. package/dist/fs/atomic-write.d.ts +1 -0
  34. package/dist/fs/atomic-write.js +27 -0
  35. package/dist/fs/atomic-write.js.map +1 -0
  36. package/dist/github/errors.d.ts +18 -0
  37. package/dist/github/errors.js +20 -0
  38. package/dist/github/errors.js.map +1 -0
  39. package/dist/github/github-client.d.ts +47 -0
  40. package/dist/github/github-client.js +417 -0
  41. package/dist/github/github-client.js.map +1 -0
  42. package/dist/github/schema.d.ts +44 -0
  43. package/dist/github/schema.js +23 -0
  44. package/dist/github/schema.js.map +1 -0
  45. package/dist/index.d.ts +26 -0
  46. package/dist/index.js +22 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/logger/logger.d.ts +36 -0
  49. package/dist/logger/logger.js +123 -0
  50. package/dist/logger/logger.js.map +1 -0
  51. package/dist/loop/run-loop-adapter.d.ts +46 -0
  52. package/dist/loop/run-loop-adapter.js +270 -0
  53. package/dist/loop/run-loop-adapter.js.map +1 -0
  54. package/dist/loop/take-over-flow.d.ts +57 -0
  55. package/dist/loop/take-over-flow.js +183 -0
  56. package/dist/loop/take-over-flow.js.map +1 -0
  57. package/dist/loop/work-loop.d.ts +95 -0
  58. package/dist/loop/work-loop.js +211 -0
  59. package/dist/loop/work-loop.js.map +1 -0
  60. package/dist/mcp/mcp-client.d.ts +27 -0
  61. package/dist/mcp/mcp-client.js +123 -0
  62. package/dist/mcp/mcp-client.js.map +1 -0
  63. package/dist/mcp/schema.d.ts +53 -0
  64. package/dist/mcp/schema.js +39 -0
  65. package/dist/mcp/schema.js.map +1 -0
  66. package/dist/openrouter/client.d.ts +28 -0
  67. package/dist/openrouter/client.js +40 -0
  68. package/dist/openrouter/client.js.map +1 -0
  69. package/dist/openrouter/model-limits.d.ts +21 -0
  70. package/dist/openrouter/model-limits.js +39 -0
  71. package/dist/openrouter/model-limits.js.map +1 -0
  72. package/dist/openrouter/server-tools.d.ts +35 -0
  73. package/dist/openrouter/server-tools.js +25 -0
  74. package/dist/openrouter/server-tools.js.map +1 -0
  75. package/dist/orchestrator/orchestrator.d.ts +60 -0
  76. package/dist/orchestrator/orchestrator.js +180 -0
  77. package/dist/orchestrator/orchestrator.js.map +1 -0
  78. package/dist/orchestrator/subagent-tools.d.ts +44 -0
  79. package/dist/orchestrator/subagent-tools.js +133 -0
  80. package/dist/orchestrator/subagent-tools.js.map +1 -0
  81. package/dist/orchestrator/system-prompts.d.ts +4 -0
  82. package/dist/orchestrator/system-prompts.js +78 -0
  83. package/dist/orchestrator/system-prompts.js.map +1 -0
  84. package/dist/plan/plan-graph.d.ts +11 -0
  85. package/dist/plan/plan-graph.js +69 -0
  86. package/dist/plan/plan-graph.js.map +1 -0
  87. package/dist/plan/schema.d.ts +30 -0
  88. package/dist/plan/schema.js +24 -0
  89. package/dist/plan/schema.js.map +1 -0
  90. package/dist/state/schema.d.ts +88 -0
  91. package/dist/state/schema.js +53 -0
  92. package/dist/state/schema.js.map +1 -0
  93. package/dist/state/state-store.d.ts +16 -0
  94. package/dist/state/state-store.js +129 -0
  95. package/dist/state/state-store.js.map +1 -0
  96. package/dist/subagents/factory.d.ts +8 -0
  97. package/dist/subagents/factory.js +10 -0
  98. package/dist/subagents/factory.js.map +1 -0
  99. package/dist/subagents/planner.d.ts +31 -0
  100. package/dist/subagents/planner.js +83 -0
  101. package/dist/subagents/planner.js.map +1 -0
  102. package/dist/subagents/reviewer.d.ts +60 -0
  103. package/dist/subagents/reviewer.js +159 -0
  104. package/dist/subagents/reviewer.js.map +1 -0
  105. package/dist/subagents/worker.d.ts +71 -0
  106. package/dist/subagents/worker.js +180 -0
  107. package/dist/subagents/worker.js.map +1 -0
  108. package/dist/testing/temp-repo.d.ts +7 -0
  109. package/dist/testing/temp-repo.js +21 -0
  110. package/dist/testing/temp-repo.js.map +1 -0
  111. package/dist/tools/datetime.d.ts +12 -0
  112. package/dist/tools/datetime.js +42 -0
  113. package/dist/tools/datetime.js.map +1 -0
  114. package/dist/tools/fetch-html.d.ts +32 -0
  115. package/dist/tools/fetch-html.js +139 -0
  116. package/dist/tools/fetch-html.js.map +1 -0
  117. package/dist/tools/github-thread-tool.d.ts +10 -0
  118. package/dist/tools/github-thread-tool.js +36 -0
  119. package/dist/tools/github-thread-tool.js.map +1 -0
  120. package/dist/tools/web-fetch.d.ts +31 -0
  121. package/dist/tools/web-fetch.js +223 -0
  122. package/dist/tools/web-fetch.js.map +1 -0
  123. package/dist/workspace/worktree-pool.d.ts +21 -0
  124. package/dist/workspace/worktree-pool.js +104 -0
  125. package/dist/workspace/worktree-pool.js.map +1 -0
  126. package/package.json +50 -0
@@ -0,0 +1,58 @@
1
+ // docs/auth.md, docs/runtime.md, docs/config.md
2
+ // Maps subagent role → capability tier → OpenRouter model handle.
3
+ // Never reads config files or env directly — that's ConfigLoader's job.
4
+ // SDK reference: docs/vendor/ai-sdk/chunk-09.md §"Subagents", chunk-04.md §"ToolLoopAgent".
5
+ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
6
+ import { DEFAULT_MODELS } from "./defaults.js";
7
+ export const ROLE_CAPABILITY = {
8
+ planner: 'smart',
9
+ worker: 'coding',
10
+ reviewer: 'smart',
11
+ orchestrator: 'fast',
12
+ };
13
+ export class Credentials {
14
+ resolved;
15
+ // Lazy: provider creation also asserts the API key is present, so callers that
16
+ // only inspect role/capability mapping (tests, dry-run) don't need a real key.
17
+ providerInstance;
18
+ constructor(resolved) {
19
+ this.resolved = resolved;
20
+ }
21
+ // Build a handle per role using ROLE_CAPABILITY. Capability fallback chain:
22
+ // models[capability] → models.generic → built-in default.
23
+ handles() {
24
+ return {
25
+ planner: this.modelFor('planner'),
26
+ worker: this.modelFor('worker'),
27
+ reviewer: this.modelFor('reviewer'),
28
+ orchestrator: this.modelFor('orchestrator'),
29
+ };
30
+ }
31
+ modelFor(role) {
32
+ return this.modelForCapability(ROLE_CAPABILITY[role]);
33
+ }
34
+ modelForCapability(capability) {
35
+ const modelId = this.resolved.models[capability] ||
36
+ this.resolved.models.generic ||
37
+ DEFAULT_MODELS[capability];
38
+ // Subagents lean on structured output (Output.object). OpenRouter may route a model to a
39
+ // provider — notably Amazon Bedrock — that rejects the AI SDK's structured-output request
40
+ // (`output_config.format`), failing the Planner/Worker/Reviewer at random. Skip Bedrock so
41
+ // those calls land on a provider that accepts the parameter.
42
+ return this.provider().chat(modelId, { provider: { ignore: ['amazon-bedrock'] } });
43
+ }
44
+ // Lets CLI fail fast before any LLM call (docs/commands/start.md §Preconditions step 2).
45
+ static assertApiKeyPresent(resolved) {
46
+ if (!resolved.openrouterApiKey || resolved.openrouterApiKey.trim() === '') {
47
+ throw new Error('OPENROUTER_API_KEY is missing. Set OPENROUTER_API_KEY in the environment, or run `aitm config set openrouterApiKey <key>` (get one at https://openrouter.ai/keys).');
48
+ }
49
+ }
50
+ provider() {
51
+ if (!this.providerInstance) {
52
+ Credentials.assertApiKeyPresent(this.resolved);
53
+ this.providerInstance = createOpenRouter({ apiKey: this.resolved.openrouterApiKey });
54
+ }
55
+ return this.providerInstance;
56
+ }
57
+ }
58
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/credentials/credentials.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,kEAAkE;AAClE,wEAAwE;AACxE,4FAA4F;AAE5F,OAAO,EAAE,gBAAgB,EAA2B,MAAM,6BAA6B,CAAC;AAGxF,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/C,MAAM,CAAC,MAAM,eAAe,GAAuC;IACjE,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,OAAO;IACjB,YAAY,EAAE,MAAM;CACrB,CAAC;AAIF,MAAM,OAAO,WAAW;IAKO;IAJ7B,+EAA+E;IAC/E,+EAA+E;IACvE,gBAAgB,CAAiC;IAEzD,YAA6B,QAAwB;QAAxB,aAAQ,GAAR,QAAQ,CAAgB;IAAG,CAAC;IAEzD,4EAA4E;IAC5E,4DAA4D;IAC5D,OAAO;QACL,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YACjC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC;YACnC,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;SAC5C,CAAC;IACJ,CAAC;IAED,QAAQ,CAAC,IAAU;QACjB,OAAO,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;IACxD,CAAC;IAED,kBAAkB,CAAC,UAAsB;QACvC,MAAM,OAAO,GACX,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC;YAChC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO;YAC5B,cAAc,CAAC,UAAU,CAAC,CAAC;QAC7B,yFAAyF;QACzF,0FAA0F;QAC1F,2FAA2F;QAC3F,6DAA6D;QAC7D,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,gBAAgB,CAAC,EAAE,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,yFAAyF;IACzF,MAAM,CAAC,mBAAmB,CAAC,QAAwB;QACjD,IAAI,CAAC,QAAQ,CAAC,gBAAgB,IAAI,QAAQ,CAAC,gBAAgB,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC1E,MAAM,IAAI,KAAK,CACb,oKAAoK,CACrK,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,QAAQ;QACd,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC3B,WAAW,CAAC,mBAAmB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/C,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,OAAO,IAAI,CAAC,gBAAgB,CAAC;IAC/B,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ import type { Capability } from '../config/schema.ts';
2
+ export declare const DEFAULT_MODELS: Record<Capability, string>;
@@ -0,0 +1,21 @@
1
+ // Canonical capability defaults — all OpenRouter routes (docs/auth.md §"LLM provider").
2
+ // User-set models.{generic,smart,coding,fast} always wins; these only fill gaps.
3
+ //
4
+ // We don't fork defaults by AgentConfigFlavor (CLAUDE.md vs AGENTS.md). Flavor is a
5
+ // *style* signal, not a *vendor* signal — every model goes through OpenRouter, so a
6
+ // project's convention file does not constrain which model serves a request. The
7
+ // flavor only affects which markdown is fed to subagent system prompts.
8
+ //
9
+ // docs/agent-config-detection.md, docs/config.md, docs/runtime.md
10
+ // Tier mapping rationale (Claude family via OpenRouter — the most flexible coding stack today):
11
+ // haiku → fast : routing, orchestration, summarization (toModelOutput compaction)
12
+ // sonnet → generic : default fallback for any unspecified tier
13
+ // opus → smart : Planner, Reviewer (architectural reasoning, critique)
14
+ // opus → coding : Worker (best-in-class code generation)
15
+ export const DEFAULT_MODELS = {
16
+ fast: 'anthropic/claude-haiku-4.5',
17
+ generic: 'anthropic/claude-sonnet-4.6',
18
+ smart: 'anthropic/claude-opus-4.7',
19
+ coding: 'anthropic/claude-opus-4.7',
20
+ };
21
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/credentials/defaults.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,iFAAiF;AACjF,EAAE;AACF,oFAAoF;AACpF,oFAAoF;AACpF,iFAAiF;AACjF,wEAAwE;AACxE,EAAE;AACF,kEAAkE;AAIlE,gGAAgG;AAChG,wFAAwF;AACxF,iEAAiE;AACjE,6EAA6E;AAC7E,8DAA8D;AAC9D,MAAM,CAAC,MAAM,cAAc,GAA+B;IACxD,IAAI,EAAE,4BAA4B;IAClC,OAAO,EAAE,6BAA6B;IACtC,KAAK,EAAE,2BAA2B;IAClC,MAAM,EAAE,2BAA2B;CACpC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function atomicWrite(path: string, contents: string): Promise<void>;
@@ -0,0 +1,27 @@
1
+ // Shared atomic-file primitive. Temp file + fsync + rename so readers never see
2
+ // a half-written file. Used by ConfigLoader.writeSnapshot, ConfigWriter, StateStore.
3
+ //
4
+ // Temp name carries a random suffix so concurrent writes to the same path don't
5
+ // clobber each other's in-flight temp file. Mode 0o600 keeps secret-bearing
6
+ // config files owner-readable only on POSIX.
7
+ import { randomUUID } from 'node:crypto';
8
+ import { open, rename, rm } from 'node:fs/promises';
9
+ export async function atomicWrite(path, contents) {
10
+ const tmp = `${path}.${randomUUID()}.tmp`;
11
+ const fh = await open(tmp, 'wx', 0o600);
12
+ try {
13
+ await fh.writeFile(contents);
14
+ await fh.sync();
15
+ }
16
+ finally {
17
+ await fh.close();
18
+ }
19
+ try {
20
+ await rename(tmp, path);
21
+ }
22
+ catch (err) {
23
+ await rm(tmp, { force: true });
24
+ throw err;
25
+ }
26
+ }
27
+ //# sourceMappingURL=atomic-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-write.js","sourceRoot":"","sources":["../../src/fs/atomic-write.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,qFAAqF;AACrF,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,6CAA6C;AAE7C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEpD,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,QAAgB;IAC9D,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,UAAU,EAAE,MAAM,CAAC;IAC1C,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAC7B,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC;IAClB,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;IACnB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/B,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,18 @@
1
+ export declare class PrNotFound extends Error {
2
+ readonly name = "PrNotFound";
3
+ }
4
+ export declare class ReviewThreadStale extends Error {
5
+ readonly name = "ReviewThreadStale";
6
+ }
7
+ export declare class CiFailed extends Error {
8
+ readonly name = "CiFailed";
9
+ }
10
+ export declare class GhCliMissing extends Error {
11
+ readonly name = "GhCliMissing";
12
+ }
13
+ export declare class GhAuthRequired extends Error {
14
+ readonly name = "GhAuthRequired";
15
+ }
16
+ export declare class MergeConflict extends Error {
17
+ readonly name = "MergeConflict";
18
+ }
@@ -0,0 +1,20 @@
1
+ // docs/github-integration.md §"Result typing" — never raw stderr; always domain errors.
2
+ export class PrNotFound extends Error {
3
+ name = 'PrNotFound';
4
+ }
5
+ export class ReviewThreadStale extends Error {
6
+ name = 'ReviewThreadStale';
7
+ }
8
+ export class CiFailed extends Error {
9
+ name = 'CiFailed';
10
+ }
11
+ export class GhCliMissing extends Error {
12
+ name = 'GhCliMissing';
13
+ }
14
+ export class GhAuthRequired extends Error {
15
+ name = 'GhAuthRequired';
16
+ }
17
+ export class MergeConflict extends Error {
18
+ name = 'MergeConflict';
19
+ }
20
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/github/errors.ts"],"names":[],"mappings":"AAAA,wFAAwF;AAExF,MAAM,OAAO,UAAW,SAAQ,KAAK;IACjB,IAAI,GAAG,YAAY,CAAC;CACvC;AAED,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IACxB,IAAI,GAAG,mBAAmB,CAAC;CAC9C;AAED,MAAM,OAAO,QAAS,SAAQ,KAAK;IACf,IAAI,GAAG,UAAU,CAAC;CACrC;AAED,MAAM,OAAO,YAAa,SAAQ,KAAK;IACnB,IAAI,GAAG,cAAc,CAAC;CACzC;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IACrB,IAAI,GAAG,gBAAgB,CAAC;CAC3C;AAED,MAAM,OAAO,aAAc,SAAQ,KAAK;IACpB,IAAI,GAAG,eAAe,CAAC;CAC1C"}
@@ -0,0 +1,47 @@
1
+ import { type CheckStatus, type PullRequest, type ReviewThread } from './schema.ts';
2
+ export type CreatePrInput = {
3
+ title: string;
4
+ body: string;
5
+ base: string;
6
+ head: string;
7
+ draft?: boolean;
8
+ labels?: string[];
9
+ };
10
+ export declare const DEFAULT_PR_LABEL = "ai-task-master";
11
+ export type MergeMethod = 'squash' | 'merge' | 'rebase';
12
+ export type RunCmdOptions = {
13
+ cwd?: string;
14
+ };
15
+ export type RunCmdResult = {
16
+ stdout: string;
17
+ stderr: string;
18
+ exitCode: number;
19
+ };
20
+ export type RunCmd = (file: string, args: readonly string[], options?: RunCmdOptions) => Promise<RunCmdResult>;
21
+ export declare const defaultRunCmd: RunCmd;
22
+ export type Sleep = (ms: number) => Promise<void>;
23
+ export declare const defaultSleep: Sleep;
24
+ export declare const CHECKS_INITIAL_DELAY_MS = 1000;
25
+ export declare const CHECKS_MAX_DELAY_MS = 60000;
26
+ export declare class GitHubClient {
27
+ private readonly cwd;
28
+ private readonly runCmd;
29
+ private readonly sleep;
30
+ constructor(cwd: string, runCmd?: RunCmd, sleep?: Sleep);
31
+ currentBranch(): Promise<string>;
32
+ defaultBranch(): Promise<string>;
33
+ getPrForBranch(branch: string): Promise<PullRequest | null>;
34
+ createPr(input: CreatePrInput): Promise<PullRequest>;
35
+ waitForChecks(pr: number): Promise<CheckStatus>;
36
+ listUnresolvedThreads(pr: number): Promise<ReviewThread[]>;
37
+ private paginateReviewThreads;
38
+ private paginateThreadComments;
39
+ replyToThread(threadId: string, body: string): Promise<void>;
40
+ resolveThread(threadId: string): Promise<void>;
41
+ private repoMeta;
42
+ mergePr(pr: number, method: MergeMethod): Promise<void>;
43
+ authStatus(): Promise<{
44
+ ok: boolean;
45
+ scopes: string[];
46
+ }>;
47
+ }
@@ -0,0 +1,417 @@
1
+ // docs/github-integration.md, docs/auth.md §"GitHub"
2
+ // Only module allowed to shell out to gh. Uses execa (docs/runtime.md — Bun.$ forbidden in src/).
3
+ import { ExecaError, execa } from 'execa';
4
+ import { z } from 'zod';
5
+ import { CiFailed, MergeConflict } from "./errors.js";
6
+ import { PullRequestSchema, } from "./schema.js";
7
+ export const DEFAULT_PR_LABEL = 'ai-task-master';
8
+ export const defaultRunCmd = async (file, args, options) => {
9
+ try {
10
+ const r = await execa(file, [...args], options?.cwd ? { cwd: options.cwd } : {});
11
+ return {
12
+ stdout: typeof r.stdout === 'string' ? r.stdout : '',
13
+ stderr: typeof r.stderr === 'string' ? r.stderr : '',
14
+ exitCode: r.exitCode ?? 0,
15
+ };
16
+ }
17
+ catch (err) {
18
+ if (err instanceof ExecaError) {
19
+ return {
20
+ stdout: typeof err.stdout === 'string' ? err.stdout : '',
21
+ stderr: typeof err.stderr === 'string' ? err.stderr : '',
22
+ exitCode: err.exitCode ?? 1,
23
+ };
24
+ }
25
+ throw err;
26
+ }
27
+ };
28
+ export const defaultSleep = (ms) => new Promise((resolve) => {
29
+ setTimeout(resolve, ms);
30
+ });
31
+ export const CHECKS_INITIAL_DELAY_MS = 1000;
32
+ export const CHECKS_MAX_DELAY_MS = 60_000;
33
+ export class GitHubClient {
34
+ cwd;
35
+ runCmd;
36
+ sleep;
37
+ // Capability matrix — docs/github-integration.md §"Capabilities".
38
+ // Backoff — docs/github-integration.md §"Rate limits" (1s, doubling, 60s cap).
39
+ constructor(cwd, runCmd = defaultRunCmd, sleep = defaultSleep) {
40
+ this.cwd = cwd;
41
+ this.runCmd = runCmd;
42
+ this.sleep = sleep;
43
+ }
44
+ async currentBranch() {
45
+ const r = await this.runCmd('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: this.cwd });
46
+ if (r.exitCode !== 0) {
47
+ throw new Error(`git rev-parse failed: ${r.stderr.trim() || r.stdout.trim()}`);
48
+ }
49
+ return r.stdout.trim();
50
+ }
51
+ async defaultBranch() {
52
+ const r = await this.runCmd('gh', ['repo', 'view', '--json', 'defaultBranchRef'], {
53
+ cwd: this.cwd,
54
+ });
55
+ if (r.exitCode !== 0) {
56
+ throw new Error(`gh repo view failed: ${r.stderr.trim() || r.stdout.trim()}`);
57
+ }
58
+ const parsed = JSON.parse(r.stdout);
59
+ if (typeof parsed === 'object' &&
60
+ parsed !== null &&
61
+ 'defaultBranchRef' in parsed &&
62
+ typeof parsed.defaultBranchRef === 'object' &&
63
+ parsed.defaultBranchRef !== null &&
64
+ 'name' in parsed.defaultBranchRef &&
65
+ typeof parsed.defaultBranchRef.name === 'string') {
66
+ return parsed.defaultBranchRef.name;
67
+ }
68
+ throw new Error(`gh repo view: unexpected JSON shape: ${r.stdout}`);
69
+ }
70
+ async getPrForBranch(branch) {
71
+ const r = await this.runCmd('gh', ['pr', 'view', branch, '--json', 'number,state,url,headRefName,baseRefName'], { cwd: this.cwd });
72
+ if (r.exitCode !== 0) {
73
+ if (isPrNotFoundStderr(r.stderr))
74
+ return null;
75
+ throw new Error(`gh pr view failed: ${r.stderr.trim() || r.stdout.trim()}`);
76
+ }
77
+ return PullRequestSchema.parse(JSON.parse(r.stdout));
78
+ }
79
+ async createPr(input) {
80
+ const labels = input.labels ?? [DEFAULT_PR_LABEL];
81
+ const args = [
82
+ 'pr',
83
+ 'create',
84
+ '--title',
85
+ input.title,
86
+ '--body',
87
+ input.body,
88
+ '--base',
89
+ input.base,
90
+ '--head',
91
+ input.head,
92
+ ];
93
+ if (input.draft)
94
+ args.push('--draft');
95
+ for (const label of labels)
96
+ args.push('--label', label);
97
+ // `gh pr create --label X` fails if X doesn't exist yet — which it won't on a fresh repo the
98
+ // first time aitm opens a PR. Ensure each label exists first (idempotent via --force; the
99
+ // result is intentionally not checked so a labels-permission gap doesn't block PR creation).
100
+ for (const label of labels) {
101
+ await this.runCmd('gh', ['label', 'create', label, '--force'], { cwd: this.cwd });
102
+ }
103
+ const r = await this.runCmd('gh', args, { cwd: this.cwd });
104
+ if (r.exitCode !== 0) {
105
+ throw new Error(`gh pr create failed: ${r.stderr.trim() || r.stdout.trim()}`);
106
+ }
107
+ // gh prints the PR URL to stdout; we re-fetch to get the full typed shape.
108
+ const pr = await this.getPrForBranch(input.head);
109
+ if (!pr) {
110
+ throw new Error(`gh pr create succeeded for ${input.head} but PR lookup returned null (stdout: ${r.stdout.trim()})`);
111
+ }
112
+ return pr;
113
+ }
114
+ async waitForChecks(pr) {
115
+ let delay = CHECKS_INITIAL_DELAY_MS;
116
+ while (true) {
117
+ const r = await this.runCmd('gh', ['pr', 'checks', String(pr), '--json', 'bucket,name,state'], { cwd: this.cwd });
118
+ // `gh pr checks` exits 8 when any check fails but still emits JSON on stdout. Treat any
119
+ // exit code as "command ran" if stdout parses; otherwise propagate the failure.
120
+ const rows = tryParseChecks(r.stdout);
121
+ if (!rows) {
122
+ throw new Error(`gh pr checks failed: ${r.stderr.trim() || r.stdout.trim()}`);
123
+ }
124
+ const status = aggregateChecks(rows);
125
+ if (status === 'failure' || status === 'cancelled') {
126
+ throw new CiFailed(`PR #${pr} ${status}: ${summarizeFailures(rows)}`);
127
+ }
128
+ if (status !== 'pending')
129
+ return status;
130
+ await this.sleep(delay);
131
+ delay = Math.min(delay * 2, CHECKS_MAX_DELAY_MS);
132
+ }
133
+ }
134
+ async listUnresolvedThreads(pr) {
135
+ const { owner, name } = await this.repoMeta();
136
+ // GitHub caps connections at 100 nodes per page — page through threads and
137
+ // their comments to avoid silently dropping data on large PRs.
138
+ const threads = await this.paginateReviewThreads(owner, name, pr);
139
+ const unresolved = threads.filter((t) => !t.isResolved);
140
+ for (const thread of unresolved) {
141
+ if (thread.comments.pageInfo.hasNextPage && thread.comments.pageInfo.endCursor) {
142
+ const rest = await this.paginateThreadComments(thread.id, thread.comments.pageInfo.endCursor);
143
+ thread.comments.nodes.push(...rest);
144
+ }
145
+ }
146
+ return unresolved.map((node) => ({
147
+ id: node.id,
148
+ isResolved: node.isResolved,
149
+ path: node.path,
150
+ comments: node.comments.nodes.map((c) => ({
151
+ id: c.id,
152
+ body: c.body,
153
+ author: c.author?.login ?? 'ghost',
154
+ })),
155
+ }));
156
+ }
157
+ async paginateReviewThreads(owner, repo, pr) {
158
+ const collected = [];
159
+ let cursor = null;
160
+ while (true) {
161
+ const args = [
162
+ 'api',
163
+ 'graphql',
164
+ '-f',
165
+ `owner=${owner}`,
166
+ '-f',
167
+ `repo=${repo}`,
168
+ '-F',
169
+ `pr=${pr}`,
170
+ '-f',
171
+ `query=${REVIEW_THREADS_QUERY}`,
172
+ ];
173
+ if (cursor)
174
+ args.push('-f', `threadsCursor=${cursor}`);
175
+ const r = await this.runCmd('gh', args, { cwd: this.cwd });
176
+ if (r.exitCode !== 0) {
177
+ throw new Error(`gh api graphql (reviewThreads) failed: ${r.stderr.trim() || r.stdout.trim()}`);
178
+ }
179
+ const parsed = GqlReviewThreadsResponseSchema.parse(JSON.parse(r.stdout));
180
+ const conn = parsed.data.repository.pullRequest.reviewThreads;
181
+ collected.push(...conn.nodes);
182
+ if (!conn.pageInfo.hasNextPage || !conn.pageInfo.endCursor)
183
+ return collected;
184
+ cursor = conn.pageInfo.endCursor;
185
+ }
186
+ }
187
+ async paginateThreadComments(threadId, startCursor) {
188
+ const collected = [];
189
+ let cursor = startCursor;
190
+ while (cursor) {
191
+ const r = await this.runCmd('gh', [
192
+ 'api',
193
+ 'graphql',
194
+ '-f',
195
+ `threadId=${threadId}`,
196
+ '-f',
197
+ `commentsCursor=${cursor}`,
198
+ '-f',
199
+ `query=${THREAD_COMMENTS_QUERY}`,
200
+ ], { cwd: this.cwd });
201
+ if (r.exitCode !== 0) {
202
+ throw new Error(`gh api graphql (threadComments) failed: ${r.stderr.trim() || r.stdout.trim()}`);
203
+ }
204
+ const parsed = GqlThreadCommentsResponseSchema.parse(JSON.parse(r.stdout));
205
+ const conn = parsed.data.node.comments;
206
+ collected.push(...conn.nodes);
207
+ cursor =
208
+ conn.pageInfo.hasNextPage && conn.pageInfo.endCursor ? conn.pageInfo.endCursor : null;
209
+ }
210
+ return collected;
211
+ }
212
+ async replyToThread(threadId, body) {
213
+ const r = await this.runCmd('gh', [
214
+ 'api',
215
+ 'graphql',
216
+ '-f',
217
+ `threadId=${threadId}`,
218
+ '-f',
219
+ `body=${body}`,
220
+ '-f',
221
+ `query=${REPLY_THREAD_MUTATION}`,
222
+ ], { cwd: this.cwd });
223
+ if (r.exitCode !== 0) {
224
+ throw new Error(`gh api graphql (replyToThread) failed: ${r.stderr.trim() || r.stdout.trim()}`);
225
+ }
226
+ }
227
+ async resolveThread(threadId) {
228
+ const r = await this.runCmd('gh', ['api', 'graphql', '-f', `threadId=${threadId}`, '-f', `query=${RESOLVE_THREAD_MUTATION}`], { cwd: this.cwd });
229
+ if (r.exitCode !== 0) {
230
+ throw new Error(`gh api graphql (resolveThread) failed: ${r.stderr.trim() || r.stdout.trim()}`);
231
+ }
232
+ }
233
+ async repoMeta() {
234
+ const r = await this.runCmd('gh', ['repo', 'view', '--json', 'owner,name'], {
235
+ cwd: this.cwd,
236
+ });
237
+ if (r.exitCode !== 0) {
238
+ throw new Error(`gh repo view failed: ${r.stderr.trim() || r.stdout.trim()}`);
239
+ }
240
+ const parsed = RepoOwnerNameSchema.parse(JSON.parse(r.stdout));
241
+ return { owner: parsed.owner.login, name: parsed.name };
242
+ }
243
+ async mergePr(pr, method) {
244
+ const r = await this.runCmd('gh', ['pr', 'merge', String(pr), `--${method}`], {
245
+ cwd: this.cwd,
246
+ });
247
+ if (r.exitCode === 0)
248
+ return;
249
+ const combined = `${r.stderr}\n${r.stdout}`;
250
+ if (/merge conflict|not mergeable|conflict/i.test(combined)) {
251
+ throw new MergeConflict(`Merge conflict on PR #${pr}: ${r.stderr.trim() || r.stdout.trim()}`);
252
+ }
253
+ throw new Error(`gh pr merge failed: ${r.stderr.trim() || r.stdout.trim()}`);
254
+ }
255
+ async authStatus() {
256
+ const r = await this.runCmd('gh', ['auth', 'status', '--hostname', 'github.com'], {
257
+ cwd: this.cwd,
258
+ });
259
+ // `gh auth status` writes its human-readable summary to stderr; stdout is usually empty.
260
+ const text = `${r.stderr}\n${r.stdout}`;
261
+ const scopes = parseScopes(text);
262
+ return { ok: r.exitCode === 0, scopes };
263
+ }
264
+ }
265
+ // `gh pr view` exits non-zero with messages like:
266
+ // "no pull requests found for branch <name>"
267
+ // "GraphQL: Could not resolve to a PullRequest..."
268
+ function isPrNotFoundStderr(stderr) {
269
+ return /no pull requests? found|could not resolve to a pullrequest|no open pull requests/i.test(stderr);
270
+ }
271
+ // `gh auth status` line shape: " - Token scopes: 'repo', 'workflow', 'read:org'"
272
+ function parseScopes(text) {
273
+ const match = text.match(/Token scopes:\s*([^\n]+)/i);
274
+ if (!match?.[1])
275
+ return [];
276
+ const scopes = [];
277
+ for (const raw of match[1].split(',')) {
278
+ const cleaned = raw.replace(/['"`]/g, '').trim();
279
+ if (cleaned)
280
+ scopes.push(cleaned);
281
+ }
282
+ return scopes;
283
+ }
284
+ // Wire shapes for `gh pr checks --json bucket,name,state`. The bucket field is the gh CLI's
285
+ // normalized status across providers (Actions, Circle, etc.); CheckStatus is our domain.
286
+ const CheckBucketSchema = z.enum(['pass', 'fail', 'pending', 'cancel', 'skipping']);
287
+ const CheckRowSchema = z.object({
288
+ bucket: CheckBucketSchema,
289
+ name: z.string(),
290
+ state: z.string(),
291
+ });
292
+ const ChecksResponseSchema = z.array(CheckRowSchema);
293
+ function tryParseChecks(stdout) {
294
+ if (!stdout.trim())
295
+ return null;
296
+ let raw;
297
+ try {
298
+ raw = JSON.parse(stdout);
299
+ }
300
+ catch {
301
+ return null;
302
+ }
303
+ const parsed = ChecksResponseSchema.safeParse(raw);
304
+ return parsed.success ? parsed.data : null;
305
+ }
306
+ const BUCKET_TO_STATUS = {
307
+ pass: 'success',
308
+ fail: 'failure',
309
+ pending: 'pending',
310
+ cancel: 'cancelled',
311
+ skipping: 'skipped',
312
+ };
313
+ function aggregateChecks(rows) {
314
+ if (rows.length === 0)
315
+ return 'success';
316
+ let pending = false;
317
+ for (const row of rows) {
318
+ const status = BUCKET_TO_STATUS[row.bucket];
319
+ if (status === 'failure')
320
+ return 'failure';
321
+ if (status === 'cancelled')
322
+ return 'cancelled';
323
+ if (status === 'pending')
324
+ pending = true;
325
+ }
326
+ return pending ? 'pending' : 'success';
327
+ }
328
+ function summarizeFailures(rows) {
329
+ const bad = rows.filter((r) => r.bucket === 'fail' || r.bucket === 'cancel');
330
+ if (bad.length === 0)
331
+ return 'unknown';
332
+ return bad.map((r) => `${r.name}=${r.bucket}`).join(', ');
333
+ }
334
+ // `gh repo view --json owner,name` returns `{ owner: { login }, name }`.
335
+ const RepoOwnerNameSchema = z.object({
336
+ owner: z.object({ login: z.string() }),
337
+ name: z.string(),
338
+ });
339
+ const REVIEW_THREADS_QUERY = `query($owner: String!, $repo: String!, $pr: Int!, $threadsCursor: String) {
340
+ repository(owner: $owner, name: $repo) {
341
+ pullRequest(number: $pr) {
342
+ reviewThreads(first: 100, after: $threadsCursor) {
343
+ pageInfo { hasNextPage endCursor }
344
+ nodes {
345
+ id
346
+ isResolved
347
+ path
348
+ comments(first: 100) {
349
+ pageInfo { hasNextPage endCursor }
350
+ nodes { id body author { login } }
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }`;
357
+ const THREAD_COMMENTS_QUERY = `query($threadId: ID!, $commentsCursor: String) {
358
+ node(id: $threadId) {
359
+ ... on PullRequestReviewThread {
360
+ comments(first: 100, after: $commentsCursor) {
361
+ pageInfo { hasNextPage endCursor }
362
+ nodes { id body author { login } }
363
+ }
364
+ }
365
+ }
366
+ }`;
367
+ const REPLY_THREAD_MUTATION = `mutation($threadId: ID!, $body: String!) {
368
+ addPullRequestReviewThreadReply(input: {pullRequestReviewThreadId: $threadId, body: $body}) {
369
+ comment { id }
370
+ }
371
+ }`;
372
+ const RESOLVE_THREAD_MUTATION = `mutation($threadId: ID!) {
373
+ resolveReviewThread(input: {threadId: $threadId}) {
374
+ thread { id isResolved }
375
+ }
376
+ }`;
377
+ const GqlPageInfoSchema = z.object({
378
+ hasNextPage: z.boolean(),
379
+ endCursor: z.string().nullable(),
380
+ });
381
+ const GqlReviewCommentSchema = z.object({
382
+ id: z.string(),
383
+ body: z.string(),
384
+ author: z.object({ login: z.string() }).nullable(),
385
+ });
386
+ const GqlReviewThreadSchema = z.object({
387
+ id: z.string(),
388
+ isResolved: z.boolean(),
389
+ path: z.string().nullable(),
390
+ comments: z.object({
391
+ pageInfo: GqlPageInfoSchema,
392
+ nodes: z.array(GqlReviewCommentSchema),
393
+ }),
394
+ });
395
+ const GqlReviewThreadsResponseSchema = z.object({
396
+ data: z.object({
397
+ repository: z.object({
398
+ pullRequest: z.object({
399
+ reviewThreads: z.object({
400
+ pageInfo: GqlPageInfoSchema,
401
+ nodes: z.array(GqlReviewThreadSchema),
402
+ }),
403
+ }),
404
+ }),
405
+ }),
406
+ });
407
+ const GqlThreadCommentsResponseSchema = z.object({
408
+ data: z.object({
409
+ node: z.object({
410
+ comments: z.object({
411
+ pageInfo: GqlPageInfoSchema,
412
+ nodes: z.array(GqlReviewCommentSchema),
413
+ }),
414
+ }),
415
+ }),
416
+ });
417
+ //# sourceMappingURL=github-client.js.map