@sal-sovereign-ai-labs/whyline 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/cli/index.js +248 -0
- package/cli/lib/agents/bob/assets/settings.json +14 -0
- package/cli/lib/agents/bob/assets/skills/whyline-check/SKILL.md +9 -0
- package/cli/lib/agents/bob/assets/skills/whyline-decide/SKILL.md +63 -0
- package/cli/lib/agents/bob/assets/skills/whyline-remove/SKILL.md +23 -0
- package/cli/lib/agents/bob/assets/skills/whyline-setup/SKILL.md +40 -0
- package/cli/lib/agents/bob/assets/skills/whyline-status/SKILL.md +23 -0
- package/cli/lib/agents/bob/assets/skills/whyline-why/SKILL.md +39 -0
- package/cli/lib/agents/bob/index.js +80 -0
- package/cli/lib/agents/index.js +26 -0
- package/cli/lib/agents/shared/hook-entry.sh +10 -0
- package/cli/lib/agents/shared/install.js +67 -0
- package/cli/lib/bom.js +171 -0
- package/cli/lib/capture.js +70 -0
- package/cli/lib/classify.js +52 -0
- package/cli/lib/commit.js +135 -0
- package/cli/lib/coverage.js +55 -0
- package/cli/lib/git.js +106 -0
- package/cli/lib/init.js +74 -0
- package/cli/lib/lenses.js +127 -0
- package/cli/lib/patch.js +98 -0
- package/cli/lib/report-template.html +697 -0
- package/cli/lib/report.js +60 -0
- package/cli/lib/review.js +63 -0
- package/cli/lib/seed.js +70 -0
- package/cli/lib/session.js +49 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SAL Sovereign AI Labs
|
|
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,129 @@
|
|
|
1
|
+
# Whyline
|
|
2
|
+
|
|
3
|
+
`git blame` tells you who. Whyline tells you why.
|
|
4
|
+
|
|
5
|
+
Every line an AI agent writes keeps the prompt that caused it, the session, the cost, the other files that task touched, whether a human edited it since, and whether it was meant to be temporary. Temporary code gets a lifecycle: it is recorded at birth, becomes due when its condition is met, and Bob removes it with evidence and your approval.
|
|
6
|
+
|
|
7
|
+
Built for IBM Bob 2.0. Bob's free lifecycle hooks record every write; six skills turn the records into answers inside Bob; git notes store everything. Last year's winner, Pedigree, proved that a commit was AI-written, for auditors. Whyline keeps why each line exists and acts on it, for developers.
|
|
8
|
+
|
|
9
|
+
Live demo report (the demo shop after the payments-v2 merge, rebuilt by CI): https://sal-sovereign-ai-labs.github.io/whyline/
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Install in a repo
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install -g @sal-sovereign-ai-labs/whyline
|
|
17
|
+
cd your-repo
|
|
18
|
+
whyline init # writes .bob/ (hooks and skills) and git hooks
|
|
19
|
+
git add .bob && git commit -m "add whyline"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then work in Bob as usual. Nothing new to type.
|
|
23
|
+
|
|
24
|
+
## Ask
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
whyline why src/payments/checkout.py:12 # who wrote it, and why
|
|
28
|
+
whyline check # temporary code and its lifecycle state
|
|
29
|
+
whyline unreviewed # AI lines no human has edited since, with coverage if you have a report
|
|
30
|
+
whyline keep flags.yaml "beta flag stays until Q1 review" # name the file, a symbol, a kind, or the id
|
|
31
|
+
whyline until legacy_export.py 2027-01-01
|
|
32
|
+
whyline watch mock_gateway.py --symbol MockGateway # fix the symbol the reference check looks for
|
|
33
|
+
whyline seed --dry-run # existing repo: find temporary-looking code that predates whyline
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Lifecycle of a temporary item:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
active --(condition met: date passed, or no references left)--> due --(you decide)--> kept | removed
|
|
40
|
+
```
|
|
41
|
+
`active` and `due` are recomputed from the repo on every `check`, so they never go stale. `kept` and `removed` are recorded decisions. Bob never changes a state on its own.
|
|
42
|
+
|
|
43
|
+
In Bob, in normal Agent mode: ask "why does this line exist?", say "remove the mock payment gateway", or "keep the beta flag until Q1". The installed skills run the right whyline command; removals end in Bob's own approval prompt.
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
1. `UserPromptSubmit` and `PostToolUse` hooks record the prompt and the exact lines each write touched (`.git/whyline/session.jsonl`).
|
|
48
|
+
2. On commit, a git hook compares what the agent wrote with what was committed: `ai`, `ai-edited`, or human. It attaches one JSON note to the commit on `refs/notes/whyline`. Nothing lands in your working tree.
|
|
49
|
+
3. Writes that look temporary (mock, demo, flag, shim, fixture, "until ...") become items with a removal condition: a date, or "no references left".
|
|
50
|
+
4. `whyline check` evaluates the conditions. Removal happens in Bob through the whyline-remove skill and Bob's normal approval prompt.
|
|
51
|
+
|
|
52
|
+
Costs 0 Bobcoins. Hooks are deterministic; Bob is only used for removals. When a Bob task spans several commits, every commit keeps the same prompt.
|
|
53
|
+
|
|
54
|
+
## Status
|
|
55
|
+
|
|
56
|
+
Hackathon build (IBM Bob 2.0 Hackathon, 25 to 27 Sep 2026). See docs/ for the research, evidence and architecture.
|
|
57
|
+
|
|
58
|
+
## CLI
|
|
59
|
+
|
|
60
|
+
| Command | Purpose | Exit code |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `whyline init` | install Bob and git hooks in the current repo | 0, 3 if not a git repo |
|
|
63
|
+
| `whyline why <file>:<line> [--json]` | origin, prompt, session, cost, siblings, item | 0 |
|
|
64
|
+
| `whyline check [--json] [--gate]` | temporary items with lifecycle state and evidence | 0; with `--gate`, 2 when any item is due |
|
|
65
|
+
| `whyline unreviewed [--json]` | AI lines with no human edit since, per file, coverage from coverage.xml or lcov.info | 0 |
|
|
66
|
+
| `whyline watch <item> --symbol Name` | change the symbol the reference check searches for | 0 |
|
|
67
|
+
| `whyline removed <item>` | record a removal done by hand (a commit message naming the file or id records it automatically) | 0 |
|
|
68
|
+
| `whyline seed [--dry-run] [--by-name]` | on an existing repo, record code carrying TODO remove, FIXME, HACK, temporary or until markers, with its age from git; `--by-name` also records mock_, compat_, examples/ and fixtures/ names | 0 |
|
|
69
|
+
| `whyline keep <item> "<reason>"` | mark an item permanent; `<item>` is a file, symbol, kind or id | 0 |
|
|
70
|
+
| `whyline until <item> <YYYY-MM-DD>` | set a date condition | 0 |
|
|
71
|
+
| `whyline bom [A..B] [--json]` | AI bill of materials for a commit range: lines changed, AI lines by agent, reviewed, items shipped, cost | 0 |
|
|
72
|
+
| `whyline report [--out file]` | one offline HTML page with every answer; regenerated after each commit | 0 |
|
|
73
|
+
| `whyline capture`, `whyline session-start`, `whyline commit` | hook entry points, always exit 0 | 0 |
|
|
74
|
+
|
|
75
|
+
`--json` prints machine-readable output with no ANSI codes. Environment: `WHYLINE_DEBUG=1` prints stack traces to stderr, `WHYLINE_BOB_DB` overrides the Bob database path.
|
|
76
|
+
|
|
77
|
+
## Known limitations
|
|
78
|
+
|
|
79
|
+
- When git metadata is stripped: a squash merge creates a new commit without the notes of the squashed ones, and a repo copied without `refs/notes/whyline` has no notes at all. The fix is to keep merge commits (or copy notes onto the squash commit with `git notes --ref whyline copy`), and to push and fetch the notes ref, which the installed pre-push and post-merge hooks do. Amend and rebase keep notes because init sets `notes.rewriteRef`.
|
|
80
|
+
- The reference check is text search (git grep for the symbol and the module name). Code reached only through strings or reflection can look unreferenced, which is why removal always goes through Bob's evidence step and your approval.
|
|
81
|
+
- "Unreviewed" means no human edit since the agent wrote the line. Review comments and PR approvals are not read yet.
|
|
82
|
+
- Recording starts at `whyline init`. Code written before that has no note and shows as human. `whyline seed` recovers temporary-looking code from before that point, but not who wrote it or why beyond the comment.
|
|
83
|
+
- Hook commands print their one status line to stderr so that stdout stays empty for the agent. `git commit` shows it; tooling that hides stderr will not.
|
|
84
|
+
|
|
85
|
+
## Speed
|
|
86
|
+
|
|
87
|
+
`npm run bench` builds the demo repo and times each command, median of 10. On a MacBook Pro with an Apple M1 Pro, Node 26: capture hook 165 ms (the only thing on Bob's write path), `why` 267 ms, `check` 344 ms, `bom` 560 ms, `unreviewed` 592 ms, session-start line 899 ms, `report` 2.0 s (runs in the background after a commit). Recording costs 0 Bobcoins.
|
|
88
|
+
|
|
89
|
+
## Built with IBM Bob
|
|
90
|
+
|
|
91
|
+
Bob is inside the product (three lifecycle hooks, six skills, the session-start line, removal through Bob's own approval prompt) and Bob built part of it: 16 Bob IDE tasks across two developers, 29 Bobcoins, task summaries in [bob_sessions/](bob_sessions/) with costs in [bob_sessions/costs.md](bob_sessions/costs.md). Bob also used Whyline on Whyline's own repository and reported five issues; three became fixes (docs/04, section 11). The rest of the code, tests and docs were written by the team with other tools.
|
|
92
|
+
|
|
93
|
+
## Business model
|
|
94
|
+
|
|
95
|
+
The CLI is free and MIT. Organisations pay for policy: enforced hooks rolled out to every developer (Bob's EnforcedHooks group policy) and the report as a compliance record per release.
|
|
96
|
+
|
|
97
|
+
## Troubleshooting
|
|
98
|
+
|
|
99
|
+
| Symptom | Do this |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `whyline why` says "git blame failed" | The file is not tracked, or you are in the wrong repo. `git ls-files <file>`. |
|
|
102
|
+
| `check` prints "no temporary items yet" | Nothing Bob wrote has been committed since install. Commit once, then `whyline check`. |
|
|
103
|
+
| No note after a commit | `whyline` is not on the PATH for git hooks (`npm link`), or the hooks were not installed (`whyline init`). See `.git/whyline/hook.err`. |
|
|
104
|
+
| Bob does not record anything | The workspace is not trusted (Bob skips workspace hooks in untrusted folders), or `.bob/settings.json` was not committed. `whyline init` again. |
|
|
105
|
+
| Cost shows as null | Bob IDE stores task cost elsewhere than Bob Shell, or sqlite3 is missing. The note is still complete. |
|
|
106
|
+
|
|
107
|
+
## Uninstall
|
|
108
|
+
|
|
109
|
+
Delete `.bob/hooks/whyline.sh`, the whyline lines in `.bob/settings.json`, and `.git/hooks/post-commit`, `post-merge`, `pre-push`. Bob and git keep working; the notes stay in the repo under `refs/notes/whyline` until you delete that ref.
|
|
110
|
+
|
|
111
|
+
## Requirements
|
|
112
|
+
|
|
113
|
+
Node 20 or newer, git. Bob IDE 2.2 or Bob Shell 2.x for the hooks. macOS and Linux are tested in CI; Windows needs Git Bash for the shell hook and is untested.
|
|
114
|
+
|
|
115
|
+
## Private by design
|
|
116
|
+
|
|
117
|
+
Whyline itself makes no network calls. Prompts are stored verbatim in git notes in your own repository, and the installed git hooks push and fetch that notes ref together with your code, so prompts travel to wherever your code travels and nowhere else. Do not paste secrets into prompts; the generated report (`.whyline-report.html`) also embeds prompts and is excluded from commits by `whyline init` through `.git/info/exclude`.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npm test # tests on temp git repos
|
|
123
|
+
npm run coverage
|
|
124
|
+
npm run smoke
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// whyline: provenance for AI-written code. Commands never throw at the agent: hook commands always exit 0.
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
|
|
6
|
+
const USAGE = `whyline <command> [--json]
|
|
7
|
+
|
|
8
|
+
init [--agent bob] install the agent's hooks (.bob/) and git hooks in this repo
|
|
9
|
+
capture [--agent bob] hook: read a hook payload on stdin, record the write (--dump or WHYLINE_DUMP=1 keeps raw payloads in .git/whyline/raw/)
|
|
10
|
+
session-start hook: print a one-line summary for Bob's context
|
|
11
|
+
commit git post-commit: attach the provenance note to HEAD
|
|
12
|
+
why <file>:<line> who wrote this line, and why
|
|
13
|
+
check [--json] [--gate] temporary items and their lifecycle state (--gate: exit 2 when any is due, for hooks and CI)
|
|
14
|
+
unreviewed [--json] AI lines no human has edited since, per file, with coverage if a report exists
|
|
15
|
+
bom [A..B] [--json] AI bill of materials for a commit range (default: last tag..HEAD, else all)
|
|
16
|
+
report [--out file] write the read-only HTML report (.whyline-report.html)
|
|
17
|
+
keep <item> "<reason>" decision: keep permanently (due -> kept)
|
|
18
|
+
until <item> <YYYY-MM-DD> change the condition to a date (stays active)
|
|
19
|
+
watch <item> --symbol X fix the symbol the reference check searches for
|
|
20
|
+
removed <item> record a removal done by hand (a commit message naming the item does this automatically)
|
|
21
|
+
seed [--dry-run] [--by-name] first run on an existing repo: record code with TODO remove, FIXME, HACK, temporary or until markers; --by-name also records mock_, compat_, examples/, fixtures/ names
|
|
22
|
+
|
|
23
|
+
<item> is a file path (or its last part), a watched symbol, a kind (mock, demo, flag, shim, fixture) when unique,
|
|
24
|
+
or the id shown by check. People name files; ids are for notes and scripts.
|
|
25
|
+
|
|
26
|
+
Lifecycle: active --(condition met)--> due --(you decide)--> kept | removed
|
|
27
|
+
active and due are recomputed from the repo on every check; kept and removed are recorded.
|
|
28
|
+
--version, --help
|
|
29
|
+
|
|
30
|
+
Exit codes: 0 ok, 1 usage or error, 2 only with check --gate when items are due, 3 not a git repository.
|
|
31
|
+
Env: WHYLINE_DEBUG=1 prints stack traces to stderr. WHYLINE_BOB_DB overrides the Bob database path.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function readStdin() {
|
|
35
|
+
try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEBUG = !!process.env.WHYLINE_DEBUG;
|
|
39
|
+
function debug(area, msg) { if (DEBUG) process.stderr.write(`[whyline:${area}] ${msg}\n`); }
|
|
40
|
+
|
|
41
|
+
function main(argv) {
|
|
42
|
+
try { return dispatch(argv); } catch (e) {
|
|
43
|
+
process.stderr.write(`whyline: ${e && e.message ? e.message : String(e)}\n`);
|
|
44
|
+
debug('main', e && e.stack ? e.stack : '');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function dispatch(argv) {
|
|
50
|
+
const [cmd, ...rest] = argv;
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const json = rest.includes('--json');
|
|
53
|
+
if (['why', 'check', 'unreviewed', 'bom', 'report', 'keep', 'until', 'watch', 'removed', 'seed'].includes(cmd) && !require('./lib/git').repoRoot(cwd)) {
|
|
54
|
+
process.stderr.write('not a git repository (run whyline inside a repo, or whyline init to set one up)\n'); return 3;
|
|
55
|
+
}
|
|
56
|
+
const agentIds = [];
|
|
57
|
+
const args = [];
|
|
58
|
+
for (let i = 0; i < rest.length; i++) {
|
|
59
|
+
if (rest[i] === '--json') continue;
|
|
60
|
+
if (rest[i] === '--agent') { if (rest[i + 1]) agentIds.push(rest[++i]); continue; }
|
|
61
|
+
args.push(rest[i]);
|
|
62
|
+
}
|
|
63
|
+
switch (cmd) {
|
|
64
|
+
case '--version': case '-v':
|
|
65
|
+
console.log(require('../package.json').version);
|
|
66
|
+
return 0;
|
|
67
|
+
case '--help': case '-h': case 'help':
|
|
68
|
+
process.stdout.write(USAGE);
|
|
69
|
+
return 0;
|
|
70
|
+
case 'capture': {
|
|
71
|
+
const session = require('./lib/session');
|
|
72
|
+
try {
|
|
73
|
+
const raw = readStdin() || '{}';
|
|
74
|
+
if (args.includes('--dump') || process.env.WHYLINE_DUMP) dumpRaw(cwd, raw);
|
|
75
|
+
const payload = JSON.parse(raw);
|
|
76
|
+
require('./lib/capture').handle(payload, { cwd, agent: agentIds[0] });
|
|
77
|
+
} catch (e) { session.logError(cwd, `capture: ${e.message}`); debug('capture', e.stack || ''); }
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
case 'session-start': {
|
|
81
|
+
// stdout is injected into Bob's context. One line, actionable, only when there is something to act on.
|
|
82
|
+
try {
|
|
83
|
+
const lenses = require('./lib/lenses');
|
|
84
|
+
const r = lenses.check(cwd);
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (r.due.length) parts.push(`${r.due.length} temporary item(s) due for removal: ${r.due.map(i => `${i.file} (${i.id})`).join(', ')}. To act, say "remove ${r.due[0].file.split('/').pop()}" and the whyline-remove skill will guide the removal`);
|
|
87
|
+
const u = lenses.unreviewed(cwd);
|
|
88
|
+
if (u.totals.aiLines) parts.push(`${u.totals.aiLines} AI-written line(s) in ${u.totals.files} file(s) have had no human edit since (run: whyline unreviewed)`);
|
|
89
|
+
if (parts.length) process.stdout.write(`whyline: ${parts.join('. ')}\n`);
|
|
90
|
+
} catch { /* silent by design */ }
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
case 'commit': {
|
|
94
|
+
try {
|
|
95
|
+
const r = require('./lib/commit').run(cwd);
|
|
96
|
+
if (r.removed && r.removed.length) process.stderr.write(`whyline: recorded removed: ${r.removed.join(', ')}\n`);
|
|
97
|
+
if (r.note) process.stderr.write(`whyline: note attached to ${r.commit.slice(0, 7)} (${r.note.ranges.length} range(s), ${r.note.items.length} item(s))\n`);
|
|
98
|
+
} catch (e) { require('./lib/session').logError(cwd, `commit: ${e.message}`); }
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
case 'why': {
|
|
102
|
+
const m = (args[0] || '').match(/^(.+):(\d+)$/);
|
|
103
|
+
if (!m) { process.stderr.write('usage: whyline why <file>:<line>\n'); return 1; }
|
|
104
|
+
const r = require('./lib/lenses').why(cwd, m[1], +m[2]);
|
|
105
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
|
|
106
|
+
printWhy(m[1], +m[2], r);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
case 'check': {
|
|
110
|
+
const gate = args.includes('--gate');
|
|
111
|
+
const r = require('./lib/lenses').check(cwd);
|
|
112
|
+
const exit = gate && r.due.length ? 2 : 0;
|
|
113
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return exit; }
|
|
114
|
+
if (!r.due.length && !r.active.length && !r.other.length) { console.log('no temporary items yet (commit something Bob wrote, then check again)'); return 0; }
|
|
115
|
+
printCheck(r);
|
|
116
|
+
return exit;
|
|
117
|
+
}
|
|
118
|
+
case 'unreviewed': {
|
|
119
|
+
const r = require('./lib/lenses').unreviewed(cwd);
|
|
120
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
|
|
121
|
+
if (!r.files.length) { console.log('no AI-written lines recorded yet (commit something Bob wrote, then run again)'); return 0; }
|
|
122
|
+
console.log('file'.padEnd(44) + 'ai lines edited ranges coverage');
|
|
123
|
+
for (const f of r.files) console.log(`${f.file.padEnd(44)}${String(f.aiLines).padStart(8)} ${String(f.editedRanges).padStart(13)} ${f.coverage == null ? 'no data' : f.coverage + '%'}`);
|
|
124
|
+
console.log(`${r.totals.aiLines} unreviewed AI lines in ${r.totals.files} file(s)${r.totals.coverageSource ? `, coverage from ${r.totals.coverageSource}` : ', no coverage report found (coverage.xml or lcov.info)'}`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
case 'bom': {
|
|
128
|
+
// Dev B's module: cli/lib/bom.js exporting bom(cwd, range) -> JSON per docs/10 and format(result) -> text
|
|
129
|
+
const mod = optional('./lib/bom');
|
|
130
|
+
if (!mod) { process.stderr.write('bom is not built yet (cli/lib/bom.js missing)\n'); return 1; }
|
|
131
|
+
const range = args[0] || defaultRange(cwd);
|
|
132
|
+
const r = (mod.bom || mod.run)(cwd, range);
|
|
133
|
+
console.log(json ? JSON.stringify(r, null, 2) : mod.format(r));
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
case 'report': {
|
|
137
|
+
// Dev B's module: cli/lib/report.js exporting render(data) -> HTML string. The router gathers the data.
|
|
138
|
+
const mod = optional('./lib/report');
|
|
139
|
+
if (!mod) { process.stderr.write('report is not built yet (cli/lib/report.js missing)\n'); return 1; }
|
|
140
|
+
const lenses = require('./lib/lenses');
|
|
141
|
+
const git = require('./lib/git');
|
|
142
|
+
const root = git.repoRoot(cwd);
|
|
143
|
+
if (!root) { process.stderr.write('not a git repository\n'); return 3; }
|
|
144
|
+
const bomMod = optional('./lib/bom');
|
|
145
|
+
const range = defaultRange(cwd);
|
|
146
|
+
const idx = lenses.index(cwd);
|
|
147
|
+
const data = {
|
|
148
|
+
generatedAt: new Date().toISOString(),
|
|
149
|
+
repo: require('node:path').basename(root),
|
|
150
|
+
head: git.head(cwd),
|
|
151
|
+
check: lenses.check(cwd),
|
|
152
|
+
unreviewed: lenses.unreviewed(cwd),
|
|
153
|
+
bom: bomMod ? (bomMod.bom || bomMod.run)(cwd, range) : null,
|
|
154
|
+
why: mod.collect ? mod.collect(cwd, idx) : null,
|
|
155
|
+
sessions: [...idx.sessions.values()],
|
|
156
|
+
ranges: idx.ranges,
|
|
157
|
+
notes: idx.notes.length,
|
|
158
|
+
};
|
|
159
|
+
const outIdx = args.indexOf('--out');
|
|
160
|
+
const out = outIdx >= 0 && args[outIdx + 1] ? args[outIdx + 1] : require('node:path').join(root, '.whyline-report.html');
|
|
161
|
+
fs.writeFileSync(out, mod.render(data));
|
|
162
|
+
console.log(`report written: ${out} (${data.notes} note(s), ${data.ranges.length} range(s))`);
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
case 'keep':
|
|
166
|
+
case 'until':
|
|
167
|
+
case 'watch':
|
|
168
|
+
case 'removed': {
|
|
169
|
+
const id = args[0], value = cmd === 'removed' ? (args[1] || 'removed by hand') : (args[1] === '--symbol' ? args[2] : args[1]);
|
|
170
|
+
const usage = { keep: '"<reason>"', until: '<YYYY-MM-DD>', watch: '--symbol <Name>', removed: '' }[cmd];
|
|
171
|
+
if (!id || !value) { process.stderr.write(`usage: whyline ${cmd} <file|symbol|kind|id> ${usage}\n`); return 1; }
|
|
172
|
+
if (cmd === 'until' && (!/^\d{4}-\d{2}-\d{2}$/.test(value) || isNaN(Date.parse(value + 'T00:00:00Z')) || new Date(value + 'T00:00:00Z').toISOString().slice(0, 10) !== value)) { process.stderr.write('until: date must be a real date in YYYY-MM-DD form\n'); return 1; }
|
|
173
|
+
const lenses = require('./lib/lenses');
|
|
174
|
+
const change = cmd === 'keep' ? { status: 'kept', reason: value }
|
|
175
|
+
: cmd === 'until' ? { status: 'active', condition: { type: 'date', on: value } }
|
|
176
|
+
: cmd === 'watch' ? { status: 'active', condition: { type: 'no_references', symbol: value } }
|
|
177
|
+
: { status: 'removed', reason: value };
|
|
178
|
+
const item = lenses.resolveItem(cwd, id);
|
|
179
|
+
const head = lenses.recordItemChange(cwd, item.id, change);
|
|
180
|
+
const said = { keep: 'kept permanently', until: `due on ${value}`, watch: `now watching symbol ${value}`, removed: 'recorded as removed' }[cmd];
|
|
181
|
+
console.log(`${item.file} (${item.id}): ${said} (recorded on ${head.slice(0, 7)})`);
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
case 'seed': {
|
|
185
|
+
const r = require('./lib/seed').run(cwd, { dryRun: args.includes('--dry-run'), byName: args.includes('--by-name') });
|
|
186
|
+
if (r.error) { process.stderr.write(`seed: ${r.error}\n`); return 3; }
|
|
187
|
+
if (json) { console.log(JSON.stringify(r, null, 2)); return 0; }
|
|
188
|
+
for (const it of r.items) console.log(` ${it.id} ${it.kind.padEnd(8)} ${it.file.padEnd(40)} since ${it.created} "${it.reason}"`);
|
|
189
|
+
if (r.items.length) console.log(`${r.items.length} item(s) ${args.includes('--dry-run') ? 'found (dry run, nothing recorded)' : 'recorded on ' + r.head.slice(0, 7)}. Next: whyline check`);
|
|
190
|
+
else console.log(`nothing new to seed from comment markers (${r.skipped} file(s) already tracked)`);
|
|
191
|
+
if (r.byNameOnly.length) console.log(`${r.byNameOnly.length} file(s) look temporary by name only (mock_, compat_, examples/, fixtures/): ${r.byNameOnly.slice(0, 5).join(', ')}${r.byNameOnly.length > 5 ? ', ...' : ''}. Add --by-name to record them.`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
case 'init':
|
|
195
|
+
return require('./lib/init').run(cwd, { agentIds: agentIds.length ? agentIds : ['bob'] });
|
|
196
|
+
default:
|
|
197
|
+
if (!cmd) { process.stdout.write(USAGE); return 0; }
|
|
198
|
+
process.stderr.write(`Unknown command: ${cmd}\nRun whyline --help for usage.\n`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Keep raw hook payloads for fixtures (one file per event). Never throws.
|
|
204
|
+
function dumpRaw(cwd, raw) {
|
|
205
|
+
try {
|
|
206
|
+
const dir = require('./lib/session').dir(cwd);
|
|
207
|
+
if (!dir) return;
|
|
208
|
+
const d = require('node:path').join(dir, 'raw');
|
|
209
|
+
fs.mkdirSync(d, { recursive: true });
|
|
210
|
+
fs.writeFileSync(require('node:path').join(d, `${Date.now()}-${process.pid}.json`), raw);
|
|
211
|
+
} catch { /* fixtures are a convenience */ }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Modules owned by the other developer may not exist yet on this branch.
|
|
215
|
+
function optional(mod) { try { return require(mod); } catch (e) { if (e.code === 'MODULE_NOT_FOUND' && String(e.message).includes(mod.replace('./', ''))) return null; throw e; } }
|
|
216
|
+
|
|
217
|
+
// Last tag to HEAD when a tag exists, else undefined, which bom.js treats as the whole history.
|
|
218
|
+
function defaultRange(cwd) {
|
|
219
|
+
const git = require('./lib/git');
|
|
220
|
+
const tag = git.tryGit(['describe', '--tags', '--abbrev=0'], { cwd });
|
|
221
|
+
return tag ? `${tag}..HEAD` : undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function printCheck(r) {
|
|
225
|
+
const col = (s, n) => String(s == null ? '' : s).padEnd(n).slice(0, n);
|
|
226
|
+
const row = it => ` ${col(it.id, 9)} ${col(it.kind, 8)} ${col(it.file, 38)} ${col(it.evidence ? it.evidence.summary : (it.reason || ''), 60)}`;
|
|
227
|
+
const section = (title, list) => { if (!list.length) return; console.log(`${title} (${list.length})`); console.log(` ${col('id', 9)} ${col('kind', 8)} ${col('file', 38)} ${col('evidence', 60)}`); list.forEach(it => console.log(row(it))); };
|
|
228
|
+
section('DUE for removal', r.due);
|
|
229
|
+
section('ACTIVE', r.active);
|
|
230
|
+
section('DECIDED', r.other.map(it => ({ ...it, evidence: { summary: `${it.state}${it.reason ? ': ' + it.reason : ''}` } })));
|
|
231
|
+
const c = r.counts;
|
|
232
|
+
const next = r.due.length ? `Next: tell Bob "remove ${r.due[0].file.split('/').pop()}", or whyline keep <file> "<reason>", or whyline why <file>:<line>` : 'Nothing is due.';
|
|
233
|
+
console.log(`${c.active} active, ${c.due} due, ${c.kept} kept, ${c.removed} removed. ${next}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function printWhy(file, line, r) {
|
|
237
|
+
if (!r.found) { console.log(`${file}:${line}: ${r.reason}`); return; }
|
|
238
|
+
if (r.origin === 'human') { console.log(`${file}:${line}\norigin human (${r.author || 'unknown'}, commit ${r.commit.slice(0, 7)})`); return; }
|
|
239
|
+
console.log(`${file}:${line}`);
|
|
240
|
+
console.log(`origin ${r.origin} (${r.agent || 'agent'}) · session ${String(r.session).slice(0, 8)} · ${r.author || '?'} · ${r.ts ? r.ts.slice(0, 10) : '?'}${r.cost != null ? ` · ${r.cost} Bobcoin` : ''}`);
|
|
241
|
+
if (r.prompt) console.log(`prompt "${r.prompt}"`);
|
|
242
|
+
if (r.siblings.length) console.log(`siblings ${r.siblings.join(' · ')}`);
|
|
243
|
+
if (r.item) console.log(`item ${r.item.id} ${r.item.kind} · ${r.item.status} · ${JSON.stringify(r.item.condition)} · "${r.item.reason}"`);
|
|
244
|
+
console.log(`commit ${r.commit.slice(0, 7)}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (require.main === module) process.exit(main(process.argv.slice(2)));
|
|
248
|
+
module.exports = { main };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{ "hooks": [{ "type": "command", "command": "sh .bob/hooks/whyline.sh session-start --agent bob", "timeout": 5 }] }
|
|
5
|
+
],
|
|
6
|
+
"UserPromptSubmit": [
|
|
7
|
+
{ "hooks": [{ "type": "command", "command": "sh .bob/hooks/whyline.sh capture --agent bob", "timeout": 5 }] }
|
|
8
|
+
],
|
|
9
|
+
"PostToolUse": [
|
|
10
|
+
{ "matcher": "^(write_file|write_to_file|apply_diff|insert_content|search_and_replace)$",
|
|
11
|
+
"hooks": [{ "type": "command", "command": "sh .bob/hooks/whyline.sh capture --agent bob", "timeout": 5 }] }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-check
|
|
3
|
+
description: List temporary code that whyline says is due for removal
|
|
4
|
+
metadata:
|
|
5
|
+
user-invocable: true
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Run `whyline check` with execute_command (exit code 0 is normal) and show its output as is. Do not edit anything. If items are due, tell the user they can say "remove <file name>" with the whyline-remove skill.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-decide
|
|
3
|
+
description: "When the user wants to keep a temporary item permanently, move its due date, or fix the symbol whyline watches for, look the item up with whyline check --json and run the right whyline command (keep, until, or watch)."
|
|
4
|
+
---
|
|
5
|
+
## When to use
|
|
6
|
+
Trigger phrases: "keep this item", "keep it permanently", "it's not temporary", "we're keeping this", "change the due date", "move the deadline", "due on", "extend the date", "it's due on", "watch for a different symbol", "fix the symbol", "wrong symbol", "watch symbol".
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
|
|
10
|
+
### Step 1 -- identify the item
|
|
11
|
+
Run `whyline check --json` with execute_command. Parse the result.
|
|
12
|
+
|
|
13
|
+
The result has three arrays: `due` (items whose condition is already met), `active` (items still waiting), and `other` (kept or removed). Each item carries:
|
|
14
|
+
- `id` -- the stable identifier (e.g. `L-3f9a2c`)
|
|
15
|
+
- `kind` -- `mock`, `demo`, `fixture`, `shim`, `flag`, or similar
|
|
16
|
+
- `file` -- the source file
|
|
17
|
+
- `lines` -- `[[start, end], ...]`
|
|
18
|
+
- `reason` -- the original reason the item was created
|
|
19
|
+
- `condition` -- `{ type: "date", on: "YYYY-MM-DD" }` or `{ type: "no_references", symbol: "Name" }`
|
|
20
|
+
- `evidence` -- what the last evaluation found (e.g. "2 reference(s): ...")
|
|
21
|
+
|
|
22
|
+
If the user named a specific item (by id, file, kind, or a description matching the reason), find it in the output. If the name is ambiguous or matches more than one item, show the candidates as a short table (id, kind, file, reason) and ask the user to choose. Never guess.
|
|
23
|
+
|
|
24
|
+
If there are no items at all, tell the user and stop.
|
|
25
|
+
|
|
26
|
+
### Step 2 -- confirm the action
|
|
27
|
+
Show the chosen item:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
id <id>
|
|
31
|
+
kind <kind>
|
|
32
|
+
file <file>
|
|
33
|
+
reason "<reason>"
|
|
34
|
+
condition <current condition>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then state the exact command you are about to run and ask for confirmation:
|
|
38
|
+
- **keep**: `whyline keep <id> "<reason the user gave>"`
|
|
39
|
+
- **until**: `whyline until <id> <YYYY-MM-DD>`
|
|
40
|
+
- **watch**: `whyline watch <id> --symbol <Name>`
|
|
41
|
+
|
|
42
|
+
For **keep**, ask the user for a short reason if they have not given one.
|
|
43
|
+
For **until**, ask for the date in YYYY-MM-DD format if they have not given one. Reject any other format and ask again.
|
|
44
|
+
For **watch**, ask for the symbol name if they have not given one.
|
|
45
|
+
|
|
46
|
+
Do not run the command until the user says yes (or equivalent).
|
|
47
|
+
|
|
48
|
+
### Step 3 -- run the command
|
|
49
|
+
Run the confirmed command with execute_command and show its output verbatim.
|
|
50
|
+
|
|
51
|
+
If the CLI exits non-zero (its exit code is 0 for normal answers, including a due list), stop and show its stderr as-is.
|
|
52
|
+
|
|
53
|
+
### Step 4 -- confirm the change
|
|
54
|
+
Run `whyline check --json` again and find the item. Show its new condition and state.
|
|
55
|
+
|
|
56
|
+
## Rules
|
|
57
|
+
- Never invent or guess an item id. Always read it from `whyline check --json`.
|
|
58
|
+
- Never skip the confirmation step.
|
|
59
|
+
- Dates must be YYYY-MM-DD; reject any other format.
|
|
60
|
+
- Show null values as "no data", never as 0.
|
|
61
|
+
- Trust the CLI output over any note in context.
|
|
62
|
+
- These commands change recorded state in git notes. They are not reversible with a simple undo. Say so in the confirmation prompt.
|
|
63
|
+
- Do not use this skill to remove items; that is handled by the whyline-remove skill.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-remove
|
|
3
|
+
description: "When the user asks to remove, delete or clean up temporary code that whyline tracks (a mock, stub, demo, flag, shim or fixture, named in plain words, by file, or by id), remove it with evidence, tests and the user's approval. Works in Agent mode."
|
|
4
|
+
---
|
|
5
|
+
## When to use
|
|
6
|
+
Trigger phrases: "remove the mock payment gateway", "remove mock_gateway.py", "delete the demo script", "clean up what whyline says is due", "remove L-12d3fa".
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
1. Find the item. Run `whyline check --json` with execute_command (exit code 0 is normal; the JSON is the answer). Match the user's words against `due` items by file name, symbol, kind or reason. If only an `active` item matches, say it is not due yet and why (its `evidence.summary`), then stop. If two items match, show both (file and id) and ask which one. Never guess.
|
|
10
|
+
2. References. Quote the item's `evidence.summary`. Then run `git grep -n -w <symbol>` for every symbol defined in the item's lines. There must be no hits outside the item's file, its own tests, and documentation.
|
|
11
|
+
3. Plan. List the exact files and line ranges to delete. If a test file only tests the item, include it. Nothing else.
|
|
12
|
+
4. Dry run. Remove the ranges, run the project's test command (package.json scripts.test, Makefile test target, or pytest), record the pass and fail counts. If tests fail, put the files back and report.
|
|
13
|
+
5. Present an evidence table (condition, references, tests, files, lines removed) and ask for approval. Do not edit anything before the user approves.
|
|
14
|
+
6. After approval: keep the edit and commit with the message `remove <file name>: <reason>`. The git hook records the item as removed. Run `whyline check` and confirm it is listed under DECIDED as removed. Suggest /create-pr as the next step.
|
|
15
|
+
|
|
16
|
+
## Rules
|
|
17
|
+
- Evidence before edits. Every claim cites a command and its output.
|
|
18
|
+
- Never widen scope: only the item's files, plus a test file that tests nothing else.
|
|
19
|
+
- If the CLI exits non-zero (its exit code is 0 for normal answers, including a due list), stop and show its stderr as-is.
|
|
20
|
+
- Never run git push. Never delete before approval.
|
|
21
|
+
|
|
22
|
+
## Output template
|
|
23
|
+
Evidence table, then "Proposed change:" with the files and lines, then the approval question. After approval: the commit id and the line from `whyline check` showing the item as removed.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-setup
|
|
3
|
+
description: Install and enable whyline provenance tracking in the current repository when the user asks Bob to set it up
|
|
4
|
+
metadata:
|
|
5
|
+
user-invocable: true
|
|
6
|
+
disable-model-invocation: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Step 1 -- check the whyline binary
|
|
10
|
+
|
|
11
|
+
Run `command -v whyline` with execute_command.
|
|
12
|
+
|
|
13
|
+
- **If it exits non-zero** (command not found): tell the user whyline is not installed and show the install command:
|
|
14
|
+
```
|
|
15
|
+
npm install -g @sal-sovereign-ai-labs/whyline
|
|
16
|
+
```
|
|
17
|
+
Then stop. Do not run any further steps until the user confirms the binary is available.
|
|
18
|
+
|
|
19
|
+
- **If it exits 0**: continue to Step 2.
|
|
20
|
+
|
|
21
|
+
## Step 2 -- run init
|
|
22
|
+
|
|
23
|
+
Run `whyline init --agent bob` with execute_command in the repository root.
|
|
24
|
+
|
|
25
|
+
- If the command exits non-zero or prints to stderr, stop and show its stderr as-is.
|
|
26
|
+
- If it exits 0, show its stdout verbatim.
|
|
27
|
+
|
|
28
|
+
## Step 3 -- tell the user what to commit
|
|
29
|
+
|
|
30
|
+
After a successful init, tell the user:
|
|
31
|
+
|
|
32
|
+
> Whyline is set up. Commit the `.bob` folder so your team gets the hooks:
|
|
33
|
+
>
|
|
34
|
+
> ```
|
|
35
|
+
> git add .bob && git commit -m "chore: add whyline provenance hooks"
|
|
36
|
+
> ```
|
|
37
|
+
>
|
|
38
|
+
> After that, every Bob session records the prompt that caused each write. Run `whyline check` at any time to see temporary items, or ask Bob "why does this line exist?" to trace any line.
|
|
39
|
+
|
|
40
|
+
Do not edit any files. Do not run git commands yourself.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-status
|
|
3
|
+
description: "When the user asks what AI code is unreviewed, how much of a release or branch is AI-written, what an AI bill of materials says, or for the Whyline report, run whyline unreviewed, whyline bom or whyline report."
|
|
4
|
+
---
|
|
5
|
+
## When to use
|
|
6
|
+
Trigger phrases: "what AI code is unreviewed", "what did the AI write in this release", "AI bill of materials", "how much of this is AI", "open the report".
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
- Unreviewed questions: run `whyline unreviewed --json` with execute_command.
|
|
10
|
+
- Release or range questions: run `whyline bom <range> --json` where `<range>` is exactly what the user named (a tag pair like `v1.0..HEAD`, a branch pair like `main..HEAD`, or a single tag). Omit the range entirely to let whyline pick the last tag or the whole history.
|
|
11
|
+
- Report requests: run `whyline report` and give the user the file path it prints.
|
|
12
|
+
|
|
13
|
+
## Rules
|
|
14
|
+
- Never invent a range, tag or item id; ask the user if it is unclear.
|
|
15
|
+
- Show null values as "no data", never as 0.
|
|
16
|
+
- Trust the CLI output over any note in context.
|
|
17
|
+
- If the CLI exits non-zero (its exit code is 0 for normal answers, including a due list), stop and show its stderr as-is.
|
|
18
|
+
- Read-only: never edit files or notes.
|
|
19
|
+
|
|
20
|
+
## Output template
|
|
21
|
+
For **unreviewed**: a table of file, AI lines, coverage (worst first), then the totals line.
|
|
22
|
+
For **bom**: one table with rows: lines changed, AI lines (with % of lines changed), by agent, reviewed (with % of AI lines), tested, active items, due items, removed items, cost ("n of m sessions"); then list any missing data fields.
|
|
23
|
+
End with one next command: `whyline why <file>:<line>` to trace a specific line, or tell the user to say "remove `<id>`" with the whyline-remove skill when any items are due.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whyline-why
|
|
3
|
+
description: "When the user asks why a line exists, who wrote it, what prompt caused it, or whether a line is AI-written, run whyline why <file>:<line> --json and explain the result."
|
|
4
|
+
---
|
|
5
|
+
## When to use
|
|
6
|
+
Trigger phrases: "why does this line exist", "who wrote this line", "what prompt caused this", "is this line AI-written", "why is this here", "trace this line", "whyline why".
|
|
7
|
+
|
|
8
|
+
## Input
|
|
9
|
+
Ask for a file path and line number if the user has not given one. Accept `<file>:<line>` notation directly.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
Run `whyline why <file>:<line> --json` with execute_command.
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
- Never guess the file or line number; ask the user if either is missing.
|
|
16
|
+
- Show null values as "no data", never as 0.
|
|
17
|
+
- Trust the CLI output over any note in context.
|
|
18
|
+
- If the CLI exits non-zero (its exit code is 0 for normal answers, including a due list), stop and show its stderr as-is.
|
|
19
|
+
- Read-only: never edit files or notes.
|
|
20
|
+
|
|
21
|
+
## Interpreting the result
|
|
22
|
+
- `found: false` -- git blame could not locate the line (file not tracked or line out of range). Show the `reason` field.
|
|
23
|
+
- `origin: "human"` -- a human wrote this line. Show the author and short commit hash.
|
|
24
|
+
- `origin: "ai"` -- the agent wrote every part of this line. Show all fields below.
|
|
25
|
+
- `origin: "ai-edited"` -- a human and an agent both touched this line. Show all fields below.
|
|
26
|
+
|
|
27
|
+
## Output template (ai-written or ai-edited)
|
|
28
|
+
```
|
|
29
|
+
<file>:<line>
|
|
30
|
+
origin <origin> (<agent>) · session <first 8 chars of session> · <author> · <date> · <cost> Bobcoin
|
|
31
|
+
prompt "<prompt>"
|
|
32
|
+
siblings <sibling ranges, or "none">
|
|
33
|
+
item <id> <kind> · <status> · <condition> · "<reason>" (omit line if item is null)
|
|
34
|
+
commit <short hash>
|
|
35
|
+
```
|
|
36
|
+
- Omit the `item` line when `item` is null.
|
|
37
|
+
- Omit the `cost` segment when cost is null.
|
|
38
|
+
- Omit the `siblings` line when the siblings array is empty.
|
|
39
|
+
- End with one next command: `whyline check` to see all temporary items, or tell the user to say "remove `<item.id>`" with the whyline-remove skill if the item status is `due`.
|