@cspeach/cli 1.1.51 → 1.1.53
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/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +31 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +304 -49
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +21 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +25 -9
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +20 -3
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +22 -10
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +113 -3
- package/dist/commands/plan-startup.js +3 -3
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/models/server-config.js +1 -1
- package/dist/one-shot.js +1 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +107 -16
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +50 -18
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +17 -1
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +5 -1
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +15 -3
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +155 -24
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +12 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +49 -8
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +18 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +13 -7
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +2 -2
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec_answers_record — record blocker answers the developer gives in chat
|
|
3
|
+
* (Task 18, F-O, 2026-10-26).
|
|
4
|
+
*
|
|
5
|
+
* Live: after "ok go ahead" the model ran `node -e` to edit the spec-gap
|
|
6
|
+
* `.cspeach.json` itself, inventing an `answeredBy`, and tripped the risky
|
|
7
|
+
* command card. This tool is the proper way: it runs the blocker form's own
|
|
8
|
+
* save step (saveBlockerAnswers: applyBlockerAnswers with the session's
|
|
9
|
+
* signed-in identity → validateEnvelope → write in place), so an answer
|
|
10
|
+
* recorded here is the same as one typed into the form.
|
|
11
|
+
*
|
|
12
|
+
* Path: resolved like file_read (teamReadTarget), so the folder-relative path
|
|
13
|
+
* the blocker note gives (`.cspeach/<name>.cspeach.json`) opens and saves the
|
|
14
|
+
* same file — in a team folder that is the seat copy. Containment and
|
|
15
|
+
* real-path checks are file_read's. Fix round 1: only a saved result the CLI
|
|
16
|
+
* writes (`.cspeach/<name>.cspeach.json`, isReadOnlyProjectFile), never the
|
|
17
|
+
* shared team `.cspeach` (teamReadTarget's fallback there is read-only), and
|
|
18
|
+
* only a spec-gap envelope.
|
|
19
|
+
*
|
|
20
|
+
* Every refusal is one line and writes nothing: a sub-agent or reader child,
|
|
21
|
+
* a path outside the project, a missing file, a file that is not a spec-gap
|
|
22
|
+
* envelope in `.cspeach/`, the shared team `.cspeach`, an unknown id, a blank
|
|
23
|
+
* answer or one over 2,000 characters, an id given twice, a revision that
|
|
24
|
+
* fails validation. Ruling R18: any question id in the file is accepted.
|
|
25
|
+
*
|
|
26
|
+
* Approvals: like file_write for project files — category 'filesystem', so no
|
|
27
|
+
* card and no Rule 8 batch count (isLocalProjectWork). Never touches SAP, so
|
|
28
|
+
* the chat lane allows it.
|
|
29
|
+
*/
|
|
30
|
+
import { promises as fs, realpathSync } from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import { registerTool } from '../index.js';
|
|
33
|
+
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, isReadOnlyProjectFile, } from '../_filesystem-shared.js';
|
|
34
|
+
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamReadTarget } from '../_team-work-path.js';
|
|
35
|
+
import { SPEC_ANSWERS_RECORD_TOOL } from '../../projects/blocker-answers-note.js';
|
|
36
|
+
import { readProjectFile } from '../../projects/status.js';
|
|
37
|
+
import { openBlockers, saveBlockerAnswers } from '../../projects/answer-blockers.js';
|
|
38
|
+
export const SPEC_ANSWERS_RECORD = SPEC_ANSWERS_RECORD_TOOL;
|
|
39
|
+
/** Fix round 1 (item 4): the longest answer recorded. */
|
|
40
|
+
export const MAX_ANSWER = 2000;
|
|
41
|
+
const QUESTION_MAX = 80;
|
|
42
|
+
const NOTHING_SAVED = 'Nothing was saved.';
|
|
43
|
+
const fail = (line) => ({ content: `error: ${line}`, is_error: true });
|
|
44
|
+
function clip(text, max) {
|
|
45
|
+
const one = text.replace(/\s*[\r\n]+\s*/g, ' ').trim();
|
|
46
|
+
return one.length > max ? `${one.slice(0, max - 1).trimEnd()}…` : one;
|
|
47
|
+
}
|
|
48
|
+
/** The same identity loop.ts gives the blocker form (getAuthorIdentity). */
|
|
49
|
+
async function sessionIdentity() {
|
|
50
|
+
const { getAuthorIdentity } = await import('../../agent/loop.js');
|
|
51
|
+
return getAuthorIdentity();
|
|
52
|
+
}
|
|
53
|
+
function realOr(p) {
|
|
54
|
+
try {
|
|
55
|
+
return realpathSync.native(p);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return path.resolve(p);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function specAnswersRecordHandler(args, ctx) {
|
|
62
|
+
// Fix round 1 (item 5): only the main conversation records answers (a child could race the form).
|
|
63
|
+
if (ctx.subagent || ctx.readerMode)
|
|
64
|
+
return fail(`answers are recorded in the main conversation only. ${NOTHING_SAVED}`);
|
|
65
|
+
if (typeof args.path !== 'string' || args.path.trim() === '') {
|
|
66
|
+
return fail(`give the path of the spec-gap file, for example .cspeach/<name>.cspeach.json. ${NOTHING_SAVED}`);
|
|
67
|
+
}
|
|
68
|
+
const given = args.path;
|
|
69
|
+
// The path as file_read opens it: in a team folder a saved result is the seat copy.
|
|
70
|
+
const t = teamReadTarget(ctx.cwd, given);
|
|
71
|
+
let real;
|
|
72
|
+
try {
|
|
73
|
+
real = await assertRealPathContained(resolveSafePath(t.root, t.path), t.root);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err instanceof PathOutsideRootError)
|
|
77
|
+
return fail(`that path is outside the project folder. ${NOTHING_SAVED}`);
|
|
78
|
+
if (err.code === 'ENOENT')
|
|
79
|
+
return fail(`file not found: ${given}. ${NOTHING_SAVED}`);
|
|
80
|
+
return fail(`${err instanceof Error ? err.message : String(err)}. ${NOTHING_SAVED}`);
|
|
81
|
+
}
|
|
82
|
+
// Fix round 1 (item 2): never the shared team .cspeach — teamReadTarget's
|
|
83
|
+
// fallback to it is for reading only, as for the write tools.
|
|
84
|
+
if (pointsIntoSharedCspeach(ctx.cwd, real))
|
|
85
|
+
return fail(`${SHARED_CSPEACH_REFUSAL} ${NOTHING_SAVED}`);
|
|
86
|
+
// Fix round 1 (item 3): only a saved result the CLI writes (.cspeach/<name>.cspeach.json),
|
|
87
|
+
// the same shape file_read's denylist carve-out opens (lane files excluded).
|
|
88
|
+
if (!isReadOnlyProjectFile(real, t.root) || !/\.cspeach\.json$/i.test(real)) {
|
|
89
|
+
return fail(`${given} is not a spec-gap file. ${NOTHING_SAVED}`);
|
|
90
|
+
}
|
|
91
|
+
if (!(await fs.stat(real)).isFile())
|
|
92
|
+
return fail(`${given} is not a spec-gap file. ${NOTHING_SAVED}`);
|
|
93
|
+
let env;
|
|
94
|
+
try {
|
|
95
|
+
env = readProjectFile(real);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return fail(`${given} is not a spec-gap file. ${NOTHING_SAVED}`);
|
|
99
|
+
}
|
|
100
|
+
if (env.artefactType !== 'spec-gap')
|
|
101
|
+
return fail(`${given} is not a spec-gap file. ${NOTHING_SAVED}`);
|
|
102
|
+
// The answers: every id must be a question in the file (ruling R18: any
|
|
103
|
+
// question, not only blockers), with a non-blank answer of at most MAX_ANSWER.
|
|
104
|
+
const list = Array.isArray(args.answers) ? args.answers : null;
|
|
105
|
+
if (!list || list.length === 0)
|
|
106
|
+
return fail(`give at least one answer as { id, answer }. ${NOTHING_SAVED}`);
|
|
107
|
+
const knownIds = new Set(env.interaction.items.map((it) => it.id));
|
|
108
|
+
const answers = new Map();
|
|
109
|
+
const order = [];
|
|
110
|
+
const problems = [];
|
|
111
|
+
for (const entry of list) {
|
|
112
|
+
const id = entry && typeof entry === 'object' && typeof entry.id === 'string'
|
|
113
|
+
? entry.id.trim() : '';
|
|
114
|
+
const raw = entry && typeof entry === 'object' ? entry.answer : undefined;
|
|
115
|
+
const answer = typeof raw === 'string' ? raw.trim() : '';
|
|
116
|
+
if (!id) {
|
|
117
|
+
problems.push('an answer has no id');
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (order.includes(id)) {
|
|
121
|
+
problems.push(`${id}: given twice`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
order.push(id);
|
|
125
|
+
if (!knownIds.has(id)) {
|
|
126
|
+
problems.push(`${id}: no question with this id in the file`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!answer) {
|
|
130
|
+
problems.push(`${id}: the answer is blank`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (answer.length > MAX_ANSWER) {
|
|
134
|
+
problems.push(`${id}: the answer is longer than 2,000 characters`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
answers.set(id, answer);
|
|
138
|
+
}
|
|
139
|
+
if (problems.length > 0)
|
|
140
|
+
return fail(`${problems.join('; ')}. ${NOTHING_SAVED}`);
|
|
141
|
+
// The form's own save step: apply with the session identity, validate, write in place.
|
|
142
|
+
const saved = saveBlockerAnswers(real, env, answers, await sessionIdentity(), new Date().toISOString());
|
|
143
|
+
if (!saved.ok)
|
|
144
|
+
return fail(`the revised file did not validate (${saved.error}). ${NOTHING_SAVED}`);
|
|
145
|
+
const rel = path.relative(realOr(t.root), real).split(path.sep).join('/');
|
|
146
|
+
const open = openBlockers(saved.revised);
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
`Saved in ${rel}.`,
|
|
150
|
+
...order.map((id) => `${id}: recorded.`),
|
|
151
|
+
open.length === 0
|
|
152
|
+
? 'No blockers are still open.'
|
|
153
|
+
: `Still open: ${open.map((b) => `${b.id} (${clip(b.text, QUESTION_MAX)})`).join(', ')}.`,
|
|
154
|
+
].join('\n'),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
registerTool({
|
|
158
|
+
name: SPEC_ANSWERS_RECORD,
|
|
159
|
+
description: 'Record answers the developer gives in chat to spec-gap questions, in the saved spec-gap file '
|
|
160
|
+
+ '(.cspeach/<name>.cspeach.json): blockers or any question in the file. The file exists only once the document has been saved, after your turn. Give each question id as in the file '
|
|
161
|
+
+ '(for example gap-004) with its answer, at most 2,000 characters. '
|
|
162
|
+
+ 'Answering a question that already has an answer replaces it. Nothing is saved if any id or answer is refused. '
|
|
163
|
+
+ 'Never edit .cspeach.json files with shell or file-edit tools. No SAP call.',
|
|
164
|
+
isMutating: true,
|
|
165
|
+
category: 'filesystem',
|
|
166
|
+
requiresSap: false,
|
|
167
|
+
input_schema: {
|
|
168
|
+
type: 'object',
|
|
169
|
+
properties: {
|
|
170
|
+
path: {
|
|
171
|
+
type: 'string',
|
|
172
|
+
description: 'Folder-relative path of the spec-gap file, for example .cspeach/<name>.cspeach.json.',
|
|
173
|
+
},
|
|
174
|
+
answers: {
|
|
175
|
+
type: 'array',
|
|
176
|
+
description: 'One entry per question answered.',
|
|
177
|
+
items: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
properties: {
|
|
180
|
+
id: { type: 'string', description: 'The question id as in the file, for example gap-004.' },
|
|
181
|
+
answer: { type: 'string', description: "The developer's answer, in their words." },
|
|
182
|
+
},
|
|
183
|
+
required: ['id', 'answer'],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
required: ['path', 'answers'],
|
|
188
|
+
},
|
|
189
|
+
handler: (args, ctx) => specAnswersRecordHandler(args, ctx),
|
|
190
|
+
});
|
|
@@ -24,6 +24,7 @@ import { PKG_RE } from '../../projects/plan-objects.js';
|
|
|
24
24
|
import { askConfirm } from '../../ui/confirm-request.js';
|
|
25
25
|
import { cardSlotEmitter } from '../../ui/card-slot.js';
|
|
26
26
|
import { isHeadless } from '../../renderer/tty.js';
|
|
27
|
+
import { STOPPED_BEFORE_START_RESULT } from '../../agent/repair-partial.js';
|
|
27
28
|
const fail = (m) => ({ content: `error: ${m}`, is_error: true });
|
|
28
29
|
const NOT_CHOSEN = 'Skeleton not chosen. Nothing was saved.';
|
|
29
30
|
const TABLE_RE = /^[ZY][A-Z0-9_]{1,15}$/;
|
|
@@ -200,6 +201,9 @@ export async function rapSkeletonResolveHandler(args, ctx, deps = {}) {
|
|
|
200
201
|
ctx.chunkEmitter?.emit('info', `RAP skeleton chosen by the rules (no confirm card here): ${SKELETON_WHO[decision.by]}.`);
|
|
201
202
|
}
|
|
202
203
|
else {
|
|
204
|
+
// LT-15 D6 (review M2) — stopped during the reads above: no card, nothing saved.
|
|
205
|
+
if (ctx.signal?.aborted)
|
|
206
|
+
return { content: STOPPED_BEFORE_START_RESULT, is_error: true };
|
|
203
207
|
const card = skeletonCardRequest(decision, blocked?.reason ?? null, names, objects9);
|
|
204
208
|
const res = await (deps.confirm ?? ((r) => askConfirm(r)))(card);
|
|
205
209
|
const value = res.value;
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -25,13 +25,14 @@ import crypto from 'node:crypto';
|
|
|
25
25
|
import { registerTool } from './index.js';
|
|
26
26
|
import { byObjectType } from '../sap/profile-gate.js';
|
|
27
27
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
28
|
+
import { writtenObjectName } from '../approvals/op-labels.js';
|
|
28
29
|
import { grantActivateMismatch } from '../approvals/grant-claims.js';
|
|
29
30
|
import { getLook } from '../renderer/look.js';
|
|
30
31
|
import { verifyActive } from './verify.js';
|
|
31
32
|
import { hasRecordedSyntaxErrorsByName, clearSyntaxErrorsAfterActivation } from './syntax-state.js';
|
|
32
33
|
import { teamFixOffers, cspeachWritesThisSession } from '../register/offer.js';
|
|
33
34
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
34
|
-
import { checkTransportFit, reusedRequestRefusal } from './transport-fit-check.js';
|
|
35
|
+
import { checkTransportFit, checkTransportNeeded, reusedRequestRefusal, noteTransportNeededAsked, takeTransportNeededAsked, } from './transport-fit-check.js';
|
|
35
36
|
import { typeCode } from '../approvals/risk-floor.js';
|
|
36
37
|
import { auditLog, SapError, hasClassicParameterBlock } from '@cspeach/sap-client';
|
|
37
38
|
import { ERR } from '../errors/codes.js';
|
|
@@ -56,6 +57,9 @@ const SET_SOURCE_SPEC = {
|
|
|
56
57
|
beforeApproval: (a) => (typeCode(a.type) === 'FUNC' && hasClassicParameterBlock(String(a.source ?? ''))
|
|
57
58
|
? { error: 'classic_parameter_block', detail: FUNC_CLASSIC_BLOCK_TEXT }
|
|
58
59
|
: undefined),
|
|
60
|
+
// Rule 7a in code (2026-10-22): on an existing class, a rewrite whose only
|
|
61
|
+
// change is inside method bodies is refused — sap_update_method per method.
|
|
62
|
+
methodOnlyGuard: (a) => ({ type: a.type, name: a.name, source: String(a.source ?? '') }),
|
|
59
63
|
approval: {
|
|
60
64
|
object: (a) => a.name,
|
|
61
65
|
op: 'modify',
|
|
@@ -94,7 +98,7 @@ const SET_SOURCE_SPEC = {
|
|
|
94
98
|
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
95
99
|
activation: chainActivation(act),
|
|
96
100
|
card: {
|
|
97
|
-
object: a.name,
|
|
101
|
+
object: writtenObjectName(a.type, a.name), // LT-11: DDLX / DCLS / BDEF name their kind
|
|
98
102
|
what: 'source',
|
|
99
103
|
transport: pre.transport,
|
|
100
104
|
oldSource,
|
|
@@ -163,10 +167,26 @@ const UPDATE_METHOD_SPEC = {
|
|
|
163
167
|
methodSource: a.method_source,
|
|
164
168
|
transport: pre.transport ?? ctx.currentTransport?.get() ?? '',
|
|
165
169
|
};
|
|
170
|
+
// LT-15 D6 — a stop (Esc / Ctrl+C) during the class read below opens no
|
|
171
|
+
// preview card: the hook is not called and the pipeline returns INTERRUPTED.
|
|
172
|
+
// No signal on the ctx: the hook is passed through as before.
|
|
173
|
+
const previewHook = ctx.previewHook;
|
|
174
|
+
let stoppedBeforeCard = false;
|
|
175
|
+
const hook = ctx.signal
|
|
176
|
+
? (input) => {
|
|
177
|
+
if (ctx.signal?.aborted) {
|
|
178
|
+
stoppedBeforeCard = true;
|
|
179
|
+
return Promise.resolve({ confirmed: false });
|
|
180
|
+
}
|
|
181
|
+
return previewHook(input);
|
|
182
|
+
}
|
|
183
|
+
: previewHook;
|
|
166
184
|
try {
|
|
167
|
-
await interceptUpdateMethod(minimal, call,
|
|
185
|
+
await interceptUpdateMethod(minimal, call, hook);
|
|
168
186
|
}
|
|
169
187
|
catch (err) {
|
|
188
|
+
if (stoppedBeforeCard)
|
|
189
|
+
return { interrupted: true };
|
|
170
190
|
if (err instanceof CancelledByUserError)
|
|
171
191
|
return { error: { error: 'cancelled_by_user', detail: err.message } };
|
|
172
192
|
return { error: { error: 'preview_failed', detail: String(err) } };
|
|
@@ -302,6 +322,42 @@ registerTool({
|
|
|
302
322
|
});
|
|
303
323
|
const isDevcCreate = (a) => String(a.type ?? '').toUpperCase().split('/')[0].trim() === 'DEVC';
|
|
304
324
|
const devcTransportCreated = (pre) => pre.extra['devc_transport_created'];
|
|
325
|
+
/** LT-1 — no request resolved for a create (explicit, project and session all empty, or blank). */
|
|
326
|
+
const noTransportChosen = (pre) => !String(pre.transport ?? '').trim();
|
|
327
|
+
/**
|
|
328
|
+
* LT-1 — the transportability check for a create with no request chosen. A
|
|
329
|
+
* refusal in the REPL session arms the adoption below (fix round 1, M-3); a
|
|
330
|
+
* sub-agent ctx (no currentTransport, agent_run.ts) never arms it.
|
|
331
|
+
*/
|
|
332
|
+
async function transportNeededGate(ctx, o) {
|
|
333
|
+
const need = await checkTransportNeeded(ctx.adt, o);
|
|
334
|
+
if (need.ok)
|
|
335
|
+
return undefined;
|
|
336
|
+
if (ctx.currentTransport)
|
|
337
|
+
noteTransportNeededAsked();
|
|
338
|
+
return need.refusal;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* LT-1 — a create that landed on a request the model named right after a
|
|
342
|
+
* transport_needed question (the person's pick of an open request) makes it
|
|
343
|
+
* the session transport, as sap_transport_create does for a new one.
|
|
344
|
+
* - Fix round 1, M-3: only after that question; any other explicit request
|
|
345
|
+
* is used for that create only, as before. The next landed create uses the
|
|
346
|
+
* question up, whichever request it went to.
|
|
347
|
+
* - Fix round 1, I-1: a ctx without currentTransport (a sub-agent) never
|
|
348
|
+
* writes the process-wide session transport and never uses the question up.
|
|
349
|
+
* - Never when the session already holds a request.
|
|
350
|
+
*/
|
|
351
|
+
function adoptExplicitTransport(transport, ctx) {
|
|
352
|
+
if (!ctx.currentTransport)
|
|
353
|
+
return;
|
|
354
|
+
if (!takeTransportNeededAsked())
|
|
355
|
+
return;
|
|
356
|
+
const t = String(transport ?? '').trim().toUpperCase();
|
|
357
|
+
if (!t || (ctx.currentTransport.get() ?? null) !== null)
|
|
358
|
+
return;
|
|
359
|
+
setCurrentTransport(t);
|
|
360
|
+
}
|
|
305
361
|
const CREATE_OBJECT_SPEC = {
|
|
306
362
|
tool: 'sap_create_object',
|
|
307
363
|
// Part 2c D6 (review I-4): a module or include needs its group — refused
|
|
@@ -323,9 +379,15 @@ const CREATE_OBJECT_SPEC = {
|
|
|
323
379
|
snapshot: { kind: 'none', reason: 'a new object has no prior source' },
|
|
324
380
|
// 1.1.31: does the chosen request fit the package? (see transport-fit-check.ts)
|
|
325
381
|
// A package create decides its own transport in the walGate (§10a), so only objects.
|
|
382
|
+
// LT-1: no request chosen at all → refused when SAP says the package is transportable.
|
|
326
383
|
preGate: async (a, ctx, pre) => {
|
|
327
384
|
if (isDevcCreate(a))
|
|
328
385
|
return { ok: true };
|
|
386
|
+
if (noTransportChosen(pre)) {
|
|
387
|
+
const refusal = await transportNeededGate(ctx, { type: a.type, name: a.name, pkg: a.package });
|
|
388
|
+
if (refusal)
|
|
389
|
+
return { error: refusal };
|
|
390
|
+
}
|
|
329
391
|
const fit = await checkTransportFit(ctx.adt, { type: a.type, name: a.name, pkg: a.package, transport: pre.transport });
|
|
330
392
|
return fit.ok ? { ok: true } : { error: fit.refusal };
|
|
331
393
|
},
|
|
@@ -397,6 +459,9 @@ const CREATE_OBJECT_SPEC = {
|
|
|
397
459
|
audit: { method: 'createObject', type: (a) => a.type, name: (a) => a.name },
|
|
398
460
|
verify: { syntax: false, activation: false },
|
|
399
461
|
walResult: (_a, r) => r,
|
|
462
|
+
// LT-1: the request the person picked becomes the session transport (only when none is set).
|
|
463
|
+
after: (a, _r, _pre, ctx) => { if (!isDevcCreate(a))
|
|
464
|
+
adoptExplicitTransport(a.transport, ctx); },
|
|
400
465
|
envelope: (a, r, pre) => {
|
|
401
466
|
if (isDevcCreate(a)) {
|
|
402
467
|
const pkgName = r.objectName || String(a.name).toUpperCase();
|
|
@@ -666,6 +731,14 @@ function ddicCreateSpec(tool, type, auditType, create) {
|
|
|
666
731
|
},
|
|
667
732
|
transport: { mode: 'three-tier' },
|
|
668
733
|
snapshot: { kind: 'none', reason: 'a new object has no prior source' },
|
|
734
|
+
// LT-1: no request chosen at all → refused when SAP says the package is
|
|
735
|
+
// transportable. No fit check for a chosen request (as before).
|
|
736
|
+
preGate: async (a, ctx, pre) => {
|
|
737
|
+
if (!noTransportChosen(pre))
|
|
738
|
+
return { ok: true };
|
|
739
|
+
const refusal = await transportNeededGate(ctx, { type, name: a.name, pkg: a.package });
|
|
740
|
+
return refusal ? { error: refusal } : { ok: true };
|
|
741
|
+
},
|
|
669
742
|
walArgs: (a) => ({ name: a.name, type, package: a.package }),
|
|
670
743
|
call: async (a, ctx, pre) => {
|
|
671
744
|
const r = await create(a, ctx, pre.transport);
|
|
@@ -694,6 +767,8 @@ function ddicCreateSpec(tool, type, auditType, create) {
|
|
|
694
767
|
audit: { method: 'createObject', type: () => auditType, name: (a) => a.name },
|
|
695
768
|
verify: { syntax: false, activation: false },
|
|
696
769
|
walResult: (_a, r) => r,
|
|
770
|
+
// LT-1: the request the person picked becomes the session transport (only when none is set).
|
|
771
|
+
after: (a, _r, _pre, ctx) => adoptExplicitTransport(a.transport, ctx),
|
|
697
772
|
envelope: (_a, r) => ({
|
|
698
773
|
created: true,
|
|
699
774
|
active: true,
|
package/dist/tools/snapshot.js
CHANGED
|
@@ -62,10 +62,15 @@ registerTool({
|
|
|
62
62
|
return { content: JSON.stringify({ snapshot: entry, size: source.length }) };
|
|
63
63
|
},
|
|
64
64
|
});
|
|
65
|
-
export async function autoSnapshot(objectName, objectType, adt) {
|
|
65
|
+
export async function autoSnapshot(objectName, objectType, adt, preRead) {
|
|
66
66
|
let src;
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
if (preRead === undefined)
|
|
69
|
+
src = await adt.getSource(objectType, objectName);
|
|
70
|
+
else if ('source' in preRead)
|
|
71
|
+
src = preRead.source;
|
|
72
|
+
else
|
|
73
|
+
throw preRead.error;
|
|
69
74
|
}
|
|
70
75
|
catch (err) {
|
|
71
76
|
if (isNotFound(err))
|
|
@@ -34,6 +34,8 @@ import { collectTurnAssistantText } from '../../agent/turn-assistant-text.js';
|
|
|
34
34
|
export const MAX_AGENT_DEPTH = 3;
|
|
35
35
|
export const DEFAULT_MAX_TOKENS = 100_000;
|
|
36
36
|
export const HARD_MAX_TOKENS = 200_000;
|
|
37
|
+
/** LT-15 F3 — the result of a child turn the person stopped (Esc / Ctrl+C). */
|
|
38
|
+
export const SUBAGENT_INTERRUPTED_LINE = 'interrupted: the subagent was stopped before it finished. What it did before the stop is unknown: check before relying on it.';
|
|
37
39
|
// Same NAME_PATTERN shape playbook_get uses — alphanumerics + `:_-`, ≤64.
|
|
38
40
|
const SKILL_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9:_-]{0,63}$/;
|
|
39
41
|
export async function agentRunHandler(args, ctx) {
|
|
@@ -124,6 +126,9 @@ export async function agentRunHandler(args, ctx) {
|
|
|
124
126
|
skillSource: ctx.skillSource,
|
|
125
127
|
// Task 18 fix M4 — an agent_run child never starts reader subagents.
|
|
126
128
|
subagent: true,
|
|
129
|
+
// LT-15 F3 — the parent's stop (Esc / Ctrl+C) reaches the child: its tool
|
|
130
|
+
// dispatch and the D6 card guards see it. No parent signal: no key.
|
|
131
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
127
132
|
// previewHook + pendingDispatch + currentTransport + todoEmitter
|
|
128
133
|
// intentionally omitted.
|
|
129
134
|
};
|
|
@@ -166,9 +171,18 @@ export async function agentRunHandler(args, ctx) {
|
|
|
166
171
|
chunkEmitter: nullSinkEmitter,
|
|
167
172
|
maxTokensOverride,
|
|
168
173
|
toolFilter,
|
|
174
|
+
// LT-15 F3 — the child turn stops at its own next safe point.
|
|
175
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
169
176
|
});
|
|
170
177
|
}
|
|
171
178
|
catch (err) {
|
|
179
|
+
// LT-15 F3 — stopped by the person: say so plainly, with what the child
|
|
180
|
+
// said before the stop. Every child tool_use is already paired (the loop
|
|
181
|
+
// records results before its next round, where the abort ends the turn).
|
|
182
|
+
if (ctx.signal?.aborted) {
|
|
183
|
+
const partial = collectTurnAssistantText(childSession.messages, 0).trim();
|
|
184
|
+
return { content: partial ? `${SUBAGENT_INTERRUPTED_LINE}\n\nWhat it said before the stop:\n${partial}` : SUBAGENT_INTERRUPTED_LINE, is_error: true };
|
|
185
|
+
}
|
|
172
186
|
const msg = err instanceof Error ? err.message : String(err);
|
|
173
187
|
return { content: `error: subagent failed — ${msg}`, is_error: true };
|
|
174
188
|
}
|
|
@@ -43,6 +43,11 @@ const FIORI_FAMILY = [
|
|
|
43
43
|
const TRANSPORT_FAMILY = [
|
|
44
44
|
'sap_transport_create', 'sap_transport_for_object', 'sap_transport_list', 'sap_transport_read', 'sap_transport_release',
|
|
45
45
|
];
|
|
46
|
+
// LT-9 (1A fix wave, 2026-10-24): the build skills create the session transport
|
|
47
|
+
// in place. Live: /abap-rap's search for "transport" came back with loaded
|
|
48
|
+
// tools only (the ATC crowd-out again), so it offered SE09 and handed off to
|
|
49
|
+
// /abap-transport. Release and read stay with /abap-transport.
|
|
50
|
+
const SESSION_TRANSPORT = ['sap_transport_create', 'sap_transport_list', 'sap_transport_for_object'];
|
|
46
51
|
/**
|
|
47
52
|
* Per-skill starter tools: the non-core tools a skill's body names as its
|
|
48
53
|
* working set (counted from `.claude/skills/<skill>/SKILL.md`). A tool left
|
|
@@ -51,15 +56,18 @@ const TRANSPORT_FAMILY = [
|
|
|
51
56
|
*/
|
|
52
57
|
export const SKILL_STARTERS = Object.freeze({
|
|
53
58
|
'abap-fiori-build': FIORI_FAMILY,
|
|
54
|
-
|
|
59
|
+
// ATC fix (2026-10-23): the body runs sap_atc_run before release; the API's search for "atc" is crowded out.
|
|
60
|
+
'abap-transport': [...TRANSPORT_FAMILY, 'sap_atc_run'],
|
|
55
61
|
'abap-transport-analysis': [...TRANSPORT_FAMILY, 'sap_atc_run', 'sap_usage_references'],
|
|
56
62
|
'abap-cap': ['fiori_scaffold_fe', 'fiori_render_smoke', 'fiori_draft_smoke'],
|
|
57
63
|
// Both flag-gated (LOCAL_BUILD_TOOLS / STANDALONE_FILE_TOOLS): skipped when off.
|
|
58
64
|
'abap-app': ['app_lane_resolve', 'read_document'],
|
|
59
65
|
'abap-atc-fix': ['sap_atc_run'],
|
|
66
|
+
// Task 18 (F-O): blocker answers given in chat are recorded with this tool, never a file edit.
|
|
67
|
+
'abap-spec-gap': ['spec_answers_record'],
|
|
60
68
|
'abap-cca': ['team_estate', 'sap_atc_run', 'sap_usage_references', 'sap_api_state'],
|
|
61
69
|
'abap-clean-core': ['sap_api_state', 'sap_atc_run', 'sap_usage_references', 'team_findings', 'team_lane_move'],
|
|
62
|
-
'abap-data-model': ['sap_create_domain', 'sap_create_data_element'],
|
|
70
|
+
'abap-data-model': ['sap_create_domain', 'sap_create_data_element', ...SESSION_TRANSPORT],
|
|
63
71
|
'abap-document': ['sap_usage_references'],
|
|
64
72
|
'abap-handover': ['sap_usage_references'],
|
|
65
73
|
'abap-impact': ['sap_usage_references', 'sap_atc_run'],
|
|
@@ -70,22 +78,24 @@ export const SKILL_STARTERS = Object.freeze({
|
|
|
70
78
|
'abap-incident': ['sap_short_dump', 'sap_snapshot_take', 'sap_atc_run', 'sap_run_unit_test', 'sap_api_state', 'sap_diff', 'sap_run'],
|
|
71
79
|
'abap-eml': ['sap_run_unit_test', 'sap_run'],
|
|
72
80
|
'abap-test': ['sap_run_unit_test', 'sap_set_class_include', 'sap_run'],
|
|
73
|
-
'abap-extend-model': ['extend_model_insert', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_set_class_include'],
|
|
81
|
+
'abap-extend-model': ['extend_model_insert', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_set_class_include', ...SESSION_TRANSPORT],
|
|
74
82
|
// RAP generator (2026-10-14): SAP's generator writes the stack; the finish check proves F1–F5.
|
|
75
83
|
// Final review M5 — standalone /abap-rap calls rap_skeleton_resolve before it builds.
|
|
76
|
-
'abap-rap': ['sap_set_class_include', 'sap_service_binding_publish', 'sap_class_includes', 'rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_run'],
|
|
84
|
+
'abap-rap': ['sap_set_class_include', 'sap_service_binding_publish', 'sap_class_includes', 'rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_run', ...SESSION_TRANSPORT],
|
|
77
85
|
// Plan steps load tools by the PLAN skill (session.tool_set_skill / params.skill), not the
|
|
78
86
|
// delegate (which only rides as the X-CSPeach-Delegate-Skill header), so the RAP
|
|
79
87
|
// skeleton + finish phases need the generator and the finish check here too.
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
// ATC fix (2026-10-23): abap-plan writes "ATC clean" into exit gates; the API's tool search for
|
|
89
|
+
// "atc" is crowded out by loaded tools ("batch", "dispatch"), so the plan step loads sap_atc_run.
|
|
90
|
+
'abap-plan': ['rap_skeleton_resolve', 'sap_generate_objects', 'rap_finish_check', 'sap_atc_run', ...SESSION_TRANSPORT],
|
|
91
|
+
'abap-modernize': ['sap_api_state', 'sap_snapshot_take', 'sap_service_binding_publish', 'sap_atc_run', ...SESSION_TRANSPORT],
|
|
82
92
|
'abap-segw': ['sap_snapshot_take', 'sap_odata_test'],
|
|
83
93
|
'abap-upgrade-fix': ['sap_snapshot_take', 'glob'],
|
|
84
94
|
'abap-upgrade-scan': ['sap_atc_run'],
|
|
85
95
|
'abap-upgrade-verify': ['sap_atc_run'],
|
|
86
96
|
// Selection texts (2026-10-04): a new PARAMETERS / SELECT-OPTIONS gets its text in the same change.
|
|
87
|
-
'abap-refactor': ['sap_get_text_elements', 'sap_set_text_elements'],
|
|
88
|
-
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements', 'sap_run'],
|
|
97
|
+
'abap-refactor': ['sap_get_text_elements', 'sap_set_text_elements', ...SESSION_TRANSPORT],
|
|
98
|
+
'abap-generate': ['sap_get_text_elements', 'sap_set_text_elements', 'sap_run', ...SESSION_TRANSPORT],
|
|
89
99
|
// Chat lane (D5): the hand-off tool is loaded on chat turns; flag-gated, so a skills-lane session never lists it.
|
|
90
100
|
// `_chat` is the one SKILL_STARTERS key that is not a catalog skill (pinned in tool-loading.test.ts).
|
|
91
101
|
'_chat': ['hand_off'],
|
|
@@ -111,6 +111,76 @@ export async function checkTransportFit(adt, o) {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
+
/** The person's LOCAL calendar date as YYYY-MM-DD (fix round 1, M-1: never UTC). */
|
|
115
|
+
export function localIsoDate(date = new Date()) {
|
|
116
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
117
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())}`;
|
|
118
|
+
}
|
|
119
|
+
/** The suggested session-transport description (owner ruling 2026-10-24), editable by the person. */
|
|
120
|
+
export function cspeachTransportDefault(date = new Date()) {
|
|
121
|
+
return `CSPeach ${localIsoDate(date)}: <work summary>`;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Fix round 1, M-3 ruling: only the person's pick after a transport_needed
|
|
125
|
+
* question becomes the session transport. A refusal in the REPL session arms
|
|
126
|
+
* this; the next landed create uses it up. Not persisted (a new CLI process
|
|
127
|
+
* starts unarmed), like the created-this-session ledger in transport.ts.
|
|
128
|
+
*/
|
|
129
|
+
let transportNeededAsked = false;
|
|
130
|
+
export function noteTransportNeededAsked() { transportNeededAsked = true; }
|
|
131
|
+
/** True once, then false: the next landed create consumes the question. */
|
|
132
|
+
export function takeTransportNeededAsked() {
|
|
133
|
+
const was = transportNeededAsked;
|
|
134
|
+
transportNeededAsked = false;
|
|
135
|
+
return was;
|
|
136
|
+
}
|
|
137
|
+
/** Test hook — module-scope state would otherwise leak across Vitest tests. */
|
|
138
|
+
export function clearTransportNeededAsked() { transportNeededAsked = false; }
|
|
139
|
+
export async function checkTransportNeeded(adt, o) {
|
|
140
|
+
const pkg = String(o.pkg ?? '').trim().toUpperCase();
|
|
141
|
+
if (!pkg || pkg.startsWith('$'))
|
|
142
|
+
return { ok: true };
|
|
143
|
+
if (typeof adt.transportCheck !== 'function')
|
|
144
|
+
return { ok: true };
|
|
145
|
+
const object = String(o.type ?? '').toUpperCase().split('/')[0].trim();
|
|
146
|
+
const objectName = String(o.name ?? '').trim().toUpperCase();
|
|
147
|
+
if (NO_FIT_CHECK_TYPES.has(object))
|
|
148
|
+
return { ok: true };
|
|
149
|
+
let uri;
|
|
150
|
+
try {
|
|
151
|
+
uri = toObjectUrl(object, objectName).toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
debug(`no object URI for ${object} ${objectName}, no transport-needed check`);
|
|
155
|
+
return { ok: true };
|
|
156
|
+
}
|
|
157
|
+
let check;
|
|
158
|
+
try {
|
|
159
|
+
check = await adt.transportCheck({ pgmid: 'R3TR', object, objectName, devclass: pkg, operation: 'I', uri });
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
debug(`transport-needed check failed, create goes ahead: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`);
|
|
163
|
+
return { ok: true };
|
|
164
|
+
}
|
|
165
|
+
if (!check.ok) {
|
|
166
|
+
debug(`transport-needed check gave no answer, create goes ahead: ${(check.error ?? '').slice(0, 200)}`);
|
|
167
|
+
return { ok: true };
|
|
168
|
+
}
|
|
169
|
+
if (!check.requestRequired || !check.transportLayer)
|
|
170
|
+
return { ok: true };
|
|
171
|
+
const shown = (check.requests ?? []).slice(0, MAX_FITTING_SHOWN).map((r) => ({ number: r.number, description: r.text ?? '' }));
|
|
172
|
+
const list = shown.map((r) => (r.description ? `${r.number} ('${r.description}')` : r.number)).join(', ');
|
|
173
|
+
const message = `Package ${pkg} is transportable, so creating ${objectName} needs a transport request, and no session transport is set yet. `
|
|
174
|
+
+ 'Nothing was created. Ask the person once with ask_question: '
|
|
175
|
+
+ `create a new session transport (recommended; sap_transport_create, proposing the description "${cspeachTransportDefault()}", which they can edit)`
|
|
176
|
+
+ (shown.length ? `, or use one of the open requests SAP lists for the package: ${list}` : '')
|
|
177
|
+
+ '. Their answer becomes the session transport for the rest of this work. '
|
|
178
|
+
+ 'Then call this tool again with the same approval_id (and the chosen request as transport, if it is an existing one).';
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
refusal: { error: 'transport_needed', message, ...(shown.length ? { fitting_requests: shown } : {}) },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
114
184
|
/**
|
|
115
185
|
* 1.1.31 — SAP answered a create with an existing request (the user's newest
|
|
116
186
|
* open one, which may belong to other work). It is never used silently, and
|
package/dist/tools/transport.js
CHANGED
|
@@ -24,7 +24,8 @@ import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
|
24
24
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
25
25
|
import { setCurrentTransport, noteConfirmedTransport } from '../repl/current-transport.js';
|
|
26
26
|
import { enrichTransportList } from './transport-fit.js';
|
|
27
|
-
import { reusedRequestRefusal } from './transport-fit-check.js';
|
|
27
|
+
import { reusedRequestRefusal, localIsoDate } from './transport-fit-check.js';
|
|
28
|
+
import { STOPPED_BEFORE_START_RESULT } from '../agent/repair-partial.js';
|
|
28
29
|
// ── Session ledger: transports CREATED by sap_transport_create this session ──
|
|
29
30
|
//
|
|
30
31
|
// Orphan-hint support (third junk transport this month, 2026-06-12 — D15/D20
|
|
@@ -63,10 +64,12 @@ registerTool({
|
|
|
63
64
|
+ 'Flow: resolve → object locked in an open transport? this tool then does NOT '
|
|
64
65
|
+ 'create anything and returns the owning transport — use it → only create when '
|
|
65
66
|
+ 'the object is free. '
|
|
66
|
-
+ 'Ask the user for the transport description using `ask_question` — DO NOT '
|
|
67
|
-
+ '
|
|
68
|
-
+ '
|
|
69
|
-
+ 'the
|
|
67
|
+
+ 'Ask the user for the transport description once, using `ask_question` — DO NOT '
|
|
68
|
+
+ 'make up a description yourself. When asking, PROPOSE the session format '
|
|
69
|
+
+ '"CSPeach <YYYY-MM-DD>: <work summary>" as the recommended default (today\'s date, '
|
|
70
|
+
+ 'the work in a few words); the user may edit it. '
|
|
71
|
+
+ 'Never tell the person they will be asked unless you ask in this turn. '
|
|
72
|
+
+ 'Use the user\'s exact answer as the `description` '
|
|
70
73
|
+ 'argument AND in your `request_approval` reason (identical strings prevent the '
|
|
71
74
|
+ 'approval mismatch failure mode observed live 2026-05-07). '
|
|
72
75
|
+ 'Requires approval_id. Returns the transport number to use in subsequent writes.',
|
|
@@ -300,12 +303,14 @@ registerTool({
|
|
|
300
303
|
// Rule 9 description format (D20: "Magic fix" accepted verbatim) —
|
|
301
304
|
// non-blocking note when the description doesn't follow the session
|
|
302
305
|
// format. The transport IS created; the user's wording always wins.
|
|
303
|
-
// Any leading label (a customer's own prefix) is fine
|
|
306
|
+
// Any leading label (a customer's own prefix) is fine. LT-9 owner ruling
|
|
307
|
+
// (2026-10-24): the suggested default is "CSPeach <date>: <work summary>".
|
|
304
308
|
const rule9Format = /^(?:\S.* )?\d{4}-\d{2}-\d{2}: .+/;
|
|
305
|
-
|
|
309
|
+
// Fix round 1, M-1: the person's local date, as in the transport_needed ask.
|
|
310
|
+
const today = localIsoDate();
|
|
306
311
|
const rule9Suggestion = rule9Format.test(args.description)
|
|
307
312
|
? undefined
|
|
308
|
-
: `Description does not follow the Rule 9 session format. Suggested: "${today}: <work summary>". `
|
|
313
|
+
: `Description does not follow the Rule 9 session format. Suggested: "CSPeach ${today}: <work summary>". `
|
|
309
314
|
+ 'Non-blocking — keep the user\'s wording if they chose it deliberately.';
|
|
310
315
|
return {
|
|
311
316
|
content: JSON.stringify({
|
|
@@ -637,6 +642,10 @@ registerTool({
|
|
|
637
642
|
};
|
|
638
643
|
}
|
|
639
644
|
}
|
|
645
|
+
// ── LT-15 D6: stopped during the reads above → no card, no release ──────
|
|
646
|
+
// Before the approval is spent, the safety card and any release call.
|
|
647
|
+
if (ctx.signal?.aborted)
|
|
648
|
+
return { content: STOPPED_BEFORE_START_RESULT, is_error: true };
|
|
640
649
|
// ── Gate 1: Approval (op release, type CTS) ─────────────────────────────
|
|
641
650
|
const verify = await verifyAndSpendApprovalId(args.approval_id, args.transport, 'release');
|
|
642
651
|
if (!verify.ok) {
|