@entelligentsia/forgecli 1.0.14 → 1.0.20
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 +142 -0
- package/dist/CHANGELOG-forge-plugin.md +61 -0
- package/dist/extensions/forgecli/config-layer.d.ts +0 -16
- package/dist/extensions/forgecli/config-layer.js +0 -5
- package/dist/extensions/forgecli/config-layer.js.map +1 -1
- package/dist/extensions/forgecli/context-governor-compaction.d.ts +83 -0
- package/dist/extensions/forgecli/context-governor-compaction.js +302 -0
- package/dist/extensions/forgecli/context-governor-compaction.js.map +1 -0
- package/dist/extensions/forgecli/context-governor.d.ts +173 -0
- package/dist/extensions/forgecli/context-governor.js +618 -0
- package/dist/extensions/forgecli/context-governor.js.map +1 -0
- package/dist/extensions/forgecli/dashboard/component.d.ts +7 -4
- package/dist/extensions/forgecli/dashboard/component.js +80 -101
- package/dist/extensions/forgecli/dashboard/component.js.map +1 -1
- package/dist/extensions/forgecli/dashboard/register.js +7 -21
- package/dist/extensions/forgecli/dashboard/register.js.map +1 -1
- package/dist/extensions/forgecli/dashboard/theme.d.ts +27 -0
- package/dist/extensions/forgecli/dashboard/theme.js +91 -0
- package/dist/extensions/forgecli/dashboard/theme.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +59 -5
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js +2 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +0 -4
- package/dist/extensions/forgecli/forge-subagent.d.ts +20 -1
- package/dist/extensions/forgecli/forge-subagent.js +17 -3
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-tools.js +3 -1
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/hook-dispatcher.d.ts +3 -1
- package/dist/extensions/forgecli/hook-dispatcher.js +37 -3
- package/dist/extensions/forgecli/hook-dispatcher.js.map +1 -1
- package/dist/extensions/forgecli/index.js +33 -1
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/lib/halt-advisor.d.ts +19 -14
- package/dist/extensions/forgecli/lib/halt-advisor.js +36 -13
- package/dist/extensions/forgecli/lib/halt-advisor.js.map +1 -1
- package/dist/extensions/forgecli/orchestrator-status-bar.d.ts +3 -2
- package/dist/extensions/forgecli/orchestrator-status-bar.js +90 -60
- package/dist/extensions/forgecli/orchestrator-status-bar.js.map +1 -1
- package/dist/extensions/forgecli/run-sprint.d.ts +3 -1
- package/dist/extensions/forgecli/run-sprint.js +1 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.d.ts +34 -1
- package/dist/extensions/forgecli/run-task.js +144 -6
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.d.ts +4 -1
- package/dist/extensions/forgecli/thread-switcher.js +36 -21
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/forge-payload/.base-pack/workflows/implement_plan.md +9 -0
- package/dist/forge-payload/.base-pack/workflows/plan_task.md +7 -0
- package/dist/forge-payload/.base-pack/workflows/review_code.md +4 -3
- package/dist/forge-payload/.base-pack/workflows/review_plan.md +4 -3
- package/dist/forge-payload/.base-pack/workflows/validate_task.md +4 -3
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/migrations.json +132 -27
- package/dist/forge-payload/meta/workflows/meta-review-implementation.md +4 -3
- package/dist/forge-payload/meta/workflows/meta-review-plan.md +4 -3
- package/dist/forge-payload/meta/workflows/meta-validate.md +4 -3
- package/dist/forge-payload/tools/collate.cjs +32 -0
- package/dist/forge-payload/tools/postflight-gate.cjs +56 -10
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,148 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.20] — 2026-06-04
|
|
11
|
+
|
|
12
|
+
Release roll-up of the post-S30 stabilization train (1.0.15–1.0.19) plus the
|
|
13
|
+
dashboard/status-bar TUI pass. Pairs with forge plugin **v1.2.17**.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **Status-bar/dashboard TUI pass** (Iron-Laws compliance follow-ups):
|
|
18
|
+
Focusable/theming/width-safety/timer guards and consolidated entry;
|
|
19
|
+
↑/Esc deactivation via the input router with ○ focus indicator;
|
|
20
|
+
○/● colour carries node status (accent=running, success=completed,
|
|
21
|
+
error=failed); spinner shows activity without a redundant node glyph.
|
|
22
|
+
|
|
23
|
+
## [1.0.19] — 2026-06-04
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Halt-advisor model = the heavy model the runtime resolves point-in-time —
|
|
28
|
+
the `advisorModel` config entry is removed.** Supersedes the 1.0.18 advisor
|
|
29
|
+
resolution. There is no dedicated knob: "heavy" is the approve/architect
|
|
30
|
+
slot resolved through the existing routing cascade (L4 phase override →
|
|
31
|
+
project → user/global → default), exactly as the approve phase itself would
|
|
32
|
+
resolve; when the cascade bottoms out at inherit, the session's current
|
|
33
|
+
model (pi default) is used. This mirrors the Claude Code plugin route, where
|
|
34
|
+
the advisor runs on the opus-class session model. The `advisorModel` key is
|
|
35
|
+
dropped from `GlobalConfig`/`ProjectConfig`/`MergedConfig`, the config
|
|
36
|
+
merge, and `forge-cli-schema.json` (it was never exposed in `/forge:config`
|
|
37
|
+
and never set in practice).
|
|
38
|
+
|
|
39
|
+
## [1.0.18] — 2026-06-04
|
|
40
|
+
|
|
41
|
+
Coordinated release matching forge plugin **v1.2.17** (postflight-gate
|
|
42
|
+
false-halt fixes). Bundles the dashboard Iron-Laws compliance pass
|
|
43
|
+
(Focusable, theming, width-safety, timer guards, consolidated entry).
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **Halt advisor no longer runs on `anthropic/undefined`.** pi's
|
|
48
|
+
`ModelRegistry.getAvailable()` returns Model objects whose identifier is
|
|
49
|
+
`.id`, not `.model`; the blind `available[0] as PersonaModel` cast produced
|
|
50
|
+
`{ provider, model: undefined }` and the advisor dispatched against a
|
|
51
|
+
nonexistent model (observed on the CART-S03-T01 halt). `resolveAdvisorModel`
|
|
52
|
+
now maps both shapes, skips entries without a usable identifier, and —
|
|
53
|
+
before falling back to the registry at all — prefers the **session's
|
|
54
|
+
current model**, which is provider-neutral and known-good. Config
|
|
55
|
+
`advisorModel` slot still wins when set.
|
|
56
|
+
|
|
57
|
+
## [1.0.17] — 2026-06-04
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- **Context-governor dedup hardening — reads-only, error-aware, re-queryable.**
|
|
62
|
+
CART-S02-T04 transcripts surfaced three Rule-1 defects:
|
|
63
|
+
1. *Write masking (serious):* the dedup key carried no read/write
|
|
64
|
+
distinction, so a `forge_artifact` WRITE confirmation was replaced by a
|
|
65
|
+
pointer registered by an earlier READ of the same path — a failed write
|
|
66
|
+
would have been silently masked. Dedup now applies only to read-like
|
|
67
|
+
commands (`read`/`get*`/`list`/`describe`/`template`); every mutation
|
|
68
|
+
result passes through verbatim, and the command participates in the key
|
|
69
|
+
(read vs list never conflate).
|
|
70
|
+
2. *Impossible re-query:* the pointer said "re-query if needed", but every
|
|
71
|
+
re-query of the same key returned another pointer — agents responded by
|
|
72
|
+
switching to bash `cat` workarounds (wasted turns, path-guess errors).
|
|
73
|
+
Pointers now ALTERNATE: served at most once in a row, the immediately
|
|
74
|
+
repeated call is honoured with full (still trimmed/clamped) content.
|
|
75
|
+
Wording updated to "call again to re-fetch".
|
|
76
|
+
3. *Error interaction:* errored results were registered and replaced by
|
|
77
|
+
pointers. Error results now bypass all curation — never registered,
|
|
78
|
+
never replaced, always verbatim.
|
|
79
|
+
|
|
80
|
+
## [1.0.16] — 2026-06-03
|
|
81
|
+
|
|
82
|
+
Coordinated release matching forge plugin **v1.2.15**.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
|
|
86
|
+
- **Zero-usage "husk" turns no longer corrupt phase telemetry.** Failed/retry
|
|
87
|
+
turns emit assistant messages with all-zero usage; the accumulator in
|
|
88
|
+
`forge-subagent.ts` counted them as turns and — because
|
|
89
|
+
`usage.totalTokens ?? prev` treats `0` as non-nullish — overwrote the
|
|
90
|
+
running `contextTokens` with 0, which is why every aborted phase transcript
|
|
91
|
+
reported `contextTokens: 0`. Husk turns are now excluded from `turns` and
|
|
92
|
+
the context figure only updates on a positive total.
|
|
93
|
+
|
|
94
|
+
- **Incomplete (cancelled/failed) phase attempts now emit their billed
|
|
95
|
+
tokens.** The cancel and halt-on-failure branches in `run-task.ts` and
|
|
96
|
+
`fix-bug.ts` returned without emitting a phase event, so the provider-billed
|
|
97
|
+
tokens of aborted attempts never reached the store — collate's COST_REPORT
|
|
98
|
+
under-counted real spend (CART-S02-T03 baseline: 259,950 tokens invisible
|
|
99
|
+
across two aborted plan passes). New `emitIncompletePhaseEvent` helper emits
|
|
100
|
+
the canonical phase event with `verdict: "aborted"` (cancel) /
|
|
101
|
+
`"failed"` (halt) and the captured partial usage; zero-token attempts are
|
|
102
|
+
skipped (no husk noise). Pairs with forge plugin v1.2.15, whose COST_REPORT
|
|
103
|
+
gains an **Incomplete Passes** section.
|
|
104
|
+
|
|
105
|
+
## [1.0.15] — 2026-06-03
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
|
|
109
|
+
- **forge-compress savings prints no longer corrupt the `/forge:dashboard`
|
|
110
|
+
TUI.** `compressWithTelemetry` (forge-tools.ts) and the `forge_artifact`
|
|
111
|
+
read path wrote dim `[forge-compress] N→M tok (X% saved)` lines straight to
|
|
112
|
+
stderr — raw ANSI under an active TUI overlay garbles the screen. The prints
|
|
113
|
+
were redundant: the same stats already flow through tool-result
|
|
114
|
+
`details.compression` → viewport-events → session-registry/orchestrator-tree
|
|
115
|
+
→ the dashboard's aggregate `⇌Nt` savings suffix. Removed both stderr writes;
|
|
116
|
+
the dashboard remains the single surface for compression savings.
|
|
117
|
+
|
|
118
|
+
- **Context governor was dormant in production — Mechanisms A–D never reached
|
|
119
|
+
phase subagent sessions.** Benchmarking CART-S02-T03 under
|
|
120
|
+
`FORGE_CTX_GOVERNOR=1` showed zero curation markers and no token reduction.
|
|
121
|
+
Three wiring defects, all in the FORGE-S30-T07 integration layer (the
|
|
122
|
+
mechanisms themselves were correct):
|
|
123
|
+
1. `registerHookDispatcher(pi, …, governor)` in `index.ts` only governs the
|
|
124
|
+
**parent** session; every phase runs in an isolated `createAgentSession`
|
|
125
|
+
subagent the parent's hooks never see.
|
|
126
|
+
2. `resolvePhaseKey` probed `ctx.persona`/`ctx.phase`, which pi never
|
|
127
|
+
populates (and `FORGE_PHASE_KEY` is read but never set) — so every phase
|
|
128
|
+
resolved the inert `default` policy.
|
|
129
|
+
3. The policy table only shipped `architect/plan` and `engineer/review`,
|
|
130
|
+
matching **no** real `${personaNoun}/${role}` pipeline key; and
|
|
131
|
+
`createGovernor` was constructed without `steerFn`/`summarySentinel`/
|
|
132
|
+
`compactFn`, leaving Mechanism B steer, Mechanism C shed, and the
|
|
133
|
+
Mechanism E proactive trigger unreachable.
|
|
134
|
+
|
|
135
|
+
Fix: new `buildGovernorFactory({ phaseKey, cwd })` (`context-governor.ts`)
|
|
136
|
+
is constructed **per phase** by `run-task.ts` — which knows the
|
|
137
|
+
`${personaNoun}/${role}` key — and injected into each subagent session via
|
|
138
|
+
`extensionFactories`, alongside a `buildForgeCompactionFactory` now carrying
|
|
139
|
+
warm-tier path opts (`cwd`/`phaseKey`/`entityId`/`sprintId` — previously
|
|
140
|
+
threaded from `index.ts` with no opts, so warm-tier merge was dead). The
|
|
141
|
+
factory wires `steerFn` → `pi.sendUserMessage(…, { deliverAs: "steer" })`,
|
|
142
|
+
`summarySentinel` → read-only `.forge/store/{tasks,bugs}/<id>.json`
|
|
143
|
+
`summaries[<phase>]` probe, and `compactFn` → `ctx.compact()`. The policy
|
|
144
|
+
table now ships entries for all six governed pipeline keys (`engineer/plan`,
|
|
145
|
+
`supervisor/review-plan`, `engineer/implement`, `supervisor/review-code`,
|
|
146
|
+
`qa-engineer/validate`, `architect/approve`) with `read` budgets kept more
|
|
147
|
+
generous than `bash` so file reads aren't over-clamped; `writeback`/`commit`
|
|
148
|
+
stay on `default`. Flag-gating is unchanged (`FORGE_CTX_GOVERNOR=1`), and
|
|
149
|
+
`registerRunTask`/`registerRunSprint` keep `extensionFactories` as a test
|
|
150
|
+
seam.
|
|
151
|
+
|
|
10
152
|
## [1.0.14] — 2026-06-03
|
|
11
153
|
|
|
12
154
|
Coordinated release matching forge plugin **v1.2.14**.
|
|
@@ -5,6 +5,67 @@ Format: newest first. Breaking changes are marked **△ Breaking**.
|
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## [1.2.17] — 2026-06-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Postflight output guard no longer false-halts every phase.** The S26-T19
|
|
13
|
+
guards were dormant until the v1.2.16 base-pack recompile activated them;
|
|
14
|
+
their first live firing halted CART-S03-T01's plan phase despite full
|
|
15
|
+
success. Two deterministic bugs in `postflight-gate.cjs`:
|
|
16
|
+
1. *Require predicates never resolved* — the outputs blocks use bare record
|
|
17
|
+
paths (`summaries.plan.verdict`) while the CLI passes
|
|
18
|
+
`state = { task: record }`; every bare require read `undefined` and
|
|
19
|
+
failed unconditionally. `readField` now falls back to the entity record
|
|
20
|
+
(`state.task` / `state.bug`) after the direct walk.
|
|
21
|
+
2. *Artifact paths missed the `sprints/` segment* — `{sprint}` was
|
|
22
|
+
substituted with the bare sprintId, probing
|
|
23
|
+
`<engineering>/<sprintId>/<taskId>` instead of the canonical
|
|
24
|
+
`<engineering>/sprints/<sprintId>/<taskId>`. New `buildSubstitutions()`
|
|
25
|
+
resolves `{sprint}` to the path segment under engineering
|
|
26
|
+
(`sprints/<id>` for tasks, `bugs` for bug records).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## [1.2.16] — 2026-06-04
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Review/validate phases no longer self-limit on iteration count**
|
|
35
|
+
([forge-engineering#34](https://github.com/Entelligentsia/forge-engineering/issues/34)).
|
|
36
|
+
The standalone-invocation fallback in `review_plan` / `review_code` /
|
|
37
|
+
`validate_task` read `maxReviewIterations` from `.forge/config.json` —
|
|
38
|
+
producing a *"Key not found"* error on every review phase and, worse,
|
|
39
|
+
duplicating a protection the orchestrator already owns. Loop budgeting and
|
|
40
|
+
termination are solely the orchestrator's job (`run-task` enforces
|
|
41
|
+
`maxIterations` deterministically; exhaustion escalates to a human), and a
|
|
42
|
+
deliberate human standalone re-run **is** the escape hatch for stuck items —
|
|
43
|
+
a phase consulting its own cap could refuse exactly that recovery. Workflows
|
|
44
|
+
now treat user-invoked runs as iteration 1 with no limit and never read an
|
|
45
|
+
iteration cap from config; orchestrated runs continue to take `N of M` from
|
|
46
|
+
the orchestrator-injected Review Loop Context block. Prompt-text only.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## [1.2.15] — 2026-06-03
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **COST_REPORT now accounts incomplete (aborted/failed) phase attempts.**
|
|
55
|
+
Cancelled and halted phase attempts bill real provider tokens, but their
|
|
56
|
+
events arrived as token-less husks — the CART-S02-T03 benchmark baseline
|
|
57
|
+
under-counted by exactly 259,950 tokens across two aborted plan passes.
|
|
58
|
+
forge-cli ≥1.0.16 now emits phase events with `verdict: "aborted"` (user
|
|
59
|
+
cancel) / `"failed"` (halt-on-failure) carrying the captured partial usage;
|
|
60
|
+
`collate.cjs` adds an **Incomplete Passes** section to COST_REPORT (task,
|
|
61
|
+
phase, outcome, tokens per attempt). Per-Task / Per-Role / Model-Split
|
|
62
|
+
totals sum verdict-agnostically, so the previously-invisible spend lands in
|
|
63
|
+
the totals automatically once events arrive. Report-layer only — no schema
|
|
64
|
+
change (`verdict` was already a free-string event field); older forge-cli
|
|
65
|
+
versions simply produce no incomplete-pass events.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
8
69
|
## [1.2.14] — 2026-06-03
|
|
9
70
|
|
|
10
71
|
### Fixed
|
|
@@ -28,33 +28,17 @@ export interface ForgeCliFeatureFlags {
|
|
|
28
28
|
export interface GlobalConfig {
|
|
29
29
|
"persona-models"?: PersonaModelsMap;
|
|
30
30
|
forgeCli?: ForgeCliFeatureFlags;
|
|
31
|
-
/**
|
|
32
|
-
* FORGE-S26-T18: optional advisor model for halt-recovery advisory.
|
|
33
|
-
* When present, the halt advisor spawns on this model instead of
|
|
34
|
-
* falling back to modelRegistry.getAvailable()[0].
|
|
35
|
-
*/
|
|
36
|
-
advisorModel?: PersonaModel;
|
|
37
31
|
}
|
|
38
32
|
export interface ProjectConfig {
|
|
39
33
|
"persona-models"?: PersonaModelsMap;
|
|
40
34
|
forgeCli?: ForgeCliFeatureFlags;
|
|
41
35
|
pipelines?: Record<string, PipelineConfig>;
|
|
42
|
-
/**
|
|
43
|
-
* FORGE-S26-T18: optional per-project advisor model override.
|
|
44
|
-
* Project config (L2) wins over global config (L1).
|
|
45
|
-
*/
|
|
46
|
-
advisorModel?: PersonaModel;
|
|
47
36
|
}
|
|
48
37
|
export interface MergedConfig {
|
|
49
38
|
/** Shallow-merged persona-models (project wins on key collision). */
|
|
50
39
|
"persona-models"?: PersonaModelsMap;
|
|
51
40
|
/** Project-only pipelines (L3/L4 config lives here). */
|
|
52
41
|
pipelines?: Record<string, PipelineConfig>;
|
|
53
|
-
/**
|
|
54
|
-
* Resolved advisor model (project wins over global; same L2>L1 precedence).
|
|
55
|
-
* Used by halt-recovery advisor (FORGE-S26-T18).
|
|
56
|
-
*/
|
|
57
|
-
advisorModel?: PersonaModel;
|
|
58
42
|
/** The raw global config for L1 lookups — null if absent or invalid. */
|
|
59
43
|
_global: GlobalConfig | null;
|
|
60
44
|
/** The raw project config for L2 lookups — null if absent or invalid. */
|
|
@@ -67,11 +67,6 @@ export function loadLayeredConfig(cwd) {
|
|
|
67
67
|
if (projectConfig?.pipelines) {
|
|
68
68
|
merged.pipelines = projectConfig.pipelines;
|
|
69
69
|
}
|
|
70
|
-
// advisorModel: project (L2) wins over global (L1)
|
|
71
|
-
const advisorModel = projectConfig?.advisorModel ?? globalConfig?.advisorModel;
|
|
72
|
-
if (advisorModel) {
|
|
73
|
-
merged.advisorModel = advisorModel;
|
|
74
|
-
}
|
|
75
70
|
return { global: globalConfig, project: projectConfig, merged, errors };
|
|
76
71
|
}
|
|
77
72
|
//# sourceMappingURL=config-layer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-layer.js","sourceRoot":"","sources":["../../../src/extensions/forgecli/config-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,SAAS,MAAM,KAAK,CAAC;AAC5B,OAAO,MAAM,MAAM,yBAAyB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,4DAA4D;AAC5D,wDAAwD;AACxD,MAAM,GAAG,GAAI,SAAiB,CAAC,OAAO,EAAE,GAAG,IAAK,SAAiB,CAAC,GAAG,IAAI,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"config-layer.js","sourceRoot":"","sources":["../../../src/extensions/forgecli/config-layer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,SAAS,MAAM,KAAK,CAAC;AAC5B,OAAO,MAAM,MAAM,yBAAyB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7E,4DAA4D;AAC5D,wDAAwD;AACxD,MAAM,GAAG,GAAI,SAAiB,CAAC,OAAO,EAAE,GAAG,IAAK,SAAiB,CAAC,GAAG,IAAI,SAAS,CAAC;AAiEnF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AACzC,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AAErC,SAAS,cAAc,CACtB,GAAY,EACZ,KAAa;IAEb,MAAM,EAAE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,CAAC,EAAE,EAAE,CAAC;QACT,MAAM,QAAQ,GACb,QAAQ,CAAC,MAAM;YACd,EAAE,GAAG,CAAC,CAAC,CAA8C,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,YAAY,IAAI,GAAG,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;aACjG,IAAI,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC;QAC3B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,KAAK,yBAAyB,QAAQ,EAAE,EAAE,CAAC;IACvF,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAmC,EAAE,CAAC;AACnE,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACrC,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IACvD,CAAC;IAAC,OAAO,IAAa,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC5C,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IACzC,MAAM,WAAW,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;IAE9C,IAAI,YAAY,GAAwB,IAAI,CAAC;IAC7C,IAAI,aAAa,GAAyB,IAAI,CAAC;IAE/C,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;QACxB,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACnD,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC;QAC5B,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;IAC7C,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,cAAc,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QACrD,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC;QAC7B,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC;IACF,CAAC;IAED,yEAAyE;IACzE,MAAM,mBAAmB,GAAqB;QAC7C,GAAG,CAAC,YAAY,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QAC3C,GAAG,CAAC,aAAa,EAAE,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;KAC5C,CAAC;IAEF,MAAM,MAAM,GAAiB;QAC5B,OAAO,EAAE,YAAY;QACrB,QAAQ,EAAE,aAAa;KACvB,CAAC;IAEF,IAAI,MAAM,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,CAAC,gBAAgB,CAAC,GAAG,mBAAmB,CAAC;IAChD,CAAC;IAED,iEAAiE;IACjE,IAAI,aAAa,EAAE,SAAS,EAAE,CAAC;QAC9B,MAAM,CAAC,SAAS,GAAG,aAAa,CAAC,SAAS,CAAC;IAC5C,CAAC;IAGD,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Structured Forge facts extracted from messagesToSummarize.
|
|
4
|
+
* All fields are arrays of strings for serialization into the compaction summary.
|
|
5
|
+
*/
|
|
6
|
+
export interface ForgeFactSummary {
|
|
7
|
+
/** FORGE-X-Y / FORGE-X-Y-TN patterns extracted from message text. */
|
|
8
|
+
storeIds: string[];
|
|
9
|
+
/** Lines containing [x] or [ ] checkbox patterns (Markdown task lists). */
|
|
10
|
+
acStateLines: string[];
|
|
11
|
+
/** Lines matching → <status> or update-status ... status <value>. */
|
|
12
|
+
transitionLines: string[];
|
|
13
|
+
/** File refs: engineering/, .forge/, *.ts/*.md/*.cjs/*.json paths. */
|
|
14
|
+
fileRefs: string[];
|
|
15
|
+
/** Text blocks containing [FRICTION] or type:friction. */
|
|
16
|
+
frictionBlocks: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Options for buildForgeCompactionFactory.
|
|
20
|
+
*/
|
|
21
|
+
export interface ForgeCompactionOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to the project CWD (for warm-tier summary file lookup).
|
|
24
|
+
* When omitted, warm-tier merge is skipped unless summaryReader is provided.
|
|
25
|
+
*/
|
|
26
|
+
cwd?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Phase key for summary filename resolution (e.g. "architect/plan").
|
|
29
|
+
* When omitted, warm-tier merge is skipped.
|
|
30
|
+
*/
|
|
31
|
+
phaseKey?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Task/entity ID for summary path resolution (e.g. "FORGE-S30-T09").
|
|
34
|
+
* When omitted, warm-tier merge is skipped.
|
|
35
|
+
*/
|
|
36
|
+
entityId?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Sprint ID for summary path resolution (e.g. "FORGE-S30").
|
|
39
|
+
* When omitted, warm-tier merge is skipped.
|
|
40
|
+
*/
|
|
41
|
+
sprintId?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Injected summary reader (test seam). Receives the resolved summary
|
|
44
|
+
* file path (empty string when path resolution is skipped) and returns
|
|
45
|
+
* the raw JSON string or null. Defaults to fs.readFileSync.
|
|
46
|
+
* Must not throw — return null on any failure.
|
|
47
|
+
*/
|
|
48
|
+
summaryReader?: (filePath: string) => string | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract structured Forge facts from an array of message-like objects.
|
|
52
|
+
*
|
|
53
|
+
* Pure function — no fs I/O, no LLM calls, provider-neutral.
|
|
54
|
+
* Accepts any array (unknown[]) to be safe against varying pi message shapes.
|
|
55
|
+
* Text content is extracted from `.content[].text` (assistant messages)
|
|
56
|
+
* and from direct string entries.
|
|
57
|
+
*
|
|
58
|
+
* @param messagesToSummarize Array of message-like objects from preparation.
|
|
59
|
+
* @returns ForgeFactSummary with all extracted patterns.
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractForgeFacts(messagesToSummarize: unknown[]): ForgeFactSummary;
|
|
62
|
+
/**
|
|
63
|
+
* Build an ExtensionFactory that registers a session_before_compact handler
|
|
64
|
+
* returning a deterministically-composed CompactionResult.
|
|
65
|
+
*
|
|
66
|
+
* The handler:
|
|
67
|
+
* 1. Validates that event.preparation is present and well-formed.
|
|
68
|
+
* 2. Extracts Forge facts from event.preparation.messagesToSummarize
|
|
69
|
+
* (no LLM call, provider-neutral).
|
|
70
|
+
* 3. Reads the warm-tier {PHASE}-SUMMARY.json if opts provide path context,
|
|
71
|
+
* or invokes opts.summaryReader("") as a test seam.
|
|
72
|
+
* 4. Assembles a compact structured summary string.
|
|
73
|
+
* 5. Returns { compaction: { summary, firstKeptEntryId, tokensBefore } }.
|
|
74
|
+
* 6. Returns undefined on any error (IL7 — lets pi compact normally).
|
|
75
|
+
*
|
|
76
|
+
* Pack 07: reads summary files but never writes .forge/store/.
|
|
77
|
+
* IL10: no pi-mono edits, no dispatch contract changes.
|
|
78
|
+
* NOT wired into index.ts/run-task.ts — deferred to T07 production integration.
|
|
79
|
+
*
|
|
80
|
+
* @param opts ForgeCompactionOptions (default: empty — no warm-tier).
|
|
81
|
+
* @returns ExtensionFactory for passing to DefaultResourceLoader.extensionFactories.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildForgeCompactionFactory(opts?: ForgeCompactionOptions): ExtensionFactory;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// context-governor-compaction.ts — Mechanism E: Forge-aware compaction handler.
|
|
2
|
+
// FORGE-S30-T09
|
|
3
|
+
//
|
|
4
|
+
// Exports:
|
|
5
|
+
// ForgeFactSummary — extracted Forge facts struct
|
|
6
|
+
// ForgeCompactionOptions — options for the compaction factory
|
|
7
|
+
// extractForgeFacts(msgs) — pure extractor (no fs I/O)
|
|
8
|
+
// buildForgeCompactionFactory(opts) — ExtensionFactory builder
|
|
9
|
+
//
|
|
10
|
+
// Design:
|
|
11
|
+
// - Thin module separate from context-governor.ts so it can import ExtensionAPI
|
|
12
|
+
// types (which require the pi pkg) without polluting the pure-logic governor.
|
|
13
|
+
// - extractForgeFacts: line-level pattern matching — no LLM call, provider-neutral,
|
|
14
|
+
// lossless on structured facts.
|
|
15
|
+
// - Warm-tier merge: reads {PHASE}-SUMMARY.json via injected summaryReader
|
|
16
|
+
// (defaults to fs.readFileSync). Best-effort — absent/malformed files silently
|
|
17
|
+
// skipped (IL7).
|
|
18
|
+
// - IL7 fallback: entire session_before_compact handler wrapped in try/catch.
|
|
19
|
+
// On any error, returns undefined — lets pi compact normally with generateSummary.
|
|
20
|
+
// - IL10/Pack 07: only reads summary files, never writes .forge/store/ or summaries.
|
|
21
|
+
// - NOT wired into index.ts/run-task.ts yet — deferred to T07 production integration.
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Pattern constants (module-level compile-time constants)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** Matches Forge store IDs in their standard forms:
|
|
28
|
+
* FORGE-S30-T09 (sprint+task)
|
|
29
|
+
* FORGE-S30 (sprint)
|
|
30
|
+
* FORGE-BUG-042 (bug, with category letters + digit suffix)
|
|
31
|
+
* Pattern: FORGE- followed by alternating ALPHANUM segments separated by hyphens.
|
|
32
|
+
* Two variants ordered longest-first so overlapping (sprint+task before sprint) works. */
|
|
33
|
+
const STORE_ID_RE = /FORGE-[A-Z]+\d*-(?:T?\d+|[A-Z]+\d*)|FORGE-[A-Z]+\d*/g;
|
|
34
|
+
/** Matches Markdown checkbox lines. */
|
|
35
|
+
const CHECKBOX_RE = /^\s*[-*]?\s*\[[ xX]\].*$/gm;
|
|
36
|
+
/** Matches status transition patterns:
|
|
37
|
+
* - Arrow: "→ implemented"
|
|
38
|
+
* - store-cli: "update-status task FORGE-S30-T09 status implementing"
|
|
39
|
+
* Using a broad match to capture both forms reliably. */
|
|
40
|
+
const TRANSITION_RE = /(?:→\s*[\w][\w-]*|update-status\s+\S+\s+\S+\s+status\s+[\w][\w-]*)/g;
|
|
41
|
+
/** Matches file reference patterns. */
|
|
42
|
+
const FILE_REF_RE = /(?:engineering\/[^\s"'`,;)]+|\.forge\/[^\s"'`,;)]+|\b[\w./\-]+\.(?:ts|md|cjs|json)\b)/g;
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// extractForgeFacts — pure extractor
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Extract structured Forge facts from an array of message-like objects.
|
|
48
|
+
*
|
|
49
|
+
* Pure function — no fs I/O, no LLM calls, provider-neutral.
|
|
50
|
+
* Accepts any array (unknown[]) to be safe against varying pi message shapes.
|
|
51
|
+
* Text content is extracted from `.content[].text` (assistant messages)
|
|
52
|
+
* and from direct string entries.
|
|
53
|
+
*
|
|
54
|
+
* @param messagesToSummarize Array of message-like objects from preparation.
|
|
55
|
+
* @returns ForgeFactSummary with all extracted patterns.
|
|
56
|
+
*/
|
|
57
|
+
export function extractForgeFacts(messagesToSummarize) {
|
|
58
|
+
const storeIdSet = new Set();
|
|
59
|
+
const acStateLines = [];
|
|
60
|
+
const transitionLineSet = new Set();
|
|
61
|
+
const fileRefSet = new Set();
|
|
62
|
+
const frictionBlocks = [];
|
|
63
|
+
for (const msg of messagesToSummarize) {
|
|
64
|
+
const texts = extractTextContent(msg);
|
|
65
|
+
for (const text of texts) {
|
|
66
|
+
// Store IDs — reset lastIndex before each matchAll
|
|
67
|
+
const storeMatches = text.matchAll(new RegExp(STORE_ID_RE.source, "g"));
|
|
68
|
+
for (const m of storeMatches) {
|
|
69
|
+
storeIdSet.add(m[0]);
|
|
70
|
+
}
|
|
71
|
+
// AC-state checkbox lines
|
|
72
|
+
const checkboxMatches = text.match(new RegExp(CHECKBOX_RE.source, "gm"));
|
|
73
|
+
if (checkboxMatches) {
|
|
74
|
+
for (const line of checkboxMatches) {
|
|
75
|
+
acStateLines.push(line.trim());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Transition lines
|
|
79
|
+
const transitionMatches = text.matchAll(new RegExp(TRANSITION_RE.source, "g"));
|
|
80
|
+
for (const m of transitionMatches) {
|
|
81
|
+
transitionLineSet.add(m[0].trim());
|
|
82
|
+
}
|
|
83
|
+
// File refs
|
|
84
|
+
const fileRefMatches = text.matchAll(new RegExp(FILE_REF_RE.source, "g"));
|
|
85
|
+
for (const m of fileRefMatches) {
|
|
86
|
+
const ref = m[0].trim();
|
|
87
|
+
if (ref.length > 2) {
|
|
88
|
+
fileRefSet.add(ref);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// FRICTION blocks — scan per line
|
|
92
|
+
for (const line of text.split("\n")) {
|
|
93
|
+
if (/\[FRICTION\]|type:friction/i.test(line)) {
|
|
94
|
+
frictionBlocks.push(line.trim());
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
storeIds: Array.from(storeIdSet),
|
|
101
|
+
acStateLines,
|
|
102
|
+
transitionLines: Array.from(transitionLineSet),
|
|
103
|
+
fileRefs: Array.from(fileRefSet),
|
|
104
|
+
frictionBlocks,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract text strings from a message-like object.
|
|
109
|
+
* Handles assistant messages (content[].text) and plain strings.
|
|
110
|
+
*/
|
|
111
|
+
function extractTextContent(msg) {
|
|
112
|
+
if (typeof msg === "string") {
|
|
113
|
+
return [msg];
|
|
114
|
+
}
|
|
115
|
+
if (typeof msg !== "object" || msg === null) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const msgObj = msg;
|
|
119
|
+
const content = msgObj["content"];
|
|
120
|
+
if (!Array.isArray(content)) {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const texts = [];
|
|
124
|
+
for (const part of content) {
|
|
125
|
+
if (typeof part === "string") {
|
|
126
|
+
texts.push(part);
|
|
127
|
+
}
|
|
128
|
+
else if (typeof part === "object" && part !== null) {
|
|
129
|
+
const p = part;
|
|
130
|
+
if (p["type"] === "text" && typeof p["text"] === "string") {
|
|
131
|
+
texts.push(p["text"]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return texts;
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Warm-tier helpers
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/** Map phase key to canonical {PHASE}-SUMMARY.json filename. */
|
|
141
|
+
function phaseSummaryFilename(phaseKey) {
|
|
142
|
+
const map = {
|
|
143
|
+
// Real run-task PHASE_PIPELINE keys (`${personaNoun}/${role}`):
|
|
144
|
+
"engineer/plan": "PLAN-SUMMARY.json",
|
|
145
|
+
"supervisor/review-plan": "REVIEW_PLAN-SUMMARY.json",
|
|
146
|
+
"engineer/implement": "IMPLEMENTATION-SUMMARY.json",
|
|
147
|
+
"supervisor/review-code": "CODE_REVIEW-SUMMARY.json",
|
|
148
|
+
"qa-engineer/validate": "VALIDATION-SUMMARY.json",
|
|
149
|
+
"architect/approve": "APPROVE-SUMMARY.json",
|
|
150
|
+
// Legacy design-time keys (test fixtures only):
|
|
151
|
+
"architect/plan": "PLAN-SUMMARY.json",
|
|
152
|
+
"engineer/review": "REVIEW-SUMMARY.json",
|
|
153
|
+
"engineer/code-review": "CODE_REVIEW-SUMMARY.json",
|
|
154
|
+
};
|
|
155
|
+
return map[phaseKey] ?? "{PHASE}-SUMMARY.json";
|
|
156
|
+
}
|
|
157
|
+
/** Default summaryReader: synchronous fs.readFileSync. Returns null on any error. */
|
|
158
|
+
function defaultSummaryReader(filePath) {
|
|
159
|
+
try {
|
|
160
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Read and parse the warm-tier {PHASE}-SUMMARY.json (synchronous).
|
|
168
|
+
* Returns the parsed object or null on any failure (IL7).
|
|
169
|
+
*
|
|
170
|
+
* When summaryReader is provided but cwd/phaseKey/entityId/sprintId are not,
|
|
171
|
+
* calls summaryReader("") — enables test seams that don't need path resolution.
|
|
172
|
+
*/
|
|
173
|
+
function readWarmTierSummary(opts) {
|
|
174
|
+
try {
|
|
175
|
+
let filePath;
|
|
176
|
+
const reader = opts.summaryReader ?? defaultSummaryReader;
|
|
177
|
+
if (opts.cwd && opts.phaseKey && opts.entityId && opts.sprintId) {
|
|
178
|
+
filePath = path.join(opts.cwd, "engineering", "sprints", opts.sprintId, opts.entityId, phaseSummaryFilename(opts.phaseKey));
|
|
179
|
+
}
|
|
180
|
+
else if (opts.summaryReader) {
|
|
181
|
+
// Test seam: summaryReader present but no path context — call with empty string.
|
|
182
|
+
filePath = "";
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const raw = reader(filePath);
|
|
188
|
+
if (!raw)
|
|
189
|
+
return null;
|
|
190
|
+
const parsed = JSON.parse(raw);
|
|
191
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
192
|
+
return null;
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Summary assembly
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
/**
|
|
203
|
+
* Assemble a compact structured summary string from extracted facts
|
|
204
|
+
* and an optional warm-tier summary.
|
|
205
|
+
*/
|
|
206
|
+
function assembleSummary(facts, warmTier) {
|
|
207
|
+
const parts = [];
|
|
208
|
+
// Warm-tier objective (highest information density — prepend).
|
|
209
|
+
if (warmTier?.objective) {
|
|
210
|
+
parts.push(`## Phase Objective\n${warmTier.objective}`);
|
|
211
|
+
}
|
|
212
|
+
// Warm-tier key changes.
|
|
213
|
+
if (warmTier?.key_changes && warmTier.key_changes.length > 0) {
|
|
214
|
+
parts.push(`## Key Changes\n${warmTier.key_changes.map((c) => `- ${c}`).join("\n")}`);
|
|
215
|
+
}
|
|
216
|
+
// Extracted store IDs.
|
|
217
|
+
if (facts.storeIds.length > 0) {
|
|
218
|
+
parts.push(`## Active Store IDs\n${facts.storeIds.join(", ")}`);
|
|
219
|
+
}
|
|
220
|
+
// AC state (checkboxes — capped at 10).
|
|
221
|
+
if (facts.acStateLines.length > 0) {
|
|
222
|
+
const sample = facts.acStateLines.slice(0, 10);
|
|
223
|
+
parts.push(`## Acceptance Criteria State\n${sample.join("\n")}`);
|
|
224
|
+
}
|
|
225
|
+
// Transitions (capped at 5).
|
|
226
|
+
if (facts.transitionLines.length > 0) {
|
|
227
|
+
const sample = facts.transitionLines.slice(0, 5);
|
|
228
|
+
parts.push(`## Status Transitions\n${sample.join("\n")}`);
|
|
229
|
+
}
|
|
230
|
+
// File refs (capped at 10).
|
|
231
|
+
if (facts.fileRefs.length > 0) {
|
|
232
|
+
const sample = facts.fileRefs.slice(0, 10);
|
|
233
|
+
parts.push(`## File References\n${sample.join("\n")}`);
|
|
234
|
+
}
|
|
235
|
+
// FRICTION blocks.
|
|
236
|
+
if (facts.frictionBlocks.length > 0) {
|
|
237
|
+
parts.push(`## FRICTION\n${facts.frictionBlocks.join("\n")}`);
|
|
238
|
+
}
|
|
239
|
+
if (parts.length === 0) {
|
|
240
|
+
return "[Forge context governor — compaction summary — no structured facts extracted]";
|
|
241
|
+
}
|
|
242
|
+
return `[Forge context governor — compaction summary]\n\n${parts.join("\n\n")}`;
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// buildForgeCompactionFactory — ExtensionFactory builder
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
/**
|
|
248
|
+
* Build an ExtensionFactory that registers a session_before_compact handler
|
|
249
|
+
* returning a deterministically-composed CompactionResult.
|
|
250
|
+
*
|
|
251
|
+
* The handler:
|
|
252
|
+
* 1. Validates that event.preparation is present and well-formed.
|
|
253
|
+
* 2. Extracts Forge facts from event.preparation.messagesToSummarize
|
|
254
|
+
* (no LLM call, provider-neutral).
|
|
255
|
+
* 3. Reads the warm-tier {PHASE}-SUMMARY.json if opts provide path context,
|
|
256
|
+
* or invokes opts.summaryReader("") as a test seam.
|
|
257
|
+
* 4. Assembles a compact structured summary string.
|
|
258
|
+
* 5. Returns { compaction: { summary, firstKeptEntryId, tokensBefore } }.
|
|
259
|
+
* 6. Returns undefined on any error (IL7 — lets pi compact normally).
|
|
260
|
+
*
|
|
261
|
+
* Pack 07: reads summary files but never writes .forge/store/.
|
|
262
|
+
* IL10: no pi-mono edits, no dispatch contract changes.
|
|
263
|
+
* NOT wired into index.ts/run-task.ts — deferred to T07 production integration.
|
|
264
|
+
*
|
|
265
|
+
* @param opts ForgeCompactionOptions (default: empty — no warm-tier).
|
|
266
|
+
* @returns ExtensionFactory for passing to DefaultResourceLoader.extensionFactories.
|
|
267
|
+
*/
|
|
268
|
+
export function buildForgeCompactionFactory(opts = {}) {
|
|
269
|
+
return (pi) => {
|
|
270
|
+
pi.on("session_before_compact", (event) => {
|
|
271
|
+
try {
|
|
272
|
+
// Validate preparation is present and has the required pass-through fields.
|
|
273
|
+
const prep = event?.preparation;
|
|
274
|
+
if (!prep || typeof prep !== "object") {
|
|
275
|
+
return undefined; // IL7 — malformed preparation
|
|
276
|
+
}
|
|
277
|
+
const { firstKeptEntryId, tokensBefore, messagesToSummarize } = prep;
|
|
278
|
+
if (typeof firstKeptEntryId !== "string" || typeof tokensBefore !== "number") {
|
|
279
|
+
return undefined; // IL7 — malformed preparation fields
|
|
280
|
+
}
|
|
281
|
+
// Extract Forge facts from message history.
|
|
282
|
+
const msgs = Array.isArray(messagesToSummarize) ? messagesToSummarize : [];
|
|
283
|
+
const facts = extractForgeFacts(msgs);
|
|
284
|
+
// Read warm-tier summary (best-effort, IL7).
|
|
285
|
+
const warmTier = readWarmTierSummary(opts);
|
|
286
|
+
// Assemble summary string.
|
|
287
|
+
const summary = assembleSummary(facts, warmTier);
|
|
288
|
+
const compaction = {
|
|
289
|
+
summary,
|
|
290
|
+
firstKeptEntryId,
|
|
291
|
+
tokensBefore,
|
|
292
|
+
};
|
|
293
|
+
return { compaction };
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// IL7: unexpected error → return undefined, let pi compact normally.
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=context-governor-compaction.js.map
|