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.
- package/.forge/hooks/check-tdd.js +79 -5
- package/.forge/hooks/forge-native-hook.js +194 -8
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +28 -0
- package/QUICKSTART.md +6 -2
- package/README.md +3 -1
- package/bin/forge-cmd.js +1 -1
- package/bin/forge.js +95 -19
- package/docs/guides/SETUP.md +4 -1
- package/docs/guides/SUPPORT.md +5 -0
- package/docs/reference/COMMANDS.md +12 -1
- package/docs/reference/shepherd.md +114 -3
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapters/beads-kernel-compat.js +67 -0
- package/lib/adapters/greptile-review-adapter.js +1 -1
- package/lib/adapters/pr-state-adapter.js +103 -8
- package/lib/adoption-profiles.js +17 -4
- package/lib/agents-config.js +5 -0
- package/lib/beads-detect.js +60 -0
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +65 -41
- package/lib/commands/_manifest.js +3 -1
- package/lib/commands/_registry.js +14 -0
- package/lib/commands/_resolve-command-opts.js +36 -60
- package/lib/commands/claim.js +2 -4
- package/lib/commands/gate.js +19 -2
- package/lib/commands/hooks.js +294 -12
- package/lib/commands/init.js +26 -20
- package/lib/commands/memory.js +81 -0
- package/lib/commands/migrate.js +0 -161
- package/lib/commands/plan.js +71 -123
- package/lib/commands/pr.js +88 -0
- package/lib/commands/prime.js +8 -1
- package/lib/commands/push.js +66 -0
- package/lib/commands/recall.js +67 -12
- package/lib/commands/recap.js +18 -4
- package/lib/commands/release.js +15 -3
- package/lib/commands/remember.js +86 -20
- package/lib/commands/serve.js +5 -2
- package/lib/commands/setup.js +135 -73
- package/lib/commands/shepherd.js +102 -4
- package/lib/commands/ship.js +40 -4
- package/lib/commands/skill.js +275 -0
- package/lib/commands/status.js +37 -32
- package/lib/commands/test.js +32 -0
- package/lib/commands/worktree.js +87 -5
- package/lib/core/runtime-graph.js +122 -17
- package/lib/forge-issues.js +13 -464
- package/lib/gate-events.js +54 -55
- package/lib/global-flags.js +30 -0
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +2 -2
- package/lib/hook-renderer.js +144 -12
- package/lib/issue-backend.js +42 -3
- package/lib/kernel/backing-issue.js +7 -1
- package/lib/kernel/broker.js +28 -0
- package/lib/kernel/migrations.js +30 -2
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/schema.js +35 -0
- package/lib/kernel/sqlite-driver.js +187 -1
- package/lib/memory-recall.js +151 -0
- package/lib/orientation.js +303 -6
- package/lib/pr-bundle.js +6 -2
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/reconcile-executor.js +659 -0
- package/lib/pr-monitor/reconcile-tick.js +138 -0
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-sticky.js +57 -8
- package/lib/pr-monitor/shepherd-lease.js +243 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +1 -1
- package/lib/pr-pull.js +113 -17
- package/lib/project-memory.js +8 -0
- package/lib/release-readiness.js +17 -1
- package/lib/rules-sync.js +4 -0
- package/lib/runtime-health.js +15 -46
- package/lib/skill-eval.js +750 -0
- package/lib/status/identity.js +46 -0
- package/lib/status/presenter.js +0 -35
- package/lib/status/snapshot.js +11 -16
- package/lib/upgrade-safety.js +52 -1
- package/lib/using-forge.js +315 -0
- package/lib/workflow/enforce-stage.js +64 -7
- package/lib/workflow/state-manager.js +23 -23
- package/package.json +2 -2
- package/rules/using-forge.md +24 -0
- package/scripts/forge-team/index.sh +0 -5
- package/scripts/forge-team/tests/dispatcher.test.sh +1 -1
- package/scripts/forge-team/tests/workflow-integration.test.sh +0 -1
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/test.js +8 -1
- package/skills/claim-safety/SKILL.md +4 -0
- package/skills/claim-safety/evals/scorecard.json +41 -0
- package/skills/coverage.json +83 -0
- package/skills/dev/SKILL.md +4 -0
- package/skills/dev/evals/scorecard.json +41 -0
- package/skills/gates/SKILL.md +80 -0
- package/skills/gates/evals/evals.json +38 -0
- package/skills/gates/evals/scorecard.json +41 -0
- package/skills/hermes-forge/SKILL.md +1 -0
- package/skills/hermes-forge/evals/scorecard.json +41 -0
- package/skills/issue-basics/SKILL.md +1 -0
- package/skills/issue-basics/evals/scorecard.json +41 -0
- package/skills/kernel/SKILL.md +38 -0
- package/skills/kernel/evals/scorecard.json +41 -0
- package/skills/memory/SKILL.md +16 -1
- package/skills/memory/evals/scorecard.json +41 -0
- package/skills/parallel-deep-research/SKILL.md +1 -0
- package/skills/parallel-deep-research/evals/scorecard.json +41 -0
- package/skills/plan/SKILL.md +6 -0
- package/skills/plan/evals/scorecard.json +41 -0
- package/skills/portability/SKILL.md +47 -0
- package/skills/portability/evals/evals.json +34 -0
- package/skills/portability/evals/scorecard.json +41 -0
- package/skills/research/SKILL.md +1 -0
- package/skills/research/evals/scorecard.json +41 -0
- package/skills/review/SKILL.md +6 -0
- package/skills/review/evals/scorecard.json +41 -0
- package/skills/rollback/SKILL.md +1 -0
- package/skills/rollback/evals/scorecard.json +41 -0
- package/skills/setup/SKILL.md +91 -0
- package/skills/setup/evals/evals.json +42 -0
- package/skills/setup/evals/scorecard.json +41 -0
- package/skills/shepherd/SKILL.md +76 -36
- package/skills/shepherd/evals/evals.json +21 -9
- package/skills/shepherd/evals/scorecard.json +41 -0
- package/skills/ship/SKILL.md +6 -0
- package/skills/ship/evals/scorecard.json +41 -0
- package/skills/smith/SKILL.md +8 -0
- package/skills/smith/evals/scorecard.json +41 -0
- package/skills/sonarcloud/SKILL.md +1 -0
- package/skills/sonarcloud/evals/scorecard.json +41 -0
- package/skills/sonarcloud-analysis/SKILL.md +1 -0
- package/skills/sonarcloud-analysis/evals/scorecard.json +41 -0
- package/skills/status/SKILL.md +3 -0
- package/skills/status/evals/scorecard.json +41 -0
- package/skills/triage-ready/SKILL.md +2 -0
- package/skills/triage-ready/evals/scorecard.json +41 -0
- package/skills/using-forge/SKILL.md +104 -0
- package/skills/using-forge/evals/scorecard.json +41 -0
- package/skills/validate/SKILL.md +4 -0
- package/skills/validate/evals/scorecard.json +41 -0
- package/skills/verify/SKILL.md +4 -0
- package/skills/verify/evals/scorecard.json +41 -0
- package/skills/worktree/SKILL.md +87 -0
- package/skills/worktree/evals/evals.json +38 -0
- package/skills/worktree/evals/scorecard.json +41 -0
- package/lib/adapters/beads-issue-adapter.js +0 -127
- package/lib/commands/board.js +0 -64
- package/lib/status/beads-snapshot.js +0 -145
- package/scripts/forge-team/lib/dashboard.sh +0 -316
- 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
|
-
};
|
package/lib/commands/board.js
DELETED
|
@@ -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
|
-
};
|