@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/discover.d.ts +4 -0
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +16 -2
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/case-triage-hint.d.ts +14 -0
- package/dist/cli/case-triage-hint.d.ts.map +1 -0
- package/dist/cli/case-triage-hint.js +23 -0
- package/dist/cli/case-triage-hint.js.map +1 -0
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/contract-derive-command.d.ts +4 -2
- package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
- package/dist/cli/commands/contract-derive-command.js +19 -5
- package/dist/cli/commands/contract-derive-command.js.map +1 -1
- package/dist/cli/commands/contract.d.ts +3 -0
- package/dist/cli/commands/contract.d.ts.map +1 -1
- package/dist/cli/commands/contract.js +40 -7
- package/dist/cli/commands/contract.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +59 -18
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/gate.d.ts.map +1 -1
- package/dist/cli/commands/gate.js +2 -1
- package/dist/cli/commands/gate.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +14 -2
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +6 -4
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/next.d.ts +7 -2
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +41 -10
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/commands/triage.d.ts +2 -1
- package/dist/cli/commands/triage.d.ts.map +1 -1
- package/dist/cli/commands/triage.js +45 -3
- package/dist/cli/commands/triage.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +6 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/json-error-on-exit.d.ts +34 -0
- package/dist/cli/json-error-on-exit.d.ts.map +1 -0
- package/dist/cli/json-error-on-exit.js +63 -0
- package/dist/cli/json-error-on-exit.js.map +1 -0
- package/dist/cli/project-sungen-invocation.d.ts +13 -0
- package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
- package/dist/cli/project-sungen-invocation.js +95 -0
- package/dist/cli/project-sungen-invocation.js.map +1 -0
- package/dist/cli/run-command-hints.d.ts +9 -0
- package/dist/cli/run-command-hints.d.ts.map +1 -0
- package/dist/cli/run-command-hints.js +84 -0
- package/dist/cli/run-command-hints.js.map +1 -0
- package/dist/cli/waiting-for-you-block.d.ts +18 -0
- package/dist/cli/waiting-for-you-block.d.ts.map +1 -0
- package/dist/cli/waiting-for-you-block.js +22 -0
- package/dist/cli/waiting-for-you-block.js.map +1 -0
- package/dist/contract/accept-opaque-steps.d.ts +3 -0
- package/dist/contract/accept-opaque-steps.d.ts.map +1 -0
- package/dist/contract/accept-opaque-steps.js +114 -0
- package/dist/contract/accept-opaque-steps.js.map +1 -0
- package/dist/contract/approve-contract.d.ts +8 -1
- package/dist/contract/approve-contract.d.ts.map +1 -1
- package/dist/contract/approve-contract.js +6 -0
- package/dist/contract/approve-contract.js.map +1 -1
- package/dist/contract/build-contract-verdict.js +2 -2
- package/dist/contract/build-contract-verdict.js.map +1 -1
- package/dist/contract/build-trace-conformance.d.ts +5 -1
- package/dist/contract/build-trace-conformance.d.ts.map +1 -1
- package/dist/contract/build-trace-conformance.js +12 -14
- package/dist/contract/build-trace-conformance.js.map +1 -1
- package/dist/contract/classify-contract-change.d.ts +3 -2
- package/dist/contract/classify-contract-change.d.ts.map +1 -1
- package/dist/contract/classify-contract-change.js.map +1 -1
- package/dist/contract/classify-trace-conformance.d.ts +18 -3
- package/dist/contract/classify-trace-conformance.d.ts.map +1 -1
- package/dist/contract/classify-trace-conformance.js +64 -17
- package/dist/contract/classify-trace-conformance.js.map +1 -1
- package/dist/contract/contract-schema.d.ts +21 -0
- package/dist/contract/contract-schema.d.ts.map +1 -1
- package/dist/contract/contract-schema.js +25 -3
- package/dist/contract/contract-schema.js.map +1 -1
- package/dist/contract/contracts-lock-file.d.ts.map +1 -1
- package/dist/contract/contracts-lock-file.js +1 -0
- package/dist/contract/contracts-lock-file.js.map +1 -1
- package/dist/contract/derive-free-feature.d.ts +19 -0
- package/dist/contract/derive-free-feature.d.ts.map +1 -1
- package/dist/contract/derive-free-feature.js +40 -2
- package/dist/contract/derive-free-feature.js.map +1 -1
- package/dist/contract/project-contract-blockers.d.ts.map +1 -1
- package/dist/contract/project-contract-blockers.js +4 -2
- package/dist/contract/project-contract-blockers.js.map +1 -1
- package/dist/contract/record-contract-approval.d.ts +16 -1
- package/dist/contract/record-contract-approval.d.ts.map +1 -1
- package/dist/contract/record-contract-approval.js +29 -1
- package/dist/contract/record-contract-approval.js.map +1 -1
- package/dist/contract/render-free-gherkin.d.ts +2 -0
- package/dist/contract/render-free-gherkin.d.ts.map +1 -1
- package/dist/contract/render-free-gherkin.js +6 -0
- package/dist/contract/render-free-gherkin.js.map +1 -1
- package/dist/contract/resolve-unit-trace-identities.d.ts +12 -0
- package/dist/contract/resolve-unit-trace-identities.d.ts.map +1 -1
- package/dist/contract/resolve-unit-trace-identities.js +33 -49
- package/dist/contract/resolve-unit-trace-identities.js.map +1 -1
- package/dist/contract/trace/collect-trace-evidence.d.ts +16 -5
- package/dist/contract/trace/collect-trace-evidence.d.ts.map +1 -1
- package/dist/contract/trace/collect-trace-evidence.js +101 -43
- package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
- package/dist/contract/trace/read-file-once.d.ts +20 -0
- package/dist/contract/trace/read-file-once.d.ts.map +1 -0
- package/dist/contract/trace/read-file-once.js +135 -0
- package/dist/contract/trace/read-file-once.js.map +1 -0
- package/dist/contract/trace/scenario-id-claims.d.ts +11 -0
- package/dist/contract/trace/scenario-id-claims.d.ts.map +1 -0
- package/dist/contract/trace/scenario-id-claims.js +89 -0
- package/dist/contract/trace/scenario-id-claims.js.map +1 -0
- package/dist/contract/trace/select-final-attempt.d.ts +4 -2
- package/dist/contract/trace/select-final-attempt.d.ts.map +1 -1
- package/dist/contract/trace/select-final-attempt.js +7 -4
- package/dist/contract/trace/select-final-attempt.js.map +1 -1
- package/dist/contract/trace/test-step-join.d.ts +3 -3
- package/dist/contract/trace/test-step-join.d.ts.map +1 -1
- package/dist/contract/trace/test-step-join.js +2 -2
- package/dist/contract/trace/test-step-join.js.map +1 -1
- package/dist/contract/trace/trace-binding.d.ts +3 -2
- package/dist/contract/trace/trace-binding.d.ts.map +1 -1
- package/dist/contract/trace/trace-binding.js +6 -3
- package/dist/contract/trace/trace-binding.js.map +1 -1
- package/dist/contract/trace/trace-file-reader.d.ts +14 -2
- package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
- package/dist/contract/trace/trace-file-reader.js +27 -45
- package/dist/contract/trace/trace-file-reader.js.map +1 -1
- package/dist/contract/trace/trace-freshness.d.ts +27 -0
- package/dist/contract/trace/trace-freshness.d.ts.map +1 -0
- package/dist/contract/trace/trace-freshness.js +146 -0
- package/dist/contract/trace/trace-freshness.js.map +1 -0
- package/dist/contract/trace/trace-identity-resolution.d.ts +2 -0
- package/dist/contract/trace/trace-identity-resolution.d.ts.map +1 -1
- package/dist/contract/trace/trace-identity-resolution.js +6 -3
- package/dist/contract/trace/trace-identity-resolution.js.map +1 -1
- package/dist/contract/trace/trace-run-record.d.ts +8 -3
- package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
- package/dist/contract/trace/trace-run-record.js +42 -22
- package/dist/contract/trace/trace-run-record.js.map +1 -1
- package/dist/contract/trace/trace-steps.js +1 -1
- package/dist/contract/trace/trace-steps.js.map +1 -1
- package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
- package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
- package/dist/contract/trace/trace-zip-limits.js +6 -2
- package/dist/contract/trace/trace-zip-limits.js.map +1 -1
- package/dist/contract/verify-approval-decisions.d.ts.map +1 -1
- package/dist/contract/verify-approval-decisions.js +9 -4
- package/dist/contract/verify-approval-decisions.js.map +1 -1
- package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
- package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
- package/dist/decisions/confirmed-decision-commands.js +21 -3
- package/dist/decisions/confirmed-decision-commands.js.map +1 -1
- package/dist/decisions/decision-schema.d.ts +2 -1
- package/dist/decisions/decision-schema.d.ts.map +1 -1
- package/dist/decisions/decision-schema.js +2 -1
- package/dist/decisions/decision-schema.js.map +1 -1
- package/dist/decisions/git-head-trust.d.ts +12 -0
- package/dist/decisions/git-head-trust.d.ts.map +1 -1
- package/dist/decisions/git-head-trust.js +52 -0
- package/dist/decisions/git-head-trust.js.map +1 -1
- package/dist/decisions/human-confirmation.d.ts +2 -0
- package/dist/decisions/human-confirmation.d.ts.map +1 -1
- package/dist/decisions/human-confirmation.js +6 -0
- package/dist/decisions/human-confirmation.js.map +1 -1
- package/dist/decisions/legacy-waiver-migration.d.ts +8 -1
- package/dist/decisions/legacy-waiver-migration.d.ts.map +1 -1
- package/dist/decisions/legacy-waiver-migration.js +13 -4
- package/dist/decisions/legacy-waiver-migration.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +11 -2
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +8 -4
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +6 -1
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +11 -4
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/xlsx-cells.js +1 -1
- package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
- package/dist/exporters/xlsx-exporter.d.ts +3 -2
- package/dist/exporters/xlsx-exporter.d.ts.map +1 -1
- package/dist/exporters/xlsx-exporter.js +4 -4
- package/dist/exporters/xlsx-exporter.js.map +1 -1
- package/dist/graph/board-human-actions.d.ts +5 -0
- package/dist/graph/board-human-actions.d.ts.map +1 -0
- package/dist/graph/board-human-actions.js +83 -0
- package/dist/graph/board-human-actions.js.map +1 -0
- package/dist/graph/evaluate-graph.d.ts +21 -1
- package/dist/graph/evaluate-graph.d.ts.map +1 -1
- package/dist/graph/evaluate-graph.js +37 -9
- package/dist/graph/evaluate-graph.js.map +1 -1
- package/dist/graph/human-action.d.ts +21 -0
- package/dist/graph/human-action.d.ts.map +1 -0
- package/dist/graph/human-action.js +3 -0
- package/dist/graph/human-action.js.map +1 -0
- package/dist/graph/manual-triage-decisions.d.ts +19 -0
- package/dist/graph/manual-triage-decisions.d.ts.map +1 -0
- package/dist/graph/manual-triage-decisions.js +53 -0
- package/dist/graph/manual-triage-decisions.js.map +1 -0
- package/dist/graph/node-decl.d.ts +16 -0
- package/dist/graph/node-decl.d.ts.map +1 -1
- package/dist/graph/node-decl.js +4 -1
- package/dist/graph/node-decl.js.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.js +22 -6
- package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
- package/dist/graph/nodes/test-case-nodes.d.ts +3 -0
- package/dist/graph/nodes/test-case-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/test-case-nodes.js +33 -4
- package/dist/graph/nodes/test-case-nodes.js.map +1 -1
- package/dist/graph/nodes/unit-audit-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/unit-audit-nodes.js +1 -0
- package/dist/graph/nodes/unit-audit-nodes.js.map +1 -1
- package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
- package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
- package/dist/graph/nodes/unit-contract-node.js +16 -1
- package/dist/graph/nodes/unit-contract-node.js.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.js +2 -1
- package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
- package/dist/graph/test-case-results.d.ts +1 -0
- package/dist/graph/test-case-results.d.ts.map +1 -1
- package/dist/graph/test-case-results.js +27 -3
- package/dist/graph/test-case-results.js.map +1 -1
- package/dist/graph/test-case-satisfied.d.ts +5 -4
- package/dist/graph/test-case-satisfied.d.ts.map +1 -1
- package/dist/graph/test-case-satisfied.js +4 -0
- package/dist/graph/test-case-satisfied.js.map +1 -1
- package/dist/graph/test-case-state-shape.d.ts +7 -1
- package/dist/graph/test-case-state-shape.d.ts.map +1 -1
- package/dist/graph/unit-artifact-probes.d.ts.map +1 -1
- package/dist/graph/unit-artifact-probes.js +3 -1
- package/dist/graph/unit-artifact-probes.js.map +1 -1
- package/dist/graph/unit-results-freshness.d.ts +29 -0
- package/dist/graph/unit-results-freshness.d.ts.map +1 -0
- package/dist/graph/unit-results-freshness.js +213 -0
- package/dist/graph/unit-results-freshness.js.map +1 -0
- package/dist/harness/audit.d.ts +3 -2
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +4 -17
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/gate-rules.yaml +3 -3
- package/dist/harness/defect-draft.d.ts.map +1 -1
- package/dist/harness/defect-draft.js +12 -2
- package/dist/harness/defect-draft.js.map +1 -1
- package/dist/harness/defects.d.ts +14 -0
- package/dist/harness/defects.d.ts.map +1 -1
- package/dist/harness/defects.js +14 -10
- package/dist/harness/defects.js.map +1 -1
- package/dist/harness/flow-check.d.ts +9 -0
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +11 -0
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/gates/audit-gate-verdict.d.ts.map +1 -1
- package/dist/harness/gates/audit-gate-verdict.js +2 -1
- package/dist/harness/gates/audit-gate-verdict.js.map +1 -1
- package/dist/harness/gates/audit-input-hash.d.ts +2 -0
- package/dist/harness/gates/audit-input-hash.d.ts.map +1 -1
- package/dist/harness/gates/audit-input-hash.js +30 -2
- package/dist/harness/gates/audit-input-hash.js.map +1 -1
- package/dist/harness/gates/audit-report-verification.d.ts +17 -6
- package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
- package/dist/harness/gates/audit-report-verification.js +55 -21
- package/dist/harness/gates/audit-report-verification.js.map +1 -1
- package/dist/harness/gates/delivery-checklist.d.ts +3 -0
- package/dist/harness/gates/delivery-checklist.d.ts.map +1 -1
- package/dist/harness/gates/delivery-checklist.js +39 -6
- package/dist/harness/gates/delivery-checklist.js.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
- package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.js +26 -1
- package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
- package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
- package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-step.js +17 -2
- package/dist/harness/gates/delivery-decision-step.js.map +1 -1
- package/dist/harness/gates/in-run-decision-check.d.ts.map +1 -1
- package/dist/harness/gates/in-run-decision-check.js +40 -12
- package/dist/harness/gates/in-run-decision-check.js.map +1 -1
- package/dist/harness/gates/migrated-waiver-import-bounds.d.ts +13 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.d.ts.map +1 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.js +141 -0
- package/dist/harness/gates/migrated-waiver-import-bounds.js.map +1 -0
- package/dist/harness/gates/migrated-waiver-review.d.ts +14 -4
- package/dist/harness/gates/migrated-waiver-review.d.ts.map +1 -1
- package/dist/harness/gates/migrated-waiver-review.js +78 -80
- package/dist/harness/gates/migrated-waiver-review.js.map +1 -1
- package/dist/harness/journey.d.ts +40 -0
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +155 -22
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/next-step.d.ts +19 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +21 -0
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/provenance.d.ts +29 -0
- package/dist/harness/provenance.d.ts.map +1 -1
- package/dist/harness/provenance.js +109 -9
- package/dist/harness/provenance.js.map +1 -1
- package/dist/harness/triage-manual-classification.d.ts +32 -0
- package/dist/harness/triage-manual-classification.d.ts.map +1 -0
- package/dist/harness/triage-manual-classification.js +56 -0
- package/dist/harness/triage-manual-classification.js.map +1 -0
- package/dist/harness/unit-paths.d.ts +8 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +23 -1
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/hooks/bash-read-only-segment.d.ts +9 -0
- package/dist/hooks/bash-read-only-segment.d.ts.map +1 -0
- package/dist/hooks/bash-read-only-segment.js +168 -0
- package/dist/hooks/bash-read-only-segment.js.map +1 -0
- package/dist/hooks/bash-write-targets.d.ts +12 -2
- package/dist/hooks/bash-write-targets.d.ts.map +1 -1
- package/dist/hooks/bash-write-targets.js +122 -29
- package/dist/hooks/bash-write-targets.js.map +1 -1
- package/dist/hooks/guard-protected-paths.d.ts +4 -0
- package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
- package/dist/hooks/guard-protected-paths.js +13 -3
- package/dist/hooks/guard-protected-paths.js.map +1 -1
- package/dist/hooks/guard-rules.d.ts +0 -17
- package/dist/hooks/guard-rules.d.ts.map +1 -1
- package/dist/hooks/guard-rules.js +79 -10
- package/dist/hooks/guard-rules.js.map +1 -1
- package/dist/hooks/guard-tty-spoof-rule.d.ts +3 -1
- package/dist/hooks/guard-tty-spoof-rule.d.ts.map +1 -1
- package/dist/hooks/guard-tty-spoof-rule.js +3 -2
- package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
- package/dist/hooks/hand-back-core.d.ts +9 -4
- package/dist/hooks/hand-back-core.d.ts.map +1 -1
- package/dist/hooks/hand-back-core.js +24 -11
- package/dist/hooks/hand-back-core.js.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.d.ts +2 -1
- package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.js +9 -4
- package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
- package/dist/hooks/stop-next-step-hook.d.ts.map +1 -1
- package/dist/hooks/stop-next-step-hook.js +1 -0
- package/dist/hooks/stop-next-step-hook.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts +5 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +12 -3
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +1 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +6 -3
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +2 -0
- package/dist/orchestrator/templates/ai-src/commands/review.md +2 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
- package/dist/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
- package/package.json +3 -3
- package/src/capabilities/discover.ts +15 -2
- package/src/cli/case-triage-hint.ts +31 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/contract-derive-command.ts +20 -8
- package/src/cli/commands/contract.ts +41 -7
- package/src/cli/commands/delivery.ts +56 -18
- package/src/cli/commands/gate.ts +2 -1
- package/src/cli/commands/generate.ts +13 -2
- package/src/cli/commands/journey.ts +8 -6
- package/src/cli/commands/next.ts +41 -12
- package/src/cli/commands/triage.ts +41 -3
- package/src/cli/commands/update.ts +6 -2
- package/src/cli/index.ts +5 -1
- package/src/cli/json-error-on-exit.ts +72 -0
- package/src/cli/project-sungen-invocation.ts +59 -0
- package/src/cli/run-command-hints.ts +47 -0
- package/src/cli/waiting-for-you-block.ts +27 -0
- package/src/contract/accept-opaque-steps.ts +114 -0
- package/src/contract/approve-contract.ts +10 -3
- package/src/contract/build-contract-verdict.ts +2 -2
- package/src/contract/build-trace-conformance.ts +11 -8
- package/src/contract/classify-contract-change.ts +3 -3
- package/src/contract/classify-trace-conformance.ts +76 -22
- package/src/contract/contract-schema.ts +31 -2
- package/src/contract/contracts-lock-file.ts +1 -0
- package/src/contract/derive-free-feature.ts +46 -3
- package/src/contract/project-contract-blockers.ts +4 -2
- package/src/contract/record-contract-approval.ts +41 -1
- package/src/contract/render-free-gherkin.ts +5 -0
- package/src/contract/resolve-unit-trace-identities.ts +35 -12
- package/src/contract/trace/collect-trace-evidence.ts +105 -36
- package/src/contract/trace/read-file-once.ts +95 -0
- package/src/contract/trace/scenario-id-claims.ts +60 -0
- package/src/contract/trace/select-final-attempt.ts +12 -5
- package/src/contract/trace/test-step-join.ts +6 -5
- package/src/contract/trace/trace-binding.ts +7 -4
- package/src/contract/trace/trace-file-reader.ts +34 -13
- package/src/contract/trace/trace-freshness.ts +110 -0
- package/src/contract/trace/trace-identity-resolution.ts +7 -4
- package/src/contract/trace/trace-run-record.ts +37 -15
- package/src/contract/trace/trace-steps.ts +1 -1
- package/src/contract/trace/trace-zip-limits.ts +6 -2
- package/src/contract/verify-approval-decisions.ts +9 -5
- package/src/decisions/confirmed-decision-commands.ts +25 -4
- package/src/decisions/decision-schema.ts +2 -1
- package/src/decisions/git-head-trust.ts +44 -0
- package/src/decisions/human-confirmation.ts +5 -0
- package/src/decisions/legacy-waiver-migration.ts +14 -3
- package/src/exporters/csv-exporter.ts +12 -4
- package/src/exporters/matrix/export.ts +11 -4
- package/src/exporters/matrix/xlsx-cells.ts +1 -1
- package/src/exporters/xlsx-exporter.ts +5 -4
- package/src/graph/board-human-actions.ts +80 -0
- package/src/graph/evaluate-graph.ts +45 -10
- package/src/graph/human-action.ts +33 -0
- package/src/graph/manual-triage-decisions.ts +55 -0
- package/src/graph/node-decl.ts +18 -0
- package/src/graph/nodes/test-case-contract-nodes.ts +28 -7
- package/src/graph/nodes/test-case-nodes.ts +37 -5
- package/src/graph/nodes/unit-audit-nodes.ts +2 -1
- package/src/graph/nodes/unit-contract-node.ts +20 -2
- package/src/graph/nodes/unit-design-nodes.ts +2 -1
- package/src/graph/test-case-results.ts +25 -4
- package/src/graph/test-case-satisfied.ts +7 -4
- package/src/graph/test-case-state-shape.ts +7 -2
- package/src/graph/unit-artifact-probes.ts +3 -1
- package/src/graph/unit-results-freshness.ts +165 -0
- package/src/harness/audit.ts +4 -13
- package/src/harness/catalog/gate-rules.yaml +3 -3
- package/src/harness/defect-draft.ts +13 -2
- package/src/harness/defects.ts +22 -9
- package/src/harness/flow-check.ts +11 -0
- package/src/harness/gates/audit-gate-verdict.ts +2 -1
- package/src/harness/gates/audit-input-hash.ts +27 -3
- package/src/harness/gates/audit-report-verification.ts +57 -24
- package/src/harness/gates/delivery-checklist.ts +39 -7
- package/src/harness/gates/delivery-decision-gate.ts +29 -1
- package/src/harness/gates/delivery-decision-step.ts +19 -3
- package/src/harness/gates/in-run-decision-check.ts +38 -13
- package/src/harness/gates/migrated-waiver-import-bounds.ts +103 -0
- package/src/harness/gates/migrated-waiver-review.ts +90 -37
- package/src/harness/journey.ts +174 -21
- package/src/harness/next-step.ts +34 -1
- package/src/harness/provenance.ts +99 -11
- package/src/harness/triage-manual-classification.ts +76 -0
- package/src/harness/unit-paths.ts +19 -0
- package/src/hooks/bash-read-only-segment.ts +130 -0
- package/src/hooks/bash-write-targets.ts +109 -23
- package/src/hooks/guard-protected-paths.ts +13 -3
- package/src/hooks/guard-rules.ts +37 -18
- package/src/hooks/guard-tty-spoof-rule.ts +6 -3
- package/src/hooks/hand-back-core.ts +24 -12
- package/src/hooks/session-start-unit-state-hook.ts +9 -4
- package/src/hooks/stop-next-step-hook.ts +1 -0
- package/src/orchestrator/assets-drift.ts +12 -5
- package/src/orchestrator/project-initializer.ts +1 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +6 -3
- package/src/orchestrator/templates/ai-src/commands/delivery.md +2 -0
- package/src/orchestrator/templates/ai-src/commands/review.md +2 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.claude.md +11 -0
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.codex.md +11 -0
- package/src/orchestrator/templates/ai-src/partials/waiting-for-you.copilot.md +11 -0
|
@@ -1,22 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Provenance stamp for deterministic reports (score-determinism D1)
|
|
2
|
+
* Provenance stamp for deterministic reports (score-determinism D1), and the RULES the audit's
|
|
3
|
+
* verdict was judged by (#933, owner decision W4).
|
|
3
4
|
*
|
|
4
|
-
* The audit score is a pure function of the project files + the bundled
|
|
5
|
-
* Stamping
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The audit score is a pure function of the project files + the bundled catalogs + the scoring
|
|
6
|
+
* code. Stamping them into the report lets two users who get different scores see IMMEDIATELY
|
|
7
|
+
* whether the cause is a rules mismatch rather than a real difference in their test design — and
|
|
8
|
+
* lets `verifyAuditReport` tell a report judged by today's rules from one judged by other rules:
|
|
9
|
+
*
|
|
10
|
+
* sungenVersion which package wrote it — informational: a newer sungen with the same rules
|
|
11
|
+
* gives the same verdict, so the version alone never makes a report stale
|
|
12
|
+
* catalogHash sha256(universal-viewpoints.yaml), first 12 hex — the viewpoint catalog
|
|
13
|
+
* rulesHash sha256(AUDIT_RULES_VERSION + every catalog file under harness/catalog/ + each
|
|
14
|
+
* bundled driver's sources/catalogs), first 12 hex — everything the verdict
|
|
15
|
+
* depends on that is not the project's own files. No package VERSION is hashed:
|
|
16
|
+
* packages are lockstep-versioned, so a release would stale every report
|
|
9
17
|
*/
|
|
10
18
|
import * as fs from 'fs';
|
|
11
19
|
import * as path from 'path';
|
|
12
20
|
import * as crypto from 'crypto';
|
|
13
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The version of the audit's SCORING CODE. Bump it (by one) in the same change whenever what
|
|
24
|
+
* `runAudit` computes for the same project can change: a sensor, a threshold, a gate term
|
|
25
|
+
* (audit-gate-terms.ts), the score model or weights, a finding that feeds the verdict, or the
|
|
26
|
+
* audit sensor of a bundled driver. Every report scored before the bump then reads stale and asks
|
|
27
|
+
* for one `sungen audit`. Catalog edits (harness/catalog/*.yaml) and bundled-driver changes (their
|
|
28
|
+
* sources and catalogs are hashed) need no bump.
|
|
29
|
+
* A release that changes none of these keeps every report trusted across the upgrade.
|
|
30
|
+
* Enforced by tests/audit/run.ts: its rules lock (tests/audit/audit-rules.lock.json) fails when the
|
|
31
|
+
* fixtures' audit output changes under an unchanged rules hash. After the bump, re-record it:
|
|
32
|
+
* npx tsx tests/audit/run.ts --record-rules-lock
|
|
33
|
+
*/
|
|
34
|
+
export const AUDIT_RULES_VERSION = 1;
|
|
35
|
+
|
|
14
36
|
export interface Provenance {
|
|
15
37
|
sungenVersion: string;
|
|
16
38
|
catalogHash: string; // sha256(universal-viewpoints.yaml), first 12 hex — the catalog that drives the score
|
|
39
|
+
rulesHash: string; // sha256(AUDIT_RULES_VERSION + harness/catalog/*.yaml + bundled drivers), first 12 hex — the verdict's rules
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A bundled driver as an input of the rules: its package dir, or null when it is not installed. */
|
|
43
|
+
export interface DriverRulesInput { name: string; version?: string; dir: string | null }
|
|
44
|
+
|
|
45
|
+
/** What is not a driver's rule: dependencies, build output, tests, docs, editor and lock files. */
|
|
46
|
+
const SKIP_DIR = new Set(['node_modules', 'dist', 'tests', 'test', 'coverage']);
|
|
47
|
+
const SKIP_FILE = /^(?:\.|package(?:-lock)?\.json$|tsconfig.*\.json$|.*\.(?:md|tsbuildinfo)$)/;
|
|
48
|
+
|
|
49
|
+
function driverFiles(root: string, rel = ''): string[] {
|
|
50
|
+
const out: string[] = [];
|
|
51
|
+
for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
|
|
52
|
+
if (e.name.startsWith('.')) continue;
|
|
53
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
54
|
+
if (e.isDirectory()) { if (!SKIP_DIR.has(e.name)) out.push(...driverFiles(root, r)); }
|
|
55
|
+
else if (e.isFile() && !SKIP_FILE.test(e.name)) out.push(r);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The package root above a driver's resolved entry file (the dir whose package.json names it). */
|
|
61
|
+
function packageRootOf(entry: string, name: string): string | null {
|
|
62
|
+
let dir = path.dirname(entry);
|
|
63
|
+
for (let i = 0; i < 8; i++) {
|
|
64
|
+
try { if (JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))?.name === name) return dir; } catch { /* keep walking */ }
|
|
65
|
+
const parent = path.dirname(dir);
|
|
66
|
+
if (parent === dir) break;
|
|
67
|
+
dir = parent;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
17
70
|
}
|
|
18
71
|
|
|
72
|
+
/** Each bundled driver as discovery would load it (project anchor, then core's), or `absent`. */
|
|
73
|
+
export function bundledDriverRulesInputs(): DriverRulesInput[] {
|
|
74
|
+
// Lazy: discovery pulls the capability registry, which nothing else here needs.
|
|
75
|
+
const { bundledDriverNames, resolveDriverCopy } = require('../capabilities/discover') as typeof import('../capabilities/discover');
|
|
76
|
+
return bundledDriverNames().map((name) => {
|
|
77
|
+
const copy = resolveDriverCopy(name);
|
|
78
|
+
const dir = copy ? packageRootOf(copy.resolved, name) : null;
|
|
79
|
+
return { name, version: copy?.version, dir };
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const CATALOG_DIR = path.join(__dirname, 'catalog');
|
|
84
|
+
const sha12 = (s: string | Buffer): string => crypto.createHash('sha256').update(s).digest('hex').slice(0, 12);
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The rules hash: the scoring version, every *.yaml of the catalog dir (by name), and each bundled
|
|
88
|
+
* driver — every source / catalog file of its package (by relative path; build output, tests and
|
|
89
|
+
* dependencies excluded), or a stable `absent` marker when it is not installed. A driver's version is
|
|
90
|
+
* deliberately left out: releases bump every package in lockstep, and only content changes the rules.
|
|
91
|
+
*/
|
|
92
|
+
export function rulesHashOf(rulesVersion: number, catalogDir: string = CATALOG_DIR, drivers: DriverRulesInput[] = bundledDriverRulesInputs()): string {
|
|
93
|
+
const files = fs.readdirSync(catalogDir).filter((f) => f.endsWith('.yaml')).sort();
|
|
94
|
+
const h = crypto.createHash('sha256').update(`audit-rules:${rulesVersion}\n`);
|
|
95
|
+
for (const f of files) h.update(`${f}\n`).update(fs.readFileSync(path.join(catalogDir, f))).update('\n');
|
|
96
|
+
for (const d of [...drivers].sort((a, b) => a.name.localeCompare(b.name, 'en'))) {
|
|
97
|
+
h.update(`driver:${d.name}\n`);
|
|
98
|
+
if (!d.dir) { h.update('absent\n'); continue; }
|
|
99
|
+
for (const f of driverFiles(d.dir)) h.update(`${f}\n`).update(fs.readFileSync(path.join(d.dir, f))).update('\n');
|
|
100
|
+
}
|
|
101
|
+
return h.digest('hex').slice(0, 12);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let cached: Provenance | null = null;
|
|
105
|
+
|
|
19
106
|
export function getProvenance(): Provenance {
|
|
107
|
+
if (cached) return cached;
|
|
20
108
|
let sungenVersion = 'unknown';
|
|
21
109
|
try {
|
|
22
110
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
@@ -24,10 +112,10 @@ export function getProvenance(): Provenance {
|
|
|
24
112
|
} catch { /* version stays 'unknown' */ }
|
|
25
113
|
|
|
26
114
|
let catalogHash = 'unknown';
|
|
27
|
-
try {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} catch { /* hash stays 'unknown' */ }
|
|
115
|
+
try { catalogHash = sha12(fs.readFileSync(path.join(CATALOG_DIR, 'universal-viewpoints.yaml'), 'utf-8')); } catch { /* hash stays 'unknown' */ }
|
|
116
|
+
let rulesHash = 'unknown';
|
|
117
|
+
try { rulesHash = rulesHashOf(AUDIT_RULES_VERSION); } catch { /* hash stays 'unknown' */ }
|
|
31
118
|
|
|
32
|
-
|
|
119
|
+
cached = { sungenVersion, catalogHash, rulesHash };
|
|
120
|
+
return cached;
|
|
33
121
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sungen triage --screen <u> --case <id> --as product|test-defect|environment --reason "…"` (#928):
|
|
3
|
+
* classify one red by hand — the one `sungen triage` left unclassified, or one it got wrong — as a
|
|
4
|
+
* `triage-classification` decision in the unit's `decisions/` (read back by graph/manual-triage-decisions.ts).
|
|
5
|
+
*
|
|
6
|
+
* validated first the class, a reason, a case that exists, is red and is not exempt — before any
|
|
7
|
+
* prompt, so a human never confirms something that is then rejected
|
|
8
|
+
* product stricter than any automatic class (it leads to a QA decision): anyone may record it
|
|
9
|
+
* test-defect, can hide a product defect behind "fix the test" / "re-run": a human types a
|
|
10
|
+
* environment confirmation on a terminal (DOS-13.4); without one it is refused, nothing is
|
|
11
|
+
* written and the exact command is printed. Only after the typed phrase does the
|
|
12
|
+
* entry carry `humanConfirmed: true`, and a reader ignores one without it
|
|
13
|
+
* bound evidence_hash = the case's red as it reads now; a re-run voids the classification
|
|
14
|
+
*/
|
|
15
|
+
import type { AppendResult } from '../decisions/append-decision';
|
|
16
|
+
import { appendDecision } from '../decisions/append-decision';
|
|
17
|
+
import type { DecisionDraft } from '../decisions/decision-schema';
|
|
18
|
+
import { gitUserName } from '../decisions/decision-source';
|
|
19
|
+
import { ConfirmationIO, formatCommand, requireTypedConfirmation } from '../decisions/human-confirmation';
|
|
20
|
+
import { resolveUnitCandidates } from '../cli/resolve-unit';
|
|
21
|
+
import { HUMAN_ONLY_TRIAGE, MANUAL_TRIAGE_AS, ManualTriageClass, TRIAGE_DECISION_KIND } from '../graph/manual-triage-decisions';
|
|
22
|
+
import { exemptionOf } from '../graph/test-case-satisfied';
|
|
23
|
+
import { testCaseStates } from '../graph/test-case-states';
|
|
24
|
+
import { unitScopeOfDir } from './gates/unit-gate-overrides';
|
|
25
|
+
|
|
26
|
+
export interface ClassifyRedCaseInput {
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
unit: string;
|
|
29
|
+
caseId: string;
|
|
30
|
+
/** product | test-defect | environment */
|
|
31
|
+
as: string;
|
|
32
|
+
reason?: string;
|
|
33
|
+
io: ConfirmationIO;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ClassifyRedCaseResult extends AppendResult { caseId: string; class: ManualTriageClass }
|
|
37
|
+
|
|
38
|
+
export async function classifyRedCase(i: ClassifyRedCaseInput): Promise<ClassifyRedCaseResult> {
|
|
39
|
+
const cls = Object.prototype.hasOwnProperty.call(MANUAL_TRIAGE_AS, i.as) ? MANUAL_TRIAGE_AS[i.as] : undefined;
|
|
40
|
+
if (!cls) throw new Error(`--as must be one of ${Object.keys(MANUAL_TRIAGE_AS).join(', ')} (got "${i.as}")`);
|
|
41
|
+
const reason = (i.reason ?? '').trim();
|
|
42
|
+
if (!reason) throw new Error('--reason is required: a classification records WHY this red is what you say it is');
|
|
43
|
+
const found = resolveUnitCandidates(i.unit, i.projectRoot);
|
|
44
|
+
if (found.length !== 1) throw new Error(`"${i.unit}" does not name exactly one unit under qa/screens, qa/flows or qa/api`);
|
|
45
|
+
const unitDir = found[0].dir;
|
|
46
|
+
const scope = unitScopeOfDir(i.projectRoot, unitDir);
|
|
47
|
+
if (!scope) throw new Error(`"${i.unit}" has no decisions scope`);
|
|
48
|
+
const cases = testCaseStates(i.projectRoot, i.unit, { unitDir, auditFindings: [] });
|
|
49
|
+
const tc = cases.find((c) => c.id === i.caseId);
|
|
50
|
+
if (!tc) {
|
|
51
|
+
const known = cases.slice(0, 8).map((c) => c.id).join(', ');
|
|
52
|
+
throw new Error(`no test case "${i.caseId}" in ${i.unit}${known ? ` — known: ${known}${cases.length > 8 ? ', …' : ''}` : ''}`);
|
|
53
|
+
}
|
|
54
|
+
if (tc.result !== 'failed' || !tc.evidence) throw new Error(`${tc.id} has no red result to classify (result: ${tc.result})`);
|
|
55
|
+
const exempt = exemptionOf(tc);
|
|
56
|
+
if (exempt) throw new Error(`${tc.id} is exempt (${exempt}) — it needs no classification`);
|
|
57
|
+
|
|
58
|
+
const human = HUMAN_ONLY_TRIAGE.has(cls);
|
|
59
|
+
if (human) {
|
|
60
|
+
await requireTypedConfirmation({
|
|
61
|
+
what: `classify the red ${tc.id} of ${i.unit} as ${cls}`,
|
|
62
|
+
phrase: tc.id,
|
|
63
|
+
command: formatCommand(['sungen', 'triage', found[0].kind === 'api' ? '--api' : '--screen', i.unit, '--case', tc.id, '--as', i.as, '--reason', reason]),
|
|
64
|
+
summary: [`case: ${tc.title}`, `automatic triage: ${tc.triage ? `${tc.triage.class} (${tc.triage.ruleId})` : 'none current'}`, `reason: ${reason}`,
|
|
65
|
+
`recorded by: ${gitUserName(i.projectRoot)}`, `as ${cls} it leaves the product-defect path — confirm only after reading its evidence (trace, error)`,
|
|
66
|
+
'it stops counting as soon as the case runs again'],
|
|
67
|
+
io: i.io,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const draft: DecisionDraft = {
|
|
71
|
+
kind: TRIAGE_DECISION_KIND, anchors: [], text: `${tc.id} is ${cls}: ${tc.title}`, reason,
|
|
72
|
+
payload: { case: tc.id, class: cls, ...(human ? { humanConfirmed: true } : {}) },
|
|
73
|
+
source: { by: gitUserName(i.projectRoot), where: 'cli' }, valid_from: new Date().toISOString(), visibility: 'internal', evidence_hash: tc.evidence,
|
|
74
|
+
};
|
|
75
|
+
return { ...appendDecision(i.projectRoot, scope, draft), caseId: tc.id, class: cls };
|
|
76
|
+
}
|
|
@@ -49,3 +49,22 @@ export function featureFilesFor(unitDir: string, unit: string): string[] {
|
|
|
49
49
|
...names.filter((n) => n !== canonicalName),
|
|
50
50
|
].map((n) => path.join(dir, n));
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The catalog-resolution id for a unit dir (relative to qa/): screen · flows/<flow> · api/<area> ·
|
|
55
|
+
* api/flows/<flow>; a bare screen is its basename. Shared by the audit, which scores and links the
|
|
56
|
+
* unit under this id, and its inputsHash, which reads what the audit reads under the same id.
|
|
57
|
+
*/
|
|
58
|
+
export function catalogIdFromScreenDir(screenDir: string): string {
|
|
59
|
+
const parts = screenDir.split(path.sep);
|
|
60
|
+
const qa = parts.lastIndexOf('qa');
|
|
61
|
+
if (qa >= 0) {
|
|
62
|
+
if (parts[qa + 1] === 'api' && parts[qa + 2] === 'flows' && parts[qa + 3]) return `api/flows/${parts[qa + 3]}`;
|
|
63
|
+
if (parts[qa + 1] === 'api' && parts[qa + 2]) return `api/${parts[qa + 2]}`;
|
|
64
|
+
if (parts[qa + 1] === 'flows' && parts[qa + 2]) return `flows/${parts[qa + 2]}`;
|
|
65
|
+
}
|
|
66
|
+
return path.basename(screenDir);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A screen unit — the only kind a sibling flow credits (TQ-4); flows and api units are not. */
|
|
70
|
+
export const isScreenUnitId = (catalogId: string): boolean => !/^(flows|api)\//.test(catalogId);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Bash segments only READ, so they may name a protected path freely (bash-protected-mention).
|
|
3
|
+
*
|
|
4
|
+
* An allowlist, fail-closed (#968): a command is read-only only in the forms listed here. It drops
|
|
5
|
+
* back to "the guard cannot tell" when
|
|
6
|
+
* - the command word has a `/` outside the system bin dirs (`./cat`, `/tmp/evil/jq`),
|
|
7
|
+
* - a word holds `$`, `{`, `*`, `?` or `[` (`$'--output'`, `${…}`, `--{output=…,stat}`, a `q*` glob can
|
|
8
|
+
* hide an option name or a path),
|
|
9
|
+
* - an option runs a program or writes a file, also abbreviated where the tool accepts that
|
|
10
|
+
* (`rg --pre`, `git diff --output` / `--outp`, ugrep's `grep --filter` / `--save-config` / `--pager`
|
|
11
|
+
* / `--config`, `sort -o` / `--o…` / `--c…`),
|
|
12
|
+
* - git has any global option before its subcommand except `--no-pager` / `-P` / `--no-optional-locks`
|
|
13
|
+
* (`-c`, `--config-env`, `--exec-path`, `-C`, `--git-dir`… can point it at another program),
|
|
14
|
+
* - its output is piped into anything that is not a read-only command or filter (`| xargs rm`, `| sh`).
|
|
15
|
+
* And the whole LINE loses the exemption (lineKeepsReadOnlyExemption) when it uses command
|
|
16
|
+
* substitution or changes shell state that a later command obeys: any `NAME=value`, `export`,
|
|
17
|
+
* `declare`, `typeset`, `set`, `setopt`, `shopt`, `alias`, `unalias`, `hash`, `unset`, `read`,
|
|
18
|
+
* `enable`, `builtin`, `trap`, `source` / `.`, `eval`, or a function definition.
|
|
19
|
+
* `git grep` is not read-only at all: `-O` runs a program, also inside a flag cluster (`-nOrm`) and
|
|
20
|
+
* as an abbreviation (`--open=`). `node -e|-p` counts only when its script can do nothing but read
|
|
21
|
+
* (isReadOnlyNodeEval).
|
|
22
|
+
*
|
|
23
|
+
* Accepted floor, not closed here: `sungen` is listed read-only (it writes approval records only
|
|
24
|
+
* through its own human gates); a path built at runtime (`$VAR` away from a protected name), a
|
|
25
|
+
* symlink, or a script that writes without naming the path is out of reach; a bare directory
|
|
26
|
+
* (`rm -rf qa/decisions`, `cd qa && rm -rf decisions/`) is not a protected-path mention (#970);
|
|
27
|
+
* `git -C <dir> …` is not followed; a same-line `D=qa/decisions; … $D` is not tracked; pathless
|
|
28
|
+
* destructive git (`clean -fdx`, `stash`, `reset --hard`, `checkout .`, `apply`, `am`) is not seen;
|
|
29
|
+
* and a quoting trick the tokenizer reads differently from the shell can turn a deny into an ask.
|
|
30
|
+
* The backstop is release-time decision integrity: the git HEAD comparison and the in-run rule.
|
|
31
|
+
*/
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import type { BashSegment } from './bash-write-targets';
|
|
34
|
+
|
|
35
|
+
const READ_ONLY = new Set(['cat', 'ls', 'head', 'tail', 'grep', 'rg', 'wc', 'diff', 'stat', 'jq', 'sungen', 'sungen.js']);
|
|
36
|
+
const GIT_READ_ONLY = new Set(['diff', 'log', 'show', 'status', 'check-ignore', 'ls-files', 'ls-tree', 'blame', 'rev-parse', 'cat-file']);
|
|
37
|
+
/** The only git global options allowed before a read-only subcommand. */
|
|
38
|
+
const GIT_SAFE_GLOBALS = new Set(['--no-pager', '-P', '--no-optional-locks']);
|
|
39
|
+
/** Filters that write only to stdout — safe as a pipe sink, in the forms isPipeFilter allows. */
|
|
40
|
+
const FILTERS = new Set(['sort', 'uniq', 'cut', 'tr']);
|
|
41
|
+
|
|
42
|
+
/** A command word with a `/` counts only from these dirs: `./cat` or `/tmp/evil/jq` is not `cat` / `jq`. */
|
|
43
|
+
const SYSTEM_BIN = new Set(['/usr/bin', '/bin', '/usr/local/bin', '/opt/homebrew/bin']);
|
|
44
|
+
/** Characters that let the shell build a word the guard cannot read: expansions, braces, globs. */
|
|
45
|
+
const OPAQUE_WORD = /[$*?[{]/;
|
|
46
|
+
|
|
47
|
+
/** Is `word` the long option `--<one of names>`, or an abbreviation of one at least `min` chars long? */
|
|
48
|
+
const longOpt = (names: string[], min: number) => (word: string): boolean => {
|
|
49
|
+
const m = /^--([^=]+)/.exec(word);
|
|
50
|
+
return !!m && names.some((n) => n === m[1] || (m[1].length >= min && n.startsWith(m[1])));
|
|
51
|
+
};
|
|
52
|
+
/** Options that make an otherwise read-only command run a program or write a file. */
|
|
53
|
+
const UNSAFE_OPT: Record<string, (word: string) => boolean> = {
|
|
54
|
+
rg: (w) => /^--(?:pre|hostname-bin)(?:=|$)/.test(w),
|
|
55
|
+
git: longOpt(['output', 'ext-diff', 'textconv'], 2),
|
|
56
|
+
// `grep` may be ugrep: --filter runs programs, --save-config writes, --pager / --view run one, --config / ---FILE load those
|
|
57
|
+
grep: (w) => w.startsWith('---') || longOpt(['filter', 'save-config', 'pager', 'view', 'config'], 3)(w),
|
|
58
|
+
};
|
|
59
|
+
const trustedWords = (seg: BashSegment): boolean =>
|
|
60
|
+
!seg.assigns.length && !seg.words.some((w) => OPAQUE_WORD.test(w))
|
|
61
|
+
&& (!seg.cmdPath.includes('/') || SYSTEM_BIN.has(path.posix.dirname(seg.cmdPath)));
|
|
62
|
+
|
|
63
|
+
/** `$(…)`, backticks, `<(…)`, `>(…)`: a command's output becomes another command's words. */
|
|
64
|
+
const SUBSTITUTION = /\$\(|`|[<>]\(/;
|
|
65
|
+
/** Commands that change shell state a later command on the line obeys. */
|
|
66
|
+
const SHELL_STATE = new Set([
|
|
67
|
+
'export', 'declare', 'typeset', 'local', 'readonly', 'set', 'setopt', 'shopt', 'alias', 'unalias', 'hash', 'unset',
|
|
68
|
+
'read', 'enable', 'builtin', 'trap', 'source', '.', 'eval', 'function',
|
|
69
|
+
]);
|
|
70
|
+
/** `name() {` / `name() (` — a function definition (`function name` is caught by SHELL_STATE). */
|
|
71
|
+
const FUNCTION_DEF = /(?:^|[\s;&|])[\w.:-]+\s*\(\s*\)\s*[{(]/;
|
|
72
|
+
|
|
73
|
+
/** May any read-only segment of this line keep its exemption? */
|
|
74
|
+
export function lineKeepsReadOnlyExemption(command: string, segs: BashSegment[]): boolean {
|
|
75
|
+
return !SUBSTITUTION.test(command) && !FUNCTION_DEF.test(command)
|
|
76
|
+
&& !segs.some((s) => s.assigns.length || SHELL_STATE.has(s.cmd));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** `node -e` / `node -p` whose script can only read: no write/exec API, no computed access, no escapes. */
|
|
80
|
+
const NODE_EVAL_FLAGS = new Set(['-e', '-p', '--eval', '--print']);
|
|
81
|
+
const NODE_UNSAFE = new RegExp([
|
|
82
|
+
'[`\\[\\\\]', // template literals, computed property access, string escapes
|
|
83
|
+
'flag', // `readFileSync(p, { flag: 'w' })` truncates p
|
|
84
|
+
'write', 'append', '\\brm', 'unlink', 'rename', 'copy', '\\bcp', 'mkd', 'truncate', 'stream', 'open', 'link',
|
|
85
|
+
'chmod', 'chown', 'utime', 'exec', 'spawn', 'fork', 'child', 'eval', 'function', 'import', 'constructor',
|
|
86
|
+
'proto', 'reflect', 'proxy', 'global', 'process', 'module', 'object', 'concat', '\\bvm\\b', 'worker', 'binding',
|
|
87
|
+
].join('|'), 'i');
|
|
88
|
+
const SAFE_REQUIRE = /\brequire\(\s*(['"])(?:(?:node:)?(?:fs|path)|[./][^'"]*\.json)\1\s*\)/g;
|
|
89
|
+
|
|
90
|
+
export function isReadOnlyNodeEval(args: string[]): boolean {
|
|
91
|
+
if (args.length !== 2 || !NODE_EVAL_FLAGS.has(args[0])) return false;
|
|
92
|
+
const script = args[1];
|
|
93
|
+
return !NODE_UNSAFE.test(script) && !/\brequire\b/.test(script.replace(SAFE_REQUIRE, ''));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `git [--no-pager|-P|--no-optional-locks]… <read-only subcommand> …` — anchored, no other global option. */
|
|
97
|
+
function isReadOnlyGit(words: string[]): boolean {
|
|
98
|
+
let k = 0;
|
|
99
|
+
while (GIT_SAFE_GLOBALS.has(words[k])) k++;
|
|
100
|
+
return GIT_READ_ONLY.has(words[k] ?? '');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Does this segment only read? (`words` holds its arguments and redirection targets.) */
|
|
104
|
+
export function isReadOnlySegment(seg: BashSegment): boolean {
|
|
105
|
+
if (!seg.cmd || !trustedWords(seg)) return false;
|
|
106
|
+
const unsafe = UNSAFE_OPT[seg.cmd];
|
|
107
|
+
if (unsafe && seg.words.some(unsafe)) return false;
|
|
108
|
+
if (seg.cmd === 'git') return isReadOnlyGit(seg.words);
|
|
109
|
+
if (seg.cmd === 'node') return isReadOnlyNodeEval(seg.words);
|
|
110
|
+
return READ_ONLY.has(seg.cmd);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A pipe sink that cannot act on what it reads: a read-only command, or a stdout-only filter. */
|
|
114
|
+
function isPipeFilter(seg: BashSegment): boolean {
|
|
115
|
+
if (isReadOnlySegment(seg)) return true;
|
|
116
|
+
if (!FILTERS.has(seg.cmd) || !trustedWords(seg)) return false;
|
|
117
|
+
// `sort -o out` / `--compress-program=…` (any `--o…` / `--c…` abbreviation) write or run; `uniq in out` writes its second operand.
|
|
118
|
+
if (seg.cmd === 'sort') return !seg.words.some((w) => /^(?:-[^-]*o|--[oc])/.test(w));
|
|
119
|
+
if (seg.cmd === 'uniq') return seg.words.every((w) => w.startsWith('-'));
|
|
120
|
+
return true; // cut, tr: operands are inputs or character sets
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Does everything downstream of segment `i` in its pipeline only read / filter? */
|
|
124
|
+
export function pipelineStaysReadOnly(segs: BashSegment[], i: number): boolean {
|
|
125
|
+
for (let j = i; segs[j]?.pipesTo; j++) {
|
|
126
|
+
const next = segs[j + 1];
|
|
127
|
+
if (!next || !isPipeFilter(next)) return false;
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
@@ -1,18 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The files a Bash command line WRITES through the three forms the pre-tool guard covers (P0.1):
|
|
3
|
-
* output redirection (`>`, `>>`, `>|`, `&>`, `2
|
|
3
|
+
* output redirection (`>`, `>>`, `>|`, `&>`, `2>`, `>&file`, zsh `>!`…), `tee`, in-place `sed -i`, and
|
|
4
|
+
* `git … --output <file>` (also abbreviated, `--outp=`) (#968).
|
|
4
5
|
*
|
|
5
|
-
* A floor, not a shell interpreter: quotes, escapes,
|
|
6
|
-
*
|
|
6
|
+
* A floor, not a shell interpreter: quotes, escapes, `$'…'` ANSI-C strings, backslash-newline
|
|
7
|
+
* continuations, `;`/`&&`/`||`/`|` and `cd` / `pushd` (with their options; an operand it cannot resolve
|
|
8
|
+
* marks the directory unknown) are understood; anything else that can write a file (`cp`, a script, `python -c`) is out of scope by
|
|
7
9
|
* design. The command string is untrusted — it is only tokenized here, never executed.
|
|
8
10
|
*/
|
|
9
11
|
import * as path from 'path';
|
|
10
12
|
|
|
11
|
-
type Tok = { kind: 'word'; value: string } | { kind: 'sep' } | { kind: 'redir'; writes: boolean };
|
|
13
|
+
type Tok = { kind: 'word'; value: string } | { kind: 'sep'; pipe?: boolean } | { kind: 'redir'; writes: boolean; dup?: boolean };
|
|
12
14
|
|
|
13
15
|
/** Commands that run another command: the real one is the next word. */
|
|
14
16
|
const WRAPPERS = new Set(['sudo', 'env', 'command', 'nohup', 'time', 'exec', 'nice']);
|
|
15
17
|
const ASSIGNMENT = /^[A-Za-z_]\w*=/;
|
|
18
|
+
const ANSI_C: Record<string, string> = {
|
|
19
|
+
a: '\x07', b: '\b', e: '\x1b', E: '\x1b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\', "'": "'", '"': '"', '?': '?',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Decode a bash `$'…'` body starting at `j` (just past `$'`); returns the text and the index past the closing `'`. */
|
|
23
|
+
function ansiC(cmd: string, j: number): [string, number] {
|
|
24
|
+
let out = '';
|
|
25
|
+
while (j < cmd.length && cmd[j] !== "'") {
|
|
26
|
+
if (cmd[j] !== '\\' || j + 1 >= cmd.length) { out += cmd[j++]; continue; }
|
|
27
|
+
const e = cmd[j + 1];
|
|
28
|
+
j += 2;
|
|
29
|
+
if (e in ANSI_C) out += ANSI_C[e];
|
|
30
|
+
else if (/[0-7]/.test(e)) {
|
|
31
|
+
let m = e;
|
|
32
|
+
while (m.length < 3 && /[0-7]/.test(cmd[j] ?? '')) m += cmd[j++];
|
|
33
|
+
out += String.fromCharCode(parseInt(m, 8) & 0xff);
|
|
34
|
+
} else if (e === 'x' || e === 'u' || e === 'U') {
|
|
35
|
+
const max = e === 'x' ? 2 : e === 'u' ? 4 : 8;
|
|
36
|
+
let h = '';
|
|
37
|
+
while (h.length < max && /[0-9a-fA-F]/.test(cmd[j] ?? '')) h += cmd[j++];
|
|
38
|
+
const cp = parseInt(h, 16);
|
|
39
|
+
out += !h ? `\\${e}` : cp <= 0x10ffff ? String.fromCodePoint(cp) : '\ufffd'; // never throw: a throw fails open
|
|
40
|
+
} else if (e === 'c' && j < cmd.length) out += String.fromCharCode(cmd.charCodeAt(j++) & 31);
|
|
41
|
+
else out += `\\${e}`;
|
|
42
|
+
}
|
|
43
|
+
return [out, j + 1];
|
|
44
|
+
}
|
|
16
45
|
|
|
17
46
|
function tokenize(cmd: string): Tok[] {
|
|
18
47
|
const toks: Tok[] = [];
|
|
@@ -21,25 +50,39 @@ function tokenize(cmd: string): Tok[] {
|
|
|
21
50
|
const flush = (): void => { if (inWord) toks.push({ kind: 'word', value: word }); word = ''; inWord = false; };
|
|
22
51
|
for (let i = 0; i < cmd.length;) {
|
|
23
52
|
const ch = cmd[i];
|
|
24
|
-
if (ch === "'") {
|
|
53
|
+
if (ch === '$' && cmd[i + 1] === "'") {
|
|
54
|
+
const [text, next] = ansiC(cmd, i + 2);
|
|
55
|
+
word += text; inWord = true; i = next;
|
|
56
|
+
} else if (ch === '$' && cmd[i + 1] === '"') {
|
|
57
|
+
i++; // `$"…"` (locale translation) reads as `"…"`
|
|
58
|
+
} else if (ch === "'") {
|
|
25
59
|
const end = cmd.indexOf("'", i + 1);
|
|
26
60
|
word += cmd.slice(i + 1, end < 0 ? cmd.length : end); inWord = true; i = end < 0 ? cmd.length : end + 1;
|
|
27
61
|
} else if (ch === '"') {
|
|
28
62
|
let j = i + 1;
|
|
29
|
-
for (; j < cmd.length && cmd[j] !== '"'; j++) {
|
|
63
|
+
for (; j < cmd.length && cmd[j] !== '"'; j++) {
|
|
64
|
+
if (cmd[j] === '\\' && cmd[j + 1] === '\n') { j++; continue; } // line continuation
|
|
65
|
+
if (cmd[j] === '\\' && j + 1 < cmd.length) j++;
|
|
66
|
+
word += cmd[j];
|
|
67
|
+
}
|
|
30
68
|
inWord = true; i = j + 1;
|
|
69
|
+
} else if (ch === '\\' && cmd[i + 1] === '\n') {
|
|
70
|
+
i += 2; // line continuation: removed entirely
|
|
31
71
|
} else if (ch === '\\' && i + 1 < cmd.length) {
|
|
32
72
|
word += cmd[i + 1]; inWord = true; i += 2;
|
|
33
73
|
} else if (ch === ' ' || ch === '\t') {
|
|
34
74
|
flush(); i++;
|
|
35
75
|
} else if ('\n;|()'.includes(ch) || (ch === '&' && cmd[i + 1] !== '>')) {
|
|
36
|
-
|
|
76
|
+
const doubled = (ch === '|' || ch === '&') && cmd[i + 1] === ch;
|
|
77
|
+
flush(); toks.push({ kind: 'sep', pipe: ch === '|' && !doubled }); i += doubled ? 2 : 1;
|
|
37
78
|
} else if (ch === '>' || (ch === '&' && cmd[i + 1] === '>')) {
|
|
38
79
|
if (ch === '>' && inWord && /^\d+$/.test(word)) { word = ''; inWord = false; } else flush(); // `2>`: an fd, not a word
|
|
39
80
|
i += ch === '&' ? 2 : 1;
|
|
40
|
-
if (cmd[i] === '>'
|
|
41
|
-
|
|
42
|
-
|
|
81
|
+
if (cmd[i] === '>') i++; // `>>`, `&>>`
|
|
82
|
+
const dup = cmd[i] === '&'; // `>&x`, `>>&x`: an fd dup only when x is an fd
|
|
83
|
+
if (dup) i++;
|
|
84
|
+
if (cmd[i] === '|' || cmd[i] === '!') i++; // clobber: `>|`, `>!`, `>>!`, `&>!`, `>&|`, `>&!`
|
|
85
|
+
toks.push({ kind: 'redir', writes: true, dup });
|
|
43
86
|
} else if (ch === '<') {
|
|
44
87
|
flush(); i++;
|
|
45
88
|
while (cmd[i] === '<') i++;
|
|
@@ -58,43 +101,86 @@ const isInPlaceFlag = (a: string): boolean => a === '--in-place' || a.startsWith
|
|
|
58
101
|
export interface BashSegment {
|
|
59
102
|
/** The command name (basename, after env assignments and wrappers like `sudo`), or ''. */
|
|
60
103
|
cmd: string;
|
|
104
|
+
/** The command word as written (`./cat`, `/usr/bin/grep`, `cat`). */
|
|
105
|
+
cmdPath: string;
|
|
61
106
|
/** Its arguments, and every redirection target, as written. */
|
|
62
107
|
words: string[];
|
|
63
108
|
/** The directory it runs in — a preceding `cd` in the same line is followed. */
|
|
64
109
|
dir: string;
|
|
65
|
-
/** Absolute paths it writes via redirection, `tee` or `
|
|
110
|
+
/** Absolute paths it writes via redirection, `tee`, `sed -i` or `git … --output`. */
|
|
66
111
|
writes: string[];
|
|
112
|
+
/** False once a `cd` / `pushd` / `popd` on the line went somewhere the guard cannot resolve. */
|
|
113
|
+
dirKnown: boolean;
|
|
114
|
+
/** It writes a relative path while `dirKnown` is false — the guard cannot tell where. */
|
|
115
|
+
writesInUnknownDir: boolean;
|
|
116
|
+
/** Leading `NAME=value` assignments (a lone `X=…` is a segment too, with cmd ''). */
|
|
117
|
+
assigns: string[];
|
|
118
|
+
/** Its stdout feeds the next segment through `|`. */
|
|
119
|
+
pipesTo: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** `git diff|log|show … --output=<file>` / `--output <file>` writes that file (git takes `--outp` for `--output`). */
|
|
123
|
+
function gitOutputTargets(args: string[]): string[] {
|
|
124
|
+
const out: string[] = [];
|
|
125
|
+
args.forEach((a, k) => {
|
|
126
|
+
const m = /^--([\w-]+)(=([\s\S]*))?$/.exec(a);
|
|
127
|
+
if (!m || m[1].length < 2 || !'output'.startsWith(m[1])) return;
|
|
128
|
+
if (m[2] !== undefined) out.push(m[3]);
|
|
129
|
+
else if (args[k + 1]) out.push(args[k + 1]);
|
|
130
|
+
});
|
|
131
|
+
return out.filter((t) => t && t !== '/dev/null' && t !== '-');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The operand of `cd` / `pushd` after its options (`-L -P -e -@`, pushd `-n`, `--`), or null when there is not exactly one. */
|
|
135
|
+
function cdOperand(args: string[]): string | null {
|
|
136
|
+
let k = 0;
|
|
137
|
+
while (k < args.length && /^-[LPe@n]+$/.test(args[k])) k++;
|
|
138
|
+
if (args[k] === '--') k++;
|
|
139
|
+
return args.length - k === 1 ? args[k] : null;
|
|
67
140
|
}
|
|
68
141
|
|
|
69
142
|
export function bashSegments(command: string, cwd: string): BashSegment[] {
|
|
70
143
|
const out: BashSegment[] = [];
|
|
71
144
|
const toks = tokenize(command);
|
|
72
145
|
let dir = cwd;
|
|
146
|
+
let dirKnown = true;
|
|
73
147
|
let seg: string[] = [];
|
|
74
148
|
let redirected: string[] = [];
|
|
75
|
-
let
|
|
76
|
-
const endSegment = (): void => {
|
|
149
|
+
let rawWrites: string[] = [];
|
|
150
|
+
const endSegment = (pipesTo = false): void => {
|
|
77
151
|
let k = 0;
|
|
78
152
|
while (k < seg.length && (ASSIGNMENT.test(seg[k]) || WRAPPERS.has(seg[k]))) k++;
|
|
79
|
-
const
|
|
153
|
+
const assigns = seg.slice(0, k).filter((w) => ASSIGNMENT.test(w));
|
|
154
|
+
const cmdPath = seg[k] ?? '';
|
|
155
|
+
const cmd = path.basename(cmdPath);
|
|
80
156
|
const args = seg.slice(k + 1).filter((a) => a !== '');
|
|
81
|
-
if (cmd === 'tee')
|
|
82
|
-
else if ((cmd === 'sed' || cmd === 'gsed') && args.some(isInPlaceFlag))
|
|
83
|
-
|
|
157
|
+
if (cmd === 'tee') rawWrites.push(...args.filter((a) => !a.startsWith('-')));
|
|
158
|
+
else if ((cmd === 'sed' || cmd === 'gsed') && args.some(isInPlaceFlag)) rawWrites.push(...args.filter((a) => !a.startsWith('-')));
|
|
159
|
+
else if (cmd === 'git') rawWrites.push(...gitOutputTargets(args));
|
|
160
|
+
const writes = rawWrites.map((a) => path.resolve(dir, a));
|
|
161
|
+
const writesInUnknownDir = !dirKnown && rawWrites.some((a) => !path.isAbsolute(a));
|
|
162
|
+
if (cmd || redirected.length || assigns.length) {
|
|
163
|
+
out.push({ cmd, cmdPath, words: [...args, ...redirected], dir, dirKnown, writes, writesInUnknownDir, assigns, pipesTo });
|
|
164
|
+
}
|
|
165
|
+
if (cmd === 'cd' || cmd === 'pushd' || cmd === 'popd') {
|
|
166
|
+
const target = cmd === 'popd' ? null : cdOperand(args);
|
|
167
|
+
if (target && !/[$~`]/.test(target) && target !== '-' && !/^[+-]\d+$/.test(target)) {
|
|
168
|
+
dir = path.resolve(dir, target);
|
|
169
|
+
dirKnown = dirKnown || path.isAbsolute(target);
|
|
170
|
+
} else dirKnown = false; // `cd`, `cd ~`, `cd $D`, `cd -`, `popd`: somewhere the guard cannot resolve
|
|
84
171
|
}
|
|
85
|
-
|
|
86
|
-
if (cmd === 'cd' && args.length === 1 && !/^[-~]/.test(args[0])) dir = path.resolve(dir, args[0]);
|
|
87
|
-
seg = []; redirected = []; writes = [];
|
|
172
|
+
seg = []; redirected = []; rawWrites = [];
|
|
88
173
|
};
|
|
89
174
|
for (let t = 0; t < toks.length; t++) {
|
|
90
175
|
const tok = toks[t];
|
|
91
|
-
if (tok.kind === 'sep') { endSegment(); continue; }
|
|
176
|
+
if (tok.kind === 'sep') { endSegment(tok.pipe === true); continue; }
|
|
92
177
|
if (tok.kind === 'redir') {
|
|
93
178
|
const target = toks[t + 1];
|
|
94
179
|
if (target?.kind !== 'word') continue;
|
|
95
180
|
t++;
|
|
96
181
|
redirected.push(target.value);
|
|
97
|
-
|
|
182
|
+
const fd = tok.dup && /^(?:\d+-?|-)$/.test(target.value); // `2>&1`, `>&2`, `2>&-`
|
|
183
|
+
if (tok.writes && !fd && target.value && target.value !== '/dev/null' && target.value !== '-') rawWrites.push(target.value);
|
|
98
184
|
continue;
|
|
99
185
|
}
|
|
100
186
|
seg.push(tok.value);
|
|
@@ -103,7 +189,7 @@ export function bashSegments(command: string, cwd: string): BashSegment[] {
|
|
|
103
189
|
return out;
|
|
104
190
|
}
|
|
105
191
|
|
|
106
|
-
/** Absolute paths the command writes via redirection, `tee` or `
|
|
192
|
+
/** Absolute paths the command writes via redirection, `tee`, `sed -i` or `git --output`, resolved against `cwd`. */
|
|
107
193
|
export function bashWriteTargets(command: string, cwd: string): string[] {
|
|
108
194
|
return bashSegments(command, cwd).flatMap((s) => s.writes);
|
|
109
195
|
}
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* — the CLI still writes the last two itself; the guard only judges an agent's tool calls
|
|
11
11
|
* trace evidence — DENY an agent write (phase 08; the verdict trusts it only as far as this guard and git review):
|
|
12
12
|
* .sungen/trace-identity/** (the driver's identity answers — written by the CLI)
|
|
13
|
-
* test-results/sungen-run-record*.json
|
|
13
|
+
* test-results[-free]/sungen-run-record*.json (run id + test-file hashes — written by the Playwright reporter;
|
|
14
|
+
* test-results-free/ is the free run's own outputDir, #941)
|
|
14
15
|
* hook control — ASK before an agent write (the agent must not switch its own guard off):
|
|
15
16
|
* qa/sungen.config.yaml · .claude/settings.json · .claude/settings.local.json ·
|
|
16
17
|
* .claude/hooks/sungen-hook.cjs
|
|
@@ -25,7 +26,7 @@ const LOCK = /^qa\/(?:.+\/)?contracts\.lock\.yaml$/i;
|
|
|
25
26
|
const DECISION = /^qa\/(?:(?:screens|flows|api(?:\/flows)?)\/[^/]+\/)?decisions\/[^/]+/i;
|
|
26
27
|
const APPROVAL_STATE = /^\.sungen\/(?:journey\/[^/]+\.state\.json|viewpoint-baseline\.json)$/i;
|
|
27
28
|
/** Trace evidence (phase 08, DOS-14.6): the driver's identity answers and the run records. */
|
|
28
|
-
const TRACE_EVIDENCE = /^(?:\.sungen\/trace-identity\/.+|test-results
|
|
29
|
+
const TRACE_EVIDENCE = /^(?:\.sungen\/trace-identity\/.+|test-results(?:-free)?\/sungen-run-record(?:\.[A-Za-z0-9._-]+)?\.json)$/i;
|
|
29
30
|
const HOOK_CONTROL = ['qa/sungen.config.yaml', '.claude/settings.json', '.claude/settings.local.json', '.claude/hooks/sungen-hook.cjs'];
|
|
30
31
|
|
|
31
32
|
/** The same paths as they appear INSIDE a command string (e.g. in `bash -c "…"` or `python -c "…"`). */
|
|
@@ -62,9 +63,18 @@ export function hookControlPath(abs: string): string | null {
|
|
|
62
63
|
return HOOK_CONTROL.find((rel) => p.endsWith(`/${rel}`)) ?? null;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/** Names near a protected path — for a word the shell expands (`$Q/decisions/x`), whose real text is unknown. */
|
|
67
|
+
const NEAR_PROTECTED = /qa|\.sungen|\.claude|decision|contracts|lock\.ya?ml|run-record|trace-identity|viewpoint|journey|settings|sungen-hook|sungen\.config/i;
|
|
68
|
+
|
|
69
|
+
/** A word the shell rewrites at runtime (`$…`, a literal `\`) that sits near a protected name: the guard cannot read it. */
|
|
70
|
+
export const opaqueNearProtected = (word: string): boolean => /[$\\]/.test(word) && NEAR_PROTECTED.test(word);
|
|
71
|
+
|
|
72
|
+
/** Does this text (a command word, a heredoc, a whole command line) name a protected path as written? */
|
|
73
|
+
export const mentionsProtectedText = (text: string): boolean => MENTIONS.some((re) => re.test(text));
|
|
74
|
+
|
|
65
75
|
/** Does this command word name a protected path — as text, or once resolved against `dir`? */
|
|
66
76
|
export function mentionsProtectedPath(word: string, dir: string): boolean {
|
|
67
|
-
if (
|
|
77
|
+
if (mentionsProtectedText(word)) return true;
|
|
68
78
|
const abs = path.resolve(dir, word);
|
|
69
79
|
return approvalRecordPath(abs) !== null || traceEvidencePath(abs) !== null || hookControlPath(abs) !== null;
|
|
70
80
|
}
|