@cspeach/cli 1.1.52 → 1.1.54
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/conversation-prompt.js +1 -0
- package/dist/agent/loop.js +150 -17
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/repair-partial.js +9 -0
- package/dist/agent/tool-dispatch.js +17 -2
- package/dist/approvals/op-labels.js +6 -3
- package/dist/approvals/render.js +3 -2
- package/dist/auth/auth-file.js +3 -0
- package/dist/auth/key-resolver.js +8 -3
- package/dist/auth/me.js +6 -2
- package/dist/cli.js +22 -0
- package/dist/commands/login.js +22 -4
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +5 -0
- package/dist/commands/plan-resume.js +2 -2
- package/dist/commands/plan-startup.js +3 -3
- package/dist/commands/sap-link.js +14 -1
- package/dist/commands/team-build-help.js +19 -0
- package/dist/commands/team-build.js +97 -0
- package/dist/commands/team-plan.js +2 -2
- package/dist/commands/team-spec.js +107 -0
- package/dist/commands/team.js +74 -4
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/journey/chain.js +8 -0
- package/dist/journey/clean-core.js +2 -2
- package/dist/journey/rebuild.js +8 -0
- package/dist/journey/run.js +9 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/licence/paths.js +5 -0
- package/dist/licence/seat-kind.js +110 -0
- package/dist/licence/seat-state.js +4 -0
- package/dist/licence/terminal-seat.js +43 -0
- package/dist/licence/turn-gate.js +4 -0
- package/dist/one-shot.js +33 -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 +133 -18
- 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 +83 -20
- package/dist/projects/saved-lines.js +19 -17
- package/dist/projects/spec-share.js +239 -0
- package/dist/register/picture.js +2 -2
- package/dist/register/store.js +19 -0
- package/dist/renderer/change-card.js +14 -0
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/tool-labels.js +2 -0
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/builtin-commands.js +4 -3
- package/dist/repl/chat-lane.js +10 -1
- package/dist/repl/file-picker.js +52 -5
- package/dist/repl/mode-cycle.js +12 -0
- package/dist/repl/open-questions-gate.js +159 -0
- package/dist/repl/ready-phrase.js +108 -0
- package/dist/repl/startup-account.js +64 -1
- package/dist/repl/team-build-screen.js +116 -0
- package/dist/repl/team-build-start.js +178 -0
- package/dist/repl.js +216 -11
- package/dist/sap-link/policy.js +4 -0
- package/dist/sap-link/registry.js +9 -3
- package/dist/sap-link/server.js +4 -1
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/user-prompt.js +2 -0
- package/dist/tools/_team-work-path.js +1 -1
- package/dist/tools/approval.js +12 -1
- package/dist/tools/cap/sap-link-tool.js +2 -0
- package/dist/tools/project/spec_answers_record.js +243 -0
- package/dist/tools/project/team_findings.js +2 -2
- package/dist/tools/seat-gate.js +41 -0
- package/dist/tools/tool-loading.js +2 -0
- package/dist/ui/approval-modal.js +2 -1
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/files-card.js +19 -1
- package/package.json +2 -2
|
@@ -26,6 +26,7 @@ How you talk
|
|
|
26
26
|
- Plain words, short sentences. Use headings, tables or lists only when they make the answer easier to read.
|
|
27
27
|
- Have a view. If something is a bad idea, say so and say why. If you're not sure, say what you'd check.
|
|
28
28
|
- When there is a next step, say what you would do in your own words. Do not end a reply with a word, phrase or command for the person to type to go on. If they ask which command does something, name it. If the next step is a workflow and they have asked for it, call hand_off now: the workflow shows its plan and asks before it writes. If it is a real choice, ask the question.
|
|
29
|
+
- When there is a clear next step, end with it as one question card (next step recommended + 'Not now'). A step that only saves local files or plans runs on yes without asking again. SAP writes still show their approval card. Use ask_question for the card.
|
|
29
30
|
- A playbook's report header (## [abap-x] — object, TL;DR, Headline, Top 3, Verdict, Next) goes only in a saved document and in the workflow's report after the person typed an /abap-… command. Not after your own hand_off, and not in other turns: there, answer as you would in conversation.
|
|
30
31
|
- After a write, say what changed and what is next. Do not explain approvals, risk ratings or activation mechanics unless asked. If something failed or stayed inactive, say so.
|
|
31
32
|
- No flattery. Apologise only when you got something wrong.
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { basename } from 'node:path';
|
|
2
|
+
import { basename, dirname } from 'node:path';
|
|
3
3
|
import { dispatchTool } from './tool-dispatch.js';
|
|
4
4
|
import { listTools, getTool, isStandalone, toolsForContext, toAnthropicTools } from '../tools/index.js';
|
|
5
|
+
import { toolsForSeat } from '../tools/seat-gate.js';
|
|
6
|
+
import { getSeatKind } from '../licence/seat-state.js';
|
|
5
7
|
import { saveSession } from '../session/store.js';
|
|
6
8
|
import { tagSessionOnSave } from '../session/project-match.js';
|
|
7
9
|
import { recordCompletedToolCall } from '../session/pending.js';
|
|
@@ -9,7 +11,7 @@ import { loadConfig } from '../config/loader.js';
|
|
|
9
11
|
import { resolveModelRole, resolveEffort, modelAcceptsEffort, resolveAutoCompactThreshold, callModelFor } from '../models/resolve.js';
|
|
10
12
|
import { getCustomerModelConfig, managedProxyServesNewShapes, servedToolLoading } from '../models/server-config.js';
|
|
11
13
|
import { familyOf, familyAcceptsEffort } from '../models/model-identity.js';
|
|
12
|
-
import { buildToolList, buildSkillToolAddition, placeSystemMessagesInPlace, resolveToolLoading, } from '../tools/tool-loading.js';
|
|
14
|
+
import { buildToolList, buildSkillToolAddition, buildToolAdditionMessage, deferredNames, surfacedToolNames, placeSystemMessagesInPlace, resolveToolLoading, } from '../tools/tool-loading.js';
|
|
13
15
|
import { settleSessionToolLoading } from './tool-loading-pin.js';
|
|
14
16
|
import { resolveSkillAlias } from '../skill-catalog.js';
|
|
15
17
|
import { retryWithBackoff } from './retry.js';
|
|
@@ -46,6 +48,7 @@ import { enrichUserMessage } from '../router/intent-extractor.js';
|
|
|
46
48
|
import { resetRule8State, shouldGateRule8 } from '../repl/rule8-detector.js';
|
|
47
49
|
import { consumeTodoSetThisTurn } from '../ui/todo-emitter.js';
|
|
48
50
|
import { runSaveCommand, runPromoteCommand, detectArtifactSkill, offerAnswerBlockers } from '../projects/index.js';
|
|
51
|
+
import { blockerAnswersNote, SPEC_ANSWERS_RECORD_TOOL } from '../projects/blocker-answers-note.js';
|
|
49
52
|
import { planLaneBlockForTurn } from '../projects/plan-lane.js';
|
|
50
53
|
import { DOCUMENT_SKILLS, WORD_AFTER_ARTIFACT_SAVE } from '../projects/document-skills.js';
|
|
51
54
|
import { metaFromMarkdown } from '../docx/doc-meta.js';
|
|
@@ -70,7 +73,7 @@ import { renderStandaloneContextBlock } from '../sap/standalone-profile.js';
|
|
|
70
73
|
import { renderStandardsBlock } from '../standards/standards-file.js';
|
|
71
74
|
import { composeSessionContextBlock } from './session-context.js';
|
|
72
75
|
import { objectKeyFromInput } from './retry-key.js';
|
|
73
|
-
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly } from './repair-partial.js';
|
|
76
|
+
import { repairPartialBlocks, stripPreFallbackBlocks, healSessionMessagesInPlace, healOrphanToolUses, isPartialJsonApiError, stripUnsignedThinking, isThinkingOnly, dropTrailingThinkingOnly, closedOffStepsLine } from './repair-partial.js';
|
|
74
77
|
import { drainSteeringEntries, enqueueSteering } from './steering-queue.js';
|
|
75
78
|
import { steeringReadLine } from '../renderer/steering-echo.js';
|
|
76
79
|
import { TurnStreamWriter } from './turn-stream.js';
|
|
@@ -432,6 +435,19 @@ function localToday() {
|
|
|
432
435
|
* package) loads only here, inside the catch: a broken docx install costs
|
|
433
436
|
* one line after the save and never the save itself.
|
|
434
437
|
*/
|
|
438
|
+
/** A spec gap saved into a team build's shared specs/ folder (merge of main into feat/team-build). */
|
|
439
|
+
async function isSharedSpecSave(cwd, saveSkill, savedPath) {
|
|
440
|
+
if (saveSkill !== 'abap-spec-gap')
|
|
441
|
+
return false;
|
|
442
|
+
try {
|
|
443
|
+
const { sharedSpecTargetFor, isSharedSpecPath } = await import('../projects/spec-share.js');
|
|
444
|
+
const shared = sharedSpecTargetFor(cwd, 'spec-gap');
|
|
445
|
+
return !!shared && isSharedSpecPath(shared.teamDir, savedPath);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
435
451
|
async function offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm) {
|
|
436
452
|
try {
|
|
437
453
|
const { offerWordCopyAfterArtifactSave } = await import('../projects/document-save.js');
|
|
@@ -448,9 +464,51 @@ async function offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm) {
|
|
|
448
464
|
p.emit(formatInfo(`Word copy not written: ${e.message}`).trimEnd());
|
|
449
465
|
}
|
|
450
466
|
}
|
|
467
|
+
/**
|
|
468
|
+
* Change options (2026-10-23) — an answer carrying the change-options marker
|
|
469
|
+
* pair gets its marked part saved (every complete pair, in order: a level
|
|
470
|
+
* picked in session adds the decided document), whatever the skill and even when the turn
|
|
471
|
+
* ends on the level question. Never on a sub-agent turn or inside a guided
|
|
472
|
+
* flow. Returns true when the marker check handled the turn (the question was
|
|
473
|
+
* asked or the document saved), so maybeOfferSave runs nothing else. An answer
|
|
474
|
+
* with no complete marker pair returns false and takes today's path unchanged.
|
|
475
|
+
*/
|
|
476
|
+
async function maybeOfferChangeOptions(p, confirm) {
|
|
477
|
+
if (p.subagent || p.guided || p.assistantText.includes('<!-- guided-run:'))
|
|
478
|
+
return false;
|
|
479
|
+
if (!p.assistantText.includes('csforge:change-options'))
|
|
480
|
+
return false;
|
|
481
|
+
const { extractChangeOptions } = await import('../projects/change-options.js');
|
|
482
|
+
// Every complete pair, in order: a level picked in session shows the
|
|
483
|
+
// document again with the decision, and that one is saved too (last).
|
|
484
|
+
const docs = extractChangeOptions(p.assistantText).filter((d) => d.markdown.trim().length > 0);
|
|
485
|
+
if (docs.length === 0)
|
|
486
|
+
return false;
|
|
487
|
+
// Set before the save question, so a declined save still routes the level reply.
|
|
488
|
+
if (p.session && !p.subagent)
|
|
489
|
+
p.session.lastChangeOptions = { object: docs[docs.length - 1].object, skill: p.skill };
|
|
490
|
+
try {
|
|
491
|
+
const { offerChangeOptionsSave } = await import('../projects/document-save.js');
|
|
492
|
+
const { loadConfig, resolveWordCopy } = await import('../config/loader.js');
|
|
493
|
+
await offerChangeOptionsSave({
|
|
494
|
+
docs: docs.map((d) => ({ object: d.object, markdown: d.markdown })), cwd: process.cwd(), today: localToday(),
|
|
495
|
+
sapSystem: p.sapSystem ?? null,
|
|
496
|
+
...(p.sessionId ? { sessionId: p.sessionId } : {}),
|
|
497
|
+
...(p.operator ? { operator: p.operator } : {}),
|
|
498
|
+
headless: isHeadless(), wordCopy: resolveWordCopy(await loadConfig()), confirm, emit: p.emit,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
p.emit(formatInfo(`Not saved. ${e.message}`).trimEnd());
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
451
506
|
export async function maybeOfferSave(p) {
|
|
452
507
|
if (p.assistantText.trim().length === 0)
|
|
453
508
|
return;
|
|
509
|
+
// Change options (2026-10-23): the marker check runs first, for every skill.
|
|
510
|
+
if (await maybeOfferChangeOptions(p, p.confirm ?? inkAwareConfirm))
|
|
511
|
+
return;
|
|
454
512
|
// Word export (2026-10-19): reviews and handovers are prose documents, not
|
|
455
513
|
// envelopes. They get a Markdown save under cspeach-out and the Word
|
|
456
514
|
// question (projects/document-save.ts). Nothing below this branch changes
|
|
@@ -547,7 +605,7 @@ export async function maybeOfferSave(p) {
|
|
|
547
605
|
tokensUsed: p.tokensUsed,
|
|
548
606
|
model: p.model,
|
|
549
607
|
author: getAuthorIdentity(),
|
|
550
|
-
cwd: process.cwd(),
|
|
608
|
+
cwd: p.cwd ?? process.cwd(),
|
|
551
609
|
prompt,
|
|
552
610
|
// Piece 2 / Q3 — interactive saves ask a Yes/No card; headless passes
|
|
553
611
|
// no confirm so the blanket-'y' prompt above still answers (B5 contract).
|
|
@@ -562,6 +620,7 @@ export async function maybeOfferSave(p) {
|
|
|
562
620
|
...(p.latestSkeleton && p.skill === saveSkill ? { latestSkeleton: p.latestSkeleton } : {}),
|
|
563
621
|
onNotSaved: (reason) => { notSaved = reason; },
|
|
564
622
|
...(p.onProjectId ? { onProjectId: p.onProjectId } : {}),
|
|
623
|
+
...(p.sapSystem ? { sapSystem: p.sapSystem } : {}),
|
|
565
624
|
// Never headless: a headless journey queues nothing.
|
|
566
625
|
...(p.journey && !isHeadless() && p.skill === saveSkill ? { journey: p.journey } : {}),
|
|
567
626
|
});
|
|
@@ -569,7 +628,9 @@ export async function maybeOfferSave(p) {
|
|
|
569
628
|
// is a document (spec gaps, design, estimate, plan, upgrade scan/report)
|
|
570
629
|
// gets the same Word step as a review: `word_copy` decides, headless
|
|
571
630
|
// never asks. The project file itself is untouched. Sub-agent turns: no.
|
|
572
|
-
|
|
631
|
+
// A team build shared spec already has its Word copy beside it (save-command.ts).
|
|
632
|
+
if (savedPath && !p.subagent && WORD_AFTER_ARTIFACT_SAVE[saveSkill]
|
|
633
|
+
&& !(await isSharedSpecSave(p.cwd ?? process.cwd(), saveSkill, savedPath))) {
|
|
573
634
|
await offerWordAfterArtifactSave(p, saveSkill, savedPath, confirm);
|
|
574
635
|
}
|
|
575
636
|
// After a spec-gap file is saved, let a developer answer the open BLOCKER
|
|
@@ -577,7 +638,31 @@ export async function maybeOfferSave(p) {
|
|
|
577
638
|
// business questions stay open for a consultant to answer in the viewer.
|
|
578
639
|
// Uses inkAwarePrompt directly (NOT the save hook's blanket-'y' headless
|
|
579
640
|
// prompt): in headless it returns '' → declined, so nothing is written.
|
|
641
|
+
// Task 16 (F-M): what the form recorded reaches the model as one note on
|
|
642
|
+
// its next call (session.blockerAnswersNote, sent by runTurn). Main turns
|
|
643
|
+
// with a session only; declined / not offered / not saved → no note.
|
|
580
644
|
if (savedPath && saveSkill === 'abap-spec-gap') {
|
|
645
|
+
// Team build (Task 9): a spec in the shared specs/ folder is never
|
|
646
|
+
// rewritten — each answer is a record of its own, and the spec is read
|
|
647
|
+
// with the records applied. Found by the folder (R10), never activeProject().
|
|
648
|
+
const { sharedSpecTargetFor, isSharedSpecPath, composeSpec, readAnswers, writeAnswerRecord, specAuthorFor } = await import('../projects/spec-share.js');
|
|
649
|
+
const { readProjectFile } = await import('../projects/status.js');
|
|
650
|
+
const shared = sharedSpecTargetFor(p.cwd ?? process.cwd(), 'spec-gap');
|
|
651
|
+
const records = shared && isSharedSpecPath(shared.teamDir, savedPath)
|
|
652
|
+
? {
|
|
653
|
+
// R11: only the answers recorded for this envelope.
|
|
654
|
+
readEnvelope: (path) => composeSpec(readProjectFile(path), readAnswers(dirname(path)).filter((a) => a.file === basename(path))),
|
|
655
|
+
recordAnswer: (a) => {
|
|
656
|
+
const specDir = dirname(savedPath);
|
|
657
|
+
writeAnswerRecord(specDir, {
|
|
658
|
+
projectId: shared.projectId, slug: basename(specDir), file: basename(savedPath),
|
|
659
|
+
gapId: a.gapId, answer: a.answer, by: specAuthorFor(shared.projectId, a.identity.name), at: a.at,
|
|
660
|
+
});
|
|
661
|
+
},
|
|
662
|
+
}
|
|
663
|
+
: {};
|
|
664
|
+
const noteSession = p.session && !p.subagent ? p.session : undefined;
|
|
665
|
+
let outcomes;
|
|
581
666
|
await offerAnswerBlockers({
|
|
582
667
|
path: savedPath,
|
|
583
668
|
identity: getAuthorIdentity(),
|
|
@@ -585,7 +670,18 @@ export async function maybeOfferSave(p) {
|
|
|
585
670
|
confirm: inkAwareConfirm,
|
|
586
671
|
choose: inkAwareChoose,
|
|
587
672
|
log: (...lines) => lines.forEach((l) => p.emit(l)),
|
|
673
|
+
...records,
|
|
674
|
+
...(noteSession ? { onOutcomes: (o) => { outcomes = o; } } : {}),
|
|
588
675
|
});
|
|
676
|
+
if (noteSession && outcomes) {
|
|
677
|
+
// The path is shown against the read tool's base (ctx.cwd), so the model opens it with file_read.
|
|
678
|
+
const note = blockerAnswersNote({
|
|
679
|
+
path: savedPath, cwd: p.readCwd ?? process.cwd(), outcomes,
|
|
680
|
+
...('readEnvelope' in records ? { readEnvelope: records.readEnvelope } : {}),
|
|
681
|
+
});
|
|
682
|
+
if (note)
|
|
683
|
+
noteSession.blockerAnswersNote = note;
|
|
684
|
+
}
|
|
589
685
|
}
|
|
590
686
|
// App lane routing (Task 7) — the harness-owned /abap-app chain. Built
|
|
591
687
|
// from the exact saved path, never from model text. Outside an app flow
|
|
@@ -1068,7 +1164,12 @@ ${imageExpansion.error}`));
|
|
|
1068
1164
|
if (mainTurn && !inlineTurn && historyHoldsInline(session.messages)) {
|
|
1069
1165
|
playbookLine = isChatSkill(params.skill) ? playbookLineSystemChat() : playbookLineSystem(params.skill);
|
|
1070
1166
|
}
|
|
1071
|
-
|
|
1167
|
+
// Task 16 (F-M) — what the blocker form recorded after the last turn: once,
|
|
1168
|
+
// on the next main turn (never a sub-agent or reader child), then dropped.
|
|
1169
|
+
const blockerNote = mainTurn && session.blockerAnswersNote
|
|
1170
|
+
? `<blocker_answers>\n${session.blockerAnswersNote}\n</blocker_answers>`
|
|
1171
|
+
: '';
|
|
1172
|
+
const sessionContextBlock = composeSessionContextBlock([sapSystemBlock, sapProfileBlock, standardsBlock, switchNote, blockerNote, playbookLine]);
|
|
1072
1173
|
// Deterministically extract structured facts from the user's prompt
|
|
1073
1174
|
// (package, transport, object names, environment hint, create intent)
|
|
1074
1175
|
// and prepend them as an XML `<session_context>` block. The model reads
|
|
@@ -1084,11 +1185,12 @@ ${imageExpansion.error}`));
|
|
|
1084
1185
|
// Final batch 1 — a caller-enriched message still gets the switch note,
|
|
1085
1186
|
// inside its own <session_context> block.
|
|
1086
1187
|
// Stage 1A — the <playbook> line rides the same way ('' ⇒ unchanged).
|
|
1087
|
-
const callerNotes = composeSessionContextBlock([switchNote, playbookLine]);
|
|
1188
|
+
const callerNotes = composeSessionContextBlock([switchNote, blockerNote, playbookLine]);
|
|
1088
1189
|
const enriched = userMessageForLLM.startsWith('<session_context>')
|
|
1089
1190
|
? (callerNotes ? userMessageForLLM.replace('<session_context>', `<session_context>\n ${callerNotes}`) : userMessageForLLM)
|
|
1090
1191
|
: enrichUserMessage(userMessageForLLM, sessionContextBlock);
|
|
1091
1192
|
const switchNoteUsed = switchNote !== '' && enriched.includes(switchNote);
|
|
1193
|
+
const blockerNoteUsed = blockerNote !== '' && enriched.includes(blockerNote);
|
|
1092
1194
|
// Phase 2 of Track B — opt-in project-context enrichment, gated by
|
|
1093
1195
|
// CSPEACH_PROJECT_CONTEXT=on. Default OFF: production behavior is
|
|
1094
1196
|
// identical to today. When on, the rendered <project_context> block
|
|
@@ -1116,6 +1218,7 @@ ${imageExpansion.error}`));
|
|
|
1116
1218
|
projectContextBlock = await maybeBuildProjectContext({
|
|
1117
1219
|
cwd: params.ctx.cwd,
|
|
1118
1220
|
sapConnection,
|
|
1221
|
+
emit: (line) => emit(chalk.dim(line)),
|
|
1119
1222
|
});
|
|
1120
1223
|
}
|
|
1121
1224
|
const contextualContent = projectContextBlock
|
|
@@ -1137,6 +1240,9 @@ ${imageExpansion.error}`));
|
|
|
1137
1240
|
lane: session.lane,
|
|
1138
1241
|
tool_set_skill: session.tool_set_skill,
|
|
1139
1242
|
turn_seq: session.turn_seq,
|
|
1243
|
+
// Task 16 fix round 1 (I-1, R17): the one-time notes this turn consumes.
|
|
1244
|
+
systemSwitchNote: session.systemSwitchNote,
|
|
1245
|
+
blockerAnswersNote: session.blockerAnswersNote,
|
|
1140
1246
|
};
|
|
1141
1247
|
let round0Steers = [];
|
|
1142
1248
|
// Images (if any) ride as blocks ahead of the text; with none this is the
|
|
@@ -1152,6 +1258,8 @@ ${imageExpansion.error}`));
|
|
|
1152
1258
|
// it is in a pushed user message.
|
|
1153
1259
|
if (switchNoteUsed)
|
|
1154
1260
|
delete session.systemSwitchNote;
|
|
1261
|
+
if (blockerNoteUsed)
|
|
1262
|
+
delete session.blockerAnswersNote;
|
|
1155
1263
|
if (imageExpansion.images.length > 0 && params.provider.name === 'local-openai-compat') {
|
|
1156
1264
|
// The local (OpenAI-compatible) provider keeps text blocks only.
|
|
1157
1265
|
emit(chalk.dim('(local model: the image is not sent — the model gets the text only)'));
|
|
@@ -1193,12 +1301,19 @@ ${imageExpansion.error}`));
|
|
|
1193
1301
|
const hiddenSapTools = connectedTurn
|
|
1194
1302
|
? pinHiddenTools(session, params.ctx.sapAlias, listTools(), params.ctx.systemProfile ?? null)
|
|
1195
1303
|
: [];
|
|
1196
|
-
|
|
1304
|
+
// Team build Task 6 — a functional seat is not offered the tools it may not
|
|
1305
|
+
// call (dispatch refuses them anyway). Developers: the same array.
|
|
1306
|
+
const contextTools = toolsForSeat(withoutTools(toolsForContext(listTools(), params.ctx), hiddenSapTools), getSeatKind());
|
|
1197
1307
|
// Task 18 fixes M4 / M5 — no read_agent for an agent_run child, and none
|
|
1198
1308
|
// when a reader would have no real read tool (standalone, file tools off).
|
|
1199
|
-
const
|
|
1309
|
+
const readerVisible = params.ctx.subagent || !hasReaderReadTools(new Set(contextTools.map((t) => t.name)))
|
|
1200
1310
|
? contextTools.filter((t) => t.name !== READ_AGENT)
|
|
1201
1311
|
: contextTools;
|
|
1312
|
+
// Task 18 fix round 1 (item 5) — an agent_run child never records spec
|
|
1313
|
+
// answers: it could race the blocker form on the same file.
|
|
1314
|
+
const visibleTools = params.ctx.subagent
|
|
1315
|
+
? readerVisible.filter((t) => t.name !== SPEC_ANSWERS_RECORD_TOOL)
|
|
1316
|
+
: readerVisible;
|
|
1202
1317
|
// Final review C1 (controller ruling) — deferred shapes only for a model on
|
|
1203
1318
|
// the probe-proven allow-list, decided by the model actually sent on the
|
|
1204
1319
|
// call (a plan-tier sonnet override, a haiku child or claude-opus-4-8 ⇒
|
|
@@ -1254,13 +1369,7 @@ ${imageExpansion.error}`));
|
|
|
1254
1369
|
// Stage 1A — skill_appended goes on the turn's first cost line (a call
|
|
1255
1370
|
// retried after a prompt-too-long recovery logs no line of its own).
|
|
1256
1371
|
let skillAppendedLogged = false;
|
|
1257
|
-
const
|
|
1258
|
-
const msg = buildSkillToolAddition({
|
|
1259
|
-
skill,
|
|
1260
|
-
toolLoading: session.tool_loading,
|
|
1261
|
-
toolList: turnTools,
|
|
1262
|
-
messages: session.messages,
|
|
1263
|
-
});
|
|
1372
|
+
const pushToolAddition = (msg) => {
|
|
1264
1373
|
if (msg)
|
|
1265
1374
|
session.messages.push(msg);
|
|
1266
1375
|
if (msg && inlineTurn) {
|
|
@@ -1270,10 +1379,24 @@ ${imageExpansion.error}`));
|
|
|
1270
1379
|
}
|
|
1271
1380
|
}
|
|
1272
1381
|
};
|
|
1382
|
+
const surfaceStarters = (skill) => {
|
|
1383
|
+
pushToolAddition(buildSkillToolAddition({
|
|
1384
|
+
skill,
|
|
1385
|
+
toolLoading: session.tool_loading,
|
|
1386
|
+
toolList: turnTools,
|
|
1387
|
+
messages: session.messages,
|
|
1388
|
+
}));
|
|
1389
|
+
};
|
|
1273
1390
|
// A turn on a different skill than the one the array was built for: the
|
|
1274
1391
|
// user message was just pushed, so the system message follows it.
|
|
1275
1392
|
if (params.skill !== session.tool_set_skill)
|
|
1276
1393
|
surfaceStarters(params.skill);
|
|
1394
|
+
// Task 18 fix round 1 (item 7) — the blocker note names spec_answers_record:
|
|
1395
|
+
// on the turn that carries it, surface the tool when this list defers it
|
|
1396
|
+
// (loaded already, static, or surfaced before: nothing is added).
|
|
1397
|
+
if (blockerNoteUsed && session.tool_loading !== 'static' && !surfacedToolNames(session.messages).has(SPEC_ANSWERS_RECORD_TOOL)) {
|
|
1398
|
+
pushToolAddition(buildToolAdditionMessage([SPEC_ANSWERS_RECORD_TOOL], deferredNames(turnTools)));
|
|
1399
|
+
}
|
|
1277
1400
|
/**
|
|
1278
1401
|
* key: `${tool_name}:${object}` — consecutive failures per (tool, target).
|
|
1279
1402
|
* For object-identifying tools (sap_set_source / sap_activate / etc.) the
|
|
@@ -1446,6 +1569,11 @@ ${imageExpansion.error}`));
|
|
|
1446
1569
|
session.lane = preTurnState.lane;
|
|
1447
1570
|
session.tool_set_skill = preTurnState.tool_set_skill;
|
|
1448
1571
|
session.turn_seq = preTurnState.turn_seq;
|
|
1572
|
+
// The re-run turn still gets the one-time notes (they were dropped on push).
|
|
1573
|
+
if (preTurnState.systemSwitchNote !== undefined)
|
|
1574
|
+
session.systemSwitchNote = preTurnState.systemSwitchNote;
|
|
1575
|
+
if (preTurnState.blockerAnswersNote !== undefined)
|
|
1576
|
+
session.blockerAnswersNote = preTurnState.blockerAnswersNote;
|
|
1449
1577
|
// Steering drained in round 0 goes back to the queue, ahead of any
|
|
1450
1578
|
// typed since, so the re-run turn still sees it.
|
|
1451
1579
|
// LT-15 Task 4 — the round-0 ones were shown as "↳ read": marked, so the
|
|
@@ -1551,7 +1679,7 @@ ${imageExpansion.error}`));
|
|
|
1551
1679
|
// that ended before the model answered the tool_addition.
|
|
1552
1680
|
placeSystemMessagesInPlace(session.messages);
|
|
1553
1681
|
if (healedOrphans > 0) {
|
|
1554
|
-
emit(chalk.dim(
|
|
1682
|
+
emit(chalk.dim(closedOffStepsLine(healedOrphans)));
|
|
1555
1683
|
}
|
|
1556
1684
|
// Task 4 — effort only from an explicit act (ruling F2); never to a
|
|
1557
1685
|
// model that rejects it (probe P1: claude-haiku-4-5* ⇒ 400).
|
|
@@ -2281,10 +2409,15 @@ ${imageExpansion.error}`));
|
|
|
2281
2409
|
model: params.modelOverride ?? resolveModelRole('session_default', cfg) ?? session.model ?? 'unknown',
|
|
2282
2410
|
emit,
|
|
2283
2411
|
sapSystem: session.sap_system,
|
|
2412
|
+
// Final review F6: the save and the team build folder follow this turn's folder.
|
|
2413
|
+
cwd: params.ctx.cwd,
|
|
2414
|
+
...(params.operator ? { operator: params.operator } : {}),
|
|
2415
|
+
sessionId: session.id,
|
|
2284
2416
|
...(params.ctx.subagent ? { subagent: true } : {}),
|
|
2285
2417
|
...(session.awaitingSkillAnswer === true ? { awaitingAnswer: true } : {}),
|
|
2286
2418
|
...(getJourneyFlow(session) || getAppFlow(session) ? { guided: true } : {}),
|
|
2287
2419
|
session,
|
|
2420
|
+
readCwd: params.ctx.cwd,
|
|
2288
2421
|
promotedFrom: promotedFromForSave,
|
|
2289
2422
|
...(promotedFromPathForSave ? { promotedFromPath: promotedFromPathForSave } : {}),
|
|
2290
2423
|
...(() => {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* Phase 3. Today the cache is process-lifetime; users get a fresh
|
|
31
31
|
* context next CLI invocation.
|
|
32
32
|
*/
|
|
33
|
-
import { getProjectContext,
|
|
33
|
+
import { getProjectContext, renderProjectContextCapped } from '../project-context/index.js';
|
|
34
34
|
/**
|
|
35
35
|
* The env-var name that gates this enrichment. Treated case-insensitively
|
|
36
36
|
* but value comparison is exact ("on" only — anything else is OFF).
|
|
@@ -70,6 +70,8 @@ export function resetProjectContextCache() {
|
|
|
70
70
|
export function __resetProjectContextCacheForTests() {
|
|
71
71
|
resetProjectContextCache();
|
|
72
72
|
}
|
|
73
|
+
/** The one plain line shown when the folder picture was shortened. */
|
|
74
|
+
export const PROJECT_CONTEXT_CUT_LINE = 'This folder is very large, so we work from a shortened picture of it.';
|
|
73
75
|
/**
|
|
74
76
|
* Returns the rendered <project_context> block for splicing into the
|
|
75
77
|
* user message, or '' when the flag is off / no context was detectable.
|
|
@@ -94,7 +96,14 @@ export async function maybeBuildProjectContext(options) {
|
|
|
94
96
|
const ctx = await getProjectContext(options.cwd, {
|
|
95
97
|
sapConnection: options.sapConnection ?? null,
|
|
96
98
|
});
|
|
97
|
-
|
|
99
|
+
const rendered = ctx ? renderProjectContextCapped(ctx) : { block: '', cut: false };
|
|
100
|
+
cachedBlock = rendered.block;
|
|
101
|
+
if (rendered.cut) {
|
|
102
|
+
try {
|
|
103
|
+
options.emit?.(PROJECT_CONTEXT_CUT_LINE);
|
|
104
|
+
}
|
|
105
|
+
catch { /* a display failure never drops the block */ }
|
|
106
|
+
}
|
|
98
107
|
}
|
|
99
108
|
catch (err) {
|
|
100
109
|
// getProjectContext is documented as never-throws; reaching this
|
|
@@ -210,6 +210,15 @@ export const INTERRUPTED_TOOL_RESULT = 'This tool call did not complete — the
|
|
|
210
210
|
* SAP change. Same shape and flags as INTERRUPTED_TOOL_RESULT (is_error: true).
|
|
211
211
|
*/
|
|
212
212
|
export const STOPPED_BEFORE_START_RESULT = 'This tool call was stopped before anything ran — no change was made in SAP. Ask again if it is still needed.';
|
|
213
|
+
/**
|
|
214
|
+
* 1.1.53 item 10 — the dim line shown when healOrphanToolUses closed off steps
|
|
215
|
+
* cut by an earlier stop. Plain words for the person; no internals.
|
|
216
|
+
*/
|
|
217
|
+
export function closedOffStepsLine(n) {
|
|
218
|
+
return n === 1
|
|
219
|
+
? '(we closed off a step that was stopped earlier)'
|
|
220
|
+
: `(we closed off ${n} steps that were stopped earlier)`;
|
|
221
|
+
}
|
|
213
222
|
/**
|
|
214
223
|
* LT-15 D6 (review M5) — the same, for the write pipeline's second guard: by
|
|
215
224
|
* then the approval was spent and SAP was read (a local snapshot may exist),
|
|
@@ -12,6 +12,8 @@ import { riskOf } from './risk-gate.js';
|
|
|
12
12
|
import { NOT_ON_THIS_RELEASE, gateWithVerify } from '../sap/profile-gate.js';
|
|
13
13
|
import { INTERRUPTED_TOOL_RESULT, STOPPED_BEFORE_START_RESULT } from './repair-partial.js';
|
|
14
14
|
import { WRITE_TOOLS } from '../tools/write-mode.js';
|
|
15
|
+
import { seatRefusal } from '../tools/seat-gate.js';
|
|
16
|
+
import { getSeatKind } from '../licence/seat-state.js';
|
|
15
17
|
const READER_TOOL_SET = new Set(READER_TOOLS);
|
|
16
18
|
/** Chat lane (spec 2026-10-17 D4) — the model-facing reason; names hand_off. */
|
|
17
19
|
export const CHAT_READ_ONLY_REASON = 'This chat reads but never changes SAP. To make this change, call hand_off with the right workflow (for example abap-refactor, abap-generate, abap-transport) and a clear request — the workflow shows its plan and asks before it writes.';
|
|
@@ -70,6 +72,15 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
70
72
|
if (ctx.readerMode && !READER_TOOL_SET.has(name)) {
|
|
71
73
|
return { content: `error: read-only reader — ${name} is not allowed here`, is_error: true };
|
|
72
74
|
}
|
|
75
|
+
// Team build Task 6 — a functional seat reads SAP and never changes it, in
|
|
76
|
+
// every lane (skill, chat, and an agent_run child, which shares the process
|
|
77
|
+
// seat) whatever write_mode, role or /mode says. The refused tools are not
|
|
78
|
+
// offered (agent/loop.ts toolsForSeat); THIS is the guarantee for a call the
|
|
79
|
+
// model makes anyway. Before the chat gate and every card below. Developers:
|
|
80
|
+
// seatRefusal is null, so today's dispatch is untouched.
|
|
81
|
+
const seat = seatRefusal(getSeatKind(), tool, args);
|
|
82
|
+
if (seat)
|
|
83
|
+
return { content: JSON.stringify({ error: 'seat_read_only', tool: name, message: seat }), is_error: true };
|
|
73
84
|
// Chat lane (spec 2026-10-17 D4, §6.3) — chat reads freely and never changes
|
|
74
85
|
// SAP. The prompt says so; THIS is the guarantee: every SAP-mutating tool and
|
|
75
86
|
// request_approval are refused while the session's last turn ran in chat,
|
|
@@ -113,8 +124,12 @@ export async function dispatchTool(name, args, ctx) {
|
|
|
113
124
|
}
|
|
114
125
|
// Final review I-3 — the D11 re-check can take seconds of SAP reads; an Esc
|
|
115
126
|
// in that window must stop the call here, not let the handler run "as unknown".
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
// 1.1.53 item 10 — the handler has not run (the gate only reads). A tool
|
|
128
|
+
// that cannot change SAP (hand_off, reads, local files) says so plainly; a
|
|
129
|
+
// tool that can change SAP keeps the "effect unknown, check" safety text.
|
|
130
|
+
if (ctx.signal?.aborted) {
|
|
131
|
+
return { content: isChatRefusedTool(tool) ? INTERRUPTED_TOOL_RESULT : STOPPED_BEFORE_START_RESULT, is_error: true };
|
|
132
|
+
}
|
|
118
133
|
// L1-6 R1-6 (owner decision 2026-09-21) — a call that hands the stored SAP
|
|
119
134
|
// login to a local child process ALWAYS asks a human, once per session per
|
|
120
135
|
// alias, even as the first mutating call of a turn (where Rule 8 below stays
|
|
@@ -3,6 +3,7 @@ import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
|
3
3
|
import { wrapRows } from '../renderer/wrap-rows.js';
|
|
4
4
|
import { shownUpTo } from '../register/atc-file.js';
|
|
5
5
|
import { transportForWrite } from '../register/project-transport.js';
|
|
6
|
+
import { decodeDisplayEntities, decodeDisplayBlock } from '../renderer/html-entities.js';
|
|
6
7
|
/**
|
|
7
8
|
* Human-readable operation + object labels for the approval surfaces.
|
|
8
9
|
*
|
|
@@ -103,7 +104,7 @@ export function affectsLine(change) {
|
|
|
103
104
|
// line comes from the change's op and type, never from a model-supplied field.
|
|
104
105
|
if (change.op === 'delete' && typeCode(change.type) === 'FUGR')
|
|
105
106
|
return FUGR_DELETE_LINE;
|
|
106
|
-
const t = sanitizeControlChars(String(change.affects ?? '')).trim();
|
|
107
|
+
const t = decodeDisplayEntities(sanitizeControlChars(String(change.affects ?? ''))).trim();
|
|
107
108
|
return t ? `Affects: ${t}` : null;
|
|
108
109
|
}
|
|
109
110
|
/** Task 7 — the longest behaviour text a card shows, and the most rows it takes. */
|
|
@@ -121,7 +122,8 @@ export function behaviourText(change) {
|
|
|
121
122
|
// shownUpTo drops whole escape sequences first (a lone sanitizeControlChars
|
|
122
123
|
// pass would leave their '[31m' tails), then every control character.
|
|
123
124
|
// The cut counts code points, so it never leaves half a surrogate pair.
|
|
124
|
-
|
|
125
|
+
// 1.1.53 item 5: entities decode before the cut, so the cut counts what shows.
|
|
126
|
+
const flat = Array.from(decodeDisplayEntities(shownUpTo(String(change.behaviour ?? ''), Number.POSITIVE_INFINITY)));
|
|
125
127
|
const t = flat.length > BEHAVIOUR_MAX ? `${flat.slice(0, BEHAVIOUR_MAX).join('')}…` : flat.join('');
|
|
126
128
|
if (!t || NOTHING_CHANGES.test(t))
|
|
127
129
|
return null;
|
|
@@ -209,7 +211,8 @@ export function displayTransport(op, explicit, session, owning, project) {
|
|
|
209
211
|
* modal's [v] view and the classic prompt's View source branch.
|
|
210
212
|
*/
|
|
211
213
|
export function toSourceLines(content) {
|
|
212
|
-
|
|
214
|
+
// 1.1.53 item 5: display decode of the whole block (kept as written when it has a raw field symbol or markup).
|
|
215
|
+
return decodeDisplayBlock(content)
|
|
213
216
|
.split('\n')
|
|
214
217
|
.filter((l) => !l.startsWith('@@') && !l.startsWith('+++') && !l.startsWith('---'))
|
|
215
218
|
.map((l) => l.replace(/^[+-] ?/, ''));
|
package/dist/approvals/render.js
CHANGED
|
@@ -14,6 +14,7 @@ import { paint } from '../renderer/theme.js';
|
|
|
14
14
|
import { sanitizeControlChars } from '../renderer/sanitize.js';
|
|
15
15
|
import { planRows, looksLikeUnifiedDiff } from './visible-changes.js';
|
|
16
16
|
import { withSpinnerSuspended } from '../renderer/tool-widget.js';
|
|
17
|
+
import { decodeDisplayEntities, decodeDisplayBlock } from '../renderer/html-entities.js';
|
|
17
18
|
// Piece 2 §1.1 — the brand accent comes from the theme role, not a legacy hex.
|
|
18
19
|
const peach = (s) => paint('brand', s);
|
|
19
20
|
/**
|
|
@@ -294,11 +295,11 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
294
295
|
// proper diff content.
|
|
295
296
|
if (change.diff) {
|
|
296
297
|
if (looksLikeUnifiedDiff(change.diff)) {
|
|
297
|
-
console.log('\n' + colorizeDiff(change.diff) + '\n');
|
|
298
|
+
console.log('\n' + colorizeDiff(decodeDisplayBlock(change.diff)) + '\n');
|
|
298
299
|
}
|
|
299
300
|
else {
|
|
300
301
|
console.log('\n' + chalk.yellow(`${getGlyphs().attention} Model did not provide a unified diff — only a description:`));
|
|
301
|
-
console.log(chalk.dim(change.diff) + '\n');
|
|
302
|
+
console.log(chalk.dim(decodeDisplayEntities(change.diff)) + '\n');
|
|
302
303
|
}
|
|
303
304
|
}
|
|
304
305
|
else {
|
package/dist/auth/auth-file.js
CHANGED
|
@@ -40,6 +40,8 @@ export async function readAuthFile() {
|
|
|
40
40
|
out.apiKey = parsed.apiKey;
|
|
41
41
|
if (typeof parsed.savedAt === 'string')
|
|
42
42
|
out.savedAt = parsed.savedAt;
|
|
43
|
+
if (typeof parsed.signedInWith === 'string')
|
|
44
|
+
out.signedInWith = parsed.signedInWith;
|
|
43
45
|
return out;
|
|
44
46
|
}
|
|
45
47
|
catch {
|
|
@@ -54,6 +56,7 @@ export async function writeAuthFile(auth) {
|
|
|
54
56
|
...(auth.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
|
|
55
57
|
customerId: auth.customerId,
|
|
56
58
|
...(auth.savedAt !== undefined ? { savedAt: auth.savedAt } : {}),
|
|
59
|
+
...(auth.signedInWith !== undefined ? { signedInWith: auth.signedInWith } : {}),
|
|
57
60
|
};
|
|
58
61
|
await writeAtomic0600(p, JSON.stringify(body, null, 2));
|
|
59
62
|
}
|
|
@@ -27,6 +27,7 @@ import { promises as fs } from 'node:fs';
|
|
|
27
27
|
import { cspeachRoot, defaultCspeachRoot } from '../config/paths.js';
|
|
28
28
|
import { readAuthFile, writeAuthFile, deleteAuthFile, authFileMtimeMs, authFileDir } from './auth-file.js';
|
|
29
29
|
import { licenceCachePath } from '../licence/paths.js';
|
|
30
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
30
31
|
export const KEYCHAIN_SERVICE = 'cspeach';
|
|
31
32
|
export const DEFAULT_ACCOUNT = 'api-key';
|
|
32
33
|
async function defaultIo() {
|
|
@@ -181,7 +182,8 @@ export async function migrateSignIn(deps = {}) {
|
|
|
181
182
|
return auth?.apiKey ? 'store-unavailable' : 'none';
|
|
182
183
|
}
|
|
183
184
|
try {
|
|
184
|
-
|
|
185
|
+
// Team build: signedInWith stays, so a new sign-in never turns "legacy".
|
|
186
|
+
await writeAuthFile({ customerId: auth.customerId, ...(auth.savedAt ? { savedAt: auth.savedAt } : {}), ...(auth.signedInWith !== undefined ? { signedInWith: auth.signedInWith } : {}) });
|
|
185
187
|
}
|
|
186
188
|
catch {
|
|
187
189
|
return 'failed'; // the store holds the key; the next start removes the copy ('removed')
|
|
@@ -217,12 +219,15 @@ export async function saveSignIn(a, deps = {}) {
|
|
|
217
219
|
catch {
|
|
218
220
|
stored = false;
|
|
219
221
|
}
|
|
222
|
+
// Team build: signedInWith marks a sign-in made by this CLI or newer; an
|
|
223
|
+
// auth.json without it is a pre-release sign-in (a developer seat).
|
|
224
|
+
const signedInWith = CLI_VERSION;
|
|
220
225
|
if (!stored) {
|
|
221
|
-
await writeAuthFile({ apiKey: a.apiKey, customerId: a.customerId, savedAt });
|
|
226
|
+
await writeAuthFile({ apiKey: a.apiKey, customerId: a.customerId, savedAt, signedInWith });
|
|
222
227
|
return 'file';
|
|
223
228
|
}
|
|
224
229
|
await recordStoreWrite(account, savedAt).catch(() => { }); // bookkeeping only, no secret
|
|
225
|
-
await writeAuthFile({ customerId: a.customerId, savedAt });
|
|
230
|
+
await writeAuthFile({ customerId: a.customerId, savedAt, signedInWith });
|
|
226
231
|
return 'store';
|
|
227
232
|
}
|
|
228
233
|
/**
|
package/dist/auth/me.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Fetch /v1/me — customer profile + entitlements + usage.
|
|
3
3
|
* Result envelope so the REPL doesn't need try/catch.
|
|
4
4
|
*/
|
|
5
|
+
import { CLI_VERSION } from '../lib/version.js';
|
|
5
6
|
/** Default /v1/me budget. The panels brand line waits for this call (D5), so a
|
|
6
7
|
* hung proxy must not mean a blank screen. */
|
|
7
8
|
export const FETCH_ME_TIMEOUT_MS = 5000;
|
|
@@ -18,8 +19,11 @@ export async function fetchMe(opts) {
|
|
|
18
19
|
try {
|
|
19
20
|
return await Promise.race([
|
|
20
21
|
(async () => {
|
|
21
|
-
// No extra headers → exactly `{ Authorization }
|
|
22
|
-
|
|
22
|
+
// No extra headers → exactly `{ User-Agent, Authorization }`. Team build
|
|
23
|
+
// (R5): every /v1/me call names this CLI, so the proxy can tell a
|
|
24
|
+
// current CLI from one too old for a functional seat. Set after the
|
|
25
|
+
// caller's headers, so no caller can drop it.
|
|
26
|
+
const r = await fetch(url, { headers: { ...(opts.headers ?? {}), 'User-Agent': `cspeach/${CLI_VERSION}`, Authorization: `Bearer ${opts.apiKey}` }, signal });
|
|
23
27
|
const serverDate = r.headers.get('date');
|
|
24
28
|
if (!r.ok) {
|
|
25
29
|
const body = await r.json().catch(() => ({}));
|
package/dist/cli.js
CHANGED
|
@@ -52,6 +52,13 @@ export async function main(argv) {
|
|
|
52
52
|
printHelp();
|
|
53
53
|
return 0;
|
|
54
54
|
}
|
|
55
|
+
// Team build (Task 12): `cspeach help team-build` prints the how-to. Above the
|
|
56
|
+
// one-shot fallback, so the words are never sent to the model as a prompt.
|
|
57
|
+
if (args[0] === 'help' && args[1] === 'team-build' && args.length === 2) {
|
|
58
|
+
const { printTeamBuildHelp } = await import('./commands/team-build-help.js');
|
|
59
|
+
printTeamBuildHelp();
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
55
62
|
if (args[0] === '--version') {
|
|
56
63
|
console.log(CLI_VERSION);
|
|
57
64
|
return 0;
|
|
@@ -248,6 +255,14 @@ export async function main(argv) {
|
|
|
248
255
|
const { input } = await import('@inquirer/prompts');
|
|
249
256
|
return input({ message: question });
|
|
250
257
|
},
|
|
258
|
+
// Team build (ruling R2, final review F2): the terminal verbs run no
|
|
259
|
+
// start-up, so the seat is resolved here — the verified record, else one
|
|
260
|
+
// /v1/me when there is none, else the offline rule. Asked only before a
|
|
261
|
+
// verb that records work.
|
|
262
|
+
seat: async () => {
|
|
263
|
+
const { resolveTerminalSeatNow } = await import('./licence/terminal-seat.js');
|
|
264
|
+
return resolveTerminalSeatNow();
|
|
265
|
+
},
|
|
251
266
|
});
|
|
252
267
|
}
|
|
253
268
|
// CAP phase 2 — run the SAP link in the foreground for a local CAP app.
|
|
@@ -256,6 +271,11 @@ export async function main(argv) {
|
|
|
256
271
|
return runSapLinkCommand(args.slice(2), {
|
|
257
272
|
print: (s) => console.log(s),
|
|
258
273
|
defaultAlias: sapAlias,
|
|
274
|
+
// Final review F3: resolved as the team verbs do; a functional seat's link reads and never writes.
|
|
275
|
+
seat: async () => {
|
|
276
|
+
const { resolveTerminalSeatNow } = await import('./licence/terminal-seat.js');
|
|
277
|
+
return resolveTerminalSeatNow();
|
|
278
|
+
},
|
|
259
279
|
standalone: async () => {
|
|
260
280
|
const { loadConfig, resolveStandalone } = await import('./config/loader.js');
|
|
261
281
|
return resolveStandalone(await loadConfig(), noSap);
|
|
@@ -334,10 +354,12 @@ Usage:
|
|
|
334
354
|
cspeach login azure Sign in to Microsoft for your company's Azure (--device-code: use a code instead of the browser)
|
|
335
355
|
cspeach logout Sign out (removes the CSPeach key from this home)
|
|
336
356
|
cspeach whoami Print current account info
|
|
357
|
+
cspeach help team-build How two people share one folder: the functional writes the spec, the developer builds it
|
|
337
358
|
cspeach team discover Read the whole estate from SAP into this folder (read only)
|
|
338
359
|
cspeach team start <packages> Start a team project in this folder: object list, first check, you own it all
|
|
339
360
|
cspeach team start --for "FI and SD" --developers "James Miller, me" Lead: set up by name from this folder's discovery; one lane each
|
|
340
361
|
cspeach team init <folder> Lead: pull the object list from SAP, start a team project
|
|
362
|
+
cspeach team spec list|ready [<slug>] Team build: the specs in this folder; mark one ready for the developer
|
|
341
363
|
cspeach team join <folder> [--as "<name>"] Record your results against a team project (on this laptop only), under the lane owner's name
|
|
342
364
|
cspeach team lane add|list Lead: who owns which packages
|
|
343
365
|
cspeach team lane move --packages <names> --to <developer> Lead: move packages by name (lanes of names only)
|