forge-workflow 0.1.0-beta.2 → 0.1.0-beta.4

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 (155) hide show
  1. package/.forge/hooks/check-tdd.js +79 -5
  2. package/.forge/hooks/forge-native-hook.js +194 -8
  3. package/AGENTS.md +2 -1
  4. package/CHANGELOG.md +28 -0
  5. package/QUICKSTART.md +6 -2
  6. package/README.md +3 -1
  7. package/bin/forge-cmd.js +1 -1
  8. package/bin/forge.js +95 -19
  9. package/docs/guides/SETUP.md +4 -1
  10. package/docs/guides/SUPPORT.md +5 -0
  11. package/docs/reference/COMMANDS.md +12 -1
  12. package/docs/reference/shepherd.md +114 -3
  13. package/lib/activation/ensure-forge-home.js +135 -0
  14. package/lib/adapters/beads-kernel-compat.js +67 -0
  15. package/lib/adapters/greptile-review-adapter.js +1 -1
  16. package/lib/adapters/pr-state-adapter.js +103 -8
  17. package/lib/adoption-profiles.js +17 -4
  18. package/lib/agents-config.js +5 -0
  19. package/lib/beads-detect.js +60 -0
  20. package/lib/commands/_aliases.js +248 -0
  21. package/lib/commands/_issue.js +65 -41
  22. package/lib/commands/_manifest.js +3 -1
  23. package/lib/commands/_registry.js +14 -0
  24. package/lib/commands/_resolve-command-opts.js +36 -60
  25. package/lib/commands/claim.js +2 -4
  26. package/lib/commands/gate.js +19 -2
  27. package/lib/commands/hooks.js +294 -12
  28. package/lib/commands/init.js +26 -20
  29. package/lib/commands/memory.js +81 -0
  30. package/lib/commands/migrate.js +0 -161
  31. package/lib/commands/plan.js +71 -123
  32. package/lib/commands/pr.js +88 -0
  33. package/lib/commands/prime.js +8 -1
  34. package/lib/commands/push.js +66 -0
  35. package/lib/commands/recall.js +67 -12
  36. package/lib/commands/recap.js +18 -4
  37. package/lib/commands/release.js +15 -3
  38. package/lib/commands/remember.js +86 -20
  39. package/lib/commands/serve.js +5 -2
  40. package/lib/commands/setup.js +135 -73
  41. package/lib/commands/shepherd.js +102 -4
  42. package/lib/commands/ship.js +40 -4
  43. package/lib/commands/skill.js +275 -0
  44. package/lib/commands/status.js +37 -32
  45. package/lib/commands/test.js +32 -0
  46. package/lib/commands/worktree.js +87 -5
  47. package/lib/core/runtime-graph.js +122 -17
  48. package/lib/forge-issues.js +13 -464
  49. package/lib/gate-events.js +54 -55
  50. package/lib/global-flags.js +30 -0
  51. package/lib/grounding/context-events.js +230 -0
  52. package/lib/grounding/read-first.js +112 -0
  53. package/lib/harness-capability-matrix.js +2 -2
  54. package/lib/hook-renderer.js +144 -12
  55. package/lib/issue-backend.js +42 -3
  56. package/lib/kernel/backing-issue.js +7 -1
  57. package/lib/kernel/broker.js +28 -0
  58. package/lib/kernel/migrations.js +30 -2
  59. package/lib/kernel/owned-kernel.js +43 -0
  60. package/lib/kernel/schema.js +35 -0
  61. package/lib/kernel/sqlite-driver.js +187 -1
  62. package/lib/memory-recall.js +151 -0
  63. package/lib/orientation.js +303 -6
  64. package/lib/pr-bundle.js +6 -2
  65. package/lib/pr-monitor/auto-actions.js +175 -0
  66. package/lib/pr-monitor/digest.js +206 -0
  67. package/lib/pr-monitor/reconcile-executor.js +659 -0
  68. package/lib/pr-monitor/reconcile-tick.js +138 -0
  69. package/lib/pr-monitor/reconcile.js +0 -0
  70. package/lib/pr-monitor/render-sticky.js +57 -8
  71. package/lib/pr-monitor/shepherd-lease.js +243 -0
  72. package/lib/pr-monitor/upsert-sticky.js +169 -0
  73. package/lib/pr-monitor/watch-lifecycle.js +1 -1
  74. package/lib/pr-pull.js +113 -17
  75. package/lib/project-memory.js +8 -0
  76. package/lib/release-readiness.js +17 -1
  77. package/lib/rules-sync.js +4 -0
  78. package/lib/runtime-health.js +15 -46
  79. package/lib/skill-eval.js +750 -0
  80. package/lib/status/identity.js +46 -0
  81. package/lib/status/presenter.js +0 -35
  82. package/lib/status/snapshot.js +11 -16
  83. package/lib/upgrade-safety.js +52 -1
  84. package/lib/using-forge.js +315 -0
  85. package/lib/workflow/enforce-stage.js +64 -7
  86. package/lib/workflow/state-manager.js +23 -23
  87. package/package.json +2 -2
  88. package/rules/using-forge.md +24 -0
  89. package/scripts/forge-team/index.sh +0 -5
  90. package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
  91. package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
  92. package/scripts/pr-auto-actions.js +93 -0
  93. package/scripts/pr-verdict-label.js +50 -0
  94. package/scripts/test.js +8 -1
  95. package/skills/claim-safety/SKILL.md +4 -0
  96. package/skills/claim-safety/evals/scorecard.json +41 -0
  97. package/skills/coverage.json +83 -0
  98. package/skills/dev/SKILL.md +4 -0
  99. package/skills/dev/evals/scorecard.json +41 -0
  100. package/skills/gates/SKILL.md +80 -0
  101. package/skills/gates/evals/evals.json +38 -0
  102. package/skills/gates/evals/scorecard.json +41 -0
  103. package/skills/hermes-forge/SKILL.md +1 -0
  104. package/skills/hermes-forge/evals/scorecard.json +41 -0
  105. package/skills/issue-basics/SKILL.md +1 -0
  106. package/skills/issue-basics/evals/scorecard.json +41 -0
  107. package/skills/kernel/SKILL.md +38 -0
  108. package/skills/kernel/evals/scorecard.json +41 -0
  109. package/skills/memory/SKILL.md +16 -1
  110. package/skills/memory/evals/scorecard.json +41 -0
  111. package/skills/parallel-deep-research/SKILL.md +1 -0
  112. package/skills/parallel-deep-research/evals/scorecard.json +41 -0
  113. package/skills/plan/SKILL.md +6 -0
  114. package/skills/plan/evals/scorecard.json +41 -0
  115. package/skills/portability/SKILL.md +47 -0
  116. package/skills/portability/evals/evals.json +34 -0
  117. package/skills/portability/evals/scorecard.json +41 -0
  118. package/skills/research/SKILL.md +1 -0
  119. package/skills/research/evals/scorecard.json +41 -0
  120. package/skills/review/SKILL.md +6 -0
  121. package/skills/review/evals/scorecard.json +41 -0
  122. package/skills/rollback/SKILL.md +1 -0
  123. package/skills/rollback/evals/scorecard.json +41 -0
  124. package/skills/setup/SKILL.md +91 -0
  125. package/skills/setup/evals/evals.json +42 -0
  126. package/skills/setup/evals/scorecard.json +41 -0
  127. package/skills/shepherd/SKILL.md +76 -36
  128. package/skills/shepherd/evals/evals.json +21 -9
  129. package/skills/shepherd/evals/scorecard.json +41 -0
  130. package/skills/ship/SKILL.md +6 -0
  131. package/skills/ship/evals/scorecard.json +41 -0
  132. package/skills/smith/SKILL.md +8 -0
  133. package/skills/smith/evals/scorecard.json +41 -0
  134. package/skills/sonarcloud/SKILL.md +1 -0
  135. package/skills/sonarcloud/evals/scorecard.json +41 -0
  136. package/skills/sonarcloud-analysis/SKILL.md +1 -0
  137. package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
  138. package/skills/status/SKILL.md +3 -0
  139. package/skills/status/evals/scorecard.json +41 -0
  140. package/skills/triage-ready/SKILL.md +2 -0
  141. package/skills/triage-ready/evals/scorecard.json +41 -0
  142. package/skills/using-forge/SKILL.md +104 -0
  143. package/skills/using-forge/evals/scorecard.json +41 -0
  144. package/skills/validate/SKILL.md +4 -0
  145. package/skills/validate/evals/scorecard.json +41 -0
  146. package/skills/verify/SKILL.md +4 -0
  147. package/skills/verify/evals/scorecard.json +41 -0
  148. package/skills/worktree/SKILL.md +87 -0
  149. package/skills/worktree/evals/evals.json +38 -0
  150. package/skills/worktree/evals/scorecard.json +41 -0
  151. package/lib/adapters/beads-issue-adapter.js +0 -127
  152. package/lib/commands/board.js +0 -64
  153. package/lib/status/beads-snapshot.js +0 -145
  154. package/scripts/forge-team/lib/dashboard.sh +0 -316
  155. package/scripts/forge-team/tests/dashboard.test.sh +0 -155
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: worktree
3
+ description: >
4
+ Forge's worktree lifecycle. `forge worktree create <slug>` makes an isolated checkout
5
+ under `.worktrees/`, creates its branch (feat/<slug>), links a kernel issue (`--issue`),
6
+ and installs dependencies — all in one step; `forge worktree list`/`remove <slug>` manage
7
+ them; `forge clean` removes worktrees whose branches already merged (squash-aware) and
8
+ fast-forwards the default branch. Use when the user says "work on this in an isolated
9
+ branch/worktree", "spin up a worktree", "parallel work on another PR or issue", "clean up
10
+ merged worktrees", or "why did my worktree miss its dependencies". Footgun it prevents:
11
+ raw `git worktree add` only creates a branch — it skips the issue link and dep install —
12
+ so always use `forge worktree create`; and `forge clean` is cwd-scoped, run it from the
13
+ primary repo root, never inside a worktree. NOT for opening/pushing a PR or the branch
14
+ (ship), NOT for planning a feature (plan), NOT the stages that create a worktree as one
15
+ step — this is the worktree lifecycle itself.
16
+ allowed-tools: Bash, Read, Grep, Glob
17
+ terminal: true
18
+ ---
19
+
20
+ Forge worktrees give a task its own isolated checkout so concurrent work never collides on `HEAD` or a shared branch. Use `forge worktree`, not raw `git worktree add` — the Forge command does the extra setup that a bare branch add skips.
21
+
22
+ # Worktree lifecycle
23
+
24
+ ## When to use
25
+
26
+ - "Work on this in an isolated branch / worktree", "spin up a worktree for this".
27
+ - Parallel work: a second PR or issue you want to develop without disturbing the current checkout.
28
+ - "Clean up the merged worktrees" — reclaim worktrees whose branches already landed.
29
+ - Diagnosing "why did my new worktree miss its dependencies / node_modules".
30
+
31
+ This skill is the **worktree lifecycle itself**. The `plan`/`ship` stages may create a worktree as one internal step, but managing worktrees directly — create, list, remove, clean — is this skill.
32
+
33
+ ## Create
34
+
35
+ ```bash
36
+ forge worktree create <slug>
37
+ ```
38
+
39
+ Creates the worktree at `.worktrees/<slug>` and, in one step:
40
+
41
+ - creates its branch (default `feat/<slug>`),
42
+ - links a kernel issue to the worktree when `--issue <id>` is given (records issue → worktree),
43
+ - installs dependencies with the repo's detected package manager (bun/pnpm/yarn/npm).
44
+
45
+ Flags:
46
+
47
+ | Flag | Purpose |
48
+ | --- | --- |
49
+ | `--branch <name>` | Custom branch name (default `feat/<slug>`). |
50
+ | `--base <ref>` | Base ref the new branch forks from (default the repo default branch, e.g. `origin/main`). |
51
+ | `--issue <id>` | Kernel issue id to link this worktree to. |
52
+ | `--work-folder <path>` | Repo-relative work-folder this issue owns (drops a `.forge-issue` marker). |
53
+
54
+ The kernel issue store lives in the git common dir that every worktree shares, so a new worktree already sees the same kernel — no per-worktree issue-store bootstrap is needed.
55
+
56
+ ## The footgun this skill prevents
57
+
58
+ **Never use raw `git worktree add`** for Forge work. It only creates a branch + checkout — it does **not** link the kernel issue and does **not** install dependencies. A worktree made that way is why deps end up missing. Always use `forge worktree create`.
59
+
60
+ ## List / remove
61
+
62
+ ```bash
63
+ forge worktree list # read the worktree registry from the kernel
64
+ forge worktree remove <slug> # git worktree remove for that slug
65
+ ```
66
+
67
+ ## Clean merged worktrees
68
+
69
+ ```bash
70
+ forge clean # remove worktrees whose branches merged; FF the default branch
71
+ forge clean --dry-run # show what would be removed, change nothing
72
+ ```
73
+
74
+ `forge clean` is **squash-aware** — it detects branches that merged via squash, not just fast-forward merges.
75
+
76
+ ### `forge clean` footguns
77
+
78
+ - **cwd-scoped.** Run it from the **primary repo root**, never from inside a `.worktrees/<slug>` checkout. There is no default-branch guard, so a bare run from the wrong place can remove a clean checkout.
79
+ - **Fast-forward the local default branch first** so squash-merge detection sees the merged state.
80
+ - **Windows long paths.** `node_modules` inside a worktree can exceed the Windows path limit; if `git worktree remove --force` de-registers the worktree but the directory lingers, delete it with a PowerShell extended-path (`\\?\`) remove.
81
+
82
+ ## Adjacent skills
83
+
84
+ - Opening or pushing a PR / the branch → `ship`.
85
+ - Planning a feature (which may create a worktree as a step) → `plan`.
86
+ - Watching an open PR toward merge → `shepherd`.
87
+ - Everyday issue create/update/close → `issue-basics`.
@@ -0,0 +1,38 @@
1
+ [
2
+ {
3
+ "query": "Spin up an isolated worktree so I can work on this branch separately.",
4
+ "should_trigger": true
5
+ },
6
+ {
7
+ "query": "Create a worktree for this issue with its branch and dependencies installed.",
8
+ "should_trigger": true
9
+ },
10
+ {
11
+ "query": "I want to work on another PR in parallel without disturbing my current checkout.",
12
+ "should_trigger": true
13
+ },
14
+ {
15
+ "query": "Clean up all the merged worktrees and fast-forward main.",
16
+ "should_trigger": true
17
+ },
18
+ {
19
+ "query": "Why did my new worktree miss its node_modules dependencies?",
20
+ "should_trigger": true
21
+ },
22
+ {
23
+ "query": "Set up an isolated branch checkout under .worktrees for this task.",
24
+ "should_trigger": true
25
+ },
26
+ {
27
+ "query": "Open a pull request and push my feature branch.",
28
+ "should_trigger": false
29
+ },
30
+ {
31
+ "query": "Plan this new feature and break it into a task list.",
32
+ "should_trigger": false
33
+ },
34
+ {
35
+ "query": "Watch my PR and tell me why it isn't merging.",
36
+ "should_trigger": false
37
+ }
38
+ ]
@@ -0,0 +1,41 @@
1
+ {
2
+ "skill": "worktree",
3
+ "fixtures": "present",
4
+ "static": {
5
+ "token_cost": {
6
+ "desc_chars": 1013,
7
+ "body_lines": 70,
8
+ "score": 44
9
+ },
10
+ "caps": {
11
+ "desc_within": true,
12
+ "body_within": true,
13
+ "score": 100
14
+ },
15
+ "description_quality": {
16
+ "has_trigger_cues": true,
17
+ "has_disambiguation_cues": true,
18
+ "adequate_length": true,
19
+ "score": 100
20
+ }
21
+ },
22
+ "router_reachability": {
23
+ "has_curated_rule": true,
24
+ "router_exempt": false,
25
+ "fixtures": "present",
26
+ "fixtures_total": 6,
27
+ "fixtures_best_hit": 6,
28
+ "reachable": true,
29
+ "keyword_alignment": 1
30
+ },
31
+ "behavioral": {
32
+ "trigger_recall": null,
33
+ "trigger_precision": null,
34
+ "disambiguation": null,
35
+ "chain_correctness": null,
36
+ "outcome_quality": null,
37
+ "variance": null,
38
+ "note": "behavioral — W5 (LLM judge): semantic recall/precision/chain/outcome/variance, not deterministic"
39
+ },
40
+ "composite": 83
41
+ }
@@ -1,127 +0,0 @@
1
- 'use strict';
2
-
3
- const {
4
- IssueAdapter,
5
- decideIssueAuthority,
6
- normalizeIssueStatus,
7
- } = require('../issue-adapter.js');
8
-
9
- class BeadsIssueAdapter extends IssueAdapter {
10
- constructor(options = {}) {
11
- super({
12
- id: options.id || 'beads',
13
- kind: 'issue',
14
- name: options.name || 'Beads Issue Adapter',
15
- version: options.version || '0.1.0',
16
- });
17
- this.runBeadsOperation = options.runBeadsOperation;
18
- }
19
-
20
- run(operation, args = [], context = {}) {
21
- if (typeof this.runBeadsOperation !== 'function') {
22
- throw new TypeError('runBeadsOperation is not configured on this adapter');
23
- }
24
-
25
- return this.runBeadsOperation(operation, args, context, context.deps || {});
26
- }
27
-
28
- list(args = [], context = {}) {
29
- return this.run('list', args, context);
30
- }
31
-
32
- ready(args = [], context = {}) {
33
- return this.run('ready', args, context);
34
- }
35
-
36
- read(args = [], context = {}) {
37
- return this.run('show', args, context);
38
- }
39
-
40
- show(args = [], context = {}) {
41
- return this.read(args, context);
42
- }
43
-
44
- create(args = [], context = {}) {
45
- return this.run('create', args, context);
46
- }
47
-
48
- search(args = [], context = {}) {
49
- return this.run('search', args, context);
50
- }
51
-
52
- stats(args = [], context = {}) {
53
- return this.run('stats', args, context);
54
- }
55
-
56
- update(args = [], context = {}) {
57
- return this.run('update', args, context);
58
- }
59
-
60
- close(args = [], context = {}) {
61
- return this.run('close', args, context);
62
- }
63
-
64
- comment(args = [], context = {}) {
65
- return this.run('comment', args, context);
66
- }
67
-
68
- depAdd(args = [], context = {}) {
69
- return this.run('dep.add', args, context);
70
- }
71
-
72
- depRemove(args = [], context = {}) {
73
- return this.run('dep.remove', args, context);
74
- }
75
-
76
- // De-bead parity: claim/release and the KAP-7/KAP-12 derived reads now route
77
- // through the backend abstraction (the translations moved out of _issue.js). The
78
- // beads layer maps claim -> `update <id> --claim`; release has no bd equivalent
79
- // and returns the Kernel-only contract error (handled in runBeadsOperation).
80
- claim(args = [], context = {}) {
81
- return this.run('claim', args, context);
82
- }
83
-
84
- release(args = [], context = {}) {
85
- return this.run('release', args, context);
86
- }
87
-
88
- blocked(args = [], context = {}) {
89
- return this.run('blocked', args, context);
90
- }
91
-
92
- stale(args = [], context = {}) {
93
- return this.run('stale', args, context);
94
- }
95
-
96
- orphans(args = [], context = {}) {
97
- return this.run('orphans', args, context);
98
- }
99
-
100
- lint(args = [], context = {}) {
101
- return this.run('lint', args, context);
102
- }
103
-
104
- // Lease ownership is Kernel-only; the beads layer returns the Kernel-only contract
105
- // error (handled in runBeadsOperation), matching the release passthrough.
106
- owns(args = [], context = {}) {
107
- return this.run('owns', args, context);
108
- }
109
-
110
- // Active-lease listing is Kernel-only (kernel_claims); the beads layer returns the
111
- // Kernel-only contract error (handled in runBeadsOperation), matching owns/release.
112
- claims(args = [], context = {}) {
113
- return this.run('claims', args, context);
114
- }
115
-
116
- mapStatus(status, context = {}) {
117
- return normalizeIssueStatus(status, context);
118
- }
119
-
120
- decideAuthority(change = {}, context = {}) {
121
- return decideIssueAuthority(change, context);
122
- }
123
- }
124
-
125
- module.exports = {
126
- BeadsIssueAdapter,
127
- };
@@ -1,64 +0,0 @@
1
- 'use strict';
2
-
3
- const { readBeadsSnapshot } = require('../status/beads-snapshot');
4
- const { buildBoardJson, formatBoard } = require('../status/presenter');
5
- const { detectRepoContext } = require('./status');
6
-
7
- function hasJsonFlag(args = [], flags = {}) {
8
- return flags.json === true || flags['--json'] === true || args.includes('--json');
9
- }
10
-
11
- function getFlagValue(args, dashedName, camelName) {
12
- const inlineDashed = args.find(arg => typeof arg === 'string' && arg.startsWith(`${dashedName}=`));
13
- if (inlineDashed) {
14
- return inlineDashed.slice(dashedName.length + 1);
15
- }
16
- const inlineCamel = args.find(arg => typeof arg === 'string' && arg.startsWith(`${camelName}=`));
17
- if (inlineCamel) {
18
- return inlineCamel.slice(camelName.length + 1);
19
- }
20
- const dashedIndex = args.indexOf(dashedName);
21
- if (dashedIndex !== -1 && dashedIndex + 1 < args.length) {
22
- const candidate = args[dashedIndex + 1];
23
- return typeof candidate === 'string' && candidate.startsWith('--') ? null : candidate;
24
- }
25
- const camelIndex = args.indexOf(camelName);
26
- if (camelIndex !== -1 && camelIndex + 1 < args.length) {
27
- const candidate = args[camelIndex + 1];
28
- return typeof candidate === 'string' && candidate.startsWith('--') ? null : candidate;
29
- }
30
- return null;
31
- }
32
-
33
- async function handler(args = [], flags = {}, projectRoot = process.cwd()) {
34
- const firstDefined = (...values) => values.find(value => value !== undefined && value !== null);
35
- const context = detectRepoContext(projectRoot);
36
- const snapshot = readBeadsSnapshot(projectRoot, {
37
- now: firstDefined(flags.now, flags['--now'], getFlagValue(args, '--now', '--now')),
38
- staleAfterDays: firstDefined(
39
- flags.staleAfterDays,
40
- flags['--stale-after-days'],
41
- flags['--staleAfterDays'],
42
- getFlagValue(args, '--stale-after-days', '--staleAfterDays')
43
- ),
44
- });
45
- const payload = { context, snapshot };
46
- const output = hasJsonFlag(args, flags)
47
- ? JSON.stringify(buildBoardJson(payload), null, 2)
48
- : formatBoard(payload);
49
-
50
- return {
51
- success: true,
52
- context,
53
- snapshot,
54
- output,
55
- };
56
- }
57
-
58
- module.exports = {
59
- name: 'board',
60
- description: 'Show a local team runtime board from kernel state',
61
- handler,
62
- getFlagValue,
63
- hasJsonFlag,
64
- };
@@ -1,145 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { secureExecFileSync } = require('../shell-utils.js');
6
-
7
- function getGitConfig(projectRoot, key) {
8
- try {
9
- return secureExecFileSync('git', ['config', key], {
10
- encoding: 'utf8',
11
- cwd: projectRoot,
12
- stdio: ['pipe', 'pipe', 'pipe'],
13
- }).trim();
14
- } catch (_error) {
15
- return '';
16
- }
17
- }
18
-
19
- function getDeveloperIdentity(projectRoot) {
20
- return {
21
- email: getGitConfig(projectRoot, 'user.email'),
22
- name: getGitConfig(projectRoot, 'user.name'),
23
- };
24
- }
25
-
26
- function normalizeEmail(value) {
27
- return typeof value === 'string' ? value.trim().toLowerCase() : '';
28
- }
29
-
30
- function readIssues(projectRoot) {
31
- const issuesPath = path.join(projectRoot, '.beads', 'issues.jsonl');
32
- if (!fs.existsSync(issuesPath)) {
33
- return [];
34
- }
35
-
36
- const raw = fs.readFileSync(issuesPath, 'utf8');
37
- const latestById = new Map();
38
-
39
- for (const line of raw.split(/\r?\n/)) {
40
- if (!line.trim()) {
41
- continue;
42
- }
43
-
44
- try {
45
- const issue = JSON.parse(line);
46
- if (issue && issue.id) {
47
- latestById.set(issue.id, issue);
48
- }
49
- } catch (_error) {
50
- // Ignore malformed rows so one bad append does not break status.
51
- }
52
- }
53
-
54
- return Array.from(latestById.values());
55
- }
56
-
57
- function isAssignedToDeveloper(issue, developer) {
58
- if (!issue || !developer) {
59
- return false;
60
- }
61
-
62
- if (issue.owner) {
63
- const developerEmail = normalizeEmail(developer.email);
64
- return developerEmail !== '' && normalizeEmail(issue.owner) === developerEmail;
65
- }
66
-
67
- return developer.name !== '' && issue.created_by === developer.name;
68
- }
69
-
70
- const DEFAULT_STALE_AFTER_DAYS = 14;
71
-
72
- function parseTimestampOrZero(value) {
73
- if (!value) {
74
- return 0;
75
- }
76
-
77
- const parsed = Date.parse(value);
78
- return Number.isNaN(parsed) ? 0 : parsed;
79
- }
80
-
81
- function sortByUpdatedAtDesc(left, right) {
82
- const leftTime = parseTimestampOrZero(left.updated_at);
83
- const rightTime = parseTimestampOrZero(right.updated_at);
84
- return rightTime - leftTime;
85
- }
86
-
87
- function hasUnresolvedDependencies(issue) {
88
- return Number(issue.dependency_count || 0) > 0;
89
- }
90
-
91
- function isOpenIssue(issue) {
92
- return issue.status === 'open' || issue.status === 'in_progress';
93
- }
94
-
95
- function isStale(issue, now, staleAfterDays) {
96
- if (!isOpenIssue(issue)) {
97
- return false;
98
- }
99
-
100
- const updatedAt = parseTimestampOrZero(issue.updated_at);
101
- if (updatedAt === 0) {
102
- return false;
103
- }
104
-
105
- const staleMs = staleAfterDays * 24 * 60 * 60 * 1000;
106
- return now.getTime() - updatedAt >= staleMs;
107
- }
108
-
109
- function readBeadsSnapshot(projectRoot, options = {}) {
110
- const developer = getDeveloperIdentity(projectRoot);
111
- const issues = readIssues(projectRoot);
112
- const parsedNow = options.now instanceof Date
113
- ? options.now
114
- : (typeof options.now === 'string' ? new Date(options.now) : null);
115
- const now = parsedNow instanceof Date && !Number.isNaN(parsedNow.getTime())
116
- ? parsedNow
117
- : new Date();
118
- const parsedStaleAfterDays = Number(options.staleAfterDays);
119
- const staleAfterDays = Number.isFinite(parsedStaleAfterDays)
120
- ? parsedStaleAfterDays
121
- : DEFAULT_STALE_AFTER_DAYS;
122
-
123
- return {
124
- developer,
125
- issues,
126
- active: issues.filter(issue => issue.status === 'in_progress').sort(sortByUpdatedAtDesc),
127
- activeAssigned: issues.filter(issue => issue.status === 'in_progress' && isAssignedToDeveloper(issue, developer)),
128
- ready: issues.filter(issue => issue.status === 'open' && !hasUnresolvedDependencies(issue)),
129
- blocked: issues.filter(issue => isOpenIssue(issue) && hasUnresolvedDependencies(issue)).sort(sortByUpdatedAtDesc),
130
- stale: issues.filter(issue => isStale(issue, now, staleAfterDays)).sort(sortByUpdatedAtDesc),
131
- recentCompleted: issues.filter(issue => issue.status === 'closed').sort(sortByUpdatedAtDesc),
132
- limits: [
133
- 'Uses local Beads issues.jsonl only.',
134
- 'Does not read GitHub review, CI, project, or sync freshness state.',
135
- ],
136
- };
137
- }
138
-
139
- module.exports = {
140
- getDeveloperIdentity,
141
- isAssignedToDeveloper,
142
- readBeadsSnapshot,
143
- hasUnresolvedDependencies,
144
- isStale,
145
- };