@dzhechkov/harness-core 0.4.3 → 0.4.5

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/sbom.json CHANGED
@@ -25,7 +25,7 @@
25
25
  "hashes": [
26
26
  {
27
27
  "alg": "SHA-256",
28
- "content": "d3b77852a0bc48aa5d7c34bbd2baa2bbcf1bc73ea86e23b57a955c5ddf61cd03"
28
+ "content": "1ea94f4b5ca525e64959975bcd7ef4b258a9c7b83801f754694f42e3f640007a"
29
29
  }
30
30
  ]
31
31
  },
@@ -35,7 +35,17 @@
35
35
  "hashes": [
36
36
  {
37
37
  "alg": "SHA-256",
38
- "content": "967d5a6521a410643e8b4e7e18898ad67683f37ecb96879596646114e7085be0"
38
+ "content": "9cb242e309547951677c3e613c5ebfb75e4ba68c8fa8f748a9ad550a47a5c2d5"
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "type": "file",
44
+ "name": "coverage/coverage-summary.json",
45
+ "hashes": [
46
+ {
47
+ "alg": "SHA-256",
48
+ "content": "f04d97d74ed66db11c5f7659953befe681537ec395f2a95c7e279acef78db15d"
39
49
  }
40
50
  ]
41
51
  },
@@ -1455,7 +1465,7 @@
1455
1465
  "hashes": [
1456
1466
  {
1457
1467
  "alg": "SHA-256",
1458
- "content": "9288224684107b9712eec5592c3727788608e384ee4cf0899f27780ee2b668df"
1468
+ "content": "4752ad599537abe839adfa152188269e5e5ddfdff48020129e9f4a8e70e56983"
1459
1469
  }
1460
1470
  ]
1461
1471
  },
@@ -1465,7 +1475,7 @@
1465
1475
  "hashes": [
1466
1476
  {
1467
1477
  "alg": "SHA-256",
1468
- "content": "06617b763dc879d43f1320fdbb422fc4c61b47aea7656ca8a6508dcbe3a62778"
1478
+ "content": "991f56d541257496aa05faec761ea0615b2468142dc8debf910a699ce50b9602"
1469
1479
  }
1470
1480
  ]
1471
1481
  },
@@ -1475,7 +1485,7 @@
1475
1485
  "hashes": [
1476
1486
  {
1477
1487
  "alg": "SHA-256",
1478
- "content": "8c5d5828f4b0ba6dd32a15de8a3eae4e683dc8a9616d7d4cc71e07d693bfe427"
1488
+ "content": "fc721ccc00ee1ba6a0e943583631b46557b51dcc91c1c9da1e0b33c040804f9b"
1479
1489
  }
1480
1490
  ]
1481
1491
  },
@@ -1485,7 +1495,7 @@
1485
1495
  "hashes": [
1486
1496
  {
1487
1497
  "alg": "SHA-256",
1488
- "content": "717e974726fcd976254760be190c0bbf38be162aea4b1d3b48fe8992c9fff9bf"
1498
+ "content": "1ef7d4f55c4f6daa9cdb87540b85d9fee7dd41008317c488c97112448d8ada07"
1489
1499
  }
1490
1500
  ]
1491
1501
  },
@@ -1495,7 +1505,7 @@
1495
1505
  "hashes": [
1496
1506
  {
1497
1507
  "alg": "SHA-256",
1498
- "content": "06ae266e7fa0fa8a390b3a9c57a5edeb6ebc53204bf47220dec633f4d0b1d0ca"
1508
+ "content": "3fe080f4356b1933ecf4b492134e90787fc472c642b7c64c6cce84a51fabddd4"
1499
1509
  }
1500
1510
  ]
1501
1511
  },
@@ -1505,7 +1515,7 @@
1505
1515
  "hashes": [
1506
1516
  {
1507
1517
  "alg": "SHA-256",
1508
- "content": "e157eff3ef69105095d02cd0f63b7871fe6c9b57bfc0b12198c3074f08edd343"
1518
+ "content": "394f11cf13102edeb37c655f4796b15372c627b19814e6bc35a5b2fbe556e9b3"
1509
1519
  }
1510
1520
  ]
1511
1521
  },
@@ -1515,7 +1525,7 @@
1515
1525
  "hashes": [
1516
1526
  {
1517
1527
  "alg": "SHA-256",
1518
- "content": "2461e2f9984617a53f8d9b0bde1b7ec2f33f60f3763c1352242698848484994f"
1528
+ "content": "9cff4d5a9745387ae90795b825e19b00ed62db2e98665bf0ddaa5f12be0ee491"
1519
1529
  }
1520
1530
  ]
1521
1531
  },
@@ -1565,7 +1575,7 @@
1565
1575
  "hashes": [
1566
1576
  {
1567
1577
  "alg": "SHA-256",
1568
- "content": "25be50f7d13606c894880980eeb7cf989bb79eba53f19f138bafd6b2865984f5"
1578
+ "content": "6d5df8252d9da89efd7ec8bb17c2bfbb2686c49cc9fe33422e2e80288c76b857"
1569
1579
  }
1570
1580
  ]
1571
1581
  },
@@ -1575,7 +1585,7 @@
1575
1585
  "hashes": [
1576
1586
  {
1577
1587
  "alg": "SHA-256",
1578
- "content": "c675242b760698382b34576b6e0a29791a9bf90f8c11969f1c7b50dfdcdab090"
1588
+ "content": "396fd7bf4041f15d8bc037bd84e740ae4c89b218e2842f84aa2d3a9ef9dcfdfc"
1579
1589
  }
1580
1590
  ]
1581
1591
  },
@@ -1585,7 +1595,7 @@
1585
1595
  "hashes": [
1586
1596
  {
1587
1597
  "alg": "SHA-256",
1588
- "content": "49ca837d8041585638ea5b14ffcc913de77b92aa619512c9cdd2f92c7458e6d5"
1598
+ "content": "3c14f7c6225775224ef74e15a5e215efa5118058cf35e55bfd46706b847a9db2"
1589
1599
  }
1590
1600
  ]
1591
1601
  },
@@ -1595,7 +1605,7 @@
1595
1605
  "hashes": [
1596
1606
  {
1597
1607
  "alg": "SHA-256",
1598
- "content": "d8cea571b3c22d4e88700c043d138cbb7ce03cd7aaea682185c55010430249e2"
1608
+ "content": "5daf83a5ff7740b5dfac606747cd7f6a13dedf060aa8e149c7ecd3afe2201d5f"
1599
1609
  }
1600
1610
  ]
1601
1611
  },
@@ -1615,7 +1625,7 @@
1615
1625
  "hashes": [
1616
1626
  {
1617
1627
  "alg": "SHA-256",
1618
- "content": "5586404be53e6aaf76ea248c420ee0f6fe1be1220e1a049112429bc5c0c0ab4c"
1628
+ "content": "6f781539cbfc72159cfc2532c53dec0adbc9061e1848e5da5ea564722dc9594a"
1619
1629
  }
1620
1630
  ]
1621
1631
  },
@@ -1625,7 +1635,7 @@
1625
1635
  "hashes": [
1626
1636
  {
1627
1637
  "alg": "SHA-256",
1628
- "content": "51f331c1fddf74623f02dfa45ac86ca3650b1ce885f33cb07b3c3101a9de38ff"
1638
+ "content": "d94695ae68573147c5d0c595e3fc6d9f12977fb41ddc5c36d77b1fa9c23df5a9"
1629
1639
  }
1630
1640
  ]
1631
1641
  },
@@ -1635,7 +1645,7 @@
1635
1645
  "hashes": [
1636
1646
  {
1637
1647
  "alg": "SHA-256",
1638
- "content": "ffecd7640d6f93c8fa6be63ccc75e4be5a05ba74593513e4ce530300ce1ce02b"
1648
+ "content": "cd7da1b9bc60372a29a970e5b5b01ab3651cf0b02716207451e0421b249bc17a"
1639
1649
  }
1640
1650
  ]
1641
1651
  },
@@ -2965,7 +2975,7 @@
2965
2975
  "hashes": [
2966
2976
  {
2967
2977
  "alg": "SHA-256",
2968
- "content": "0e932851e1e1edfc773c1e3d5a11bd87ccbfe7a6eab245f1e2f1c34f442122d6"
2978
+ "content": "e83c3cbd2e2ed5a861bf916ab8bf88f6984dfdc785a0168550ea47bb36484d6a"
2969
2979
  }
2970
2980
  ]
2971
2981
  },
@@ -2975,7 +2985,7 @@
2975
2985
  "hashes": [
2976
2986
  {
2977
2987
  "alg": "SHA-256",
2978
- "content": "1218960ee42e7d8fa4e33a52585467763494bb5300a891507116cde99bb15bfc"
2988
+ "content": "b9e63b4eef608e1ae96fca3283638ab327df62ccd027acca74aec7760ee87d0e"
2979
2989
  }
2980
2990
  ]
2981
2991
  },
@@ -2985,7 +2995,7 @@
2985
2995
  "hashes": [
2986
2996
  {
2987
2997
  "alg": "SHA-256",
2988
- "content": "52b8c82aef616b7b91cdcb62de5fc35ed62dba4573e9c9b1c4064bfd5a167747"
2998
+ "content": "51b505f28dcf7a61052e012c148e57a487b094bee18ea0f8878ae5ad68ec8826"
2989
2999
  }
2990
3000
  ]
2991
3001
  },
@@ -2995,7 +3005,7 @@
2995
3005
  "hashes": [
2996
3006
  {
2997
3007
  "alg": "SHA-256",
2998
- "content": "d57185b28dc7f08e02a4f7b79d4cba53ffe395fd05a9edf4992d7f41ef08b25f"
3008
+ "content": "250a219b82f8efc1a6f5de75296b119365df9313ae473ea335d8c8bba7ff8661"
2999
3009
  }
3000
3010
  ]
3001
3011
  },
@@ -3405,7 +3415,7 @@
3405
3415
  "hashes": [
3406
3416
  {
3407
3417
  "alg": "SHA-256",
3408
- "content": "4e0045cf445e1f6a2e173912acc2ce6b76730dbc0502219d09324644eba71d91"
3418
+ "content": "2ba5f75f3f18c385a140889bf10117011aa892a6df79a2ef759f131089e052ce"
3409
3419
  }
3410
3420
  ]
3411
3421
  },
@@ -3765,7 +3775,7 @@
3765
3775
  "hashes": [
3766
3776
  {
3767
3777
  "alg": "SHA-256",
3768
- "content": "7433b7f1dea51a494d4a4587872d46339383b69052e8fd2127d9feffe47eaa43"
3778
+ "content": "156bb512de2c3ced8fe5a3419c0e29682e89573d12eae6abd8aebd15534d3bb0"
3769
3779
  }
3770
3780
  ]
3771
3781
  },
@@ -3775,7 +3785,7 @@
3775
3785
  "hashes": [
3776
3786
  {
3777
3787
  "alg": "SHA-256",
3778
- "content": "3312482046cad99aa5cdedb5a87b6a5ffa3755688296f89f7081e77f47b3d5e2"
3788
+ "content": "ce8c666b5d7fb16578a4489cafae56a0baf08dc786b052e40632692e713998f8"
3779
3789
  }
3780
3790
  ]
3781
3791
  },
@@ -3795,7 +3805,7 @@
3795
3805
  "hashes": [
3796
3806
  {
3797
3807
  "alg": "SHA-256",
3798
- "content": "701c17c11c744a1535993ff37acb2897a105bbc3cb66283f5b64bd0b89c100c7"
3808
+ "content": "42960dd51a381997be9f48bf23a7f6b2aa85c48479eb2b6ca884ed0d20838d74"
3799
3809
  }
3800
3810
  ]
3801
3811
  },
@@ -3805,7 +3815,7 @@
3805
3815
  "hashes": [
3806
3816
  {
3807
3817
  "alg": "SHA-256",
3808
- "content": "6761caa91ce493512cf5d2791ec4f83ca33b4c1dfc1ce9c3b8068812b4034efd"
3818
+ "content": "d01c1a598c1b8cfa91600d4bcd2bfae492279cf27e01c07672c532449c825bb1"
3809
3819
  }
3810
3820
  ]
3811
3821
  },
@@ -4135,7 +4145,7 @@
4135
4145
  "hashes": [
4136
4146
  {
4137
4147
  "alg": "SHA-256",
4138
- "content": "a7f4cffd407fa3d9cb87bce37cb33153d98c40da4e2da74c4f9e804dc32b77fc"
4148
+ "content": "ca2f5d5f65be107e7c1740a5447b729f08ec297baf5383a3dbe473e2c3c588da"
4139
4149
  }
4140
4150
  ]
4141
4151
  },
@@ -4409,6 +4419,36 @@
4409
4419
  }
4410
4420
  ]
4411
4421
  },
4422
+ {
4423
+ "type": "file",
4424
+ "name": "test/cfr-discovery-gate.test.ts",
4425
+ "hashes": [
4426
+ {
4427
+ "alg": "SHA-256",
4428
+ "content": "b4d343527c5a92c624dd90daf4400d938f885836bd671e10ba3a58f05047a52d"
4429
+ }
4430
+ ]
4431
+ },
4432
+ {
4433
+ "type": "file",
4434
+ "name": "test/cfr-handoff-exits.test.ts",
4435
+ "hashes": [
4436
+ {
4437
+ "alg": "SHA-256",
4438
+ "content": "b93f89785bb853b466d40bc2c515d11ec3ae6e00c47f7364dce9a9544bc9c8eb"
4439
+ }
4440
+ ]
4441
+ },
4442
+ {
4443
+ "type": "file",
4444
+ "name": "test/cfr-tool-perimeter.test.ts",
4445
+ "hashes": [
4446
+ {
4447
+ "alg": "SHA-256",
4448
+ "content": "ae004e51a76a8c979e27916056baa33eac60bdfa427cd1cb7791f65aa8ad0125"
4449
+ }
4450
+ ]
4451
+ },
4412
4452
  {
4413
4453
  "type": "file",
4414
4454
  "name": "test/challenge-panel.test.ts",
@@ -4745,7 +4785,7 @@
4745
4785
  "hashes": [
4746
4786
  {
4747
4787
  "alg": "SHA-256",
4748
- "content": "6aab094440f13c40664e4b33e005ea1fb0fb1c9e9f1848fdde44865a53c43535"
4788
+ "content": "027a4a5c5257725bb12c91360e02a8161b9036279eddc7ca2ca8217d1ec32deb"
4749
4789
  }
4750
4790
  ]
4751
4791
  },
@@ -4905,7 +4945,7 @@
4905
4945
  "hashes": [
4906
4946
  {
4907
4947
  "alg": "SHA-256",
4908
- "content": "7699bd5d5ea9a5a423681091678282efe96a6a978f9ee938f0911267e6463410"
4948
+ "content": "eeb59ddb49116a20dd0b32395c8d4d6049df0579d2efb10731affbb51719c9a9"
4909
4949
  }
4910
4950
  ]
4911
4951
  },
@@ -4915,7 +4955,7 @@
4915
4955
  "hashes": [
4916
4956
  {
4917
4957
  "alg": "SHA-256",
4918
- "content": "3681b27b5ce247b77e33bd78f17873ab5e94c693b0b523a192944ec1194b6fbe"
4958
+ "content": "edf746d4e1f00d0b178c38890f4a1fdd875b8dedc9e5f7f13003680b96e42d2c"
4919
4959
  }
4920
4960
  ]
4921
4961
  },
@@ -4925,7 +4965,7 @@
4925
4965
  "hashes": [
4926
4966
  {
4927
4967
  "alg": "SHA-256",
4928
- "content": "e8732fadbe26cf8a0f85cc0aad90b34d0859ad98003351a1f1776f5bea777bb9"
4968
+ "content": "0fad2ea7c4dd5c5cfc8a9cad61b5b2b2a3f17144ca746b3f9d936e2c86c250fd"
4929
4969
  }
4930
4970
  ]
4931
4971
  },
@@ -5015,7 +5055,7 @@
5015
5055
  "hashes": [
5016
5056
  {
5017
5057
  "alg": "SHA-256",
5018
- "content": "f4bed0557865d5fb5c6cf064a6ffb1425d79e09a9b24a3055477a6b1bb05c1ee"
5058
+ "content": "9ff92463ea18e3a120df5841ab7c857b96e15d77089280a5ff3749f499b4d029"
5019
5059
  }
5020
5060
  ]
5021
5061
  },
@@ -5045,7 +5085,7 @@
5045
5085
  "hashes": [
5046
5086
  {
5047
5087
  "alg": "SHA-256",
5048
- "content": "a26d2b3cc2434cc0846e73c1c6d5c6d7223e5be5a24217b75f62d494affa2089"
5088
+ "content": "66711d04297969e702fa6fcdeeee3d87dac01a9e0ce2203a33569080c532f601"
5049
5089
  }
5050
5090
  ]
5051
5091
  },
@@ -5055,7 +5095,7 @@
5055
5095
  "hashes": [
5056
5096
  {
5057
5097
  "alg": "SHA-256",
5058
- "content": "bf6bb4a399d46af5f867a3155697589bcaa93d58051f19746886afe74acf7b68"
5098
+ "content": "13a00f4e8f5d2e8ff4a011af70bdcc200ffa93e144ada85e707a23c15f770551"
5059
5099
  }
5060
5100
  ]
5061
5101
  },
@@ -5169,6 +5209,16 @@
5169
5209
  }
5170
5210
  ]
5171
5211
  },
5212
+ {
5213
+ "type": "file",
5214
+ "name": "test/registry-not-discovered.test.ts",
5215
+ "hashes": [
5216
+ {
5217
+ "alg": "SHA-256",
5218
+ "content": "d3e308b2527256b2c2e2499f1dc4841fba948168befb36a810439995b10b30f5"
5219
+ }
5220
+ ]
5221
+ },
5172
5222
  {
5173
5223
  "type": "file",
5174
5224
  "name": "test/registry.test.ts",
@@ -5285,7 +5335,7 @@
5285
5335
  "hashes": [
5286
5336
  {
5287
5337
  "alg": "SHA-256",
5288
- "content": "313b27df8d3fdfedd76d70bd6934933db13e234b645b8c7cf80144ff559ed35b"
5338
+ "content": "2296310c423ac11900b8577b8b5ca0f61587b6d57c78cf369bfbcb736ef777fb"
5289
5339
  }
5290
5340
  ]
5291
5341
  },
@@ -32,6 +32,7 @@ export const LOOP_BLOB_NAMES = ["checkpoints","training-pairs","model-resolver",
32
32
  * tracked dz-backlog item, deliberately NOT claimed here. */
33
33
  export const BLOB_COVERAGE_MANIFEST: { coveredWorkflows: string[] } = {
34
34
  "coveredWorkflows": [
35
+ ".claude/workflows/cfr-pipeline.js",
35
36
  ".claude/workflows/feature-adr.js",
36
37
  ".claude/workflows/health-advisor.js",
37
38
  "packages/@dzhechkov/skills-feature-adr/templates/.claude/workflows/feature-adr.js"
package/src/loop-lint.ts CHANGED
@@ -41,7 +41,7 @@ export interface LintRun {
41
41
  verdict: LintVerdict;
42
42
  mode: LintMode;
43
43
  findings: LintFinding[];
44
- /** Per-rule verdict map (17 rules — every rule reports, none silently skipped). */
44
+ /** Per-rule verdict map (18 rules — every rule reports, none silently skipped). */
45
45
  rules: Record<string, LintSeverity>;
46
46
  }
47
47
 
@@ -62,6 +62,7 @@ export const LINT_RULES = [
62
62
  'pause-wired',
63
63
  'dispatch-by-deliverable',
64
64
  'no-agent-outside-runstep',
65
+ 'tool-perimeter-declared',
65
66
  'size-budget',
66
67
  ] as const;
67
68
  export type LintRuleId = (typeof LINT_RULES)[number];
@@ -563,6 +564,58 @@ function ruleNoAgentOutsideRunstep(ctx: Ctx): LintFinding[] {
563
564
  return out;
564
565
  }
565
566
 
567
+ /** The `<server>:<capability>` grammar a declared perimeter entry must match (ADR-002 §1). Two or
568
+ * more colon-separated lowercase segments — a bare `gitlab` names a server with no capability and
569
+ * is exactly the shape that reads as "everything on that server". */
570
+ export const TOOL_PERIMETER_ENTRY_RE = /^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)+$/;
571
+
572
+ /**
573
+ * `tool-perimeter-declared` (cfr-pipeline ADR-002) — every DISPATCHING step (`agent`|`gate`)
574
+ * declares its MCP tool perimeter, well-formed and without duplicates, and the rendered contract
575
+ * line agrees with the declared array.
576
+ *
577
+ * GENERIC by construction: it knows nothing about GitLab, Jira, Wiki or JSM — only that a
578
+ * dispatching step must SAY what it may touch. **Absence FLAGS: silence is never permission.**
579
+ * `tools: []` is the correct, meaningful declaration for a step that touches no external tool.
580
+ *
581
+ * SEVERITY IS STAGED (checkpoint amendment 2, overriding ADR-002 §Rationale-5's FAIL-everywhere
582
+ * stance): WARN by default, FAIL only under `--require-plan`. That keeps the published 0.4.x lint
583
+ * contract of harness-core/harness-cli non-breaking while still making the rule a hard gate
584
+ * exactly where cfr-pipeline runs it. Plan-less scripts report `inconclusive` (`no-plan-binding`)
585
+ * like every other plan-anchored rule — never a silent pass.
586
+ */
587
+ function ruleToolPerimeterDeclared(ctx: Ctx): LintFinding[] {
588
+ if (ctx.projection === null) return noPlan('tool-perimeter-declared');
589
+ const sev: LintSeverity = ctx.mode === 'require-plan' ? 'fail' : 'warn';
590
+ const out: LintFinding[] = [];
591
+ for (const f of ctx.projection.facts) {
592
+ if (f.kind !== 'agent' && f.kind !== 'gate') continue;
593
+ if (f.tools === null) {
594
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `dispatching step ${f.id} declares no \`tools\` perimeter — absence FLAGS: silence is never permission (declare \`tools: []\` if the step touches no external tool)`, anchor: f.id });
595
+ continue;
596
+ }
597
+ const seen = new Set<string>();
598
+ for (const entry of f.tools) {
599
+ if (!TOOL_PERIMETER_ENTRY_RE.test(entry)) {
600
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: tool perimeter entry "${entry}" does not match <server>:<capability> (${String(TOOL_PERIMETER_ENTRY_RE)}) — a malformed entry cannot be matched against a real server inventory`, anchor: f.id });
601
+ }
602
+ if (seen.has(entry)) {
603
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: tool perimeter entry "${entry}" is declared twice — a duplicate hides a copy/paste of the wrong stage's perimeter`, anchor: f.id });
604
+ }
605
+ seen.add(entry);
606
+ }
607
+ // script cross-check: the rendered contract line must agree with the DECLARED array — a plan
608
+ // edit that never re-rendered would otherwise pass on the plan half alone.
609
+ if (f.tools.length > 0) {
610
+ const expected = 'declared MCP tool allowlist (plan tools): ' + f.tools.join(', ') + ' — use NOTHING outside it.';
611
+ if (!ctx.script.includes(expected)) {
612
+ out.push({ rule: 'tool-perimeter-declared', severity: sev, message: `step ${f.id}: the rendered script carries no contract line matching the declared perimeter [${f.tools.join(', ')}] — the script is stale against the plan, or the perimeter is decorative`, anchor: f.id });
613
+ }
614
+ }
615
+ }
616
+ return out;
617
+ }
618
+
566
619
  function ruleSizeBudget(ctx: Ctx): LintFinding[] {
567
620
  const n = ctx.lines.length;
568
621
  if (n > SIZE_BUDGET_WARN_LINES) {
@@ -623,6 +676,7 @@ export function lint(scriptText: string, opts: LintOptions = {}): LintRun {
623
676
  run('pause-wired', () => rulePauseWired(ctx));
624
677
  run('dispatch-by-deliverable', () => ruleDispatchByDeliverable(ctx));
625
678
  run('no-agent-outside-runstep', () => ruleNoAgentOutsideRunstep(ctx));
679
+ run('tool-perimeter-declared', () => ruleToolPerimeterDeclared(ctx));
626
680
  run('size-budget', () => ruleSizeBudget(ctx));
627
681
 
628
682
  const severities = Object.values(rules);
package/src/loop-plan.ts CHANGED
@@ -115,6 +115,20 @@ export interface LoopStep {
115
115
  * exec fingerprint in two review rounds; see roadmap. */
116
116
  checkpoint?: boolean;
117
117
  budget?: { maxAgents: number };
118
+ /**
119
+ * DECLARED MCP tool perimeter for this dispatching step (cfr-pipeline ADR-002).
120
+ *
121
+ * A `<server>:<capability>` allowlist (`^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)+$`, checked by the
122
+ * `tool-perimeter-declared` lint rule, not by the schema — the schema owns only the TYPE).
123
+ * ENACTED by `stepPromptAssembly`: a non-empty array renders a fixed contract line into the
124
+ * step's prompt, so the perimeter is COMMUNICATED to the agent rather than decorative. An empty
125
+ * array is meaningful and is the correct value for a step that touches no external tool.
126
+ *
127
+ * HONESTY, said once and never softened elsewhere: this is a DECLARATION, not enforcement.
128
+ * `agent()` exposes no tool restriction; real enforcement lives at the MCP server. No document
129
+ * may call this a sandbox.
130
+ */
131
+ tools?: string[];
118
132
  /** Dispatch route. v1 NARROWING (QE round 6): only 'inline' is enacted — 'codex-wrapper' and
119
133
  * 'codex-exec' are VALIDATED-AWAY (ENACT-DISPATCH). The fire-and-forget wrapper returns a stub
120
134
  * that reads as a clean result, and codex-exec had no live-proven enactment here; see roadmap. */
@@ -328,6 +342,7 @@ export const FIELD_DOMAINS: Record<string, FieldDomain> = {
328
342
  'LoopStep.cache': { t: 'record' },
329
343
  'LoopStep.checkpoint': { t: 'boolean' },
330
344
  'LoopStep.budget': { t: 'record' },
345
+ 'LoopStep.tools': { t: 'string[]' },
331
346
  'LoopStep.dispatch': { t: 'enum', values: ['inline', 'codex-wrapper', 'codex-exec'] },
332
347
  'LoopStep.pauseState': { t: 'string' },
333
348
  'artifacts.reads': { t: 'string[]' },
@@ -535,6 +550,9 @@ export const STEP_FIELD_KINDS: Record<string, readonly StepKind[]> = {
535
550
  budget: ALL_KINDS, // budget is an ACCOUNTING weight — summed into the rendered budget guard for every kind
536
551
  prompt: DISPATCHING_KINDS,
537
552
  artifacts: DISPATCHING_KINDS,
553
+ // the perimeter rides the PROMPT, so it enacts exactly where a prompt is dispatched — `tools`
554
+ // on a fanout/join/pause step would be an unperformed promise (KIND-1 rejects it).
555
+ tools: DISPATCHING_KINDS,
538
556
  model: DISPATCHING_KINDS,
539
557
  deliverable: DISPATCHING_KINDS,
540
558
  idempotent: DISPATCHING_KINDS,
@@ -1275,6 +1293,9 @@ export interface LintProjection {
1275
1293
  dispatch: DispatchRoute;
1276
1294
  cacheable: boolean;
1277
1295
  writes: string[];
1296
+ /** DECLARED tool perimeter, or `null` when the step declares none (ADR-002: absence FLAGS —
1297
+ * silence is never permission, so `null` and `[]` must stay distinguishable here). */
1298
+ tools: string[] | null;
1278
1299
  }[];
1279
1300
  pauses: { state: string; resumeArg: string }[];
1280
1301
  checkpointingEnabled: boolean;
@@ -1345,6 +1366,7 @@ export function toLintProjection(plan: LoopPlan): LintProjection {
1345
1366
  dispatch: s.dispatch ?? 'inline',
1346
1367
  cacheable: s.cacheable === true,
1347
1368
  writes: s.artifacts?.writes ?? [],
1369
+ tools: Array.isArray(s.tools) ? [...s.tools] : null,
1348
1370
  };
1349
1371
  });
1350
1372
 
@@ -199,6 +199,14 @@ function stepPromptAssembly(s: LoopStep, plan: LoopPlan): string[] {
199
199
  const fileNote = (s.deliverable ?? 'return-value') === 'file' ? '; your deliverable is the written file(s), not your reply' : '';
200
200
  lines.push(` ${jsString('declared outputs (plan artifacts.writes): ' + writes.join(', ') + ' — write them' + fileNote + '. The loop verifies they land.')},`);
201
201
  }
202
+ // ENACTS LoopStep.tools (cfr-pipeline ADR-002): the declared perimeter is COMMUNICATED to the
203
+ // agent in FIXED wording, so its presence is greppable by a layer-1 test. The second sentence is
204
+ // not decoration — it is the honesty clause the whole feature rests on: a declaration is not
205
+ // enforcement, and every environment this ships into today serves the tools as labeled stubs.
206
+ const tools = s.tools ?? [];
207
+ if (tools.length > 0) {
208
+ lines.push(` ${jsString('declared MCP tool allowlist (plan tools): ' + tools.join(', ') + ' — use NOTHING outside it. In this environment every one of these is a labeled STUB, not a live integration; enforcement lives at the MCP server, not here.')},`);
209
+ }
202
210
  if (s.kind === 'gate') {
203
211
  lines.push(` ${jsString('GATE PROTOCOL (kind: ' + (gateCfg?.kind ?? 'gate') + '): end your reply with exactly one line "GATE: PASS" or "GATE: FAIL" — the loop PARSES this verdict and never synthesizes one.')},`);
204
212
  }