framein 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/adr.js +17 -0
- package/dist/anomaly.js +39 -0
- package/dist/bin.js +27 -0
- package/dist/blast.js +51 -0
- package/dist/brief.js +21 -0
- package/dist/capsule.js +64 -0
- package/dist/cli.js +2090 -0
- package/dist/db.js +7 -0
- package/dist/debt.js +42 -0
- package/dist/delegate.js +64 -0
- package/dist/detect.js +118 -0
- package/dist/disagree.js +85 -0
- package/dist/evidence.js +72 -0
- package/dist/fileWriter.js +35 -0
- package/dist/ingest.js +38 -0
- package/dist/managedBlock.js +101 -0
- package/dist/mcpRegister.js +85 -0
- package/dist/mcpServer.js +138 -0
- package/dist/palette.js +63 -0
- package/dist/projector.js +65 -0
- package/dist/quota.js +30 -0
- package/dist/recipe.js +55 -0
- package/dist/rescue.js +38 -0
- package/dist/roles.js +61 -0
- package/dist/select.js +50 -0
- package/dist/shell.js +127 -0
- package/dist/stats.js +74 -0
- package/dist/store.js +319 -0
- package/dist/task.js +94 -0
- package/dist/trust.js +39 -0
- package/dist/types.js +3 -0
- package/dist/ui/banner.js +32 -0
- package/dist/ui/capabilities.js +35 -0
- package/dist/ui/theme.js +49 -0
- package/dist/wrappers.js +63 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frameout (Framein)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Framein
|
|
2
|
+
|
|
3
|
+
**Keep one work frame across Claude, Codex, and Gemini.**
|
|
4
|
+
|
|
5
|
+
Start with one agent, challenge it with another, switch when needed, and close the work with
|
|
6
|
+
validation.
|
|
7
|
+
|
|
8
|
+
Framein is a local work-state layer for AI coding agents. Keep using Claude, Codex, Gemini,
|
|
9
|
+
slash-command frameworks, skill packs, role-based workflows, or your own agent setup. Framein keeps
|
|
10
|
+
the work underneath them stable: a task contract, decision trail, risk state, validation results, and
|
|
11
|
+
a compact capsule the next model can read.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
start in Claude -> challenge with Codex -> switch when needed -> validate before ship
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Status: **public pre-release** (`v0.0.4`). Runtime dependencies: **zero**. Required Node:
|
|
18
|
+
**22.5.0+**.
|
|
19
|
+
|
|
20
|
+
[Website](https://www.framein.dev) · [Manual](docs/MANUAL.md) · [Install notes](docs/INSTALL.md) · [Code signing policy](docs/CODE_SIGNING.md) · [Security](SECURITY.md)
|
|
21
|
+
|
|
22
|
+
## Why Framein?
|
|
23
|
+
|
|
24
|
+
Good PRDs, plans, ADRs, and skill packs help any model do better work. That is useful, and Framein is
|
|
25
|
+
designed to coexist with it.
|
|
26
|
+
|
|
27
|
+
The pain Framein targets starts when the work has to survive beyond one model or one clean session:
|
|
28
|
+
|
|
29
|
+
- Your lead model gets stuck and repeats the same approach.
|
|
30
|
+
- You want a different model to challenge the plan, diff, or risk.
|
|
31
|
+
- You need to switch lead model because of quota, model fit, or a dead end.
|
|
32
|
+
- The agent says the task is done before build and tests ran.
|
|
33
|
+
- The next session gets a chat summary instead of the actual contract, validation state, failed attempts, and decisions.
|
|
34
|
+
|
|
35
|
+
Framein does not replace the coding agent or pretend to be a full multi-agent cockpit. It keeps one
|
|
36
|
+
local work frame under the agents you already use.
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
The public npm package is the intended install path, but `framein` is not published to npm yet.
|
|
41
|
+
Until that publish step is complete, install from the public source repository:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/framein-dev/framein.git
|
|
45
|
+
cd framein
|
|
46
|
+
npm install
|
|
47
|
+
npm run build
|
|
48
|
+
npm install -g .
|
|
49
|
+
framein --version
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
After npm publication, the install path becomes:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install -g framein
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Initialize a project:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd your-project
|
|
62
|
+
framein init
|
|
63
|
+
framein integrations install all --write
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Run the work loop:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
framein start "add Google OAuth, keep email login"
|
|
70
|
+
framein verify
|
|
71
|
+
|
|
72
|
+
# When another model should review or continue:
|
|
73
|
+
framein challenge "review the OAuth callback plan" --run
|
|
74
|
+
framein capsule codex
|
|
75
|
+
|
|
76
|
+
framein ship
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use `challenge` when another model should review a claim or plan. Use `capsule <agent>` when a
|
|
80
|
+
different model should continue from the same local facts. `verify` is a rehearsal; `ship` is the
|
|
81
|
+
enforced gate and exits non-zero when hard validation fails.
|
|
82
|
+
|
|
83
|
+
## What You See
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
$ framein start "add Google OAuth, keep email login"
|
|
87
|
+
task contract
|
|
88
|
+
goal add Google OAuth, keep email login
|
|
89
|
+
preserve existing email login
|
|
90
|
+
|
|
91
|
+
$ framein challenge "OAuth callback stores state in session" --run
|
|
92
|
+
reviewer codex
|
|
93
|
+
verdict change required
|
|
94
|
+
required add nonce/state validation
|
|
95
|
+
|
|
96
|
+
$ framein capsule gemini
|
|
97
|
+
next lead prepared from facts:
|
|
98
|
+
contract · diff · tests · decisions
|
|
99
|
+
|
|
100
|
+
$ framein ship
|
|
101
|
+
build ok · tests 42/42
|
|
102
|
+
risk high: auth/ touched
|
|
103
|
+
status ready with human gate
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The important part is not the text UI. It is that every command writes to the same local work frame,
|
|
107
|
+
so terminal commands, native agent wrappers, MCP tools, and the next model all read the same facts.
|
|
108
|
+
|
|
109
|
+
## Core Commands
|
|
110
|
+
|
|
111
|
+
| Need | Command | What it does |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| Define done | `framein start "<goal>"` | Creates a Task Contract: goal, acceptance, protected areas, non-goals |
|
|
114
|
+
| Edit the contract | `framein task show` / `framein task amend ...` | Reviews or updates the definition of done |
|
|
115
|
+
| Get second opinion | `framein challenge "<proposal>" --run` | Asks a different reviewer role for a bounded objection |
|
|
116
|
+
| Switch model/session | `framein capsule [agent]` | Prepares the next lead from contract, diff, validation, ADRs, and ledger |
|
|
117
|
+
| Run validation | `framein verify` | Runs configured build/test checks and records the result |
|
|
118
|
+
| Check risk | `framein risk` | Flags sensitive blast radius from changed files |
|
|
119
|
+
| Decide ship readiness | `framein ship` | Enforced validation and risk gate for commit/deploy readiness |
|
|
120
|
+
| Recover from loops | `framein rescue` | Detects repeated failures or thrash and offers options |
|
|
121
|
+
| Save a green point | `framein checkpoint <label>` | Records the current commit as last known good |
|
|
122
|
+
|
|
123
|
+
Full reference: [`docs/MANUAL.md`](docs/MANUAL.md).
|
|
124
|
+
|
|
125
|
+
## Native Agent Surface
|
|
126
|
+
|
|
127
|
+
Framein installs logic-less wrappers into the tools agents already understand:
|
|
128
|
+
|
|
129
|
+
| Host | Surface | Example |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| Claude / Gemini | slash commands | `/fr:verify`, `/fr:ship`, `/fr:risk` |
|
|
132
|
+
| Codex | project skills | `$fr-verify`, `$fr-ship`, `$fr-capsule` |
|
|
133
|
+
| Terminal / CI | CLI + JSON | `framein ship --json` |
|
|
134
|
+
| MCP-capable clients | local stdio MCP server | `framein mcp serve` |
|
|
135
|
+
|
|
136
|
+
The generated agent commands expose the same agent-facing verbs across hosts:
|
|
137
|
+
|
|
138
|
+
| Intent | Claude / Gemini | Codex skill |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| Start or reset the task contract | `/fr:start` | `$fr-start` |
|
|
141
|
+
| Run build/test validation | `/fr:verify` | `$fr-verify` |
|
|
142
|
+
| Check commit/deploy readiness | `/fr:ship` | `$fr-ship` |
|
|
143
|
+
| Detect a repair loop | `/fr:rescue` | `$fr-rescue` |
|
|
144
|
+
| Read current Framein state | `/fr:status` | `$fr-status` |
|
|
145
|
+
| Ask an independent model to review | `/fr:challenge` | `$fr-challenge` |
|
|
146
|
+
| Check changed-file risk | `/fr:risk` | `$fr-risk` |
|
|
147
|
+
| Show or amend the task contract | `/fr:task` | `$fr-task` |
|
|
148
|
+
| Prepare a model switch | `/fr:capsule` | `$fr-capsule` |
|
|
149
|
+
| Resolve a reviewer debate | `/fr:decide` | `$fr-decide` |
|
|
150
|
+
|
|
151
|
+
The wrappers do not contain product logic. They call the same local `framein` engine, so a command
|
|
152
|
+
invoked from an agent, a terminal, or CI reads and writes the same contract, validation results, risk, and ledger.
|
|
153
|
+
|
|
154
|
+
Windows note: generated wrappers use `framein.cmd` to avoid PowerShell execution-policy failures from
|
|
155
|
+
the npm `.ps1` shim inside agent shells.
|
|
156
|
+
|
|
157
|
+
## How It Works
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
framein.store.json (git-friendly snapshot) <-> .frame/store.db (local cache)
|
|
161
|
+
|
|
|
162
|
+
v
|
|
163
|
+
Task Contract · ADRs · memory · write locks · ledger · validation results
|
|
164
|
+
|
|
|
165
|
+
v
|
|
166
|
+
managed block projection
|
|
167
|
+
|
|
|
168
|
+
+--> CLAUDE.md
|
|
169
|
+
+--> AGENTS.md
|
|
170
|
+
+--> GEMINI.md
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Important behavior:
|
|
174
|
+
|
|
175
|
+
- `framein init` creates `.frame/store.db`, projects managed blocks, and ensures `.frame/` is ignored.
|
|
176
|
+
- `framein export` writes `framein.store.json` when you want a git-canonical text snapshot.
|
|
177
|
+
- Managed blocks are byte-identical across native context files.
|
|
178
|
+
- User-authored text outside managed markers is preserved.
|
|
179
|
+
- ADRs are append-only; corrections use superseding records.
|
|
180
|
+
- Write locks are atomic conditional upserts with TTL.
|
|
181
|
+
- Runtime dependencies stay at zero.
|
|
182
|
+
|
|
183
|
+
## Trust Boundary
|
|
184
|
+
|
|
185
|
+
Framein is local-first:
|
|
186
|
+
|
|
187
|
+
- No provider credentials are collected.
|
|
188
|
+
- No remote credential relay or subscription pooling.
|
|
189
|
+
- Claude, Codex, and Gemini keep their official CLI authentication.
|
|
190
|
+
- Existing MCP servers and skills are detected/recommended, not proxied or cross-executed.
|
|
191
|
+
- No terminal I/O (TTY) screen-scraping.
|
|
192
|
+
- `framein trust` previews permission-bypass flags; it does not silently enable them.
|
|
193
|
+
- Destructive recovery uses explicit flags, for example `framein rewind --force`.
|
|
194
|
+
- Deployment remains a human gate.
|
|
195
|
+
|
|
196
|
+
## Current Status
|
|
197
|
+
|
|
198
|
+
Solid in the current pre-release:
|
|
199
|
+
|
|
200
|
+
- Store, import/export, managed-block projection
|
|
201
|
+
- Task Contract, Verification Gate, Risk Gate, Rescue, Capsule, Challenge/Decide
|
|
202
|
+
- Logic-less `/fr:*` and `$fr-*` wrappers
|
|
203
|
+
- MCP stdio server and registration helpers
|
|
204
|
+
- Headless delegation to real CLIs where available
|
|
205
|
+
- Windows author environment live-verified
|
|
206
|
+
- `244` automated tests passing
|
|
207
|
+
|
|
208
|
+
Still being validated:
|
|
209
|
+
|
|
210
|
+
- public npm publication and post-publish install verification
|
|
211
|
+
- signed standalone executable release hardening for Windows and macOS
|
|
212
|
+
- multi-developer workflows
|
|
213
|
+
- interactive lobby paths such as `/lead`, `/go`, and inline command palette
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npm install
|
|
219
|
+
npm run build
|
|
220
|
+
npm test
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Tests compile first and run from `dist/` through Node's built-in test runner.
|
|
224
|
+
|
|
225
|
+
Useful focused commands:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
node --no-warnings --test dist/store.test.js
|
|
229
|
+
node --no-warnings --test --test-name-pattern="supersede" dist/**/*.test.js
|
|
230
|
+
node --no-warnings dist/cli.js <cmd>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Node **22.5.0+** is required because Framein uses built-in `node:sqlite`.
|
|
234
|
+
|
|
235
|
+
## Documentation
|
|
236
|
+
|
|
237
|
+
- Manual: [`docs/MANUAL.md`](docs/MANUAL.md)
|
|
238
|
+
- Korean manual backup: [`docs/MANUAL.ko.md`](docs/MANUAL.ko.md)
|
|
239
|
+
- Install troubleshooting: [`docs/INSTALL.md`](docs/INSTALL.md) / [`docs/INSTALL.ko.md`](docs/INSTALL.ko.md)
|
|
240
|
+
- Code signing policy: [`docs/CODE_SIGNING.md`](docs/CODE_SIGNING.md)
|
|
241
|
+
- Website: [framein.dev](https://www.framein.dev)
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT. Framein by [Frameout](https://frameout.co.kr).
|
|
246
|
+
|
|
247
|
+
Please keep the copyright and license notice when redistributing substantial
|
|
248
|
+
portions of Framein. See [`NOTICE`](NOTICE) for suggested attribution and brand
|
|
249
|
+
usage notes.
|
package/dist/adr.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ADR digest: a compact index embedded into the projected native files.
|
|
2
|
+
// Full ADRs live in the store (queried live via MCP); files carry only a digest.
|
|
3
|
+
export function buildAdrDigest(adrs, opts = {}) {
|
|
4
|
+
if (adrs.length === 0)
|
|
5
|
+
return '_No decisions recorded yet._';
|
|
6
|
+
const max = opts.max ?? 10;
|
|
7
|
+
// Derived (append-only): an ADR is superseded only if a LATER one references it.
|
|
8
|
+
const supersededIds = new Set(adrs.filter((a) => a.supersedes != null && a.id > a.supersedes).map((a) => a.supersedes));
|
|
9
|
+
const recent = [...adrs].sort((a, b) => b.id - a.id).slice(0, max);
|
|
10
|
+
const lines = recent.map((a) => {
|
|
11
|
+
const status = supersededIds.has(a.id) ? 'superseded' : a.status;
|
|
12
|
+
return `- [ADR-${a.id}] ${a.title} (${status})`;
|
|
13
|
+
});
|
|
14
|
+
const overflow = adrs.length > recent.length
|
|
15
|
+
? `\n- …and ${adrs.length - recent.length} earlier decision(s)` : '';
|
|
16
|
+
return `${adrs.length} decision(s) recorded. Latest:\n${lines.join('\n')}${overflow}`;
|
|
17
|
+
}
|
package/dist/anomaly.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Audit cadence (ADR-0005, F-AUDIT-3): detect "thrash" signals from the task ledger so a
|
|
2
|
+
// reviewer can be pulled in only when an agent is going in circles — not on every turn.
|
|
3
|
+
// Pure function over ledger entries; thresholds are tunable (PRD §11.8).
|
|
4
|
+
export function detectThrash(entries, opts = {}) {
|
|
5
|
+
const editThreshold = opts.repeatedEdits ?? 3;
|
|
6
|
+
const failThreshold = opts.repeatedFailures ?? 2;
|
|
7
|
+
const noProgress = opts.noProgressTurns ?? 5;
|
|
8
|
+
const signals = [];
|
|
9
|
+
const editCounts = new Map();
|
|
10
|
+
const failCounts = new Map();
|
|
11
|
+
for (const e of entries) {
|
|
12
|
+
if (e.kind === 'edit' && e.target)
|
|
13
|
+
editCounts.set(e.target, (editCounts.get(e.target) ?? 0) + 1);
|
|
14
|
+
if (e.kind === 'test-fail' && e.target)
|
|
15
|
+
failCounts.set(e.target, (failCounts.get(e.target) ?? 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
for (const [target, count] of editCounts) {
|
|
18
|
+
if (count >= editThreshold)
|
|
19
|
+
signals.push({ kind: 'repeated-edits', target, count, message: `'${target}' edited ${count}× — possible thrash loop` });
|
|
20
|
+
}
|
|
21
|
+
for (const [target, count] of failCounts) {
|
|
22
|
+
if (count >= failThreshold)
|
|
23
|
+
signals.push({ kind: 'repeated-failure', target, count, message: `'${target}' failed ${count}× — stuck on the same test` });
|
|
24
|
+
}
|
|
25
|
+
// turns accumulated since the last real progress (edit/commit). Other events (ask,
|
|
26
|
+
// test-fail) are neither progress nor turns — they're skipped, not counted.
|
|
27
|
+
let trailingTurns = 0;
|
|
28
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
29
|
+
const k = entries[i].kind;
|
|
30
|
+
if (k === 'edit' || k === 'commit')
|
|
31
|
+
break;
|
|
32
|
+
if (k === 'turn')
|
|
33
|
+
trailingTurns++;
|
|
34
|
+
}
|
|
35
|
+
if (trailingTurns >= noProgress) {
|
|
36
|
+
signals.push({ kind: 'no-progress', count: trailingTurns, message: `${trailingTurns} turns without an edit/commit — may be going in circles` });
|
|
37
|
+
}
|
|
38
|
+
return signals;
|
|
39
|
+
}
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// framein installed-bin entry. The node:sqlite ExperimentalWarning (and any other Node warning) is
|
|
3
|
+
// printed at module-LOAD time, before any in-process filter can run — the only reliable suppression is
|
|
4
|
+
// Node's own `--no-warnings`. So this tiny entry, which imports NOTHING that loads node:sqlite, re-execs
|
|
5
|
+
// the CLI once under `--no-warnings`. stdio:'inherit' keeps stdin/stdout/stderr byte-exact (MCP serve
|
|
6
|
+
// NDJSON, `ask --interactive` / shell `/go` hand-overs all pass straight through) and the child's exit
|
|
7
|
+
// code is propagated. FRAMEIN_NOWARN guards against a re-exec loop; running `node dist/cli.js` directly
|
|
8
|
+
// (dev/tests) bypasses this entirely.
|
|
9
|
+
//
|
|
10
|
+
// EXCEPTION: `mcp serve` is machine-facing — MCP clients read NDJSON on stdout and ignore stderr, so the
|
|
11
|
+
// SQLite warning is harmless there. We skip the re-exec for it to avoid adding any startup latency to the
|
|
12
|
+
// server an agent just spawned (a slow handshake can make a client cancel the first tool call).
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
const argv = process.argv.slice(2);
|
|
15
|
+
const isMcpServe = argv[0] === 'mcp' && argv[1] === 'serve';
|
|
16
|
+
if (process.env.FRAMEIN_NOWARN === undefined && !isMcpServe && typeof process.argv[1] === 'string') {
|
|
17
|
+
const res = spawnSync(process.execPath, ['--no-warnings', process.argv[1], ...process.argv.slice(2)], {
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
env: { ...process.env, FRAMEIN_NOWARN: '1' },
|
|
20
|
+
});
|
|
21
|
+
if (res.error) {
|
|
22
|
+
console.error(res.error.message);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
process.exit(res.status ?? 1);
|
|
26
|
+
}
|
|
27
|
+
await import('./cli.js');
|
package/dist/blast.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Blast Radius Guard (F-LOOP-6, ADR-0008): detect when a change touches sensitive code and raise
|
|
2
|
+
// the required gates — but only when risk actually changes, matching the audit cadence (ADR-0005:
|
|
3
|
+
// not every task). Pure: map changed file paths to a risk level + required gates. Reading the
|
|
4
|
+
// changed files (git) and acting on the gate live in cli.ts.
|
|
5
|
+
import { PLAIN } from './ui/theme.js';
|
|
6
|
+
// Order matters only for readability; each file is matched against every rule.
|
|
7
|
+
const RULES = [
|
|
8
|
+
{ category: 'secrets', level: 'high', pattern: /(^|\/)\.env(\.|$)|secret|credential|\.pem$|\.key$/i, gate: 'secret scan / rotation validation' },
|
|
9
|
+
{ category: 'auth', level: 'high', pattern: /auth|login|session|oauth|permission|rbac|password/i, gate: 'security review' },
|
|
10
|
+
{ category: 'payment', level: 'high', pattern: /payment|billing|stripe|checkout|invoice|charge/i, gate: 'security review (payments)' },
|
|
11
|
+
{ category: 'migration', level: 'high', pattern: /migrat|\.sql$|schema\.|prisma\/migrations|alembic/i, gate: 'migration rollback validation' },
|
|
12
|
+
{ category: 'deploy', level: 'high', pattern: /dockerfile|docker-compose|\.tf$|terraform|fly\.toml|vercel\.json|(^|\/)k8s\/|\.github\/workflows/i, gate: 'deploy rollback plan' },
|
|
13
|
+
{ category: 'deps', level: 'medium', pattern: /(^|\/)package\.json$|package-lock\.json|yarn\.lock|pnpm-lock\.yaml/i, gate: 'dependency justification' },
|
|
14
|
+
{ category: 'config', level: 'medium', pattern: /(^|\/)config\/|\.env\.example$|settings\.(json|py|ts)|\.config\./i, gate: 'config review' },
|
|
15
|
+
];
|
|
16
|
+
const RANK = { low: 0, medium: 1, high: 2 };
|
|
17
|
+
export function riskRank(level) { return RANK[level]; }
|
|
18
|
+
export function assessBlastRadius(changedFiles) {
|
|
19
|
+
const hits = [];
|
|
20
|
+
const gates = new Set();
|
|
21
|
+
let level = 'low';
|
|
22
|
+
for (const file of changedFiles) {
|
|
23
|
+
for (const rule of RULES) {
|
|
24
|
+
if (rule.pattern.test(file)) {
|
|
25
|
+
hits.push({ category: rule.category, file });
|
|
26
|
+
gates.add(rule.gate);
|
|
27
|
+
if (RANK[rule.level] > RANK[level])
|
|
28
|
+
level = rule.level;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { level, hits, requiredGates: [...gates] };
|
|
33
|
+
}
|
|
34
|
+
/** A message when risk INCREASED vs the previous assessment (cadence: only speak on change). */
|
|
35
|
+
export function riskTransition(prev, curr) {
|
|
36
|
+
if (prev === undefined || RANK[curr] <= RANK[prev])
|
|
37
|
+
return undefined;
|
|
38
|
+
return `Risk level changed: ${prev.toUpperCase()} → ${curr.toUpperCase()}`;
|
|
39
|
+
}
|
|
40
|
+
export function renderBlast(a, ui = PLAIN) {
|
|
41
|
+
if (a.level === 'low')
|
|
42
|
+
return `Risk level: ${ui.tone('LOW', 'success')} (no sensitive files touched)`;
|
|
43
|
+
const tone = a.level === 'high' ? 'danger' : 'warning';
|
|
44
|
+
const lines = [`Risk level: ${ui.tone(a.level.toUpperCase(), tone)}`, 'Reason:'];
|
|
45
|
+
for (const h of a.hits)
|
|
46
|
+
lines.push(` - ${h.category}: ${h.file}`);
|
|
47
|
+
lines.push('Required before ship:');
|
|
48
|
+
for (const g of a.requiredGates)
|
|
49
|
+
lines.push(` - ${g}`);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
package/dist/brief.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Ownership Brief (F-LOOP-10, ADR-0008): make the explainer produce a doc the user can take
|
|
2
|
+
// OWNERSHIP of — not just a friendly recap. Pure: render the brief skeleton, filling the facts
|
|
3
|
+
// framein already knows (changed files, how to test, how to roll back) and leaving the narrative
|
|
4
|
+
// sections for the live explainer role. Gathering the facts lives in cli.ts.
|
|
5
|
+
const TBD = ' (for the explainer role to fill)';
|
|
6
|
+
export function ownershipBrief(input) {
|
|
7
|
+
const changed = input.changedFiles?.length
|
|
8
|
+
? input.changedFiles.map((f) => ` - ${f}`).join('\n')
|
|
9
|
+
: ' (no changed files detected)';
|
|
10
|
+
const sections = [
|
|
11
|
+
['What changed', changed],
|
|
12
|
+
['How to test it', input.testCommand ? ` ${input.testCommand}` : ' (no test command found)'],
|
|
13
|
+
['How to roll it back', input.lastGreen ? ` git reset --hard ${input.lastGreen.slice(0, 7)} (last green checkpoint)` : ' (no checkpoint recorded — run `frame checkpoint`)'],
|
|
14
|
+
['How requests flow', TBD],
|
|
15
|
+
['Where configuration lives', TBD],
|
|
16
|
+
['Known limitations', TBD],
|
|
17
|
+
['What will likely break next', TBD],
|
|
18
|
+
];
|
|
19
|
+
const head = `Ownership brief${input.goal ? `: ${input.goal}` : ''}`;
|
|
20
|
+
return [head, '', ...sections.map(([h, b]) => `## ${h}\n${b}`)].join('\n');
|
|
21
|
+
}
|
package/dist/capsule.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Task Capsule (F-LOOP-4, ADR-0008): when a session compacts, hits quota, or switches CLI, hand
|
|
2
|
+
// over an AUTO-GENERATED structured state — not the chat transcript. The capsule is assembled from
|
|
3
|
+
// what framein already holds (contract + ADRs + git + validation results + ledger), so "no manual
|
|
4
|
+
// handoff; Framein rebuilds the working context from validation results." Pure assembly; the CLI
|
|
5
|
+
// gathers the inputs.
|
|
6
|
+
import { detectThrash } from './anomaly.js';
|
|
7
|
+
import { PLAIN } from './ui/theme.js';
|
|
8
|
+
export function buildCapsule(input) {
|
|
9
|
+
const ledger = input.ledger ?? [];
|
|
10
|
+
const recentActivity = ledger.slice(-8).map((e) => `${e.kind}${e.target ? ' ' + e.target : ''}`);
|
|
11
|
+
// Derive a blocker from a repeated-failure signal when one isn't supplied explicitly.
|
|
12
|
+
let blocker = input.blocker;
|
|
13
|
+
const testsAreGreen = input.testSummary !== null && input.testSummary !== undefined && input.testSummary.failed === 0;
|
|
14
|
+
if (!blocker && !testsAreGreen && ledger.length) {
|
|
15
|
+
const fail = detectThrash(ledger).find((s) => s.kind === 'repeated-failure');
|
|
16
|
+
if (fail)
|
|
17
|
+
blocker = fail.message;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
goal: input.goal ?? '(no task contract)',
|
|
21
|
+
branch: input.branch,
|
|
22
|
+
lastGreen: input.lastGreen,
|
|
23
|
+
decisions: input.decisions ?? [],
|
|
24
|
+
changed: input.changedFiles ?? [],
|
|
25
|
+
evidence: input.testSummary ?? undefined,
|
|
26
|
+
blocker,
|
|
27
|
+
lastDelegation: input.lastDelegation,
|
|
28
|
+
handoffTarget: input.handoffTarget,
|
|
29
|
+
recentActivity,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const short = (sha) => sha.slice(0, 7);
|
|
33
|
+
/** Readable capsule for `frame resume` / `frame capsule show`. Empty sections are omitted. */
|
|
34
|
+
export function renderCapsule(c, ui = PLAIN) {
|
|
35
|
+
const lines = [`task: ${c.goal}`];
|
|
36
|
+
if (c.branch)
|
|
37
|
+
lines.push(`branch: ${c.branch}`);
|
|
38
|
+
if (c.lastGreen)
|
|
39
|
+
lines.push(`last_green: ${short(c.lastGreen)}`);
|
|
40
|
+
if (c.decisions.length) {
|
|
41
|
+
lines.push('decisions:');
|
|
42
|
+
for (const d of c.decisions)
|
|
43
|
+
lines.push(` - ADR-${d.id}: ${d.title}`);
|
|
44
|
+
}
|
|
45
|
+
if (c.changed.length) {
|
|
46
|
+
lines.push('changed:');
|
|
47
|
+
for (const f of c.changed)
|
|
48
|
+
lines.push(` - ${f}`);
|
|
49
|
+
}
|
|
50
|
+
if (c.evidence)
|
|
51
|
+
lines.push(`validation: tests ${c.evidence.passed} passed, ${c.evidence.failed} failed`);
|
|
52
|
+
if (c.lastDelegation)
|
|
53
|
+
lines.push(`last_delegation: ${c.lastDelegation.agent} (${c.lastDelegation.ok ? 'ok' : 'failed'})`);
|
|
54
|
+
if (c.handoffTarget)
|
|
55
|
+
lines.push(`handoff: ${c.handoffTarget} (armed)`);
|
|
56
|
+
if (c.blocker)
|
|
57
|
+
lines.push(ui.tone(`current_blocker: ${c.blocker}`, 'danger'));
|
|
58
|
+
if (c.recentActivity.length) {
|
|
59
|
+
lines.push('recent:');
|
|
60
|
+
for (const a of c.recentActivity)
|
|
61
|
+
lines.push(` - ${a}`);
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|