cawdev-cli 1.0.11-beta → 1.1.1
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/README.md +11 -1
- package/lib/finish-check.mjs +34 -1
- package/lib/harness-prompt.mjs +6 -0
- package/lib/order.mjs +7 -0
- package/lib/protected.mjs +225 -0
- package/lib/roadmap-format.mjs +25 -10
- package/lib/runner-config.mjs +9 -0
- package/lib/secrets.mjs +2 -2
- package/lib/sonar-analysis.mjs +52 -0
- package/lib/sonarqube-mcp.mjs +50 -0
- package/lib/stage-tools.mjs +54 -1
- package/mcp/README.md +48 -36
- package/mcp/server.mjs +263 -72
- package/package.json +1 -1
- package/runner/README.md +101 -25
- package/runner/attach.mjs +46 -1
- package/runner/cawdev.mjs +20 -2
- package/runner/configure.mjs +62 -0
- package/runner/git-host.mjs +298 -5
- package/runner/pause-note.mjs +104 -0
- package/runner/push-guard.mjs +333 -0
- package/runner/runner.mjs +675 -177
- package/runner/split.mjs +1 -1
package/mcp/server.mjs
CHANGED
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
summaryOf,
|
|
22
22
|
withinCeiling,
|
|
23
23
|
} from '../lib/tool-rules.mjs';
|
|
24
|
+
import {
|
|
25
|
+
isProtected, notAllowed, protectedChanges, protectedPolicy, protectedRefusal, relativeTo,
|
|
26
|
+
} from '../lib/protected.mjs';
|
|
24
27
|
import { findDestructive, withinScope } from '../lib/secrets.mjs';
|
|
28
|
+
import { byCodeUnit } from '../lib/order.mjs';
|
|
25
29
|
import { COMMITS_ITS_WORK, isUnfinished, leftOver, notDoneYet } from '../lib/finish-check.mjs';
|
|
26
30
|
|
|
27
31
|
const NAME = 'cawdev';
|
|
@@ -375,6 +379,41 @@ const TOOLS = [
|
|
|
375
379
|
},
|
|
376
380
|
},
|
|
377
381
|
|
|
382
|
+
{
|
|
383
|
+
name: 'sprint_list',
|
|
384
|
+
description:
|
|
385
|
+
'The sprints a project\'s cards can be filed into, with how far each has got — R489. Sprints ' +
|
|
386
|
+
'are a team\'s: written `S7` inside the team and `Core team · S7` outside it. Read this before ' +
|
|
387
|
+
'filing a card into one with roadmap_create / roadmap_update `sprint`. State narrows it ' +
|
|
388
|
+
'(ACTIVE, UPCOMING, COMPLETED; comma-separated); the default is ACTIVE and UPCOMING.',
|
|
389
|
+
inputSchema: {
|
|
390
|
+
type: 'object',
|
|
391
|
+
properties: {
|
|
392
|
+
...PROJECT_ARGUMENT,
|
|
393
|
+
state: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
description: 'ACTIVE, UPCOMING, COMPLETED — one or several, comma-separated. Default ACTIVE,UPCOMING.',
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
handler: async (config, args) => {
|
|
400
|
+
const slug = await resolveProject(config, args.project);
|
|
401
|
+
const state = args.state || 'ACTIVE,UPCOMING';
|
|
402
|
+
const sprints = await api(config, `/api/projects/${slug}/sprints?state=${encodeURIComponent(state)}`);
|
|
403
|
+
if (!sprints.length) return `${slug} has no sprints in ${state}.`;
|
|
404
|
+
return sprints
|
|
405
|
+
.map((sprint) => {
|
|
406
|
+
const counts = sprint.counts ?? {};
|
|
407
|
+
return (
|
|
408
|
+
`${sprint.label ?? sprint.ref}${sprint.name ? ` ${sprint.name}` : ''} — ` +
|
|
409
|
+
`${String(sprint.state).toLowerCase()} — ${counts.landed ?? 0} of ${counts.cards ?? 0} landed` +
|
|
410
|
+
(sprint.goal ? `\n goal: ${sprint.goal}` : '')
|
|
411
|
+
);
|
|
412
|
+
})
|
|
413
|
+
.join('\n');
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
|
|
378
417
|
{
|
|
379
418
|
name: 'roadmap_statuses',
|
|
380
419
|
description:
|
|
@@ -396,18 +435,17 @@ const TOOLS = [
|
|
|
396
435
|
{
|
|
397
436
|
name: 'roadmap_list',
|
|
398
437
|
description:
|
|
399
|
-
'A project\'s roadmap
|
|
400
|
-
'to survey without pulling every
|
|
438
|
+
'A project\'s roadmap cards, optionally filtered by status or by sprint. Use brief=true ' +
|
|
439
|
+
'to survey without pulling every card\'s body into context.',
|
|
401
440
|
inputSchema: {
|
|
402
441
|
type: 'object',
|
|
403
442
|
properties: {
|
|
404
443
|
...PROJECT_ARGUMENT,
|
|
405
444
|
sprint: {
|
|
406
|
-
type: 'integer',
|
|
407
|
-
minimum: 1,
|
|
445
|
+
type: ['integer', 'string'],
|
|
408
446
|
description:
|
|
409
|
-
'Only cards in this sprint,
|
|
410
|
-
'sprint for is refused naming the ones it has.',
|
|
447
|
+
'Only cards in this sprint: `S7` (or 7), "Core team · S7" when two teams have an S7, or a ' +
|
|
448
|
+
'sprint id. A sprint the project has none for is refused naming the ones it has.',
|
|
411
449
|
},
|
|
412
450
|
status: {
|
|
413
451
|
type: 'string',
|
|
@@ -439,7 +477,9 @@ const TOOLS = [
|
|
|
439
477
|
if (args.status) query.set('status', args.status);
|
|
440
478
|
// R257. The API refuses a number with no sprint behind it, naming the
|
|
441
479
|
// sprints there are; that sentence is passed through as the tool error.
|
|
442
|
-
if (args.sprint
|
|
480
|
+
if (args.sprint !== undefined && args.sprint !== '' && args.sprint !== 0) {
|
|
481
|
+
query.set('sprint', String(args.sprint));
|
|
482
|
+
}
|
|
443
483
|
if (args.assignee) query.set('assignee', args.assignee);
|
|
444
484
|
query.set('brief', String(brief));
|
|
445
485
|
|
|
@@ -454,7 +494,7 @@ const TOOLS = [
|
|
|
454
494
|
{
|
|
455
495
|
name: 'roadmap_get',
|
|
456
496
|
description:
|
|
457
|
-
'One roadmap
|
|
497
|
+
'One roadmap card in full: its body, THE PLAN somebody agreed for it, and the ' +
|
|
458
498
|
'discussion on it. Read the plan before doing any of the work — it is what was decided, ' +
|
|
459
499
|
'and it names the files. Read the comments before proposing anything: they are where an ' +
|
|
460
500
|
'objection was answered, and re-proposing what was talked out three months ago is the ' +
|
|
@@ -594,7 +634,7 @@ const TOOLS = [
|
|
|
594
634
|
{
|
|
595
635
|
name: 'roadmap_comment',
|
|
596
636
|
description:
|
|
597
|
-
'Say something about
|
|
637
|
+
'Say something about a card, beside the card rather than inside it. Use it for the ' +
|
|
598
638
|
'argument: an objection, a measurement, why an obvious approach was not taken. The body ' +
|
|
599
639
|
'is where a settled conclusion is written down — edit that when the discussion reaches ' +
|
|
600
640
|
'one. Comments cannot be deleted, by you or by anyone.',
|
|
@@ -680,12 +720,12 @@ const TOOLS = [
|
|
|
680
720
|
{
|
|
681
721
|
name: 'roadmap_create',
|
|
682
722
|
description:
|
|
683
|
-
'Create a roadmap
|
|
684
|
-
'a roadmap card and i91 on an issue, one
|
|
723
|
+
'Create a roadmap card. The platform allocates its permanent number — written R91 on ' +
|
|
724
|
+
'a roadmap card and i91 on an issue, two sequences, one per kind — and the ' +
|
|
685
725
|
'project decides where a card you file starts — the BACKLOG by default for a card an ' +
|
|
686
726
|
'agent files, waiting until a person or a sprint brings it in — unless you say ' +
|
|
687
727
|
'otherwise; a status that requires something must be given it. A card in the BACKLOG ' +
|
|
688
|
-
'holds no sprint: giving it one moves it to CONSIDERING. A plan
|
|
728
|
+
'holds no sprint: giving it one moves it to CONSIDERING. A plan run\'s card lands ' +
|
|
689
729
|
'where the console\'s do (R380).',
|
|
690
730
|
inputSchema: {
|
|
691
731
|
type: 'object',
|
|
@@ -714,12 +754,12 @@ const TOOLS = [
|
|
|
714
754
|
reason: { type: 'string' },
|
|
715
755
|
section: { type: 'string', description: 'Which part of the roadmap, e.g. "Phase 2 — …".' },
|
|
716
756
|
sprint: {
|
|
717
|
-
type: 'integer',
|
|
718
|
-
minimum: 0,
|
|
757
|
+
type: ['integer', 'string'],
|
|
719
758
|
description:
|
|
720
|
-
'The sprint to file this card into
|
|
721
|
-
'
|
|
722
|
-
'
|
|
759
|
+
'The sprint to file this card into: `S7` (or 7) when the project\'s teams have one S7, ' +
|
|
760
|
+
'"Core team · S7" when two teams do, a sprint id, or 0 / "" to take it out of its sprint; ' +
|
|
761
|
+
'leave it out to leave it alone. A COMPLETED sprint refuses a card that is not already in it, ' +
|
|
762
|
+
'and only a person can reopen one — do not retry. Sprint_list shows what there is.',
|
|
723
763
|
},
|
|
724
764
|
related: {
|
|
725
765
|
type: 'array',
|
|
@@ -893,7 +933,9 @@ const TOOLS = [
|
|
|
893
933
|
'person should decide about: `kind` is your opinion, ISSUE (broken) or FEATURE (should ' +
|
|
894
934
|
'also do / do better). A WRITER later accepts it as a roadmap card, accepts it as an ' +
|
|
895
935
|
'issue, or refuses it with a reason. Use this when you are not sure it deserves a card; ' +
|
|
896
|
-
'use roadmap_create or issue_file when you are.'
|
|
936
|
+
'use roadmap_create or issue_file when you are. Inside a run, this is where something you ' +
|
|
937
|
+
'found that is NOT this card\'s goes — one item per suggestion: cawdev links it to this run ' +
|
|
938
|
+
'and its card, so you need not write either into the body.',
|
|
897
939
|
inputSchema: {
|
|
898
940
|
type: 'object',
|
|
899
941
|
properties: {
|
|
@@ -929,7 +971,7 @@ const TOOLS = [
|
|
|
929
971
|
{
|
|
930
972
|
name: 'roadmap_update',
|
|
931
973
|
description:
|
|
932
|
-
'Edit
|
|
974
|
+
'Edit a card\'s title, body, section, sprint, tags, parent, related refs or the cards it ' +
|
|
933
975
|
'starts after. Use roadmap_set_status to move it.',
|
|
934
976
|
inputSchema: {
|
|
935
977
|
type: 'object',
|
|
@@ -943,12 +985,12 @@ const TOOLS = [
|
|
|
943
985
|
body: { type: 'string' },
|
|
944
986
|
section: { type: 'string' },
|
|
945
987
|
sprint: {
|
|
946
|
-
type: 'integer',
|
|
947
|
-
minimum: 0,
|
|
988
|
+
type: ['integer', 'string'],
|
|
948
989
|
description:
|
|
949
|
-
'The sprint to file this card into
|
|
950
|
-
'
|
|
951
|
-
'
|
|
990
|
+
'The sprint to file this card into: `S7` (or 7) when the project\'s teams have one S7, ' +
|
|
991
|
+
'"Core team · S7" when two teams do, a sprint id, or 0 / "" to take it out of its sprint; ' +
|
|
992
|
+
'leave it out to leave it alone. A COMPLETED sprint refuses a card that is not already in it, ' +
|
|
993
|
+
'and only a person can reopen one — do not retry. Sprint_list shows what there is.',
|
|
952
994
|
},
|
|
953
995
|
related: {
|
|
954
996
|
type: 'array',
|
|
@@ -1027,7 +1069,7 @@ const TOOLS = [
|
|
|
1027
1069
|
{
|
|
1028
1070
|
name: 'roadmap_set_status',
|
|
1029
1071
|
description:
|
|
1030
|
-
'Move
|
|
1072
|
+
'Move a card to a status. Any status may move to any other — the rules are about what ' +
|
|
1031
1073
|
'a status must carry, not a permitted path. IN_DEVELOPMENT needs a branch, MERGED the ' +
|
|
1032
1074
|
'merge — the PR, the merge commit or the sha — and SHIPPED a version. Move a card to ' +
|
|
1033
1075
|
'MERGED when its pull request lands; the branch may then be deleted. ' +
|
|
@@ -1076,7 +1118,7 @@ const TOOLS = [
|
|
|
1076
1118
|
{
|
|
1077
1119
|
name: 'roadmap_decline',
|
|
1078
1120
|
description:
|
|
1079
|
-
'Decline
|
|
1121
|
+
'Decline a card, with a reason. This is the only exit a card has — there is no delete. ' +
|
|
1080
1122
|
'The reason is the point: it stops the idea being proposed again.',
|
|
1081
1123
|
inputSchema: {
|
|
1082
1124
|
type: 'object',
|
|
@@ -1153,10 +1195,10 @@ const TOOLS = [
|
|
|
1153
1195
|
entryNumber: {
|
|
1154
1196
|
type: ['string', 'integer'],
|
|
1155
1197
|
description:
|
|
1156
|
-
'The roadmap card or issue this entry describes — R156 — by ref: R91, i91, or a '
|
|
1198
|
+
'The roadmap card or issue this changelog entry describes — R156 — by ref: R91, i91, or a '
|
|
1157
1199
|
+ 'bare number for a roadmap card (R221). Set it. Until R156 the only link was the '
|
|
1158
1200
|
+ 'ref you type into the prose, which cannot be queried, so a release could not '
|
|
1159
|
-
+ 'tell whether a card already had
|
|
1201
|
+
+ 'tell whether a card already had a changelog entry and would write a second one beside '
|
|
1160
1202
|
+ 'the first. Put the ref in the text as well; that is what a reader sees.',
|
|
1161
1203
|
},
|
|
1162
1204
|
},
|
|
@@ -1189,8 +1231,8 @@ const TOOLS = [
|
|
|
1189
1231
|
entryNumber: {
|
|
1190
1232
|
type: ['string', 'integer'],
|
|
1191
1233
|
description:
|
|
1192
|
-
'The roadmap card or issue this entry describes — R156 — by ref (R91, i91; a bare '
|
|
1193
|
-
+ 'number is a roadmap card). Set it on an older entry that has never been linked; '
|
|
1234
|
+
'The roadmap card or issue this changelog entry describes — R156 — by ref (R91, i91; a bare '
|
|
1235
|
+
+ 'number is a roadmap card). Set it on an older changelog entry that has never been linked; '
|
|
1194
1236
|
+ 'omitting it leaves whatever link is already there.',
|
|
1195
1237
|
},
|
|
1196
1238
|
},
|
|
@@ -1211,9 +1253,9 @@ const TOOLS = [
|
|
|
1211
1253
|
{
|
|
1212
1254
|
name: 'task_current',
|
|
1213
1255
|
description:
|
|
1214
|
-
'What you are working on: the roadmap
|
|
1256
|
+
'What you are working on: the roadmap card, THE PLAN agreed for it, its branch, and ' +
|
|
1215
1257
|
'everything already said and asked on this run. Call it first, and again whenever you ' +
|
|
1216
|
-
'are unsure where you are — a resumed or confused session re-orients from this alone.',
|
|
1258
|
+
'are unsure where you are — a resumed or confused agent session re-orients from this alone.',
|
|
1217
1259
|
inputSchema: { type: 'object', properties: {} },
|
|
1218
1260
|
handler: async (config) => {
|
|
1219
1261
|
const { runId, project } = await requireRun(config);
|
|
@@ -1408,6 +1450,14 @@ const TOOLS = [
|
|
|
1408
1450
|
return notDoneYet(left);
|
|
1409
1451
|
}
|
|
1410
1452
|
}
|
|
1453
|
+
// R562. The diff gate: the branch against the commit it was cut from,
|
|
1454
|
+
// so a shell `git checkout main -- pom.xml` is caught as an Edit is.
|
|
1455
|
+
if (run && COMMITS_ITS_WORK.has(run.profile)) {
|
|
1456
|
+
const refusal = await protectedGate(config, project, runId);
|
|
1457
|
+
if (refusal) {
|
|
1458
|
+
return refusal;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1411
1461
|
}
|
|
1412
1462
|
|
|
1413
1463
|
const message = await api(config, `/api/projects/${project}/runs/${runId}/messages`, {
|
|
@@ -1435,20 +1485,20 @@ const TOOLS = [
|
|
|
1435
1485
|
{
|
|
1436
1486
|
name: 'propose_entry',
|
|
1437
1487
|
description:
|
|
1438
|
-
'Record something an audit found, or a card a scoping
|
|
1488
|
+
'Record something an audit found, or a card a scoping run cut, as a proposed card. ' +
|
|
1439
1489
|
'`kind` is your opinion: "issue" ' +
|
|
1440
1490
|
'(broken, unsafe, or loses data — with a severity) or "roadmap" (it should also do this, ' +
|
|
1441
1491
|
'or do it better — no severity). A PERSON decides, and may file it the other way — you ' +
|
|
1442
|
-
'are not creating
|
|
1492
|
+
'are not creating a card, you are suggesting it. Default "issue", so an audit written ' +
|
|
1443
1493
|
'before there was a choice files what it always did. Severity is "critical" (broken, ' +
|
|
1444
1494
|
'unsafe, or loses data), "medium" (it will hurt, but not today) or "minor" (worth doing, ' +
|
|
1445
|
-
'nobody is bleeding). `section` is where you think it belongs; a scoping
|
|
1495
|
+
'nobody is bleeding). `section` is where you think it belongs; a scoping run names ' +
|
|
1446
1496
|
'ONE for every card of the idea. A roadmap card MUST say what it starts after — `after`: ' +
|
|
1447
1497
|
'`[]` for a card that starts after none, `"#2"` for the second card you proposed in this ' +
|
|
1448
1498
|
'run (the number this tool answered with), `"R12"` for a card already on the roadmap. ' +
|
|
1449
1499
|
'You can only name what has already been proposed, so file in build order. Write each ' +
|
|
1450
|
-
'one as
|
|
1451
|
-
'or a scoping
|
|
1500
|
+
'one as a card would be written, and say where in the code you saw it. Only an audit ' +
|
|
1501
|
+
'or a scoping run may use this.',
|
|
1452
1502
|
inputSchema: {
|
|
1453
1503
|
type: 'object',
|
|
1454
1504
|
properties: {
|
|
@@ -1473,7 +1523,7 @@ const TOOLS = [
|
|
|
1473
1523
|
type: 'string',
|
|
1474
1524
|
description:
|
|
1475
1525
|
'The roadmap section this belongs under, in your opinion. Optional. The person ' +
|
|
1476
|
-
'accepting sees it as the default and may file it elsewhere. A scoping
|
|
1526
|
+
'accepting sees it as the default and may file it elsewhere. A scoping run ' +
|
|
1477
1527
|
'uses the same section on every card it cuts from one idea.',
|
|
1478
1528
|
},
|
|
1479
1529
|
after: {
|
|
@@ -1485,7 +1535,7 @@ const TOOLS = [
|
|
|
1485
1535
|
'card you proposed in THIS run, by the number this tool answered with; it must ' +
|
|
1486
1536
|
'already have been proposed, so propose what a card depends on before the card. ' +
|
|
1487
1537
|
'`"R12"` or `"i7"` names a card already on the roadmap. A person adds the cards ' +
|
|
1488
|
-
'in this order, and a coding
|
|
1538
|
+
'in this order, and a coding run on one waits until what it starts after has ' +
|
|
1489
1539
|
'landed.',
|
|
1490
1540
|
},
|
|
1491
1541
|
},
|
|
@@ -1565,7 +1615,7 @@ const TOOLS = [
|
|
|
1565
1615
|
name: 'ask_group',
|
|
1566
1616
|
description:
|
|
1567
1617
|
'Ask a ROUND of questions at once, and wait for all of them. Use it when you have several ' +
|
|
1568
|
-
'things to settle that belong together — they arrive in the person\'s
|
|
1618
|
+
'things to settle that belong together — they arrive in the person\'s Waiting on you page under one ' +
|
|
1569
1619
|
'title, on one form, instead of interrupting them once per question. At most 12 in a ' +
|
|
1570
1620
|
'round; ask the rest in the next one. Blocks for up to ten minutes; if the round is ' +
|
|
1571
1621
|
'unfinished by then it returns a group_id for await_group.',
|
|
@@ -1575,7 +1625,7 @@ const TOOLS = [
|
|
|
1575
1625
|
title: {
|
|
1576
1626
|
type: 'string',
|
|
1577
1627
|
description:
|
|
1578
|
-
'What this round is called in
|
|
1628
|
+
'What this round is called in Waiting on you. A CTO Interview uses "CTO Interview".',
|
|
1579
1629
|
},
|
|
1580
1630
|
intro: {
|
|
1581
1631
|
type: 'string',
|
|
@@ -1705,7 +1755,7 @@ const TOOLS = [
|
|
|
1705
1755
|
name: 'await_answer',
|
|
1706
1756
|
description:
|
|
1707
1757
|
'Resume waiting for a question ask_user handed back. Between the two you experience one ' +
|
|
1708
|
-
'natural blocking ask; the person experiences
|
|
1758
|
+
'natural blocking ask; the person experiences a Waiting on you item.',
|
|
1709
1759
|
inputSchema: {
|
|
1710
1760
|
type: 'object',
|
|
1711
1761
|
properties: { question_id: { type: 'string' } },
|
|
@@ -1784,7 +1834,49 @@ async function decide(config, args) {
|
|
|
1784
1834
|
// R110. The shield FIRST, and the ordering is the point — see shieldSays.
|
|
1785
1835
|
// A hit does not deny; it skips the rules and goes straight to a person,
|
|
1786
1836
|
// and it is recorded so somebody can see what was stopped.
|
|
1787
|
-
const
|
|
1837
|
+
const policy = shieldPolicy();
|
|
1838
|
+
|
|
1839
|
+
// R562. A protected file, BEFORE the shield and the rules: no stored rule
|
|
1840
|
+
// may cover it, and no session grant either. A path already allowed on this
|
|
1841
|
+
// branch goes straight through; otherwise REFUSE denies and ASK raises a
|
|
1842
|
+
// request that says plainly it is about a protected file.
|
|
1843
|
+
const target = input.file_path ?? input.path ?? input.notebook_path;
|
|
1844
|
+
if (target && policy.protected.globs.length && WRITE_TOOLS.has(toolName)
|
|
1845
|
+
&& isProtected(String(target), policy.root, policy.protected.globs)) {
|
|
1846
|
+
const relative = relativeTo(String(target), policy.root) ?? String(target);
|
|
1847
|
+
const allowed = await protectedAllowedNow(config, project, runId, policy.protected);
|
|
1848
|
+
if (allowed.has(relative)) {
|
|
1849
|
+
return allow(input, 'a person allowed this protected file');
|
|
1850
|
+
}
|
|
1851
|
+
await api(config, `/api/projects/${project}/runs/${runId}/blocks`, {
|
|
1852
|
+
method: 'POST',
|
|
1853
|
+
body: { kind: 'PROTECTED', detail: `${toolName} ${relative}` },
|
|
1854
|
+
}).catch(() => {
|
|
1855
|
+
// Bookkeeping, as for the shield below.
|
|
1856
|
+
});
|
|
1857
|
+
if (policy.protected.mode !== 'ASK') {
|
|
1858
|
+
return deny(
|
|
1859
|
+
`${relative} is protected in this project: an agent never changes it. Do not work ` +
|
|
1860
|
+
`around it (no shell, no git checkout). Report blocked if the work needs it.`,
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
const asked = await api(config, `/api/projects/${project}/runs/${runId}/approvals`, {
|
|
1864
|
+
method: 'POST',
|
|
1865
|
+
body: {
|
|
1866
|
+
toolName,
|
|
1867
|
+
toolInput: JSON.stringify(input),
|
|
1868
|
+
summary: `PROTECTED FILE — ${summaryOf(toolName, input)}`,
|
|
1869
|
+
suggestion: null,
|
|
1870
|
+
exactPattern: null,
|
|
1871
|
+
exactWithinCeiling: false,
|
|
1872
|
+
toolUseId: args.tool_use_id,
|
|
1873
|
+
protectedPaths: [relative],
|
|
1874
|
+
},
|
|
1875
|
+
});
|
|
1876
|
+
return await answerOf(config, project, runId, asked.id, input);
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
const stopped = shieldSays(policy, toolName, input);
|
|
1788
1880
|
if (stopped) {
|
|
1789
1881
|
await api(config, `/api/projects/${project}/runs/${runId}/blocks`, {
|
|
1790
1882
|
method: 'POST',
|
|
@@ -1857,26 +1949,7 @@ async function decide(config, args) {
|
|
|
1857
1949
|
},
|
|
1858
1950
|
});
|
|
1859
1951
|
|
|
1860
|
-
|
|
1861
|
-
if (decided?.state === 'ALLOWED') {
|
|
1862
|
-
return allow(input, decided.reason ?? 'allowed by a person');
|
|
1863
|
-
}
|
|
1864
|
-
if (decided?.state === 'DENIED') {
|
|
1865
|
-
return deny(
|
|
1866
|
-
`${decided.reason ?? 'A person refused this.'} Do not try to work around it — report ` +
|
|
1867
|
-
`blocked and say what you needed, or ask for a different approach.`,
|
|
1868
|
-
);
|
|
1869
|
-
}
|
|
1870
|
-
// EXPIRED, or the poll gave up before the platform expired it. Same answer
|
|
1871
|
-
// either way, and the difference is on the run for whoever reads it later.
|
|
1872
|
-
// R339: a person may still allow it late, and if they do the session is
|
|
1873
|
-
// sent a message saying so — so this says to carry on, not to stop.
|
|
1874
|
-
return deny(
|
|
1875
|
-
`Nobody answered this permission request in time. Do not retry it in a loop. Carry on ` +
|
|
1876
|
-
`with whatever does not need it; if a person allows it later you will be sent a ` +
|
|
1877
|
-
`message saying so, and may run it then. If nothing else can be done without it, ` +
|
|
1878
|
-
`report blocked and say exactly what you needed to run and why.`,
|
|
1879
|
-
);
|
|
1952
|
+
return await answerOf(config, project, runId, asked.id, input);
|
|
1880
1953
|
} catch (failure) {
|
|
1881
1954
|
return deny(
|
|
1882
1955
|
`cawdev could not be asked whether this is allowed (${failure.message}). Treating that as ` +
|
|
@@ -1885,6 +1958,104 @@ async function decide(config, args) {
|
|
|
1885
1958
|
}
|
|
1886
1959
|
}
|
|
1887
1960
|
|
|
1961
|
+
/** The tools whose path is a file they change; Read, Grep and Glob carry a path too. */
|
|
1962
|
+
const WRITE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit']);
|
|
1963
|
+
|
|
1964
|
+
/** Waits for the person and turns the decision into the answer the CLI reads. */
|
|
1965
|
+
async function answerOf(config, project, runId, approvalId, input) {
|
|
1966
|
+
const decided = await pollForDecision(config, project, runId, approvalId);
|
|
1967
|
+
if (decided?.state === 'ALLOWED') {
|
|
1968
|
+
return allow(input, decided.reason ?? 'allowed by a person');
|
|
1969
|
+
}
|
|
1970
|
+
if (decided?.state === 'DENIED') {
|
|
1971
|
+
return deny(
|
|
1972
|
+
`${decided.reason ?? 'A person refused this.'} Do not try to work around it — report ` +
|
|
1973
|
+
`blocked and say what you needed, or ask for a different approach.`,
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
// EXPIRED, or the poll gave up before the platform expired it. Same answer
|
|
1977
|
+
// either way, and the difference is on the run for whoever reads it later.
|
|
1978
|
+
// R339: a person may still allow it late, and if they do the session is
|
|
1979
|
+
// sent a message saying so — so this says to carry on, not to stop.
|
|
1980
|
+
return deny(
|
|
1981
|
+
`Nobody answered this permission request in time. Do not retry it in a loop. Carry on ` +
|
|
1982
|
+
`with whatever does not need it; if a person allows it later you will be sent a ` +
|
|
1983
|
+
`message saying so, and may run it then. If nothing else can be done without it, ` +
|
|
1984
|
+
`report blocked and say exactly what you needed to run and why.`,
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* The protected paths a person has allowed on this branch — the claim's list
|
|
1990
|
+
* plus this run's own ALLOWED protected requests, read fresh (R562). Never
|
|
1991
|
+
* throws: not knowing what was allowed means nothing was.
|
|
1992
|
+
*/
|
|
1993
|
+
async function protectedAllowedNow(config, project, runId, policy) {
|
|
1994
|
+
const allowed = new Set(policy.allowed ?? []);
|
|
1995
|
+
try {
|
|
1996
|
+
const asked = await api(config, `/api/projects/${project}/runs/${runId}/approvals`);
|
|
1997
|
+
for (const approval of asked ?? []) {
|
|
1998
|
+
if (approval?.state === 'ALLOWED' && Array.isArray(approval.protectedPaths)) {
|
|
1999
|
+
approval.protectedPaths.forEach((path) => allowed.add(path));
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
} catch {
|
|
2003
|
+
// The claim's list stands.
|
|
2004
|
+
}
|
|
2005
|
+
return allowed;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
/**
|
|
2009
|
+
* The protected-file gate on `report done` — R562. Null when the branch is
|
|
2010
|
+
* clean of unallowed protected changes (or nothing can be judged), else the
|
|
2011
|
+
* refusal the session reads. In ASK mode a person is asked once, for all of
|
|
2012
|
+
* them, and ALLOWED lets the run finish.
|
|
2013
|
+
*/
|
|
2014
|
+
async function protectedGate(config, project, runId) {
|
|
2015
|
+
const policy = shieldPolicy().protected;
|
|
2016
|
+
const base = process.env.CAWDEV_PROTECTED_BASE || null;
|
|
2017
|
+
if (!policy.globs.length || !base) {
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
const root = process.env.CAWDEV_WORKSPACE ?? process.cwd();
|
|
2021
|
+
const changed = await protectedChanges(root, base, policy.globs);
|
|
2022
|
+
const allowed = await protectedAllowedNow(config, project, runId, policy);
|
|
2023
|
+
const held = notAllowed(changed, [...allowed]);
|
|
2024
|
+
if (!held.length) {
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
const list = held.join(', ');
|
|
2028
|
+
await api(config, `/api/projects/${project}/runs/${runId}/blocks`, {
|
|
2029
|
+
method: 'POST',
|
|
2030
|
+
body: { kind: 'PROTECTED', detail: `branch changes ${list}` },
|
|
2031
|
+
}).catch(() => {
|
|
2032
|
+
// Bookkeeping.
|
|
2033
|
+
});
|
|
2034
|
+
const refusal = protectedRefusal(held, base);
|
|
2035
|
+
if (policy.mode !== 'ASK') {
|
|
2036
|
+
return refusal;
|
|
2037
|
+
}
|
|
2038
|
+
const asked = await api(config, `/api/projects/${project}/runs/${runId}/approvals`, {
|
|
2039
|
+
method: 'POST',
|
|
2040
|
+
body: {
|
|
2041
|
+
toolName: 'ProtectedChange',
|
|
2042
|
+
toolInput: JSON.stringify({ paths: held }),
|
|
2043
|
+
summary: `Changes protected file(s): ${list}`,
|
|
2044
|
+
suggestion: null,
|
|
2045
|
+
exactPattern: null,
|
|
2046
|
+
exactWithinCeiling: false,
|
|
2047
|
+
protectedPaths: held,
|
|
2048
|
+
},
|
|
2049
|
+
});
|
|
2050
|
+
const decided = await pollForDecision(config, project, runId, asked.id);
|
|
2051
|
+
if (decided?.state === 'ALLOWED') {
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
return decided?.state === 'DENIED' && decided.reason
|
|
2055
|
+
? `${refusal} A person refused: ${decided.reason}`
|
|
2056
|
+
: refusal;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1888
2059
|
function allow(updatedInput, reason) {
|
|
1889
2060
|
// `updatedInput` echoed back unchanged. The hook exists to say yes or no,
|
|
1890
2061
|
// not to rewrite what the agent was about to do behind its back.
|
|
@@ -1977,10 +2148,18 @@ function shieldPolicy() {
|
|
|
1977
2148
|
blockSecrets: parsed.blockSecrets !== false,
|
|
1978
2149
|
blockDestructive: parsed.blockDestructive !== false,
|
|
1979
2150
|
pathScope: Array.isArray(parsed.pathScope) ? parsed.pathScope : null,
|
|
2151
|
+
// R562. Unreadable is REFUSE with no globs — never a way of allowing.
|
|
2152
|
+
protected: protectedPolicy(parsed),
|
|
1980
2153
|
root: process.env.CAWDEV_WORKSPACE ?? process.cwd(),
|
|
1981
2154
|
};
|
|
1982
2155
|
} catch {
|
|
1983
|
-
return {
|
|
2156
|
+
return {
|
|
2157
|
+
blockSecrets: true,
|
|
2158
|
+
blockDestructive: true,
|
|
2159
|
+
pathScope: null,
|
|
2160
|
+
protected: protectedPolicy(null),
|
|
2161
|
+
root: process.cwd(),
|
|
2162
|
+
};
|
|
1984
2163
|
}
|
|
1985
2164
|
}
|
|
1986
2165
|
|
|
@@ -2065,11 +2244,23 @@ function approvalTimeoutSeconds() {
|
|
|
2065
2244
|
// The same goes for comments — nothing here removes one, and nothing there
|
|
2066
2245
|
// does either.
|
|
2067
2246
|
//
|
|
2068
|
-
// Nor is there a sprint_open,
|
|
2069
|
-
//
|
|
2070
|
-
//
|
|
2071
|
-
//
|
|
2072
|
-
//
|
|
2247
|
+
// Nor is there a sprint_open, sprint_start, sprint_complete or sprint_rename —
|
|
2248
|
+
// R257, and a team's since R474. Opening, starting, completing and renaming a
|
|
2249
|
+
// sprint are a person's acts on the page, and the API says so structurally
|
|
2250
|
+
// (the /api/teams/{id}/sprints writes take a session, not a token). An
|
|
2251
|
+
// agent's whole reach here is reading the sprints (sprint_list) and a card's
|
|
2252
|
+
// sprint, and filing a card into one with roadmap_create / roadmap_update
|
|
2253
|
+
// `sprint`.
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* R489. A card's sprint as a line: `Core team · S7 Name (active)`. The label
|
|
2257
|
+
* is the server's one spelling; `S7` alone means nothing outside its team.
|
|
2258
|
+
*/
|
|
2259
|
+
function sprintLine(sprint) {
|
|
2260
|
+
const label = sprint.label ?? sprint.ref ?? `S${sprint.number}`;
|
|
2261
|
+
const state = String(sprint.state ?? '').toLowerCase();
|
|
2262
|
+
return `${label}${sprint.name ? ` ${sprint.name}` : ''}${state ? ` (${state})` : ''}`;
|
|
2263
|
+
}
|
|
2073
2264
|
|
|
2074
2265
|
function pick(source, keys) {
|
|
2075
2266
|
const out = {};
|
|
@@ -2318,7 +2509,7 @@ function originWord(origin) {
|
|
|
2318
2509
|
if (!origin || origin.kind === 'NONE') return null;
|
|
2319
2510
|
switch (origin.kind) {
|
|
2320
2511
|
case 'SONARQUBE': return 'SonarQube';
|
|
2321
|
-
case 'FEEDBACK': return 'Feedback';
|
|
2512
|
+
case 'FEEDBACK': return origin.label ? `Feedback (from ${origin.label})` : 'Feedback';
|
|
2322
2513
|
case 'APPLICATION': return origin.label ? `an application (${origin.label})` : 'an application';
|
|
2323
2514
|
case 'SESSION': return origin.label ? `a session (${origin.label})` : 'a session';
|
|
2324
2515
|
default: return null;
|
|
@@ -2343,7 +2534,7 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2343
2534
|
if (entry.section) lines.push(` section: ${entry.section}`);
|
|
2344
2535
|
// R315. Sorted for the same reason the export is: a reader's shape must not
|
|
2345
2536
|
// depend on the set's own insertion order.
|
|
2346
|
-
if (entry.tags?.length) lines.push(` tags: ${[...entry.tags].sort().map((t) => `#${t}`).join(' ')}`);
|
|
2537
|
+
if (entry.tags?.length) lines.push(` tags: ${[...entry.tags].sort(byCodeUnit).map((t) => `#${t}`).join(' ')}`);
|
|
2347
2538
|
// R331. A username, never the email.
|
|
2348
2539
|
if (entry.assignee) lines.push(` assignee: ${entry.assignee.username}`);
|
|
2349
2540
|
// R335. Not printed at MEDIUM, as the export does not write it.
|
|
@@ -2361,7 +2552,7 @@ function formatEntry(entry, { brief, comments, plan }) {
|
|
|
2361
2552
|
// depend on which tool printed the card; a closed sprint says so.
|
|
2362
2553
|
if (entry.sprint) {
|
|
2363
2554
|
lines.push(
|
|
2364
|
-
` sprint: ${entry.sprint
|
|
2555
|
+
` sprint: ${sprintLine(entry.sprint)}`,
|
|
2365
2556
|
);
|
|
2366
2557
|
}
|
|
2367
2558
|
if (entry.related?.length) {
|