@cspeach/cli 1.1.30 → 1.1.31
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/.cspeach/project.json +4 -0
- package/README.md +11 -1
- package/dist/agent/loop.js +121 -8
- package/dist/cli.js +36 -14
- package/dist/commands/config-set.js +1 -1
- package/dist/commands/plan-continue.js +1 -1
- package/dist/commands/plan-gate.js +8 -1
- package/dist/commands/plan-resume.js +24 -18
- package/dist/commands/plan-startup.js +49 -6
- package/dist/commands/team-discover.js +195 -0
- package/dist/commands/team-plan.js +585 -0
- package/dist/commands/team.js +375 -41
- package/dist/journey/chain.js +728 -0
- package/dist/journey/clean-core.js +213 -0
- package/dist/journey/flow.js +166 -0
- package/dist/journey/run.js +435 -0
- package/dist/journey/session-deps.js +77 -0
- package/dist/journey/trigger.js +153 -0
- package/dist/lane/app-chain.js +9 -7
- package/dist/lane/chain-token.js +9 -5
- package/dist/lane/roots.js +97 -41
- package/dist/one-shot.js +20 -1
- package/dist/projects/clean-core-confirm.js +156 -0
- package/dist/projects/detail-path.js +9 -4
- package/dist/projects/extract-clean-core.js +3 -0
- package/dist/projects/migration.js +4 -105
- package/dist/projects/plan-lane.js +67 -17
- package/dist/projects/plan-sources.js +13 -7
- package/dist/projects/project-folder.js +131 -0
- package/dist/projects/promote-command.js +39 -0
- package/dist/projects/save-command.js +83 -12
- package/dist/projects/saved-lines.js +17 -3
- package/dist/projects/sort-command.js +499 -0
- package/dist/projects/sort-core.js +248 -0
- package/dist/projects/workspace-retire.js +48 -0
- package/dist/projects/workspace.js +182 -121
- package/dist/register/discover.js +901 -0
- package/dist/register/findings.js +5 -2
- package/dist/register/offer.js +100 -8
- package/dist/register/picture.js +6 -2
- package/dist/register/plan-import.js +325 -0
- package/dist/register/records.js +25 -0
- package/dist/register/status-file/estate-parts.js +168 -0
- package/dist/register/status-file/plan-parts.js +68 -0
- package/dist/register/status-file/progress-chart.js +110 -0
- package/dist/register/status-file/register-map.js +11 -3
- package/dist/register/status-file/register-model.js +40 -6
- package/dist/register/status-file/register-parts.js +57 -11
- package/dist/register/status-file/register-status-file.js +24 -6
- package/dist/register/status-file/team-css.js +1 -1
- package/dist/register/status-file-sync.js +3 -0
- package/dist/register/status-html.js +3 -1
- package/dist/register/store.js +40 -2
- package/dist/register/workbooks.js +27 -7
- package/dist/register/xlsx.js +13 -3
- package/dist/register/xref.js +117 -0
- package/dist/renderer/startup-lines.js +11 -0
- package/dist/renderer/tool-labels.js +2 -1
- package/dist/renderer/tool-widget.js +15 -1
- package/dist/repl/at-completer.js +6 -2
- package/dist/repl/at-picker.js +1 -1
- package/dist/repl/journey-entry.js +25 -0
- package/dist/repl.js +157 -39
- package/dist/session/backfill.js +342 -0
- package/dist/session/project-match.js +63 -0
- package/dist/session/resume.js +72 -24
- package/dist/session/schema.js +3 -1
- package/dist/session/store.js +96 -51
- package/dist/skill-catalog.js +2 -2
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/_filesystem-shared.js +174 -12
- package/dist/tools/_team-work-path.js +133 -0
- package/dist/tools/approval.js +7 -1
- package/dist/tools/filesystem/extract-xlsx.js +27 -3
- package/dist/tools/filesystem/file-edit.js +12 -4
- package/dist/tools/filesystem/file-read.js +8 -4
- package/dist/tools/filesystem/file-write.js +12 -4
- package/dist/tools/filesystem/glob.js +33 -8
- package/dist/tools/filesystem/grep.js +9 -4
- package/dist/tools/filesystem/read-document.js +6 -3
- package/dist/tools/project/cca_merge.js +8 -2
- package/dist/tools/project/team_estate.js +91 -0
- package/dist/tools/project/team_findings.js +77 -4
- package/dist/tools/project/upgrade_merge_progress.js +8 -2
- package/dist/tools/sap-read.js +21 -4
- package/dist/tools/sap-write.js +200 -10
- package/dist/tools/subagent/read_agent.js +3 -0
- package/dist/tools/tool-loading.js +1 -1
- package/dist/tools/transport-fit-check.js +117 -0
- package/dist/tools/transport-fit.js +15 -7
- package/dist/tools/transport.js +57 -2
- package/dist/ui/app.js +2 -2
- package/dist/ui/approval-modal.js +74 -6
- package/dist/ui/widgets/confirm-card.js +68 -5
- package/dist/util/open-in-browser.js +143 -0
- package/package.json +120 -120
|
@@ -43,10 +43,11 @@ export function colIndex(ref) {
|
|
|
43
43
|
return n - 1;
|
|
44
44
|
}
|
|
45
45
|
/** One worksheet's cells as a rectangular grid of strings. */
|
|
46
|
-
export function sheetRows(xml, strings, budget) {
|
|
46
|
+
export function sheetRows(xml, strings, budget, opts = {}) {
|
|
47
|
+
const maxRows = opts.maxRows ?? MAX_SHEET_ROWS;
|
|
47
48
|
const rows = [];
|
|
48
49
|
for (const rowMatch of xml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)) {
|
|
49
|
-
if (rows.length >=
|
|
50
|
+
if (rows.length >= maxRows || budget.cells <= 0)
|
|
50
51
|
break;
|
|
51
52
|
const cells = [];
|
|
52
53
|
for (const cell of rowMatch[1].matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>|<c\b([^>]*)\/>/g)) {
|
|
@@ -68,7 +69,7 @@ export function sheetRows(xml, strings, budget) {
|
|
|
68
69
|
}
|
|
69
70
|
while (cells.length < at)
|
|
70
71
|
cells.push('');
|
|
71
|
-
cells[at] = value.replace(/[\t\r\n]+/g, ' ').trim();
|
|
72
|
+
cells[at] = (opts.keepLines ? value.replace(/\r\n?/g, '\n') : value.replace(/[\t\r\n]+/g, ' ')).trim();
|
|
72
73
|
budget.cells -= 1;
|
|
73
74
|
if (budget.cells <= 0)
|
|
74
75
|
break;
|
|
@@ -137,3 +138,26 @@ export function extractXlsx(buffer) {
|
|
|
137
138
|
warnings.push('no cell text found — the workbook may be empty or hold only charts/images');
|
|
138
139
|
return { text, sheets: sheets.length, warnings };
|
|
139
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Every sheet as rows of cell text, for a caller that reads a workbook back as
|
|
143
|
+
* data (the Plan sheet, spec 2026-10-06 §8). Numbers come back as their text
|
|
144
|
+
* (`2`, `46000`), shared and inline strings as written, line breaks inside a
|
|
145
|
+
* cell kept. No row cap: a Plan sheet holds one row per object. Throws on a
|
|
146
|
+
* file that is not a readable .xlsx.
|
|
147
|
+
*/
|
|
148
|
+
export function readSheets(buffer) {
|
|
149
|
+
const zip = new AdmZip(buffer);
|
|
150
|
+
const read = (p) => {
|
|
151
|
+
const entry = zip.getEntry(p);
|
|
152
|
+
return entry ? entry.getData().toString('utf-8') : null;
|
|
153
|
+
};
|
|
154
|
+
const strings = sharedStrings(read('xl/sharedStrings.xml'));
|
|
155
|
+
const budget = { cells: Number.POSITIVE_INFINITY };
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const s of sheetOrder(read('xl/workbook.xml'), read('xl/_rels/workbook.xml.rels'))) {
|
|
158
|
+
const xml = read(s.path);
|
|
159
|
+
if (xml !== null)
|
|
160
|
+
out.push({ name: s.name, rows: sheetRows(xml, strings, budget, { maxRows: Number.POSITIVE_INFINITY, keepLines: true }) });
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
@@ -15,6 +15,7 @@ import { promises as fs } from 'node:fs';
|
|
|
15
15
|
import * as path from 'node:path';
|
|
16
16
|
import { registerTool } from '../index.js';
|
|
17
17
|
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, blockedExecutableExtension, } from '../_filesystem-shared.js';
|
|
18
|
+
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamWorkTarget } from '../_team-work-path.js';
|
|
18
19
|
export async function fileEditHandler(args, ctx) {
|
|
19
20
|
// 1. Validate args before any IO
|
|
20
21
|
if (args.old_string === args.new_string) {
|
|
@@ -24,9 +25,11 @@ export async function fileEditHandler(args, ctx) {
|
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
// 2. Path containment — sync phase (pure string math)
|
|
28
|
+
// Task 15 — in a team folder a skill working file is edited in the seat.
|
|
29
|
+
const t = teamWorkTarget(ctx.cwd, args.path);
|
|
27
30
|
let abs;
|
|
28
31
|
try {
|
|
29
|
-
abs = resolveSafePath(
|
|
32
|
+
abs = resolveSafePath(t.root, t.path);
|
|
30
33
|
}
|
|
31
34
|
catch (err) {
|
|
32
35
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -37,7 +40,7 @@ export async function fileEditHandler(args, ctx) {
|
|
|
37
40
|
// 3. Path containment — async phase (follows symlinks)
|
|
38
41
|
let realAbs;
|
|
39
42
|
try {
|
|
40
|
-
realAbs = await assertRealPathContained(abs,
|
|
43
|
+
realAbs = await assertRealPathContained(abs, t.root);
|
|
41
44
|
}
|
|
42
45
|
catch (err) {
|
|
43
46
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -49,7 +52,7 @@ export async function fileEditHandler(args, ctx) {
|
|
|
49
52
|
return { content: `error: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
50
53
|
}
|
|
51
54
|
// 4. Denylist check: refuse sensitive in-root paths
|
|
52
|
-
const denylist = isDenylistedPath(realAbs,
|
|
55
|
+
const denylist = isDenylistedPath(realAbs, t.root);
|
|
53
56
|
if (denylist.blocked) {
|
|
54
57
|
return {
|
|
55
58
|
content: `error: refusing to edit sensitive path "${denylist.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`,
|
|
@@ -66,6 +69,11 @@ export async function fileEditHandler(args, ctx) {
|
|
|
66
69
|
is_error: true,
|
|
67
70
|
};
|
|
68
71
|
}
|
|
72
|
+
// 4c. Team folder: the real path never lies in the shared .cspeach
|
|
73
|
+
// (an 8.3 name, a junction or an odd spelling would get it there).
|
|
74
|
+
if (pointsIntoSharedCspeach(ctx.cwd, realAbs)) {
|
|
75
|
+
return { content: `error: ${SHARED_CSPEACH_REFUSAL}`, is_error: true };
|
|
76
|
+
}
|
|
69
77
|
// 5. Read the file
|
|
70
78
|
let content;
|
|
71
79
|
try {
|
|
@@ -114,7 +122,7 @@ export async function fileEditHandler(args, ctx) {
|
|
|
114
122
|
return { content: `error: write failed — ${err.message}`, is_error: true };
|
|
115
123
|
}
|
|
116
124
|
const noun = replacementCount === 1 ? 'replacement' : 'replacements';
|
|
117
|
-
const relPath = path.relative(path.resolve(
|
|
125
|
+
const relPath = path.relative(path.resolve(t.root), realAbs).replace(/\\/g, '/');
|
|
118
126
|
return { content: `edited ${relPath} (${replacementCount} ${noun})` };
|
|
119
127
|
}
|
|
120
128
|
registerTool({
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { promises as fs } from 'node:fs';
|
|
12
12
|
import { registerTool } from '../index.js';
|
|
13
13
|
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, isReadOnlyProjectFile } from '../_filesystem-shared.js';
|
|
14
|
+
import { teamReadTarget } from '../_team-work-path.js';
|
|
14
15
|
import { spillToolResult, isUnderSpillRoot } from '../result-spill.js';
|
|
15
16
|
import { basename } from 'node:path';
|
|
16
17
|
const MAX_LINES = 2000;
|
|
@@ -22,9 +23,12 @@ export async function fileReadHandler(args, ctx) {
|
|
|
22
23
|
if (args.limit !== undefined && args.limit < 1) {
|
|
23
24
|
return { content: `error: limit must be a positive integer (got ${args.limit})`, is_error: true };
|
|
24
25
|
}
|
|
26
|
+
// Task 15 — in a team folder a skill working file, a saved result
|
|
27
|
+
// (.cspeach/*.cspeach.json) or a lane decision is read from the seat.
|
|
28
|
+
const t = teamReadTarget(ctx.cwd, args.path);
|
|
25
29
|
let abs;
|
|
26
30
|
try {
|
|
27
|
-
abs = resolveSafePath(
|
|
31
|
+
abs = resolveSafePath(t.root, t.path);
|
|
28
32
|
}
|
|
29
33
|
catch (err) {
|
|
30
34
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -34,7 +38,7 @@ export async function fileReadHandler(args, ctx) {
|
|
|
34
38
|
}
|
|
35
39
|
let realAbs;
|
|
36
40
|
try {
|
|
37
|
-
realAbs = await assertRealPathContained(abs,
|
|
41
|
+
realAbs = await assertRealPathContained(abs, t.root);
|
|
38
42
|
}
|
|
39
43
|
catch (err) {
|
|
40
44
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -48,8 +52,8 @@ export async function fileReadHandler(args, ctx) {
|
|
|
48
52
|
// Denylist check: refuse sensitive in-root paths. The project's own saved
|
|
49
53
|
// results (.cspeach/*.cspeach.json, .cspeach/lane/*.json) are readable —
|
|
50
54
|
// isReadOnlyProjectFile; the write tools never get this carve-out.
|
|
51
|
-
const denylist = isDenylistedPath(realAbs,
|
|
52
|
-
if (denylist.blocked && !isReadOnlyProjectFile(realAbs,
|
|
55
|
+
const denylist = isDenylistedPath(realAbs, t.root);
|
|
56
|
+
if (denylist.blocked && !isReadOnlyProjectFile(realAbs, t.root)) {
|
|
53
57
|
return {
|
|
54
58
|
content: `error: refusing to read sensitive path "${denylist.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`,
|
|
55
59
|
is_error: true,
|
|
@@ -19,6 +19,7 @@ import { promises as fs, readFileSync } from 'node:fs';
|
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { registerTool } from '../index.js';
|
|
21
21
|
import { resolveSafePath, realPathForWrite, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, blockedExecutableExtension, } from '../_filesystem-shared.js';
|
|
22
|
+
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamWorkTarget } from '../_team-work-path.js';
|
|
22
23
|
/**
|
|
23
24
|
* 2026-09-23 (Glenn's first run) — a standalone run writes GUIDE.md and said
|
|
24
25
|
* nothing about it. He opened it in a text editor, then tried to drop it into
|
|
@@ -57,9 +58,11 @@ function noteGuideWritten(relPath, ctx) {
|
|
|
57
58
|
}
|
|
58
59
|
export async function fileWriteHandler(args, ctx) {
|
|
59
60
|
// 1. Path containment — sync phase (pure string math, catches .. traversals)
|
|
61
|
+
// Task 15 — in a team folder a skill working file goes to the seat.
|
|
62
|
+
const t = teamWorkTarget(ctx.cwd, args.path);
|
|
60
63
|
let abs;
|
|
61
64
|
try {
|
|
62
|
-
abs = resolveSafePath(
|
|
65
|
+
abs = resolveSafePath(t.root, t.path);
|
|
63
66
|
}
|
|
64
67
|
catch (err) {
|
|
65
68
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -74,7 +77,7 @@ export async function fileWriteHandler(args, ctx) {
|
|
|
74
77
|
// `.git` behind a link (`docs -> .git`) or a short name (`GIT~1`).
|
|
75
78
|
let target;
|
|
76
79
|
try {
|
|
77
|
-
target = await realPathForWrite(abs,
|
|
80
|
+
target = await realPathForWrite(abs, t.root);
|
|
78
81
|
}
|
|
79
82
|
catch (err) {
|
|
80
83
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -83,7 +86,7 @@ export async function fileWriteHandler(args, ctx) {
|
|
|
83
86
|
return { content: `error: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
84
87
|
}
|
|
85
88
|
// 3. Denylist check: refuse sensitive in-root paths
|
|
86
|
-
const denylist = isDenylistedPath(target,
|
|
89
|
+
const denylist = isDenylistedPath(target, t.root);
|
|
87
90
|
if (denylist.blocked) {
|
|
88
91
|
return {
|
|
89
92
|
content: `error: refusing to write sensitive path "${denylist.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`,
|
|
@@ -99,6 +102,11 @@ export async function fileWriteHandler(args, ctx) {
|
|
|
99
102
|
is_error: true,
|
|
100
103
|
};
|
|
101
104
|
}
|
|
105
|
+
// 3c. Team folder: the real landing path never lies in the shared .cspeach
|
|
106
|
+
// (an 8.3 name, a junction or an odd spelling would get it there).
|
|
107
|
+
if (pointsIntoSharedCspeach(ctx.cwd, target)) {
|
|
108
|
+
return { content: `error: ${SHARED_CSPEACH_REFUSAL}`, is_error: true };
|
|
109
|
+
}
|
|
102
110
|
// 4. Ensure parent directory exists (no-op if it already does)
|
|
103
111
|
try {
|
|
104
112
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
@@ -117,7 +125,7 @@ export async function fileWriteHandler(args, ctx) {
|
|
|
117
125
|
await fs.unlink(tmpPath).catch(() => undefined);
|
|
118
126
|
return { content: `error: write failed — ${err.message}`, is_error: true };
|
|
119
127
|
}
|
|
120
|
-
const relPath = path.relative(path.resolve(
|
|
128
|
+
const relPath = path.relative(path.resolve(t.root), target).replace(/\\/g, '/');
|
|
121
129
|
const bytes = Buffer.byteLength(args.content, 'utf-8');
|
|
122
130
|
noteGuideWritten(relPath, ctx);
|
|
123
131
|
return { content: `wrote ${relPath} (${bytes} bytes)` };
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import { promises as fs } from 'node:fs';
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { registerTool } from '../index.js';
|
|
22
|
-
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, isDenylistedPath, isReadOnlyProjectFile, DENYLIST_DESCRIPTION, } from '../_filesystem-shared.js';
|
|
22
|
+
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, isDenylistedPath, isReadOnlyProjectFile, isSeatReadOnlyFile, DENYLIST_DESCRIPTION, } from '../_filesystem-shared.js';
|
|
23
|
+
import { isReadOnlyRel, isWorkRel, teamWorkRoot, teamWorkTarget } from '../_team-work-path.js';
|
|
23
24
|
const MAX_RESULTS = 500;
|
|
24
25
|
const MAX_FILES_TRAVERSED = 10_000;
|
|
25
26
|
const ALWAYS_SKIPPED_DIRS = new Set([
|
|
@@ -85,10 +86,14 @@ export async function globHandler(args, ctx) {
|
|
|
85
86
|
if (!args.pattern || typeof args.pattern !== 'string') {
|
|
86
87
|
return { content: 'error: pattern is required (non-empty string)', is_error: true };
|
|
87
88
|
}
|
|
89
|
+
// Task 15 — in a team folder a search root under `.cspeach/<work-subdir>`
|
|
90
|
+
// is the seat's; `root` is then the seat work root for every check below.
|
|
91
|
+
const t = args.cwd ? teamWorkTarget(ctx.cwd, args.cwd) : { root: ctx.cwd, path: '' };
|
|
92
|
+
const root = t.root;
|
|
88
93
|
// Resolve search root — defaults to ctx.cwd
|
|
89
94
|
let searchRoot;
|
|
90
95
|
try {
|
|
91
|
-
searchRoot = args.cwd ? resolveSafePath(
|
|
96
|
+
searchRoot = args.cwd ? resolveSafePath(root, t.path) : path.resolve(root);
|
|
92
97
|
}
|
|
93
98
|
catch (err) {
|
|
94
99
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -115,7 +120,7 @@ export async function globHandler(args, ctx) {
|
|
|
115
120
|
const wantsCspeach = isCspeachPattern(args.pattern);
|
|
116
121
|
const collected = [];
|
|
117
122
|
let budget = MAX_FILES_TRAVERSED;
|
|
118
|
-
async function walk(dir, relPrefix) {
|
|
123
|
+
async function walk(dir, relPrefix, denyRoot = root) {
|
|
119
124
|
if (budget <= 0 || collected.length >= MAX_RESULTS * 4)
|
|
120
125
|
return;
|
|
121
126
|
let entries;
|
|
@@ -146,7 +151,7 @@ export async function globHandler(args, ctx) {
|
|
|
146
151
|
// work subdirs); the per-file read check decides what is listed.
|
|
147
152
|
if (ALWAYS_SKIPPED_DIRS.has(ent.name) && !intoCspeach && !inCspeach(relPrefix))
|
|
148
153
|
continue;
|
|
149
|
-
await walk(path.join(dir, ent.name), relPath);
|
|
154
|
+
await walk(path.join(dir, ent.name), relPath, denyRoot);
|
|
150
155
|
}
|
|
151
156
|
else if (ent.isFile()) {
|
|
152
157
|
budget--;
|
|
@@ -154,8 +159,8 @@ export async function globHandler(args, ctx) {
|
|
|
154
159
|
continue;
|
|
155
160
|
// Denylist check on the matched path
|
|
156
161
|
const abs = path.join(dir, ent.name);
|
|
157
|
-
const denylist = isDenylistedPath(abs,
|
|
158
|
-
if (denylist.blocked && !isReadOnlyProjectFile(abs,
|
|
162
|
+
const denylist = isDenylistedPath(abs, denyRoot);
|
|
163
|
+
if (denylist.blocked && !isReadOnlyProjectFile(abs, denyRoot))
|
|
159
164
|
continue;
|
|
160
165
|
let mtime = 0;
|
|
161
166
|
try {
|
|
@@ -175,7 +180,7 @@ export async function globHandler(args, ctx) {
|
|
|
175
180
|
// per-file denylist check below sees real paths too.
|
|
176
181
|
let realRoot;
|
|
177
182
|
try {
|
|
178
|
-
realRoot = await assertRealPathContained(searchRoot,
|
|
183
|
+
realRoot = await assertRealPathContained(searchRoot, root);
|
|
179
184
|
}
|
|
180
185
|
catch (err) {
|
|
181
186
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -184,7 +189,7 @@ export async function globHandler(args, ctx) {
|
|
|
184
189
|
return { content: `error: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
185
190
|
}
|
|
186
191
|
for (const cand of [searchRoot, realRoot]) {
|
|
187
|
-
const verdict = isDenylistedPath(cand,
|
|
192
|
+
const verdict = isDenylistedPath(cand, root);
|
|
188
193
|
if (verdict.blocked) {
|
|
189
194
|
return {
|
|
190
195
|
content: `error: refusing to list protected path "${verdict.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`,
|
|
@@ -193,6 +198,26 @@ export async function globHandler(args, ctx) {
|
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
await walk(realRoot, '');
|
|
201
|
+
// Task 15 + ruling 27 — a `.cspeach/…` pattern from a TEAM folder's root
|
|
202
|
+
// lists what a save and the skills left in the seat (working files, saved
|
|
203
|
+
// results `.cspeach/*.cspeach.json`, lane decisions `.cspeach/lane/*.json`),
|
|
204
|
+
// as a solo folder lists its own: the seat's `.cspeach` is walked as
|
|
205
|
+
// `.cspeach`, every file judged against the seat work root, so only those
|
|
206
|
+
// shapes are listed. The team folder's own copies of those shapes (left
|
|
207
|
+
// from before) are not listed — file_read would not reach them.
|
|
208
|
+
const seatRoot = wantsCspeach && searchRoot === path.resolve(ctx.cwd) ? teamWorkRoot(ctx.cwd) : null;
|
|
209
|
+
if (seatRoot !== null) {
|
|
210
|
+
for (let i = collected.length - 1; i >= 0; i--) {
|
|
211
|
+
const rel = collected[i].relPath;
|
|
212
|
+
if (isWorkRel(rel) || isReadOnlyRel(rel))
|
|
213
|
+
collected.splice(i, 1);
|
|
214
|
+
}
|
|
215
|
+
const seatCspeach = path.join(seatRoot, '.cspeach');
|
|
216
|
+
// The folder itself must be the seat's, spelled and real (no link on the way).
|
|
217
|
+
if (isSeatReadOnlyFile(path.join(seatCspeach, '_.cspeach.json'))) {
|
|
218
|
+
await walk(seatCspeach, '.cspeach', seatRoot);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
196
221
|
// Sort by mtime descending, cap at MAX_RESULTS
|
|
197
222
|
collected.sort((a, b) => b.mtime - a.mtime);
|
|
198
223
|
const sample = collected.slice(0, MAX_RESULTS).map((f) => f.relPath);
|
|
@@ -22,6 +22,7 @@ import * as path from 'node:path';
|
|
|
22
22
|
import { registerTool } from '../index.js';
|
|
23
23
|
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, isDenylistedPath, DENYLIST_DESCRIPTION, } from '../_filesystem-shared.js';
|
|
24
24
|
import { globToRegex } from './glob.js';
|
|
25
|
+
import { teamWorkTarget } from '../_team-work-path.js';
|
|
25
26
|
const MAX_MATCHES = 250;
|
|
26
27
|
const MAX_FILES_TRAVERSED = 10_000;
|
|
27
28
|
const MAX_FILE_BYTES = 2_000_000;
|
|
@@ -40,9 +41,13 @@ export async function grepHandler(args, ctx) {
|
|
|
40
41
|
catch (err) {
|
|
41
42
|
return { content: `error: invalid regex — ${err.message}`, is_error: true };
|
|
42
43
|
}
|
|
44
|
+
// Task 15 — in a team folder a search root under `.cspeach/<work-subdir>`
|
|
45
|
+
// is the seat's; `root` is then the seat work root for every check below.
|
|
46
|
+
const t = args.path ? teamWorkTarget(ctx.cwd, args.path) : { root: ctx.cwd, path: '' };
|
|
47
|
+
const root = t.root;
|
|
43
48
|
let searchRoot;
|
|
44
49
|
try {
|
|
45
|
-
searchRoot = args.path ? resolveSafePath(
|
|
50
|
+
searchRoot = args.path ? resolveSafePath(root, t.path) : path.resolve(root);
|
|
46
51
|
}
|
|
47
52
|
catch (err) {
|
|
48
53
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -98,7 +103,7 @@ export async function grepHandler(args, ctx) {
|
|
|
98
103
|
if (fileFilter && !fileFilter.test(relPath))
|
|
99
104
|
continue;
|
|
100
105
|
const abs = path.join(dir, ent.name);
|
|
101
|
-
const denylist = isDenylistedPath(abs,
|
|
106
|
+
const denylist = isDenylistedPath(abs, root);
|
|
102
107
|
if (denylist.blocked)
|
|
103
108
|
continue;
|
|
104
109
|
let stat;
|
|
@@ -135,7 +140,7 @@ export async function grepHandler(args, ctx) {
|
|
|
135
140
|
// per-file denylist check below sees real paths too.
|
|
136
141
|
let realRoot;
|
|
137
142
|
try {
|
|
138
|
-
realRoot = await assertRealPathContained(searchRoot,
|
|
143
|
+
realRoot = await assertRealPathContained(searchRoot, root);
|
|
139
144
|
}
|
|
140
145
|
catch (err) {
|
|
141
146
|
if (err instanceof PathOutsideRootError) {
|
|
@@ -144,7 +149,7 @@ export async function grepHandler(args, ctx) {
|
|
|
144
149
|
return { content: `error: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
145
150
|
}
|
|
146
151
|
for (const cand of [searchRoot, realRoot]) {
|
|
147
|
-
const verdict = isDenylistedPath(cand,
|
|
152
|
+
const verdict = isDenylistedPath(cand, root);
|
|
148
153
|
if (verdict.blocked) {
|
|
149
154
|
return {
|
|
150
155
|
content: `error: refusing to search protected path "${verdict.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`,
|
|
@@ -8,6 +8,7 @@ import { promises as fs } from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { registerTool } from '../index.js';
|
|
10
10
|
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, } from '../_filesystem-shared.js';
|
|
11
|
+
import { teamWorkTarget } from '../_team-work-path.js';
|
|
11
12
|
import { extractDocument, DocumentTooLargeError } from './extract-document.js';
|
|
12
13
|
const MAX_LINES = 2000;
|
|
13
14
|
const MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
@@ -20,9 +21,11 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
20
21
|
if (args.limit !== undefined && args.limit < 1) {
|
|
21
22
|
return { content: `error: limit must be a positive integer (got ${args.limit})`, is_error: true };
|
|
22
23
|
}
|
|
24
|
+
// Task 15 — in a team folder a skill working file is read from the seat.
|
|
25
|
+
const t = teamWorkTarget(ctx.cwd, args.path);
|
|
23
26
|
let abs;
|
|
24
27
|
try {
|
|
25
|
-
abs = resolveSafePath(
|
|
28
|
+
abs = resolveSafePath(t.root, t.path);
|
|
26
29
|
}
|
|
27
30
|
catch (err) {
|
|
28
31
|
if (err instanceof PathOutsideRootError)
|
|
@@ -41,7 +44,7 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
41
44
|
}
|
|
42
45
|
let realAbs;
|
|
43
46
|
try {
|
|
44
|
-
realAbs = await assertRealPathContained(abs,
|
|
47
|
+
realAbs = await assertRealPathContained(abs, t.root);
|
|
45
48
|
}
|
|
46
49
|
catch (err) {
|
|
47
50
|
if (err instanceof PathOutsideRootError)
|
|
@@ -50,7 +53,7 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
50
53
|
return { content: 'error: file not found', is_error: true };
|
|
51
54
|
return { content: `error: ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
52
55
|
}
|
|
53
|
-
const denylist = isDenylistedPath(realAbs,
|
|
56
|
+
const denylist = isDenylistedPath(realAbs, t.root);
|
|
54
57
|
if (denylist.blocked) {
|
|
55
58
|
return { content: `error: refusing to read sensitive path "${denylist.relFromRoot}" (denylist: ${DENYLIST_DESCRIPTION})`, is_error: true };
|
|
56
59
|
}
|
|
@@ -30,6 +30,7 @@ import { registerTool } from '../index.js';
|
|
|
30
30
|
import { mergeCcaAssessments, renderCcaMergeManifest, } from '../../projects/merge-cca.js';
|
|
31
31
|
import { loadEnvelope, MANIFEST_PASTE_INSTRUCTIONS } from './_merge-shared.js';
|
|
32
32
|
import { assertWritable } from '../_filesystem-shared.js';
|
|
33
|
+
import { pointsIntoSharedCspeach, SHARED_CSPEACH_REFUSAL, teamWorkTarget } from '../_team-work-path.js';
|
|
33
34
|
export async function ccaMergeHandler(args, ctx) {
|
|
34
35
|
if (!Array.isArray(args.inputs) || args.inputs.some((i) => typeof i !== 'string')) {
|
|
35
36
|
return { content: 'error: inputs must be an array of envelope paths / @-tokens.', is_error: true };
|
|
@@ -98,11 +99,16 @@ export async function ccaMergeHandler(args, ctx) {
|
|
|
98
99
|
// mergedFrom: the input envelopes [{ file, label }]
|
|
99
100
|
// }
|
|
100
101
|
let detailWritten = true;
|
|
101
|
-
|
|
102
|
+
// Task 15 — in a team folder the detail file is a working file: the seat's.
|
|
103
|
+
const t = teamWorkTarget(ctx.cwd, merged.detailPath);
|
|
104
|
+
const detailAbs = isAbsolute(t.path) ? t.path : join(t.root, t.path);
|
|
102
105
|
try {
|
|
103
106
|
// SEC (review round 2): the real landing path must stay in the project and
|
|
104
107
|
// not be protected (a junctioned .cspeach/<domain> -> .git is refused).
|
|
105
|
-
await assertWritable(detailAbs,
|
|
108
|
+
const realDetail = await assertWritable(detailAbs, t.root);
|
|
109
|
+
// Final fix wave, item 3 — never into the shared .cspeach of a team folder.
|
|
110
|
+
if (pointsIntoSharedCspeach(ctx.cwd, realDetail))
|
|
111
|
+
throw new Error(SHARED_CSPEACH_REFUSAL);
|
|
106
112
|
mkdirSync(dirname(detailAbs), { recursive: true });
|
|
107
113
|
writeFileSync(detailAbs, JSON.stringify({
|
|
108
114
|
generatedAt: new Date().toISOString(),
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* team_estate — read-only (spec 2026-10-06 §10).
|
|
3
|
+
*
|
|
4
|
+
* The estate inventory the lead already pulled with `cspeach team discover`:
|
|
5
|
+
* packages, totals, the usage sentence and its sources, unknowns, modifications
|
|
6
|
+
* of SAP objects and how far the cross-reference index was read — and, on
|
|
7
|
+
* request, the objects of named packages with their evidence. What
|
|
8
|
+
* /abap-cca judges from. No SAP call, no SQL, no write.
|
|
9
|
+
*
|
|
10
|
+
* Read from the newest discovery in the team project this laptop joined, or
|
|
11
|
+
* from the working folder's `discovery/` when no team project is joined.
|
|
12
|
+
*/
|
|
13
|
+
import { estateSummaryOf } from '@cspeach/register-core';
|
|
14
|
+
import { registerTool } from '../index.js';
|
|
15
|
+
import { shownUpTo } from '../../register/findings.js';
|
|
16
|
+
import { activeProject, readEstate } from '../../register/store.js';
|
|
17
|
+
const CAP = 200;
|
|
18
|
+
const MODIFICATIONS = 50;
|
|
19
|
+
const TEXT_CAP = 300;
|
|
20
|
+
const NO_ESTATE = 'No estate inventory yet. The lead runs: cspeach team discover --out <folder>';
|
|
21
|
+
const match = (pattern, value) => new RegExp(`^${pattern.trim().replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i').test(value);
|
|
22
|
+
/**
|
|
23
|
+
* Every string cleaned the way a finding title is, before it reaches the model:
|
|
24
|
+
* the discovery files were written on the lead's laptop out of SAP text.
|
|
25
|
+
*/
|
|
26
|
+
function clean(v, depth = 0) {
|
|
27
|
+
if (typeof v === 'string')
|
|
28
|
+
return shownUpTo(v, TEXT_CAP);
|
|
29
|
+
if (depth > 4 || v === null || typeof v !== 'object')
|
|
30
|
+
return v;
|
|
31
|
+
if (Array.isArray(v))
|
|
32
|
+
return v.map((x) => clean(x, depth + 1));
|
|
33
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, clean(x, depth + 1)]));
|
|
34
|
+
}
|
|
35
|
+
export async function teamEstateHandler(args, ctx) {
|
|
36
|
+
try {
|
|
37
|
+
const dir = activeProject()?.projectDir ?? ctx.cwd;
|
|
38
|
+
const found = readEstate(dir);
|
|
39
|
+
const summary = found ? estateSummaryOf(found.estate) : null;
|
|
40
|
+
if (!found || !summary)
|
|
41
|
+
return { content: NO_ESTATE, is_error: true };
|
|
42
|
+
const body = {
|
|
43
|
+
takenAt: summary.takenAt,
|
|
44
|
+
packages: summary.packages,
|
|
45
|
+
totals: summary.totals,
|
|
46
|
+
usageLine: summary.usageLine,
|
|
47
|
+
usageSources: summary.usageSources,
|
|
48
|
+
unknowns: summary.unknowns,
|
|
49
|
+
modifications: summary.modifications.slice(0, MODIFICATIONS),
|
|
50
|
+
...(summary.modifications.length > MODIFICATIONS ? { modificationsMore: summary.modifications.length - MODIFICATIONS } : {}),
|
|
51
|
+
xref: summary.xref,
|
|
52
|
+
};
|
|
53
|
+
const patterns = (args.packages ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
54
|
+
if (patterns.length) {
|
|
55
|
+
const all = found.objects?.kind === 'estate-objects'
|
|
56
|
+
&& Array.isArray(found.objects.objects)
|
|
57
|
+
? found.objects.objects : [];
|
|
58
|
+
const picked = all.filter((o) => !!o && typeof o === 'object'
|
|
59
|
+
&& typeof o.name === 'string' && typeof o.type === 'string' && typeof o.package === 'string'
|
|
60
|
+
&& patterns.some((p) => match(p, o.package)));
|
|
61
|
+
body.objects = picked.slice(0, CAP).map((o) => ({
|
|
62
|
+
type: o.type, name: o.name, package: o.package,
|
|
63
|
+
changed: o.changed ?? null, callers: o.callers ?? null, batch: o.batch ?? null, usage: o.usage ?? { state: 'unknown' },
|
|
64
|
+
}));
|
|
65
|
+
if (picked.length > CAP)
|
|
66
|
+
body.more = picked.length - CAP;
|
|
67
|
+
if (!found.objects)
|
|
68
|
+
body.objectsNote = 'The object list of this inventory could not be read.';
|
|
69
|
+
}
|
|
70
|
+
return { content: JSON.stringify(clean(body)) };
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { content: `The estate inventory could not be read: ${e instanceof Error ? e.message : String(e)}`, is_error: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
registerTool({
|
|
77
|
+
name: 'team_estate',
|
|
78
|
+
description: 'Read-only. The newest estate inventory the lead pulled with cspeach team discover: packages, totals, usage sources and '
|
|
79
|
+
+ 'the usage sentence, unknowns, modifications of SAP objects, cross-reference coverage. With packages, also the objects of those '
|
|
80
|
+
+ 'packages with callers, batch runs, usage and last change (at most 200). Use it for /abap-cca. No SAP call.',
|
|
81
|
+
isMutating: false,
|
|
82
|
+
category: 'project',
|
|
83
|
+
requiresSap: false,
|
|
84
|
+
input_schema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
packages: { type: 'string', description: 'Package patterns, comma-separated, e.g. "ZFI_*,ZOLD_TOOLS". Adds their objects.' },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
handler: (args, ctx) => teamEstateHandler(args, ctx),
|
|
91
|
+
});
|
|
@@ -5,17 +5,52 @@
|
|
|
5
5
|
* package patterns) with the findings measurement zero recorded for them, read
|
|
6
6
|
* from the team project this laptop joined. No SAP call, no second ATC run, no
|
|
7
7
|
* write: every decision is still recorded by program code, one Yes per row.
|
|
8
|
+
*
|
|
9
|
+
* Pages (guided journey, Task 4): `limit` (default 40, at most 200) and
|
|
10
|
+
* `after` (the key "TYPE|NAME" of the last object seen) give one page, with
|
|
11
|
+
* `more` and `next`. Without either, the first 200 and `more`, as before.
|
|
12
|
+
* While a journey triage is queued in the session, only the page the journey
|
|
13
|
+
* handed out (journey/flow.ts, at most 40 keys) is shown: the skill needs no
|
|
14
|
+
* change to work in pages.
|
|
8
15
|
*/
|
|
9
16
|
import { dispositionOf, DISPOSITION_WORD, inLane } from '@cspeach/register-core';
|
|
10
17
|
import { registerTool } from '../index.js';
|
|
11
18
|
import { findingsByObject, shownUpTo } from '../../register/findings.js';
|
|
12
19
|
import { registerFor } from '../../register/picture.js';
|
|
13
20
|
import { activeProject, readProjectFile } from '../../register/store.js';
|
|
21
|
+
import { shown } from '../../register/atc-file.js';
|
|
22
|
+
import { journeyPage } from '../../journey/flow.js';
|
|
14
23
|
const CAP = 200;
|
|
24
|
+
/** One page when `after` or `limit` is given. */
|
|
25
|
+
const PAGE = 40;
|
|
15
26
|
/** A decision's reason was written on another laptop and goes to the model: cleaned like a finding title, kept whole up to 300. */
|
|
16
27
|
const REASON_CAP = 300;
|
|
28
|
+
/**
|
|
29
|
+
* Guided journey (Task 7): the triage skill tells the person to record a row
|
|
30
|
+
* "your way with `cspeach team decide`". In the journey the person types no
|
|
31
|
+
* command, and the skill is not reseeded in this slice, so the page the
|
|
32
|
+
* journey hands out carries this note for the model.
|
|
33
|
+
*/
|
|
34
|
+
export const JOURNEY_NOTE = "This triage is one step of CSPeach's guided clean-core run. The person types no commands: "
|
|
35
|
+
+ 'never suggest a cspeach command (not cspeach team decide, not any other). Say instead that CSPeach asks about each row, '
|
|
36
|
+
+ 'and a row skipped is asked again later. Use plain words: "your objects", "your project" — never team, lane, outbox or writer.';
|
|
17
37
|
const match = (pattern, value) => new RegExp(`^${pattern.trim().replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i').test(value);
|
|
18
|
-
|
|
38
|
+
function usageOf(u) {
|
|
39
|
+
return {
|
|
40
|
+
state: u.state,
|
|
41
|
+
...(u.source ? { source: shownUpTo(u.source, 60) } : {}),
|
|
42
|
+
...(u.period ? { period: { from: shownUpTo(u.period.from, 30), to: shownUpTo(u.period.to, 30) } } : {}),
|
|
43
|
+
...(u.lastUsed ? { lastUsed: shownUpTo(u.lastUsed, 30) } : {}),
|
|
44
|
+
...(u.why ? { why: shownUpTo(u.why, REASON_CAP) } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** A number only when the index was read; otherwise why not, in words. */
|
|
48
|
+
function callersOf(c) {
|
|
49
|
+
if (!c)
|
|
50
|
+
return 'not read: no estate inventory for this object';
|
|
51
|
+
return c.read ? c.objects : `not read: ${shownUpTo(c.why, REASON_CAP)}`;
|
|
52
|
+
}
|
|
53
|
+
export async function teamFindingsHandler(args, ctx) {
|
|
19
54
|
const joined = activeProject();
|
|
20
55
|
if (!joined)
|
|
21
56
|
return { content: 'This laptop has not joined a team project. The developer runs: cspeach team join <folder>', is_error: true };
|
|
@@ -37,8 +72,28 @@ export async function teamFindingsHandler(args, _ctx) {
|
|
|
37
72
|
objects = objects.filter((o) => patterns.some((p) => match(p, o.package)));
|
|
38
73
|
scope = `packages ${patterns.join(', ')}`;
|
|
39
74
|
}
|
|
75
|
+
// A journey triage sees only the page the journey handed out.
|
|
76
|
+
const page = journeyPage(ctx?.session);
|
|
77
|
+
if (page) {
|
|
78
|
+
const keys = new Set(page.map((k) => k.toUpperCase()));
|
|
79
|
+
objects = objects.filter((o) => keys.has(o.key.toUpperCase()));
|
|
80
|
+
}
|
|
81
|
+
// An empty after is no after.
|
|
82
|
+
const paged = (typeof args.after === 'string' && args.after.trim() !== '') || args.limit !== undefined;
|
|
83
|
+
let start = 0;
|
|
84
|
+
if (typeof args.after === 'string' && args.after.trim()) {
|
|
85
|
+
const after = args.after.trim().toUpperCase();
|
|
86
|
+
const at = objects.findIndex((o) => o.key.toUpperCase() === after);
|
|
87
|
+
if (at === -1)
|
|
88
|
+
return { content: `There is no object ${shown(args.after)} in this scope. Leave out after to start at the first.`, is_error: true };
|
|
89
|
+
start = at + 1;
|
|
90
|
+
}
|
|
91
|
+
const limit = !paged ? CAP
|
|
92
|
+
: Number.isFinite(args.limit) ? Math.min(CAP, Math.max(1, Math.floor(args.limit))) : PAGE;
|
|
93
|
+
const slice = objects.slice(start, start + limit);
|
|
94
|
+
const more = objects.length - start - slice.length;
|
|
40
95
|
const findings = findingsByObject(joined.projectDir, joined.outboxDir);
|
|
41
|
-
const
|
|
96
|
+
const list = slice.map((o) => {
|
|
42
97
|
const f = findings.get(o.key);
|
|
43
98
|
const d = dispositionOf(o.decision);
|
|
44
99
|
return {
|
|
@@ -46,9 +101,23 @@ export async function teamFindingsHandler(args, _ctx) {
|
|
|
46
101
|
owner: register.lanes.find((l) => l.id === o.lane)?.owner ?? null,
|
|
47
102
|
decided: d ? { disposition: DISPOSITION_WORD[d], level: o.decision.level ?? null, targetLevel: o.decision.targetLevel ?? null, reason: typeof o.decision.reason === 'string' ? shownUpTo(o.decision.reason, REASON_CAP) : null } : null,
|
|
48
103
|
baselineAt: f?.baselineAt ?? null, baseline: f?.baseline ?? [], openNow: o.openFindings,
|
|
104
|
+
// Evidence (spec 2026-10-06 §10): usage only with its source, callers only when the index was read,
|
|
105
|
+
// and when the code last CHANGED — which is never use.
|
|
106
|
+
usage: usageOf(o.evidence.usage),
|
|
107
|
+
callers: callersOf(o.evidence.callers),
|
|
108
|
+
lastChanged: o.evidence.changed?.at ? shownUpTo(o.evidence.changed.at, 30) : null,
|
|
109
|
+
retireCandidate: o.retireCandidate,
|
|
110
|
+
priority: { band: o.priority.effective.band, source: o.priority.effective.source },
|
|
49
111
|
};
|
|
50
112
|
});
|
|
51
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
content: JSON.stringify({
|
|
115
|
+
teamProject: project.name, scope, objects: list,
|
|
116
|
+
...(page ? { journey: JOURNEY_NOTE } : {}),
|
|
117
|
+
...(more > 0 ? { more } : {}),
|
|
118
|
+
...(paged && more > 0 && slice.length ? { next: slice[slice.length - 1].key } : {}),
|
|
119
|
+
}),
|
|
120
|
+
};
|
|
52
121
|
}
|
|
53
122
|
catch (e) {
|
|
54
123
|
return { content: `The team project could not be read: ${e instanceof Error ? e.message : String(e)}`, is_error: true };
|
|
@@ -57,7 +126,9 @@ export async function teamFindingsHandler(args, _ctx) {
|
|
|
57
126
|
registerTool({
|
|
58
127
|
name: 'team_findings',
|
|
59
128
|
description: 'Read-only. The objects of a lane (or package patterns) in the team project this laptop joined, with the findings '
|
|
60
|
-
+ 'measurement zero recorded for each
|
|
129
|
+
+ 'measurement zero recorded for each, any decision already recorded, and per object: usage (state and source), callers from the '
|
|
130
|
+
+ 'index (or why they were not read), last change (a change, not use), whether it is a retire candidate, and its priority. '
|
|
131
|
+
+ 'Use it for a /abap-clean-core triage. No SAP call.',
|
|
61
132
|
isMutating: false,
|
|
62
133
|
category: 'project',
|
|
63
134
|
requiresSap: false,
|
|
@@ -66,6 +137,8 @@ registerTool({
|
|
|
66
137
|
properties: {
|
|
67
138
|
lane: { type: 'string', description: 'Lane id, e.g. "fin".' },
|
|
68
139
|
packages: { type: 'string', description: 'Package patterns, comma-separated, e.g. "ZCC_DEMO_*". Ignored when lane is given.' },
|
|
140
|
+
after: { type: 'string', description: 'Page on: the key "TYPE|NAME" of the last object seen (the `next` of the previous page).' },
|
|
141
|
+
limit: { type: 'number', description: 'Objects per page, 1 to 200 (default 40 when paging). Leave out both after and limit for the first 200.' },
|
|
69
142
|
},
|
|
70
143
|
},
|
|
71
144
|
handler: (args, ctx) => teamFindingsHandler(args, ctx),
|