@sienklogic/plan-build-run 2.38.0 → 2.39.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.
- package/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/plugins/copilot-pbr/agents/executor.agent.md +13 -0
- package/plugins/copilot-pbr/plugin.json +1 -1
- package/plugins/copilot-pbr/references/config-reference.md +22 -0
- package/plugins/copilot-pbr/references/git-integration.md +30 -0
- package/plugins/copilot-pbr/references/plan-authoring.md +28 -0
- package/plugins/copilot-pbr/references/plan-format.md +4 -0
- package/plugins/copilot-pbr/skills/begin/SKILL.md +22 -0
- package/plugins/copilot-pbr/skills/build/SKILL.md +45 -0
- package/plugins/copilot-pbr/skills/explore/SKILL.md +17 -0
- package/plugins/copilot-pbr/skills/milestone/SKILL.md +38 -0
- package/plugins/copilot-pbr/templates/pr-body.md.tmpl +22 -0
- package/plugins/cursor-pbr/.cursor-plugin/plugin.json +1 -1
- package/plugins/cursor-pbr/agents/executor.md +13 -0
- package/plugins/cursor-pbr/references/config-reference.md +22 -0
- package/plugins/cursor-pbr/references/git-integration.md +30 -0
- package/plugins/cursor-pbr/references/plan-authoring.md +28 -0
- package/plugins/cursor-pbr/references/plan-format.md +4 -0
- package/plugins/cursor-pbr/skills/begin/SKILL.md +22 -0
- package/plugins/cursor-pbr/skills/build/SKILL.md +45 -0
- package/plugins/cursor-pbr/skills/explore/SKILL.md +17 -0
- package/plugins/cursor-pbr/skills/milestone/SKILL.md +38 -0
- package/plugins/cursor-pbr/templates/pr-body.md.tmpl +22 -0
- package/plugins/pbr/.claude-plugin/plugin.json +1 -1
- package/plugins/pbr/agents/executor.md +13 -0
- package/plugins/pbr/references/config-reference.md +22 -0
- package/plugins/pbr/references/git-integration.md +30 -0
- package/plugins/pbr/references/plan-authoring.md +28 -0
- package/plugins/pbr/references/plan-format.md +4 -0
- package/plugins/pbr/scripts/lib/learnings.js +312 -0
- package/plugins/pbr/scripts/milestone-learnings.js +283 -0
- package/plugins/pbr/scripts/pbr-tools.js +43 -1
- package/plugins/pbr/scripts/progress-tracker.js +24 -1
- package/plugins/pbr/skills/begin/SKILL.md +23 -0
- package/plugins/pbr/skills/build/SKILL.md +45 -0
- package/plugins/pbr/skills/explore/SKILL.md +16 -0
- package/plugins/pbr/skills/milestone/SKILL.md +38 -0
- package/plugins/pbr/skills/plan/SKILL.md +23 -0
- package/plugins/pbr/templates/pr-body.md.tmpl +22 -0
|
@@ -615,6 +615,26 @@ Resume at: Task {N+1} (or re-execute checkpoint task with user's answer)
|
|
|
615
615
|
Continue execution from the checkpoint. Skip completed tasks. Process the checkpoint resolution, then continue with remaining tasks. Write SUMMARY.md when done.
|
|
616
616
|
```
|
|
617
617
|
|
|
618
|
+
#### 6e-ii. CI Gate (after wave completion, conditional)
|
|
619
|
+
|
|
620
|
+
If `config.ci.gate_enabled` is `true` AND `config.git.branching` is not `none`:
|
|
621
|
+
|
|
622
|
+
1. Push current commits: `git push`
|
|
623
|
+
2. Wait 5 seconds for CI to trigger
|
|
624
|
+
3. Check: `gh run list --branch $(git branch --show-current) --limit 1 --json status,conclusion,url`
|
|
625
|
+
4. If in_progress: poll every 15 seconds up to `config.ci.wait_timeout_seconds`
|
|
626
|
+
5. If failed/timed out: show warning box:
|
|
627
|
+
|
|
628
|
+
```
|
|
629
|
+
⚠ CI Status: {conclusion}
|
|
630
|
+
Run: {url}
|
|
631
|
+
Options: [Wait] [Continue anyway] [Abort]
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
6. Use AskUserQuestion to present options: Wait / Continue anyway / Abort
|
|
635
|
+
7. If "Continue anyway": log deviation — `DEVIATION: CI gate bypassed for wave {N}`
|
|
636
|
+
8. If "Abort": stop build, update STATE.md
|
|
637
|
+
|
|
618
638
|
#### 6f. Update STATE.md
|
|
619
639
|
|
|
620
640
|
After each wave completes (all plans in the wave are done, skipped, or aborted):
|
|
@@ -790,6 +810,31 @@ If `git.branching` is `phase`:
|
|
|
790
810
|
- If "Yes, merge": complete the merge and delete the phase branch
|
|
791
811
|
- If "No, keep" or "Other": leave the branch as-is and inform the user
|
|
792
812
|
|
|
813
|
+
**8d-ii. PR Creation (when branching enabled):**
|
|
814
|
+
|
|
815
|
+
If `config.git.branching` is `phase` or `milestone` AND phase verification passed:
|
|
816
|
+
|
|
817
|
+
1. Push the phase branch: `git push -u origin {branch-name}`
|
|
818
|
+
2. If `config.git.auto_pr` is `true`:
|
|
819
|
+
- Run: `gh pr create --title "feat({phase-scope}): {phase-slug}" --body "$(cat <<'EOF'
|
|
820
|
+
## Phase {N}: {phase name}
|
|
821
|
+
|
|
822
|
+
**Goal**: {phase goal from ROADMAP.md}
|
|
823
|
+
|
|
824
|
+
### Key Files
|
|
825
|
+
{key_files from SUMMARY.md, bulleted}
|
|
826
|
+
|
|
827
|
+
### Verification
|
|
828
|
+
{pass/fail status from VERIFICATION.md}
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
Generated by Plan-Build-Run
|
|
832
|
+
EOF
|
|
833
|
+
)"`
|
|
834
|
+
3. If `config.git.auto_pr` is `false`:
|
|
835
|
+
- Use AskUserQuestion to ask: "Phase branch pushed. Create a PR?"
|
|
836
|
+
- Options: Yes (create PR as above) / No / Later (skip)
|
|
837
|
+
|
|
793
838
|
**8e. Auto-advance / auto-continue (conditional):**
|
|
794
839
|
|
|
795
840
|
**If `features.auto_advance` is `true` AND `mode` is `autonomous`:**
|
|
@@ -116,6 +116,19 @@ When a knowledge gap emerges during the conversation — you're unsure about a l
|
|
|
116
116
|
|
|
117
117
|
Display to the user: `◐ Spawning researcher...`
|
|
118
118
|
|
|
119
|
+
**Learnings injection (opt-in):** Check for relevant tech stack learnings:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" 2>/dev/null
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If non-empty JSON array returned:
|
|
126
|
+
|
|
127
|
+
- Write to temp file: `node {resolved_plugin_root}/scripts/pbr-tools.js learnings query --tags "stack,tech" > /tmp/pbr-learnings-$$.md`
|
|
128
|
+
- Note path as `{learnings_temp_path}`; add as item 3 in the researcher's `files_to_read` block below
|
|
129
|
+
|
|
130
|
+
If no learnings or command fails: omit the extra files_to_read entry.
|
|
131
|
+
|
|
119
132
|
```
|
|
120
133
|
Task({
|
|
121
134
|
subagent_type: "pbr:researcher",
|
|
@@ -123,6 +136,7 @@ Task({
|
|
|
123
136
|
CRITICAL: Read these files BEFORE any other action:
|
|
124
137
|
1. .planning/CONTEXT.md — locked decisions and constraints (if exists)
|
|
125
138
|
2. .planning/STATE.md — current project state (if exists)
|
|
139
|
+
{if learnings_temp_path exists}3. {learnings_temp_path} — cross-project learnings (tech stack patterns from past PBR projects){/if}
|
|
126
140
|
</files_to_read>
|
|
127
141
|
<research_assignment>
|
|
128
142
|
Topic: {specific research question}
|
|
@@ -136,6 +150,9 @@ Task({
|
|
|
136
150
|
})
|
|
137
151
|
```
|
|
138
152
|
|
|
153
|
+
If `{learnings_temp_path}` was produced above, replace `{if...}{/if}` with the actual line. If no learnings were found, omit item 3 entirely.
|
|
154
|
+
```
|
|
155
|
+
|
|
139
156
|
After the researcher completes, check for completion markers in the Task() output:
|
|
140
157
|
|
|
141
158
|
- If `## RESEARCH COMPLETE` is present: proceed normally
|
|
@@ -413,6 +413,28 @@ Archive a completed milestone and prepare for the next one.
|
|
|
413
413
|
- Key deliverables: {summary from Step 4}
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
+
7d. **Aggregate learnings from milestone phases:**
|
|
417
|
+
|
|
418
|
+
**CRITICAL: Run learnings aggregation NOW. Do NOT skip this step.**
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
node ${PLUGIN_ROOT}/scripts/milestone-learnings.js .planning/milestones/{version} --project {project-name-from-STATE.md}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
- If the script outputs an error, log it but do NOT abort milestone completion — learnings aggregation is advisory.
|
|
425
|
+
- Display the aggregation summary line to the user (e.g., "Learnings aggregated: 12 new, 3 updated, 0 errors").
|
|
426
|
+
- After aggregation, check for triggered deferral thresholds:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
node ${PLUGIN_ROOT}/scripts/pbr-tools.js learnings check-thresholds
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
If any thresholds are triggered, display each as a notification:
|
|
433
|
+
|
|
434
|
+
```
|
|
435
|
+
Note: Learnings threshold met — {key}: {trigger}. Consider implementing the deferred feature.
|
|
436
|
+
```
|
|
437
|
+
|
|
416
438
|
8. **Git tag:**
|
|
417
439
|
```bash
|
|
418
440
|
git tag -a {version} -m "Milestone: {name}"
|
|
@@ -424,6 +446,22 @@ Archive a completed milestone and prepare for the next one.
|
|
|
424
446
|
git commit -m "docs(planning): complete milestone {version}"
|
|
425
447
|
```
|
|
426
448
|
|
|
449
|
+
### Post-Completion Smoke Test
|
|
450
|
+
|
|
451
|
+
If `config.deployment.smoke_test_command` is set and non-empty:
|
|
452
|
+
|
|
453
|
+
1. Run the command via Bash
|
|
454
|
+
2. If exit code 0: display "Smoke test passed" with command output
|
|
455
|
+
3. If exit code non-zero: display advisory warning:
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
⚠ Smoke test failed (exit code {N})
|
|
459
|
+
Command: {smoke_test_command}
|
|
460
|
+
Output: {first 20 lines of output}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
This is advisory only — the milestone is already archived. Surface it as a potential issue for the user to investigate.
|
|
464
|
+
|
|
427
465
|
10. **Confirm** with branded output:
|
|
428
466
|
```
|
|
429
467
|
╔══════════════════════════════════════════════════════════════╗
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Phase <%= phase_number %>: <%= phase_name %>
|
|
2
|
+
|
|
3
|
+
**Goal**: <%= phase_goal %>
|
|
4
|
+
|
|
5
|
+
### Key Files Changed
|
|
6
|
+
<% key_files.forEach(f => { %>
|
|
7
|
+
- `<%= f %>`
|
|
8
|
+
<% }) %>
|
|
9
|
+
|
|
10
|
+
### Verification
|
|
11
|
+
- Status: <%= verification_status %>
|
|
12
|
+
- Must-haves: <%= must_haves_passed %>/<%= must_haves_total %> passed
|
|
13
|
+
|
|
14
|
+
<% if (closes_issues && closes_issues.length > 0) { %>
|
|
15
|
+
### Issues
|
|
16
|
+
<% closes_issues.forEach(n => { %>
|
|
17
|
+
Closes #<%= n %>
|
|
18
|
+
<% }) %>
|
|
19
|
+
<% } %>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
*Generated by [Plan-Build-Run](https://github.com/SienkLogic/plan-build-run)*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pbr",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.39.0",
|
|
4
4
|
"description": "Plan-Build-Run — Structured development workflow for Claude Code. Solves context rot through disciplined subagent delegation, structured planning, atomic execution, and goal-backward verification.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "SienkLogic",
|
|
@@ -120,6 +120,19 @@ One task = one commit. Exception: TDD tasks get 3 commits (RED, GREEN, REFACTOR)
|
|
|
120
120
|
|
|
121
121
|
Stage only files listed in the task's `<files>`. If git commit fails with lock error, retry up to 3 times with 2s delay.
|
|
122
122
|
|
|
123
|
+
### Issue Auto-Close
|
|
124
|
+
|
|
125
|
+
When the plan frontmatter contains a non-empty `closes_issues` array, append issue-closing syntax to the **final** commit body for the plan:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
git commit -m "feat(01-02): implement user auth
|
|
129
|
+
|
|
130
|
+
Closes #42
|
|
131
|
+
Closes #57"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Only append to the LAST commit of the plan — intermediate commits (RED/GREEN in TDD, partial progress) should NOT include closing syntax.
|
|
135
|
+
|
|
123
136
|
---
|
|
124
137
|
|
|
125
138
|
## Deviation Rules
|
|
@@ -155,11 +155,23 @@ Controls git integration and branching strategy.
|
|
|
155
155
|
| `phase_branch_template` | string | `plan-build-run/phase-{phase}-{slug}` | Phase branch name pattern |
|
|
156
156
|
| `milestone_branch_template` | string | `plan-build-run/{milestone}-{slug}` | Milestone branch name pattern |
|
|
157
157
|
| `mode` | string | `enabled` | Git mode: `enabled` or `disabled` |
|
|
158
|
+
| `auto_pr` | boolean | `false` | Create a GitHub PR after successful phase verification when branching is enabled |
|
|
158
159
|
|
|
159
160
|
When `git.mode` is `disabled`, no git commands run at all -- no commits, branching, or hook validation. Useful for prototyping or non-git projects. See `references/git-integration.md` for full branching strategy details.
|
|
160
161
|
|
|
161
162
|
---
|
|
162
163
|
|
|
164
|
+
## ci
|
|
165
|
+
|
|
166
|
+
Controls CI integration for build gates.
|
|
167
|
+
|
|
168
|
+
| Property | Type | Default | Description |
|
|
169
|
+
|----------|------|---------|-------------|
|
|
170
|
+
| `ci.gate_enabled` | boolean | `false` | Block wave advancement until CI passes |
|
|
171
|
+
| `ci.wait_timeout_seconds` | number | `120` | Max seconds to wait for CI completion |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
163
175
|
## gates
|
|
164
176
|
|
|
165
177
|
Confirmation gates that pause execution to ask the user before proceeding. Setting a gate to `false` makes that step automatic.
|
|
@@ -219,6 +231,16 @@ This value is overridden by the active depth profile if a `depth_profiles` entry
|
|
|
219
231
|
|
|
220
232
|
---
|
|
221
233
|
|
|
234
|
+
## deployment
|
|
235
|
+
|
|
236
|
+
Controls post-milestone deployment verification.
|
|
237
|
+
|
|
238
|
+
| Property | Type | Default | Description |
|
|
239
|
+
|----------|------|---------|-------------|
|
|
240
|
+
| `deployment.smoke_test_command` | string | `""` | Bash command to run after milestone completion (e.g., `"curl -sf https://myapp.com/health"`) |
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
222
244
|
## depth_profiles
|
|
223
245
|
|
|
224
246
|
Override the built-in depth profile defaults. Each key (`quick`, `standard`, `comprehensive`) maps to an object of settings that take effect when that depth is active.
|
|
@@ -174,6 +174,36 @@ Branch name templates are configured in `config.json`:
|
|
|
174
174
|
- `git.phase_branch_template`: Default `plan-build-run/phase-{phase}-{slug}`
|
|
175
175
|
- `git.milestone_branch_template`: Default `plan-build-run/{milestone}-{slug}`
|
|
176
176
|
|
|
177
|
+
### PR Creation
|
|
178
|
+
|
|
179
|
+
When `git.auto_pr: true` and `git.branching` is `phase` or `milestone`, the build skill creates a GitHub PR after verification passes:
|
|
180
|
+
|
|
181
|
+
1. Push the phase branch to the remote
|
|
182
|
+
2. Create a PR via `gh pr create` with structured title and body
|
|
183
|
+
3. PR title follows the commit convention: `feat(phase-{N}): {phase slug}`
|
|
184
|
+
4. PR body includes phase goal, key files changed, and must-have verification results
|
|
185
|
+
|
|
186
|
+
When `git.auto_pr: false` (default), the build skill offers the user a choice after verification:
|
|
187
|
+
- Create PR now
|
|
188
|
+
- Skip PR creation
|
|
189
|
+
- Push branch only (create PR later)
|
|
190
|
+
|
|
191
|
+
PR creation requires `gh` CLI authenticated with repo write access.
|
|
192
|
+
|
|
193
|
+
### CI Integration
|
|
194
|
+
|
|
195
|
+
When `ci.gate_enabled: true`, the build skill checks GitHub Actions status after each wave completes:
|
|
196
|
+
|
|
197
|
+
1. Run: `gh run list --branch $(git branch --show-current) --limit 1 --json status,conclusion,url`
|
|
198
|
+
2. If `status == completed` and `conclusion == success`: proceed to next wave
|
|
199
|
+
3. If `status == in_progress`: wait up to `ci.wait_timeout_seconds`, re-check
|
|
200
|
+
4. If `conclusion != success` or timeout: surface warning with run URL and offer:
|
|
201
|
+
- **Wait**: continue polling
|
|
202
|
+
- **Continue anyway**: proceed despite CI failure (logged as deviation)
|
|
203
|
+
- **Abort**: stop the build
|
|
204
|
+
|
|
205
|
+
CI gate requires `gh` CLI and GitHub Actions configured on the repository. The gate only activates when there are commits pushed to a remote branch (branching must be enabled).
|
|
206
|
+
|
|
177
207
|
### Git Mode
|
|
178
208
|
|
|
179
209
|
The `git.mode` field controls whether git integration is active:
|
|
@@ -157,6 +157,34 @@ When a plan requires research before execution, set the `discovery` field in pla
|
|
|
157
157
|
|
|
158
158
|
---
|
|
159
159
|
|
|
160
|
+
## TDD Decision Heuristic
|
|
161
|
+
|
|
162
|
+
When assigning `tdd="true"` or `tdd="false"` on a task, apply this test:
|
|
163
|
+
|
|
164
|
+
> **Can you write `expect(fn(input)).toBe(output)` before writing `fn`?**
|
|
165
|
+
> Yes → `tdd="true"`. No → `tdd="false"`.
|
|
166
|
+
|
|
167
|
+
### When TDD Adds Value
|
|
168
|
+
|
|
169
|
+
- Pure functions and data transformations
|
|
170
|
+
- Business logic with defined inputs/outputs
|
|
171
|
+
- API response parsing and validation
|
|
172
|
+
- State machines and workflow transitions
|
|
173
|
+
- Utility functions and helpers
|
|
174
|
+
|
|
175
|
+
### When to Skip TDD
|
|
176
|
+
|
|
177
|
+
- UI rendering and layout (test after)
|
|
178
|
+
- Configuration and environment setup
|
|
179
|
+
- Glue code wiring modules together
|
|
180
|
+
- Simple CRUD with no business logic
|
|
181
|
+
- File system operations and I/O plumbing
|
|
182
|
+
- One-off scripts and migrations
|
|
183
|
+
|
|
184
|
+
When the global config `features.tdd_mode: true` is set, all tasks default to TDD. The planner should still set `tdd="false"` on tasks matching the skip list above — the global flag is a project preference, not a mandate for every task.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
160
188
|
## Dependency Graph Rules
|
|
161
189
|
|
|
162
190
|
### File Conflict Detection
|
|
@@ -46,6 +46,9 @@ consumes:
|
|
|
46
46
|
requirement_ids:
|
|
47
47
|
- "P02-G1"
|
|
48
48
|
- "P02-G2"
|
|
49
|
+
closes_issues:
|
|
50
|
+
- 42
|
|
51
|
+
- 57
|
|
49
52
|
---
|
|
50
53
|
```
|
|
51
54
|
|
|
@@ -71,6 +74,7 @@ requirement_ids:
|
|
|
71
74
|
| `requirement_ids` | NO | array | Requirement IDs from REQUIREMENTS.md or ROADMAP.md goal IDs that this plan addresses. Enables bidirectional traceability between plans and requirements/goals. |
|
|
72
75
|
| `dependency_fingerprints` | NO | object | Hashes of dependency phase SUMMARY.md files at plan-creation time. Used to detect stale plans. |
|
|
73
76
|
| `data_contracts` | NO | array | Cross-boundary parameter mappings for calls where arguments originate from external boundaries. Format: `"param: source (context) [fallback]"` |
|
|
77
|
+
| `closes_issues` | NO | number[] | GitHub issue numbers to close when this plan's final commit lands. Default: `[]` |
|
|
74
78
|
|
|
75
79
|
### Data Contracts
|
|
76
80
|
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* learnings.js — Global cross-project learnings store for Plan-Build-Run.
|
|
5
|
+
*
|
|
6
|
+
* Storage: JSONL at ~/.claude/learnings.jsonl
|
|
7
|
+
* Schema v1: id, source_project, type, tags, confidence, occurrences, summary, detail, custom_tags
|
|
8
|
+
*
|
|
9
|
+
* Usage (library):
|
|
10
|
+
* const { learningsIngest, learningsQuery } = require('./lib/learnings');
|
|
11
|
+
*
|
|
12
|
+
* Usage (CLI via pbr-tools.js):
|
|
13
|
+
* node pbr-tools.js learnings ingest <json-file>
|
|
14
|
+
* node pbr-tools.js learnings query [--tags X] [--min-confidence Y] [--stack S] [--type T]
|
|
15
|
+
* node pbr-tools.js learnings check-thresholds
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// --- Constants ---
|
|
24
|
+
|
|
25
|
+
const GLOBAL_LEARNINGS_PATH = path.join(os.homedir(), '.claude', 'learnings.jsonl');
|
|
26
|
+
|
|
27
|
+
// Schema v1 type taxonomy
|
|
28
|
+
const LEARNING_TYPES = [
|
|
29
|
+
'tech-pattern', // positive: "this tech/pattern worked well"
|
|
30
|
+
'anti-pattern', // negative: "avoid this approach"
|
|
31
|
+
'estimation-metric', // timing/sizing data (e.g., "OAuth takes ~3 phases")
|
|
32
|
+
'planning-failure', // what went wrong in planning
|
|
33
|
+
'deferred-item', // common deferrals with trigger conditions
|
|
34
|
+
'stack-insight', // tech stack compatibility facts
|
|
35
|
+
'process-win', // positive process patterns
|
|
36
|
+
'process-failure' // negative process patterns
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Confidence tiers based on occurrence count
|
|
40
|
+
const CONFIDENCE_TIERS = { low: 1, medium: 2, high: 3 };
|
|
41
|
+
|
|
42
|
+
// Deferral thresholds for the 4 deferred items from phase 45
|
|
43
|
+
const DEFERRAL_THRESHOLDS = [
|
|
44
|
+
{ key: 'organic-taxonomy', trigger: 'count > 50', check: (count) => count > 50 },
|
|
45
|
+
{ key: 'statistical-confidence', trigger: 'any_tag >= 20', check: (_c, tagMax) => tagMax >= 20 },
|
|
46
|
+
{ key: 'audit-integration', trigger: 'audits > 10', check: (_c, _t, audits) => audits > 10 },
|
|
47
|
+
{ key: 'executor-injection', trigger: 'queries > 30', check: (_c, _t, _a, queries) => queries > 30 }
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// --- Core functions ---
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute confidence tier from occurrence count.
|
|
54
|
+
* @param {number} occurrences
|
|
55
|
+
* @returns {'low'|'medium'|'high'}
|
|
56
|
+
*/
|
|
57
|
+
function computeConfidence(occurrences) {
|
|
58
|
+
if (occurrences >= 3) return 'high';
|
|
59
|
+
if (occurrences === 2) return 'medium';
|
|
60
|
+
return 'low';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate a learning entry against schema v1.
|
|
65
|
+
* @param {object} entry
|
|
66
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
67
|
+
*/
|
|
68
|
+
function validateEntry(entry) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
|
|
71
|
+
if (!entry) {
|
|
72
|
+
return { valid: false, errors: ['entry is null or undefined'] };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Required fields
|
|
76
|
+
const required = ['id', 'source_project', 'type', 'tags', 'confidence', 'occurrences', 'summary'];
|
|
77
|
+
for (const field of required) {
|
|
78
|
+
if (entry[field] === undefined || entry[field] === null) {
|
|
79
|
+
errors.push(`missing required field: ${field}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Type must be in taxonomy
|
|
84
|
+
if (entry.type !== undefined && !LEARNING_TYPES.includes(entry.type)) {
|
|
85
|
+
errors.push(`invalid type: "${entry.type}". Must be one of: ${LEARNING_TYPES.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Confidence must be valid tier
|
|
89
|
+
if (entry.confidence !== undefined && !['low', 'medium', 'high'].includes(entry.confidence)) {
|
|
90
|
+
errors.push(`invalid confidence: "${entry.confidence}". Must be one of: low, medium, high`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Occurrences must be positive integer
|
|
94
|
+
if (entry.occurrences !== undefined) {
|
|
95
|
+
if (!Number.isInteger(entry.occurrences) || entry.occurrences < 1) {
|
|
96
|
+
errors.push(`occurrences must be a positive integer, got: ${entry.occurrences}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tags must be a non-empty array of strings
|
|
101
|
+
if (entry.tags !== undefined) {
|
|
102
|
+
if (!Array.isArray(entry.tags)) {
|
|
103
|
+
errors.push('tags must be an array');
|
|
104
|
+
} else if (entry.tags.length === 0) {
|
|
105
|
+
errors.push('tags must be a non-empty array');
|
|
106
|
+
} else {
|
|
107
|
+
const nonStrings = entry.tags.filter(t => typeof t !== 'string');
|
|
108
|
+
if (nonStrings.length > 0) {
|
|
109
|
+
errors.push(`all tags must be strings; got ${nonStrings.length} non-string value(s)`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { valid: errors.length === 0, errors };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load all entries from the learnings JSONL file.
|
|
119
|
+
* @param {string} [filePath] — defaults to GLOBAL_LEARNINGS_PATH
|
|
120
|
+
* @returns {object[]}
|
|
121
|
+
*/
|
|
122
|
+
function loadAll(filePath) {
|
|
123
|
+
const target = filePath || GLOBAL_LEARNINGS_PATH;
|
|
124
|
+
if (!fs.existsSync(target)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const content = fs.readFileSync(target, 'utf8');
|
|
128
|
+
return content
|
|
129
|
+
.split('\n')
|
|
130
|
+
.filter(line => line.trim().length > 0)
|
|
131
|
+
.reduce((acc, line) => {
|
|
132
|
+
try {
|
|
133
|
+
acc.push(JSON.parse(line));
|
|
134
|
+
} catch (_e) {
|
|
135
|
+
console.error(`[learnings] Skipping malformed line: ${line.slice(0, 80)}`);
|
|
136
|
+
}
|
|
137
|
+
return acc;
|
|
138
|
+
}, []);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Save all entries to the learnings JSONL file.
|
|
143
|
+
* @param {object[]} entries
|
|
144
|
+
* @param {string} [filePath] — defaults to GLOBAL_LEARNINGS_PATH
|
|
145
|
+
*/
|
|
146
|
+
function saveAll(entries, filePath) {
|
|
147
|
+
const target = filePath || GLOBAL_LEARNINGS_PATH;
|
|
148
|
+
const dir = path.dirname(target);
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
const content = entries.map(e => JSON.stringify(e)).join('\n') + (entries.length > 0 ? '\n' : '');
|
|
151
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Ingest a learning entry into the global store.
|
|
156
|
+
* Deduplicates by source_project + type + summary.
|
|
157
|
+
* If a duplicate is found, increments occurrences and updates confidence.
|
|
158
|
+
* @param {object} rawEntry
|
|
159
|
+
* @param {{ filePath?: string }} [options]
|
|
160
|
+
* @returns {{ action: 'created'|'updated', entry: object }}
|
|
161
|
+
*/
|
|
162
|
+
function learningsIngest(rawEntry, options = {}) {
|
|
163
|
+
const filePath = options.filePath || GLOBAL_LEARNINGS_PATH;
|
|
164
|
+
|
|
165
|
+
// Fill in generated fields if missing
|
|
166
|
+
const entry = Object.assign({}, rawEntry);
|
|
167
|
+
if (!entry.id) {
|
|
168
|
+
try {
|
|
169
|
+
entry.id = crypto.randomUUID();
|
|
170
|
+
} catch (_e) {
|
|
171
|
+
entry.id = Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!entry.created_at) {
|
|
175
|
+
entry.created_at = new Date().toISOString();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const existing = loadAll(filePath);
|
|
179
|
+
|
|
180
|
+
// Dedup check: same source_project + type + summary
|
|
181
|
+
const dupIndex = existing.findIndex(
|
|
182
|
+
e => e.source_project === entry.source_project &&
|
|
183
|
+
e.type === entry.type &&
|
|
184
|
+
e.summary === entry.summary
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
if (dupIndex !== -1) {
|
|
188
|
+
// Update existing entry
|
|
189
|
+
const dup = existing[dupIndex];
|
|
190
|
+
dup.occurrences = (dup.occurrences || 1) + 1;
|
|
191
|
+
dup.confidence = computeConfidence(dup.occurrences);
|
|
192
|
+
dup.updated_at = new Date().toISOString();
|
|
193
|
+
saveAll(existing, filePath);
|
|
194
|
+
return { action: 'updated', entry: dup };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// New entry — validate before saving
|
|
198
|
+
const validation = validateEntry(entry);
|
|
199
|
+
if (!validation.valid) {
|
|
200
|
+
throw new Error(`Invalid learning entry: ${validation.errors.join('; ')}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
existing.push(entry);
|
|
204
|
+
saveAll(existing, filePath);
|
|
205
|
+
return { action: 'created', entry };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Query learnings with optional filters.
|
|
210
|
+
* @param {{ tags?: string[], minConfidence?: string, stack?: string, type?: string }} [filters]
|
|
211
|
+
* @param {{ filePath?: string }} [options]
|
|
212
|
+
* @returns {object[]} Matching entries sorted by occurrences descending
|
|
213
|
+
*/
|
|
214
|
+
function learningsQuery(filters = {}, options = {}) {
|
|
215
|
+
const filePath = options.filePath || GLOBAL_LEARNINGS_PATH;
|
|
216
|
+
let entries = loadAll(filePath);
|
|
217
|
+
|
|
218
|
+
// Filter by tags (ALL listed tags must be present)
|
|
219
|
+
if (filters.tags && filters.tags.length > 0) {
|
|
220
|
+
entries = entries.filter(e => {
|
|
221
|
+
const entryTags = e.tags || [];
|
|
222
|
+
return filters.tags.every(t => entryTags.includes(t));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Filter by minConfidence (entry confidence must be >= threshold)
|
|
227
|
+
if (filters.minConfidence && filters.minConfidence !== 'low') {
|
|
228
|
+
const minLevel = CONFIDENCE_TIERS[filters.minConfidence];
|
|
229
|
+
if (minLevel !== undefined) {
|
|
230
|
+
entries = entries.filter(e => {
|
|
231
|
+
const entryLevel = CONFIDENCE_TIERS[e.confidence] || 0;
|
|
232
|
+
return entryLevel >= minLevel;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Filter by stack: matches if tags include "stack:<value>" OR stack_tags includes value
|
|
238
|
+
if (filters.stack) {
|
|
239
|
+
const stackTag = `stack:${filters.stack}`;
|
|
240
|
+
entries = entries.filter(e => {
|
|
241
|
+
const entryTags = e.tags || [];
|
|
242
|
+
const stackTags = e.stack_tags || [];
|
|
243
|
+
return entryTags.includes(stackTag) || stackTags.includes(filters.stack);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Filter by type (exact match)
|
|
248
|
+
if (filters.type) {
|
|
249
|
+
entries = entries.filter(e => e.type === filters.type);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Sort by occurrences descending
|
|
253
|
+
entries.sort((a, b) => (b.occurrences || 1) - (a.occurrences || 1));
|
|
254
|
+
|
|
255
|
+
return entries;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check deferral thresholds against current learnings data.
|
|
260
|
+
* @param {{ filePath?: string }} [options]
|
|
261
|
+
* @returns {Array<{ key: string, trigger: string, message: string }>} Triggered thresholds
|
|
262
|
+
*/
|
|
263
|
+
function checkDeferralThresholds(options = {}) {
|
|
264
|
+
const filePath = options.filePath || GLOBAL_LEARNINGS_PATH;
|
|
265
|
+
const entries = loadAll(filePath);
|
|
266
|
+
|
|
267
|
+
const totalCount = entries.length;
|
|
268
|
+
|
|
269
|
+
// Compute max occurrences for any single tag
|
|
270
|
+
const tagCounts = {};
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
for (const tag of (entry.tags || [])) {
|
|
273
|
+
tagCounts[tag] = (tagCounts[tag] || 0) + (entry.occurrences || 1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const tagMax = Object.values(tagCounts).reduce((max, v) => Math.max(max, v), 0);
|
|
277
|
+
|
|
278
|
+
// Count audit-type entries
|
|
279
|
+
const auditCount = entries.filter(e => e.type === 'planning-failure' || e.type === 'process-failure').length;
|
|
280
|
+
|
|
281
|
+
// Query count: default 0 (separate counter not implemented in v1 — threshold tracked externally)
|
|
282
|
+
const queryCount = 0;
|
|
283
|
+
|
|
284
|
+
const triggered = [];
|
|
285
|
+
for (const threshold of DEFERRAL_THRESHOLDS) {
|
|
286
|
+
if (threshold.check(totalCount, tagMax, auditCount, queryCount)) {
|
|
287
|
+
triggered.push({
|
|
288
|
+
key: threshold.key,
|
|
289
|
+
trigger: threshold.trigger,
|
|
290
|
+
message: `Deferral threshold met: ${threshold.key} (${threshold.trigger})`
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return triggered;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --- Exports ---
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
GLOBAL_LEARNINGS_PATH,
|
|
302
|
+
LEARNING_TYPES,
|
|
303
|
+
CONFIDENCE_TIERS,
|
|
304
|
+
DEFERRAL_THRESHOLDS,
|
|
305
|
+
computeConfidence,
|
|
306
|
+
validateEntry,
|
|
307
|
+
loadAll,
|
|
308
|
+
saveAll,
|
|
309
|
+
learningsIngest,
|
|
310
|
+
learningsQuery,
|
|
311
|
+
checkDeferralThresholds
|
|
312
|
+
};
|