@yemi33/minions 0.1.2427 → 0.1.2429
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/docs/README.md +2 -2
- package/docs/completion-reports.md +1 -1
- package/docs/design-state-storage.md +3 -1
- package/docs/security.md +5 -3
- package/docs/self-improvement.md +9 -8
- package/docs/team-memory.md +51 -70
- package/engine/features.js +0 -12
- package/engine/lifecycle.js +0 -10
- package/engine/playbook.js +6 -6
- package/engine/shared.js +199 -37
- package/package.json +1 -1
package/docs/README.md
CHANGED
|
@@ -66,12 +66,12 @@ Architecture, design proposals, and lifecycle references for people working on t
|
|
|
66
66
|
- [qa-runbooks.md](qa-runbooks.md) — Per-project QA runbook schema, storage layout (`projects/<name>/runbooks/<id>.json`), CRUD endpoints, run-record lifecycle, and the `qa-validate` agent sidecar contract.
|
|
67
67
|
- [rfc-completion-json.md](rfc-completion-json.md) — RFC for replacing stdout regex-scraping with a structured `completion.json` control-plane protocol.
|
|
68
68
|
- [runtime-adapters.md](runtime-adapters.md) — Runtime adapter contract (`engine/runtimes/*`): how the engine talks to Claude Code, Copilot CLI, and future CLIs through a single capability-flagged interface.
|
|
69
|
-
- [self-improvement.md](self-improvement.md) — The six self-improvement mechanisms (learnings inbox, per-agent history, review feedback, quality metrics, etc.) that form Minions' continuous feedback loop, including structured review learning (durable,
|
|
69
|
+
- [self-improvement.md](self-improvement.md) — The six self-improvement mechanisms (learnings inbox, per-agent history, review feedback, quality metrics, etc.) that form Minions' continuous feedback loop, including structured review learning (durable, always-on lessons from verified review findings).
|
|
70
70
|
- [shared-lifecycle-module-map.md](shared-lifecycle-module-map.md) — Documentation-only module-boundary audit of `engine/shared.js` / `engine/lifecycle.js`: function inventory, cross-file call graph, and a proposed split map (no code moved).
|
|
71
71
|
- [skills.md](skills.md) — Skill block format: how agents emit reusable `\`\`\`skill` blocks and how the engine extracts them into native personal-skill directories.
|
|
72
72
|
- [slim-ux/concepts.md](slim-ux/concepts.md) — Slim-UX design notes: simplified surface concepts driving the project picker, inline project link, and decoupled folder picker.
|
|
73
73
|
- [slim-ux/architecture-suggestions.md](slim-ux/architecture-suggestions.md) — Slim-UX follow-up architecture suggestions paired with `concepts.md`.
|
|
74
|
-
- [team-memory.md](team-memory.md) — End-to-end hybrid memory system: file-backed inputs and consolidation, SQL/FTS5 records, retrieval and fallback, prompt bounds, episodic capture, the review-learning lifecycle (capture/recall/promotion/contradiction/diagnostics) and its
|
|
74
|
+
- [team-memory.md](team-memory.md) — End-to-end hybrid memory system: file-backed inputs and consolidation, SQL/FTS5 records, retrieval and fallback, prompt bounds, episodic capture, the review-learning lifecycle (capture/recall/promotion/contradiction/diagnostics) and its rollout history + acceptance measurement, security, APIs, and operations.
|
|
75
75
|
- [timeouts-and-liveness.md](timeouts-and-liveness.md) — What kills (or doesn't kill) a live tracked agent: the wall-clock vs steering kill invariants, spawn-phase watchdog gates, steering safety nets, and stale-orphan detection ladder.
|
|
76
76
|
- [visual-evidence-ci.md](visual-evidence-ci.md) — GitHub Actions before/after dashboard capture: path and label triggers, deterministic base/head fixtures, artifact/comment lifecycle, trust boundary, and local reproduction.
|
|
77
77
|
- [watches.md](watches.md) — Persistent monitoring jobs: target-type registry, conditions, follow-up actions, and the `watches.d/` plugin folder.
|
|
@@ -195,7 +195,7 @@ These fields do not replace `reviewFindingResolution`. An unchanged
|
|
|
195
195
|
review-feedback fix must still set `noop: true` and prove the exact trusted
|
|
196
196
|
finding through the stricter no-op contract below.
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
The engine processes normalized
|
|
199
199
|
entries only after the completion is otherwise successful and all PR/work-item
|
|
200
200
|
contracts pass. `accepted-fixed` requires verified remote-head advancement or
|
|
201
201
|
an independent approval of the exact current review head.
|
|
@@ -48,7 +48,9 @@ release .lock file
|
|
|
48
48
|
Key properties:
|
|
49
49
|
- **Synchronous blocking** — `withFileLock` spins with `sleepMs(25)` until lock acquired or 5s timeout (source: `engine/shared.js` `withFileLock` ~L1329)
|
|
50
50
|
- **Whole-file granularity** — updating one field in one work item rewrites all 180 items (370 KB)
|
|
51
|
-
- **Stale lock recovery** — locks older than 5 min (`LOCK_STALE_MS = 300_000`) are force-removed; holders that recorded a `{pid, ts}` payload are kept alive past the threshold while `process.kill(pid, 0)` succeeds, with a hard last-resort cap at 5×LOCK_STALE_MS (source: `engine/shared.js`, P-b7d4e8f2)
|
|
51
|
+
- **Stale lock recovery** — locks older than 5 min (`LOCK_STALE_MS = 300_000`) are force-removed; holders that recorded a `{pid, ts, token}` payload are kept alive past the threshold while `process.kill(pid, 0)` succeeds, with a hard last-resort cap at 5×LOCK_STALE_MS (source: `engine/shared.js`, P-b7d4e8f2)
|
|
52
|
+
- **Identity-checked reap and release** — every acquisition stamps a unique `token` into the lock payload. Reaping never unlinks the lock path directly: the reaper re-observes the same token after a short grace delay, takes custody with an atomic `rename`, and verifies the quarantined file is the acquisition it judged stale (restoring it otherwise). Release likewise unlinks only while the on-disk token is still ours. Removing a lock by path alone is a TOCTOU that lets a second writer into the critical section and silently loses one update (source: `engine/shared.js` `withFileLock`, W-ms2nbtkj00016ffc)
|
|
53
|
+
- **Compromised sections fail loudly** — if the lock is gone or re-owned when the critical section ends, `withFileLock` throws `ELOCKCOMPROMISED`. `mutateJsonFileLocked` / `mutateTextFileLocked` first re-apply the mutation against the current on-disk state (up to `LOCK_COMPROMISE_REDOS`) when their own write did not survive, so a raced update is recovered rather than dropped
|
|
52
54
|
- **Read caching** — only `dispatch.json` has a 2s TTL cache (source: `engine/queries.js`)
|
|
53
55
|
|
|
54
56
|
### 1.3 Read vs Write Ratio
|
package/docs/security.md
CHANGED
|
@@ -160,9 +160,11 @@ defenses enforced at the persistence boundary in `engine/review-learning.js`:
|
|
|
160
160
|
actually showed it, and only within its own project. A forged id, an unshown
|
|
161
161
|
lesson, or a cross-project id is rejected as a diagnostic — it cannot quarantine a
|
|
162
162
|
lesson it was never given.
|
|
163
|
-
- **
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
- **Structurally bounded, not flag-bounded.** Capture and recall are always on;
|
|
164
|
+
their safety comes from the constraints above (verified findings only,
|
|
165
|
+
project-scoped byte-capped recall, trusted-trace binding, reversible
|
|
166
|
+
quarantine), not from a switch. Stored records stay auditable through the
|
|
167
|
+
diagnostics endpoints.
|
|
166
168
|
- **Injection-flag / nonce gates still apply.** A completion flagged
|
|
167
169
|
`failure_class: "injection-flagged"` or failing the nonce check never reaches
|
|
168
170
|
review-learning capture or application processing.
|
package/docs/self-improvement.md
CHANGED
|
@@ -161,7 +161,7 @@ in the future, avoid the patterns flagged here.
|
|
|
161
161
|
|
|
162
162
|
Without this, review findings only exist in the inbox file under the reviewer's name. The author never explicitly sees them unless they happen to read the consolidated notes.md. The feedback loop ensures the author gets a direct, targeted learning from every review.
|
|
163
163
|
|
|
164
|
-
### 3a. Structured review learning (durable,
|
|
164
|
+
### 3a. Structured review learning (durable, always-on)
|
|
165
165
|
|
|
166
166
|
The narrative feedback file above is per-PR and consolidated into free-text notes.
|
|
167
167
|
**Review learning** is its durable, structured evolution: a verified reviewer
|
|
@@ -174,11 +174,11 @@ deterministically. It is fully specified in
|
|
|
174
174
|
and a stable finding key `<project>|<canonical-pr-id>|<host-namespaced-source-key>`.
|
|
175
175
|
They are globally excluded from generic FTS recall and only surface through the
|
|
176
176
|
dedicated project-scoped path.
|
|
177
|
-
- **Capture.**
|
|
177
|
+
- **Capture.** Only a completed Minions
|
|
178
178
|
`changes-requested` verdict creates an unresolved *candidate*; a verified
|
|
179
179
|
`accepted-fixed` / `satisfied-elsewhere` disposition promotes it to one active
|
|
180
180
|
lesson. Invalid/stale/deferred close or hold the candidate.
|
|
181
|
-
- **Recall.**
|
|
181
|
+
- **Recall.** A dispatch with a resolved project
|
|
182
182
|
recalls up to 4 records / 4 KiB as a separately fenced `## Applicable Prior Review
|
|
183
183
|
Lessons` section, ranked by exact symbol → file → tag overlap.
|
|
184
184
|
- **Application, correction, supersession.** Agents report `applied` /
|
|
@@ -189,11 +189,12 @@ deterministically. It is fully specified in
|
|
|
189
189
|
- **Promotion.** A high-confidence, repeatedly observed lesson can be human-promoted
|
|
190
190
|
into repository guidance (`CLAUDE.md` / `AGENTS.md` / Copilot instructions or a
|
|
191
191
|
project skill) via a normal PR — never by editing a checkout.
|
|
192
|
-
- **Diagnostics
|
|
193
|
-
rollout aggregates, and the
|
|
194
|
-
`/api/memory-records/*`.
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
- **Diagnostics & retention.** Retrieval decisions, a lifecycle timeline,
|
|
193
|
+
rollout aggregates, and the rollout comparison are exposed under
|
|
194
|
+
`/api/memory-records/*`. Capture and recall are unconditional — the temporary
|
|
195
|
+
`review-learning-capture` / `review-learning-recall` flags were removed once
|
|
196
|
+
the staged rollout completed (see
|
|
197
|
+
[team-memory.md → Rollout history and acceptance](team-memory.md#rollout-history-and-acceptance)).
|
|
197
198
|
|
|
198
199
|
## 4. Human Feedback on PRs
|
|
199
200
|
|
package/docs/team-memory.md
CHANGED
|
@@ -272,7 +272,7 @@ Migration 027 adds the storage foundation for review-derived procedural memory:
|
|
|
272
272
|
replace the hash.
|
|
273
273
|
- Human-feedback fix dispatches snapshot that bounded source list in
|
|
274
274
|
`meta.commentSources`, including contexts coalesced during cooldown.
|
|
275
|
-
-
|
|
275
|
+
- Only a completed Minions
|
|
276
276
|
`changes-requested` verdict creates an unresolved candidate. Review identity,
|
|
277
277
|
project, canonical PR, configured author, reviewer, dispatch, source item,
|
|
278
278
|
and a source hash are persisted; the raw review note remains only in its
|
|
@@ -378,7 +378,7 @@ and they are **globally excluded** from the generic FTS pack
|
|
|
378
378
|
(`searchMemoryRecords` drops `source_type='review-learning'` unless a caller
|
|
379
379
|
passes `includeReviewLearning`, which only the operator dashboard search does).
|
|
380
380
|
|
|
381
|
-
When
|
|
381
|
+
When a dispatch has a resolved
|
|
382
382
|
project context, the path:
|
|
383
383
|
|
|
384
384
|
1. lists active project-scoped lessons straight from the review-learning index
|
|
@@ -399,9 +399,8 @@ Each run records bounded telemetry through `recordRetrievalRun` with
|
|
|
399
399
|
`retrieval_kind='review-learning'`, the dispatch and work-item ids, selected
|
|
400
400
|
record ids, per-record scores, and reason codes (`exact-symbol`, `exact-file`,
|
|
401
401
|
`exact-tag`, `lexical`, `not-applicable`, `record-cap`, `byte-cap`) — never any
|
|
402
|
-
prompt or comment body.
|
|
403
|
-
|
|
404
|
-
Center / unrelated central work receive no section.
|
|
402
|
+
prompt or comment body. Projectless Command Center / unrelated central work
|
|
403
|
+
receive no section.
|
|
405
404
|
|
|
406
405
|
## Prompt assembly and fallback
|
|
407
406
|
|
|
@@ -412,8 +411,8 @@ appends memory context in this order:
|
|
|
412
411
|
2. Either **Relevant Memory** or the legacy pair:
|
|
413
412
|
- **Team Notes** from `notes.md`;
|
|
414
413
|
- **Personal Memory** from `knowledge/agents/<agentId>.md`.
|
|
415
|
-
3. **Applicable Prior Review Lessons**, when
|
|
416
|
-
dispatch has a project
|
|
414
|
+
3. **Applicable Prior Review Lessons**, when the
|
|
415
|
+
dispatch has a project and at least one applicable lesson exists (bounded and
|
|
417
416
|
fenced independently of the generic memory budget).
|
|
418
417
|
4. Later non-memory appendices such as propagated project instructions.
|
|
419
418
|
|
|
@@ -498,8 +497,6 @@ completion result.
|
|
|
498
497
|
| Key | Default | Purpose |
|
|
499
498
|
|-----|---------|---------|
|
|
500
499
|
| `features.memoryRetrieval` | `true` | Enables structured retrieval. Feature resolution is environment override, then `config.features`, then registry default. |
|
|
501
|
-
| `features.review-learning-capture` | `false` | Temporary gate for capturing verified review-learning candidates and lessons. |
|
|
502
|
-
| `features.review-learning-recall` | `false` | Temporary independent gate for the dedicated review-learning recall path. |
|
|
503
500
|
| `engine.memoryRetrievalShadowMode` | `false` | Measure retrieval without replacing legacy prompt memory. |
|
|
504
501
|
| `engine.memoryRetrievalTopK` | `8` | Maximum selected records. |
|
|
505
502
|
| `engine.memoryRetrievalMaxBytes` | `8192` | UTF-8 byte budget for selected record blocks. |
|
|
@@ -513,52 +510,41 @@ completion result.
|
|
|
513
510
|
| `engine.agentMemorySummaryDays` | `30` | Age trigger for summary eligibility. |
|
|
514
511
|
|
|
515
512
|
Feature flags are exposed by the generic Dashboard **Settings > Features**
|
|
516
|
-
surface.
|
|
517
|
-
the
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
prompt injection is gated, never storage or inspection.
|
|
546
|
-
|
|
547
|
-
### Stage 1 — capture-only for 30 days
|
|
548
|
-
|
|
549
|
-
Enable `review-learning-capture` only (recall stays off). This is the measurement
|
|
550
|
-
baseline: verified findings become candidates and lessons and accrue `created`,
|
|
551
|
-
`retrieved`-less, and `repeated-finding` lifecycle events, but **no lesson is ever
|
|
552
|
-
injected into a prompt**. Capture-only changes no rendered prompt. Run this for at
|
|
553
|
-
least 30 days to collect a per-project, per-normalized-area baseline recurrence
|
|
554
|
-
rate (the share of resolved valid findings that recurred).
|
|
513
|
+
surface. Settings also exposes shadow mode, episodic capture, retrieval bounds,
|
|
514
|
+
the inbox threshold, and automatic KB sweep. The personal-file window and summary
|
|
515
|
+
keys are configuration-only today.
|
|
516
|
+
|
|
517
|
+
Review-learning capture and recall are **not** flagged — they graduated to
|
|
518
|
+
always-on behaviour and have no config or environment override. Their bounds are
|
|
519
|
+
structural, not switchable: capture only accepts verified findings, recall is
|
|
520
|
+
project-scoped and byte-bounded, contradictions are quarantined reversibly, and
|
|
521
|
+
review-learning records are globally excluded from generic FTS retrieval.
|
|
522
|
+
|
|
523
|
+
The environment override for structured retrieval is
|
|
524
|
+
`MINIONS_FEATURE_MEMORYRETRIEVAL=1|0`.
|
|
525
|
+
|
|
526
|
+
## Rollout history and acceptance
|
|
527
|
+
|
|
528
|
+
Review-learning shipped behind two independent, default-off flags
|
|
529
|
+
(`review-learning-capture`, `review-learning-recall`) so the schema, capture,
|
|
530
|
+
recall, promotion, contradiction, and diagnostics code could all land on `main`
|
|
531
|
+
without changing a single rendered prompt. That staged rollout is **complete**:
|
|
532
|
+
both flags were removed and the behaviour is now unconditional. The stages below
|
|
533
|
+
are retained as history and as the shape of the acceptance measurement, which is
|
|
534
|
+
still exposed by the diagnostics endpoints.
|
|
535
|
+
|
|
536
|
+
### Stage 1 — capture-only measurement window
|
|
537
|
+
|
|
538
|
+
Capture ran alone (recall still gated off): verified findings became candidates
|
|
539
|
+
and lessons and accrued `created` and `repeated-finding` lifecycle events, but no
|
|
540
|
+
lesson was injected into a prompt. That window is the recurrence baseline — a
|
|
541
|
+
per-project, per-normalized-area share of resolved valid findings that recurred.
|
|
555
542
|
|
|
556
543
|
### Stage 2 — one-project recall canary
|
|
557
544
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
bounded, fenced lessons on dispatches in that project. Watch the canary for:
|
|
545
|
+
Recall was enabled for a single project. Recall injects at most 4 records / 4 KiB
|
|
546
|
+
of bounded, fenced lessons on dispatches in that project. The canary was watched
|
|
547
|
+
for:
|
|
562
548
|
|
|
563
549
|
- contradiction alerts (`review-learning-contradiction` inbox notes) — an
|
|
564
550
|
evidence-backed contradiction quarantines the offending lesson automatically;
|
|
@@ -566,7 +552,7 @@ bounded, fenced lessons on dispatches in that project. Watch the canary for:
|
|
|
566
552
|
confirm lessons are selected for the right file/symbol reasons; and
|
|
567
553
|
- the recurrence rate for the canary project's first 30 active days.
|
|
568
554
|
|
|
569
|
-
###
|
|
555
|
+
### The success gate (still live)
|
|
570
556
|
|
|
571
557
|
`GET /api/memory-records/rollout-comparison`
|
|
572
558
|
(`memoryStore.getReviewLearningRolloutComparison`) compares the **30-day
|
|
@@ -576,23 +562,18 @@ grouped by project and normalized area. It only reports `status: "success"` for
|
|
|
576
562
|
(`minimumSample`, default 20 — otherwise `status: "insufficient-sample"`) **and**
|
|
577
563
|
the repeated valid-finding rate fell by at least `minReduction` (default `0.25` =
|
|
578
564
|
25%). A zero baseline rate is reported as `no-improvement`, never a divide-by-zero
|
|
579
|
-
success.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
###
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
accruing new candidates/lessons. Existing records remain in SQL and stay
|
|
592
|
-
auditable through the diagnostics endpoints; nothing is deleted.
|
|
593
|
-
|
|
594
|
-
No migration, restart-ordering, or data cleanup is required to roll back — the flags
|
|
595
|
-
gate prompt behaviour, not storage.
|
|
565
|
+
success. The comparison endpoint remains the way to measure review-learning's
|
|
566
|
+
effect per project and area now that the behaviour is always on.
|
|
567
|
+
|
|
568
|
+
### Reverting is a code change, not a switch
|
|
569
|
+
|
|
570
|
+
There is no flag to flip any more. Both capture and recall are unconditional, so
|
|
571
|
+
backing the behaviour out means reverting the graduation commit. What has not
|
|
572
|
+
changed is that recall is a pure prompt-injection layer over storage: a revert
|
|
573
|
+
stops candidates accruing and stops the `## Applicable Prior Review Lessons`
|
|
574
|
+
section rendering, but every existing record stays in SQL and stays auditable
|
|
575
|
+
through the diagnostics endpoints. Nothing is deleted, and no migration,
|
|
576
|
+
restart-ordering, or data cleanup is involved.
|
|
596
577
|
|
|
597
578
|
### Promotion into repository guidance
|
|
598
579
|
|
package/engine/features.js
CHANGED
|
@@ -102,18 +102,6 @@ const FEATURES = {
|
|
|
102
102
|
addedIn: '0.1.2233',
|
|
103
103
|
expires: '2027-01-31',
|
|
104
104
|
},
|
|
105
|
-
'review-learning-capture': {
|
|
106
|
-
description: 'Capture verified code-review findings as bounded, project-scoped review-learning candidates and procedural lessons.',
|
|
107
|
-
default: false,
|
|
108
|
-
addedIn: '0.1.2203',
|
|
109
|
-
expires: '2027-01-31',
|
|
110
|
-
},
|
|
111
|
-
'review-learning-recall': {
|
|
112
|
-
description: 'Recall bounded applicable review lessons through the dedicated project-scoped review-learning path.',
|
|
113
|
-
default: false,
|
|
114
|
-
addedIn: '0.1.2203',
|
|
115
|
-
expires: '2027-01-31',
|
|
116
|
-
},
|
|
117
105
|
};
|
|
118
106
|
|
|
119
107
|
const ENV_TRUTHY = new Set(['1', 'true', 'on', 'yes']);
|
package/engine/lifecycle.js
CHANGED
|
@@ -17,7 +17,6 @@ const harness = require('./harness');
|
|
|
17
17
|
const reviewLearning = require('./review-learning');
|
|
18
18
|
const smallStateStore = require('./small-state-store');
|
|
19
19
|
const prdStore = require('./prd-store');
|
|
20
|
-
const features = require('./features');
|
|
21
20
|
const memoryStore = require('./memory-store');
|
|
22
21
|
const promotion = require('./promotion');
|
|
23
22
|
const { isBranchActive } = require('./cooldown');
|
|
@@ -3915,7 +3914,6 @@ function createReviewFeedbackForAuthor(reviewerAgentId, pr, config, opts = {}) {
|
|
|
3915
3914
|
const authorAgentId = pr.agent.toLowerCase();
|
|
3916
3915
|
if (!config.agents?.[authorAgentId]) return null;
|
|
3917
3916
|
if (opts.reviewVerdict !== REVIEW_STATUS.CHANGES_REQUESTED) return null;
|
|
3918
|
-
if (!features.isFeatureOn('review-learning-capture', config)) return null;
|
|
3919
3917
|
|
|
3920
3918
|
const project = opts.project || opts.dispatchItem?.meta?.project || null;
|
|
3921
3919
|
if (!project?.name) {
|
|
@@ -4512,7 +4510,6 @@ function _recordRepeatedFindingEvent(lesson, group, match, provenance) {
|
|
|
4512
4510
|
function captureVerifiedReviewLearnings({
|
|
4513
4511
|
dispatchItem,
|
|
4514
4512
|
agentId,
|
|
4515
|
-
config,
|
|
4516
4513
|
structuredCompletion,
|
|
4517
4514
|
branchChange,
|
|
4518
4515
|
reviewFixResolution,
|
|
@@ -4522,7 +4519,6 @@ function captureVerifiedReviewLearnings({
|
|
|
4522
4519
|
const supportedType = dispatchItem?.type === WORK_TYPE.REVIEW
|
|
4523
4520
|
|| shared.isFixLikeWorkType(dispatchItem?.type);
|
|
4524
4521
|
if (!supportedType || !Array.isArray(rawLearnings) || rawLearnings.length === 0) return result;
|
|
4525
|
-
if (!features.isFeatureOn('review-learning-capture', config || {})) return result;
|
|
4526
4522
|
|
|
4527
4523
|
let groups;
|
|
4528
4524
|
try {
|
|
@@ -4715,7 +4711,6 @@ function _resolveDispatchProject(dispatchItem) {
|
|
|
4715
4711
|
function processReviewLearningApplications({
|
|
4716
4712
|
dispatchItem,
|
|
4717
4713
|
agentId,
|
|
4718
|
-
config,
|
|
4719
4714
|
structuredCompletion,
|
|
4720
4715
|
} = {}) {
|
|
4721
4716
|
const result = {
|
|
@@ -4727,9 +4722,6 @@ function processReviewLearningApplications({
|
|
|
4727
4722
|
};
|
|
4728
4723
|
const rawApplications = structuredCompletion?.reviewLearningApplications;
|
|
4729
4724
|
if (!Array.isArray(rawApplications) || rawApplications.length === 0) return result;
|
|
4730
|
-
// Applications describe how recalled lessons fared, so they are only
|
|
4731
|
-
// meaningful when the recall path is active.
|
|
4732
|
-
if (!features.isFeatureOn('review-learning-recall', config || {})) return result;
|
|
4733
4725
|
|
|
4734
4726
|
let applications;
|
|
4735
4727
|
try {
|
|
@@ -6753,7 +6745,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6753
6745
|
captureVerifiedReviewLearnings({
|
|
6754
6746
|
dispatchItem,
|
|
6755
6747
|
agentId,
|
|
6756
|
-
config,
|
|
6757
6748
|
structuredCompletion,
|
|
6758
6749
|
branchChange: prFixBranchChange,
|
|
6759
6750
|
reviewFixResolution,
|
|
@@ -6772,7 +6763,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6772
6763
|
processReviewLearningApplications({
|
|
6773
6764
|
dispatchItem,
|
|
6774
6765
|
agentId,
|
|
6775
|
-
config,
|
|
6776
6766
|
structuredCompletion,
|
|
6777
6767
|
});
|
|
6778
6768
|
} catch (err) {
|
package/engine/playbook.js
CHANGED
|
@@ -784,12 +784,12 @@ function renderPlaybook(type, vars) {
|
|
|
784
784
|
}
|
|
785
785
|
|
|
786
786
|
// Reserved deterministic review-lesson recall (P-a6137579). A dedicated
|
|
787
|
-
// project + audience path, independent of the generic memory budget above
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
if (projectName
|
|
787
|
+
// project + audience path, independent of the generic memory budget above.
|
|
788
|
+
// Review-learning records are globally excluded from generic recall, so this
|
|
789
|
+
// section is the only place they reach a prompt. Only dispatches with a
|
|
790
|
+
// resolved project context are eligible; projectless CC / central work get no
|
|
791
|
+
// section.
|
|
792
|
+
if (projectName) {
|
|
793
793
|
try {
|
|
794
794
|
const applicable = memoryRetrieval.retrieveApplicableReviewLearnings({
|
|
795
795
|
title: vars.item_name || vars.pr_title || '',
|
package/engine/shared.js
CHANGED
|
@@ -972,7 +972,16 @@ function mutateTextFileLocked(filePath, mutateFn, {
|
|
|
972
972
|
safeWrite(filePath, finalText);
|
|
973
973
|
}
|
|
974
974
|
return finalText;
|
|
975
|
-
}, {
|
|
975
|
+
}, {
|
|
976
|
+
retries,
|
|
977
|
+
retryBackoffMs,
|
|
978
|
+
compromiseRedos: LOCK_COMPROMISE_REDOS,
|
|
979
|
+
// Our lock was stolen mid-section, so another writer shared it. Redo the
|
|
980
|
+
// read-modify-write against the current on-disk state only if our own write
|
|
981
|
+
// did not survive — otherwise the update is intact and a redo would just
|
|
982
|
+
// re-apply it.
|
|
983
|
+
onCompromised: (written) => (safeRead(filePath) === written ? 'accept' : 'redo'),
|
|
984
|
+
});
|
|
976
985
|
}
|
|
977
986
|
|
|
978
987
|
function safeUnlink(p) {
|
|
@@ -1406,14 +1415,73 @@ function pruneCrashDiagnosticsReports(config) {
|
|
|
1406
1415
|
return removed;
|
|
1407
1416
|
}
|
|
1408
1417
|
|
|
1418
|
+
/**
|
|
1419
|
+
* Read a lock file's holder payload.
|
|
1420
|
+
*
|
|
1421
|
+
* Returns `{ missing, unreadable, payload }` so callers can tell "the lock is
|
|
1422
|
+
* gone" (definitive — someone removed it) from "we couldn't read it right now"
|
|
1423
|
+
* (transient Windows sharing violation) from "it's there but has no parseable
|
|
1424
|
+
* payload" (legacy lock, or the sub-millisecond window between create and
|
|
1425
|
+
* payload write).
|
|
1426
|
+
*/
|
|
1427
|
+
function _readLockPayload(lockPath) {
|
|
1428
|
+
let raw;
|
|
1429
|
+
try {
|
|
1430
|
+
raw = fs.readFileSync(lockPath, 'utf8');
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
if (err && err.code === 'ENOENT') return { missing: true, unreadable: false, payload: null };
|
|
1433
|
+
return { missing: false, unreadable: true, payload: null };
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const parsed = JSON.parse(raw);
|
|
1437
|
+
if (parsed && typeof parsed === 'object') return { missing: false, unreadable: false, payload: parsed };
|
|
1438
|
+
} catch { /* empty (create/write window) or legacy lock → no payload */ }
|
|
1439
|
+
return { missing: false, unreadable: false, payload: null };
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
/**
|
|
1443
|
+
* Does `payload` describe the same lock acquisition we observed and judged
|
|
1444
|
+
* stale? `observed` is the payload read during the EEXIST inspection.
|
|
1445
|
+
*
|
|
1446
|
+
* Tokens are the authoritative identity. Locks written by an older engine (or
|
|
1447
|
+
* caught in the create/write window) have no token, so fall back to pid — and
|
|
1448
|
+
* treat "still no payload" as "still the same anonymous lock".
|
|
1449
|
+
*/
|
|
1450
|
+
function _isSameLockAcquisition(observed, payload) {
|
|
1451
|
+
if (observed && observed.token) return !!(payload && payload.token === observed.token);
|
|
1452
|
+
if (payload && payload.token) return false; // a fresh tokened lock replaced it
|
|
1453
|
+
if (observed && Number.isFinite(observed.pid)) return !!(payload && payload.pid === observed.pid);
|
|
1454
|
+
return payload === null;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Bounded redo budget for a critical section whose lock was compromised (stolen
|
|
1458
|
+
// by a reaper or otherwise re-owned mid-section). Read-modify-write callers
|
|
1459
|
+
// re-run against the current on-disk state so a raced write is re-applied
|
|
1460
|
+
// instead of being silently lost; see mutateJsonFileLocked/mutateTextFileLocked.
|
|
1461
|
+
const LOCK_COMPROMISE_REDOS = 3;
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Run `fn` under an exclusive file lock at `lockPath`.
|
|
1465
|
+
*
|
|
1466
|
+
* `onCompromised(result)` is consulted when the lock was stolen mid-section
|
|
1467
|
+
* (a reaper removed it, or it is now owned by a different token) — i.e. another
|
|
1468
|
+
* holder shared the critical section. Return `'accept'` to keep `result`, or
|
|
1469
|
+
* `'redo'` to re-run `fn` under a fresh lock (bounded by `compromiseRedos`).
|
|
1470
|
+
* Without the hook a compromised section throws `ELOCKCOMPROMISED` rather than
|
|
1471
|
+
* returning a value that may have been produced from, or clobbered by, a raced
|
|
1472
|
+
* snapshot.
|
|
1473
|
+
*/
|
|
1409
1474
|
function withFileLock(lockPath, fn, {
|
|
1410
1475
|
timeoutMs = 5000,
|
|
1411
1476
|
retryDelayMs = 25,
|
|
1412
1477
|
retries = 0,
|
|
1413
|
-
retryBackoffMs = 1000
|
|
1478
|
+
retryBackoffMs = 1000,
|
|
1479
|
+
compromiseRedos = 0,
|
|
1480
|
+
onCompromised = null
|
|
1414
1481
|
} = {}) {
|
|
1415
1482
|
let lastErr = null;
|
|
1416
1483
|
const maxAttempts = 1 + Math.max(0, retries);
|
|
1484
|
+
let redosLeft = Math.max(0, compromiseRedos);
|
|
1417
1485
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1418
1486
|
if (attempt > 0) {
|
|
1419
1487
|
// Exponential backoff between retry attempts: retryBackoffMs * 2^(attempt-1)
|
|
@@ -1421,19 +1489,32 @@ function withFileLock(lockPath, fn, {
|
|
|
1421
1489
|
sleepMs(backoff);
|
|
1422
1490
|
}
|
|
1423
1491
|
const start = Date.now();
|
|
1424
|
-
let
|
|
1492
|
+
let acquired = false;
|
|
1493
|
+
// Per-acquisition identity. The reaper and the release path both compare
|
|
1494
|
+
// against this so neither ever destroys a lock it does not own — an unlink
|
|
1495
|
+
// keyed only on the path is a TOCTOU that lets two holders into the
|
|
1496
|
+
// critical section at once (W-ms2nbtkj00016ffc).
|
|
1497
|
+
const token = `${process.pid}-${uid()}`;
|
|
1498
|
+
let tokenPersisted = false;
|
|
1425
1499
|
while (Date.now() - start < timeoutMs) {
|
|
1426
1500
|
try {
|
|
1427
1501
|
const dir = path.dirname(lockPath);
|
|
1428
1502
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1429
|
-
fd = fs.openSync(lockPath, 'wx');
|
|
1503
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
1430
1504
|
// P-b7d4e8f2 — record holder identity so the stale-lock reaper can
|
|
1431
1505
|
// distinguish a still-alive slow holder from a crashed one. Best-effort:
|
|
1432
1506
|
// the lock's existence (not its contents) provides mutual exclusion, so
|
|
1433
|
-
// a write failure here must NOT abort acquisition
|
|
1507
|
+
// a write failure here must NOT abort acquisition (it only downgrades us
|
|
1508
|
+
// to the legacy pid-keyed release path).
|
|
1434
1509
|
try {
|
|
1435
|
-
fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
1510
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, ts: Date.now(), token }));
|
|
1511
|
+
tokenPersisted = true;
|
|
1436
1512
|
} catch { /* payload is advisory; lock semantics unaffected */ }
|
|
1513
|
+
// Close immediately: an open handle keeps the file in Windows'
|
|
1514
|
+
// "pending delete" limbo after unlink, which makes waiters keep seeing
|
|
1515
|
+
// (and repeatedly trying to reap) a lock that is already released.
|
|
1516
|
+
try { fs.closeSync(fd); } catch { /* handle is not load-bearing */ }
|
|
1517
|
+
acquired = true;
|
|
1437
1518
|
break;
|
|
1438
1519
|
} catch (err) {
|
|
1439
1520
|
// EEXIST is the cross-platform exclusive-create collision.
|
|
@@ -1453,29 +1534,60 @@ function withFileLock(lockPath, fn, {
|
|
|
1453
1534
|
try {
|
|
1454
1535
|
const stat = fs.statSync(lockPath);
|
|
1455
1536
|
const mtimeAge = Date.now() - stat.mtimeMs;
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) {
|
|
1461
|
-
holderPid = parsed.pid;
|
|
1462
|
-
}
|
|
1463
|
-
} catch { /* legacy/empty/corrupt lock → mtime-only fallback */ }
|
|
1537
|
+
const observed = _readLockPayload(lockPath).payload;
|
|
1538
|
+
const holderPid = observed && Number.isFinite(observed.pid) && observed.pid > 0
|
|
1539
|
+
? observed.pid
|
|
1540
|
+
: null;
|
|
1464
1541
|
|
|
1465
1542
|
const shouldReap = holderPid !== null
|
|
1466
1543
|
? !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5
|
|
1467
1544
|
: mtimeAge > LOCK_STALE_MS;
|
|
1468
1545
|
|
|
1469
1546
|
if (shouldReap) {
|
|
1547
|
+
// Do not race a lock that is simply being released. Require the
|
|
1548
|
+
// SAME acquisition (identified by its token) to still be present
|
|
1549
|
+
// after a short grace delay: a normally-releasing holder's lock
|
|
1550
|
+
// disappears inside that window, and a fresh holder's lock carries
|
|
1551
|
+
// a different token. Without this, the reaper routinely removed a
|
|
1552
|
+
// brand-new live holder's lock in the microseconds after the
|
|
1553
|
+
// previous holder exited (W-ms2nbtkj00016ffc).
|
|
1554
|
+
sleepMs(retryDelayMs);
|
|
1555
|
+
const recheck = _readLockPayload(lockPath);
|
|
1556
|
+
if (recheck.missing || recheck.unreadable
|
|
1557
|
+
|| !_isSameLockAcquisition(observed, recheck.payload)) {
|
|
1558
|
+
continue; // resolved itself — back to normal contention
|
|
1559
|
+
}
|
|
1560
|
+
// Reap by IDENTITY, never by path. Renaming is atomic, so exactly
|
|
1561
|
+
// one racer takes custody and can then verify that what it took is
|
|
1562
|
+
// the acquisition it judged stale.
|
|
1563
|
+
const reapPath = `${lockPath}.reap-${process.pid}-${uid()}`;
|
|
1564
|
+
let reaped = false;
|
|
1470
1565
|
try {
|
|
1471
|
-
fs.
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
const tolerable =
|
|
1477
|
-
(process.platform === 'win32' &&
|
|
1478
|
-
if (!tolerable) throw
|
|
1566
|
+
fs.renameSync(lockPath, reapPath);
|
|
1567
|
+
reaped = true;
|
|
1568
|
+
} catch (reapErr) {
|
|
1569
|
+
// ENOENT: another reaper/holder already removed it — retry.
|
|
1570
|
+
// EPERM on Windows: pending-delete state — retry.
|
|
1571
|
+
const tolerable = reapErr.code === 'ENOENT' ||
|
|
1572
|
+
(process.platform === 'win32' && reapErr.code === 'EPERM');
|
|
1573
|
+
if (!tolerable) throw reapErr;
|
|
1574
|
+
}
|
|
1575
|
+
if (reaped) {
|
|
1576
|
+
const quarantined = _readLockPayload(reapPath).payload;
|
|
1577
|
+
if (_isSameLockAcquisition(observed, quarantined)) {
|
|
1578
|
+
try { fs.unlinkSync(reapPath); } catch { /* already gone */ }
|
|
1579
|
+
} else {
|
|
1580
|
+
// We took custody of a lock we never judged stale — a live
|
|
1581
|
+
// holder replaced the stale one in the last instant. Put it
|
|
1582
|
+
// back and keep waiting. linkSync fails if a newer lock already
|
|
1583
|
+
// exists, which is the correct outcome (that lock is the live
|
|
1584
|
+
// one); the displaced holder then detects the compromise at
|
|
1585
|
+
// release rather than losing its write silently.
|
|
1586
|
+
try { fs.linkSync(reapPath, lockPath); } catch { /* superseded */ }
|
|
1587
|
+
try { fs.unlinkSync(reapPath); } catch { /* already gone */ }
|
|
1588
|
+
sleepMs(retryDelayMs);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1479
1591
|
}
|
|
1480
1592
|
continue; // lock just removed — retry immediately
|
|
1481
1593
|
}
|
|
@@ -1490,31 +1602,64 @@ function withFileLock(lockPath, fn, {
|
|
|
1490
1602
|
sleepMs(retryDelayMs);
|
|
1491
1603
|
}
|
|
1492
1604
|
}
|
|
1493
|
-
if (
|
|
1605
|
+
if (!acquired) {
|
|
1494
1606
|
lastErr = new Error(`Lock timeout: ${lockPath}`);
|
|
1495
1607
|
continue; // retry if attempts remain
|
|
1496
1608
|
}
|
|
1497
1609
|
|
|
1610
|
+
let result;
|
|
1611
|
+
let thrown = null;
|
|
1498
1612
|
try {
|
|
1499
|
-
|
|
1613
|
+
result = fn();
|
|
1500
1614
|
// P-a3f9b2c1 — Defensive: detect a thenable return and throw synchronously.
|
|
1501
|
-
// The
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1615
|
+
// The lock is released immediately after `fn()` returns; an async callback
|
|
1616
|
+
// would let the lock be released before its body completes, silently
|
|
1617
|
+
// breaking mutual exclusion. Release below, then throw so the caller
|
|
1618
|
+
// cannot ignore the failure.
|
|
1505
1619
|
if (result && typeof result.then === 'function') {
|
|
1506
|
-
|
|
1507
|
-
try { fs.unlinkSync(lockPath); } catch { /* cleanup */ }
|
|
1508
|
-
fd = null; // suppress double-cleanup in `finally`
|
|
1620
|
+
result = undefined;
|
|
1509
1621
|
throw new Error('withFileLock: fn must be synchronous; got Promise. Use synchronous operations only.');
|
|
1510
1622
|
}
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
thrown = err;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Release by IDENTITY. If the lock file is gone, or now carries a different
|
|
1628
|
+
// token, a reaper stole it mid-section: someone else was in the critical
|
|
1629
|
+
// section with us, so (a) do NOT unlink — that would destroy the successor's
|
|
1630
|
+
// lock and cascade the breach — and (b) report the breach instead of
|
|
1631
|
+
// silently returning a value that may have been produced from, or clobbered
|
|
1632
|
+
// by, a raced snapshot.
|
|
1633
|
+
let compromised = false;
|
|
1634
|
+
const held = tokenPersisted ? _readLockPayload(lockPath) : null;
|
|
1635
|
+
if (held && held.missing) {
|
|
1636
|
+
compromised = true;
|
|
1637
|
+
} else if (held && held.payload && held.payload.token && held.payload.token !== token) {
|
|
1638
|
+
compromised = true;
|
|
1639
|
+
} else {
|
|
1640
|
+
try {
|
|
1641
|
+
fs.unlinkSync(lockPath);
|
|
1642
|
+
} catch (unlinkErr) {
|
|
1643
|
+
// ENOENT means it was removed under us — same breach as `missing`.
|
|
1644
|
+
if (unlinkErr && unlinkErr.code === 'ENOENT') compromised = true;
|
|
1645
|
+
// Anything else is best-effort; the stale reaper will clear it.
|
|
1516
1646
|
}
|
|
1517
1647
|
}
|
|
1648
|
+
|
|
1649
|
+
if (thrown) throw thrown;
|
|
1650
|
+
if (compromised) {
|
|
1651
|
+
if (typeof onCompromised === 'function' && redosLeft > 0) {
|
|
1652
|
+
redosLeft--;
|
|
1653
|
+
if (onCompromised(result) !== 'redo') return result;
|
|
1654
|
+
attempt--; // a compromised section is not a failed acquisition attempt
|
|
1655
|
+
sleepMs(retryDelayMs);
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
const err = new Error(`Lock compromised (concurrent holder detected): ${lockPath}`);
|
|
1659
|
+
err.code = 'ELOCKCOMPROMISED';
|
|
1660
|
+
throw err;
|
|
1661
|
+
}
|
|
1662
|
+
return result;
|
|
1518
1663
|
}
|
|
1519
1664
|
throw lastErr;
|
|
1520
1665
|
}
|
|
@@ -1640,7 +1785,21 @@ function mutateJsonFileLocked(filePath, mutateFn, {
|
|
|
1640
1785
|
}
|
|
1641
1786
|
throw error;
|
|
1642
1787
|
}
|
|
1643
|
-
}, {
|
|
1788
|
+
}, {
|
|
1789
|
+
retries,
|
|
1790
|
+
retryBackoffMs,
|
|
1791
|
+
compromiseRedos: LOCK_COMPROMISE_REDOS,
|
|
1792
|
+
// Our lock was stolen mid-section, so another writer shared it and one of
|
|
1793
|
+
// the two updates was clobbered. Redo the read-modify-write against the
|
|
1794
|
+
// current on-disk state only if our own write did not survive; the other
|
|
1795
|
+
// writer detects and redoes its own loss the same way, so the union is
|
|
1796
|
+
// preserved without double-applying an intact mutation.
|
|
1797
|
+
onCompromised: (written) => {
|
|
1798
|
+
if (written === null) return fs.existsSync(filePath) ? 'redo' : 'accept';
|
|
1799
|
+
const current = safeJsonNoRestore(filePath);
|
|
1800
|
+
return JSON.stringify(current) === JSON.stringify(written) ? 'accept' : 'redo';
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
1644
1803
|
}
|
|
1645
1804
|
|
|
1646
1805
|
function mutateControl(mutator) {
|
|
@@ -9264,6 +9423,9 @@ module.exports = {
|
|
|
9264
9423
|
resolveDispatchPrompt,
|
|
9265
9424
|
deleteDispatchPromptSidecar,
|
|
9266
9425
|
withFileLock,
|
|
9426
|
+
_readLockPayload, // exported for testing
|
|
9427
|
+
_isSameLockAcquisition, // exported for testing
|
|
9428
|
+
LOCK_COMPROMISE_REDOS, // exported for testing
|
|
9267
9429
|
mutateJsonFileLocked,
|
|
9268
9430
|
mutateControl, recordEngineRespawn,
|
|
9269
9431
|
mutateEngineState, // W-mp60tw0u000j3931
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2429",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|