pm-ops 2026.8.29 → 2026.9.2

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/CHANGELOG.md CHANGED
@@ -4,14 +4,63 @@
4
4
 
5
5
  ### Fixed
6
6
 
7
- - Harden literal scalar assignment parsing ([ops-mjnd](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-mjnd.toon))
8
- - Escaped shell metacharacter in scalar value bypasses the attestation gate ([ops-5mv7](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-5mv7.toon))
9
- - Shell scalar assignments were read from raw text, so a comment could make an unattested publish pass the attestation gate ([ops-q6wi](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-q6wi.toon))
7
+ - Repaired the ops-5jp6 history drift that had blocked two stacked release pull requests ([ops-yxej](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-yxej.toon))
8
+
9
+ ### Security
10
+
11
+ - Four more fail modes in the canonical gate: an arm pattern read as an opener, an escaped run key read as data, a sequence marker missing from the indent, and composite actions unscanned ([ops-cxrx](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-cxrx.toon))
12
+ - Attributing a case arm label by presence rather than position refused an attested publish, and a quoted key hid a phantom one ([ops-setc](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-setc.toon))
13
+ - An arm that opens a nested case skipped the sibling reset, so a publish could borrow a flag from a mutually exclusive arm ([ops-rsbn](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-rsbn.toon))
14
+
15
+ ## 2026.9.2 - 2026-09-02
16
+
17
+ ### Added
18
+
19
+ - Publish the canonical attestation scanner as pm-ops exports ([ops-4b34](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/features/ops-4b34.toon))
20
+ - A release-completeness gate that requires every release tag to have a matching npm version and GitHub Release ([ops-i8yp](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/features/ops-i8yp.toon))
21
+
22
+ ### Fixed
23
+
24
+ - A case opener sharing its first arm cleared the outer scope and refused a genuinely attested publish ([ops-cout](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-cout.toon))
25
+ - Three shell-scope and quoting fail-opens attest unproven npm publishes ([ops-p9lb](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-p9lb.toon))
26
+ - A malformed npm reply crashed the release audit, and the docstring already promised it would not ([ops-f5m7](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-f5m7.toon))
27
+ - The revert-proof test read its vulnerable fixture from origin/main, so merging it would have turned it red ([ops-9ktn](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-9ktn.toon))
28
+ - The paginated release listing was posted rather than fetched, because gh api switches to POST as soon as a parameter is present ([ops-eugo](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-eugo.toon))
29
+ - P2: GitHub Release history truncated by gh release list -L 500 (a capped read misses older releases) ([ops-ud1m](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-ud1m.toon))
30
+ - P2: exit 0 inside the retry loop instead of returning status to loop-level termination ([ops-emii](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-emii.toon))
31
+ - P1: Completeness gate makes a partial release permanent (release:check blocks repair) ([ops-94me](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-94me.toon))
32
+ - The bun install verification retried a cached negative manifest, so published versions were never observed ([ops-i3ie](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-i3ie.toon))
33
+
34
+ ### Security
35
+
36
+ - Skipping the sibling-arm reset for any depth-increasing segment let a later arm borrow the previous arm's binding ([ops-narm](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-narm.toon))
37
+ - A tracked executable whose name merely ends in package.json has its publish hidden from the audit ([ops-fnbd](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-fnbd.toon))
38
+ - A case opener sharing a segment with its first arm leaks that arm's binding past esac ([ops-csop](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-csop.toon))
39
+ - Branch-local and compound-line publish bindings let an unattested npm publish pass the attestation gate ([ops-brsc](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-brsc.toon))
40
+ - Security: repository-host check matches too loosely (notgithub.com accepted as GitHub) ([ops-042o](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-042o.toon))
41
+
42
+ ### Other
43
+
44
+ - Converge the pinned pm-cli on 2026.8.31 and repair history hashes ([ops-uulf](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/chores/ops-uulf.toon))
45
+
46
+ ## 2026.8.31 - 2026-08-31
47
+
48
+ ### Fixed
49
+
50
+ - Finish shell-accurate heredoc and unset handling in publish attestation ([ops-62d1](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-62d1.toon))
51
+ - The release changelog remained Unreleased after the release tag was created ([ops-3ete](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-3ete.toon))
52
+
53
+ ### Security
54
+
55
+ - A heredoc body and a discarded binding both let an unattested publish pass the attestation gate ([ops-5jp6](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-5jp6.toon))
10
56
 
11
57
  ## 2026.8.29 - 2026-08-29
12
58
 
13
59
  ### Fixed
14
60
 
61
+ - Harden literal scalar assignment parsing ([ops-mjnd](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-mjnd.toon))
62
+ - Escaped shell metacharacter in scalar value bypasses the attestation gate ([ops-5mv7](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-5mv7.toon))
63
+ - Shell scalar assignments were read from raw text, so a comment could make an unattested publish pass the attestation gate ([ops-q6wi](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-q6wi.toon))
15
64
  - A multi-line fixture joined with a literal backslash-n made the array-flag case pass without exercising it ([ops-xmb6](https://github.com/unbraind/pm-ops/blob/main/.agents/pm/issues/ops-xmb6.toon))
16
65
 
17
66
  ## 2026.8.28 - 2026-08-28
@@ -0,0 +1,177 @@
1
+ import { type ShellCommand, type SourceFile, type VerifierResult } from "./shell-scan.ts";
2
+ /** The flag that attaches a build attestation to the published tarball. */
3
+ export declare const ATTESTATION_FLAG = "--provenance";
4
+ /** Whether the auditor recognized any publish command at all. */
5
+ export type PublishRecognition = {
6
+ readonly kind: "none";
7
+ } | {
8
+ readonly kind: "recognized";
9
+ readonly count: number;
10
+ };
11
+ /** Publish-attestation outcome with non-vacuity preserved as structured data. */
12
+ export interface PublishAttestationResult extends VerifierResult {
13
+ /** Recognition cannot be inferred from an empty failure list. */
14
+ readonly recognition: PublishRecognition;
15
+ }
16
+ /** One publish invocation found in a tracked file. */
17
+ export interface PublishInvocation {
18
+ /** File the invocation was found in. */
19
+ file: string;
20
+ /** The program the invocation runs, reduced to its basename. */
21
+ program: string;
22
+ /** The invocation's tokens, quoting resolved. */
23
+ command: ShellCommand;
24
+ }
25
+ /** Publishers other than npm, which this repository has no attested path for. */
26
+ export declare const FOREIGN_PUBLISHERS: Set<string>;
27
+ /**
28
+ * Join manifest script bodies for legacy text consumers without continuations.
29
+ *
30
+ * The auditor itself analyzes each returned body in a fresh scope; this text
31
+ * form remains for callers that need the earlier newline-joined contract.
32
+ *
33
+ * @param text - The manifest's contents.
34
+ * @returns One sanitized line per script body, newline joined.
35
+ */
36
+ export declare function manifestCommandLines(text: string): string;
37
+ /**
38
+ * Decide whether one command is a direct `npm publish`.
39
+ *
40
+ * `publish` does not have to follow `npm` immediately: npm accepts its
41
+ * configuration flags anywhere on the line, so `npm --access public publish` is
42
+ * a real publish that an adjacency test discards silently, leaving an attested
43
+ * sibling elsewhere in the file to carry the audit to a pass.
44
+ *
45
+ * Reading the first non-flag word as the subcommand does not work either,
46
+ * because npm has flags that take a separate value (`--access public`) and
47
+ * flags that do not (`--ignore-scripts`), and telling them apart needs npm's
48
+ * own option table. So the word is looked for anywhere in the arguments, and
49
+ * only a preceding runner subcommand rules it out -- `npm run publish` runs a
50
+ * package script whose body is scanned from the manifest, and requiring the
51
+ * flag on the runner would report a defect that is not there.
52
+ *
53
+ * The residual imprecision is `npm --tag publish ...`, a dist-tag named after
54
+ * the subcommand, which this reads as a publish. That direction is deliberate:
55
+ * a false positive is a report line to argue with, a false negative is an
56
+ * unattested artifact on the registry.
57
+ *
58
+ * The program is checked in command position by the caller, so `echo npm
59
+ * publish` and `notnpm publish` never reach here.
60
+ *
61
+ * @param command - One simple command's tokens.
62
+ * @returns True when the command publishes.
63
+ */
64
+ export declare function isPublishCommand(command: ShellCommand): boolean;
65
+ /**
66
+ * Decide whether one publish command actually enables the attestation.
67
+ *
68
+ * A substring test is not enough. `--provenance=false`, `--provenance false` and
69
+ * `--no-provenance` all contain the flag's spelling and all turn the
70
+ * attestation off, so a containment check accepts precisely the regression this
71
+ * gate exists to catch -- while reporting the file as attested. `--provenance-file`
72
+ * is a different flag entirely and must not be read as this one.
73
+ *
74
+ * Tokens are judged in order and the last one wins, which is how npm resolves a
75
+ * flag given more than once: `--provenance --no-provenance` publishes without an
76
+ * attestation, so this must answer false for it.
77
+ *
78
+ * Quoting is irrelevant to the shell and so is irrelevant here: `npm publish
79
+ * "--provenance"` is attested, and the scan this replaces read it as bare.
80
+ *
81
+ * @param command - One simple command's tokens.
82
+ * @returns True when the command publishes with an attestation.
83
+ */
84
+ export declare function attestationEnabled(command: ShellCommand): boolean;
85
+ /**
86
+ * Find every publish invocation in one source, isolating package scripts.
87
+ *
88
+ * npm launches every manifest script in a fresh shell, so scalar evidence from
89
+ * one body can never attest a publish in another body. A workflow file's `run:`
90
+ * blocks are dedented first — before {@link joinContinuations} and heredoc
91
+ * detection — because that is what happens to them before bash sees the text:
92
+ * YAML strips the block indentation when it delivers the value. The order
93
+ * matters: `dedentRunBlocks` reads the raw YAML line structure to find block
94
+ * headers and strip their indentation, and `joinContinuations` would merge a
95
+ * `run:` header with the next line when a backslash continuation sits between
96
+ * them, destroying the header the dedent needs to see. Heredoc detection runs
97
+ * after both, because a heredoc terminator must match at the start of the line
98
+ * bash receives — which is the dedented, continuation-joined text. The one
99
+ * scanner rule that is whitespace-sensitive is the heredoc terminator, so
100
+ * dedenting before that rule runs is what makes a `run: |2` block's heredoc
101
+ * terminator match instead of swallowing the rest of the file.
102
+ *
103
+ * @param source - The file's path and contents.
104
+ * @returns The publish invocations found, in file and script order.
105
+ */
106
+ export declare function publishInvocationsIn(source: SourceFile): PublishInvocation[];
107
+ /**
108
+ * Render an invocation back to a readable command for a report line.
109
+ *
110
+ * @param command - The invocation's tokens.
111
+ * @returns The command as a single space-separated string.
112
+ */
113
+ export declare function renderCommand(command: ShellCommand): string;
114
+ /**
115
+ * Audit every publish invocation across the given files.
116
+ *
117
+ * An absent invocation is a failure rather than a pass: a scan that finds
118
+ * nothing has either been pointed at the wrong files or outlived the workflow
119
+ * it guards, and both look identical to a clean result unless said out loud.
120
+ *
121
+ * A publisher other than npm fails outright rather than being checked for a
122
+ * flag. This repository's attested path is npm's `--provenance`; no equivalent
123
+ * is configured for yarn, pnpm or bun, so such an invocation is an unattested
124
+ * publish path regardless of the flags it carries, and guessing at another
125
+ * tool's spelling would be a gate that only looked strict.
126
+ *
127
+ * @param sources - The tracked files to scan.
128
+ * @returns Failures and per-file notes.
129
+ */
130
+ export declare function auditPublishAttestation(sources: SourceFile[]): PublishAttestationResult;
131
+ /**
132
+ * Decide whether a tracked path can run a command.
133
+ *
134
+ * The previous enumeration named two paths -- `.github/workflows` and
135
+ * `package.json` -- which meant a publish added to any tracked script was never
136
+ * audited, and because the workflow's own attested publish satisfied the
137
+ * non-vacuity check the gate still reported that every invocation was attested.
138
+ * Auditing every shape that can execute closes that, and a shebang is honoured
139
+ * so an extensionless tracked script is not a blind spot either.
140
+ *
141
+ * Build output is excluded. `dist/` is generated from sources this scan already
142
+ * reads, it is regenerated and compared byte-for-byte on the release path, and
143
+ * including it would audit a bundled copy of a command rather than the command.
144
+ *
145
+ * @param path - Repository-relative path.
146
+ * @param firstLine - The file's first line, for shebang detection.
147
+ * @returns True when the file should be scanned.
148
+ */
149
+ export declare function isExecutableSource(path: string, firstLine: string): boolean;
150
+ /**
151
+ * List the tracked files that can run a publish.
152
+ *
153
+ * Git is asked rather than the filesystem walked, so an untracked scratch copy
154
+ * of a workflow cannot satisfy or fail the gate. `-z` is used because a tracked
155
+ * path may legally contain a newline, and splitting such a listing on newlines
156
+ * invents two paths that do not exist and drops the one that does.
157
+ *
158
+ * @param root - Repository root.
159
+ * @returns Repository-relative paths of every tracked file that can execute.
160
+ */
161
+ export declare function trackedPublishSources(root: string): string[];
162
+ /**
163
+ * Read the tracked sources and audit them.
164
+ *
165
+ * @param root - Repository root to verify.
166
+ * @returns Failures and notes for the whole repository.
167
+ */
168
+ export declare function verify(root: string): PublishAttestationResult;
169
+ /**
170
+ * Print a result and set a failing exit code when it failed.
171
+ *
172
+ * @param result - The audit outcome.
173
+ * @param write - Sink for the report lines.
174
+ * @param exit - Called with the process exit code when there were failures.
175
+ */
176
+ export declare function report(result: VerifierResult, write: (line: string) => void, exit: (code: number) => void): void;
177
+ //# sourceMappingURL=attestation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attestation.d.ts","sourceRoot":"","sources":["../attestation.ts"],"names":[],"mappings":"AAuBA,OAAO,EAeL,KAAK,YAAY,EACjB,KAAK,UAAU,EAIf,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D,iFAAiF;AACjF,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC9D,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;CAC1C;AAED,sDAAsD;AACtD,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,OAAO,EAAE,YAAY,CAAC;CACvB;AAED,iFAAiF;AACjF,eAAO,MAAM,kBAAkB,aAAmC,CAAC;AA2EnE;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzD;AAuBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAe/D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAwBjE;AAsKD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,iBAAiB,EAAE,CAO5E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAE3D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,wBAAwB,CAqCvF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAI3E;AAiCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAU5D;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,wBAAwB,CAM7D;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CACpB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,EAC7B,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAC3B,IAAI,CASN"}