@warren-dean/code-kit 0.12.0 → 0.12.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +2 -2
- package/hooks/lib/rules.mjs +34 -0
- package/hooks/mod/decisions.mjs +79 -3
- package/hooks/mod/panes.mjs +82 -11
- package/hooks/mod/register.mjs +59 -2
- package/hooks/mod/register.test.ts +102 -6
- package/hooks/mod/view.mjs +65 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "code-kit",
|
|
10
10
|
"source": "./",
|
|
11
11
|
"description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
|
|
12
|
-
"version": "0.12.
|
|
12
|
+
"version": "0.12.2",
|
|
13
13
|
"category": "development",
|
|
14
14
|
"tags": ["guardrails", "architecture", "lanes", "layers", "spec-driven", "ci", "multi-agent"],
|
|
15
15
|
"license": "MIT"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-kit",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules, from idea to merged code: spec design, lanes, layer rules, protected paths with an approval log, spec-check and design gates, proof-before-finish checks, dispatch, review, build status and CI verification, configured per project from its docs",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Warren Langeveldt"
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
What changed in each release of code-kit, newest first. Versions follow the plugin manifest and the npm package `@warren-dean/code-kit`. Each release is also on [GitHub Releases](https://github.com/warrendeanlangeveldt/code-kit/releases).
|
|
4
4
|
|
|
5
|
+
## 0.12.2 (2026-10-13)
|
|
6
|
+
|
|
7
|
+
The Decisions tab is readable, and old package requests stop waiting.
|
|
8
|
+
|
|
9
|
+
- **Decisions as short cards.** The tab says how many of each kind wait ("10 approvals · 2 questions"), then lists each as a card: its kind, a one-line headline ("Approve fastify, jose, drizzle-orm and 4 more") and who it's for, with commands cut at their first pipe instead of whole scripts. Five show at a time; **j** and **k** move, and the selected one opens in full below.
|
|
10
|
+
- **Package requests close once installed.** A request for a package closes once the package is in a committed `package.json`, whoever installed it, so installs already done no longer wait for you.
|
|
11
|
+
|
|
12
|
+
## 0.12.1 (2026-10-13)
|
|
13
|
+
|
|
14
|
+
Ask about an approval before you give it.
|
|
15
|
+
|
|
16
|
+
- **Ask about it, in the Approve pane.** When a request isn't clear, ask what it's for right there. The answer comes from a side call over the lead's conversation and what code-kit knows of the request (what was asked for and for whom, why it was refused, its story, the limits it missed), and it stays in the pane: nothing is added to the lead's session. Follow-ups carry on the conversation. Before the lead has replied, it answers from code-kit's facts alone. Nothing is approved until you press Approve.
|
|
17
|
+
|
|
5
18
|
## 0.12.0 (2026-10-13)
|
|
6
19
|
|
|
7
20
|
What only you can decide reaches you, and waits until you answer.
|
package/README.md
CHANGED
|
@@ -280,7 +280,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
280
280
|
- **The band** sits above the prompt. While the build is moving (the lead mid-turn, an agent at work, the loop started) it opens with the mission: **goal**, the milestone being built and how many of its stories are done; **now**, what the lead is doing and the loop's state, with **Pause** or **Resume**; **next**, the step the loop takes next; and **agents**, a line per agent at work with its current tool and how long since. With Context Graph installed, a line says when a lane edited files without understanding them, and only then. Beneath, labelled **you**, what only you can decide, each with the story it's for ("for ST-4 Booking form (BOOK-1)"), and **d** ("Why, and what each choice does") opens it as a card in the Decisions tab; usage and the plan's pause are labelled **note**. In a team (`team` in the config), it also names what of the team needs you, and only that: your stale claim (Keep or Release), a claim of yours changing the same files as someone else's, an intent touching what you own or lease (every intent, with Agree and Drop, for the tech lead), and a pull request in a lane you own. While the loop runs, a lane's finished work is the lead's to review and merge, so it never waits here; with the loop off, Review and Merge are offered to you. With nothing happening and nothing waiting, the band is gone. It shows:
|
|
281
281
|
- an agent's call held for you: installing a new package, writing or committing a protected file, or a lane changing the kit. Rather than stopping the agent, the call waits while the band asks ("web wants to install dayjs · 1:53"). **Approve** opens the confirmation and, once you confirm, the same call runs; **Refuse**, or no answer within `harness.hold.minutes` (2 unless set), refuses it as before. Under `autonomy: autonomous`, a call within `approvals.delegate` is approved as delegated without asking. Nothing is held under `claude -p` or the SDK, where nobody can answer;
|
|
282
282
|
- the lead loop, under `harness.autonomy`. `/code-kit:init` asks you how hands-off you want it. Until you've chosen, the loop's first step waits in the band ("code-kit can keep the build moving between your prompts") with **Keep going**, **Ask each time** and **Off**, and your answer is recorded as your setting. Once the lead has finished a turn, the loop keeps the build moving: with the lead idle and nothing typed in the prompt, it asks `code-kit next` and prompts the lead to dispatch the ready stories, review a finished one, or build its own, never the same step twice while nothing has changed, and never a story an agent is still on: one its lane agent is building, or a reviewer is reviewing, waits until that agent finishes ("waiting: ST-10 is still with its agent"). A step the lead was prompted with twice in a row and that is still next goes to you instead, in the band: "Merge lead/st-1: still next after 2 prompts. Ask the lead again?" with **Go**. **Pause** and **Resume** stop and start it. Under `propose`, each step waits in the band for **Go**; under `off`, the loop does nothing. A lane agent with no tool call for `harness.stall.nudgeMinutes` (5) is messaged, unless its work is done (it handed back, or its branch is in review or the merge queue); at `stall.restartMinutes` (10) the lead is asked to stop it and dispatch its story again, at most `stall.maxRestarts` (2) times, and then the band flags the lane. When every story is done, the band says so and the loop stops. With `harness.agents.reviewer.on`, a finished branch is first reviewed in the background by code-kit's reviewer agent, which the lead starts: it runs `verify` and the review checklist, read-only, and grades each finding blocker, concern or nit. The lead's review prompt then carries the findings, and a blocker means the branch is sent back. The pane shows each review, running with its clock or what it found. If the reviewer hasn't reported within 15 minutes, or the plan's 5-hour use is past `harness.background.pauseAtPercent`, the lead reviews without it. A branch the lead's review passes joins the merge queue (`code-kit queue`). Branches merge one at a time, in the order they passed, each verified against the base as it is then: the lead merges where `approvals.delegate.merge` is on under `autonomous`, otherwise the band shows the head with **Merge** for you. Who may merge is read from the config committed on the base, so a branch can't grant itself merges. There's no first merge for you: setting up commits the rules you approved straight onto the base, and in a project set up before that, the lead puts them there itself (`code-kit queue rules`) before its first merge. A merged lane's worktree is removed when it holds nothing uncommitted. A head that conflicts or fails verify goes back to its lane. The pane lists the queue, and lanes whose branch waits in it read `queued`;
|
|
283
|
-
- approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite. A request waits until it's answered, its file is committed by someone who could, or it's dropped (at most a week); on its card, **Not needed** drops it;
|
|
283
|
+
- approvals agents have asked for: **Approve…** opens a confirmation with a reason prefilled from the request, which you keep or rewrite. When it isn't clear what it's for, **Ask about it** in that pane: the answer comes from a side call over the lead's conversation and code-kit's facts about the request, shown in the pane and never added to the lead's session; follow-ups carry on the aside. A request waits until it's answered, its file is committed by someone who could, or it's dropped (at most a week); on its card, **Not needed** drops it;
|
|
284
284
|
- questions from the lead ("The lead asks: which weather provider?"): what it can't settle and only you can, asked with `code-kit ask`. **Answer…** opens it as a card with each of the lead's options and what it does, and **Something else…** for your own words; your answer is recorded and sent to the lead. A question can hold its story, which then reads `waiting on you`: nothing dispatches or reviews it, and the stories that depend on it wait, until you answer;
|
|
285
285
|
- your brief's open questions: **Answer…** sends your answer to the lead, which records it in the brief's decisions; **Later** puts one off until the next session;
|
|
286
286
|
- a call Claude Code's own permission check refused (its auto mode, a deny rule), which code-kit can't approve past: "Claude Code refused the lead's `pnpm db:generate`". **Put it in the prompt** puts a command in your prompt after `!` for you to run, **Done it** tells the lead you ran or allowed it, and **Not now** tells it to carry on with other work;
|
|
@@ -290,7 +290,7 @@ With Claude Code 2.1.287 or later, code-kit's mod shows the project in the sessi
|
|
|
290
290
|
- a story using more than three times the median of the stories finished this session (once three have finished): **Lanes** opens the pane;
|
|
291
291
|
- background agents paused, once the plan’s 5-hour use passes `harness.background.pauseAtPercent` (80% unless set).
|
|
292
292
|
|
|
293
|
-
Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab
|
|
293
|
+
Each button has a digit: type it in an empty prompt to press it. The band keeps one line per kind ("2 approvals waiting"); the Decisions tab says how many of each kind, then lists a short card for each decision, five at a time (**j** and **k** move), with the selected one in full below.
|
|
294
294
|
|
|
295
295
|
- **`/lanes`** opens the Lanes pane. Its first line counts the stories by state (building, in review, queued, sent back, blocked, ready, merged), with the loop's state and the plan's 5-hour use; press a count to show only those lanes. Beneath it, whatever needs you (held calls, approvals, reviews, merges, stalled lanes, outliers) is pinned with its key. Tabs on **1** to **7**: **Lanes** opens on the loop as a card: its mode, the steps it took, the one running and the next. Then each lane on fixed columns: its name, state and story, its agent's current or last tool call with the time since beneath (amber once quiet, red when stalled), and its tokens; the lead reads orchestrating while its turn runs. A selected lane shows its stories as cards (state, requirements, what each waits on or its branch), then its agent and the paths it owns. **Decisions**, each decision that needs you as a card: what you asked for, the story and its requirements, what was happening, what's already settled, why the lead couldn't settle it, and each option with what it does; **Journal**, the build from your request to now in plain sentences, by day: the request and the decisions in the brief, the specification, each story handed out, sent back and merged, each approval and who gave it, each milestone finished, and the loop's steps this session, yours in yellow. Each lane has its own small pixel character, tinted in the lane's colour, whose pose shows its state: working (animated while its agent works), waiting on you, quiet, stalled, done or resting; the lead's wears a crown. Under each lane, its story's timeline on a shared time axis: building, then in review, with send-backs (↩), approvals (◆) and the merge (✓) marked where they fell; **Code map**, the whole project's folders by layer when no story is at work, or a story's files in a column per layer with the imports between them: a file fills with a lane's colour while that lane reads or edits it, and says whether its why (its Context Graph card) is written, out of date or not written yet; **j**/**k** select a file to see its card and what it imports and what imports it, and **o** opens it in Context Graph (box-drawing text in the terminal, an SVG on Desktop); **Trail**, every requirement in the specs as a cell by spec, its glyph and colour saying whether it's done and tested, done but untested, in progress, todo, without a story or removed, with a legend counting each. **h**/**l** and **j**/**k** move between cells and specs (or pick one from the list), showing the requirement's stories, their branches, the code they changed and the tests that name it; beneath the cells, the spec's trail drawn as a flow, each requirement to its stories, code and tests, with a missing link dashed in red; **Queue**, the merge queue; and **Usage**. **j** and **k** move between lanes, and the selected lane's acts are on **a** (approve), **r** (review), **s** (send back, with your reason), **m** (merge), **n** (nudge its agent) and **x** (stop it); **p** pauses or resumes the loop, and **f** filters the lanes by name, agent or story. **Enter** (or **o**) opens the selected lane's story: its requirements beside its spec-check report, the reviewer's findings by severity (each a way into the diff), `verify`'s problems, its usage and loop steps, and its diff against the base, file by file, with the acts its state allows. **b** or Escape goes back to the lanes. Inline (when the terminal can't dock it beside the transcript), it shows the counts, what needs you and a line per lane. It stays current within 2 seconds of a commit, a branch change, a plan edit or an agent starting or stopping. `/lanes` again, or Escape, closes it.
|
|
296
296
|
- **Refusals** a code-kit hook gives an agent are drawn as cards: what was refused, the rule, what to do, and Approve… where your approval would allow it. The full text is a press away.
|
package/hooks/lib/rules.mjs
CHANGED
|
@@ -415,6 +415,7 @@ export function openRequests(root, now = Date.now()) {
|
|
|
415
415
|
// allowed), or the file it named was committed since by someone who could.
|
|
416
416
|
if (r.names.every((n) => grantedSince(root, given, n, r))) continue;
|
|
417
417
|
if (committedSince(root, r)) continue;
|
|
418
|
+
if (installedSince(root, r)) continue;
|
|
418
419
|
open.push(r);
|
|
419
420
|
}
|
|
420
421
|
return open;
|
|
@@ -439,6 +440,39 @@ function grantedSince(root, given, name, r) {
|
|
|
439
440
|
});
|
|
440
441
|
}
|
|
441
442
|
|
|
443
|
+
/**
|
|
444
|
+
* CARD-7: whether every package a dependency request named has since been added to a committed
|
|
445
|
+
* manifest, on any branch: the install happened (approved, or run by the person themselves), so it no
|
|
446
|
+
* longer waits.
|
|
447
|
+
*/
|
|
448
|
+
function installedSince(root, r) {
|
|
449
|
+
if (!r.names.length || !r.names.every((n) => n.startsWith(DEPENDENCY_APPROVAL))) return false;
|
|
450
|
+
return r.names.every((name) => {
|
|
451
|
+
const pkg = name.slice(DEPENDENCY_APPROVAL.length).replaceAll('+', '/');
|
|
452
|
+
try {
|
|
453
|
+
const out = execFileSync(
|
|
454
|
+
'git',
|
|
455
|
+
[
|
|
456
|
+
'-C',
|
|
457
|
+
root,
|
|
458
|
+
'log',
|
|
459
|
+
'--all',
|
|
460
|
+
'-1',
|
|
461
|
+
'--format=%ct',
|
|
462
|
+
`--since=@${Math.floor(Date.parse(r.at) / 1000)}`,
|
|
463
|
+
`-S"${pkg}"`,
|
|
464
|
+
'--',
|
|
465
|
+
':(glob)**/package.json',
|
|
466
|
+
],
|
|
467
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] },
|
|
468
|
+
);
|
|
469
|
+
return Boolean(out.trim());
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
442
476
|
/** Whether the file a request named has been committed, on any branch, since it was asked. */
|
|
443
477
|
function committedSince(root, r) {
|
|
444
478
|
const path = String(r.what);
|
package/hooks/mod/decisions.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the person, each with its lineage. The band's "you" lines say what needs them; a decision card says why:
|
|
3
3
|
// what they asked for, the story and requirements it belongs to, what the agent was doing, what was already
|
|
4
4
|
// settled, why the lead couldn't settle this itself, and each option with what happens next.
|
|
5
|
-
import { askWords } from './view.mjs';
|
|
5
|
+
import { approvalLabel, askWords, commandItself } from './view.mjs';
|
|
6
6
|
|
|
7
7
|
/** The kinds of band line that are the person's to decide; the rest are notes. */
|
|
8
8
|
export const DECISION_KINDS = new Set([
|
|
@@ -136,6 +136,70 @@ function optionsOf(line) {
|
|
|
136
136
|
return line.actions;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/** A kind of decision as its card's tag says it. */
|
|
140
|
+
const TAG = {
|
|
141
|
+
held: 'held call',
|
|
142
|
+
approvals: 'approval',
|
|
143
|
+
stalled: 'stalled',
|
|
144
|
+
proposal: 'next step',
|
|
145
|
+
queue: 'merge',
|
|
146
|
+
review: 'review',
|
|
147
|
+
notice: 'notice',
|
|
148
|
+
question: 'question',
|
|
149
|
+
brief: 'your brief',
|
|
150
|
+
denied: 'refused',
|
|
151
|
+
report: 'bug report',
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** At most `n` characters on one line, cut with an ellipsis. */
|
|
155
|
+
const clip = (text, n) => {
|
|
156
|
+
const one = String(text ?? '')
|
|
157
|
+
.replace(/\s+/g, ' ')
|
|
158
|
+
.trim();
|
|
159
|
+
return one.length > n ? `${one.slice(0, n - 1)}…` : one;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** Names as a person reads them, at most three, then how many more. */
|
|
163
|
+
const someNames = (names) => {
|
|
164
|
+
const shown = names.slice(0, 3).map(approvalLabel).join(', ');
|
|
165
|
+
return names.length > 3 ? `${shown} and ${names.length - 3} more` : shown;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* CARD-9: a decision's headline and the line under it, short enough for a card in a list:
|
|
170
|
+
* { tag, headline, sub }.
|
|
171
|
+
*/
|
|
172
|
+
function headlineOf(line, story) {
|
|
173
|
+
const forStory = story ? ` · ${story.id}` : '';
|
|
174
|
+
if (line.kind === 'approvals') {
|
|
175
|
+
const who = line.request.lane ? `the ${line.request.lane} lane` : line.request.actor;
|
|
176
|
+
return {
|
|
177
|
+
headline: `Approve ${someNames(line.request.names)}`,
|
|
178
|
+
sub: `for ${who}${forStory} · ${clip(commandItself(line.files?.[0] ?? line.request.what), 70)}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (line.kind === 'held')
|
|
182
|
+
return {
|
|
183
|
+
headline: `${line.held.who} wants to ${line.text.replace(/^.*? wants to /, '').replace(/ · \d+:\d+$/, '')}`,
|
|
184
|
+
sub: `held${forStory} · ${line.text.match(/\d+:\d+$/)?.[0] ?? ''} left`,
|
|
185
|
+
};
|
|
186
|
+
if (line.kind === 'question')
|
|
187
|
+
return {
|
|
188
|
+
headline: line.question.question,
|
|
189
|
+
sub: `from the lead${forStory}${line.question.blocks ? ', which waits on it' : ''}`,
|
|
190
|
+
};
|
|
191
|
+
if (line.kind === 'brief')
|
|
192
|
+
return { headline: line.brief.question, sub: `open in ${line.brief.path}` };
|
|
193
|
+
if (line.kind === 'denied')
|
|
194
|
+
return {
|
|
195
|
+
headline: `Claude Code refused ${line.denial.who}'s call`,
|
|
196
|
+
sub: clip(line.denial.command ?? line.denial.what, 80),
|
|
197
|
+
};
|
|
198
|
+
if (line.kind === 'report')
|
|
199
|
+
return { headline: line.report.title, sub: "drafted by an agent, for code-kit's GitHub" };
|
|
200
|
+
return { headline: line.text, sub: story ? `${story.id} ${story.title ?? ''}` : '' };
|
|
201
|
+
}
|
|
202
|
+
|
|
139
203
|
/** Why a line needs the person, when the line itself doesn't say. */
|
|
140
204
|
function whyOf(line, check) {
|
|
141
205
|
if (line.kind === 'held') {
|
|
@@ -243,9 +307,13 @@ export function decisionCards(lines, context = {}) {
|
|
|
243
307
|
`${a.name} is approved${a.lane ? ` for the ${a.lane} lane` : ''}${a.reason ? `: ${a.reason}` : ''}`,
|
|
244
308
|
),
|
|
245
309
|
].slice(-5);
|
|
310
|
+
const { headline, sub } = headlineOf(line, story);
|
|
246
311
|
return {
|
|
247
312
|
id: `${line.kind}-${i}`,
|
|
248
313
|
kind: line.kind,
|
|
314
|
+
tag: TAG[line.kind] ?? 'decision',
|
|
315
|
+
headline: clip(headline, 100),
|
|
316
|
+
sub,
|
|
249
317
|
line,
|
|
250
318
|
// The band line it came from: one line can stand for several decisions (CARD-8).
|
|
251
319
|
source,
|
|
@@ -260,14 +328,22 @@ export function decisionCards(lines, context = {}) {
|
|
|
260
328
|
})),
|
|
261
329
|
doing:
|
|
262
330
|
line.held?.command ??
|
|
263
|
-
(line.request
|
|
331
|
+
(line.request
|
|
332
|
+
? clip(
|
|
333
|
+
askWords(
|
|
334
|
+
line.request,
|
|
335
|
+
(line.files ?? [line.request.what]).map((f) => commandItself(f)),
|
|
336
|
+
),
|
|
337
|
+
300,
|
|
338
|
+
)
|
|
339
|
+
: null) ??
|
|
264
340
|
(line.proposal?.label ? `Next step: ${line.proposal.label}` : null) ??
|
|
265
341
|
(line.intent
|
|
266
342
|
? `Intent #${line.intent.id}: ${line.intent.about}${line.intent.touches.length ? ` (touches ${line.intent.touches.join(', ')})` : ''}`
|
|
267
343
|
: null) ??
|
|
268
344
|
(line.pull ? `Pull request #${line.pull.number} on ${line.pull.branch}` : null) ??
|
|
269
345
|
(line.question?.blocks ? `${line.question.story} waits on your answer.` : null) ??
|
|
270
|
-
(line.denial ? (line.denial.command ?? line.denial.what) : null) ??
|
|
346
|
+
(line.denial ? clip(line.denial.command ?? line.denial.what, 400) : null) ??
|
|
271
347
|
(line.report ? line.report.body : null),
|
|
272
348
|
settled,
|
|
273
349
|
why: whyOf(line, check),
|
package/hooks/mod/panes.mjs
CHANGED
|
@@ -1257,10 +1257,15 @@ export function codeTab(view, { Box, Text, Button, Svg }, on) {
|
|
|
1257
1257
|
return [picker, drawing, legend, detail, keys];
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
+
/** CARD-9: how many decision cards the list shows at once, around the selected one. */
|
|
1261
|
+
const CARDS_SHOWN = 5;
|
|
1262
|
+
|
|
1260
1263
|
/**
|
|
1261
|
-
* DECIDE-3: the Decisions tab:
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1264
|
+
* DECIDE-3, CARD-9: the Decisions tab: how many decisions need the person, by kind; then each as a short
|
|
1265
|
+
* card (its kind, a one-line headline, and who or what it's for), a few at a time around the selected
|
|
1266
|
+
* one, with j and k to move; then the selected one in full, with its lineage (what they asked for, the
|
|
1267
|
+
* story and requirements, what was happening, what's settled, why it needs them) and its options, each
|
|
1268
|
+
* saying what it does. `on`: onDecision(i), onNeed(id, line).
|
|
1264
1269
|
*/
|
|
1265
1270
|
export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
|
|
1266
1271
|
const text = (value, style = {}) => Text({ ...style, children: [value] });
|
|
@@ -1277,27 +1282,82 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
|
|
|
1277
1282
|
text(value, { wrap: 'wrap', ...style }),
|
|
1278
1283
|
],
|
|
1279
1284
|
});
|
|
1285
|
+
const kinds = new Map();
|
|
1286
|
+
for (const c of cards) kinds.set(c.tag, (kinds.get(c.tag) ?? 0) + 1);
|
|
1287
|
+
const from = Math.max(0, Math.min(at - Math.floor(CARDS_SHOWN / 2), cards.length - CARDS_SHOWN));
|
|
1288
|
+
const shown = cards.slice(from, from + CARDS_SHOWN);
|
|
1280
1289
|
const list = Box({
|
|
1281
1290
|
key: 'decision-list',
|
|
1282
1291
|
flexDirection: 'column',
|
|
1283
|
-
children:
|
|
1292
|
+
children: [
|
|
1293
|
+
...(from > 0 ? [text(` ↑ ${from} more`, { key: 'decision-above', dimColor: true })] : []),
|
|
1294
|
+
...shown.map((c, n) => {
|
|
1295
|
+
const i = from + n;
|
|
1296
|
+
return Box({
|
|
1297
|
+
key: `decision-item-${i}`,
|
|
1298
|
+
flexDirection: 'column',
|
|
1299
|
+
borderStyle: 'round',
|
|
1300
|
+
borderColor: i === at ? 'yellow' : 'gray',
|
|
1301
|
+
paddingX: 1,
|
|
1302
|
+
children: [
|
|
1303
|
+
Box({
|
|
1304
|
+
flexDirection: 'row',
|
|
1305
|
+
columnGap: 1,
|
|
1306
|
+
children: [
|
|
1307
|
+
text(c.tag.toUpperCase(), { color: i === at ? 'yellow' : 'cyan', bold: true }),
|
|
1308
|
+
Button({
|
|
1309
|
+
key: `decision-pick-${i}`,
|
|
1310
|
+
label: c.headline,
|
|
1311
|
+
plain: true,
|
|
1312
|
+
onPress: () => on.onDecision(i),
|
|
1313
|
+
}),
|
|
1314
|
+
],
|
|
1315
|
+
}),
|
|
1316
|
+
...(c.sub ? [text(c.sub, { dimColor: true, wrap: 'truncate-end' })] : []),
|
|
1317
|
+
],
|
|
1318
|
+
});
|
|
1319
|
+
}),
|
|
1320
|
+
...(from + CARDS_SHOWN < cards.length
|
|
1321
|
+
? [
|
|
1322
|
+
text(` ↓ ${cards.length - from - CARDS_SHOWN} more`, {
|
|
1323
|
+
key: 'decision-below',
|
|
1324
|
+
dimColor: true,
|
|
1325
|
+
}),
|
|
1326
|
+
]
|
|
1327
|
+
: []),
|
|
1328
|
+
],
|
|
1329
|
+
});
|
|
1330
|
+
const keys = Box({
|
|
1331
|
+
key: 'decision-keys',
|
|
1332
|
+
flexDirection: 'row',
|
|
1333
|
+
columnGap: 2,
|
|
1334
|
+
children: [
|
|
1284
1335
|
Button({
|
|
1285
|
-
key:
|
|
1286
|
-
label:
|
|
1336
|
+
key: 'decision-next',
|
|
1337
|
+
label: 'Next',
|
|
1338
|
+
hotkey: 'j',
|
|
1287
1339
|
plain: true,
|
|
1288
|
-
onPress: () => on.onDecision(
|
|
1340
|
+
onPress: () => on.onDecision(Math.min(at + 1, cards.length - 1)),
|
|
1289
1341
|
}),
|
|
1290
|
-
|
|
1342
|
+
Button({
|
|
1343
|
+
key: 'decision-prev',
|
|
1344
|
+
label: 'Previous',
|
|
1345
|
+
hotkey: 'k',
|
|
1346
|
+
plain: true,
|
|
1347
|
+
onPress: () => on.onDecision(Math.max(at - 1, 0)),
|
|
1348
|
+
}),
|
|
1349
|
+
text(`${at + 1} of ${cards.length}`, { dimColor: true }),
|
|
1350
|
+
],
|
|
1291
1351
|
});
|
|
1292
1352
|
const detail = Box({
|
|
1293
1353
|
key: 'decision-card',
|
|
1294
1354
|
flexDirection: 'column',
|
|
1295
|
-
borderStyle: '
|
|
1355
|
+
borderStyle: 'double',
|
|
1296
1356
|
borderColor: 'yellow',
|
|
1297
1357
|
paddingX: 1,
|
|
1298
1358
|
rowGap: 0,
|
|
1299
1359
|
children: [
|
|
1300
|
-
text(card.
|
|
1360
|
+
text(card.headline, { bold: true, wrap: 'wrap' }),
|
|
1301
1361
|
...(card.asked ? [row('You asked for', card.asked)] : []),
|
|
1302
1362
|
...(card.story
|
|
1303
1363
|
? [
|
|
@@ -1353,7 +1413,18 @@ export function decisionsTab(cards, ui, { Box, Text, Button }, on) {
|
|
|
1353
1413
|
`${cards.length} decision${cards.length === 1 ? '' : 's'} need${cards.length === 1 ? 's' : ''} you. Everything else is the lead's.`,
|
|
1354
1414
|
{ bold: true },
|
|
1355
1415
|
),
|
|
1356
|
-
|
|
1416
|
+
Box({
|
|
1417
|
+
key: 'decision-kinds',
|
|
1418
|
+
children: [
|
|
1419
|
+
text(
|
|
1420
|
+
[...kinds]
|
|
1421
|
+
.map(([tag, n]) => `${n} ${tag}${n === 1 || tag.endsWith('brief') ? '' : 's'}`)
|
|
1422
|
+
.join(' · '),
|
|
1423
|
+
{ dimColor: true },
|
|
1424
|
+
),
|
|
1425
|
+
],
|
|
1426
|
+
}),
|
|
1427
|
+
...(cards.length > 1 ? [list, keys] : []),
|
|
1357
1428
|
detail,
|
|
1358
1429
|
];
|
|
1359
1430
|
}
|
package/hooks/mod/register.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
shownState,
|
|
20
20
|
attribution,
|
|
21
21
|
answerPane,
|
|
22
|
+
approvalAsidePrompt,
|
|
22
23
|
approvePane,
|
|
23
24
|
commandItself,
|
|
24
25
|
band,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
withoutInFlight,
|
|
60
61
|
} from './loop.mjs';
|
|
61
62
|
import { goalOf, nextText } from './mission.mjs';
|
|
62
|
-
import { contextOf, decisionCards } from './decisions.mjs';
|
|
63
|
+
import { contextOf, decisionCards, limitsWhy } from './decisions.mjs';
|
|
63
64
|
import { DEFAULT_TEAM_UI, TEAM_ID, teamFrame } from './teamview.mjs';
|
|
64
65
|
import { glowOf, mapStoryOf, touchOf } from './codemap.mjs';
|
|
65
66
|
import { harnessSettings } from '../lib/harness.mjs';
|
|
@@ -501,6 +502,8 @@ export function register(on) {
|
|
|
501
502
|
reason: prefilledReason(line.request),
|
|
502
503
|
error: null,
|
|
503
504
|
heldId: line.heldId ?? null,
|
|
505
|
+
outside: line.outside ?? null,
|
|
506
|
+
aside: { thread: [], draft: '' },
|
|
504
507
|
};
|
|
505
508
|
await $.ui.open({ id: APPROVE_ID, title: 'Approve', focus: true, closeOnEscape: true });
|
|
506
509
|
$.ui.invalidate('ui.render');
|
|
@@ -663,9 +666,58 @@ export function register(on) {
|
|
|
663
666
|
// HOLD-2: Approve opens ACT-1's confirmation for the held call; Refuse lets the refusal through.
|
|
664
667
|
approveHeld: (line) =>
|
|
665
668
|
act.approve({
|
|
666
|
-
request: {
|
|
669
|
+
request: {
|
|
670
|
+
names: line.held.names,
|
|
671
|
+
lane: line.held.lane,
|
|
672
|
+
what: line.held.what,
|
|
673
|
+
why: line.held.refusal ?? null,
|
|
674
|
+
},
|
|
667
675
|
heldId: line.held.id,
|
|
676
|
+
outside: line.held.outside ?? null,
|
|
668
677
|
}),
|
|
678
|
+
// ACT-5: a question about the approval, answered in its pane by a side call over the lead's own
|
|
679
|
+
// conversation (it shares the prompt cache, and nothing is added to the conversation); before the
|
|
680
|
+
// lead's first reply, from code-kit's facts alone.
|
|
681
|
+
askApproval: async (question) => {
|
|
682
|
+
const q = question.trim();
|
|
683
|
+
if (!approving || !q) return;
|
|
684
|
+
const asked = approving;
|
|
685
|
+
const thread = asked.aside.thread;
|
|
686
|
+
const turn = { question: q, pending: true };
|
|
687
|
+
approving = { ...asked, aside: { thread: [...thread, turn], draft: '' } };
|
|
688
|
+
$.ui.invalidate('ui.render');
|
|
689
|
+
const row = (model.state?.lanes ?? []).find((l) => l.name === asked.request.lane);
|
|
690
|
+
const prompt = approvalAsidePrompt({
|
|
691
|
+
request: asked.request,
|
|
692
|
+
story: row?.story ?? null,
|
|
693
|
+
outside: limitsWhy(asked.outside) ?? null,
|
|
694
|
+
thread,
|
|
695
|
+
question: q,
|
|
696
|
+
});
|
|
697
|
+
let reply = await $.model
|
|
698
|
+
.fork({ prompt })
|
|
699
|
+
.catch((err) => ({ isAnswered: false, reason: String(err?.message ?? err) }));
|
|
700
|
+
if (!reply.isAnswered && reply.reason === 'nothing-to-fork')
|
|
701
|
+
reply = await $.model
|
|
702
|
+
.complete({ model: 'sonnet', prompt, maxTokens: 800 })
|
|
703
|
+
.catch((err) => ({ isAnswered: false, reason: String(err?.message ?? err) }));
|
|
704
|
+
// Answered for this approval only: a pane closed or moved on meanwhile keeps what it has.
|
|
705
|
+
if (!approving || approving.request !== asked.request) return;
|
|
706
|
+
const done = reply.isAnswered
|
|
707
|
+
? { question: q, answer: reply.text }
|
|
708
|
+
: {
|
|
709
|
+
question: q,
|
|
710
|
+
error: `No answer: ${reply.reason}${reply.status ? ` (${reply.status})` : ''}`,
|
|
711
|
+
};
|
|
712
|
+
approving = {
|
|
713
|
+
...approving,
|
|
714
|
+
aside: {
|
|
715
|
+
...approving.aside,
|
|
716
|
+
thread: approving.aside.thread.map((t) => (t === turn ? done : t)),
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
$.ui.invalidate('ui.render');
|
|
720
|
+
},
|
|
669
721
|
refuseHeld: async (line) => {
|
|
670
722
|
await answerHeld(line.held.id, 'refused');
|
|
671
723
|
},
|
|
@@ -1934,6 +1986,11 @@ export function register(on) {
|
|
|
1934
1986
|
},
|
|
1935
1987
|
onSubmit: (value) => act.confirmApproval(value),
|
|
1936
1988
|
onCancel: () => act.cancelApproval(),
|
|
1989
|
+
onAskInput: (value) => {
|
|
1990
|
+
approving = { ...approving, aside: { ...approving.aside, draft: value } };
|
|
1991
|
+
$.ui.invalidate('ui.render');
|
|
1992
|
+
},
|
|
1993
|
+
onAsk: (value) => act.askApproval(value),
|
|
1937
1994
|
});
|
|
1938
1995
|
});
|
|
1939
1996
|
|
|
@@ -160,6 +160,8 @@ function project() {
|
|
|
160
160
|
questions: [] as any[], // `code-kit ask --json`
|
|
161
161
|
reports: [] as any[], // `code-kit report --json`
|
|
162
162
|
fills: [] as string[], // what the mod put in the prompt
|
|
163
|
+
sideAsks: [] as any[], // side calls: { fork } or { model }, with their prompt
|
|
164
|
+
sideReply: { isAnswered: true, text: 'dayjs formats the booking dates.', usage: null } as any,
|
|
163
165
|
holdWaits: false, // false: the hold ends at once, as if timed out (tests about cards, not hold)
|
|
164
166
|
waiting: new Map<string, (answer: string) => void>(),
|
|
165
167
|
};
|
|
@@ -347,6 +349,14 @@ function stub(on: any, w: World) {
|
|
|
347
349
|
return { value: { agent: `code-kit:${e.name}` } };
|
|
348
350
|
});
|
|
349
351
|
on('prompt.read', () => ({ value: { text: w.draft, cursor: w.draft.length } }));
|
|
352
|
+
on('model.fork', ($: any, e: any) => {
|
|
353
|
+
w.sideAsks.push({ fork: true, prompt: e.prompt });
|
|
354
|
+
return { value: w.sideReply };
|
|
355
|
+
});
|
|
356
|
+
on('model.complete', ($: any, e: any) => {
|
|
357
|
+
w.sideAsks.push({ model: e.model, prompt: e.prompt });
|
|
358
|
+
return { value: { isAnswered: true, text: 'From the facts alone.', usage: null } };
|
|
359
|
+
});
|
|
350
360
|
// Claude Code's own settings hooks beneath: none answer a permission denial.
|
|
351
361
|
on('classic.PermissionDenied', () => ({}));
|
|
352
362
|
on('prompt.fill', ($: any, e: any) => {
|
|
@@ -644,6 +654,56 @@ test('ACT-1 the person may type their own reason, and an empty one is not accept
|
|
|
644
654
|
await ui.unmount();
|
|
645
655
|
});
|
|
646
656
|
|
|
657
|
+
test("ACT-5 the person asks about an approval in its pane; the answer stays there, out of the lead's conversation", async ($, on) => {
|
|
658
|
+
const w = project();
|
|
659
|
+
w.requests = [dayjs];
|
|
660
|
+
await start($, on, w);
|
|
661
|
+
const ui = await bandUi($);
|
|
662
|
+
await press($, 'band-approve');
|
|
663
|
+
const confirm = await pane($, APPROVE);
|
|
664
|
+
await $.ui.input({
|
|
665
|
+
plugin: 'code-kit',
|
|
666
|
+
key: 'approve-ask',
|
|
667
|
+
text: 'Why does the booking form need this?',
|
|
668
|
+
});
|
|
669
|
+
expect(w.sideAsks).toHaveLength(1);
|
|
670
|
+
expect(w.sideAsks[0].fork).toBe(true);
|
|
671
|
+
expect(w.sideAsks[0].prompt).toMatch(/approvals asked for: dep-dayjs/);
|
|
672
|
+
expect(w.sideAsks[0].prompt).toMatch(/for: the web lane/);
|
|
673
|
+
expect(w.sideAsks[0].prompt).toMatch(/story: ST-4 Booking form/);
|
|
674
|
+
expect(w.sideAsks[0].prompt).toMatch(/Question: Why does the booking form need this\?$/);
|
|
675
|
+
expect(
|
|
676
|
+
await confirm.find({ type: 'Text', text: 'You: Why does the booking form need this?' }),
|
|
677
|
+
).toBeDefined();
|
|
678
|
+
expect(JSON.stringify(await confirm.find({ key: 'aside-a-0' }))).toMatch(
|
|
679
|
+
/dayjs formats the booking dates/,
|
|
680
|
+
);
|
|
681
|
+
// A follow-up carries the aside so far; nothing went to the lead, and nothing was approved.
|
|
682
|
+
await $.ui.input({ plugin: 'code-kit', key: 'approve-ask', text: 'Is it maintained?' });
|
|
683
|
+
expect(w.sideAsks[1].prompt).toMatch(
|
|
684
|
+
/Q: Why does the booking form need this\?\nA: dayjs formats the booking dates\./,
|
|
685
|
+
);
|
|
686
|
+
expect(w.prompts).toEqual([]);
|
|
687
|
+
expect(w.acts).toEqual([]);
|
|
688
|
+
await confirm.unmount();
|
|
689
|
+
await ui.unmount();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test('ACT-5 before the lead has replied there is nothing to fork: it answers from the facts alone', async ($, on) => {
|
|
693
|
+
const w = project();
|
|
694
|
+
w.requests = [dayjs];
|
|
695
|
+
w.sideReply = { isAnswered: false, reason: 'nothing-to-fork' };
|
|
696
|
+
await start($, on, w);
|
|
697
|
+
const ui = await bandUi($);
|
|
698
|
+
await press($, 'band-approve');
|
|
699
|
+
const confirm = await pane($, APPROVE);
|
|
700
|
+
await $.ui.input({ plugin: 'code-kit', key: 'approve-ask', text: 'What is it?' });
|
|
701
|
+
expect(w.sideAsks.map((a) => a.model ?? 'fork')).toEqual(['fork', 'sonnet']);
|
|
702
|
+
expect(JSON.stringify(await confirm.find({ key: 'aside-a-0' }))).toMatch(/From the facts alone/);
|
|
703
|
+
await confirm.unmount();
|
|
704
|
+
await ui.unmount();
|
|
705
|
+
});
|
|
706
|
+
|
|
647
707
|
test('ACT-1 cancelling writes nothing', async ($, on) => {
|
|
648
708
|
const w = project();
|
|
649
709
|
w.requests = [dayjs];
|
|
@@ -1461,13 +1521,10 @@ test("ASK-4 the brief's open questions are the person's decisions: answered to t
|
|
|
1461
1521
|
await press($, 'band-openDecision');
|
|
1462
1522
|
await band.unmount();
|
|
1463
1523
|
const ui = await pane($, PANE);
|
|
1464
|
-
expect(
|
|
1465
|
-
|
|
1466
|
-
).toBeDefined();
|
|
1524
|
+
expect(await ui.find({ type: 'Text', text: 'Accounts: AWS, Supabase, Fly' })).toBeDefined();
|
|
1525
|
+
expect(await ui.find({ type: 'Text', text: 'open in docs/brief.md' })).toBeDefined();
|
|
1467
1526
|
await press($, 'decide-laterBrief', PANE);
|
|
1468
|
-
expect(
|
|
1469
|
-
await ui.find({ type: 'Text', text: 'From your brief: The product domain' }),
|
|
1470
|
-
).toBeDefined();
|
|
1527
|
+
expect(await ui.find({ type: 'Text', text: 'The product domain' })).toBeDefined();
|
|
1471
1528
|
await press($, 'decide-answerBrief', PANE);
|
|
1472
1529
|
const box = await pane($, 'code-kit-answer');
|
|
1473
1530
|
await $.ui.input({ plugin: 'code-kit', key: 'answer-text', text: 'myaila.com.au' });
|
|
@@ -1542,6 +1599,45 @@ test('CARD-8 two approvals waiting are two decisions, each approved or dropped o
|
|
|
1542
1599
|
await ui.unmount();
|
|
1543
1600
|
});
|
|
1544
1601
|
|
|
1602
|
+
test('CARD-9 many decisions read as short cards, a few at a time, with the selected one in full', async ($, on) => {
|
|
1603
|
+
const w = project();
|
|
1604
|
+
w.requests = Array.from({ length: 8 }, (_, i) => ({
|
|
1605
|
+
...dayjs,
|
|
1606
|
+
at: `2026-10-06T00:0${i}:00.000Z`,
|
|
1607
|
+
names: [`dep-pkg${i}`, 'dep-a', 'dep-b', 'dep-c'],
|
|
1608
|
+
what: `pnpm --filter @app/web add pkg${i} a b c 2>&1 | tail -20 && cat > tsconfig.json <<'EOF'\n{ "x": 1 }\nEOF`,
|
|
1609
|
+
}));
|
|
1610
|
+
w.questions = [question()];
|
|
1611
|
+
await start($, on, w);
|
|
1612
|
+
const band = await bandUi($);
|
|
1613
|
+
await press($, 'band-decide');
|
|
1614
|
+
await band.unmount();
|
|
1615
|
+
const ui = await pane($, PANE);
|
|
1616
|
+
expect((await ui.find({ key: 'decision-kinds' }))?.text).toBe('8 approvals · 1 question');
|
|
1617
|
+
// Five cards at a time, each a headline and a line under it; the command cut at its pipe, on one line.
|
|
1618
|
+
expect(await ui.find({ key: 'decision-item-4' })).toBeDefined();
|
|
1619
|
+
expect(await ui.find({ key: 'decision-item-5' })).toBeUndefined();
|
|
1620
|
+
expect(await ui.find({ type: 'Text', text: ' ↓ 4 more' })).toBeDefined();
|
|
1621
|
+
expect((await ui.find({ key: 'decision-pick-0' }))?.props.label).toBe(
|
|
1622
|
+
'Approve pkg0, a, b and 1 more',
|
|
1623
|
+
);
|
|
1624
|
+
expect(
|
|
1625
|
+
await ui.find({
|
|
1626
|
+
type: 'Text',
|
|
1627
|
+
text: 'for the web lane · ST-4 · pnpm --filter @app/web add pkg0 a b c',
|
|
1628
|
+
}),
|
|
1629
|
+
).toBeDefined();
|
|
1630
|
+
expect((await ui.findAll({ type: 'Text' })).some((t: any) => /tsconfig|EOF/.test(t.text))).toBe(
|
|
1631
|
+
false,
|
|
1632
|
+
);
|
|
1633
|
+
await press($, 'decision-next', PANE);
|
|
1634
|
+
await press($, 'decision-next', PANE);
|
|
1635
|
+
await press($, 'decision-next', PANE);
|
|
1636
|
+
expect(await ui.find({ type: 'Text', text: /↑ \d+ more/ })).toBeDefined();
|
|
1637
|
+
expect((await ui.findAll({ type: 'Text' })).some((t: any) => t.text === '4 of 9')).toBe(true);
|
|
1638
|
+
await ui.unmount();
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1545
1641
|
test('REPORT-3 a bug report an agent drafted waits for the person, who reads it and sends it, or discards it', async ($, on) => {
|
|
1546
1642
|
const w = project();
|
|
1547
1643
|
w.reports = [
|
package/hooks/mod/view.mjs
CHANGED
|
@@ -1057,10 +1057,11 @@ const NOTE_KINDS = new Set(['usage', 'paused', 'finish', 'done', 'teamDown']);
|
|
|
1057
1057
|
/** The Approve… confirmation (ACT-1): what it grants, to whom, for how long, and the reason. */
|
|
1058
1058
|
export function approvePane(
|
|
1059
1059
|
approving,
|
|
1060
|
-
{ Box, Text, Input, Button },
|
|
1061
|
-
{ onInput, onSubmit, onCancel },
|
|
1060
|
+
{ Box, Text, Input, Button, Markdown },
|
|
1061
|
+
{ onInput, onSubmit, onCancel, onAskInput, onAsk },
|
|
1062
1062
|
) {
|
|
1063
1063
|
const { request, reason, error } = approving;
|
|
1064
|
+
const aside = approving.aside ?? { thread: [], draft: '' };
|
|
1064
1065
|
return Box({
|
|
1065
1066
|
flexDirection: 'column',
|
|
1066
1067
|
rowGap: 1,
|
|
@@ -1071,6 +1072,30 @@ export function approvePane(
|
|
|
1071
1072
|
`For ${request.lane ? `the ${request.lane} lane` : 'any agent'}, ${request.names.every((n) => n.startsWith('dep-')) ? 'until the install is committed (at most 7 days)' : 'for 60 minutes'}. Asked for: ${request.what}`,
|
|
1072
1073
|
],
|
|
1073
1074
|
}),
|
|
1075
|
+
// ACT-5: an aside about this approval, answered here and never in the lead's conversation.
|
|
1076
|
+
...aside.thread.flatMap((turn, i) => [
|
|
1077
|
+
Text({ key: `aside-q-${i}`, color: 'cyan', children: [`You: ${turn.question}`] }),
|
|
1078
|
+
turn.pending
|
|
1079
|
+
? Text({ key: `aside-a-${i}`, dimColor: true, children: ['Asking…'] })
|
|
1080
|
+
: turn.error
|
|
1081
|
+
? Text({ key: `aside-a-${i}`, color: 'red', children: [turn.error] })
|
|
1082
|
+
: Markdown
|
|
1083
|
+
? Markdown({ key: `aside-a-${i}`, text: turn.answer })
|
|
1084
|
+
: Text({ key: `aside-a-${i}`, children: [turn.answer] }),
|
|
1085
|
+
]),
|
|
1086
|
+
...(onAsk
|
|
1087
|
+
? [
|
|
1088
|
+
Input({
|
|
1089
|
+
key: 'approve-ask',
|
|
1090
|
+
label: 'Ask about it',
|
|
1091
|
+
value: aside.draft,
|
|
1092
|
+
placeholder: 'What is it for, and what does approving it let happen?',
|
|
1093
|
+
submitLabel: 'Ask',
|
|
1094
|
+
onInput: onAskInput,
|
|
1095
|
+
onSubmit: onAsk,
|
|
1096
|
+
}),
|
|
1097
|
+
]
|
|
1098
|
+
: []),
|
|
1074
1099
|
Input({
|
|
1075
1100
|
key: 'approve-reason',
|
|
1076
1101
|
label: 'Reason',
|
|
@@ -1093,6 +1118,44 @@ export function approvePane(
|
|
|
1093
1118
|
});
|
|
1094
1119
|
}
|
|
1095
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* ACT-5: the side call's prompt for a question about an approval: what code-kit knows of the request (the
|
|
1123
|
+
* names, the lane, what was refused and why, its story, the limits it missed), the aside so far, and the
|
|
1124
|
+
* question. It answers in the pane, never in the conversation, and acts on nothing.
|
|
1125
|
+
*/
|
|
1126
|
+
export function approvalAsidePrompt({
|
|
1127
|
+
request,
|
|
1128
|
+
story = null,
|
|
1129
|
+
outside = null,
|
|
1130
|
+
thread = [],
|
|
1131
|
+
question,
|
|
1132
|
+
}) {
|
|
1133
|
+
return [
|
|
1134
|
+
"The person is deciding whether to approve something code-kit stopped, and asks you about it in code-kit's approval pane. Answer in a few short sentences, in plain words: what it is, why it's needed for the work, what approving it lets happen, and anything risky about it. Use what you know from this conversation and the facts below. If you don't know, say so; don't guess. This answer goes to that pane, not into the conversation: don't act on it, and don't change your plans because of it.",
|
|
1135
|
+
'',
|
|
1136
|
+
'The approval:',
|
|
1137
|
+
`- approvals asked for: ${request.names.join(', ')}`,
|
|
1138
|
+
`- for: ${request.lane ? `the ${request.lane} lane` : (request.actor ?? 'an agent')}`,
|
|
1139
|
+
`- what was refused: ${request.what}`,
|
|
1140
|
+
...(request.why ? [`- why code-kit refused it: ${request.why}`] : []),
|
|
1141
|
+
...(story
|
|
1142
|
+
? [
|
|
1143
|
+
`- story: ${story.id} ${story.title ?? ''}${story.requirements?.length ? ` (${story.requirements.join(', ')})` : ''}`,
|
|
1144
|
+
]
|
|
1145
|
+
: []),
|
|
1146
|
+
...(outside ? [`- outside the limits the person set for the lead: ${outside}`] : []),
|
|
1147
|
+
...(thread.length
|
|
1148
|
+
? [
|
|
1149
|
+
'',
|
|
1150
|
+
'Asked so far:',
|
|
1151
|
+
...thread.flatMap((t) => [`Q: ${t.question}`, `A: ${t.answer ?? t.error ?? ''}`]),
|
|
1152
|
+
]
|
|
1153
|
+
: []),
|
|
1154
|
+
'',
|
|
1155
|
+
`Question: ${question}`,
|
|
1156
|
+
].join('\n');
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1096
1159
|
export const ANSWER_ID = 'code-kit-answer';
|
|
1097
1160
|
|
|
1098
1161
|
/** ASK-2, ASK-4: the person's answer to the lead's question, or to one in their brief, typed. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warren-dean/code-kit",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Keeps AI coding agents inside your project's rules: lanes, layer rules, protected paths and proof before finish, enforced by Claude Code hooks and in CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Warren Langeveldt",
|