@planu/cli 5.3.22 → 5.3.24

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/engine/evidence-index/done-drift.js +15 -9
  3. package/dist/engine/execution/validate-job-executor.js +22 -9
  4. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  5. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +4 -4
  6. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  7. package/dist/engine/planu-core.darwin-x64.node.sbom.json +4 -4
  8. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  9. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +4 -4
  10. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  11. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +4 -4
  12. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  13. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +4 -4
  14. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  15. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +4 -4
  16. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  17. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +4 -4
  18. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  19. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +4 -4
  20. package/dist/engine/readiness-checker.d.ts +5 -0
  21. package/dist/engine/readiness-checker.js +6 -7
  22. package/dist/engine/spec-format/technical-md-populator.js +37 -5
  23. package/dist/tools/challenge-spec.js +67 -2
  24. package/dist/tools/create-spec.d.ts +21 -0
  25. package/dist/tools/create-spec.js +92 -2
  26. package/dist/tools/schemas/validate-output-schema.d.ts +1 -0
  27. package/dist/tools/schemas/validate-output-schema.js +1 -0
  28. package/dist/tools/validate.js +4 -3
  29. package/package.json +9 -9
  30. package/planu-native.json +1 -1
  31. package/planu-plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## [5.3.24] - 2026-08-18
2
+
3
+ ### Bug Fixes
4
+ - fix(spec-1558): regenerate source-quality receipt bound to the amended release commit
5
+ - fix(spec-1553): declare attachedAt in the published validate output schema
6
+ - fix(spec-1553): name the receipt publication cause instead of one flat string
7
+ - fix(spec-1554): bound the Technical extractor to the section heading level
8
+ - fix(spec-1552): surface the stored rejection reason in done-drift diagnostics
9
+ - fix(spec-1549): resolve out-of-scope items from the spec document, not the cache
10
+ - fix(spec-1525): admit prose file paths outside src/ and tests/ without truncating them
11
+ - fix(spec-1540): scope the native lockfile mask to a version-controlled lockfile
12
+ - fix(spec-1524): bind the source-quality receipt to a mask-normalized tree digest
13
+
14
+ ### Chores
15
+ - chore(spec-1553): close lifecycle with reconciled scope and done evidence
16
+ - chore(spec-1524): close lifecycle with reconciled scope and done evidence
17
+
18
+
19
+ ## [5.3.23] - 2026-08-13
20
+
21
+ ### Bug Fixes
22
+ - fix(spec-1520): exclude paths whose only mention negates the work from grounded modify targets
23
+
24
+
1
25
  ## [5.3.22] - 2026-08-13
2
26
 
3
27
  ### Bug Fixes
@@ -4,6 +4,20 @@ function normalizedChangedFile(value) {
4
4
  const normalized = normalizeChangedFilePath(value);
5
5
  return normalized.status === 'valid' ? normalized.path : null;
6
6
  }
7
+ function withReason(status, reason) {
8
+ return reason ? `${status}: ${reason}` : status;
9
+ }
10
+ function formatStaleOrInvalidRecord(entry, record) {
11
+ if (record.kind !== 'changed-file') {
12
+ return `${entry.criterion}: ${record.value} (${withReason(record.status, record.reason)})`;
13
+ }
14
+ const normalized = normalizedChangedFile(record.value);
15
+ if (!normalized) {
16
+ return `${entry.criterion}: invalid changed-file (${record.status})`;
17
+ }
18
+ const normalizedReason = record.reason?.split(record.value).join(normalized);
19
+ return `${entry.criterion}: ${normalized} (${withReason(record.status, normalizedReason)})`;
20
+ }
7
21
  function buildNeverDriftPaths(ownSpecMdPath) {
8
22
  const neverDriftPaths = new Set(PLANU_BOOKKEEPING_FILES);
9
23
  const normalizedOwnSpecMdPath = ownSpecMdPath ? normalizedChangedFile(ownSpecMdPath) : null;
@@ -49,15 +63,7 @@ export function checkDoneDriftContract(args) {
49
63
  }
50
64
  const staleOrInvalid = args.index.criteria.flatMap((entry) => entry.evidence
51
65
  .filter((record) => record.status === 'stale' || record.status === 'invalid')
52
- .map((record) => {
53
- if (record.kind !== 'changed-file') {
54
- return `${entry.criterion}: ${record.value} (${record.status})`;
55
- }
56
- const normalized = normalizedChangedFile(record.value);
57
- return normalized
58
- ? `${entry.criterion}: ${normalized} (${record.status})`
59
- : `${entry.criterion}: invalid changed-file (${record.status})`;
60
- }));
66
+ .map((record) => formatStaleOrInvalidRecord(entry, record)));
61
67
  if (staleOrInvalid.length > 0) {
62
68
  issues.push({
63
69
  code: 'done_drift_stale_evidence',
@@ -22,7 +22,11 @@ import { MacOSKeychainOperationError } from '../../security/adapters/macos-keych
22
22
  import { createDurableJobHeartbeat } from './durable-job-heartbeat-runtime.js';
23
23
  const WORKSPACE_ID = 'global';
24
24
  const activeScopes = new Map();
25
- const RECEIPT_PENDING_ERROR = '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job';
25
+ const RECEIPT_PUBLICATION_FAILURE_MESSAGES = {
26
+ keychain_timeout: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=keychain_timeout: the signing key could not be read from the keychain before the read timed out',
27
+ publication_deadline_elapsed: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=publication_deadline_elapsed: the receipt publication deadline elapsed before the receipt was signed',
28
+ publication_interrupted: '[Planu] VALIDATION_RECEIPT_PENDING action=restart_job cause=publication_interrupted: receipt publication was interrupted after the receipt was signed and persisted; check the provenance files under handoffs/<spec>/*.provenance.json for an already-persisted receiptId before restarting',
29
+ };
26
30
  class ReceiptPublicationTimeoutError extends Error {
27
31
  constructor() {
28
32
  super('[Planu] Receipt publication deadline elapsed');
@@ -43,10 +47,17 @@ class ValidationAuthorityLostError extends Error {
43
47
  this.name = 'ValidationAuthorityLostError';
44
48
  }
45
49
  }
46
- function recoverableReceiptError(error) {
47
- return (error instanceof ReceiptPublicationTimeoutError ||
48
- error instanceof ReceiptPublicationInterruptedError ||
49
- (error instanceof MacOSKeychainOperationError && error.code === 'KEYCHAIN_TIMEOUT'));
50
+ function classifyReceiptPublicationFailure(error) {
51
+ if (error instanceof ReceiptPublicationTimeoutError) {
52
+ return 'publication_deadline_elapsed';
53
+ }
54
+ if (error instanceof ReceiptPublicationInterruptedError) {
55
+ return 'publication_interrupted';
56
+ }
57
+ if (error instanceof MacOSKeychainOperationError && error.code === 'KEYCHAIN_TIMEOUT') {
58
+ return 'keychain_timeout';
59
+ }
60
+ return undefined;
50
61
  }
51
62
  function decodeUtf8Prefix(encoded, prefixLength) {
52
63
  try {
@@ -321,13 +332,14 @@ export class ValidateJobExecutor {
321
332
  if (current.state === 'cancelled') {
322
333
  return current;
323
334
  }
324
- if (recoverableReceiptError(error) &&
335
+ const receiptFailureCause = classifyReceiptPublicationFailure(error);
336
+ if (receiptFailureCause &&
325
337
  parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
326
338
  return this.runtime.fail({
327
339
  ...identity,
328
340
  workerId: this.workerId,
329
341
  fencingToken,
330
- error: RECEIPT_PENDING_ERROR,
342
+ error: RECEIPT_PUBLICATION_FAILURE_MESSAGES[receiptFailureCause],
331
343
  });
332
344
  }
333
345
  if (parseValidationReceiptPendingCheckpoint(current.checkpoint, this.receiptCheckpointLimits())) {
@@ -430,12 +442,13 @@ export class ValidateJobExecutor {
430
442
  if (current.state === 'cancelled') {
431
443
  return current;
432
444
  }
433
- if (recoverableReceiptError(error)) {
445
+ const receiptFailureCause = classifyReceiptPublicationFailure(error);
446
+ if (receiptFailureCause) {
434
447
  return this.runtime.fail({
435
448
  ...identity,
436
449
  workerId: this.workerId,
437
450
  fencingToken,
438
- error: RECEIPT_PENDING_ERROR,
451
+ error: RECEIPT_PUBLICATION_FAILURE_MESSAGES[receiptFailureCause],
439
452
  });
440
453
  }
441
454
  this.recordFencedProgress(identity, fencingToken, {
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-aarch64-apple-darwin",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-aarch64-apple-darwin",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -17,15 +17,15 @@
17
17
  "platform": "darwin",
18
18
  "arch": "arm64",
19
19
  "libc": null,
20
- "artifactSha256": "4730c8bca1ecd506f9332cfd7754a3b2705272b7cfead67ed8a4a857ea51af47",
20
+ "artifactSha256": "2df1b275f0d3987ef6fa75a7d46b274c798aece674ff001ab7fb7af169cfc1f3",
21
21
  "sbom": {
22
22
  "format": "CycloneDX-1.5",
23
23
  "path": "planu-core.darwin-arm64.node.sbom.json",
24
- "sha256": "379172a509801b9bb9072e03b95da643218c75c02a8cc7e11719193f37f028aa"
24
+ "sha256": "9899ebee8d32c9b58992bd9a95647e85bd86ac76ee64c34f534f5ce18ec33c2c"
25
25
  },
26
26
  "provenance": {
27
27
  "builder": "scripts/build-rust-local.sh",
28
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
28
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
29
29
  "sourceTreeDirty": false,
30
30
  "sourceDateEpoch": 1,
31
31
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -34,6 +34,6 @@
34
34
  "signature": {
35
35
  "algorithm": "Ed25519",
36
36
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
37
- "value": "a6pf9ANIEnE+aYR1QZVf9LJOna7hPvbCmKSUoAYas2JtKtY5HkBvE/9FyN7j3PaG7P8SGXgCPNbAhZ4umyq2Dg=="
37
+ "value": "0/ir1zotrdZMQ3fKgbpHGoU6fXXo8qZp+Ugs18Oslbxb6vVvRBuj2UoKofDsyPGjx2mHadvTUPX42B/EZd1SAg=="
38
38
  }
39
39
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "4730c8bca1ecd506f9332cfd7754a3b2705272b7cfead67ed8a4a857ea51af47"
14
+ "content": "2df1b275f0d3987ef6fa75a7d46b274c798aece674ff001ab7fb7af169cfc1f3"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-x86_64-apple-darwin",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-x86_64-apple-darwin",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -17,15 +17,15 @@
17
17
  "platform": "darwin",
18
18
  "arch": "x64",
19
19
  "libc": null,
20
- "artifactSha256": "9204816afcda7990a7caa85991ced38335301a2db840a58d7c1017f72ad7fb44",
20
+ "artifactSha256": "136a1351ca4278942b7c5b0130eb7cdfee35b85b667888d4041bffcd0b213ace",
21
21
  "sbom": {
22
22
  "format": "CycloneDX-1.5",
23
23
  "path": "planu-core.darwin-x64.node.sbom.json",
24
- "sha256": "ba853b859c2a9a22ff223d48566f616d05ab8479b3656c8912dd8cbee8617f53"
24
+ "sha256": "3f8f6cace3e98634bc5fb5b8debb71c45507b890b4aa918437358cf16b55d776"
25
25
  },
26
26
  "provenance": {
27
27
  "builder": "scripts/build-rust-local.sh",
28
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
28
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
29
29
  "sourceTreeDirty": false,
30
30
  "sourceDateEpoch": 1,
31
31
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -34,6 +34,6 @@
34
34
  "signature": {
35
35
  "algorithm": "Ed25519",
36
36
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
37
- "value": "n8UTXIAzGoISDWY5vDfDxlQBqP5oso3K37jRE5+kTUnedRmCvVUwPlTedlxMKXI+JWaE/gyq5tOl5V8BNOtXBg=="
37
+ "value": "Aj0I1f16YmStyyCshvbh7h+leFhLBtGAOv5Jy8lCH5oXVc2RBirqfHNAU/4yQ1vFu3JTl3H5hIaa1Kc1LwQtCg=="
38
38
  }
39
39
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "9204816afcda7990a7caa85991ced38335301a2db840a58d7c1017f72ad7fb44"
14
+ "content": "136a1351ca4278942b7c5b0130eb7cdfee35b85b667888d4041bffcd0b213ace"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-aarch64-unknown-linux-gnu",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-aarch64-unknown-linux-gnu",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "arm64",
18
18
  "libc": "glibc",
19
- "artifactSha256": "acfa6ff4ed2e68598b779cd84eeac69900fd6911893d2ff26414a47304b2ccde",
19
+ "artifactSha256": "5c16c2a04af25efc03e74eecfacaad43f5835dfb01f0a898f6d2b93efd9a3c33",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-arm64-gnu.node.sbom.json",
23
- "sha256": "cebfe96400b95cf97111e7f04e1992a9c7fbbd4071b44154d33ec10f1d3046ed"
23
+ "sha256": "48a3812ed288b4801a82bf40f37d02e48e014a90f58f7bc955c2b480749cd44e"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "Xyq0s76WcCzbKEu0nFqrYE0lzt5+UWllzUqcKnpFuXXSJhrvtEyTIW4N37ldPqEeZ4tTsfVAcWooxqpMT8aECA=="
36
+ "value": "ogNVi4bjUVYnZ3o32PbiLKjRM4FhpcRuLoWSBpHe+vMe/M+oL+aZAX3M86UdTHoxEtUm32rJJPBBXA3CpQzIBQ=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "acfa6ff4ed2e68598b779cd84eeac69900fd6911893d2ff26414a47304b2ccde"
14
+ "content": "5c16c2a04af25efc03e74eecfacaad43f5835dfb01f0a898f6d2b93efd9a3c33"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-aarch64-unknown-linux-musl",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-aarch64-unknown-linux-musl",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "arm64",
18
18
  "libc": "musl",
19
- "artifactSha256": "b102c6bec361271c98c9940b50a37c2110fd0a4e7c8e96f7b12e904e1162e2a2",
19
+ "artifactSha256": "8a4cd204fed117c35b4daea2d03d7f36c5ac2309967ed1d0f48e4291c19a705f",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-arm64-musl.node.sbom.json",
23
- "sha256": "6d912c7181d379ae613ffb00e2e70af7beade6da0890b892b801e31ed77074c7"
23
+ "sha256": "812aeaa48a5c2d31b1b108a005eb47586f8a3cf48de44ee7a43b356d7626a286"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "rdhGtK396/2ZwQtLhz9D+o6VvucindJmLidQT3Yjy+A0dXp8IyT1MJm7g+KzVD6b+SfqWu6HgubuEgGfhyH9CQ=="
36
+ "value": "3w2XPj4GH8F/UdRBJMNWiOacR+v5h2H/aof6SqdJDADu5S2sGg0IEolD9m7sB3HE/ZgPL7iulY4KPckXGMNnAQ=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "b102c6bec361271c98c9940b50a37c2110fd0a4e7c8e96f7b12e904e1162e2a2"
14
+ "content": "8a4cd204fed117c35b4daea2d03d7f36c5ac2309967ed1d0f48e4291c19a705f"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-x86_64-unknown-linux-gnu",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-x86_64-unknown-linux-gnu",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "x64",
18
18
  "libc": "glibc",
19
- "artifactSha256": "5d2c48ac0de6d60629345143648c78e291a4cba707756face2957f27afb7d36b",
19
+ "artifactSha256": "a9158723fa6e3e5972ffc03b6115035c1f0dd6be8b109b4ae1f7f5dad54f9062",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-x64-gnu.node.sbom.json",
23
- "sha256": "d6125400832c0bdee2823af553d4e6b717b15cba47133ca88aaf91e2183ef6e1"
23
+ "sha256": "34a5c6540f4234a5ac3d4b3d1be2ff50e28b294161a4b4190996e28b3defc899"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "Cb1ueFKWd9mC0uzyo/kPIexFGBTxuVsJTk8knsTODmsR2ooH4rG/fqCqN2M1ZCO1kC7Zrpn5LwiifEcbUhUQBw=="
36
+ "value": "Gz3e+x1vn4R0ppFCaBwO4vQH1RqrV3PvTp7/Zi6V0mu3u6o1GYmo7JrLvlmR4I34IWkfO+cG6Hk/Ls++j7dnBw=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "5d2c48ac0de6d60629345143648c78e291a4cba707756face2957f27afb7d36b"
14
+ "content": "a9158723fa6e3e5972ffc03b6115035c1f0dd6be8b109b4ae1f7f5dad54f9062"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-x86_64-unknown-linux-musl",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-x86_64-unknown-linux-musl",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "linux",
17
17
  "arch": "x64",
18
18
  "libc": "musl",
19
- "artifactSha256": "43926c2400637b9670c3c869da140fedfa85dd786da4de04122c8f5022d3c5cc",
19
+ "artifactSha256": "165e48d9d7bb68965782b9f690af378cee9651c9c1b9ab31e6b9fa4bedad514a",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.linux-x64-musl.node.sbom.json",
23
- "sha256": "5db161ed910755ad5fe11ebc10d190f81fdf6b69779f21c048819f3be7bcb34e"
23
+ "sha256": "101e2eca5854d33b36a9570a10e8f4816f43961ad899863110ba4d2596cf89dc"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "0NDYIwRlP6yJ/LWIt3in/B9XD367mRJM0WRoX5H4eERYz4/jtuyKA9yPXUh0sYnnY3roOFctTkbd4eLtFmtqBQ=="
36
+ "value": "bHo9hflPuXFXSTyWqcxf2rV+OlBmqGvUMOgp4WV7TfKOC1nCUV7Ay/1LJXlgqN/XykbfHC8821FpHWLXQSnJDQ=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "43926c2400637b9670c3c869da140fedfa85dd786da4de04122c8f5022d3c5cc"
14
+ "content": "165e48d9d7bb68965782b9f690af378cee9651c9c1b9ab31e6b9fa4bedad514a"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-aarch64-pc-windows-msvc",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-aarch64-pc-windows-msvc",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "win32",
17
17
  "arch": "arm64",
18
18
  "libc": null,
19
- "artifactSha256": "2895b0fa8aad3dd46ad59f9b1e8a327dcbbaffe7357ea78830e232a292944920",
19
+ "artifactSha256": "363abf41c92e4a6d5e4f7cc821185cca9a84fe5fb99006b8e01d16d106bbfa52",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.win32-arm64-msvc.node.sbom.json",
23
- "sha256": "2e14b8772ac8aa9ec07793b8e1db4e72dd23541ed338a1fbe728884082fbe796"
23
+ "sha256": "5f31abd4a8f741b8a72701f428fa777b41a504c1c095f903bdf54b5ccd93b74e"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "r0Wa9w/3KI0OAHWMTjGqerm90i5pEtHM8VWAKrEb4oqqtxjUQaYhPhKubZfTE4YOdrvn+1RXdoVaU8jMScdSAA=="
36
+ "value": "Dj6P860+E1Yi4MGQd3lnA6OmAmf5EvL8mZqfiI81OwYH19GQwnKnBUi5vjh64I781B1lyOwUOPjU+jLpJ8lhCQ=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "2895b0fa8aad3dd46ad59f9b1e8a327dcbbaffe7357ea78830e232a292944920"
14
+ "content": "363abf41c92e4a6d5e4f7cc821185cca9a84fe5fb99006b8e01d16d106bbfa52"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
- "cliVersion": "5.3.22",
4
- "engineVersion": "5.3.22",
5
- "buildId": "1-5.3.22-x86_64-pc-windows-msvc",
3
+ "cliVersion": "5.3.24",
4
+ "engineVersion": "5.3.24",
5
+ "buildId": "1-5.3.24-x86_64-pc-windows-msvc",
6
6
  "protocolAbi": 1,
7
7
  "capabilityAbi": 2,
8
8
  "nodeApiAbi": 4,
@@ -16,15 +16,15 @@
16
16
  "platform": "win32",
17
17
  "arch": "x64",
18
18
  "libc": null,
19
- "artifactSha256": "832a639657313dad460661ba34672d01ac1fc5674a655362f7d936e48353c0e5",
19
+ "artifactSha256": "0b6374ac67d414e13e2e02be6be96a388ed9096f517587f812c533c5fb37992f",
20
20
  "sbom": {
21
21
  "format": "CycloneDX-1.5",
22
22
  "path": "planu-core.win32-x64-msvc.node.sbom.json",
23
- "sha256": "2fac480bb810bb3201adc6a82859ace5aeb0b46e62da7cd257fb8089d1ade10f"
23
+ "sha256": "aaaafe7c267638012a520782ad059014a0641b55005e2a09a5bace53a00d3d3e"
24
24
  },
25
25
  "provenance": {
26
26
  "builder": "scripts/build-rust-local.sh",
27
- "sourceCommit": "faabf932259a10c5cabfeedf510c23d686b45e83",
27
+ "sourceCommit": "888ffb9504055a10ea38d0c31dd0c9881139e88c",
28
28
  "sourceTreeDirty": false,
29
29
  "sourceDateEpoch": 1,
30
30
  "rustToolchain": "rustc 1.95.0 (59807616e 2026-04-14)",
@@ -33,6 +33,6 @@
33
33
  "signature": {
34
34
  "algorithm": "Ed25519",
35
35
  "keyId": "e2ee765cb5ad9fbdc433ece332bce26d746d5e350b3acde0721a4c8c6337ff7b",
36
- "value": "MudkeMsYJwviSCoe775V8V4/N8d1dgRiQNRBlaGGrozpQb/cavJjNWpL8CoQEVsjX9n1CwjOeTVPu45gDzqqDQ=="
36
+ "value": "zUMqIwI3WSWZ0QbesWdiZfnIlwAnwkhfwN2Us0lmUCtoKOtQEkqckxNZCY7tuxbzhS091T5LUp7Y+HOUeUViCg=="
37
37
  }
38
38
  }
@@ -4,14 +4,14 @@
4
4
  "version": 1,
5
5
  "metadata": {
6
6
  "component": {
7
- "bom-ref": "pkg:cargo/planu-core@5.3.22",
7
+ "bom-ref": "pkg:cargo/planu-core@5.3.24",
8
8
  "type": "library",
9
9
  "name": "planu-core",
10
- "version": "5.3.22",
10
+ "version": "5.3.24",
11
11
  "hashes": [
12
12
  {
13
13
  "alg": "SHA-256",
14
- "content": "832a639657313dad460661ba34672d01ac1fc5674a655362f7d936e48353c0e5"
14
+ "content": "0b6374ac67d414e13e2e02be6be96a388ed9096f517587f812c533c5fb37992f"
15
15
  }
16
16
  ]
17
17
  }
@@ -1106,7 +1106,7 @@
1106
1106
  "dependsOn": []
1107
1107
  },
1108
1108
  {
1109
- "ref": "pkg:cargo/planu-core@5.3.22",
1109
+ "ref": "pkg:cargo/planu-core@5.3.24",
1110
1110
  "dependsOn": [
1111
1111
  "pkg:cargo/core-foundation@0.10.1",
1112
1112
  "pkg:cargo/hmac@0.12.1",
@@ -17,6 +17,11 @@ import type { Spec, ReadinessReport } from '../types/index.js';
17
17
  * paragraphs inside the AC section are visible to checkSpecificityGate.
18
18
  */
19
19
  export declare function extractCriteriaLines(huContent: string): string[];
20
+ /**
21
+ * Extract the content of the `## Technical` section from a spec body.
22
+ * Returns an empty string when the section is absent.
23
+ */
24
+ export declare function extractTechnicalSection(body: string): string;
20
25
  /**
21
26
  * SPEC-784: Returns true when the ## Technical body contains a pointer
22
27
  * like "See technical.md" or "See `some-file` spec.md" — a placeholder
@@ -205,17 +205,16 @@ function buildRecommendations(score, huPoints, criteriaPoints, filesPoints, deps
205
205
  * Extract the content of the `## Technical` section from a spec body.
206
206
  * Returns an empty string when the section is absent.
207
207
  */
208
- function extractTechnicalSection(body) {
209
- // Match ## Technical (case-insensitive, optional trailing whitespace)
210
- const sectionRe = /^#{1,6}\s+technical\b[^\n]*/im;
208
+ export function extractTechnicalSection(body) {
209
+ const sectionRe = /^(#{1,6})\s+technical\b[^\n]*/im;
211
210
  const match = sectionRe.exec(body);
212
- if (!match) {
211
+ const headingHashes = match?.[1];
212
+ if (!match || !headingHashes) {
213
213
  return '';
214
214
  }
215
215
  const afterHeader = body.slice(match.index + match[0].length);
216
- // Content ends at the next same-or-higher heading or end of string
217
- const nextHeadingRe = /^#{1,6}\s+/m;
218
- const nextMatch = nextHeadingRe.exec(afterHeader);
216
+ const sameOrHigherHeadingRe = new RegExp(`^#{1,${String(headingHashes.length)}}\\s+`, 'm');
217
+ const nextMatch = sameOrHigherHeadingRe.exec(afterHeader);
219
218
  return nextMatch ? afterHeader.slice(0, nextMatch.index) : afterHeader;
220
219
  }
221
220
  /**
@@ -1,8 +1,8 @@
1
1
  // engine/spec-format/technical-md-populator.ts — SPEC-586: Parse spec body to populate ## Technical
2
2
  import { stat } from 'node:fs/promises';
3
- import { join } from 'node:path';
3
+ import { dirname, extname, join } from 'node:path';
4
4
  import { findMarkdownSectionRange } from './markdown-sections.js';
5
- const FILE_PATH_RE = /\b(?:src|tests)\/[a-zA-Z0-9/_.-]+\.[a-z]+\b/g;
5
+ const FILE_PATH_RE = /(?<![\w/.:-])\.?[\w-]+(?:\.[\w-]+)*(?:\/\.?[\w-]+(?:\.[\w-]+)*)+/g;
6
6
  function stripFencedBlocks(content) {
7
7
  const kept = [];
8
8
  let fence = null;
@@ -24,8 +24,8 @@ function stripFencedBlocks(content) {
24
24
  }
25
25
  return kept.join('\n');
26
26
  }
27
- const SLASH_PATH_BULLET_RE = /^-\s+`?((?:[\w.-]+\/)+[\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]*)`?(?=[\s`]|$)/;
28
- const BARE_FILE_BULLET_RE = /^-\s+`?([\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]*)`?\s*$/;
27
+ const SLASH_PATH_BULLET_RE = /^-\s+`?((?:[\w.-]+\/)+[\w-][\w.-]*(?:\.[a-zA-Z][a-zA-Z0-9]*)?)`?(?=[\s`]|$)/;
28
+ const BARE_FILE_BULLET_RE = /^-\s+`?([\w-][\w.-]*\.[a-zA-Z][a-zA-Z0-9]*|[\w-]+)`?\s*$/;
29
29
  function extractPathsFromSection(sectionText) {
30
30
  const paths = [];
31
31
  for (const line of sectionText.split('\n')) {
@@ -81,6 +81,32 @@ async function categorizeByExistence(paths, projectPath) {
81
81
  }
82
82
  return { create, modify, test };
83
83
  }
84
+ function hasLetterInitialExtension(candidate) {
85
+ return /^[a-zA-Z]/.test(extname(candidate).slice(1));
86
+ }
87
+ async function isExistingFile(path) {
88
+ try {
89
+ return (await stat(path)).isFile();
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ async function isExistingDirectory(path) {
96
+ try {
97
+ return (await stat(path)).isDirectory();
98
+ }
99
+ catch {
100
+ return false;
101
+ }
102
+ }
103
+ async function isAdmissibleProseCandidate(candidate, projectPath) {
104
+ const full = join(projectPath, candidate);
105
+ if (await isExistingFile(full)) {
106
+ return true;
107
+ }
108
+ return hasLetterInitialExtension(candidate) && (await isExistingDirectory(dirname(full)));
109
+ }
84
110
  /**
85
111
  * Attempt to extract file paths from spec body for auto-populating ## Technical.
86
112
  * Returns null when no structured file section is found (caller uses placeholder).
@@ -91,9 +117,15 @@ export async function extractFilesFromSpecBody(specBody, projectPath) {
91
117
  (structured.create.length > 0 || structured.modify.length > 0 || structured.test.length > 0)) {
92
118
  return structured;
93
119
  }
94
- const allPaths = [
120
+ const candidates = [
95
121
  ...new Set(Array.from(stripFencedBlocks(specBody).matchAll(FILE_PATH_RE), (m) => m[0])),
96
122
  ];
123
+ const allPaths = [];
124
+ for (const candidate of candidates) {
125
+ if (await isAdmissibleProseCandidate(candidate, projectPath)) {
126
+ allPaths.push(candidate);
127
+ }
128
+ }
97
129
  if (allPaths.length === 0) {
98
130
  return null;
99
131
  }
@@ -1,6 +1,7 @@
1
1
  // tools/challenge-spec.ts — Devil's advocate resilience testing (SPEC-595: elicitation added)
2
2
  // Analyzes a spec from adversarial perspectives: failure scenarios,
3
3
  // concurrency issues, scale limits, security holes, and data consistency.
4
+ import { readFile } from 'node:fs/promises';
4
5
  import { specStore, knowledgeStore } from '../storage/index.js';
5
6
  // SPEC-1011 Bug F: fallback resolver using disk fingerprints
6
7
  import { resolveProjectFromPath } from '../storage/project-resolver.js';
@@ -27,6 +28,69 @@ const ALL_FOCUS_AREAS = [
27
28
  'security',
28
29
  'data-consistency',
29
30
  ];
31
+ function extractOutOfScopeSection(content) {
32
+ const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
33
+ const fencePattern = /^[ \t]{0,3}(`{3,}|~{3,})/;
34
+ let fence = null;
35
+ let found = false;
36
+ let capturing = false;
37
+ const captured = [];
38
+ for (const line of content.split('\n')) {
39
+ const fenceMatch = fencePattern.exec(line);
40
+ if (fenceMatch?.[1]) {
41
+ const marker = fenceMatch[1].charAt(0);
42
+ if (fence === null) {
43
+ fence = { marker, length: fenceMatch[1].length };
44
+ }
45
+ else if (fence.marker === marker && fenceMatch[1].length >= fence.length) {
46
+ fence = null;
47
+ }
48
+ if (capturing) {
49
+ captured.push(line);
50
+ }
51
+ continue;
52
+ }
53
+ if (fence !== null) {
54
+ if (capturing) {
55
+ captured.push(line);
56
+ }
57
+ continue;
58
+ }
59
+ const heading = headingPattern.exec(line);
60
+ if (heading?.[1] && heading[2] !== undefined) {
61
+ if (capturing) {
62
+ break;
63
+ }
64
+ if (!found && heading[2].trim().toLowerCase() === 'out of scope') {
65
+ found = true;
66
+ capturing = true;
67
+ }
68
+ continue;
69
+ }
70
+ if (capturing) {
71
+ captured.push(line);
72
+ }
73
+ }
74
+ return found ? captured.join('\n') : null;
75
+ }
76
+ async function resolveDocumentOutOfScope(spec) {
77
+ if (!spec.specPath) {
78
+ return [];
79
+ }
80
+ let fileContent;
81
+ try {
82
+ fileContent = await readFile(spec.specPath, 'utf-8');
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ const sectionContent = extractOutOfScopeSection(fileContent);
88
+ return sectionContent === null ? [] : extractListItems(sectionContent);
89
+ }
90
+ async function resolveOutOfScopeItems(spec) {
91
+ const documentItems = await resolveDocumentOutOfScope(spec);
92
+ return documentItems.length > 0 ? documentItems : (spec.outOfScope ?? []);
93
+ }
30
94
  /**
31
95
  * SPEC-615 AC3: detect contradictions between spec criteria and prior decisions.
32
96
  * Mutates failureScenarios in place. Best-effort — never throws.
@@ -150,12 +214,13 @@ export async function handleChallengeSpec(args, server) {
150
214
  }
151
215
  failureScenarios.push(...collectCapabilityScenarios({ spec, specContent, knowledge, focusAreas, capabilities }));
152
216
  // SPEC-612: Check for contradictions between outOfScope and acceptance criteria
153
- if (spec.outOfScope !== undefined && spec.outOfScope.length > 0) {
217
+ const resolvedOutOfScope = await resolveOutOfScopeItems(spec);
218
+ if (resolvedOutOfScope.length > 0) {
154
219
  const criteriaRange = findMarkdownSectionRange(specContent, 'Acceptance Criteria');
155
220
  const criteriaTexts = criteriaRange === null
156
221
  ? []
157
222
  : extractListItems(specContent.slice(criteriaRange.contentStart, criteriaRange.end));
158
- const contradictionResult = checkScopeContradictions(spec.outOfScope, criteriaTexts);
223
+ const contradictionResult = checkScopeContradictions(resolvedOutOfScope, criteriaTexts);
159
224
  for (const c of contradictionResult.contradictions) {
160
225
  failureScenarios.push({
161
226
  scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}"`,
@@ -1,5 +1,26 @@
1
1
  import type { CreateSpecInput, ToolResult } from '../types/index.js';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { type LeanFileEntry } from '../engine/spec-format/lean-technical-generator.js';
4
+ import { type AutopilotAnalysis } from './create-spec/autopilot-analyzer.js';
5
+ import type { TechnicalReferenceGroundingRecord } from '../types/spec-grounding.js';
6
+ export declare function groundTechnicalFiles(input: {
7
+ files: {
8
+ create: LeanFileEntry[];
9
+ modify: LeanFileEntry[];
10
+ test: LeanFileEntry[];
11
+ };
12
+ projectPath: string;
13
+ userInput: string;
14
+ autopilot: AutopilotAnalysis;
15
+ }): Promise<{
16
+ files: {
17
+ create: LeanFileEntry[];
18
+ modify: LeanFileEntry[];
19
+ test: LeanFileEntry[];
20
+ };
21
+ records: TechnicalReferenceGroundingRecord[];
22
+ advisoryFiles: LeanFileEntry[];
23
+ }>;
3
24
  export declare function resolveGroundedVerificationCommands(projectPath: string, testPaths: string[]): Promise<string[]>;
4
25
  export declare function handleCreateSpec(inputParams: CreateSpecInput, server?: McpServer): Promise<ToolResult>;
5
26
  //# sourceMappingURL=create-spec.d.ts.map
@@ -102,7 +102,7 @@ async function resolveTechnicalFiles(input) {
102
102
  }
103
103
  return { create: [], modify: [], test: [] };
104
104
  }
105
- async function groundTechnicalFiles(input) {
105
+ export async function groundTechnicalFiles(input) {
106
106
  const userInput = input.userInput.toLowerCase();
107
107
  const autopilotPaths = new Set([
108
108
  ...input.autopilot.suggestedFiles.modify.map((file) => file.path),
@@ -118,7 +118,7 @@ async function groundTechnicalFiles(input) {
118
118
  const advisoryFiles = [];
119
119
  for (const section of ['create', 'modify', 'test']) {
120
120
  for (const file of input.files[section]) {
121
- const pathInUserInput = userInput.includes(file.path.toLowerCase());
121
+ const pathInUserInput = pathSurvivesNegativeClauseCheck(userInput, file.path.toLowerCase());
122
122
  const exists = await pathExists(pathJoin(input.projectPath, file.path));
123
123
  const fromAutopilot = autopilotPaths.has(file.path);
124
124
  const derivedTest = section === 'test' && isDerivedFromGroundedSource(file.path, grounded.modify);
@@ -146,6 +146,96 @@ async function groundTechnicalFiles(input) {
146
146
  }
147
147
  return { files: grounded, records, advisoryFiles };
148
148
  }
149
+ const NEGATIVE_WORK_CLAUSE_RE = /\b(?:remains? unchanged|is not touched|not touched|must not (?:change|be changed|be modified|touch|be touched)|unchanged|out of scope)\b/;
150
+ const CLAUSE_DELIMITERS = ['\n', '. ', '; ', ', and ', ', but '];
151
+ const SENTENCE_DELIMITERS = ['\n', '. '];
152
+ const BACK_REFERENCE_SUBJECT_RE = /^(?:and\s+|but\s+)?(?:it|they|this|that|these|those)\s+(?:must|remains?|stays?|is|are|was|were|should|shall|will|cannot|can|does|do)\b/;
153
+ const PATH_LIKE_TOKEN_RE = /\S+\/\S*\.\w+/;
154
+ const CLAUSE_SPLIT_RE = new RegExp(CLAUSE_DELIMITERS.map((delimiter) => delimiter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
155
+ function computeDelimitedBounds(text, matchIndex, matchLength, delimiters) {
156
+ const startCandidates = delimiters.map((delimiter) => {
157
+ const index = text.lastIndexOf(delimiter, matchIndex);
158
+ return index === -1 ? -1 : index + delimiter.length;
159
+ });
160
+ const start = Math.max(0, ...startCandidates);
161
+ const searchAfter = matchIndex + matchLength;
162
+ const endCandidates = delimiters
163
+ .map((delimiter) => text.indexOf(delimiter, searchAfter))
164
+ .filter((value) => value !== -1);
165
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : text.length;
166
+ return [start, end];
167
+ }
168
+ function findClauseBounds(text, matchIndex, matchLength) {
169
+ return computeDelimitedBounds(text, matchIndex, matchLength, CLAUSE_DELIMITERS);
170
+ }
171
+ function findSentenceBounds(text, matchIndex, matchLength) {
172
+ return computeDelimitedBounds(text, matchIndex, matchLength, SENTENCE_DELIMITERS);
173
+ }
174
+ function splitClausesWithOffsets(sentence, sentenceStart) {
175
+ const clauses = [];
176
+ let cursor = 0;
177
+ CLAUSE_SPLIT_RE.lastIndex = 0;
178
+ let delimiterMatch = CLAUSE_SPLIT_RE.exec(sentence);
179
+ while (delimiterMatch !== null) {
180
+ clauses.push([sentence.slice(cursor, delimiterMatch.index), sentenceStart + cursor]);
181
+ cursor = delimiterMatch.index + delimiterMatch[0].length;
182
+ delimiterMatch = CLAUSE_SPLIT_RE.exec(sentence);
183
+ }
184
+ clauses.push([sentence.slice(cursor), sentenceStart + cursor]);
185
+ return clauses;
186
+ }
187
+ function sentenceHasBackReferencedNegation(userInput, matchIndex, matchLength) {
188
+ const [sentenceStart, sentenceEnd] = findSentenceBounds(userInput, matchIndex, matchLength);
189
+ const sentence = userInput.slice(sentenceStart, sentenceEnd);
190
+ const clauses = splitClausesWithOffsets(sentence, sentenceStart);
191
+ const matchEnd = matchIndex + matchLength;
192
+ const currentClauseIndex = clauses.findIndex(([clauseText, clauseStart]) => matchIndex >= clauseStart && matchEnd <= clauseStart + clauseText.length);
193
+ if (currentClauseIndex === -1) {
194
+ return false;
195
+ }
196
+ const currentClauseEntry = clauses[currentClauseIndex];
197
+ if (currentClauseEntry === undefined) {
198
+ return false;
199
+ }
200
+ const [currentClauseText, currentClauseStart] = currentClauseEntry;
201
+ const currentClauseTail = currentClauseText.slice(matchEnd - currentClauseStart);
202
+ if (PATH_LIKE_TOKEN_RE.test(currentClauseTail)) {
203
+ return false;
204
+ }
205
+ for (let index = currentClauseIndex + 1; index < clauses.length; index += 1) {
206
+ const clauseEntry = clauses[index];
207
+ if (clauseEntry === undefined) {
208
+ continue;
209
+ }
210
+ const [clauseText] = clauseEntry;
211
+ if (PATH_LIKE_TOKEN_RE.test(clauseText)) {
212
+ return false;
213
+ }
214
+ const trimmedClause = clauseText.replace(/^\s+/, '');
215
+ if (BACK_REFERENCE_SUBJECT_RE.test(trimmedClause) &&
216
+ NEGATIVE_WORK_CLAUSE_RE.test(trimmedClause)) {
217
+ return true;
218
+ }
219
+ }
220
+ return false;
221
+ }
222
+ function pathSurvivesNegativeClauseCheck(userInput, lowerPath) {
223
+ let searchFrom = 0;
224
+ for (;;) {
225
+ const matchIndex = userInput.indexOf(lowerPath, searchFrom);
226
+ if (matchIndex === -1) {
227
+ return false;
228
+ }
229
+ const [clauseStart, clauseEnd] = findClauseBounds(userInput, matchIndex, lowerPath.length);
230
+ const clause = userInput.slice(clauseStart, clauseEnd);
231
+ const ownClauseNegated = NEGATIVE_WORK_CLAUSE_RE.test(clause);
232
+ const backReferencedNegation = sentenceHasBackReferencedNegation(userInput, matchIndex, lowerPath.length);
233
+ if (!ownClauseNegated && !backReferencedNegation) {
234
+ return true;
235
+ }
236
+ searchFrom = matchIndex + lowerPath.length;
237
+ }
238
+ }
149
239
  function isDerivedFromGroundedSource(testPath, modifyFiles) {
150
240
  return modifyFiles.some((file) => {
151
241
  const withoutSrc = file.path.startsWith('src/') ? file.path.slice(4) : file.path;
@@ -243,6 +243,7 @@ export declare const ValidateOutputSchema: {
243
243
  running: "running";
244
244
  }>;
245
245
  submittedAt: z.ZodISODateTime;
246
+ attachedAt: z.ZodISODateTime;
246
247
  cursor: z.ZodString;
247
248
  queue: z.ZodObject<{
248
249
  status: z.ZodEnum<{
@@ -194,6 +194,7 @@ export const ValidateOutputSchema = {
194
194
  workspaceId: z.string().min(1).max(128),
195
195
  state: z.enum(['accepted', 'running', 'checkpointed']),
196
196
  submittedAt: z.iso.datetime(),
197
+ attachedAt: z.iso.datetime(),
197
198
  cursor: z.string().regex(/^[a-f0-9]{64}$/),
198
199
  queue: z.object({
199
200
  status: z.enum(['active', 'queued', 'terminal']),
@@ -212,7 +212,7 @@ export async function handleValidate(args) {
212
212
  if (active.state === 'accepted' || active.state === 'checkpointed') {
213
213
  scheduleValidateJobExecution(projectId, active.workspaceId);
214
214
  }
215
- return validationAcceptedResult(active, projectId, runtime);
215
+ return validationAcceptedResult(active, projectId, runtime, { justCreated: false });
216
216
  }
217
217
  const job = createPendingValidationSubmission({
218
218
  runtime,
@@ -228,7 +228,7 @@ export async function handleValidate(args) {
228
228
  if (job.state === 'accepted' || job.state === 'checkpointed') {
229
229
  scheduleValidateJobExecution(projectId, job.workspaceId);
230
230
  }
231
- return validationAcceptedResult(job, projectId, runtime);
231
+ return validationAcceptedResult(job, projectId, runtime, { justCreated: true });
232
232
  }
233
233
  catch (e_1) {
234
234
  env_1.error = e_1;
@@ -276,7 +276,7 @@ async function acquireValidationSubmissionLock(projectId, specId) {
276
276
  }
277
277
  throw new Error('[Planu] Timed out waiting for the validation submission lock');
278
278
  }
279
- function validationAcceptedResult(job, projectId, runtime) {
279
+ function validationAcceptedResult(job, projectId, runtime, { justCreated }) {
280
280
  const observation = runtime.observe(job);
281
281
  const accepted = {
282
282
  schemaVersion: 1,
@@ -285,6 +285,7 @@ function validationAcceptedResult(job, projectId, runtime) {
285
285
  workspaceId: job.workspaceId,
286
286
  state: job.state,
287
287
  submittedAt: job.createdAt,
288
+ attachedAt: justCreated ? job.createdAt : new Date().toISOString(),
288
289
  cursor: publicJobCursor(observation),
289
290
  queue: observation.queue,
290
291
  terminalStates: ['completed', 'failed', 'cancelled', 'dead-letter'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.22",
3
+ "version": "5.3.24",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,14 +36,14 @@
36
36
  "packageName": "@planu/core"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@planu/core-darwin-arm64": "5.3.22",
40
- "@planu/core-darwin-x64": "5.3.22",
41
- "@planu/core-linux-arm64-gnu": "5.3.22",
42
- "@planu/core-linux-arm64-musl": "5.3.22",
43
- "@planu/core-linux-x64-gnu": "5.3.22",
44
- "@planu/core-linux-x64-musl": "5.3.22",
45
- "@planu/core-win32-arm64-msvc": "5.3.22",
46
- "@planu/core-win32-x64-msvc": "5.3.22"
39
+ "@planu/core-darwin-arm64": "5.3.24",
40
+ "@planu/core-darwin-x64": "5.3.24",
41
+ "@planu/core-linux-arm64-gnu": "5.3.24",
42
+ "@planu/core-linux-arm64-musl": "5.3.24",
43
+ "@planu/core-linux-x64-gnu": "5.3.24",
44
+ "@planu/core-linux-x64-musl": "5.3.24",
45
+ "@planu/core-win32-arm64-msvc": "5.3.24",
46
+ "@planu/core-win32-x64-msvc": "5.3.24"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "5.3.22",
4
+ "version": "5.3.24",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "5.3.22",
5
+ "version": "5.3.24",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",