@cratis/pi 7.101.0 → 7.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +17 -0
  2. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +10 -1
  3. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +2 -0
  4. package/package/corpus/harnesses/copilot/instructions/secrets.instructions.md +4 -0
  5. package/package/corpus/hooks/README.md +11 -1
  6. package/package/corpus/hooks/scripts/cratis-check-pr.mjs +12 -1
  7. package/package/corpus/hooks/scripts/cratis-guard-secrets.sh +79 -23
  8. package/package/corpus/hooks/scripts/cratis-guard-writes.sh +145 -40
  9. package/package/corpus/rules/github-actions.md +17 -0
  10. package/package/corpus/rules/profiles.md +10 -1
  11. package/package/corpus/rules/pull-requests.md +2 -0
  12. package/package/corpus/rules/secrets.md +4 -0
  13. package/package/corpus/skills/analyzing-dotnet-performance/LICENSE +23 -0
  14. package/package/corpus/skills/analyzing-dotnet-performance/NOTICE +36 -0
  15. package/package/corpus/skills/analyzing-dotnet-performance/SKILL.md +214 -0
  16. package/package/corpus/skills/analyzing-dotnet-performance/references/async-patterns.md +122 -0
  17. package/package/corpus/skills/analyzing-dotnet-performance/references/collections-and-linq.md +220 -0
  18. package/package/corpus/skills/analyzing-dotnet-performance/references/critical-patterns.md +291 -0
  19. package/package/corpus/skills/analyzing-dotnet-performance/references/io-and-serialization.md +135 -0
  20. package/package/corpus/skills/analyzing-dotnet-performance/references/memory-and-strings.md +219 -0
  21. package/package/corpus/skills/analyzing-dotnet-performance/references/regex-patterns.md +95 -0
  22. package/package/corpus/skills/analyzing-dotnet-performance/references/structural-patterns.md +38 -0
  23. package/package/corpus/skills/binlog-generation/LICENSE +23 -0
  24. package/package/corpus/skills/binlog-generation/NOTICE +22 -0
  25. package/package/corpus/skills/binlog-generation/SKILL.md +129 -0
  26. package/package/corpus/skills/check-bin-obj-clash/LICENSE +23 -0
  27. package/package/corpus/skills/check-bin-obj-clash/NOTICE +27 -0
  28. package/package/corpus/skills/check-bin-obj-clash/SKILL.md +127 -0
  29. package/package/corpus/skills/check-bin-obj-clash/references/common-causes-and-fixes.md +206 -0
  30. package/package/corpus/skills/coverage-analysis/LICENSE +23 -0
  31. package/package/corpus/skills/coverage-analysis/NOTICE +32 -0
  32. package/package/corpus/skills/coverage-analysis/SKILL.md +198 -0
  33. package/package/corpus/skills/coverage-analysis/references/guidelines.md +68 -0
  34. package/package/corpus/skills/coverage-analysis/references/output-format.md +91 -0
  35. package/package/corpus/skills/coverage-analysis/references/report-generation.md +58 -0
  36. package/package/corpus/skills/coverage-analysis/references/setup-discovery.md +307 -0
  37. package/package/corpus/skills/coverage-analysis/scripts/Compute-CrapScores.ps1 +80 -0
  38. package/package/corpus/skills/coverage-analysis/scripts/CoverageData.ps1 +272 -0
  39. package/package/corpus/skills/coverage-analysis/scripts/Extract-MethodCoverage.ps1 +65 -0
  40. package/package/corpus/skills/cratis-ci-pipeline-performance/LICENSE +2 -0
  41. package/package/corpus/skills/cratis-ci-pipeline-performance/SKILL.md +201 -0
  42. package/package/corpus/skills/cratis-ci-pipeline-performance/references/build-once-and-shard.md +158 -0
  43. package/package/corpus/skills/cratis-ci-pipeline-performance/references/caching.md +86 -0
  44. package/package/corpus/skills/cratis-ci-pipeline-performance/references/flaky-and-hung-jobs.md +70 -0
  45. package/package/corpus/skills/cratis-ci-pipeline-performance/references/publish-pipelines.md +67 -0
  46. package/package/corpus/skills/cratis-ci-pipeline-performance/references/required-checks-and-path-filters.md +117 -0
  47. package/package/corpus/skills/cratis-ci-pipeline-performance/references/runner-sizing-and-images.md +65 -0
  48. package/package/corpus/skills/cratis-ci-pipeline-performance/references/security-and-pinning.md +73 -0
  49. package/package/corpus/skills/cratis-ci-pipeline-performance/references/sources.md +33 -0
  50. package/package/corpus/skills/cratis-ci-pipeline-performance/references/test-setup-cost.md +82 -0
  51. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/ci-timings.sh +356 -0
  52. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1003.json +79 -0
  53. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1007.json +79 -0
  54. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/jobs/1010.json +79 -0
  55. package/package/corpus/skills/cratis-ci-pipeline-performance/scripts/fixtures/runs.json +102 -0
  56. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/LICENSE +2 -0
  57. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/SKILL.md +244 -0
  58. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/docker-images.md +86 -0
  59. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/dotnet-nuget.md +87 -0
  60. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/go-jvm-python-elixir.md +83 -0
  61. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/npm-yarn.md +78 -0
  62. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/other-environments.md +60 -0
  63. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/references/worked-examples.md +97 -0
  64. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/cratis-local-packages.sh +778 -0
  65. package/package/corpus/skills/cratis-engineering-local-cross-repo-verification/scripts/npm-restore-check.cjs +109 -0
  66. package/package/corpus/skills/csharp-refactoring/LICENSE +23 -0
  67. package/package/corpus/skills/csharp-refactoring/NOTICE +22 -0
  68. package/package/corpus/skills/csharp-refactoring/SKILL.md +175 -0
  69. package/package/corpus/skills/csharp-refactoring/references/operation-catalog.md +37 -0
  70. package/package/corpus/skills/directory-build-organization/LICENSE +23 -0
  71. package/package/corpus/skills/directory-build-organization/NOTICE +21 -0
  72. package/package/corpus/skills/directory-build-organization/SKILL.md +205 -0
  73. package/package/corpus/skills/directory-build-organization/references/common-patterns.md +56 -0
  74. package/package/corpus/skills/directory-build-organization/references/multi-level-examples.md +167 -0
  75. package/package/corpus/skills/directory-build-organization/references/targetframework-props-pitfall.md +54 -0
  76. package/package/corpus/skills/dotnet-aot-compat/LICENSE +23 -0
  77. package/package/corpus/skills/dotnet-aot-compat/NOTICE +18 -0
  78. package/package/corpus/skills/dotnet-aot-compat/SKILL.md +265 -0
  79. package/package/corpus/skills/dotnet-aot-compat/references/polyfills.md +43 -0
  80. package/package/corpus/skills/dotnet-trace-collect/LICENSE +23 -0
  81. package/package/corpus/skills/dotnet-trace-collect/NOTICE +65 -0
  82. package/package/corpus/skills/dotnet-trace-collect/SKILL.md +116 -0
  83. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-monitor.md +97 -0
  84. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect-linux.md +84 -0
  85. package/package/corpus/skills/dotnet-trace-collect/references/dotnet-trace-collect.md +91 -0
  86. package/package/corpus/skills/dotnet-trace-collect/references/perfcollect.md +200 -0
  87. package/package/corpus/skills/dotnet-trace-collect/references/perfview.md +141 -0
  88. package/package/corpus/skills/dotnet-trace-collect/references/tool-selection.md +160 -0
  89. package/package/corpus/skills/dump-collect/LICENSE +23 -0
  90. package/package/corpus/skills/dump-collect/NOTICE +40 -0
  91. package/package/corpus/skills/dump-collect/SKILL.md +97 -0
  92. package/package/corpus/skills/dump-collect/references/container-dumps.md +412 -0
  93. package/package/corpus/skills/dump-collect/references/coreclr-dumps.md +108 -0
  94. package/package/corpus/skills/dump-collect/references/nativeaot-dumps.md +280 -0
  95. package/package/corpus/skills/eliminate-redundant-restores/LICENSE +23 -0
  96. package/package/corpus/skills/eliminate-redundant-restores/NOTICE +19 -0
  97. package/package/corpus/skills/eliminate-redundant-restores/SKILL.md +112 -0
  98. package/package/corpus/skills/extension-points/LICENSE +23 -0
  99. package/package/corpus/skills/extension-points/NOTICE +18 -0
  100. package/package/corpus/skills/extension-points/SKILL.md +248 -0
  101. package/package/corpus/skills/filter-syntax/LICENSE +23 -0
  102. package/package/corpus/skills/filter-syntax/NOTICE +17 -0
  103. package/package/corpus/skills/filter-syntax/SKILL.md +179 -0
  104. package/package/corpus/skills/including-generated-files/LICENSE +23 -0
  105. package/package/corpus/skills/including-generated-files/NOTICE +17 -0
  106. package/package/corpus/skills/including-generated-files/SKILL.md +139 -0
  107. package/package/corpus/skills/microbenchmarking/LICENSE +23 -0
  108. package/package/corpus/skills/microbenchmarking/NOTICE +36 -0
  109. package/package/corpus/skills/microbenchmarking/SKILL.md +131 -0
  110. package/package/corpus/skills/microbenchmarking/references/bdn-internals-and-tuning.md +143 -0
  111. package/package/corpus/skills/microbenchmarking/references/comparison-strategies.md +200 -0
  112. package/package/corpus/skills/microbenchmarking/references/diagnosers-and-exporters.md +150 -0
  113. package/package/corpus/skills/microbenchmarking/references/project-setup-and-running.md +189 -0
  114. package/package/corpus/skills/microbenchmarking/references/writing-benchmarks.md +310 -0
  115. package/package/corpus/skills/msbuild/LICENSE +23 -0
  116. package/package/corpus/skills/msbuild/NOTICE +55 -0
  117. package/package/corpus/skills/msbuild/SKILL.md +117 -0
  118. package/package/corpus/skills/msbuild/references/antipatterns/additional-antipatterns.md +291 -0
  119. package/package/corpus/skills/msbuild/references/antipatterns/incremental-build-inputs-outputs.md +57 -0
  120. package/package/corpus/skills/msbuild/references/antipatterns/private-assets.md +29 -0
  121. package/package/corpus/skills/msbuild/references/binlog-failure-analysis.md +85 -0
  122. package/package/corpus/skills/msbuild/references/binlog-generation.md +69 -0
  123. package/package/corpus/skills/msbuild/references/build-perf-baseline.md +493 -0
  124. package/package/corpus/skills/msbuild/references/build-perf-diagnostics.md +268 -0
  125. package/package/corpus/skills/msbuild/references/copy-to-output-directory.md +183 -0
  126. package/package/corpus/skills/msbuild/references/eval-performance.md +190 -0
  127. package/package/corpus/skills/msbuild/references/extension-points.md +281 -0
  128. package/package/corpus/skills/msbuild/references/incremental-build.md +365 -0
  129. package/package/corpus/skills/msbuild/references/msbuild-antipatterns.md +401 -0
  130. package/package/corpus/skills/orleans/LICENSE +21 -0
  131. package/package/corpus/skills/orleans/NOTICE +64 -0
  132. package/package/corpus/skills/orleans/SKILL.md +140 -0
  133. package/package/corpus/skills/orleans/references/anti-patterns.md +660 -0
  134. package/package/corpus/skills/orleans/references/configuration-api.md +933 -0
  135. package/package/corpus/skills/orleans/references/examples.md +66 -0
  136. package/package/corpus/skills/orleans/references/grain-api.md +784 -0
  137. package/package/corpus/skills/orleans/references/grains.md +75 -0
  138. package/package/corpus/skills/orleans/references/hosting.md +71 -0
  139. package/package/corpus/skills/orleans/references/implementation.md +591 -0
  140. package/package/corpus/skills/orleans/references/official-docs-index.md +250 -0
  141. package/package/corpus/skills/orleans/references/patterns.md +811 -0
  142. package/package/corpus/skills/orleans/references/persistence-api.md +495 -0
  143. package/package/corpus/skills/orleans/references/primitive-selection.md +35 -0
  144. package/package/corpus/skills/orleans/references/scheduling-and-services.md +295 -0
  145. package/package/corpus/skills/orleans/references/serialization-api.md +299 -0
  146. package/package/corpus/skills/orleans/references/streaming-api.md +396 -0
  147. package/package/corpus/skills/orleans/references/testing-patterns.md +210 -0
  148. package/package/corpus/skills/platform-detection/LICENSE +23 -0
  149. package/package/corpus/skills/platform-detection/NOTICE +17 -0
  150. package/package/corpus/skills/platform-detection/SKILL.md +230 -0
  151. package/package/corpus/skills/platform-detection/references/command-mode.md +27 -0
  152. package/package/corpus/skills/profiling/LICENSE +21 -0
  153. package/package/corpus/skills/profiling/NOTICE +50 -0
  154. package/package/corpus/skills/profiling/SKILL.md +144 -0
  155. package/package/corpus/skills/profiling/references/commands.md +439 -0
  156. package/package/corpus/skills/profiling/references/patterns.md +586 -0
  157. package/package/corpus/skills/profiling/references/profiling.md +190 -0
  158. package/package/corpus/skills/run-tests/LICENSE +23 -0
  159. package/package/corpus/skills/run-tests/NOTICE +19 -0
  160. package/package/corpus/skills/run-tests/SKILL.md +305 -0
  161. package/package/corpus/skills/target-authoring/LICENSE +23 -0
  162. package/package/corpus/skills/target-authoring/NOTICE +17 -0
  163. package/package/corpus/skills/target-authoring/SKILL.md +165 -0
  164. package/package/corpus/skills/vectorization/LICENSE +23 -0
  165. package/package/corpus/skills/vectorization/NOTICE +17 -0
  166. package/package/corpus/skills/vectorization/SKILL.md +189 -0
  167. package/package/profile-catalog.json +87 -1
  168. package/package.json +3 -2
@@ -45,6 +45,23 @@ paths:
45
45
  `cancel-in-progress: true`, keyed `${{ github.workflow }}-${{ github.ref }}`.
46
46
  **Never** cancel-in-progress on publish, release, or deploy workflows — a cancelled
47
47
  half-publish is worse than a queued one.
48
+ - **Publish and release workflows never cancel** (`cancel-in-progress: false`) and set
49
+ `queue: max`. A group holds one running and one pending run; a newer arrival replaces
50
+ the pending one. Merges into the default branch share a group (a push, a repository-wide
51
+ key, or `github.ref` on a merged `pull_request` `closed` event, which is the base
52
+ branch), so without `queue: max` a newer merge replaces the older pending release.
53
+ `queue: max` keeps up to 100 pending runs, processed in order, and applies only while
54
+ `cancel-in-progress` is false or unset. Keep a single pending slot only for a documented
55
+ design where each run releases everything since the last release, so a replaced run
56
+ loses nothing; say so in a comment beside the `concurrency` block.
57
+ - **A required check must not be skippable by `paths:` alone.** A workflow that never
58
+ starts never reports, and the pull request hangs. Gate jobs on a change-detection job
59
+ and require one always-run aggregator job (`if: always()`, checks every `needs.*.result`).
60
+ - **Pin third-party actions by full commit SHA** (version in a comment) and set minimal
61
+ `permissions:` on each job. Run `zizmor` locally on edited workflows: its `unpinned-uses`
62
+ and `excessive-permissions` audits check exactly this. `actionlint` checks syntax,
63
+ expressions and embedded shell, not pinning. Do not assume CI runs either tool.
64
+ - To make a workflow faster, use the `cratis-ci-pipeline-performance` skill.
48
65
 
49
66
  ## Matrices and operating systems
50
67
 
@@ -87,11 +87,20 @@ profile includes neither Screenplay nor Stage.
87
87
  | Profile ID | Description | Automatically Includes |
88
88
  |---|---|---|
89
89
  | `cratis/engineering` | Engineering conventions and workflows | `cratis/engineering/core`, `cratis/engineering/csharp`, `cratis/engineering/elixir`, `cratis/engineering/go`, `cratis/engineering/kotlin`, `cratis/engineering/react`, `cratis/engineering/typescript` |
90
- | `cratis/engineering/csharp` | C# engineering conventions and framework Orleans jobs (`cratis-orleans-jobs`, alongside `orleans.md`) | `cratis/engineering/core` |
90
+ | `cratis/engineering/csharp` | C# engineering conventions, .NET engineering skills, and framework Orleans jobs (`cratis-orleans-jobs`, alongside `orleans.md`) | `cratis/engineering/core`, `cratis/engineering/dotnet` |
91
+ | `cratis/engineering/dotnet` | Upstream .NET engineering workflows | `cratis/engineering/dotnet/performance`, `cratis/engineering/dotnet/diagnostics`, `cratis/engineering/dotnet/msbuild`, `cratis/engineering/dotnet/orleans`, `cratis/engineering/dotnet/testing`, `cratis/engineering/dotnet/refactoring` |
92
+ | `cratis/engineering/dotnet/performance` | Microbenchmarking, performance patterns, SIMD, and profiling | (no child profiles) |
93
+ | `cratis/engineering/dotnet/diagnostics` | Trace and crash-dump collection | (no child profiles) |
94
+ | `cratis/engineering/dotnet/msbuild` | MSBuild troubleshooting, build authoring, output paths, and binlogs | (no child profiles) |
95
+ | `cratis/engineering/dotnet/orleans` | Orleans primitives, hosting, persistence, and runtime behavior | (no child profiles) |
96
+ | `cratis/engineering/dotnet/testing` | Test-platform detection, runner commands, filters, and coverage | (no child profiles) |
97
+ | `cratis/engineering/dotnet/refactoring` | Behavior-preserving C# refactoring and AOT compatibility | (no child profiles) |
91
98
  | `cratis/engineering/go` | Go framework parity and immutable module releases | `cratis/engineering/core`, `cratis/language/go` |
92
99
  | `cratis/engineering/typescript` | TypeScript engineering conventions | `cratis/engineering/core` |
93
100
  | `cratis/engineering/react` | React engineering conventions | `cratis/engineering/core` |
94
101
 
102
+ The `cratis/engineering/dotnet` profiles carry upstream skills under MIT, with attribution and changes in each skill's NOTICE. Cratis rules and the `cratis-*` skills win where they differ.
103
+
95
104
  The `cratis/engineering` profile also carries [issue-assignment.md](../../../rules/issue-assignment.md): when an agent starts work on a numbered issue in a known repository and nobody is assigned, it assigns the current `gh` user.
96
105
 
97
106
  ### Language Profiles
@@ -9,6 +9,8 @@ applyTo: "**/*"
9
9
 
10
10
  **The PR description is published verbatim as the GitHub release. In repositories released by `cratis/release-action`, it also closes every issue written as `(#n)`; elsewhere, follow the repository's own release and issue-closing process.** Write the note the person upgrading should read, not a development write-up. The same contract applies to release notes typed into a manual workflow run or written straight into a release. Where it is installed, the `policy / verify` check enforces it on release-bound PRs into the default branch carrying `major`/`minor`/`patch` (it re-runs when the label is added); its release-note step warns on `no-release` and unlabelled PRs, which must still be written this way because any of them may become releasable. `release-action` does not run for a `no-release` PR, so `(#n)` closes nothing there. Editing the description re-runs the check. HTML comments are not shown on the release page and the check ignores them for headings, keywords and placeholders, and `release-action` removes them before publishing, so a `(#n)` only inside a comment closes nothing; the check fails it, so never put an issue reference in a comment. The template's own comment can stay: its `(#123)` examples are inline code, and the comment is not published.
11
11
 
12
+ **A change to a hook script or its wiring (`.cratis/ai/hooks/**`) needs a release-note line that tells consumers to run `cratis ai update`; a hook fix does not reach a repository until they do.**
13
+
12
14
  ### Allowed shape
13
15
 
14
16
  - Follow the repository's pull request template (`.github/pull_request_template.md`) within this contract.
@@ -21,6 +21,10 @@ Every line is tagged **[contract]** (binding) per the Three Levels of Authority
21
21
  - **[contract] Project the field you need and say which one:** `-o jsonpath='{.spec.containers[*].image}'`,
22
22
  `-o custom-columns=…`, `kubectl get … -o name`, status fields, `kubectl logs`, `docker inspect --format`.
23
23
  Read a `.env.example`, never the `.env`.
24
+ - **[contract] The hook reads command text only.** It cannot see a command piped into a shell
25
+ (`… | sh`, `bash -c "$(…)"`), a recursive search with no named file (`grep -r PASSWORD .`), or a
26
+ value reached through a variable, alias or script. These stay your rule to keep; output redaction
27
+ is the only backstop.
24
28
 
25
29
  ## Values
26
30
 
@@ -11,7 +11,7 @@ Three layers:
11
11
  | Pattern pass | `PostToolUse` on a write | `scripts/cratis-pattern-scan.sh` | zero tokens until a match | appends a one-line reminder to context, never blocks |
12
12
  | Hard block | `PreToolUse` on a write | `scripts/cratis-guard-writes.sh` | zero | exits **2** — the write does not happen |
13
13
  | Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-store-mutations.sh` | zero; parses only commands that mention `cratis` | exits **2** — the store-changing `cratis chronicle` command does not run |
14
- | Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-secrets.sh` | zero; parses only commands that mention a credential-bearing tool or file | exits **2** — a command that prints credentials (full `kubectl` specs and secrets, `env`/`printenv`, `docker inspect`, secret-manager reads, `.env`/kubeconfig/key files) does not run unless it projects a field; see [`secrets.md`](../rules/secrets.md) |
14
+ | Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-secrets.sh` | zero; parses only commands that mention a credential-bearing tool or file | exits **2** — a command that prints credentials (full `kubectl` specs and secrets, `env`/`printenv`, `docker inspect`, secret-manager reads, `.env`/kubeconfig/key files; also behind `/usr/bin/env`, `command`, attached options such as `-ePATTERN`, a bare `{.}` projection) does not run unless it projects a field; piped shells and recursive searches with no named file stay uncovered; see [`secrets.md`](../rules/secrets.md) |
15
15
  | Hard block | `PreToolUse` on `Bash` | `scripts/cratis-guard-pr-body.sh` | fetches shared rules only for `gh pr create/edit` | exits **2** for inline bodies, invalid notes or conflicting release labels; warns and allows when offline with no cache |
16
16
  | Cleanup of delivered work | `PostToolUse` on `Bash` (`gh pr merge`, `git pull/merge/worktree`) and `SessionStart` (Pi: `tool_result` and `session_start`) | `scripts/cratis-cleanup-worktrees.sh --hook` | zero when the command is unrelated | never blocks; removes delivered worktrees and branches, reports what it kept |
17
17
  | Quality gate (Claude) | `Stop` | `scripts/cratis-quality-gate.sh` | one build/test run, only when relevant files changed | exits **2** — the turn does not end |
@@ -252,6 +252,16 @@ execution, so it cannot validate a body that the command has yet to write, even
252
252
  already exists. Dependabot's generated description remains exempt (including GitHub CLI's
253
253
  `app/dependabot` bot identity), but its release-intent label is still checked.
254
254
 
255
+ A release that changes a hook script or its wiring must say in its release notes that consumers run
256
+ `cratis ai update`: hooks are managed files, so a fix reaches a repository only once it updates.
257
+
258
+ The direct checker exits non-zero, with remediation text, in a `github.com/Cratis/` repository whose
259
+ workflows hold no policy caller (none of the recognized callers above): without one, nothing enforces the
260
+ pull-request contract there, and a pass would claim otherwise. A repository outside Cratis still falls back to
261
+ the reviewed pin below, and the Bash guard stays a no-op where there is no caller. A contract spec in the AI corpus's own
262
+ verification runs the checker over vendored copies of the callers Cratis/Workflows publishes; with
263
+ `CRATIS_CONTRACT_LIVE=1` it compares them with Cratis/Workflows `main`.
264
+
255
265
  Run the same checker directly:
256
266
 
257
267
  ```bash
@@ -71,6 +71,13 @@ function callers(cwd = process.cwd()) {
71
71
  } catch { return []; }
72
72
  }
73
73
 
74
+ function missingCaller(repository) {
75
+ return `No Cratis/Workflows policy caller found for ${repository}, so its pull-request contract is not enforced and this check cannot vouch for it. `
76
+ + 'Add .github/workflows/verify-pull-request-policy.yml calling Cratis/Workflows/.github/workflows/verify-pull-request-policy.yml '
77
+ + '(a legacy verify-release-notes, verify-semver-label or verify-release-intent caller also counts), then run this check again. '
78
+ + 'Cratis/Workflows bootstrap-common-workflows installs the caller; if the repository is meant to have none, do not use this check there.';
79
+ }
80
+
74
81
  function repositoryName(value) {
75
82
  return value?.replace(/^(?:https?:\/\/github\.com\/|(?:ssh:\/\/)?git@github\.com[:/])/i, '')
76
83
  .replace(/^github\.com\//i, '').replace(/\/$/, '').replace(/\.git$/i, '');
@@ -216,7 +223,11 @@ function check(argv, repositoryCallers, lookupOptions) {
216
223
  // The release-intent check above still applies to bot-authored pull requests.
217
224
  if (author === 'dependabot[bot]') return 0;
218
225
  const base = values.base || pull?.baseRefName || defaultBranch;
219
- const file = rules(warn, repositoryCallers || targetCallers(repository, process.cwd(), warn));
226
+ const found = repositoryCallers || targetCallers(repository, process.cwd(), warn);
227
+ // A Cratis repository without a policy caller has no enforced pull-request contract; passing here would
228
+ // let the author believe it has one. A fetch failure is not this case: targetCallers falls back to a caller.
229
+ if (!found.length && /^Cratis\/[^/]+$/i.test(repository)) throw new Error(missingCaller(repository));
230
+ const file = rules(warn, found);
220
231
  if (!file) return 3;
221
232
  const env = { PATH: process.env.PATH, HOME: homedir(), LANG: 'C.UTF-8',
222
233
  PR_BODY: body, PR_LABELS: JSON.stringify([...labels]),
@@ -10,8 +10,9 @@
10
10
  # Exit 2 blocks the tool call and sends stderr back to the model; every other command exits 0.
11
11
  #
12
12
  # What it sees is the command text. A credential reached through a variable, an alias, a function, a script
13
- # file or a pipe into a shell is invisible to it; a projection it cannot see through (`-o jsonpath` that dumps
14
- # `{.}`) is trusted. See .cratis/ai/hooks/README.md.
13
+ # file or a pipe into a shell is invisible to it, as is a recursive search with no named file; a projection it
14
+ # cannot see through (a template or `-o jsonpath` that is not a bare `{.}`) is trusted. A redirect does not
15
+ # exempt anything: `env > f` is blocked like `env`. See .cratis/ai/hooks/README.md.
15
16
  #
16
17
  # Escape hatch for a human who authorized the read — set in the environment the agent harness was started
17
18
  # from, never inside the command (an assignment in the command is ignored):
@@ -54,13 +55,26 @@ function base(word) { word = tolower(word); sub(/.*\//, "", word); return word }
54
55
 
55
56
  # ── tokenizer: splits text into simple commands, queueing nested command text ──
56
57
  function flush_word() {
57
- if (inword) W[++nw] = word
58
- word = ""; inword = 0
58
+ if (inword) { W[++nw] = word; WP[nw] = wplain }
59
+ word = ""; inword = 0; wquoted = 0; wplain = 0
59
60
  }
60
- function end_cmd() {
61
+ # Output redirections are removed before a command is analyzed, so a redirect never shifts a CLI's subcommand
62
+ # or operands (`kubectl get > /dev/stdout secrets`). Nothing is exempted: the command itself is still analyzed.
63
+ # A word is a redirection only when its whole operator lies in the word's unquoted prefix (WP: characters before
64
+ # the first quoted or escaped one), so `1">"b` and `">x"` stay operands; input redirections (`< file`) stay too.
65
+ function end_cmd( k, m, w) {
61
66
  flush_word()
62
- if (nw > 0) analyze(W, 1, nw)
63
- split("", W); nw = 0
67
+ m = 0
68
+ for (k = 1; k <= nw; k++) {
69
+ w = W[k]
70
+ if (match(w, /^([0-9]*|&)>[>|]?/) && RLENGTH <= WP[k]) {
71
+ if (RLENGTH == length(w)) k++
72
+ continue
73
+ }
74
+ V[++m] = w
75
+ }
76
+ if (m > 0) analyze(V, 1, m)
77
+ split("", W); split("", WP); split("", V); nw = 0
64
78
  }
65
79
  function cmdsub(s, i, n, start, depth, c, j) {
66
80
  start = i; depth = 1
@@ -97,23 +111,23 @@ function read_heredocs(s, i, n, h, e, line, cmp, body) {
97
111
  return i
98
112
  }
99
113
  function tokenize(s, i, n, c, c2, j, dash, delim) {
100
- n = length(s); i = 1; nw = 0; word = ""; inword = 0; nheredoc = 0; hd_shell = 0
101
- split("", W)
114
+ n = length(s); i = 1; nw = 0; word = ""; inword = 0; wquoted = 0; wplain = 0; nheredoc = 0; hd_shell = 0
115
+ split("", W); split("", WP)
102
116
  while (i <= n) {
103
117
  c = substr(s, i, 1)
104
118
  if (c == "\\") {
105
119
  c2 = substr(s, i + 1, 1)
106
120
  if (c2 == "\n") { i += 2; continue }
107
- word = word c2; inword = 1; i += 2; continue
121
+ wquoted = 1; word = word c2; inword = 1; i += 2; continue
108
122
  }
109
123
  if (c == "\047") {
110
- inword = 1
124
+ wquoted = 1; inword = 1
111
125
  j = index(substr(s, i + 1), "\047")
112
126
  if (j == 0) { word = word substr(s, i + 1); i = n + 1; continue }
113
127
  word = word substr(s, i + 1, j - 1); i += j + 1; continue
114
128
  }
115
129
  if (c == "\"") {
116
- inword = 1; i++
130
+ wquoted = 1; inword = 1; i++
117
131
  while (i <= n) {
118
132
  c = substr(s, i, 1)
119
133
  if (c == "\\") { word = word substr(s, i + 1, 1); i += 2; continue }
@@ -128,9 +142,9 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
128
142
  }
129
143
  continue
130
144
  }
131
- if (c == "$" && substr(s, i + 1, 1) == "(") { inword = 1; i = cmdsub(s, i + 2, n); word = word "x"; continue }
145
+ if (c == "$" && substr(s, i + 1, 1) == "(") { wquoted = 1; inword = 1; i = cmdsub(s, i + 2, n); word = word "x"; continue }
132
146
  if (c == "`") {
133
- inword = 1
147
+ wquoted = 1; inword = 1
134
148
  j = index(substr(s, i + 1), "`")
135
149
  if (j == 0) { i = n + 1; continue }
136
150
  enqueue(substr(s, i + 1, j - 1)); i += j + 1; word = word "x"; continue
@@ -146,6 +160,9 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
146
160
  if (nheredoc > 0) i = read_heredocs(s, i, n)
147
161
  continue
148
162
  }
163
+ # `>&`, `>|` and `&>` are parts of a redirection operator, not command separators.
164
+ if ((c == "&" || c == "|") && inword && !wquoted && word ~ />$/) { word = word c; wplain++; i++; continue }
165
+ if (c == "&" && substr(s, i + 1, 1) == ">") { flush_word(); word = c; inword = 1; wplain = 1; i++; continue }
149
166
  if (c == ";" || c == "&" || c == "|" || c == "(" || c == ")") { end_cmd(); i++; continue }
150
167
  if (substr(s, i, 2) == "<<" && substr(s, i, 3) != "<<<") {
151
168
  flush_word(); i += 2; dash = 0
@@ -161,6 +178,7 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
161
178
  nheredoc++; HD[nheredoc] = delim; HDDASH[nheredoc] = dash
162
179
  continue
163
180
  }
181
+ if (!wquoted) wplain++
164
182
  word = word c; inword = 1; i++
165
183
  }
166
184
  end_cmd()
@@ -169,7 +187,7 @@ function tokenize(s, i, n, c, c2, j, dash, delim) {
169
187
  # ── option parsing shared by the CLIs: positionals P/PI, output format, -f/--format value, --raw ──
170
188
  function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
171
189
  split("", P); split("", PI)
172
- np = 0; outfmt = ""; fmtval = ""; dd = 0; raw = 0
190
+ np = 0; outfmt = ""; fmtval = ""; tmpl = ""; dd = 0; raw = 0
173
191
  for (i = s; i <= n; i++) {
174
192
  w = A[i]
175
193
  if (w == "--") { dd = i; break }
@@ -178,6 +196,7 @@ function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
178
196
  if (eq) { key = substr(w, 1, eq - 1); val = substr(w, eq + 1) }
179
197
  if (key == "--output") { if (!eq) { i++; val = A[i] } outfmt = tolower(val) }
180
198
  else if (key == "--format") { if (!eq) { i++; val = A[i] } fmtval = val }
199
+ else if (key == "--template") { if (!eq) { i++; val = A[i] } tmpl = val }
181
200
  else if (key == "--raw") raw = 1
182
201
  else if (!eq && (key in LV)) i++
183
202
  continue
@@ -196,6 +215,11 @@ function kparse(A, s, n, shortvals, i, w, eq, key, val, f, rest) {
196
215
  np++; P[np] = w; PI[np] = i
197
216
  }
198
217
  }
218
+ # `-o jsonpath={.}` and `-o go-template={{.}}` print the whole object, exactly like -o json.
219
+ function proj_leaks(t) {
220
+ t = tolower(t); gsub(/[ \t]/, "", t)
221
+ return t ~ /[{](\.|@|\.items|\.items\[\*\])[}]/
222
+ }
199
223
  function skipopts(A, s, n, vals) {
200
224
  while (s <= n && substr(A[s], 1, 1) == "-") {
201
225
  if (length(A[s]) == 2 && index(vals, substr(A[s], 2, 1))) s += 2
@@ -257,6 +281,8 @@ function kube(A, s, n, verb, m, parts, k, r, rlist) {
257
281
  report("kubectl describe " r " prints the full container spec, including inline credentials", "kubectl get " r " -o jsonpath=" SQ "{.items[*].spec.containers[*].image}" SQ ", -o custom-columns=NAME:.metadata.name,STATUS:.status.phase, -o name, or kubectl get events / kubectl logs; say which field you need")
258
282
  else if (outfmt == "json" || outfmt == "yaml")
259
283
  report("kubectl get " r " -o " outfmt " prints the full container spec, including inline credentials", "-o jsonpath=" SQ "{.items[*].spec.containers[*].image}" SQ ", -o custom-columns=NAME:.metadata.name,STATUS:.status.phase, -o name, -o wide, or the status fields you need")
284
+ else if ((outfmt ~ /^(jsonpath|go-template|jsonpath-as-json)=/ || tmpl != "") && proj_leaks(tmpl outfmt))
285
+ report("kubectl get " r " with a projection that selects the whole object prints the full container spec, including inline credentials", "-o jsonpath=" SQ "{.items[*].spec.containers[*].image}" SQ ", -o custom-columns=NAME:.metadata.name,STATUS:.status.phase, -o name, -o wide, or the status fields you need")
260
286
  }
261
287
  }
262
288
  }
@@ -323,19 +349,36 @@ function gcloud(A, s, n, k) {
323
349
  if (P[1] == "secrets" && P[2] == "versions" && P[3] == "access") report("gcloud secrets versions access prints the secret", "gcloud secrets versions list, or gcloud secrets describe (metadata only)")
324
350
  if (P[1] == "auth") for (k = 2; k <= np; k++) if (P[k] ~ /^print-(access|identity)-token$/) report("gcloud auth " P[k] " prints a token", "gcloud auth list")
325
351
  }
326
- function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued, valued2) {
327
- b = base(A[s]); mode = ""; valued = " "; valued2 = " "
328
- if (b ~ /^(grep|egrep|fgrep|rg|ag)$/) { mode = "pattern"; valued = " -m -A -B -C -g -t -T -d -D --glob --type --type-not --max-count --include --exclude --exclude-dir --context --after-context --before-context --max-depth --threads " }
329
- else if (b ~ /^(sed|awk|gawk|jq|yq)$/) { mode = "script"; valued = " -v -F --indent "; valued2 = " --arg --argjson --slurpfile --rawfile " }
352
+ # Scans one attached or clustered short option (-ePATTERN, -nePATTERN, -m5, -F:) of a pattern or script tool:
353
+ # sc_pat is set when it supplies the pattern or script, sc_skip when its value is the next word.
354
+ function scan_short(w, shortvals, sed_like, i, c, last) {
355
+ sc_pat = 0; sc_skip = 0; last = length(w)
356
+ for (i = 2; i <= last; i++) {
357
+ c = substr(w, i, 1)
358
+ if (c == "e" || c == "f") { sc_pat = 1; sc_skip = (i == last); return }
359
+ if (index(shortvals, c)) { sc_skip = (i == last); return }
360
+ if (sed_like && c == "i") return
361
+ }
362
+ }
363
+ function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued, valued2, shortvals) {
364
+ b = base(A[s]); mode = ""; valued = " "; valued2 = " "; shortvals = ""
365
+ if (b ~ /^(grep|egrep|fgrep|rg|ag)$/) { mode = "pattern"; shortvals = (b ~ /^(grep|egrep|fgrep)$/) ? "mABCdD" : (b == "rg") ? "mABCdjEMrgtT" : "ABCGgmpW"; valued = " --glob --type --type-not --max-count --include --exclude --exclude-dir --context --after-context --before-context --max-depth --threads " }
366
+ else if (b ~ /^(sed|awk|gawk|jq|yq)$/) { mode = "script"; shortvals = "vF"; valued = " --indent "; valued2 = " --arg --argjson --slurpfile --rawfile " }
330
367
  # The first operand is the pattern or script unless -e/-f/--regexp/--file/--expression supplies it; only the rest are files.
331
368
  havepat = 0
332
- if (mode != "") for (k = s + 1; k <= n; k++) if (A[k] ~ /^-[a-zA-Z]*[ef]$/ || A[k] ~ /^--(regexp|file|expression|from-file)(=|$)/) havepat = 1
369
+ if (mode != "") for (k = s + 1; k <= n; k++) {
370
+ w = A[k]
371
+ if (w == "--") break
372
+ if (w ~ /^--(regexp|file|expression|from-file)(=|$)/) havepat = 1
373
+ else if (w ~ /^-[a-zA-Z]/) { scan_short(w, shortvals, b == "sed"); if (sc_pat) havepat = 1 }
374
+ }
333
375
  need = (mode != "" && !havepat); afterdd = 0
334
376
  for (k = s + 1; k <= n; k++) {
335
377
  w = A[k]
336
378
  if (w == "--" && !afterdd) { afterdd = 1; continue }
337
379
  if (!afterdd && substr(w, 1, 1) == "-" && length(w) > 1) {
338
- if (mode != "" && (w ~ /^-[a-zA-Z]*[ef]$/ || w ~ /^--(regexp|file|expression|from-file)$/ || index(valued, " " w " "))) k++
380
+ if (mode != "" && w ~ /^-[a-zA-Z]/) { scan_short(w, shortvals, b == "sed"); if (sc_skip) k++ }
381
+ else if (mode != "" && (w ~ /^--(regexp|file|expression|from-file)$/ || index(valued, " " w " "))) k++
339
382
  else if (index(valued2, " " w " ")) k += 2
340
383
  continue
341
384
  }
@@ -345,13 +388,22 @@ function reader(A, s, n, b, k, w, mode, havepat, need, afterdd, kind, valued,
345
388
  }
346
389
  }
347
390
 
391
+ function drop_redirects(A, s, n, k, m, w) {
392
+ m = s
393
+ for (k = s + 1; k <= n; k++) {
394
+ w = A[k]
395
+ if (w ~ /^[0-9]*[<>]/) { if (w ~ /^[0-9]*[<>]+&?-?$/) k++; continue }
396
+ A[++m] = w
397
+ }
398
+ return m
399
+ }
348
400
  function analyze(A, s, n, w, b, k, names, p) {
349
401
  while (s <= n) {
350
402
  w = A[s]; b = base(w)
351
403
  if (is_assign(w)) { s++; continue }
352
404
  if (w ~ /^[0-9]*[<>]/) { s += (w ~ /^[0-9]*[<>]+&?-?$/) ? 2 : 1; continue }
353
405
  if (b in KW) { s++; continue }
354
- if (b == "command" || b == "builtin") { s++; if (s <= n && (A[s] == "-v" || A[s] == "-V")) return; continue }
406
+ if (b == "command" || b == "builtin") { s++; if (s <= n && (A[s] == "-v" || A[s] == "-V")) return; if (s <= n && A[s] == "-p") s++; continue }
355
407
  if (b == "sudo" || b == "doas") { s = skipopts(A, s + 1, n, "ugChprtUD"); continue }
356
408
  if (b == "xargs") { s = skipopts(A, s + 1, n, "IniPLdsEa"); continue }
357
409
  if (b == "timeout") { s = skipopts(A, s + 1, n, "ks") + 1; continue }
@@ -363,6 +415,7 @@ function analyze(A, s, n, w, b, k, names, p) {
363
415
  while (s <= n) {
364
416
  w = A[s]
365
417
  if (is_assign(w)) s++
418
+ else if (w ~ /^[0-9]*[<>]/) s += (w ~ /^[0-9]*[<>]+&?-?$/) ? 2 : 1
366
419
  else if (w == "-u" || w == "-C" || w == "-S" || w == "--unset" || w == "--chdir" || w == "--split-string") s += 2
367
420
  else if (substr(w, 1, 1) == "-") s++
368
421
  else break
@@ -393,6 +446,8 @@ function analyze(A, s, n, w, b, k, names, p) {
393
446
  for (k = s + 1; k <= n; k++) if (A[k] ~ /^--show-secrets/) report("pulumi --show-secrets prints decrypted secrets", "pulumi config / pulumi stack output without --show-secrets (secrets stay masked)")
394
447
  return
395
448
  }
449
+ # A redirect is not a variable name: `printenv > f` and `export > f` still dump the whole environment.
450
+ if (b == "printenv" || b == "export" || b == "declare" || b == "typeset") n = drop_redirects(A, s, n)
396
451
  if (b == "printenv") {
397
452
  names = 0
398
453
  for (k = s + 1; k <= n; k++) {
@@ -426,7 +481,8 @@ BEGIN {
426
481
  for (i in L) KW[L[i]] = 1
427
482
  split("pods po pod deployments deployment deploy statefulsets statefulset sts daemonsets daemonset ds jobs job cronjobs cronjob cj configmaps configmap cm replicasets replicaset rs replicationcontrollers rc all", L, " ")
428
483
  for (i in L) SENS[L[i]] = 1
429
- enqueue(ENVIRON["GS_COMMAND"])
484
+ CMD = ENVIRON["GS_COMMAND"]
485
+ enqueue(CMD)
430
486
  while (qi < queued) { qi++; tokenize(queue[qi]) }
431
487
  }
432
488
  AWK