@nonameolsson/nightjar 0.0.0-stage → 0.2.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +405 -2
  3. package/dist/account.d.ts +12 -0
  4. package/dist/account.js +57 -0
  5. package/dist/account.js.map +1 -0
  6. package/dist/adapter-module.d.ts +18 -0
  7. package/dist/adapter-module.js +83 -0
  8. package/dist/adapter-module.js.map +1 -0
  9. package/dist/adapters.d.ts +15 -0
  10. package/dist/adapters.js +160 -0
  11. package/dist/adapters.js.map +1 -0
  12. package/dist/args.d.ts +13 -0
  13. package/dist/args.js +84 -0
  14. package/dist/args.js.map +1 -0
  15. package/dist/ask.d.ts +6 -0
  16. package/dist/ask.js +27 -0
  17. package/dist/ask.js.map +1 -0
  18. package/dist/azure-devops/api.d.ts +34 -0
  19. package/dist/azure-devops/api.js +98 -0
  20. package/dist/azure-devops/api.js.map +1 -0
  21. package/dist/azure-devops/code-host.d.ts +36 -0
  22. package/dist/azure-devops/code-host.js +185 -0
  23. package/dist/azure-devops/code-host.js.map +1 -0
  24. package/dist/azure-devops/fake-azure-devops.d.ts +90 -0
  25. package/dist/azure-devops/fake-azure-devops.js +728 -0
  26. package/dist/azure-devops/fake-azure-devops.js.map +1 -0
  27. package/dist/azure-devops/index.d.ts +8 -0
  28. package/dist/azure-devops/index.js +11 -0
  29. package/dist/azure-devops/index.js.map +1 -0
  30. package/dist/azure-devops/sign-in.d.ts +11 -0
  31. package/dist/azure-devops/sign-in.js +68 -0
  32. package/dist/azure-devops/sign-in.js.map +1 -0
  33. package/dist/azure-devops/text.d.ts +1 -0
  34. package/dist/azure-devops/text.js +41 -0
  35. package/dist/azure-devops/text.js.map +1 -0
  36. package/dist/azure-devops/tracker.d.ts +44 -0
  37. package/dist/azure-devops/tracker.js +333 -0
  38. package/dist/azure-devops/tracker.js.map +1 -0
  39. package/dist/build.d.ts +75 -0
  40. package/dist/build.js +698 -0
  41. package/dist/build.js.map +1 -0
  42. package/dist/check.d.ts +8 -0
  43. package/dist/check.js +25 -0
  44. package/dist/check.js.map +1 -0
  45. package/dist/checks.d.ts +45 -0
  46. package/dist/checks.js +243 -0
  47. package/dist/checks.js.map +1 -0
  48. package/dist/cli.d.ts +2 -0
  49. package/dist/cli.js +11 -0
  50. package/dist/cli.js.map +1 -0
  51. package/dist/code-host.d.ts +19 -0
  52. package/dist/code-host.js +2 -0
  53. package/dist/code-host.js.map +1 -0
  54. package/dist/config.d.ts +86 -0
  55. package/dist/config.js +294 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/contract/case.d.ts +4 -0
  58. package/dist/contract/case.js +2 -0
  59. package/dist/contract/case.js.map +1 -0
  60. package/dist/contract/code-host.d.ts +24 -0
  61. package/dist/contract/code-host.js +108 -0
  62. package/dist/contract/code-host.js.map +1 -0
  63. package/dist/contract/command.d.ts +2 -0
  64. package/dist/contract/command.js +36 -0
  65. package/dist/contract/command.js.map +1 -0
  66. package/dist/contract/feature-writer.d.ts +3 -0
  67. package/dist/contract/feature-writer.js +101 -0
  68. package/dist/contract/feature-writer.js.map +1 -0
  69. package/dist/contract/run.d.ts +18 -0
  70. package/dist/contract/run.js +89 -0
  71. package/dist/contract/run.js.map +1 -0
  72. package/dist/contract/tracker.d.ts +30 -0
  73. package/dist/contract/tracker.js +250 -0
  74. package/dist/contract/tracker.js.map +1 -0
  75. package/dist/create.d.ts +9 -0
  76. package/dist/create.js +112 -0
  77. package/dist/create.js.map +1 -0
  78. package/dist/doctor.d.ts +4 -0
  79. package/dist/doctor.js +42 -0
  80. package/dist/doctor.js.map +1 -0
  81. package/dist/fakes/code-host.d.ts +21 -0
  82. package/dist/fakes/code-host.js +42 -0
  83. package/dist/fakes/code-host.js.map +1 -0
  84. package/dist/fakes/harness.d.ts +7 -0
  85. package/dist/fakes/harness.js +68 -0
  86. package/dist/fakes/harness.js.map +1 -0
  87. package/dist/fakes/machine.d.ts +11 -0
  88. package/dist/fakes/machine.js +101 -0
  89. package/dist/fakes/machine.js.map +1 -0
  90. package/dist/fakes/tracker.d.ts +40 -0
  91. package/dist/fakes/tracker.js +116 -0
  92. package/dist/fakes/tracker.js.map +1 -0
  93. package/dist/feature-file.d.ts +12 -0
  94. package/dist/feature-file.js +84 -0
  95. package/dist/feature-file.js.map +1 -0
  96. package/dist/git.d.ts +9 -0
  97. package/dist/git.js +12 -0
  98. package/dist/git.js.map +1 -0
  99. package/dist/github/api.d.ts +37 -0
  100. package/dist/github/api.js +66 -0
  101. package/dist/github/api.js.map +1 -0
  102. package/dist/github/code-host.d.ts +19 -0
  103. package/dist/github/code-host.js +105 -0
  104. package/dist/github/code-host.js.map +1 -0
  105. package/dist/github/fake-gh.d.ts +17 -0
  106. package/dist/github/fake-gh.js +51 -0
  107. package/dist/github/fake-gh.js.map +1 -0
  108. package/dist/github/fake-github.d.ts +58 -0
  109. package/dist/github/fake-github.js +285 -0
  110. package/dist/github/fake-github.js.map +1 -0
  111. package/dist/github/labels.d.ts +3 -0
  112. package/dist/github/labels.js +44 -0
  113. package/dist/github/labels.js.map +1 -0
  114. package/dist/github/tracker.d.ts +28 -0
  115. package/dist/github/tracker.js +208 -0
  116. package/dist/github/tracker.js.map +1 -0
  117. package/dist/index.d.ts +16 -0
  118. package/dist/index.js +13 -0
  119. package/dist/index.js.map +1 -0
  120. package/dist/init.d.ts +19 -0
  121. package/dist/init.js +225 -0
  122. package/dist/init.js.map +1 -0
  123. package/dist/ledger.d.ts +13 -0
  124. package/dist/ledger.js +49 -0
  125. package/dist/ledger.js.map +1 -0
  126. package/dist/main.d.ts +11 -0
  127. package/dist/main.js +215 -0
  128. package/dist/main.js.map +1 -0
  129. package/dist/person-config.d.ts +9 -0
  130. package/dist/person-config.js +46 -0
  131. package/dist/person-config.js.map +1 -0
  132. package/dist/plan.d.ts +9 -0
  133. package/dist/plan.js +27 -0
  134. package/dist/plan.js.map +1 -0
  135. package/dist/ports.d.ts +16 -0
  136. package/dist/ports.js +104 -0
  137. package/dist/ports.js.map +1 -0
  138. package/dist/prompt.d.ts +166 -0
  139. package/dist/prompt.js +217 -0
  140. package/dist/prompt.js.map +1 -0
  141. package/dist/queue.d.ts +20 -0
  142. package/dist/queue.js +94 -0
  143. package/dist/queue.js.map +1 -0
  144. package/dist/say.d.ts +2 -0
  145. package/dist/say.js +4 -0
  146. package/dist/say.js.map +1 -0
  147. package/dist/serve.d.ts +10 -0
  148. package/dist/serve.js +85 -0
  149. package/dist/serve.js.map +1 -0
  150. package/dist/session.d.ts +39 -0
  151. package/dist/session.js +144 -0
  152. package/dist/session.js.map +1 -0
  153. package/dist/settings.d.ts +10 -0
  154. package/dist/settings.js +92 -0
  155. package/dist/settings.js.map +1 -0
  156. package/dist/signals.d.ts +5 -0
  157. package/dist/signals.js +13 -0
  158. package/dist/signals.js.map +1 -0
  159. package/dist/skills.d.ts +4 -0
  160. package/dist/skills.js +51 -0
  161. package/dist/skills.js.map +1 -0
  162. package/dist/status-shape.d.ts +53 -0
  163. package/dist/status-shape.js +3 -0
  164. package/dist/status-shape.js.map +1 -0
  165. package/dist/status.d.ts +23 -0
  166. package/dist/status.js +56 -0
  167. package/dist/status.js.map +1 -0
  168. package/dist/tracker.d.ts +56 -0
  169. package/dist/tracker.js +12 -0
  170. package/dist/tracker.js.map +1 -0
  171. package/dist/workflow.d.ts +33 -0
  172. package/dist/workflow.js +53 -0
  173. package/dist/workflow.js.map +1 -0
  174. package/dist/worktree.d.ts +44 -0
  175. package/dist/worktree.js +193 -0
  176. package/dist/worktree.js.map +1 -0
  177. package/docs/ticket-contract.md +95 -0
  178. package/package.json +74 -5
  179. package/plugin/nightjar-status/.claude-plugin/plugin.json +5 -0
  180. package/plugin/nightjar-status/hooks/format.ts +48 -0
  181. package/plugin/nightjar-status/hooks/hooks.json +1 -0
  182. package/plugin/nightjar-status/hooks/register.ts +51 -0
  183. package/schema/factory.schema.json +437 -0
  184. package/skills/nightjar-feature/SKILL.md +73 -0
  185. package/src/status-shape.ts +44 -0
@@ -0,0 +1,53 @@
1
+ // A Feature's own `**Workflow:**` line, as written; the bold is optional.
2
+ export function workflowLine(body) {
3
+ const match = /^(?:\*\*)?Workflow:(?:\*\*)?[ \t]*([^*\s].*?)\s*$/im.exec(body);
4
+ return match?.[1]?.replace(/^`(.*)`$/, '$1').trim() || undefined;
5
+ }
6
+ export function chooseWorkflow(settings, body) {
7
+ const known = Object.keys(settings.workflows ?? {});
8
+ const asked = workflowLine(body);
9
+ const name = asked === undefined
10
+ ? settings.defaultWorkflow
11
+ : (known.find((n) => n === asked) ??
12
+ known.find((n) => n.toLowerCase() === asked.toLowerCase()));
13
+ if (asked === undefined && name === undefined)
14
+ return { kind: 'none' };
15
+ const workflow = name === undefined ? undefined : settings.workflows?.[name];
16
+ if (name === undefined || workflow === undefined) {
17
+ const list = known.length === 0 ? 'factory.json names none' : known.map((n) => `\`${n}\``).join(', ');
18
+ return {
19
+ kind: 'refused',
20
+ why: `its \`**Workflow:**\` line names \`${asked ?? ''}\`, which is no Workflow. The Workflows known: ${list}.`
21
+ };
22
+ }
23
+ const broken = settings.brokenWorkflows?.[name];
24
+ if (broken !== undefined) {
25
+ return { kind: 'refused', why: `the Workflow \`${name}\` cannot run: ${broken}` };
26
+ }
27
+ return { kind: 'workflow', name, workflow };
28
+ }
29
+ // What a Step is called in a commit trailer, which tells a later run it is done.
30
+ export const stepKey = (workflow, step, ticket) => `${workflow}/${step}${ticket === null ? '' : ` #${ticket}`}`;
31
+ // The skills a Step's prompt names with a slash, such as `/some-skill args`.
32
+ export function skillsNamed(prompt) {
33
+ const names = [
34
+ ...prompt.matchAll(/(?<![\w/.:~-])\/([a-z][a-z0-9_-]*(?::[a-z][a-z0-9_-]*)?)(?![\w/.-])/g)
35
+ ];
36
+ return [...new Set(names.flatMap((m) => (m[1] ? [m[1]] : [])))];
37
+ }
38
+ // The pull request's section on a Workflow, or null when it ran no Step.
39
+ export function workflowSection(name, reports, ref) {
40
+ if (reports.length === 0)
41
+ return null;
42
+ const lines = reports.map((r) => {
43
+ const where = `\`${r.step.name}\`${r.ticket === null ? '' : ` (after ${ref(r.ticket)})`}`;
44
+ if (r.state === 'done')
45
+ return `- ${where}: done`;
46
+ if (r.state === 'undone')
47
+ return `- ${where}: **not done**, ${r.why ?? 'it did not finish'}; its changes were thrown away`;
48
+ const left = r.left.map((l) => `\n - ${l}`).join('');
49
+ return `- ${where}: its condition (${r.step.until ?? ''}) did not hold after ${String(r.rounds ?? 0)} round${r.rounds === 1 ? '' : 's'}. Left:${left || ' (it listed nothing)'}`;
50
+ });
51
+ return `## Workflow \`${name}\`\n\n${lines.join('\n')}`;
52
+ }
53
+ //# sourceMappingURL=workflow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.js","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAQA,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,IAAY;IACxC,MAAM,KAAK,GAAG,qDAAqD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/E,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;AAClE,CAAC;AAeD,MAAM,UAAU,cAAc,CAAC,QAA0B,EAAE,IAAY;IACtE,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IACpD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,GACT,KAAK,KAAK,SAAS;QAClB,CAAC,CAAC,QAAQ,CAAC,eAAe;QAC1B,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACvE,MAAM,QAAQ,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC;IAC7E,IAAI,IAAI,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClD,MAAM,IAAI,GACT,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1F,OAAO;YACN,IAAI,EAAE,SAAS;YACf,GAAG,EAAE,sCAAsC,KAAK,IAAI,EAAE,kDAAkD,IAAI,GAAG;SAC/G,CAAC;IACH,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,kBAAkB,IAAI,kBAAkB,MAAM,EAAE,EAAE,CAAC;IACnF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC7C,CAAC;AAED,iFAAiF;AACjF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,QAAgB,EAAE,IAAY,EAAE,MAAqB,EAAE,EAAE,CAChF,GAAG,QAAQ,IAAI,IAAI,GAAG,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC;AAE9D,6EAA6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAc;IACzC,MAAM,KAAK,GAAG;QACb,GAAG,MAAM,CAAC,QAAQ,CAAC,sEAAsE,CAAC;KAC1F,CAAC;IACF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACjE,CAAC;AAaD,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAC9B,IAAY,EACZ,OAA8B,EAC9B,GAA2B;IAE3B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;QAC1F,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;YAAE,OAAO,KAAK,KAAK,QAAQ,CAAC;QAClD,IAAI,CAAC,CAAC,KAAK,KAAK,QAAQ;YACvB,OAAO,KAAK,KAAK,mBAAmB,CAAC,CAAC,GAAG,IAAI,mBAAmB,gCAAgC,CAAC;QAClG,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtD,OAAO,KAAK,KAAK,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,wBAAwB,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI,IAAI,sBAAsB,EAAE,CAAC;IAClL,CAAC,CAAC,CAAC;IACH,OAAO,iBAAiB,IAAI,SAAS,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACzD,CAAC"}
@@ -0,0 +1,44 @@
1
+ import { type Env } from './git.js';
2
+ export declare class GitFailure extends Error {
3
+ }
4
+ export declare const TICKET_TRAILER = "Factory-Ticket";
5
+ export declare const STEP_TRAILER = "Factory-Step";
6
+ export declare function branchFor(id: string, title: string): string;
7
+ export declare function worktreesFor(home: string, root: string): string;
8
+ export interface FeatureBranch {
9
+ readonly branch: string;
10
+ readonly folder: string;
11
+ readonly base: string;
12
+ }
13
+ export declare class Worktree {
14
+ readonly at: FeatureBranch;
15
+ private readonly env;
16
+ constructor(at: FeatureBranch, env: Env);
17
+ git(args: readonly string[]): Promise<string>;
18
+ private setupMarker;
19
+ isSetUp: () => Promise<boolean>;
20
+ markSetUp: () => Promise<void>;
21
+ head: () => Promise<string>;
22
+ ahead: () => Promise<number>;
23
+ built(): Promise<Set<string>>;
24
+ commitTicket(id: string, title: string, summary: string, session: string): Promise<boolean>;
25
+ notes(): Promise<string>;
26
+ stepsDone(): Promise<Set<string>>;
27
+ diff(limit: number): Promise<{
28
+ text: string;
29
+ truncated: boolean;
30
+ }>;
31
+ commitStep(o: {
32
+ workflow: string;
33
+ step: string;
34
+ key: string;
35
+ after: string | null;
36
+ summary: string;
37
+ session: string;
38
+ }): Promise<boolean>;
39
+ revert(before: string): Promise<void>;
40
+ discard(before: string, ticket: string): Promise<string | null>;
41
+ }
42
+ export declare function openWorktree(root: string, worktrees: string, branch: string, baseBranch: string, env: Env): Promise<Worktree>;
43
+ export declare function defaultBranch(root: string, env: Env): Promise<string | null>;
44
+ export declare function signing(root: string, env: Env): Promise<'signs' | 'unsigned' | 'fails'>;
@@ -0,0 +1,193 @@
1
+ // Only Nightjar commits in the worktree or saves a failed attempt; the Code host pushes.
2
+ import { createHash } from 'node:crypto';
3
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { basename, dirname, join, resolve } from 'node:path';
5
+ import { firstLine, run } from './git.js';
6
+ // A git command Nightjar cannot go on without.
7
+ export class GitFailure extends Error {
8
+ }
9
+ // Names the Ticket a commit built, so a later run on the branch knows what is done.
10
+ export const TICKET_TRAILER = 'Factory-Ticket';
11
+ // Names the Workflow Step a commit holds, so a later run knows it is done.
12
+ export const STEP_TRAILER = 'Factory-Step';
13
+ // The notes the Steps of a run share, in the worktree and never committed.
14
+ const NOTES = '.factory/notes';
15
+ const BRANCH_PREFIX = 'factory/';
16
+ // An item id as a piece of a branch name: whatever a ref may not hold becomes a dash.
17
+ const refPart = (id) => id.replace(/[^A-Za-z0-9_-]+/g, '-');
18
+ export function branchFor(id, title) {
19
+ const slug = title
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, '-')
22
+ .replace(/^-+|-+$/g, '')
23
+ .slice(0, 40)
24
+ .replace(/-+$/, '');
25
+ return `${BRANCH_PREFIX}${refPart(id)}${slug ? `-${slug}` : ''}`;
26
+ }
27
+ // Out of the checkout, so the one a person works in is never touched; the hash keeps two checkouts of one name apart.
28
+ export function worktreesFor(home, root) {
29
+ const hash = createHash('sha256').update(root).digest('hex').slice(0, 8);
30
+ return join(home, '.nightjar', 'worktrees', `${basename(root)}-${hash}`);
31
+ }
32
+ export class Worktree {
33
+ at;
34
+ env;
35
+ constructor(at, env) {
36
+ this.at = at;
37
+ this.env = env;
38
+ }
39
+ async git(args) {
40
+ const got = await run('git', args, this.at.folder, this.env);
41
+ if (got.missing)
42
+ throw new GitFailure('git is not on the PATH.');
43
+ if (!got.ok)
44
+ throw new GitFailure(`git ${args[0] ?? ''} failed: ${firstLine(got.err)}`);
45
+ return got.out;
46
+ }
47
+ // In git's own folder for the worktree, so neither `git clean` nor a commit touches it.
48
+ async setupMarker() {
49
+ const path = (await this.git(['rev-parse', '--git-path', 'nightjar-setup'])).trim();
50
+ return resolve(this.at.folder, path);
51
+ }
52
+ isSetUp = async () => existsSync(await this.setupMarker());
53
+ markSetUp = async () => writeFileSync(await this.setupMarker(), '');
54
+ head = async () => (await this.git(['rev-parse', 'HEAD'])).trim();
55
+ ahead = async () => Number((await this.git(['rev-list', '--count', `${this.at.base}..HEAD`])).trim());
56
+ // The Tickets the branch's own commits built, from their trailers, oldest first.
57
+ async built() {
58
+ const out = await this.git([
59
+ 'log',
60
+ '--reverse',
61
+ `--format=%(trailers:key=${TICKET_TRAILER},valueonly)`,
62
+ `${this.at.base}..HEAD`
63
+ ]);
64
+ return new Set([...out.matchAll(/^#(\S+)$/gm)].flatMap((m) => (m[1] ? [m[1]] : [])));
65
+ }
66
+ // One commit of everything the session left, or false when it left nothing.
67
+ async commitTicket(id, title, summary, session) {
68
+ await this.git(['add', '-A']);
69
+ if ((await this.git(['diff', '--cached', '--name-only'])).trim() === '')
70
+ return false;
71
+ const message = `${title} (#${id})\n\n${summary.trim()}\n\n${TICKET_TRAILER}: #${id}\nFactory-Session: ${session}`;
72
+ await this.git(['commit', '-q', '-m', message]);
73
+ return true;
74
+ }
75
+ // The folder the Steps share, made, and kept out of every `git add` and `git clean` by the checkout's exclude file.
76
+ async notes() {
77
+ const folder = join(this.at.folder, NOTES);
78
+ mkdirSync(folder, { recursive: true });
79
+ const path = (await this.git(['rev-parse', '--git-path', 'info/exclude'])).trim();
80
+ const exclude = resolve(this.at.folder, path);
81
+ const line = `/${NOTES}/`;
82
+ const text = existsSync(exclude) ? readFileSync(exclude, 'utf8') : '';
83
+ if (!text.split('\n').includes(line)) {
84
+ mkdirSync(dirname(exclude), { recursive: true });
85
+ appendFileSync(exclude, `${text === '' || text.endsWith('\n') ? '' : '\n'}${line}\n`);
86
+ }
87
+ return folder;
88
+ }
89
+ // The Steps the branch's own commits hold, as their trailers name them.
90
+ async stepsDone() {
91
+ const out = await this.git([
92
+ 'log',
93
+ `--format=%(trailers:key=${STEP_TRAILER},valueonly)`,
94
+ `${this.at.base}..HEAD`
95
+ ]);
96
+ return new Set(out.split('\n').flatMap((l) => (l.trim() === '' ? [] : [l.trim()])));
97
+ }
98
+ // The Feature's diff against its base, cut at `limit` characters.
99
+ async diff(limit) {
100
+ const text = await this.git(['diff', `${this.at.base}...HEAD`]);
101
+ return text.length > limit
102
+ ? { text: text.slice(0, limit), truncated: true }
103
+ : { text, truncated: false };
104
+ }
105
+ // One commit of what a Step left, naming the Workflow and the Step; false when it changed no file.
106
+ async commitStep(o) {
107
+ await this.git(['add', '-A']);
108
+ if ((await this.git(['diff', '--cached', '--name-only'])).trim() === '')
109
+ return false;
110
+ const subject = `${o.workflow}: ${o.step}${o.after === null ? '' : ` (after #${o.after})`}`;
111
+ const message = `${subject}\n\n${o.summary.trim()}\n\n${STEP_TRAILER}: ${o.key}\nFactory-Session: ${o.session}`;
112
+ await this.git(['commit', '-q', '-m', message]);
113
+ return true;
114
+ }
115
+ // Throws away what a Step left, keeping nothing: the Step is listed as not done.
116
+ async revert(before) {
117
+ await this.git(['reset', '-q', '--hard', before]);
118
+ await this.git(['clean', '-q', '-fd']);
119
+ }
120
+ // Keeps what the session left on a side branch, which it names; null when it left nothing.
121
+ async discard(before, ticket) {
122
+ await this.git(['add', '-A']);
123
+ const moved = (await this.head()) !== before;
124
+ const staged = (await this.git(['diff', '--cached', '--name-only'])).trim() !== '';
125
+ let side = null;
126
+ if (moved || staged) {
127
+ const tree = (await this.git(['write-tree'])).trim();
128
+ const saved = (await this.git([
129
+ 'commit-tree',
130
+ '-p',
131
+ 'HEAD',
132
+ '-m',
133
+ `nightjar: the failed attempt at #${ticket}`,
134
+ tree
135
+ ])).trim();
136
+ side = `${this.at.branch}-attempt-${refPart(ticket)}`;
137
+ await this.git(['branch', '-f', side, saved]);
138
+ }
139
+ await this.git(['reset', '-q', '--hard', before]);
140
+ await this.git(['clean', '-q', '-fd']);
141
+ return side;
142
+ }
143
+ }
144
+ function inRoot(root, args, env) {
145
+ return run('git', ['-C', root, ...args], root, env);
146
+ }
147
+ const hasRef = async (root, ref, env) => (await inRoot(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], env)).ok;
148
+ // Reuses the worktree a run before left, else the pushed branch; a worktree a stopped run left dirty is cleaned.
149
+ export async function openWorktree(root, worktrees, branch, baseBranch, env) {
150
+ const must = async (args) => {
151
+ const got = await inRoot(root, args, env);
152
+ if (got.missing)
153
+ throw new GitFailure('git is not on the PATH.');
154
+ if (!got.ok)
155
+ throw new GitFailure(`git ${args[0] ?? ''} failed: ${firstLine(got.err)}`);
156
+ };
157
+ await must(['fetch', '-q', 'origin', baseBranch]);
158
+ // the branch a run before pushed, which may not be there yet
159
+ await inRoot(root, ['fetch', '-q', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`], env);
160
+ const base = `origin/${baseBranch}`;
161
+ const folder = join(worktrees, branch.slice(BRANCH_PREFIX.length));
162
+ const tree = new Worktree({ branch, folder, base }, env);
163
+ if (existsSync(folder)) {
164
+ await tree.git(['reset', '-q', '--hard']);
165
+ await tree.git(['clean', '-q', '-fd']);
166
+ return tree;
167
+ }
168
+ await must(['worktree', 'prune']);
169
+ if (await hasRef(root, `refs/heads/${branch}`, env)) {
170
+ await must(['worktree', 'add', '-q', folder, branch]);
171
+ }
172
+ else if (await hasRef(root, `refs/remotes/origin/${branch}`, env)) {
173
+ await must(['worktree', 'add', '-q', '--no-track', '-b', branch, folder, `origin/${branch}`]);
174
+ }
175
+ else {
176
+ await must(['worktree', 'add', '-q', '--no-track', '-b', branch, folder, base]);
177
+ }
178
+ return tree;
179
+ }
180
+ // The branch origin's HEAD points at, such as main, or null when origin names none.
181
+ export async function defaultBranch(root, env) {
182
+ const got = await inRoot(root, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], env);
183
+ return got.ok ? got.out.trim().replace(/^origin\//, '') || null : null;
184
+ }
185
+ // How git signs a commit here: a locked signing key fails every commit of the run.
186
+ export async function signing(root, env) {
187
+ const sign = await inRoot(root, ['config', '--bool', 'commit.gpgsign'], env);
188
+ if (sign.out.trim() !== 'true')
189
+ return 'unsigned';
190
+ const probe = ['commit-tree', '-S', '-p', 'HEAD', '-m', 'nightjar preflight', 'HEAD^{tree}'];
191
+ return (await inRoot(root, probe, env)).ok ? 'signs' : 'fails';
192
+ }
193
+ //# sourceMappingURL=worktree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree.js","sourceRoot":"","sources":["../src/worktree.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ # The Ticket contract
2
+
3
+ What a Ticket must hold for Nightjar to build it, on any Tracker, and the Feature file that `nightjar create` turns into a Feature and its Tickets. The words are defined in [GLOSSARY.md](../GLOSSARY.md).
4
+
5
+ ## A Ticket
6
+
7
+ A Ticket is one child of a Feature, built as one commit. It holds:
8
+
9
+ - **Acceptance criteria**, in its body: a heading named `Acceptance criteria` with a checklist under it. The Coding agent builds to them and the review reads the diff against them.
10
+
11
+ ```markdown
12
+ ### Acceptance criteria
13
+
14
+ - [ ] comments escape through src/escape.ts
15
+ - [ ] a comment with <b> shows it as text
16
+ ```
17
+
18
+ - **Blocking links**, on the Tracker: the Tickets that must be closed, or already built on the Feature's branch, before this one is built. On GitHub these are **blocked by** links; on Azure DevOps Boards, **Predecessor** links. Tickets are built in that order, then in the Feature's own child order (the Build order). On Azure DevOps Boards the child order is the backlog rank (Stack Rank or Backlog Priority), and for children nobody has ranked, the order they were linked; so the Build order there is Predecessor links, then backlog rank, then child-link order.
19
+ - Optionally, a **`**Model:**`** and an **`**Effort:**`** line, anywhere in its body, each on a line of its own:
20
+
21
+ ```markdown
22
+ **Model:** opus
23
+ **Effort:** high
24
+ ```
25
+
26
+ The first word after the colon is read, in any case; the bold is optional. A Ticket without them runs on `model` and `effort` from `factory.json`; `--model` and `--effort` on the command line win over both.
27
+
28
+ A Ticket is built only while it carries the ready Signal (`ready-for-agent` by default). Its Feature carries no Signal until a person assigns it to themselves and gives it the Go (`factory-go` by default).
29
+
30
+ ## A Feature's Workflow
31
+
32
+ A Feature may carry a **`**Workflow:**`** line anywhere in its body, on a line of its own, to add the Steps of a Workflow from `factory.json` to its build:
33
+
34
+ ```markdown
35
+ **Workflow:** design-review
36
+ ```
37
+
38
+ The rest of the line is the Workflow's name. Without the line the Feature gets the repo's `defaultWorkflow`, if it has one; a name no Workflow has marks the Feature as needing info. See the README's Workflows section.
39
+
40
+ ## The Feature file
41
+
42
+ `nightjar create FILE` makes a Feature and its Tickets on the Tracker `factory.json` names, from one Markdown file:
43
+
44
+ ```markdown
45
+ # Share one HTML escape
46
+
47
+ The spec: the problem, the solution, the decisions. Any Markdown, headings included.
48
+
49
+ ## Ticket 1: Move the escape into one module
50
+
51
+ **Model:** opus
52
+ **Effort:** high
53
+
54
+ What to build.
55
+
56
+ ### Acceptance criteria
57
+
58
+ - [ ] one module exports escapeHtml
59
+
60
+ ## Ticket 2: Use it in the comment renderer
61
+
62
+ **Blocked by:** 1
63
+
64
+ What to build.
65
+
66
+ ### Acceptance criteria
67
+
68
+ - [ ] comments escape through it
69
+ ```
70
+
71
+ - The first line is `# ` and the Feature's title. Everything up to the first Ticket is the Feature's body.
72
+ - Each Ticket starts with `## Ticket N: Title`, numbered 1, 2, 3 in the order they are to be built. A `## Ticket` line inside a fenced code block is text.
73
+ - `**Blocked by:** 1, 2` names Tickets above it by number. It becomes blocking links on the Tracker and leaves the body; a Ticket can only wait for Tickets above it, so the links never form a loop. Leave the line out, or write `none`, for a Ticket that waits for nothing.
74
+ - Every Ticket has acceptance criteria. `**Model:**` and `**Effort:**` lines, when there, have a value, and stay in the body as written.
75
+
76
+ `nightjar create --dry-run FILE` reads the file and says what it would make, making nothing. A file out of this shape is refused before anything is made, with every fault and its line.
77
+
78
+ `nightjar create` then makes, in order: the Feature, with no Signal; each Ticket as its last child, linked to its blockers and then given the ready Signal. It prints each with the Tracker's reference (`#12`, `AB#12`). If the Tracker fails part way, it names what it made before the fault and undoes nothing. It never assigns the Feature and never gives the Go: that stays with a person.
79
+
80
+ | Tracker | The Feature | Each Ticket | Blocking links |
81
+ | --------------------- | ------------------------------ | ------------------------------------------------- | ------------------- |
82
+ | GitHub Issues | an issue | an issue, made a sub-issue of the Feature | blocked by |
83
+ | Azure DevOps Boards | a work item of `featureType` | a child work item of `ticketType` | Predecessor |
84
+ | A module | `createFeature` | `createTicket` | `block` |
85
+
86
+ On Azure DevOps Boards, `factory.json` names the work item type of a Ticket as `tracker.ticketType` (such as `User Story`, `Product Backlog Item` or `Issue`); without it `nightjar create` refuses. Bodies are written as Markdown. A Tracker module makes Features when it has all three methods of `FeatureWriter` ([custom-adapters.md](custom-adapters.md)).
87
+
88
+ ## The skill
89
+
90
+ The package ships a Claude Code skill, `nightjar-feature`, that writes a Feature file from an agreed idea and makes it with `nightjar create`. It needs no other skill. To use it in a repo:
91
+
92
+ ```bash
93
+ mkdir -p .claude/skills
94
+ cp -R "$(npm root -g)/@nonameolsson/nightjar/skills/nightjar-feature" .claude/skills/
95
+ ```
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
- "name": "@nonameolsson/nightjar",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@nonameolsson/nightjar",
3
+ "version": "0.2.0",
4
+ "description": "Builds a repo's Features from its Tracker with nobody at the keyboard, one draft pull request per Feature.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/nonameolsson/nightjar.git"
9
+ },
10
+ "homepage": "https://github.com/nonameolsson/nightjar#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/nonameolsson/nightjar/issues"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "keywords": [
18
+ "claude-code",
19
+ "agent",
20
+ "automation",
21
+ "github",
22
+ "azure-devops"
23
+ ],
24
+ "type": "module",
25
+ "bin": {
26
+ "nightjar": "dist/cli.js"
27
+ },
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./schema/factory.schema.json": "./schema/factory.schema.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "!dist/**/*.spec.*",
38
+ "schema",
39
+ "skills",
40
+ "docs/ticket-contract.md",
41
+ "plugin/nightjar-status",
42
+ "!plugin/**/*.spec.ts",
43
+ "src/status-shape.ts"
44
+ ],
45
+ "engines": {
46
+ "node": ">=24"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.build.json",
50
+ "typecheck": "tsc --noEmit && tsc --noEmit -p plugin",
51
+ "lint": "eslint .",
52
+ "format": "prettier --write .",
53
+ "format:check": "prettier --check .",
54
+ "test": "vitest run",
55
+ "schema": "tsx scripts/schema.ts",
56
+ "leaks": "tsx scripts/leak-check.ts",
57
+ "pack:check": "tsx scripts/pack-install.ts",
58
+ "check": "npm run typecheck && npm run lint && npm run format:check && npm test && npm run build && npm run leaks && npm run pack:check",
59
+ "prepack": "npm run build"
60
+ },
61
+ "dependencies": {
62
+ "zod": "^4.6.7"
63
+ },
64
+ "devDependencies": {
65
+ "@eslint/js": "^10.0.1",
66
+ "@types/node": "^24",
67
+ "eslint": "^10.14.0",
68
+ "eslint-config-prettier": "^10.1.8",
69
+ "prettier": "^3.9.10",
70
+ "tsx": "^4.23.15",
71
+ "typescript": "^6.0.3",
72
+ "typescript-eslint": "^8.72.1",
73
+ "vitest": "^5.0.5"
74
+ }
75
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "nightjar-status",
3
+ "version": "0.1.0",
4
+ "description": "Nightjar in the Claude Code status line, and a toast when it builds a Ticket, opens a pull request or needs a person."
5
+ }
@@ -0,0 +1,48 @@
1
+ // What the status line and the toasts say, from .factory/status.json (shape: src/status-shape.ts).
2
+ import type { Event, Status } from '../../../src/status-shape.js';
3
+
4
+ const two = (n: number) => String(n).padStart(2, '0');
5
+ const clock = (iso: string) => {
6
+ const d = new Date(iso);
7
+ return `${two(d.getHours())}:${two(d.getMinutes())}`;
8
+ };
9
+ const minutes = (from: string, nowMs: number) =>
10
+ `${String(Math.max(0, Math.round((nowMs - Date.parse(from)) / 60_000)))}m`;
11
+ const sameDay = (iso: string, nowMs: number) =>
12
+ new Date(iso).toDateString() === new Date(nowMs).toDateString();
13
+
14
+ // The status line, or undefined when Nightjar is off.
15
+ export function line(status: Status, nowMs: number): string | undefined {
16
+ const said = what(status, nowMs);
17
+ if (said === undefined || !status.codeMoved) return said;
18
+ return `${said} · restart: main has new Nightjar code (${status.codeMoved})`;
19
+ }
20
+
21
+ function what(status: Status, nowMs: number): string | undefined {
22
+ const d = status.doing;
23
+ // the file keeps yesterday's spend until Nightjar writes again
24
+ const spent = sameDay(status.at, nowMs) ? status.spentTodayUsd : 0;
25
+ const today = `$${spent.toFixed(2)} today`;
26
+ if (d.kind === 'off') return undefined;
27
+ if (d.kind === 'idle') return `nightjar: idle, next poll ${clock(d.nextPollAt)} · ${today}`;
28
+ if (d.kind === 'paused') {
29
+ const until = d.until ? ` until ${clock(d.until)}` : '';
30
+ return `nightjar: paused${until}: ${d.why} · ${today}`;
31
+ }
32
+ const where = d.ticket === null ? `#${d.feature}` : `#${d.feature} › #${d.ticket}`;
33
+ const round =
34
+ d.round !== null && d.rounds !== null && (d.step === 'fix' || d.round > 1)
35
+ ? ` ${String(d.round)}/${String(d.rounds)}`
36
+ : '';
37
+ return `nightjar: ${where} ${d.step}${round} · ${minutes(d.startedAt, nowMs)} · ${today}`;
38
+ }
39
+
40
+ export function toast(e: Event): string {
41
+ if (e.kind === 'built') return `nightjar: #${e.ticket} built (#${e.feature})`;
42
+ if (e.kind === 'pull') return `nightjar: #${e.feature} draft pull request ${e.url}`;
43
+ if (e.kind === 'failed') return `nightjar: #${e.feature} failed: ${e.why}`;
44
+ if (e.kind === 'stale') {
45
+ return `nightjar: main has new Nightjar code (${e.commit}); restart it from an updated main`;
46
+ }
47
+ return `nightjar: #${e.ticket ?? e.feature} needs a person: ${e.why}`;
48
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.ts"] }
@@ -0,0 +1,51 @@
1
+ import type { EngineInterface, Register } from 'claude-code';
2
+ import type { Status } from '../../../src/status-shape.js';
3
+ import { line, toast } from './format';
4
+
5
+ const EVERY_MS = 5_000;
6
+
7
+ // undefined until the first read, which takes the events already there without toasting them
8
+ let seen: number | undefined;
9
+
10
+ // .factory/ is in the main checkout, the folder that holds the git common dir, worktree or not
11
+ async function statusFile($: EngineInterface): Promise<string | undefined> {
12
+ const git = await $.process.run([
13
+ 'git',
14
+ 'rev-parse',
15
+ '--path-format=absolute',
16
+ '--git-common-dir'
17
+ ]);
18
+ if (git.exitCode !== 0) return undefined;
19
+ return `${git.stdout.trim().replace(/\/\.git$/, '')}/.factory/status.json`;
20
+ }
21
+
22
+ async function tick($: EngineInterface, path: string): Promise<void> {
23
+ if (!(await $.fs.exists(path))) return $.ui.status(undefined);
24
+ let status: Status;
25
+ try {
26
+ status = JSON.parse(await $.fs.read(path)) as Status;
27
+ } catch {
28
+ // Nightjar renames its writes into place, so this is a file someone broke; wait for the next
29
+ return;
30
+ }
31
+ if (seen !== undefined) {
32
+ for (const e of status.recent) if (e.seq > seen) $.ui.toast(toast(e), { timeoutMs: 8_000 });
33
+ }
34
+ seen = status.recent.at(-1)?.seq ?? 0;
35
+ const isOff = status.doing.kind === 'off';
36
+ if (!isOff && (await $.process.run(['kill', '-0', String(status.pid)])).exitCode !== 0) {
37
+ return $.ui.status('nightjar: stopped without writing off');
38
+ }
39
+ $.ui.status(line(status, await $.clock.now()));
40
+ }
41
+
42
+ export const register: Register = (on) => {
43
+ on('session.start', async ($, e, next) => {
44
+ const done = await next(e);
45
+ const path = await statusFile($);
46
+ if (path === undefined) return done;
47
+ await tick($, path);
48
+ $.clock.every(EVERY_MS, () => void tick($, path));
49
+ return done;
50
+ });
51
+ };