forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7
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/AGENTS.md +4 -0
- package/CHANGELOG.md +48 -0
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/bin/forge.js +12 -1
- package/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +4 -4
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/shepherd.md +63 -13
- package/lib/adapters/pr-state-adapter.js +15 -2
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +48 -18
- package/lib/commands/clean.js +57 -1
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +54 -6
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +720 -73
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +2 -0
- package/lib/commands/preflight.js +10 -2
- package/lib/commands/push.js +120 -9
- package/lib/commands/recall.js +95 -61
- package/lib/commands/release.js +23 -2
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +132 -4
- package/lib/commands/shepherd.js +578 -72
- package/lib/commands/ship.js +15 -69
- package/lib/commands/skill.js +8 -0
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +163 -4
- package/lib/commands/validate.js +78 -22
- package/lib/commands/worktree.js +155 -19
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-issues.js +78 -0
- package/lib/gate-events.js +98 -10
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/hook-renderer.js +29 -1
- package/lib/issue-render.js +19 -0
- package/lib/kernel/broker.js +723 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3322 -183
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +94 -27
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +59 -0
- package/lib/merge-rules.js +135 -17
- package/lib/npm-publish-workflow.js +233 -40
- package/lib/package-root.js +2 -0
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +0 -37
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1101 -625
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +121 -24
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +28 -19
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +144 -38
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-shepherd.js +17 -3
- package/lib/project-memory.js +145 -3
- package/lib/protected-state-authority.js +799 -4
- package/lib/protected-state-surfaces.js +181 -3
- package/lib/release-readiness.js +2 -3
- package/lib/review-adapter.js +65 -0
- package/lib/skills-sync.js +65 -32
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/validation-receipt.js +190 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +16 -4
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/commitlint.js +13 -15
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runtime.js +3 -2
- package/scripts/process-tree.js +14 -2
- package/scripts/protected-state-check.js +440 -17
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/test-full-suite.js +704 -18
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +96 -15
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +2 -0
- package/skills/review/evals/scorecard.json +2 -2
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +19 -2
- package/skills/shepherd/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
package/lib/pr-monitor/watch.js
CHANGED
|
@@ -15,29 +15,23 @@
|
|
|
15
15
|
* and only pushed if the next tick did not recover/green it — the JOURNAL still
|
|
16
16
|
* records everything (authority), so `events --since` replay stays complete.
|
|
17
17
|
*
|
|
18
|
-
* Every external effect is injectable (emit/sleep/rng/now/gather/
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Every external effect is injectable (emit/sleep/rng/now/gather/owner authority,
|
|
19
|
+
* plus `maxPasses`/`signal`) so tests exercise the loop with a fake clock and fake
|
|
20
|
+
* provider, never touching live GitHub or waiting 60s.
|
|
21
21
|
*
|
|
22
22
|
* @module pr-monitor/watch
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
const { runMonitorPass } = require('./monitor');
|
|
26
26
|
const journal = require('./journal');
|
|
27
|
+
const watchOwner = require('./watch-owner');
|
|
27
28
|
const { EVENT_TYPES: T } = require('./events');
|
|
28
29
|
|
|
29
30
|
const DEFAULT_INTERVAL_MS = 60000;
|
|
31
|
+
const DEFAULT_OWNER_HEARTBEAT_MS = 60000;
|
|
30
32
|
const JITTER_RATIO = 0.2;
|
|
31
33
|
const TERMINAL_TYPES = new Set([T.PR_MERGED, T.PR_CLOSED]);
|
|
32
34
|
|
|
33
|
-
// In-process ownership of the watcher slot, keyed by journal dir. The journal
|
|
34
|
-
// PID file serializes claims ACROSS processes, but it cannot make claims within
|
|
35
|
-
// ONE process idempotent: after writePid, `watcherRunning(dir)` returns false
|
|
36
|
-
// because the PID equals process.pid, so a second same-process watchLoop() would
|
|
37
|
-
// also claim, run in parallel, and (on either exit) remove the shared PID while
|
|
38
|
-
// the other is still active. This Set closes that gap for the loop lifetime.
|
|
39
|
-
const ACTIVE_DIRS = new Set();
|
|
40
|
-
|
|
41
35
|
/** Default push sink: one NDJSON line per event to stdout. */
|
|
42
36
|
function defaultEmit(event) {
|
|
43
37
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
@@ -78,30 +72,6 @@ function isAborted(signal) {
|
|
|
78
72
|
return Boolean(signal && signal.aborted);
|
|
79
73
|
}
|
|
80
74
|
|
|
81
|
-
/**
|
|
82
|
-
* Atomically claim the watcher slot for this PR. The `watcherRunning` check and
|
|
83
|
-
* the `writePid` write run TOGETHER inside the cross-process journal lock (so two
|
|
84
|
-
* starts in DIFFERENT processes can never both pass), AND the in-process
|
|
85
|
-
* `ACTIVE_DIRS` guard rejects a SECOND concurrent `watchLoop()` in the SAME
|
|
86
|
-
* process (which the PID file alone misses, since our own PID reads as "not
|
|
87
|
-
* running"). Returns true only when the slot was newly claimed; the caller must
|
|
88
|
-
* release with `releaseClaim(dir)` on loop exit. The (possibly injected)
|
|
89
|
-
* `watcherRunning`/`writePid` primitives are honored, so test injection works.
|
|
90
|
-
*/
|
|
91
|
-
function defaultClaim(dir, { watcherRunning, writePid, lockOpts }) {
|
|
92
|
-
return journal.withJournalLock(dir, () => {
|
|
93
|
-
if (ACTIVE_DIRS.has(dir) || watcherRunning(dir)) return false;
|
|
94
|
-
writePid(dir);
|
|
95
|
-
ACTIVE_DIRS.add(dir);
|
|
96
|
-
return true;
|
|
97
|
-
}, lockOpts);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Release the in-process watcher claim for `dir` (idempotent). */
|
|
101
|
-
function releaseClaim(dir) {
|
|
102
|
-
ACTIVE_DIRS.delete(dir);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
75
|
/**
|
|
106
76
|
* Split a pass's events into: held-candidate failures (by check name), terminal
|
|
107
77
|
* events (pr.merged/closed), and everything else (emitted immediately).
|
|
@@ -144,6 +114,20 @@ function confirmHeld(pending, contra, emit) {
|
|
|
144
114
|
}
|
|
145
115
|
}
|
|
146
116
|
|
|
117
|
+
function emitTerminalPass(state, events, emit) {
|
|
118
|
+
const { failures, terminal, others } = partition(events);
|
|
119
|
+
const contra = contradictions(events);
|
|
120
|
+
const heldNames = new Set(state.pending.keys());
|
|
121
|
+
confirmHeld(state.pending, contra, emit);
|
|
122
|
+
state.pending = new Map();
|
|
123
|
+
for (const event of others) {
|
|
124
|
+
if (event.type === T.CHECK_RECOVERED && heldNames.has(checkName(event))) continue;
|
|
125
|
+
emit(event);
|
|
126
|
+
}
|
|
127
|
+
for (const failure of failures.values()) emit(failure);
|
|
128
|
+
for (const event of terminal) emit(event);
|
|
129
|
+
}
|
|
130
|
+
|
|
147
131
|
/**
|
|
148
132
|
* Run ONE watch tick: a bounded monitor pass, then stream its events with the
|
|
149
133
|
* 2-pass flap debounce applied. Mutates `state.pending` (the held failures).
|
|
@@ -151,10 +135,18 @@ function confirmHeld(pending, contra, emit) {
|
|
|
151
135
|
* @returns {Promise<{ terminal: boolean }>} terminal=true → stop the loop.
|
|
152
136
|
*/
|
|
153
137
|
async function runWatchPass(state, ctx, emit) {
|
|
154
|
-
const
|
|
155
|
-
|
|
138
|
+
const monitorPass = ctx.runMonitorPass || runMonitorPass;
|
|
139
|
+
const { events: rawEvents, terminalReceiptId, journalCursor } = await monitorPass({
|
|
140
|
+
...ctx, dir: ctx.dir, gather: ctx.gather, now: ctx.now, enrich: ctx.enrich, lockOpts: ctx.lockOpts,
|
|
156
141
|
});
|
|
142
|
+
const events = ctx.dir && Number.isSafeInteger(journalCursor)
|
|
143
|
+
? journal.readEventsSince(ctx.dir, journalCursor)
|
|
144
|
+
: rawEvents;
|
|
157
145
|
const { failures, terminal, others } = partition(events);
|
|
146
|
+
if (terminalReceiptId) {
|
|
147
|
+
emitTerminalPass(state, events, emit);
|
|
148
|
+
return { terminal: true, terminalReceiptId };
|
|
149
|
+
}
|
|
158
150
|
const contra = contradictions(events);
|
|
159
151
|
const heldNames = new Set(state.pending.keys());
|
|
160
152
|
|
|
@@ -168,7 +160,7 @@ async function runWatchPass(state, ctx, emit) {
|
|
|
168
160
|
if (terminal.length) {
|
|
169
161
|
for (const failure of failures.values()) emit(failure);
|
|
170
162
|
for (const t of terminal) emit(t);
|
|
171
|
-
return { terminal: true };
|
|
163
|
+
return { terminal: false, receiptUnavailable: true };
|
|
172
164
|
}
|
|
173
165
|
state.pending = failures;
|
|
174
166
|
return { terminal: false };
|
|
@@ -192,51 +184,130 @@ async function runWatchPass(state, ctx, emit) {
|
|
|
192
184
|
* @returns {Promise<{ started: boolean, passes: number, stopped: boolean, reason?: string }>}
|
|
193
185
|
*/
|
|
194
186
|
async function watchLoop(ctx) {
|
|
195
|
-
const { dir } = ctx;
|
|
196
187
|
const emit = ctx.emit || defaultEmit;
|
|
197
188
|
const sleep = ctx.sleep || defaultSleep;
|
|
198
189
|
const rng = ctx.rng || Math.random;
|
|
199
190
|
const intervalMs = ctx.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
191
|
+
const ownerHeartbeatMs = ctx.ownerHeartbeatMs ?? DEFAULT_OWNER_HEARTBEAT_MS;
|
|
200
192
|
const maxPasses = ctx.maxPasses ?? Infinity;
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
193
|
+
const owner = ctx.owner || watchOwner;
|
|
194
|
+
const ownerOptions = ctx.ownerOptions || {};
|
|
195
|
+
const identity = { repo: String(ctx.repo || '').toLowerCase(), pr: Number(ctx.pr) };
|
|
196
|
+
const generation = ctx.generation;
|
|
197
|
+
const controllerPid = Number(ctx.controllerPid);
|
|
198
|
+
const pid = Number(ctx.pid ?? process.pid);
|
|
199
|
+
|
|
200
|
+
// Exact generation binding makes a duplicate or stale child a clean no-op.
|
|
201
|
+
if (!Number.isSafeInteger(identity.pr) || identity.pr <= 0 || !identity.repo
|
|
202
|
+
|| !generation || !Number.isSafeInteger(controllerPid) || controllerPid <= 0
|
|
203
|
+
|| !Number.isSafeInteger(pid) || pid <= 0 || typeof owner.bindRunning !== 'function') {
|
|
204
|
+
return { started: false, passes: 0, stopped: false, reason: 'authority-unavailable' };
|
|
205
|
+
}
|
|
206
|
+
const bound = await owner.bindRunning(identity, { generation, controllerPid, pid }, ownerOptions);
|
|
207
|
+
if (!bound?.ok) {
|
|
208
|
+
return { started: false, passes: 0, stopped: false, reason: bound?.reason || 'bind-failed' };
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
const state = { pending: new Map() };
|
|
215
212
|
let passes = 0;
|
|
216
213
|
let stopped = false;
|
|
214
|
+
let terminalPending = false;
|
|
215
|
+
let reason;
|
|
216
|
+
let heartbeatFailure;
|
|
217
|
+
let heartbeatInFlight = null;
|
|
218
|
+
const checkpointOwner = () => {
|
|
219
|
+
if (heartbeatInFlight) return heartbeatInFlight;
|
|
220
|
+
heartbeatInFlight = (async () => {
|
|
221
|
+
try {
|
|
222
|
+
const checkpoint = await owner.heartbeat(identity, { generation, pid }, ownerOptions);
|
|
223
|
+
if (!checkpoint?.ok) heartbeatFailure = checkpoint?.reason || 'owner-lost';
|
|
224
|
+
} catch {
|
|
225
|
+
heartbeatFailure = 'owner-lost';
|
|
226
|
+
} finally {
|
|
227
|
+
heartbeatInFlight = null;
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
return heartbeatInFlight;
|
|
231
|
+
};
|
|
232
|
+
const scheduleHeartbeat = ctx.setInterval || setInterval;
|
|
233
|
+
const cancelHeartbeat = ctx.clearInterval || clearInterval;
|
|
234
|
+
const heartbeatTimer = scheduleHeartbeat(() => { void checkpointOwner(); }, ownerHeartbeatMs);
|
|
235
|
+
heartbeatTimer?.unref?.();
|
|
217
236
|
try {
|
|
218
237
|
while (passes < maxPasses && !isAborted(ctx.signal)) {
|
|
238
|
+
if (heartbeatFailure) {
|
|
239
|
+
reason = heartbeatFailure;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
const current = await owner.readOwner(identity, ownerOptions);
|
|
243
|
+
if (!current?.ok || current.record?.generation !== generation || current.record?.watcherPid !== pid) {
|
|
244
|
+
reason = current?.reason || 'owner-lost';
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
if (current.record.phase === 'stop_requested') {
|
|
248
|
+
stopped = true;
|
|
249
|
+
reason = 'stop-requested';
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
if (current.record.phase === 'terminal_pending') {
|
|
253
|
+
stopped = true;
|
|
254
|
+
terminalPending = true;
|
|
255
|
+
reason = 'terminal-pending';
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
if (current.record.phase !== 'running') {
|
|
259
|
+
reason = 'owner-not-running';
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
219
262
|
const result = await runWatchPass(state, ctx, emit);
|
|
220
263
|
passes += 1;
|
|
221
|
-
if (result.terminal) {
|
|
264
|
+
if (result.terminal) {
|
|
265
|
+
// Keep the exact running row for dead-owner receipt recovery if this write fails.
|
|
266
|
+
terminalPending = true;
|
|
267
|
+
const recorded = await owner.recordTerminal(identity, {
|
|
268
|
+
generation, pid, terminalReceiptId: result.terminalReceiptId,
|
|
269
|
+
}, ownerOptions);
|
|
270
|
+
if (recorded?.ok) {
|
|
271
|
+
stopped = true;
|
|
272
|
+
terminalPending = true;
|
|
273
|
+
reason = 'terminal-pending';
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
reason = recorded?.reason || 'receipt-unavailable';
|
|
277
|
+
}
|
|
278
|
+
await checkpointOwner();
|
|
279
|
+
if (heartbeatFailure) {
|
|
280
|
+
reason = heartbeatFailure;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
222
283
|
if (passes >= maxPasses || isAborted(ctx.signal)) break;
|
|
223
284
|
await sleep(jitter(intervalMs, rng), ctx.signal);
|
|
224
285
|
}
|
|
225
286
|
} finally {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
287
|
+
cancelHeartbeat(heartbeatTimer);
|
|
288
|
+
await heartbeatInFlight;
|
|
289
|
+
// Terminal ownership remains until a later dead-PID completion transaction;
|
|
290
|
+
// every other process exit releases its exact generation cooperatively.
|
|
291
|
+
if (!terminalPending) {
|
|
292
|
+
try {
|
|
293
|
+
const stopRequested = typeof owner.requestStop === 'function'
|
|
294
|
+
? await owner.requestStop(identity, { generation, pid }, ownerOptions)
|
|
295
|
+
: { ok: true };
|
|
296
|
+
if (stopRequested?.ok) {
|
|
297
|
+
await owner.releaseNonterminal(identity, { generation, pid }, ownerOptions);
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
// Preserve the owner row for a later dead-watcher recovery transaction.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
230
303
|
}
|
|
231
|
-
return { started: true, passes, stopped };
|
|
304
|
+
return { started: true, passes, stopped, ...(reason ? { reason } : {}) };
|
|
232
305
|
}
|
|
233
306
|
|
|
234
307
|
module.exports = {
|
|
235
308
|
watchLoop,
|
|
236
309
|
runWatchPass,
|
|
237
310
|
defaultSleep,
|
|
238
|
-
defaultClaim,
|
|
239
|
-
releaseClaim,
|
|
240
311
|
jitter,
|
|
241
312
|
partition,
|
|
242
313
|
contradictions,
|
package/lib/pr-shepherd.js
CHANGED
|
@@ -211,7 +211,7 @@ async function handleFailedRequired({
|
|
|
211
211
|
* @returns {Promise<object>} decision envelope.
|
|
212
212
|
*/
|
|
213
213
|
async function handleBehindBase({
|
|
214
|
-
behind, autoRebase, cleanTree, adapter, baseRef, headUnchanged, actions,
|
|
214
|
+
behind, autoRebase, cleanTree, adapter, baseRef, expectedHead, headUnchanged, actions,
|
|
215
215
|
}) {
|
|
216
216
|
if (!autoRebase) {
|
|
217
217
|
return result('ESCALATE', {
|
|
@@ -240,10 +240,22 @@ async function handleBehindBase({
|
|
|
240
240
|
});
|
|
241
241
|
}
|
|
242
242
|
try {
|
|
243
|
-
await adapter.rebaseOntoBase({ baseRef });
|
|
243
|
+
const rebaseResult = await adapter.rebaseOntoBase({ baseRef, expectedHead });
|
|
244
|
+
const verifiedHead = rebaseResult?.previousHead === expectedHead
|
|
245
|
+
&& typeof rebaseResult?.headSha === 'string'
|
|
246
|
+
&& /^[0-9a-f]{40}$/i.test(rebaseResult.headSha)
|
|
247
|
+
? rebaseResult.headSha.toLowerCase()
|
|
248
|
+
: null;
|
|
249
|
+
if (!verifiedHead) {
|
|
250
|
+
return result('ESCALATE', {
|
|
251
|
+
actions,
|
|
252
|
+
reason: 'Rebase completed without a lease-bound post-action head; refusing convergence handoff.',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
244
255
|
actions.push({ type: 'rebase', baseRef });
|
|
245
256
|
return result('PENDING', {
|
|
246
257
|
actions,
|
|
258
|
+
expectedHead: verifiedHead,
|
|
247
259
|
reason: 'Rebased onto base and force-pushed with lease. Awaiting CI on the next scheduled pass.',
|
|
248
260
|
});
|
|
249
261
|
} catch (error) {
|
|
@@ -478,7 +490,8 @@ async function runShepherdPass(ctx) {
|
|
|
478
490
|
// never rebase — force autoRebase off so the branch-behind path only escalates. ---
|
|
479
491
|
if (behind > 0) {
|
|
480
492
|
return handleBehindBase({
|
|
481
|
-
behind, autoRebase: dryRun ? false : autoRebase, cleanTree, adapter, baseRef,
|
|
493
|
+
behind, autoRebase: dryRun ? false : autoRebase, cleanTree, adapter, baseRef,
|
|
494
|
+
expectedHead: startSha, headUnchanged, actions,
|
|
482
495
|
});
|
|
483
496
|
}
|
|
484
497
|
|
|
@@ -500,6 +513,7 @@ async function runShepherdPass(ctx) {
|
|
|
500
513
|
}
|
|
501
514
|
return result('MERGE_READY', {
|
|
502
515
|
actions,
|
|
516
|
+
expectedHead: startSha,
|
|
503
517
|
reason: 'All required checks are green and the branch is up to date. Handing off to the human to merge in the GitHub UI — the shepherd never merges.',
|
|
504
518
|
});
|
|
505
519
|
}
|
package/lib/project-memory.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
+
const { createHash, randomUUID } = require('node:crypto');
|
|
6
|
+
const { types: { isProxy } } = require('node:util');
|
|
5
7
|
const { resolveKernelDatabasePath } = require('./kernel/cli-broker-factory');
|
|
6
8
|
const { resolveGitCommonDir } = require('./kernel/broker');
|
|
7
9
|
const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
|
|
8
10
|
const { normalizeRecallHit } = require('./memory-recall');
|
|
11
|
+
const { createUsageEvidenceStore } = require('@forge/memory');
|
|
9
12
|
|
|
10
13
|
// Project memory is a Forge read model persisted in the kernel store (kernel_memories),
|
|
11
14
|
// written DIRECTLY rather than through the issue CAS/guarded-event path. The store seam
|
|
@@ -146,20 +149,28 @@ function list(projectRoot, options = {}) {
|
|
|
146
149
|
// driver so recall never loads and re-sorts the whole table. `options.agents` (a
|
|
147
150
|
// source_agent allow-list) scopes the read, e.g. to human `remember` notes only.
|
|
148
151
|
function recent(projectRoot, limit, options = {}) {
|
|
149
|
-
return resolveStore(projectRoot, options).recentMemories(limit, {
|
|
152
|
+
return resolveStore(projectRoot, options).recentMemories(limit, {
|
|
153
|
+
agents: options.agents,
|
|
154
|
+
kind: options.kind,
|
|
155
|
+
});
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// Total stored memories (optionally scoped by `options.agents`) — paired with `recent` so
|
|
153
159
|
// recall can report "showing N of TOTAL".
|
|
154
160
|
function count(projectRoot, options = {}) {
|
|
155
|
-
return resolveStore(projectRoot, options).countMemories({
|
|
161
|
+
return resolveStore(projectRoot, options).countMemories({
|
|
162
|
+
agents: options.agents,
|
|
163
|
+
kind: options.kind,
|
|
164
|
+
});
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
// BM25 top-N recall over the FTS5 index (token-AND). Unlike `search` (the legacy LIKE
|
|
159
168
|
// helper) this does not short-circuit an empty query — the driver falls back to recent so
|
|
160
169
|
// recall stays capped either way.
|
|
161
170
|
function searchRanked(projectRoot, query, limit, options = {}) {
|
|
162
|
-
return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit
|
|
171
|
+
return resolveStore(projectRoot, options).searchMemoriesRanked(query, limit, {
|
|
172
|
+
kind: options.kind,
|
|
173
|
+
});
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
// Relevance-only BM25 recall that returns the raw bm25 `score` per entry, so a caller can
|
|
@@ -178,6 +189,131 @@ function searchRankedScored(projectRoot, query, limit, options = {}) {
|
|
|
178
189
|
.map(hit => normalizeRecallHit(hit, projectId));
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
function opaqueUsageIdentity(kind, value) {
|
|
193
|
+
return createHash('sha256').update(`${kind}\0${value}`).digest('hex');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function memoryUsageIdentity(key) {
|
|
197
|
+
return opaqueUsageIdentity('forge.memory.key.v1', key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function ownDataValue(value, field) {
|
|
201
|
+
if (!value || typeof value !== 'object' || isProxy(value)) return undefined;
|
|
202
|
+
let descriptor;
|
|
203
|
+
try { descriptor = Object.getOwnPropertyDescriptor(value, field); } catch { return undefined; }
|
|
204
|
+
return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function usageResolutionOptions(options) {
|
|
208
|
+
const resolutionOptions = {};
|
|
209
|
+
for (const field of ['store', 'gitCommonDir', 'platform', 'realpath', 'databasePath']) {
|
|
210
|
+
const option = ownDataValue(options, field);
|
|
211
|
+
if (option !== undefined) resolutionOptions[field] = option;
|
|
212
|
+
}
|
|
213
|
+
return resolutionOptions;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function usageInvocationId(options) {
|
|
217
|
+
const value = ownDataValue(options, 'invocationId');
|
|
218
|
+
return typeof value === 'string' && value ? value : randomUUID();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function usageObservedAt(options) {
|
|
222
|
+
const invocationStartedAt = ownDataValue(options, 'invocationStartedAt');
|
|
223
|
+
if (typeof invocationStartedAt === 'string') return invocationStartedAt;
|
|
224
|
+
const now = ownDataValue(options, 'now');
|
|
225
|
+
if (typeof now === 'string') return now;
|
|
226
|
+
return new Date().toISOString();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function usageMemoryIdentities(notes) {
|
|
230
|
+
return notes
|
|
231
|
+
.map(note => ownDataValue(note, 'id'))
|
|
232
|
+
.filter(id => typeof id === 'string' && id.length > 0)
|
|
233
|
+
.map(memoryUsageIdentity);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function appendRecallUsage(store, identities, invocationId, scope, selectionDigest, observedAt) {
|
|
237
|
+
let appended = 0;
|
|
238
|
+
let failed = 0;
|
|
239
|
+
for (const memoryId of identities) {
|
|
240
|
+
try {
|
|
241
|
+
const result = store.append({
|
|
242
|
+
event_id: opaqueUsageIdentity('forge.memory.used.event.v1', `${invocationId}\0${memoryId}`),
|
|
243
|
+
memory_id: memoryId,
|
|
244
|
+
scope,
|
|
245
|
+
use_kind: 'cli-recall',
|
|
246
|
+
consumer_id: 'cli-recall',
|
|
247
|
+
selection_digest: selectionDigest,
|
|
248
|
+
observed_at: observedAt,
|
|
249
|
+
idempotency_key: opaqueUsageIdentity('forge.memory.used.idempotency.v1', `${invocationId}\0${memoryId}`),
|
|
250
|
+
});
|
|
251
|
+
if (result?.appended) appended += 1;
|
|
252
|
+
} catch {
|
|
253
|
+
// Evidence is advisory: a storage failure must never hide a useful recall.
|
|
254
|
+
failed += 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return { attempted: identities.length, appended, failed };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Recall evidence deliberately accepts only the returned note identifiers. It hashes those
|
|
261
|
+
// identifiers immediately, so neither a user path, query, nor note content can reach storage.
|
|
262
|
+
function recordRecallUsage(projectRoot, selected, options = {}) {
|
|
263
|
+
const notes = Array.isArray(selected) ? selected : [];
|
|
264
|
+
if (notes.length === 0) return { attempted: 0, appended: 0, failed: 0 };
|
|
265
|
+
const usageStore = ownDataValue(options, 'usageStore');
|
|
266
|
+
const resolutionOptions = usageResolutionOptions(options);
|
|
267
|
+
const invocationId = usageInvocationId(options);
|
|
268
|
+
const observedAt = usageObservedAt(options);
|
|
269
|
+
const identities = usageMemoryIdentities(notes);
|
|
270
|
+
if (identities.length === 0) return { attempted: 0, appended: 0, failed: 0 };
|
|
271
|
+
let scope;
|
|
272
|
+
let selectionDigest;
|
|
273
|
+
let store;
|
|
274
|
+
try {
|
|
275
|
+
scope = opaqueUsageIdentity('forge.memory.scope.v1', resolveProjectId(projectRoot, resolutionOptions));
|
|
276
|
+
selectionDigest = opaqueUsageIdentity('forge.memory.selection.v1', identities.join('\0'));
|
|
277
|
+
store = createUsageEvidenceStore(usageStore || resolveStore(projectRoot, resolutionOptions));
|
|
278
|
+
} catch {
|
|
279
|
+
return { attempted: identities.length, appended: 0, failed: identities.length };
|
|
280
|
+
}
|
|
281
|
+
return appendRecallUsage(store, identities, invocationId, scope, selectionDigest, observedAt);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function usageProjection(projectRoot, key, options = {}) {
|
|
285
|
+
if (typeof key !== 'string' || !key) return null;
|
|
286
|
+
try {
|
|
287
|
+
const usageStore = ownDataValue(options, 'usageStore');
|
|
288
|
+
return createUsageEvidenceStore(usageStore || resolveStore(projectRoot, options))
|
|
289
|
+
.projection(opaqueUsageIdentity('forge.memory.key.v1', key));
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function usageProjectionStatus(projectRoot, keys, options = {}) {
|
|
296
|
+
if (!Array.isArray(keys) || keys.length === 0) return { available: true, projections: new Map() };
|
|
297
|
+
const rawKeys = [...new Set(keys.filter(key => typeof key === 'string' && key))].slice(0, 200);
|
|
298
|
+
if (rawKeys.length === 0) return { available: true, projections: new Map() };
|
|
299
|
+
const usageStore = ownDataValue(options, 'usageStore');
|
|
300
|
+
const identities = rawKeys.map(memoryUsageIdentity);
|
|
301
|
+
try {
|
|
302
|
+
const rows = createUsageEvidenceStore(usageStore || resolveStore(projectRoot, options)).projections(identities);
|
|
303
|
+
const keyByIdentity = new Map(rawKeys.map((key, index) => [identities[index], key]));
|
|
304
|
+
return {
|
|
305
|
+
available: true,
|
|
306
|
+
projections: new Map(rows.map(row => [keyByIdentity.get(row.memory_id), row]).filter(([key]) => key)),
|
|
307
|
+
};
|
|
308
|
+
} catch {
|
|
309
|
+
return { available: false, projections: new Map() };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function usageProjections(projectRoot, keys, options = {}) {
|
|
314
|
+
return usageProjectionStatus(projectRoot, keys, options).projections;
|
|
315
|
+
}
|
|
316
|
+
|
|
181
317
|
// Close and forget every cached default store. The CLI process is short-lived (the OS
|
|
182
318
|
// closes the handle on exit), so this is mainly a lifecycle helper for long-lived hosts and
|
|
183
319
|
// tests — it releases the SQLite/WAL handle before a temp dir is removed.
|
|
@@ -201,6 +337,12 @@ module.exports = {
|
|
|
201
337
|
count,
|
|
202
338
|
searchRanked,
|
|
203
339
|
searchRankedScored,
|
|
340
|
+
recordRecallUsage,
|
|
341
|
+
usageProjection,
|
|
342
|
+
usageProjectionStatus,
|
|
343
|
+
usageProjections,
|
|
344
|
+
memoryUsageIdentity,
|
|
345
|
+
resolveStore,
|
|
204
346
|
resolveProjectId,
|
|
205
347
|
closeAll,
|
|
206
348
|
};
|