@dzhechkov/harness-core 0.5.1 → 0.5.3
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/.dz-manifest.json +68 -28
- package/README.md +8 -2
- package/dist/feature-adr-checkpoints.d.ts +93 -1
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +105 -1
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loop-blobs.generated.js +5 -5
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/loop-render.js +1 -1
- package/dist/loop-render.js.map +1 -1
- package/dist/loop-trace.d.ts +75 -3
- package/dist/loop-trace.d.ts.map +1 -1
- package/dist/loop-trace.js +62 -6
- package/dist/loop-trace.js.map +1 -1
- package/dist/trace-corroborate.d.ts +48 -0
- package/dist/trace-corroborate.d.ts.map +1 -0
- package/dist/trace-corroborate.js +172 -0
- package/dist/trace-corroborate.js.map +1 -0
- package/dist/workflow-run.d.ts +15 -0
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +19 -1
- package/dist/workflow-run.js.map +1 -1
- package/package.json +18 -18
- package/sbom.json +127 -27
- package/src/feature-adr-checkpoints.ts +140 -1
- package/src/index.ts +1 -0
- package/src/loop-blobs.generated.ts +5 -5
- package/src/loop-render.ts +1 -1
- package/src/loop-trace.ts +123 -6
- package/src/trace-corroborate.ts +205 -0
- package/src/workflow-run.ts +31 -1
- package/LICENSE +0 -21
package/sbom.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"hashes": [
|
|
16
16
|
{
|
|
17
17
|
"alg": "SHA-256",
|
|
18
|
-
"content": "
|
|
18
|
+
"content": "1790811c8031105b1f281ecaff8a2e5b66a33ac9a323cbe63fb0caae67835b0d"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
@@ -1525,7 +1525,7 @@
|
|
|
1525
1525
|
"hashes": [
|
|
1526
1526
|
{
|
|
1527
1527
|
"alg": "SHA-256",
|
|
1528
|
-
"content": "
|
|
1528
|
+
"content": "837b65bceef392d58a8b85257443e796c8da7219e2c7d5749df952ff4ba9a7d9"
|
|
1529
1529
|
}
|
|
1530
1530
|
]
|
|
1531
1531
|
},
|
|
@@ -1535,7 +1535,7 @@
|
|
|
1535
1535
|
"hashes": [
|
|
1536
1536
|
{
|
|
1537
1537
|
"alg": "SHA-256",
|
|
1538
|
-
"content": "
|
|
1538
|
+
"content": "2c88a22a96119c8ed14d67c1830bd85857e0c43f8a730951c1279ec9442decd6"
|
|
1539
1539
|
}
|
|
1540
1540
|
]
|
|
1541
1541
|
},
|
|
@@ -1545,7 +1545,7 @@
|
|
|
1545
1545
|
"hashes": [
|
|
1546
1546
|
{
|
|
1547
1547
|
"alg": "SHA-256",
|
|
1548
|
-
"content": "
|
|
1548
|
+
"content": "5c208320312a34f0e4d09b2cea3d50a057253771a67b60a0ee0d0907a382546c"
|
|
1549
1549
|
}
|
|
1550
1550
|
]
|
|
1551
1551
|
},
|
|
@@ -1555,7 +1555,7 @@
|
|
|
1555
1555
|
"hashes": [
|
|
1556
1556
|
{
|
|
1557
1557
|
"alg": "SHA-256",
|
|
1558
|
-
"content": "
|
|
1558
|
+
"content": "262dd0fca9a734cc81ba33c215adb9ef88b343ee56091c017abda45c68bc2508"
|
|
1559
1559
|
}
|
|
1560
1560
|
]
|
|
1561
1561
|
},
|
|
@@ -1625,7 +1625,7 @@
|
|
|
1625
1625
|
"hashes": [
|
|
1626
1626
|
{
|
|
1627
1627
|
"alg": "SHA-256",
|
|
1628
|
-
"content": "
|
|
1628
|
+
"content": "115740b3c04b6e906e135eb9d1e462ae4197cdba558d4eb620e29e541b4de288"
|
|
1629
1629
|
}
|
|
1630
1630
|
]
|
|
1631
1631
|
},
|
|
@@ -1635,7 +1635,7 @@
|
|
|
1635
1635
|
"hashes": [
|
|
1636
1636
|
{
|
|
1637
1637
|
"alg": "SHA-256",
|
|
1638
|
-
"content": "
|
|
1638
|
+
"content": "2cbadd2514704ccf6cfbf7e44fc50cad3d3701bc91b98651015896ebe209449a"
|
|
1639
1639
|
}
|
|
1640
1640
|
]
|
|
1641
1641
|
},
|
|
@@ -1785,7 +1785,7 @@
|
|
|
1785
1785
|
"hashes": [
|
|
1786
1786
|
{
|
|
1787
1787
|
"alg": "SHA-256",
|
|
1788
|
-
"content": "
|
|
1788
|
+
"content": "49b8fc106dee22095dc02c43e224de52b9026dbf77d2f2e2654219a776017a09"
|
|
1789
1789
|
}
|
|
1790
1790
|
]
|
|
1791
1791
|
},
|
|
@@ -1795,7 +1795,7 @@
|
|
|
1795
1795
|
"hashes": [
|
|
1796
1796
|
{
|
|
1797
1797
|
"alg": "SHA-256",
|
|
1798
|
-
"content": "
|
|
1798
|
+
"content": "170a7cdafb569ce8d9d1bd7c63ab83d4689cd3b1ed8a1f02536f5affd9dd6d5f"
|
|
1799
1799
|
}
|
|
1800
1800
|
]
|
|
1801
1801
|
},
|
|
@@ -1845,7 +1845,7 @@
|
|
|
1845
1845
|
"hashes": [
|
|
1846
1846
|
{
|
|
1847
1847
|
"alg": "SHA-256",
|
|
1848
|
-
"content": "
|
|
1848
|
+
"content": "bd228e00e217a99ee7f192c9e87d4e1c794c8ffb52323b77b87ee93f601f013a"
|
|
1849
1849
|
}
|
|
1850
1850
|
]
|
|
1851
1851
|
},
|
|
@@ -1855,7 +1855,7 @@
|
|
|
1855
1855
|
"hashes": [
|
|
1856
1856
|
{
|
|
1857
1857
|
"alg": "SHA-256",
|
|
1858
|
-
"content": "
|
|
1858
|
+
"content": "4c37b21c750133def0498c6d1b0fddfb2525500ced4dc58634ae1234425337d3"
|
|
1859
1859
|
}
|
|
1860
1860
|
]
|
|
1861
1861
|
},
|
|
@@ -1865,7 +1865,7 @@
|
|
|
1865
1865
|
"hashes": [
|
|
1866
1866
|
{
|
|
1867
1867
|
"alg": "SHA-256",
|
|
1868
|
-
"content": "
|
|
1868
|
+
"content": "83f2afea542e80bf09f9ccf9a56a9d352111a8a3cd7744bf8f2ccad84d796c6a"
|
|
1869
1869
|
}
|
|
1870
1870
|
]
|
|
1871
1871
|
},
|
|
@@ -1875,7 +1875,7 @@
|
|
|
1875
1875
|
"hashes": [
|
|
1876
1876
|
{
|
|
1877
1877
|
"alg": "SHA-256",
|
|
1878
|
-
"content": "
|
|
1878
|
+
"content": "93a3007d6a7a906344f8f85272aef1928811997d331202171558763ab917b9d6"
|
|
1879
1879
|
}
|
|
1880
1880
|
]
|
|
1881
1881
|
},
|
|
@@ -3599,6 +3599,46 @@
|
|
|
3599
3599
|
}
|
|
3600
3600
|
]
|
|
3601
3601
|
},
|
|
3602
|
+
{
|
|
3603
|
+
"type": "file",
|
|
3604
|
+
"name": "dist/trace-corroborate.d.ts",
|
|
3605
|
+
"hashes": [
|
|
3606
|
+
{
|
|
3607
|
+
"alg": "SHA-256",
|
|
3608
|
+
"content": "ebc36a47e4f8359eb7359a1e1697e9a135b2a281ecb1f795e99da7f9476f7f4c"
|
|
3609
|
+
}
|
|
3610
|
+
]
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
"type": "file",
|
|
3614
|
+
"name": "dist/trace-corroborate.d.ts.map",
|
|
3615
|
+
"hashes": [
|
|
3616
|
+
{
|
|
3617
|
+
"alg": "SHA-256",
|
|
3618
|
+
"content": "7a2f343525b6585d0cd54644282bc49b741baffd066ebff0f023c036b622e6f8"
|
|
3619
|
+
}
|
|
3620
|
+
]
|
|
3621
|
+
},
|
|
3622
|
+
{
|
|
3623
|
+
"type": "file",
|
|
3624
|
+
"name": "dist/trace-corroborate.js",
|
|
3625
|
+
"hashes": [
|
|
3626
|
+
{
|
|
3627
|
+
"alg": "SHA-256",
|
|
3628
|
+
"content": "1b78df722776f6e253e3d0babb90c8557b5d733528125c793b7519ec724db96b"
|
|
3629
|
+
}
|
|
3630
|
+
]
|
|
3631
|
+
},
|
|
3632
|
+
{
|
|
3633
|
+
"type": "file",
|
|
3634
|
+
"name": "dist/trace-corroborate.js.map",
|
|
3635
|
+
"hashes": [
|
|
3636
|
+
{
|
|
3637
|
+
"alg": "SHA-256",
|
|
3638
|
+
"content": "7c95e98228f86fda4f4503b04efaf149466f9bd78fbb549a8aad49c8a4f3b7ab"
|
|
3639
|
+
}
|
|
3640
|
+
]
|
|
3641
|
+
},
|
|
3602
3642
|
{
|
|
3603
3643
|
"type": "file",
|
|
3604
3644
|
"name": "dist/upgrade.d.ts",
|
|
@@ -3765,7 +3805,7 @@
|
|
|
3765
3805
|
"hashes": [
|
|
3766
3806
|
{
|
|
3767
3807
|
"alg": "SHA-256",
|
|
3768
|
-
"content": "
|
|
3808
|
+
"content": "94d154db3feb9676e515fc04129ac299f967ff46b8a7daedc51ec201f7eb5259"
|
|
3769
3809
|
}
|
|
3770
3810
|
]
|
|
3771
3811
|
},
|
|
@@ -3775,7 +3815,7 @@
|
|
|
3775
3815
|
"hashes": [
|
|
3776
3816
|
{
|
|
3777
3817
|
"alg": "SHA-256",
|
|
3778
|
-
"content": "
|
|
3818
|
+
"content": "0bda62d918a427203c946a0d6ec9fdb67a2223573939bc358fa9d48545e34361"
|
|
3779
3819
|
}
|
|
3780
3820
|
]
|
|
3781
3821
|
},
|
|
@@ -3785,7 +3825,7 @@
|
|
|
3785
3825
|
"hashes": [
|
|
3786
3826
|
{
|
|
3787
3827
|
"alg": "SHA-256",
|
|
3788
|
-
"content": "
|
|
3828
|
+
"content": "c5d24bc9e5a8731ecd9e83e0fc3c3bac02421b76028906b20d2a113f04efbe59"
|
|
3789
3829
|
}
|
|
3790
3830
|
]
|
|
3791
3831
|
},
|
|
@@ -3795,7 +3835,7 @@
|
|
|
3795
3835
|
"hashes": [
|
|
3796
3836
|
{
|
|
3797
3837
|
"alg": "SHA-256",
|
|
3798
|
-
"content": "
|
|
3838
|
+
"content": "f9a9a320dde4bae0d6f6b0b2e7c03c08127e452f7fa7e00d78ea9f9b816bc6e7"
|
|
3799
3839
|
}
|
|
3800
3840
|
]
|
|
3801
3841
|
},
|
|
@@ -3845,7 +3885,7 @@
|
|
|
3845
3885
|
"hashes": [
|
|
3846
3886
|
{
|
|
3847
3887
|
"alg": "SHA-256",
|
|
3848
|
-
"content": "
|
|
3888
|
+
"content": "f44d3dd18b5332681d861707c5644b1cbb027b217b9fda07a0b8510b6346d1a8"
|
|
3849
3889
|
}
|
|
3850
3890
|
]
|
|
3851
3891
|
},
|
|
@@ -4225,7 +4265,7 @@
|
|
|
4225
4265
|
"hashes": [
|
|
4226
4266
|
{
|
|
4227
4267
|
"alg": "SHA-256",
|
|
4228
|
-
"content": "
|
|
4268
|
+
"content": "b741c4477d04f5fdb88f88601b8528f56e9b1fdc6a849dece4af157a81c0fe57"
|
|
4229
4269
|
}
|
|
4230
4270
|
]
|
|
4231
4271
|
},
|
|
@@ -4245,7 +4285,7 @@
|
|
|
4245
4285
|
"hashes": [
|
|
4246
4286
|
{
|
|
4247
4287
|
"alg": "SHA-256",
|
|
4248
|
-
"content": "
|
|
4288
|
+
"content": "929be36a3c9fd2cf3ba6e6b7328dd8d7928d9c031c38bb62880ba234c9553334"
|
|
4249
4289
|
}
|
|
4250
4290
|
]
|
|
4251
4291
|
},
|
|
@@ -4285,7 +4325,7 @@
|
|
|
4285
4325
|
"hashes": [
|
|
4286
4326
|
{
|
|
4287
4327
|
"alg": "SHA-256",
|
|
4288
|
-
"content": "
|
|
4328
|
+
"content": "69d0daac70fda89235352be2cc2e40bcfe62651aedab8056c1919ed062d4a75b"
|
|
4289
4329
|
}
|
|
4290
4330
|
]
|
|
4291
4331
|
},
|
|
@@ -4305,7 +4345,7 @@
|
|
|
4305
4345
|
"hashes": [
|
|
4306
4346
|
{
|
|
4307
4347
|
"alg": "SHA-256",
|
|
4308
|
-
"content": "
|
|
4348
|
+
"content": "ea7d5b64c7805fd4622e7a892e0e3f1f03e4fdbb883f5fe377fcd64330775330"
|
|
4309
4349
|
}
|
|
4310
4350
|
]
|
|
4311
4351
|
},
|
|
@@ -4729,6 +4769,16 @@
|
|
|
4729
4769
|
}
|
|
4730
4770
|
]
|
|
4731
4771
|
},
|
|
4772
|
+
{
|
|
4773
|
+
"type": "file",
|
|
4774
|
+
"name": "src/trace-corroborate.ts",
|
|
4775
|
+
"hashes": [
|
|
4776
|
+
{
|
|
4777
|
+
"alg": "SHA-256",
|
|
4778
|
+
"content": "28a872378dce68940237a99e8ffe57d3da049cccb0ffc315fbddf8ba65e80ee8"
|
|
4779
|
+
}
|
|
4780
|
+
]
|
|
4781
|
+
},
|
|
4732
4782
|
{
|
|
4733
4783
|
"type": "file",
|
|
4734
4784
|
"name": "src/upgrade.ts",
|
|
@@ -4775,7 +4825,7 @@
|
|
|
4775
4825
|
"hashes": [
|
|
4776
4826
|
{
|
|
4777
4827
|
"alg": "SHA-256",
|
|
4778
|
-
"content": "
|
|
4828
|
+
"content": "f1aaf39fcb5088afe3c63185c05a0d22f0507d29c342e3024836f46e3996d430"
|
|
4779
4829
|
}
|
|
4780
4830
|
]
|
|
4781
4831
|
},
|
|
@@ -5339,6 +5389,26 @@
|
|
|
5339
5389
|
}
|
|
5340
5390
|
]
|
|
5341
5391
|
},
|
|
5392
|
+
{
|
|
5393
|
+
"type": "file",
|
|
5394
|
+
"name": "test/feature-adr-workflow-write-discipline.test.ts",
|
|
5395
|
+
"hashes": [
|
|
5396
|
+
{
|
|
5397
|
+
"alg": "SHA-256",
|
|
5398
|
+
"content": "f491c12d9bb0a361095e9ec7cab216b60ea51f1c839256b8b4476bf0f8d5f4ca"
|
|
5399
|
+
}
|
|
5400
|
+
]
|
|
5401
|
+
},
|
|
5402
|
+
{
|
|
5403
|
+
"type": "file",
|
|
5404
|
+
"name": "test/feature-adr-write-discipline.test.ts",
|
|
5405
|
+
"hashes": [
|
|
5406
|
+
{
|
|
5407
|
+
"alg": "SHA-256",
|
|
5408
|
+
"content": "e44795d46da74d4f21bf6817d3884f1428f8adfd814c5174490c9c372299932e"
|
|
5409
|
+
}
|
|
5410
|
+
]
|
|
5411
|
+
},
|
|
5342
5412
|
{
|
|
5343
5413
|
"type": "file",
|
|
5344
5414
|
"name": "test/files-whitelist-guard.test.ts",
|
|
@@ -5435,7 +5505,7 @@
|
|
|
5435
5505
|
"hashes": [
|
|
5436
5506
|
{
|
|
5437
5507
|
"alg": "SHA-256",
|
|
5438
|
-
"content": "
|
|
5508
|
+
"content": "f20a7850a3313d5bb2431d81e8717694f2edf4cda5c67133c0b80d7549e2cbab"
|
|
5439
5509
|
}
|
|
5440
5510
|
]
|
|
5441
5511
|
},
|
|
@@ -5445,7 +5515,7 @@
|
|
|
5445
5515
|
"hashes": [
|
|
5446
5516
|
{
|
|
5447
5517
|
"alg": "SHA-256",
|
|
5448
|
-
"content": "
|
|
5518
|
+
"content": "2bc8d4bd9c2bf15a3522d00b01a18611ef973f4b9e71ef5c1894ea72628294a8"
|
|
5449
5519
|
}
|
|
5450
5520
|
]
|
|
5451
5521
|
},
|
|
@@ -5655,7 +5725,7 @@
|
|
|
5655
5725
|
"hashes": [
|
|
5656
5726
|
{
|
|
5657
5727
|
"alg": "SHA-256",
|
|
5658
|
-
"content": "
|
|
5728
|
+
"content": "95d04dbdb46e7f1fac3f6cfd5ec7705579a436ea7118823e11cb752da7f9adea"
|
|
5659
5729
|
}
|
|
5660
5730
|
]
|
|
5661
5731
|
},
|
|
@@ -5855,7 +5925,7 @@
|
|
|
5855
5925
|
"hashes": [
|
|
5856
5926
|
{
|
|
5857
5927
|
"alg": "SHA-256",
|
|
5858
|
-
"content": "
|
|
5928
|
+
"content": "e9ccae5e13df6370a8564326e5766aa1e03973c595bfdf73765d54191acf98c4"
|
|
5859
5929
|
}
|
|
5860
5930
|
]
|
|
5861
5931
|
},
|
|
@@ -6209,6 +6279,16 @@
|
|
|
6209
6279
|
}
|
|
6210
6280
|
]
|
|
6211
6281
|
},
|
|
6282
|
+
{
|
|
6283
|
+
"type": "file",
|
|
6284
|
+
"name": "test/trace-attestation.test.ts",
|
|
6285
|
+
"hashes": [
|
|
6286
|
+
{
|
|
6287
|
+
"alg": "SHA-256",
|
|
6288
|
+
"content": "f4888117c97180797e6437abc1ef0c6aa1f1e48ee7c2df33d60a65e8bf11b35c"
|
|
6289
|
+
}
|
|
6290
|
+
]
|
|
6291
|
+
},
|
|
6212
6292
|
{
|
|
6213
6293
|
"type": "file",
|
|
6214
6294
|
"name": "test/trace-bundle.test.ts",
|
|
@@ -6219,6 +6299,26 @@
|
|
|
6219
6299
|
}
|
|
6220
6300
|
]
|
|
6221
6301
|
},
|
|
6302
|
+
{
|
|
6303
|
+
"type": "file",
|
|
6304
|
+
"name": "test/trace-corroborate.test.ts",
|
|
6305
|
+
"hashes": [
|
|
6306
|
+
{
|
|
6307
|
+
"alg": "SHA-256",
|
|
6308
|
+
"content": "9e845bb448c1300966e76a030b2b0d88c3aa3cf666d85b58ddd981e15d4dcd85"
|
|
6309
|
+
}
|
|
6310
|
+
]
|
|
6311
|
+
},
|
|
6312
|
+
{
|
|
6313
|
+
"type": "file",
|
|
6314
|
+
"name": "test/trace-qe-round1.test.ts",
|
|
6315
|
+
"hashes": [
|
|
6316
|
+
{
|
|
6317
|
+
"alg": "SHA-256",
|
|
6318
|
+
"content": "7790c41628d7223a45aa61977511d1ddd6cd2cd8dc2dfdcdd248bb25c2effeca"
|
|
6319
|
+
}
|
|
6320
|
+
]
|
|
6321
|
+
},
|
|
6222
6322
|
{
|
|
6223
6323
|
"type": "file",
|
|
6224
6324
|
"name": "test/trusttier-schema.test.ts",
|
|
@@ -62,7 +62,14 @@ export const CHECKPOINT_MAX_RESULT_CHARS = 12_000;
|
|
|
62
62
|
/** Checkpoint format/logic version — SALTED into every input hash. Bump it whenever the workflow's
|
|
63
63
|
* stage semantics, prompts, or composite result shapes change: every pre-existing checkpoint then
|
|
64
64
|
* hashes stale and re-runs, instead of an old-format entry resuming into new logic (Codex QE #5). */
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Bumped 'fa-ckpt-2' → 'fa-ckpt-3' on 2026-08-20: the `design` entry changed meaning. It used to be
|
|
67
|
+
* the ONE record for the whole parallel design fan; it is now a completeness marker sitting above
|
|
68
|
+
* four per-sibling records. A stale-format entry must hash stale rather than resume into new logic,
|
|
69
|
+
* so every in-flight `.fa-state/` re-runs router+design+plan once. That is the intended one-time
|
|
70
|
+
* price of the change, not an accident.
|
|
71
|
+
*/
|
|
72
|
+
export const CKPT_SCHEMA_VERSION = 'fa-ckpt-3';
|
|
66
73
|
|
|
67
74
|
/** FNV-1a 32-bit over UTF-16 code units, hex-encoded (one pass; building block for the 64-bit form). */
|
|
68
75
|
export function fnv1a(str: string): string {
|
|
@@ -116,6 +123,90 @@ export interface ResumeDecision {
|
|
|
116
123
|
* resumes in any mode — force skips only the artifact probe, never the input check (a checkpoint
|
|
117
124
|
* for different inputs is a different feature). A malformed/null recorded result is treated as
|
|
118
125
|
* no-checkpoint (Codex QE #8 — a null result must not resume as a real one). */
|
|
126
|
+
/** The tier-active design sub-stages, in fan order. Namespaced keys are plain strings, so the
|
|
127
|
+
* checkpoint parser's last-wins-per-stage rule and its malformed-line erase rule work unchanged. */
|
|
128
|
+
export const DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'] as const;
|
|
129
|
+
export type DesignSubstage = (typeof DESIGN_SUBSTAGES)[number];
|
|
130
|
+
|
|
131
|
+
/** `design:requirements`, … — the checkpoint stage key for one sibling. */
|
|
132
|
+
export function designStageKey(sub: DesignSubstage | string): string {
|
|
133
|
+
return 'design:' + sub;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DesignFanVerdict {
|
|
137
|
+
complete: boolean;
|
|
138
|
+
missingSubstages: string[];
|
|
139
|
+
missingArtifacts: string[];
|
|
140
|
+
/** Why the fan is not complete — 'ok' when it is. Never collapse 'probe-not-established' into
|
|
141
|
+
* 'ok': a gate that could not read its inputs is INCONCLUSIVE, and inconclusive is not a pass. */
|
|
142
|
+
reason: 'ok' | 'substage-missing' | 'artifact-missing' | 'probe-not-established';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* SP-1 (completeness). The `design` stage may hand a value to Step 6 ONLY when every tier-required
|
|
147
|
+
* sibling produced a result AND every tier-required design artifact is on disk.
|
|
148
|
+
*
|
|
149
|
+
* This is the property the old all-or-nothing WRITE gate was implementing by accident, and the
|
|
150
|
+
* reason it must survive the split: a one-file probe once accepted a design missing its ADR and
|
|
151
|
+
* architecture (Codex QE #2). Splitting the write gate without keeping this would reopen that hole,
|
|
152
|
+
* so the two are landed together and tested together.
|
|
153
|
+
*
|
|
154
|
+
* It is deliberately a READ-side predicate. What may be WRITTEN (one sibling's completed work) and
|
|
155
|
+
* what may be CONSUMED (a whole design) are different questions; the previous code answered the
|
|
156
|
+
* second by crippling the first, which is how one dead agent discarded three finished artifacts.
|
|
157
|
+
*/
|
|
158
|
+
export function decideDesignFanResume(opts: {
|
|
159
|
+
/**
|
|
160
|
+
* The LIVE result of each required sibling, in `required` order — what the fan just returned.
|
|
161
|
+
*
|
|
162
|
+
* Round 1 of cross-family review graded this D and was right: the first version read the
|
|
163
|
+
* START-OF-RUN checkpoint snapshot instead. That snapshot is wrong in both directions. A sibling
|
|
164
|
+
* that succeeded THIS run is absent from it, and — the dangerous half — a stale non-null entry
|
|
165
|
+
* from a PREVIOUS run stays in it even when this run's retry returned null, so an incomplete
|
|
166
|
+
* design could be declared complete. That is the exact hole the old all-or-nothing write gate
|
|
167
|
+
* existed to prevent, reopened by reading the wrong source.
|
|
168
|
+
*
|
|
169
|
+
* The live result is authoritative and free: a sibling that returns non-null wrote its artifact,
|
|
170
|
+
* and a sibling that died returns null. Nothing needs to be re-probed to know that.
|
|
171
|
+
*/
|
|
172
|
+
results: readonly unknown[];
|
|
173
|
+
required: readonly string[];
|
|
174
|
+
/** Every artifact the tier requires from this fan. Empty ⇒ the artifact half is not checked. */
|
|
175
|
+
artifacts: readonly string[];
|
|
176
|
+
/**
|
|
177
|
+
* A listing taken AFTER the fan ran — never the start-of-run one, which is why the parameter is
|
|
178
|
+
* named for that and not just `listing`. Rounds 2 and 3 pinned both halves of this:
|
|
179
|
+
* • round 2 — fed the START-OF-RUN listing, the probe called every fresh M+ fan incomplete,
|
|
180
|
+
* because a listing taken before the fan cannot contain what the fan is about to write;
|
|
181
|
+
* • round 3 — with the probe removed entirely, a sibling that returns non-null having written
|
|
182
|
+
* only its primary artifact (L-tier requirements writes 01_requirements.md and skips
|
|
183
|
+
* 02_research.md) was accepted, and Step 6 planned with no research behind it. A non-null
|
|
184
|
+
* result is the agent's own word; it is not evidence that a file exists.
|
|
185
|
+
* `null` means the probe could not be read. That is INCONCLUSIVE, not clean.
|
|
186
|
+
*/
|
|
187
|
+
postRunListing: ReadonlySet<string> | null;
|
|
188
|
+
}): DesignFanVerdict {
|
|
189
|
+
const missingSubstages: string[] = [];
|
|
190
|
+
opts.required.forEach((sub, i) => {
|
|
191
|
+
const r = opts.results[i];
|
|
192
|
+
if (r === null || r === undefined) missingSubstages.push(sub);
|
|
193
|
+
});
|
|
194
|
+
const missingArtifacts: string[] = [];
|
|
195
|
+
let probeMissing = false;
|
|
196
|
+
if (opts.artifacts.length > 0) {
|
|
197
|
+
if (opts.postRunListing === null) probeMissing = true;
|
|
198
|
+
else for (const rel of opts.artifacts) if (!opts.postRunListing.has(rel)) missingArtifacts.push(rel);
|
|
199
|
+
}
|
|
200
|
+
// Substage first: a dead sibling explains its own missing artifacts, and naming the artifacts
|
|
201
|
+
// instead would send the operator looking for a file when the agent is what died.
|
|
202
|
+
const reason: DesignFanVerdict['reason'] =
|
|
203
|
+
missingSubstages.length > 0 ? 'substage-missing'
|
|
204
|
+
: probeMissing ? 'probe-not-established'
|
|
205
|
+
: missingArtifacts.length > 0 ? 'artifact-missing'
|
|
206
|
+
: 'ok';
|
|
207
|
+
return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };
|
|
208
|
+
}
|
|
209
|
+
|
|
119
210
|
export function decideCheckpointResume(opts: {
|
|
120
211
|
mode: ResumeMode;
|
|
121
212
|
entry: CheckpointEntry | undefined;
|
|
@@ -547,3 +638,51 @@ export function codeStageResultShapeValid(r: unknown): boolean {
|
|
|
547
638
|
if (v.landingProtocol !== 2) return false
|
|
548
639
|
return v.landingStatus === 'landed' || v.landingStatus === 'genuinely-not-landed' || v.landingStatus === 'inconclusive' || v.landingStatus === 'synchronous'
|
|
549
640
|
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Parse the design artifact probe's stdout into "which required artifacts exist".
|
|
644
|
+
*
|
|
645
|
+
* The probe is relayed by a MODEL, not read from a pipe: the workflow sandbox cannot run a shell, so
|
|
646
|
+
* an agent runs the command and hands back what it saw. That makes the transcript forgeable in
|
|
647
|
+
* principle, and cross-family review round 7 showed it forgeable in PRACTICE by accident — an agent
|
|
648
|
+
* that narrates ("Expected output when present: HAVE:01_requirements.md … Actual stdout: …") emits a
|
|
649
|
+
* line identical to the real token, and a permissive parser took it for evidence. The gate then passed a
|
|
650
|
+
* design whose artifact did not exist, which is the one thing this gate exists to prevent.
|
|
651
|
+
*
|
|
652
|
+
* So the transcript is validated STRICTLY, not scanned: after trimming blank lines, the output must be
|
|
653
|
+
* exactly some subset of the known HAVE tokens followed by exactly one sentinel, and nothing else.
|
|
654
|
+
* Anything unexpected — narration, a second sentinel, a shell error, a code fence — makes the probe
|
|
655
|
+
* NOT ESTABLISHED (null), which is a refusal, never a pass.
|
|
656
|
+
*
|
|
657
|
+
* What this does NOT do, named plainly: it cannot stop an agent that deliberately emits precisely the
|
|
658
|
+
* expected transcript and nothing else. That residual is the same trust the whole pipeline places in
|
|
659
|
+
* a relaying agent (the checkpoint reader and the Step-7.5 landing barrier share it). What it does do
|
|
660
|
+
* is make ACCIDENTAL forgery — the kind that actually happened — impossible rather than likely.
|
|
661
|
+
*
|
|
662
|
+
* @returns a Set of the required artifacts that exist, or `null` when the transcript is not trustworthy.
|
|
663
|
+
*/
|
|
664
|
+
export function parseArtifactProbe(opts: {
|
|
665
|
+
stdout: string | null | undefined;
|
|
666
|
+
sentinel: string;
|
|
667
|
+
required: readonly string[];
|
|
668
|
+
}): Set<string> | null {
|
|
669
|
+
if (opts.stdout === null || opts.stdout === undefined) return null;
|
|
670
|
+
const known = new Map<string, string>();
|
|
671
|
+
for (const rel of opts.required) known.set('HAVE:' + rel, rel);
|
|
672
|
+
const found = new Set<string>();
|
|
673
|
+
let sentinels = 0;
|
|
674
|
+
for (const raw of String(opts.stdout).split('\n')) {
|
|
675
|
+
const line = raw.trim();
|
|
676
|
+
if (line === '') continue;
|
|
677
|
+
if (line === opts.sentinel) { sentinels++; continue; }
|
|
678
|
+
// A token AFTER the sentinel is as wrong as an unknown line: the command emits every check first.
|
|
679
|
+
if (sentinels > 0) return null;
|
|
680
|
+
const rel = known.get(line);
|
|
681
|
+
if (rel === undefined) return null;
|
|
682
|
+
found.add(rel);
|
|
683
|
+
}
|
|
684
|
+
// Exactly one. Zero means the command never completed (or never ran); two means something other
|
|
685
|
+
// than the command produced output, and there is no way to tell which half was real.
|
|
686
|
+
if (sentinels !== 1) return null;
|
|
687
|
+
return found;
|
|
688
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ export {
|
|
|
65
65
|
type LintOptions,
|
|
66
66
|
} from './loop-lint.js';
|
|
67
67
|
export * from './loop-trace.js';
|
|
68
|
+
export * from './trace-corroborate.js';
|
|
68
69
|
export * from './trace-bundle.js';
|
|
69
70
|
export { BLOBS as LOOP_BLOBS, LOOP_BLOB_NAMES, BLOB_COVERAGE_MANIFEST } from './loop-blobs.generated.js';
|
|
70
71
|
export type { LoopBlob } from './loop-blobs.generated.js';
|
|
@@ -43,11 +43,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
43
43
|
"checkpoints": {
|
|
44
44
|
name: "checkpoints",
|
|
45
45
|
version: "1.1.0",
|
|
46
|
-
contentHash: "
|
|
46
|
+
contentHash: "d4dc6c956be3dd279b225bc5061ec4255babc44ffb5ca58646c9b6ea04a0c8c2",
|
|
47
47
|
sourcePath: "packages/@dzhechkov/harness-core/src/feature-adr-checkpoints.ts",
|
|
48
48
|
requires: [],
|
|
49
|
-
exports: ["CHECKPOINT_STAGES","STAGE_ARTIFACTS","CHECKPOINT_MAX_RESULT_CHARS","CKPT_SCHEMA_VERSION","fnv1a","fnv1a64","checkpointInputHash","resumeMode","decideCheckpointResume","serializeCheckpoint","CHECKPOINT_LS_SENTINEL","parseCheckpointRead","shellQuote","checkpointReadCmd","checkpointAppendCmd"],
|
|
50
|
-
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: null,\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-
|
|
49
|
+
exports: ["CHECKPOINT_STAGES","STAGE_ARTIFACTS","CHECKPOINT_MAX_RESULT_CHARS","CKPT_SCHEMA_VERSION","fnv1a","fnv1a64","checkpointInputHash","resumeMode","decideCheckpointResume","serializeCheckpoint","CHECKPOINT_LS_SENTINEL","parseCheckpointRead","shellQuote","checkpointReadCmd","checkpointAppendCmd","DESIGN_SUBSTAGES","designStageKey","decideDesignFanResume","parseArtifactProbe"],
|
|
50
|
+
code: "const CHECKPOINT_STAGES = ['router', 'design', 'plan', 'code', 'qe', 'fleet'];\nconst STAGE_ARTIFACTS = {\n router: null,\n design: '01_requirements.md',\n plan: '06_implementation_plan.md',\n code: '07_code_changes/change_manifest.md',\n qe: '08_qe_report.md',\n fleet: '09_fleet_qe_assessment.md',\n};\nconst CHECKPOINT_MAX_RESULT_CHARS = 12000;\nconst CKPT_SCHEMA_VERSION = 'fa-ckpt-3';\nfunction fnv1a(str) {\n let h = 0x811c9dc5;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;\n }\n return h.toString(16).padStart(8, '0');\n}\nfunction fnv1a64(str) {\n return fnv1a(str) + fnv1a('fa-ckpt-salt' + str);\n}\nfunction checkpointInputHash(stage, parts) {\n return fnv1a64(JSON.stringify([CKPT_SCHEMA_VERSION, stage, ...parts.map((p) => (p === undefined ? null : p))]));\n}\nfunction resumeMode(raw) {\n return raw === 'never' ? 'never' : raw === 'force' ? 'force' : 'auto';\n}\nconst DESIGN_SUBSTAGES = ['requirements', 'adr', 'qcsd', 'architecture'];\nfunction designStageKey(sub) {\n return 'design:' + sub;\n}\nfunction decideDesignFanResume(opts) {\n const missingSubstages = [];\n opts.required.forEach((sub, i) => {\n const r = opts.results[i];\n if (r === null || r === undefined)\n missingSubstages.push(sub);\n });\n const missingArtifacts = [];\n let probeMissing = false;\n if (opts.artifacts.length > 0) {\n if (opts.postRunListing === null)\n probeMissing = true;\n else\n for (const rel of opts.artifacts)\n if (!opts.postRunListing.has(rel))\n missingArtifacts.push(rel);\n }\n const reason = missingSubstages.length > 0 ? 'substage-missing'\n : probeMissing ? 'probe-not-established'\n : missingArtifacts.length > 0 ? 'artifact-missing'\n : 'ok';\n return { complete: reason === 'ok', missingSubstages, missingArtifacts, reason };\n}\nfunction decideCheckpointResume(opts) {\n if (opts.mode === 'never')\n return { resume: false, reason: 'mode-never' };\n if (!opts.entry || opts.entry.result === null || opts.entry.result === undefined) {\n return { resume: false, reason: 'no-checkpoint' };\n }\n if (opts.entry.inputHash !== opts.inputHash)\n return { resume: false, reason: 'stale-input' };\n if (opts.mode === 'force')\n return { resume: true, reason: 'resumed-force' };\n const required = opts.artifactRel === null ? [] : (typeof opts.artifactRel === 'string' ? [opts.artifactRel] : opts.artifactRel);\n for (const rel of required) {\n if (!opts.listing.has(rel))\n return { resume: false, reason: 'artifact-missing' };\n }\n return { resume: true, reason: 'resumed' };\n}\nfunction serializeCheckpoint(stage, inputHash, result) {\n if (result === null || result === undefined)\n return null;\n let line;\n try {\n line = JSON.stringify({ stage, inputHash, result });\n }\n catch {\n return null;\n }\n if (typeof line !== 'string' || line.length > CHECKPOINT_MAX_RESULT_CHARS)\n return null;\n return line;\n}\nconst CHECKPOINT_LS_SENTINEL = '---FA-CKPT-LS---';\nfunction parseCheckpointRead(text) {\n const out = { entries: {}, listing: new Set(), malformedLines: 0 };\n const raw = String(text ?? '');\n const lines = raw.split('\\n');\n const sentinelAt = lines.findIndex((l) => l.trim() === CHECKPOINT_LS_SENTINEL);\n const body = sentinelAt === -1 ? lines : lines.slice(0, sentinelAt);\n const ls = sentinelAt === -1 ? [] : lines.slice(sentinelAt + 1);\n for (const line of body) {\n const t = line.trim();\n if (t === '')\n continue;\n try {\n const e = JSON.parse(t);\n if (e && typeof e === 'object' && typeof e.stage === 'string' && typeof e.inputHash === 'string' && 'result' in e && e.result !== null && e.result !== undefined) {\n out.entries[e.stage] = e;\n }\n else {\n if (e && typeof e === 'object' && typeof e.stage === 'string')\n delete out.entries[e.stage];\n out.malformedLines++;\n }\n }\n catch {\n out.malformedLines++;\n }\n }\n for (const line of ls) {\n const t = line.trim();\n if (t !== '')\n out.listing.add(t);\n }\n return out;\n}\nfunction shellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction checkpointReadCmd(fdirAbs) {\n const q = shellQuote(fdirAbs);\n return ('cat ' + q + '/.fa-state/checkpoints.jsonl 2>/dev/null || true; ' +\n \"echo '\" + CHECKPOINT_LS_SENTINEL + \"'; \" +\n 'cd ' + q + ' 2>/dev/null && find . -maxdepth 2 -type f 2>/dev/null | sed \"s|^\\\\./||\" || true');\n}\nfunction checkpointAppendCmd(fdirAbs, line) {\n const dir = shellQuote(fdirAbs + '/.fa-state');\n const file = shellQuote(fdirAbs + '/.fa-state/checkpoints.jsonl');\n return 'mkdir -p ' + dir + \" && printf '%s\\\\n' \" + shellQuote(line) + ' >> ' + file;\n}\nfunction parseArtifactProbe(opts) {\n if (opts.stdout === null || opts.stdout === undefined)\n return null;\n const known = new Map();\n for (const rel of opts.required)\n known.set('HAVE:' + rel, rel);\n const found = new Set();\n let sentinels = 0;\n for (const raw of String(opts.stdout).split('\\n')) {\n const line = raw.trim();\n if (line === '')\n continue;\n if (line === opts.sentinel) {\n sentinels++;\n continue;\n }\n if (sentinels > 0)\n return null;\n const rel = known.get(line);\n if (rel === undefined)\n return null;\n found.add(rel);\n }\n if (sentinels !== 1)\n return null;\n return found;\n}",
|
|
51
51
|
},
|
|
52
52
|
"training-pairs": {
|
|
53
53
|
name: "training-pairs",
|
|
@@ -97,11 +97,11 @@ export const BLOBS: Record<string, LoopBlob> = {
|
|
|
97
97
|
"trace": {
|
|
98
98
|
name: "trace",
|
|
99
99
|
version: "1.1.0",
|
|
100
|
-
contentHash: "
|
|
100
|
+
contentHash: "1270f9aac5fbe5693af8bcc26af55e81aeb41ef4b911dbda72c23ea248072344",
|
|
101
101
|
sourcePath: "packages/@dzhechkov/harness-core/src/loop-trace.ts",
|
|
102
102
|
requires: [],
|
|
103
103
|
exports: ["LOOP_TRACE_SCHEMA_VERSION","TRACE_RUNID_RE","TRACE_KEY_RE","traceShellQuote","traceValidateEvent","traceInit","traceOnDispatch","traceOnSettle","traceClose","traceFlushCmd","traceFaRecordCmd","traceLedgerLine","traceLedgerAppendCmd"],
|
|
104
|
-
code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
|
|
104
|
+
code: "const LOOP_TRACE_SCHEMA_VERSION = 1;\nconst TRACE_RUNID_RE = /^[a-z0-9-]{1,40}$/;\nconst TRACE_KEY_RE = /^[a-z0-9_.:-]{1,64}$/i;\nfunction traceShellQuote(s) {\n return \"'\" + String(s).replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction traceValidateEvent(e) {\n if (typeof e !== 'object' || e === null || Array.isArray(e))\n return 'event must be an object';\n const ev = e;\n if (ev['v'] !== 1)\n return 'v must be 1';\n if (typeof ev['runId'] !== 'string' || !TRACE_RUNID_RE.test(ev['runId']))\n return 'runId fails its VO regex';\n if (typeof ev['seq'] !== 'number' || !Number.isInteger(ev['seq']) || ev['seq'] < 1)\n return 'seq must be a positive integer';\n const kind = ev['event'];\n if (kind === 'dispatched') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (typeof ev['stepId'] !== 'string' || !TRACE_KEY_RE.test(ev['stepId']))\n return 'stepId fails its VO regex';\n if (ev['itemKey'] !== null && (typeof ev['itemKey'] !== 'string' || !TRACE_KEY_RE.test(ev['itemKey'])))\n return 'itemKey fails its VO regex';\n if (typeof ev['attempt'] !== 'number' || ev['attempt'] < 1)\n return 'attempt must be >= 1';\n if (typeof ev['phase'] !== 'string' || ev['phase'] === '')\n return 'phase required';\n if (!Array.isArray(ev['causedBy']) || ev['causedBy'].some((n) => typeof n !== 'number'))\n return 'causedBy must be a number array';\n return null;\n }\n if (kind === 'settled') {\n if (typeof ev['invocationId'] !== 'string' || ev['invocationId'] === '')\n return 'invocationId required';\n if (ev['outcome'] !== 'ok' && ev['outcome'] !== 'null' && ev['outcome'] !== 'error')\n return 'outcome must be ok|null|error';\n return null;\n }\n if (kind === 'run.opened') {\n if (typeof ev['planDigest'] !== 'string' || typeof ev['execFp'] !== 'string')\n return 'run.opened needs planDigest + execFp';\n const ep = ev['emitterPath'];\n if (ep !== undefined && ep !== 'dz-process' && ep !== 'rendered-script')\n return 'emitterPath must be dz-process|rendered-script';\n return null;\n }\n if (kind === 'run.closed') {\n const c = ev['counts'];\n if (typeof c !== 'object' || c === null)\n return 'run.closed needs counts';\n return null;\n }\n return 'unknown event kind';\n}\nfunction traceInit(runId, planDigest, execFp, emitterPath) {\n if (!TRACE_RUNID_RE.test(runId))\n throw new Error('loop-trace: runId fails ' + String(TRACE_RUNID_RE));\n const state = { runId, seq: 0, dispatched: 0, settled: 0, buffer: [] };\n const opened = { v: 1, runId, seq: ++state.seq, event: 'run.opened', planDigest, execFp, emitterPath };\n traceBuffer(state, opened);\n return state;\n}\nfunction traceBuffer(state, e) {\n const err = traceValidateEvent(e);\n if (err !== null)\n throw new Error('loop-trace: refusing non-conforming event (' + err + ') — the authoritative ordering source is never repaired later');\n state.buffer.push(JSON.stringify(e));\n}\nfunction traceOnDispatch(state, e) {\n const seq = ++state.seq;\n state.dispatched++;\n traceBuffer(state, {\n v: 1,\n runId: state.runId,\n seq,\n event: 'dispatched',\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n attempt: e.attempt,\n phase: e.phase,\n model: e.model,\n causedBy: e.causedBy,\n });\n return seq;\n}\nfunction traceOnSettle(state, e) {\n const seq = ++state.seq;\n state.settled++;\n traceBuffer(state, { v: 1, runId: state.runId, seq, event: 'settled', invocationId: e.invocationId, outcome: e.outcome });\n return seq;\n}\nfunction traceClose(state) {\n const closed = {\n v: 1,\n runId: state.runId,\n seq: ++state.seq,\n event: 'run.closed',\n counts: { dispatched: state.dispatched, settled: state.settled },\n };\n traceBuffer(state, closed);\n}\nfunction traceFlushCmd(state, traceFileAbs) {\n if (state.buffer.length === 0)\n return null;\n const lines = state.buffer.splice(0, state.buffer.length);\n const file = traceShellQuote(traceFileAbs);\n const dir = traceShellQuote(traceFileAbs.replace(/\\/[^/]*$/, ''));\n const printfs = lines\n .map((l) => \"printf '%s\\\\n' \" + traceShellQuote(l) + ' | sed \"s/}$/,\\\\\"wallTime\\\\\":\\\\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\\\"}/\" >> ' + file)\n .join(' && ');\n return 'mkdir -p ' + dir + ' && ' + printfs;\n}\nfunction traceFaRecordCmd(dzBin, slug, stepLabel, projectAbs) {\n if (typeof slug !== 'string' || slug === ''\n || typeof stepLabel !== 'string' || stepLabel === ''\n || typeof projectAbs !== 'string' || projectAbs === '')\n return null;\n const bin = typeof dzBin === 'string' && dzBin !== '' ? dzBin : 'dz';\n const cmd = traceShellQuote(bin) + ' statusline --fa-record --slug ' + traceShellQuote(slug)\n + ' --step ' + traceShellQuote(stepLabel) + ' --kind loop --project ' + traceShellQuote(projectAbs);\n return cmd + ' >/dev/null 2>&1';\n}\nfunction traceLedgerLine(opts) {\n try {\n if (typeof opts.slug !== 'string' || opts.slug === '')\n return null;\n const agents = typeof opts.agents === 'number'\n && Number.isFinite(opts.agents)\n && Number.isInteger(opts.agents)\n && opts.agents >= 0\n ? opts.agents\n : 0;\n const date = typeof opts.date === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(opts.date) ? opts.date : null;\n const outcome = typeof opts.outcome === 'string' && opts.outcome !== '' ? opts.outcome : 'unknown';\n const line = JSON.stringify({\n slug: opts.slug,\n stage: 'loop-run',\n tier: null,\n tokens: null,\n minutes: null,\n agents,\n coder: null,\n grade: null,\n date,\n auto: true,\n outcome,\n runId: typeof opts.runId === 'string' ? opts.runId : null,\n planDigest: typeof opts.planDigest === 'string' ? opts.planDigest : null,\n });\n return line.length <= 4000 ? line : null;\n }\n catch {\n return null;\n }\n}\nfunction traceLedgerAppendCmd(repoAbs, line) {\n if (typeof repoAbs !== 'string' || repoAbs === '' || typeof line !== 'string' || line === '')\n return null;\n const dir = traceShellQuote(repoAbs + '/.dz/feature-adr');\n const file = traceShellQuote(repoAbs + '/.dz/feature-adr/run-cost-ledger.jsonl');\n return 'mkdir -p ' + dir\n + \" && printf '%s' \" + traceShellQuote(line)\n + ' | sed \"s/\\\\\"date\\\\\":null/\\\\\"date\\\\\":\\\\\"$(date -u +%Y-%m-%d)\\\\\"/\" >> ' + file\n + \" && printf '\\\\n' >> \" + file\n + ' && echo LEDGER-OK';\n}\nfunction invocations(run) {\n const out = new Map();\n for (const e of run.events) {\n if (e.event === 'dispatched') {\n out.set(e.invocationId, {\n invocationId: e.invocationId,\n stepId: e.stepId,\n itemKey: e.itemKey,\n dispatchSeq: e.seq,\n settleSeq: null,\n causedBy: e.causedBy,\n });\n }\n else if (e.event === 'settled') {\n const inv = out.get(e.invocationId);\n if (inv)\n inv.settleSeq = e.seq;\n }\n }\n return [...out.values()];\n}",
|
|
105
105
|
},
|
|
106
106
|
"loop-semantics": {
|
|
107
107
|
name: "loop-semantics",
|
package/src/loop-render.ts
CHANGED
|
@@ -653,7 +653,7 @@ function renderRuntime(plan: LoopPlan, planDig: string, execFp: string, blobs: L
|
|
|
653
653
|
}
|
|
654
654
|
if (traceOn) {
|
|
655
655
|
lines.push(`// trace wiring (blob-provided emitter; hooks INSIDE runStep — ADR-003)`);
|
|
656
|
-
lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP)`);
|
|
656
|
+
lines.push(`const __traceState = traceInit(RUN_ID, PLAN_DIGEST, EXEC_FP, 'rendered-script')`);
|
|
657
657
|
lines.push(`__hooks.onDispatch = function (e) { return traceOnDispatch(__traceState, e) }`);
|
|
658
658
|
lines.push(`__hooks.onSettle = function (e) { return traceOnSettle(__traceState, e) }`);
|
|
659
659
|
lines.push(`async function __traceFlushNow(phaseName, stepLabel) {`);
|