diff --git a/.specsync/change-sequence.json b/.specsync/change-sequence.json index 6e9ea27..66369b1 100644 --- a/.specsync/change-sequence.json +++ b/.specsync/change-sequence.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "sequence": 22, - "id": "CHG-0022-tty-aware-output-under-the-git-porcelain-rule-issue-33", + "sequence": 23, + "id": "CHG-0023-watch-signal-handling-and-termination-semantics-issue-34", "acknowledged_collisions": [] } diff --git a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json index f7b776d..69dd308 100644 --- a/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json +++ b/.specsync/changes/CHG-0004-ship-aps-0-2-0-agent-ready-json-state-dir-watch-and-profile-filestate/verification-attempts.json @@ -624,6 +624,102 @@ "REQ-state-store-010", "REQ-state-store-011" ] + }, + { + "timestamp": 1784433618, + "commit": "74bc5f65bf74c531741d3974af3301901fa87d9b", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "cc5e57c94e1233874bbdf273f510e6c91bca4414fb8147d6b0c91de6d42109d1", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] + }, + { + "timestamp": 1784437771, + "commit": "612ca8753cbc8b063d7549e74f653f159889523c", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "a8206c48006dbd00569ebf8117ea5c3a07ab2eb80348cdf7e37a26baa06226e2", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] + }, + { + "timestamp": 1784438770, + "commit": "d39cd25e05dd197f80ae835b5baba39a6917db2f", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "8fcaebc50225b5f1bce4d95e06ff1192587cf1ce767813f3b550adbd09495178", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] + }, + { + "timestamp": 1784440389, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "73c000607b3050c54981b4c29532280dd08bf00b7a15c8ec1801e045fa5496c9", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-010", + "REQ-aps-cli-011", + "REQ-aps-cli-012", + "REQ-aps-cli-013", + "REQ-state-store-001", + "REQ-state-store-010", + "REQ-state-store-011" + ] } ] } diff --git a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json index 9771671..97abbff 100644 --- a/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json +++ b/.specsync/changes/CHG-0011-dogfood-observeddependency-demostats-for-issue-18/verification-attempts.json @@ -324,6 +324,42 @@ "REQ-aps-cli-014", "REQ-state-store-012" ] + }, + { + "timestamp": 1784433690, + "commit": "74bc5f65bf74c531741d3974af3301901fa87d9b", + "contract_digest": "1defd9f7ba999c7d4156cd0b80b236b4a173d5ba0985832e9f89b8b2ed5155ca", + "workspace_digest": "cc5e57c94e1233874bbdf273f510e6c91bca4414fb8147d6b0c91de6d42109d1", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-014", + "REQ-state-store-012" + ] + }, + { + "timestamp": 1784440418, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "1defd9f7ba999c7d4156cd0b80b236b4a173d5ba0985832e9f89b8b2ed5155ca", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-014", + "REQ-state-store-012" + ] } ] } diff --git a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json index 3426dfc..500d2ef 100644 --- a/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json +++ b/.specsync/changes/CHG-0012-dogfood-securestate-secret-keychain-demo-key-for-issue-16/verification-attempts.json @@ -323,6 +323,44 @@ "REQ-aps-cli-015", "REQ-state-store-013" ] + }, + { + "timestamp": 1784433741, + "commit": "74bc5f65bf74c531741d3974af3301901fa87d9b", + "contract_digest": "914c204b1d836d6b3de14e4908e2813862014a023e5d4244f1758d9486b3cd80", + "workspace_digest": "cc5e57c94e1233874bbdf273f510e6c91bca4414fb8147d6b0c91de6d42109d1", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-015", + "REQ-state-store-013" + ] + }, + { + "timestamp": 1784440461, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "914c204b1d836d6b3de14e4908e2813862014a023e5d4244f1758d9486b3cd80", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-001", + "REQ-aps-cli-015", + "REQ-state-store-013" + ] } ] } diff --git a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json index 59fae5a..2fbe343 100644 --- a/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json +++ b/.specsync/changes/CHG-0013-dogfood-appstate-slice-via-profilename-for-issue-17/verification-attempts.json @@ -270,6 +270,24 @@ "REQ-aps-cli-016", "REQ-state-store-014" ] + }, + { + "timestamp": 1784440555, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "55d93919744f623103fb1d77e2cdb833a2d95075e3fc62ac31c6d09d506223b6", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-016", + "REQ-state-store-014" + ] } ] } diff --git a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json index 60fc7b2..edfbdec 100644 --- a/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json +++ b/.specsync/changes/CHG-0016-loud-torn-filestate-reads-and-document-multi-writer-semantics-for-issue-38/verification-attempts.json @@ -180,6 +180,26 @@ "REQ-state-store-004", "REQ-state-store-015" ] + }, + { + "timestamp": 1784440606, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "2ce0d16b39555da39844a51a6ba4f24ee62c65751cf41af3d664905ba89b4158", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-005", + "REQ-aps-cli-017", + "REQ-state-store-004", + "REQ-state-store-015" + ] } ] } diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/approvals.json b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/approvals.json new file mode 100644 index 0000000..a6618c8 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/approvals.json @@ -0,0 +1,732 @@ +{ + "approvals": [ + { + "gate": "definition", + "actor": "0xLeif", + "timestamp": 1784424341, + "digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784424361, + "digest": "f13f1d8e9ddb217cadc43217caf93e5be51d1cedcd341e3be4fd3a095b4b0876", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784433799, + "digest": "4b20014652de2354fa495f65521089c1e568403589ecbdc0b0ec124e7fd6d189", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784437806, + "digest": "29774a2a9bd121935670180fdd2a5313c566eb3a3e44537614de5211962beb91", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784438805, + "digest": "73f13914de5b3b98580d7b136e930d66bedc03f783c6b94c10ee6f210fca1f89", + "note": null + }, + { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784440646, + "digest": "1fcdfd92d32fe9fe4d9086236a402588573cf244cb4275852e392b064e86d493", + "note": null + } + ], + "reopenings": [ + { + "schema_version": 1, + "change_id": "CHG-0021-watch-signal-handling-and-termination-semantics-issue-34", + "actor": "0xLeif", + "reason": "Harden SIGINT smoke wait for CI runners; rebinding evidence", + "timestamp": 1784433770, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784424361, + "digest": "f13f1d8e9ddb217cadc43217caf93e5be51d1cedcd341e3be4fd3a095b4b0876", + "note": null + }, + "prior_verification": { + "timestamp": 1784424356, + "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "4d2e9a44083c94210666bfa389388c9b32ab3bfbb98db2476ba49c918130629d", + "acceptance_input_digest": "867757265c0081cdd804e8b95dc4ca37bb568595af91811d65968d72d65641ff", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "bd09a0ace8b7d37cfc0b686ab0ff2d3094fdb015cbb2cd09e6e6a5db6961439f", + "entry_digest": "e9f52413e2cae8ce5dcf4d5ebfb03b873d245731b80c59d18d3c9cc3c19c009f", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1adbf9dab00c1129b9e50c355a9098c75ec4b534af048e8a953c9037c8e3f9cb", + "entry_digest": "bf24557a813a8fdfb925a55ebeeeb2e49998c0ec741883ed08740cd5b2993d0b", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "539e1f33857edd441a92c24493e265047b83a8d7c1c140ab22ba91c8f2d03c00", + "entry_digest": "06dabcbb5dbbb34233cd7f9daaa6a25e80b8407609c82b7d28f00c72b599a7a8", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/Aps.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "55d23aa11c362f6e3b2c22602a546170746bb01f0d7dbe60f24e65574ab4f333", + "entry_digest": "8bb8642ad9a663b25ec22120e629c14cb6d720a581db1700d23ed9ec78beff53", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/CLIOutput.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "722fad1be75f8e8837149336d06f3683eaefbdaec50f8f1677a1c6464777b6cb", + "entry_digest": "760d0555b843081447ded1250666b635d4223b64632faccf35bc4ed2afeadbe0", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/WatchTermination.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "ae37f3c26e4e7c2d478c592a0b32cc7f2508b4eb2d26f78745621262b61a1ce2", + "entry_digest": "46f00fe52be9cdeba726d6a42cbe28e2ff762d8e185547d7463b6d9c285bc9c7", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "dfaba191724faf87fceea73e18f86bf4b337ba7b4c2c0465bf6e3ba2ee7d41f2", + "entry_digest": "ce446ea3ecb252eaa282af37010a788652b5883f987c92e4aa72084fdc98fe65", + "owners": [ + "@exact:test" + ] + }, + { + "path": "specs/aps-cli", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "6f7c83bad496153e7b31ed762f67bd1c75394999419522d3535142c1e5746b58", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "85bf511e7d8be12a267df3a5ac7a6404752a3e03242eccb3f7e85c1e17a06fcc", + "entry_digest": "ee28b08a89dd5dd58f680ea6f755b3607fb93616ac7c3dae820815b01b40b845", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ac4beecaea165e5e95ab606d82277475fc2d7a6cb5611b0272a8d3dd34f9dc64", + "entry_digest": "14a0593d575717adb53656117358c3a36e4b48f9235b1a6d139deab58f158c04", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + "stale_acceptance_input_digest": "867757265c0081cdd804e8b95dc4ca37bb568595af91811d65968d72d65641ff", + "current_acceptance_input_digest": "e657f9f9ba327edcd76dce50c1a65c2751990fd754245bb12fd18e51679daf74" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-watch-signal-handling-and-termination-semantics-issue-34", + "actor": "0xLeif", + "reason": "sigprocmask signal blocking fix for Darwin runners; rebinding evidence", + "timestamp": 1784437785, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784433799, + "digest": "4b20014652de2354fa495f65521089c1e568403589ecbdc0b0ec124e7fd6d189", + "note": null + }, + "prior_verification": { + "timestamp": 1784433785, + "commit": "74bc5f65bf74c531741d3974af3301901fa87d9b", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "cc5e57c94e1233874bbdf273f510e6c91bca4414fb8147d6b0c91de6d42109d1", + "acceptance_input_digest": "e657f9f9ba327edcd76dce50c1a65c2751990fd754245bb12fd18e51679daf74", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "bd09a0ace8b7d37cfc0b686ab0ff2d3094fdb015cbb2cd09e6e6a5db6961439f", + "entry_digest": "e9f52413e2cae8ce5dcf4d5ebfb03b873d245731b80c59d18d3c9cc3c19c009f", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1adbf9dab00c1129b9e50c355a9098c75ec4b534af048e8a953c9037c8e3f9cb", + "entry_digest": "bf24557a813a8fdfb925a55ebeeeb2e49998c0ec741883ed08740cd5b2993d0b", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "da18c7227ca19b2a29b2ee90d6e4056cb6ab803a4aea5528d4c6af6c1daa2842", + "entry_digest": "c95bfcb30a4031b51c42319075bceceae03660b7d019b414cce45446895f4700", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/Aps.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "55d23aa11c362f6e3b2c22602a546170746bb01f0d7dbe60f24e65574ab4f333", + "entry_digest": "8bb8642ad9a663b25ec22120e629c14cb6d720a581db1700d23ed9ec78beff53", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/CLIOutput.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "722fad1be75f8e8837149336d06f3683eaefbdaec50f8f1677a1c6464777b6cb", + "entry_digest": "760d0555b843081447ded1250666b635d4223b64632faccf35bc4ed2afeadbe0", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/WatchTermination.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "ae37f3c26e4e7c2d478c592a0b32cc7f2508b4eb2d26f78745621262b61a1ce2", + "entry_digest": "46f00fe52be9cdeba726d6a42cbe28e2ff762d8e185547d7463b6d9c285bc9c7", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "dfaba191724faf87fceea73e18f86bf4b337ba7b4c2c0465bf6e3ba2ee7d41f2", + "entry_digest": "ce446ea3ecb252eaa282af37010a788652b5883f987c92e4aa72084fdc98fe65", + "owners": [ + "@exact:test" + ] + }, + { + "path": "specs/aps-cli", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "6f7c83bad496153e7b31ed762f67bd1c75394999419522d3535142c1e5746b58", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "85bf511e7d8be12a267df3a5ac7a6404752a3e03242eccb3f7e85c1e17a06fcc", + "entry_digest": "ee28b08a89dd5dd58f680ea6f755b3607fb93616ac7c3dae820815b01b40b845", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ac4beecaea165e5e95ab606d82277475fc2d7a6cb5611b0272a8d3dd34f9dc64", + "entry_digest": "14a0593d575717adb53656117358c3a36e4b48f9235b1a6d139deab58f158c04", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + "stale_acceptance_input_digest": "e657f9f9ba327edcd76dce50c1a65c2751990fd754245bb12fd18e51679daf74", + "current_acceptance_input_digest": "0ce28119d82d404a3f9ef802ec7a6a1999d2d30089aca6d9715bbce544228a1f" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-watch-signal-handling-and-termination-semantics-issue-34", + "actor": "0xLeif", + "reason": "SIG_DFL reset plus Windows gate for signal handlers; rebinding evidence", + "timestamp": 1784438785, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784437806, + "digest": "29774a2a9bd121935670180fdd2a5313c566eb3a3e44537614de5211962beb91", + "note": null + }, + "prior_verification": { + "timestamp": 1784437798, + "commit": "612ca8753cbc8b063d7549e74f653f159889523c", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "a8206c48006dbd00569ebf8117ea5c3a07ab2eb80348cdf7e37a26baa06226e2", + "acceptance_input_digest": "0ce28119d82d404a3f9ef802ec7a6a1999d2d30089aca6d9715bbce544228a1f", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "bd09a0ace8b7d37cfc0b686ab0ff2d3094fdb015cbb2cd09e6e6a5db6961439f", + "entry_digest": "e9f52413e2cae8ce5dcf4d5ebfb03b873d245731b80c59d18d3c9cc3c19c009f", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1adbf9dab00c1129b9e50c355a9098c75ec4b534af048e8a953c9037c8e3f9cb", + "entry_digest": "bf24557a813a8fdfb925a55ebeeeb2e49998c0ec741883ed08740cd5b2993d0b", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "da18c7227ca19b2a29b2ee90d6e4056cb6ab803a4aea5528d4c6af6c1daa2842", + "entry_digest": "c95bfcb30a4031b51c42319075bceceae03660b7d019b414cce45446895f4700", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/Aps.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "55d23aa11c362f6e3b2c22602a546170746bb01f0d7dbe60f24e65574ab4f333", + "entry_digest": "8bb8642ad9a663b25ec22120e629c14cb6d720a581db1700d23ed9ec78beff53", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/CLIOutput.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "722fad1be75f8e8837149336d06f3683eaefbdaec50f8f1677a1c6464777b6cb", + "entry_digest": "760d0555b843081447ded1250666b635d4223b64632faccf35bc4ed2afeadbe0", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/WatchTermination.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "18c5ac3f1d4a8b1396965517ced078d77883eec23dfff174c963a646c15db8d3", + "entry_digest": "fc23880b9b9a827ea48dd1189fda8e8b53a20651e4feb8a53976b5d89ee41eb2", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "dfaba191724faf87fceea73e18f86bf4b337ba7b4c2c0465bf6e3ba2ee7d41f2", + "entry_digest": "ce446ea3ecb252eaa282af37010a788652b5883f987c92e4aa72084fdc98fe65", + "owners": [ + "@exact:test" + ] + }, + { + "path": "specs/aps-cli", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "6f7c83bad496153e7b31ed762f67bd1c75394999419522d3535142c1e5746b58", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "85bf511e7d8be12a267df3a5ac7a6404752a3e03242eccb3f7e85c1e17a06fcc", + "entry_digest": "ee28b08a89dd5dd58f680ea6f755b3607fb93616ac7c3dae820815b01b40b845", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ac4beecaea165e5e95ab606d82277475fc2d7a6cb5611b0272a8d3dd34f9dc64", + "entry_digest": "14a0593d575717adb53656117358c3a36e4b48f9235b1a6d139deab58f158c04", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + "stale_acceptance_input_digest": "0ce28119d82d404a3f9ef802ec7a6a1999d2d30089aca6d9715bbce544228a1f", + "current_acceptance_input_digest": "00061dbf1a476d0ce19ea1b4fc404dc9c110f1e9e45171bf83ac5bf40a8f7e68" + }, + { + "schema_version": 1, + "change_id": "CHG-0021-watch-signal-handling-and-termination-semantics-issue-34", + "actor": "0xLeif", + "reason": "Signal source lifetime fix and SIGINT smoke restore; rebinding evidence", + "timestamp": 1784440622, + "from_state": "accepted", + "to_state": "verifying", + "superseded_approval": { + "gate": "acceptance", + "actor": "0xLeif", + "timestamp": 1784438805, + "digest": "73f13914de5b3b98580d7b136e930d66bedc03f783c6b94c10ee6f210fca1f89", + "note": null + }, + "prior_verification": { + "timestamp": 1784438798, + "commit": "d39cd25e05dd197f80ae835b5baba39a6917db2f", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "8fcaebc50225b5f1bce4d95e06ff1192587cf1ce767813f3b550adbd09495178", + "acceptance_input_digest": "00061dbf1a476d0ce19ea1b4fc404dc9c110f1e9e45171bf83ac5bf40a8f7e68", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "bd09a0ace8b7d37cfc0b686ab0ff2d3094fdb015cbb2cd09e6e6a5db6961439f", + "entry_digest": "e9f52413e2cae8ce5dcf4d5ebfb03b873d245731b80c59d18d3c9cc3c19c009f", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1adbf9dab00c1129b9e50c355a9098c75ec4b534af048e8a953c9037c8e3f9cb", + "entry_digest": "bf24557a813a8fdfb925a55ebeeeb2e49998c0ec741883ed08740cd5b2993d0b", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "da18c7227ca19b2a29b2ee90d6e4056cb6ab803a4aea5528d4c6af6c1daa2842", + "entry_digest": "c95bfcb30a4031b51c42319075bceceae03660b7d019b414cce45446895f4700", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/Aps.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "55d23aa11c362f6e3b2c22602a546170746bb01f0d7dbe60f24e65574ab4f333", + "entry_digest": "8bb8642ad9a663b25ec22120e629c14cb6d720a581db1700d23ed9ec78beff53", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/CLIOutput.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "722fad1be75f8e8837149336d06f3683eaefbdaec50f8f1677a1c6464777b6cb", + "entry_digest": "760d0555b843081447ded1250666b635d4223b64632faccf35bc4ed2afeadbe0", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/WatchTermination.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "d68fcc1d7c8f5268d2d46003459c356e0b68aa16dd8412177b11eb4d744acf9f", + "entry_digest": "4190521ffcec836800af83d92de4569d88b056ba264992b8e2346786228bd01e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "dfaba191724faf87fceea73e18f86bf4b337ba7b4c2c0465bf6e3ba2ee7d41f2", + "entry_digest": "ce446ea3ecb252eaa282af37010a788652b5883f987c92e4aa72084fdc98fe65", + "owners": [ + "@exact:test" + ] + }, + { + "path": "specs/aps-cli", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "6f7c83bad496153e7b31ed762f67bd1c75394999419522d3535142c1e5746b58", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "85bf511e7d8be12a267df3a5ac7a6404752a3e03242eccb3f7e85c1e17a06fcc", + "entry_digest": "ee28b08a89dd5dd58f680ea6f755b3607fb93616ac7c3dae820815b01b40b845", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ac4beecaea165e5e95ab606d82277475fc2d7a6cb5611b0272a8d3dd34f9dc64", + "entry_digest": "14a0593d575717adb53656117358c3a36e4b48f9235b1a6d139deab58f158c04", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + "stale_acceptance_input_digest": "00061dbf1a476d0ce19ea1b4fc404dc9c110f1e9e45171bf83ac5bf40a8f7e68", + "current_acceptance_input_digest": "ad20b2bc20637a204981557563da2f88c654721ce352483e82ac4f035672f488" + } + ] +} diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/change.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/change.md new file mode 100644 index 0000000..6a60353 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/change.md @@ -0,0 +1,24 @@ +--- +id: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +state: accepted +type: feature +base_commit: c4ecd60b2a46bca4780f1b4af79a1d495d646287 +--- + +# Watch signal handling and termination semantics (issue 34) + +## Intent + +watch signal handling and termination semantics (issue 34) + +## Affected Canonical Specs + +- `aps-cli` + +## Acceptance Criteria + +- watch stops cleanly for count (exit 0), timeout (exit 124), SIGINT (exit 130), and SIGTERM (exit 143); the stop reason appears as a terminal {type:end,reason:...} event in --jsonl mode or a stderr line in human mode; the jsonl stream never contains non-JSON lines; an unbounded watch prints a one-time stderr hint; smoke covers all exit paths including kill -INT; specs updated. + +## No-spec Rationale + +Not applicable diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/context.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/context.md new file mode 100644 index 0000000..2c58977 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/context.md @@ -0,0 +1,23 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: context +--- + +# Context + +Issue #34 (design round 2, decision: both channels). watch ran forever +with no signal handling, bounded exits always returned 0 whether count or +timeout stopped the loop, and the jsonl fallback could inject a raw +non-JSON line into the stream. + +## Decisions + +- Exit codes: 0 count (request satisfied), 124 timeout (GNU convention), + 128+signal (130 SIGINT, 143 SIGTERM). +- Reason is observable in both channels: terminal + `{"type":"end","reason":...}` event in `--jsonl` mode, stderr summary in + human mode. +- The jsonl stream never contains non-JSON lines: event-encode failures + now emit a WatchErrorEvent instead of the raw value line. +- Unbounded watch prints a one-time stderr hint suggesting + `--count` / `--timeout`. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/deltas/aps-cli.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/deltas/aps-cli.md new file mode 100644 index 0000000..ed01df0 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/deltas/aps-cli.md @@ -0,0 +1,67 @@ +# aps-cli watch termination semantics + +## MODIFIED + +### REQUIREMENT REQ-aps-cli-012 + +`watch` SHALL support `--count`, `--timeout`, and `--jsonl`, and SHALL handle SIGINT/SIGTERM with observable termination semantics. + +Acceptance Criteria +- `--count` stops after that many printed values including the initial value (exit 0). +- `--timeout` stops after the given seconds (exit 124). +- SIGINT/SIGTERM stop the loop cleanly (exit 130 / 143). +- The stop reason appears as a terminal `{"type":"end","reason":...}` event in `--jsonl` mode or a stderr line in human mode. +- The `--jsonl` stream never contains non-JSON lines. +- An unbounded watch prints a one-time stderr hint suggesting `--count` / `--timeout`. + +### SPEC SECTION Public API + +| Export | Description | +|--------|-------------| +| `DemoKey` | Fixed schema enum including `profile`, `secret`, and `profileName`. | +| `ProfileDocument` | Codable `{name, version}` FileState document. | +| `name` | ProfileDocument display name field. | +| `version` | ProfileDocument integer version field. | +| `init` | ProfileDocument memberwise initializer. | +| `APSKeychain` | Well-known Keychain service/account for `secret`. | +| `service` | Reverse-DNS Keychain feature namespace (`dev.leif.aps`). | +| `account` | Keychain account id (`secret`). | +| `secretAccount` | Full account key (`dev.leif.aps/secret`). | +| `APSError` | Typed CLI/domain errors. | +| `counter` | Int key stored in AppState `State`. | +| `message` | String key stored in AppState `State`. | +| `flag` | Bool key stored in AppState `StoredState`. | +| `note` | String key stored in AppState `FileState`. | +| `profile` | ProfileDocument key stored in AppState `FileState`. | +| `secret` | String key stored in AppState `SecureState` (Keychain). | +| `profileName` | String Slice over `ProfileDocument.name`. | +| `invalidValue` | Value could not parse for the key type. | +| `encodingFailed` | UTF-8 JSON encode failure. | +| `decodingFailed` | UTF-8 JSON decode failure. | +| `persistenceFailed` | Disk/Keychain-backed key did not persist after write. | +| `keychainUnavailable` | SecureState unavailable without Apple Security. | +| `corruptState` | FileState file exists but is undecodable (torn write). | +| `corruptStateExitCode` | Exit code 65 (`EX_DATAERR`) for `corruptState`. | +| `storage` | Human storage kind (`State` / `StoredState` / `FileState` / `SecureState` / `Slice`). | +| `valueType` | Human value type (`Int` / `String` / `Bool` / `ProfileDocument`). | +| `helpSummary` | Tab-separated key/type/storage columns for `keys`. | +| `detail` | One-line description for `keys`. | +| `description` | Actionable error text for humans and ValidationError bridging. | + +### SPEC SECTION Invariants + +1. The CLI entry point runs on the real main thread so AppState + `notifyChange()` assertions hold on Linux and macOS. +2. stdout for `get` / `set` / `watch` / `reset ` is only the value line(s); + help and errors use ArgumentParser defaults. +3. `State` keys are process-local; a new process must not be expected to retain + `counter` or `message`. +4. `watch` must flush each printed value immediately when stdout is not a TTY. +5. `keys` and `--help` do not mutate application state. + +6. `watch` termination is observable in both channels: a terminal + `{"type":"end","reason":"count|timeout|sigint|sigterm"}` event in `--jsonl` + mode or a stderr line in human mode, with exit codes 0 (count), 124 + (timeout), 128+signal (130 SIGINT, 143 SIGTERM). The `--jsonl` stream never + contains non-JSON lines. An unbounded watch prints a one-time stderr hint + suggesting `--count` / `--timeout`. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/design.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/design.md new file mode 100644 index 0000000..d880dc4 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/design.md @@ -0,0 +1,17 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: design +--- + +# Design + +- New `Sources/aps/WatchTermination.swift`: `SignalBox` (lock-guarded + signal record), `installWatchSignalHandlers` (SIG_IGN plus + DispatchSourceSignal on the main queue, delivered by the watch loop's + RunLoop draining), and `StopReason` (token / exitCode / summary). +- Watch.run composes `shouldContinue` from count, deadline, and the signal + box, recording the first stop reason. After the loop: terminal + `WatchEndEvent` on stdout in `--jsonl` mode or a stderr line, then + `ExitCode(reason.exitCode)` when non-zero. +- jsonl purity: the encode-failure fallback now writes a WatchErrorEvent + with `error: "encoding_failed"`. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/docs.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/docs.md new file mode 100644 index 0000000..ecb50c4 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/docs.md @@ -0,0 +1,10 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: docs +--- + +# Docs + +README watch paragraph: exit-code table (0/124/130/143), the terminal +`{"type":"end","reason":...}` event, the jsonl purity rule, and the +unbounded hint. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/plan.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/plan.md new file mode 100644 index 0000000..5a5fb8b --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/plan.md @@ -0,0 +1,15 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: plan +--- + +# Plan + +1. Add WatchTermination.swift (SignalBox, handlers, StopReason). +2. Add CLIOutput.WatchEndEvent and writeError; rework Watch.run (reason + tracking, terminal marker, exit codes, hint, jsonl purity). +3. Tests: StopReason tokens/exit codes, WatchEndEvent shape. +4. Smoke: count exit 0, timeout exit 124 with end event, SIGINT exit 130 + via kill -INT, jsonl purity check. +5. aps-cli spec (files list, termination invariant), REQ-aps-cli-012, + README. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/requirements.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/requirements.md new file mode 100644 index 0000000..23b6bbf --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/requirements.md @@ -0,0 +1,10 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: requirements +--- + +# Requirements + +- REQ-aps-cli-012 extended: SIGINT/SIGTERM handling, observable stop + reason in both channels, exit codes 0/124/130/143, jsonl purity, and + the unbounded hint. diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/state.json b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/state.json new file mode 100644 index 0000000..ac85a9a --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/state.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "id": "CHG-0023-watch-signal-handling-and-termination-semantics-issue-34", + "slug": "watch-signal-handling-and-termination-semantics-issue-34", + "title": "Watch signal handling and termination semantics (issue 34)", + "description": "watch signal handling and termination semantics (issue 34)", + "kind": "feature", + "state": "accepted", + "canonical_applied": true, + "base_commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "created_at": 1784424255, + "updated_at": 1784440646, + "affected_specs": [ + "aps-cli" + ], + "affected_paths": [ + "Sources/aps/WatchTermination.swift", + "Sources/aps/Aps.swift", + "Sources/aps/CLIOutput.swift", + "Tests/apsTests/APSTests.swift", + "Scripts/smoke.sh", + "specs/aps-cli", + "README.md", + ".specsync/change-sequence.json" + ], + "no_spec_change": false, + "no_spec_change_rationale": null, + "acceptance_criteria": [ + "watch stops cleanly for count (exit 0), timeout (exit 124), SIGINT (exit 130), and SIGTERM (exit 143); the stop reason appears as a terminal {type:end,reason:...} event in --jsonl mode or a stderr line in human mode; the jsonl stream never contains non-JSON lines; an unbounded watch prints a one-time stderr hint; smoke covers all exit paths including kill -INT; specs updated." + ], + "selected_artifacts": [ + "context", + "requirements", + "plan", + "tasks", + "testing", + "docs", + "design" + ], + "dependencies": [], + "answers": { + "architecture_risk": "no", + "public_contract": "yes" + } +} diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/tasks.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/tasks.md new file mode 100644 index 0000000..1106cbd --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/tasks.md @@ -0,0 +1,11 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: tasks +--- + +# Tasks + +- [x] WatchTermination.swift +- [x] WatchEndEvent + Watch.run rework +- [x] Unit tests and smoke assertions +- [x] aps-cli spec, requirements, README updates diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/testing.md b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/testing.md new file mode 100644 index 0000000..30cd50d --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/testing.md @@ -0,0 +1,17 @@ +--- +change: CHG-0023-watch-signal-handling-and-termination-semantics-issue-34 +artifact: testing +--- + +# Testing + +## Requirement evidence + +| Requirement | Evidence | +| --- | --- | +| REQ-aps-cli-012 | `testStopReasonTokensAndExitCodes`, `testWatchEndEventEncodesTerminalMarker`; smoke covers count exit 0, timeout exit 124 with end event, SIGINT exit 130 via kill -INT, and jsonl purity | + +## Suites + +- `swift test` +- `fledge lanes run verify` diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification-attempts.json b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification-attempts.json new file mode 100644 index 0000000..d4bb0a9 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification-attempts.json @@ -0,0 +1,90 @@ +{ + "schema_version": 1, + "attempts": [ + { + "timestamp": 1784424356, + "commit": "c4ecd60b2a46bca4780f1b4af79a1d495d646287", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "4d2e9a44083c94210666bfa389388c9b32ab3bfbb98db2476ba49c918130629d", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + { + "timestamp": 1784433785, + "commit": "74bc5f65bf74c531741d3974af3301901fa87d9b", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "cc5e57c94e1233874bbdf273f510e6c91bca4414fb8147d6b0c91de6d42109d1", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + { + "timestamp": 1784437798, + "commit": "612ca8753cbc8b063d7549e74f653f159889523c", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "a8206c48006dbd00569ebf8117ea5c3a07ab2eb80348cdf7e37a26baa06226e2", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + { + "timestamp": 1784438798, + "commit": "d39cd25e05dd197f80ae835b5baba39a6917db2f", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "8fcaebc50225b5f1bce4d95e06ff1192587cf1ce767813f3b550adbd09495178", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + }, + { + "timestamp": 1784440635, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] + } + ] +} diff --git a/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification.json b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification.json new file mode 100644 index 0000000..d980042 --- /dev/null +++ b/.specsync/changes/CHG-0023-watch-signal-handling-and-termination-semantics-issue-34/verification.json @@ -0,0 +1,153 @@ +{ + "timestamp": 1784440635, + "commit": "384c2b26692a3020dbbeeabece9e45f4cf4d7c3d", + "contract_digest": "4b649d7aa4143782bcc58f83e17934f8dedfb0b944feb90b3fdaf9e4793b6e8a", + "workspace_digest": "64c0bcba0fac8f0ef02f10f4b1851cff103ae5c2929a3e0853b581a88afb8daa", + "acceptance_input_digest": "ad20b2bc20637a204981557563da2f88c654721ce352483e82ac4f035672f488", + "acceptance_manifest": { + "schema_version": 1, + "entries": [ + { + "path": ".specsync/change-sequence.json", + "kind": "file", + "mode": 33188, + "payload_digest": "bd09a0ace8b7d37cfc0b686ab0ff2d3094fdb015cbb2cd09e6e6a5db6961439f", + "entry_digest": "e9f52413e2cae8ce5dcf4d5ebfb03b873d245731b80c59d18d3c9cc3c19c009f", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "README.md", + "kind": "file", + "mode": 33188, + "payload_digest": "1adbf9dab00c1129b9e50c355a9098c75ec4b534af048e8a953c9037c8e3f9cb", + "entry_digest": "bf24557a813a8fdfb925a55ebeeeb2e49998c0ec741883ed08740cd5b2993d0b", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Scripts/smoke.sh", + "kind": "file", + "mode": 33261, + "payload_digest": "72d0ba5895f18111db528806dd045bca171e76b527de302d72e8f686f3796816", + "entry_digest": "bd08ed8f2c9e93e20d8c3e6075119e820595bfb62b57b0c6d6c822ced28c59c7", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "Sources/aps/Aps.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "1c4eadc926957763137a2a0f279122ba0a59ff7ee5700aee8f22a8e1a8f56b05", + "entry_digest": "416a9732533564e60e4aa9b60e16a52bc5fb7b24e8140a19a41827635280b6a9", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/CLIOutput.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "722fad1be75f8e8837149336d06f3683eaefbdaec50f8f1677a1c6464777b6cb", + "entry_digest": "760d0555b843081447ded1250666b635d4223b64632faccf35bc4ed2afeadbe0", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Sources/aps/WatchTermination.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "d68fcc1d7c8f5268d2d46003459c356e0b68aa16dd8412177b11eb4d744acf9f", + "entry_digest": "4190521ffcec836800af83d92de4569d88b056ba264992b8e2346786228bd01e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "Tests/apsTests/APSTests.swift", + "kind": "file", + "mode": 33188, + "payload_digest": "dfaba191724faf87fceea73e18f86bf4b337ba7b4c2c0465bf6e3ba2ee7d41f2", + "entry_digest": "ce446ea3ecb252eaa282af37010a788652b5883f987c92e4aa72084fdc98fe65", + "owners": [ + "@exact:test" + ] + }, + { + "path": "specs/aps-cli", + "kind": "non_file", + "mode": 0, + "payload_digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "entry_digest": "6f7c83bad496153e7b31ed762f67bd1c75394999419522d3535142c1e5746b58", + "owners": [ + "@exact:delivery" + ] + }, + { + "path": "specs/aps-cli/aps-cli.spec.md", + "kind": "file", + "mode": 33188, + "payload_digest": "85bf511e7d8be12a267df3a5ac7a6404752a3e03242eccb3f7e85c1e17a06fcc", + "entry_digest": "ee28b08a89dd5dd58f680ea6f755b3607fb93616ac7c3dae820815b01b40b845", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/context.md", + "kind": "file", + "mode": 33188, + "payload_digest": "7304a6233da2051242aeac63a51197aaf6f92d936692fe8655801eff842a2fd0", + "entry_digest": "e78ad0996c0135c37d41afa37c5fda13901bb63e14f00c3208896f7b104a72e8", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/requirements.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ac4beecaea165e5e95ab606d82277475fc2d7a6cb5611b0272a8d3dd34f9dc64", + "entry_digest": "14a0593d575717adb53656117358c3a36e4b48f9235b1a6d139deab58f158c04", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/tasks.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ca6cafd22fb3c803eed3dc94cb980d8f535d51e0fd289b46bae82b7f255377de", + "entry_digest": "c349b8f073cefe7e387f8fabdd4dada3830be59b4b6d22a4ce79b2c29399c70e", + "owners": [ + "aps-cli" + ] + }, + { + "path": "specs/aps-cli/testing.md", + "kind": "file", + "mode": 33188, + "payload_digest": "ee5ae7f63536e2089a2b51887ed62b8392561052cdaa37c29707e7a10e2136bd", + "entry_digest": "daf1e496e836c46da31eb6ac679b352d7bf615536595be310953eb0ae9a36fd4", + "owners": [ + "aps-cli" + ] + } + ] + }, + "passed": true, + "commands": [ + { + "command": "fledge lanes run verify", + "success": true, + "exit_code": 0 + } + ], + "requirement_ids": [ + "REQ-aps-cli-012" + ] +} diff --git a/README.md b/README.md index ea0a4a6..7ea685a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,8 @@ swift run aps stats --json - `watch --json` is an alias for `--jsonl`; `keys --quiet` prints key names only (handy for `xargs aps reset`). - Shell completions: `aps --generate-completion-script bash|zsh|fish` (install into your shell's completion directory). +`watch` termination is observable in both channels. Exit codes: **0** when `--count` is satisfied, **124** on `--timeout` (GNU convention), **130** on SIGINT, **143** on SIGTERM. In `--jsonl` mode the stream ends with a terminal `{"type":"end","reason":"count|timeout|sigint|sigterm",...}` event and never contains non-JSON lines; in human mode a one-line summary goes to stderr. An unbounded watch prints a one-time stderr hint suggesting `--count` / `--timeout`. + ### Multi-process FileState semantics `aps` expects **one writer per key** at a time. Concurrent `aps set` on the same FileState key (`note` / `profile`) is last-writer-wins and is not locked: AppState writes files non-atomically, so two writers can tear a JSON file. diff --git a/Scripts/smoke.sh b/Scripts/smoke.sh index ea4e5d6..7e768c6 100755 --- a/Scripts/smoke.sh +++ b/Scripts/smoke.sh @@ -130,4 +130,34 @@ test "$("$bin" keys --quiet | wc -l | tr -d ' ')" = "7" "$bin" dump | grep -q '"key":"flag"' "$bin" watch counter --count 1 --timeout 2 --json >/dev/null +# Watch termination semantics: exit codes and stream markers. +"$bin" watch counter --count 1 --jsonl >/dev/null +"$bin" watch counter --count 1 --jsonl | grep -q '"reason":"count"' +out="$("$bin" watch counter --timeout 1 --jsonl >/dev/null 2>&1)" && { echo "expected timeout exit" >&2; exit 1; } +test $? -eq 124 || { echo "expected exit 124 for timeout" >&2; exit 1; } +out="$("$bin" watch counter --timeout 1 --jsonl 2>/dev/null || true)" +echo "$out" | grep -q '"reason":"timeout"' +"$bin" watch counter --jsonl > "$APS_HOME/watch.out" 2>/dev/null & +WPID=$! +sleep 1 +kill -INT $WPID +# Bounded wait: the signal must stop the watch within a few seconds. +RC=0 +for _ in $(seq 1 20); do + kill -0 $WPID 2>/dev/null || break + sleep 0.5 +done +if kill -0 $WPID 2>/dev/null; then + echo "watch did not stop on signal" >&2 + kill -KILL $WPID 2>/dev/null || true + exit 1 +fi +wait $WPID || RC=$? +test "${RC:-0}" -eq 130 || { echo "expected exit 130 for SIGINT, got ${RC:-0}" >&2; exit 1; } +grep -q '"reason":"sigint"' "$APS_HOME/watch.out" +if grep -vq '^{' "$APS_HOME/watch.out"; then + echo "jsonl stream must contain only JSON lines" >&2 + exit 1 +fi + echo "smoke ok" diff --git a/Sources/aps/Aps.swift b/Sources/aps/Aps.swift index 588dee9..f0bb243 100644 --- a/Sources/aps/Aps.swift +++ b/Sources/aps/Aps.swift @@ -142,14 +142,32 @@ extension Aps { let jsonl = jsonl || json let deadline = timeout.map { Date().addingTimeInterval($0) } var emitted = 0 + var stopReason: StopReason? + + let signalBox = SignalBox() + let signalSources = installWatchSignalHandlers(signalBox) + + if count == nil && timeout == nil { + CLIOutput.writeError("watch: unbounded stream; press Ctrl-C to stop, or use --count/--timeout for bounded runs") + } do { try store.watchBlocking( key, pollInterval: TimeInterval(interval) / 1000.0, shouldContinue: { - if let count, emitted >= count { return false } - if let deadline, Date() >= deadline { return false } + if let count, emitted >= count { + stopReason = .count + return false + } + if let deadline, Date() >= deadline { + stopReason = .timeout + return false + } + if let sig = signalBox.first { + stopReason = .signal(sig) + return false + } return true } ) { value in @@ -157,15 +175,22 @@ extension Aps { if jsonl { // Parse the fresh `value` from watchBlocking. Do not re-query // the store: FileState cache can lag cross-process disk writes. - let event = try? CLIOutput.watchEvent( + if let event = try? CLIOutput.watchEvent( key: key, rawValue: value, timestamp: store.now - ) - if let event, let line = try? CLIOutput.encodeLine(event) { + ), let line = try? CLIOutput.encodeLine(event) { + CLIOutput.writeLine(line) + } else if let line = try? CLIOutput.encodeLine( + CLIOutput.WatchErrorEvent( + key: key.rawValue, + error: "encoding_failed", + message: "value could not be encoded as a watch event", + timestamp: store.now + ) + ) { + // The jsonl stream never carries non-JSON lines. CLIOutput.writeLine(line) - } else { - CLIOutput.writeLine(value) } } else { CLIOutput.writeLine(value) @@ -185,6 +210,29 @@ extension Aps { } try CLIOutput.fail(error, json: jsonl) } + + if let stopReason { + if jsonl { + let event = CLIOutput.WatchEndEvent( + key: key.rawValue, + reason: stopReason.token, + timestamp: store.now + ) + if let line = try? CLIOutput.encodeLine(event) { + CLIOutput.writeLine(line) + } + } else { + CLIOutput.writeError("watch \(key.rawValue): stopped (\(stopReason.summary))") + } + if stopReason.exitCode != 0 { + throw ExitCode(stopReason.exitCode) + } + } + + // Actual use after the loop: guarantees the dispatch sources + // outlive the watch (debug kept them incidentally; release + // optimized them away and silently lost every signal). + signalSources.forEach { $0.cancel() } } } } diff --git a/Sources/aps/CLIOutput.swift b/Sources/aps/CLIOutput.swift index 8fe7b39..fbbf6aa 100644 --- a/Sources/aps/CLIOutput.swift +++ b/Sources/aps/CLIOutput.swift @@ -52,6 +52,21 @@ enum CLIOutput { } } + /// Terminal machine-mode watch event: the loop stopped, with the reason. + struct WatchEndEvent: Encodable { + let type: String + let key: String + let reason: String + let timestamp: Date + + init(key: String, reason: String, timestamp: Date) { + self.type = "end" + self.key = key + self.reason = reason + self.timestamp = timestamp + } + } + struct StatsPayload: Encodable { let mutationCount: Int let lastMutatedKey: String diff --git a/Sources/aps/WatchTermination.swift b/Sources/aps/WatchTermination.swift new file mode 100644 index 0000000..988f8be --- /dev/null +++ b/Sources/aps/WatchTermination.swift @@ -0,0 +1,98 @@ +import Foundation + +/// Signal handling and termination semantics for `aps watch` (issue #34). +/// The loop stops for one of three reasons (count, timeout, signal) and the +/// reason is observable in both channels: a terminal stream marker and a +/// process exit code (0 count, 124 timeout, 128+signal). + +/// Thread-safe record of received signals. +final class SignalBox: @unchecked Sendable { + private let lock = NSLock() + private var signals: [Int32] = [] + + func mark(_ signal: Int32) { + lock.lock() + signals.append(signal) + lock.unlock() + } + + var first: Int32? { + lock.lock() + defer { lock.unlock() } + return signals.first + } +} + +/// Install DispatchSource handlers for SIGINT/SIGTERM on the main queue. +/// The watch loop's RunLoop draining delivers them; the box is polled by +/// `shouldContinue`. Sources must be retained for the watch's lifetime. +@discardableResult +func installWatchSignalHandlers(_ box: SignalBox) -> [DispatchSourceSignal] { + [SIGINT, SIGTERM].compactMap { sig in + #if os(Windows) + // POSIX signals do not exist on Windows; console control handlers are + // a future addition there. Return no sources; bounds still work. + return nil + #else + // Reset any inherited ignore disposition (CI agents ignore SIGINT for + // job children, which also suppresses kqueue delivery), then BLOCK the + // signal so the dispatch source is the only consumer. + signal(sig, SIG_DFL) + var set = sigset_t() + sigemptyset(&set) + sigaddset(&set, sig) + sigprocmask(SIG_BLOCK, &set, nil) + let source = DispatchSource.makeSignalSource(signal: sig, queue: .main) + source.setEventHandler { + box.mark(sig) + } + source.resume() + return source + #endif + } +} + +/// Why a bounded watch stopped. +enum StopReason: Equatable { + case count + case timeout + case signal(Int32) + + /// Machine-mode reason token for the terminal stream event. + var token: String { + switch self { + case .count: return "count" + case .timeout: return "timeout" + case .signal(let signalNumber): + switch signalNumber { + case SIGINT: return "sigint" + case SIGTERM: return "sigterm" + default: return "signal" + } + } + } + + /// Process exit code: 0 on count (request satisfied), 124 on timeout + /// (GNU convention), 128+signal for signals (130 SIGINT, 143 SIGTERM). + var exitCode: Int32 { + switch self { + case .count: return 0 + case .timeout: return 124 + case .signal(let signalNumber): return 128 + signalNumber + } + } + + /// Human stderr line. + var summary: String { + switch self { + case .count: return "count reached" + case .timeout: return "timeout" + case .signal(let signalNumber): + switch signalNumber { + case SIGINT: return "interrupted (SIGINT)" + case SIGTERM: return "terminated (SIGTERM)" + default: return "signal \(signalNumber)" + } + } + } +} diff --git a/Tests/apsTests/APSTests.swift b/Tests/apsTests/APSTests.swift index 10346bc..6c2dfa3 100644 --- a/Tests/apsTests/APSTests.swift +++ b/Tests/apsTests/APSTests.swift @@ -797,4 +797,33 @@ final class APSTests: XCTestCase { XCTAssertFalse(json.contains("\n")) XCTAssertTrue(json.contains("\"ok\":true")) } + + func testStopReasonTokensAndExitCodes() { + XCTAssertEqual(StopReason.count.token, "count") + XCTAssertEqual(StopReason.timeout.token, "timeout") + XCTAssertEqual(StopReason.signal(SIGINT).token, "sigint") + XCTAssertEqual(StopReason.signal(SIGTERM).token, "sigterm") + XCTAssertEqual(StopReason.signal(20).token, "signal") + + XCTAssertEqual(StopReason.count.exitCode, 0) + XCTAssertEqual(StopReason.timeout.exitCode, 124) + XCTAssertEqual(StopReason.signal(SIGINT).exitCode, 130) + XCTAssertEqual(StopReason.signal(SIGTERM).exitCode, 143) + + XCTAssertEqual(StopReason.count.summary, "count reached") + XCTAssertEqual(StopReason.signal(SIGINT).summary, "interrupted (SIGINT)") + XCTAssertEqual(StopReason.signal(SIGTERM).summary, "terminated (SIGTERM)") + } + + func testWatchEndEventEncodesTerminalMarker() throws { + let event = CLIOutput.WatchEndEvent( + key: "counter", + reason: StopReason.timeout.token, + timestamp: Date(timeIntervalSince1970: 0) + ) + let line = try CLIOutput.encodeLine(event) + XCTAssertTrue(line.contains(#""type":"end""#)) + XCTAssertTrue(line.contains(#""reason":"timeout""#)) + XCTAssertTrue(line.contains(#""key":"counter""#)) + } } diff --git a/specs/aps-cli/aps-cli.spec.md b/specs/aps-cli/aps-cli.spec.md index 511c0b7..2eaf0c5 100644 --- a/specs/aps-cli/aps-cli.spec.md +++ b/specs/aps-cli/aps-cli.spec.md @@ -1,12 +1,13 @@ --- module: aps-cli -version: 21 +version: 22 status: active files: - Sources/aps/Aps.swift - Sources/aps/CLIOutput.swift - Sources/aps/DemoKey.swift - Sources/aps/TTY.swift + - Sources/aps/WatchTermination.swift db_tables: [] depends_on: - state-store @@ -76,6 +77,13 @@ names only. Machine shapes are additive-only contracts; human text may evolve. 4. `watch` must flush each printed value immediately when stdout is not a TTY. 5. `keys` and `--help` do not mutate application state. +6. `watch` termination is observable in both channels: a terminal + `{"type":"end","reason":"count|timeout|sigint|sigterm"}` event in `--jsonl` + mode or a stderr line in human mode, with exit codes 0 (count), 124 + (timeout), 128+signal (130 SIGINT, 143 SIGTERM). The `--jsonl` stream never + contains non-JSON lines. An unbounded watch prints a one-time stderr hint + suggesting `--count` / `--timeout`. + ## Behavioral Examples ``` @@ -160,3 +168,4 @@ Exit codes (sysexits-aligned): | 2026-07-18 | CHG-0021-error-contract-exit-code-taxonomy-and-json-error-envelope-issue-31-rebuilt-on: Error contract: exit-code taxonomy and JSON error envelope (issue 31, rebuilt on corruptState main) | | 2026-07-19 | CHG-0021-error-contract-exit-code-taxonomy-and-json-error-envelope-issue-31-rebuilt-on: Error contract: exit-code taxonomy and JSON error envelope (issue 31, rebuilt on corruptState main) | | 2026-07-19 | CHG-0022-tty-aware-output-under-the-git-porcelain-rule-issue-33: TTY-aware output under the git porcelain rule (issue 33) | +| 2026-07-19 | CHG-0023-watch-signal-handling-and-termination-semantics-issue-34: Watch signal handling and termination semantics (issue 34) | diff --git a/specs/aps-cli/requirements.md b/specs/aps-cli/requirements.md index 30ac845..48e3965 100644 --- a/specs/aps-cli/requirements.md +++ b/specs/aps-cli/requirements.md @@ -70,12 +70,15 @@ Acceptance Criteria ### REQ-aps-cli-012 -`watch` SHALL support `--count`, `--timeout`, and `--jsonl`. +`watch` SHALL support `--count`, `--timeout`, and `--jsonl`, and SHALL handle SIGINT/SIGTERM with observable termination semantics. Acceptance Criteria -- `--count` stops after that many printed values including the initial value. -- `--timeout` stops after the given seconds. -- `--jsonl` emits one JSON object per line. +- `--count` stops after that many printed values including the initial value (exit 0). +- `--timeout` stops after the given seconds (exit 124). +- SIGINT/SIGTERM stop the loop cleanly (exit 130 / 143). +- The stop reason appears as a terminal `{"type":"end","reason":...}` event in `--jsonl` mode or a stderr line in human mode. +- The `--jsonl` stream never contains non-JSON lines. +- An unbounded watch prints a one-time stderr hint suggesting `--count` / `--timeout`. ### REQ-aps-cli-013