@webpieces/pr-gate 0.4.564 → 0.4.566

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 (57) hide show
  1. package/bin/wp-finish-push-dev.js +27 -0
  2. package/bin/wp-push-dev.js +27 -0
  3. package/package.json +5 -3
  4. package/src/dashboard/checklist-comment-row.d.ts +42 -0
  5. package/src/dashboard/checklist-comment-row.js +59 -0
  6. package/src/dashboard/checklist-comment-row.js.map +1 -0
  7. package/src/dashboard/dashboard.d.ts +19 -29
  8. package/src/dashboard/dashboard.js +57 -53
  9. package/src/dashboard/dashboard.js.map +1 -1
  10. package/src/scripts/commands/finish-push-dev-command.d.ts +28 -0
  11. package/src/scripts/commands/finish-push-dev-command.js +62 -0
  12. package/src/scripts/commands/finish-push-dev-command.js.map +1 -0
  13. package/src/scripts/commands/finish-upsert-pr-command.d.ts +9 -0
  14. package/src/scripts/commands/finish-upsert-pr-command.js +24 -4
  15. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
  16. package/src/scripts/commands/push-dev-command.d.ts +89 -0
  17. package/src/scripts/commands/push-dev-command.js +249 -0
  18. package/src/scripts/commands/push-dev-command.js.map +1 -0
  19. package/src/scripts/commands/review-upsert-pr-command.d.ts +12 -1
  20. package/src/scripts/commands/review-upsert-pr-command.js +20 -4
  21. package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
  22. package/src/scripts/pr-gate-app.d.ts +24 -3
  23. package/src/scripts/pr-gate-app.js +48 -4
  24. package/src/scripts/pr-gate-app.js.map +1 -1
  25. package/src/scripts/workflow/checklist-detector.js +1 -1
  26. package/src/scripts/workflow/checklist-detector.js.map +1 -1
  27. package/src/scripts/workflow/checklist-scanner.d.ts +21 -1
  28. package/src/scripts/workflow/checklist-scanner.js +31 -3
  29. package/src/scripts/workflow/checklist-scanner.js.map +1 -1
  30. package/src/scripts/workflow/dev-deploy-refs.d.ts +66 -0
  31. package/src/scripts/workflow/dev-deploy-refs.js +143 -0
  32. package/src/scripts/workflow/dev-deploy-refs.js.map +1 -0
  33. package/src/scripts/workflow/dev-resolve-runner.d.ts +72 -0
  34. package/src/scripts/workflow/dev-resolve-runner.js +180 -0
  35. package/src/scripts/workflow/dev-resolve-runner.js.map +1 -0
  36. package/src/scripts/workflow/git-exec.d.ts +20 -1
  37. package/src/scripts/workflow/git-exec.js +30 -3
  38. package/src/scripts/workflow/git-exec.js.map +1 -1
  39. package/src/scripts/workflow/push-dev-state.d.ts +52 -0
  40. package/src/scripts/workflow/push-dev-state.js +147 -0
  41. package/src/scripts/workflow/push-dev-state.js.map +1 -0
  42. package/src/scripts/workflow/review-report.d.ts +62 -1
  43. package/src/scripts/workflow/review-report.js +165 -34
  44. package/src/scripts/workflow/review-report.js.map +1 -1
  45. package/src/scripts/workflow/reviewer-briefing-builder.js +1 -0
  46. package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -1
  47. package/src/scripts/workflow/reviewer-verdict-gate.d.ts +10 -0
  48. package/src/scripts/workflow/reviewer-verdict-gate.js +17 -1
  49. package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
  50. package/src/scripts/wp-finish-push-dev.d.ts +2 -0
  51. package/src/scripts/wp-finish-push-dev.js +20 -0
  52. package/src/scripts/wp-finish-push-dev.js.map +1 -0
  53. package/src/scripts/wp-push-dev.d.ts +2 -0
  54. package/src/scripts/wp-push-dev.js +44 -0
  55. package/src/scripts/wp-push-dev.js.map +1 -0
  56. package/src/scripts/wp-review-upsert-pr.js +9 -2
  57. package/src/scripts/wp-review-upsert-pr.js.map +1 -1
@@ -1,5 +1,16 @@
1
1
  import { RepoRootFinder } from '@webpieces/rules-config';
2
2
  import { GitStatusEntry, GitStatusParser } from './git-status';
3
+ /**
4
+ * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks
5
+ * `outcome.ok` rather than re-deriving "did it work" from a status number at every site.
6
+ */
7
+ export declare class GitOutcome {
8
+ status: number;
9
+ stdout: string;
10
+ stderr: string;
11
+ constructor(status: number, stdout: string, stderr: string);
12
+ get ok(): boolean;
13
+ }
3
14
  /** Shared git precondition checks + push logic for the PR/merge workflow. */
4
15
  export declare class GitExec {
5
16
  private readonly repoRootFinder;
@@ -48,6 +59,14 @@ export declare class GitExec {
48
59
  * 3-point squash rewrites history) and -u for a brand-new branch.
49
60
  */
50
61
  ensurePushed(currentBranch: string): void;
51
- private gitQuery;
62
+ /**
63
+ * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the
64
+ * questions whose "no" is an answer rather than an error (does this remote ref exist? did this merge
65
+ * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on
66
+ * failure would operate on wrong state.
67
+ */
68
+ tryGit(args: string[], cwd: string): GitOutcome;
69
+ /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */
70
+ gitQuery(args: string[], cwd: string, failMsg: string): string;
52
71
  private rawGitQuery;
53
72
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GitExec = void 0;
3
+ exports.GitExec = exports.GitOutcome = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
@@ -10,6 +10,24 @@ const SEP = '━━━━━━━━━━━━━━━━━━━━━━
10
10
  // The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the
11
11
  // REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).
12
12
  const GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';
13
+ /**
14
+ * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks
15
+ * `outcome.ok` rather than re-deriving "did it work" from a status number at every site.
16
+ */
17
+ class GitOutcome {
18
+ status;
19
+ stdout;
20
+ stderr;
21
+ constructor(status, stdout, stderr) {
22
+ this.status = status;
23
+ this.stdout = stdout;
24
+ this.stderr = stderr;
25
+ }
26
+ get ok() {
27
+ return this.status === 0;
28
+ }
29
+ }
30
+ exports.GitOutcome = GitOutcome;
13
31
  /** Shared git precondition checks + push logic for the PR/merge workflow. */
14
32
  let GitExec = class GitExec {
15
33
  repoRootFinder;
@@ -106,8 +124,17 @@ let GitExec = class GitExec {
106
124
  this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');
107
125
  }
108
126
  }
109
- // Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit
110
- // because `git ls-files --others` is scoped to the cwd subtree.
127
+ /**
128
+ * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the
129
+ * questions whose "no" is an answer rather than an error (does this remote ref exist? did this merge
130
+ * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on
131
+ * failure would operate on wrong state.
132
+ */
133
+ tryGit(args, cwd) {
134
+ const out = (0, child_process_1.spawnSync)('git', args, { encoding: 'utf8', cwd });
135
+ return new GitOutcome(out.status ?? 1, (out.stdout ?? '').trim(), (out.stderr ?? '').trim());
136
+ }
137
+ /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */
111
138
  gitQuery(args, cwd, failMsg) {
112
139
  return this.rawGitQuery(args, cwd, failMsg).trim();
113
140
  }
@@ -1 +1 @@
1
- {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":";;;;AAAA,iDAA0C;AAC1C,0DAAuE;AACvE,yCAA2D;AAC3D,6CAA+D;AAE/D,MAAM,GAAG,GAAG,0DAA0D,CAAC;AACvE,oGAAoG;AACpG,qGAAqG;AACrG,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AAE1D,6EAA6E;AAEtE,IAAM,OAAO,GAAb,MAAM,OAAO;IAEK;IACA;IAFrB,YACqB,cAA8B,EAC9B,YAA6B;QAD7B,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAiB;IAC/C,CAAC;IAEJ;;;;;;;;;;;;OAYG;IACH,eAAe,CAAC,GAAW;QACvB,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,GAAG,EAClD,mEAAmE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACjG,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,GAAW;QACrB,OAAO,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,mEAAmE;IACnE,cAAc,CAAC,GAAW;QACtB,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,UAAU,EAAE,UAAU,EAAE,oBAAoB,CAAC,EAAE,GAAG,EAAE,yDAAyD,CAAC,CAAC;IACzI,CAAC;IAED;;;;OAIG;IACH,eAAe,CAAC,GAAW;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO;QACzB,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG;YACV,sDAAsD;YACtD,GAAG,GAAG,IAAI;YACV,2EAA2E;YAC3E,4EAA4E;YAC5E,0CAA0C;YAC1C,KAAK,GAAG,MAAM;YACd,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,qBAAqB,CAAC,qCAAqC;YACvG,GAAG,CACN,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,iBAAiB,CAAC,GAAW;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,SAAS,KAAK,EAAE;YAAE,OAAO;QAC7B,MAAM,IAAI,2BAAY,CAAC,CAAC,EACpB,IAAI,GAAG,GAAG;YACV,0DAA0D;YAC1D,GAAG,GAAG,IAAI;YACV,6EAA6E;YAC7E,mCAAmC;YACnC,SAAS,GAAG,MAAM;YAClB,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,GAAG,EAAE,qBAAqB,CAAC,qCAAqC;YACvG,GAAG,CACN,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,IAAc,EAAE,MAAc;QACxC,MAAM,MAAM,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAC5D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,KAAK,MAAM,SAAS,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrG,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,aAAqB;QAC9B,MAAM,YAAY,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,CAAC,WAAW,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;QACrH,IAAI,YAAY,EAAE,CAAC;YACf,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,aAAa,EAAE,CAAC,EAAE,uBAAuB,CAAC,CAAC;QACnH,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,aAAa,EAAE,CAAC,EAAE,2BAA2B,CAAC,CAAC;QACvG,CAAC;IACL,CAAC;IAED,gGAAgG;IAChG,gEAAgE;IACxD,QAAQ,CAAC,IAAc,EAAE,GAAW,EAAE,OAAe;QACzD,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,mFAAmF;IAC3E,WAAW,CAAC,IAAc,EAAE,GAAW,EAAE,OAAe;QAC5D,MAAM,GAAG,GAAG,IAAA,yBAAS,EAAC,KAAK,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9D,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACnB,MAAM,IAAI,2BAAY,CAAC,CAAC,EAAE,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,GAAG,CAAC,MAAM,CAAC;IACtB,CAAC;CACJ,CAAA;AArHY,0BAAO;kBAAP,OAAO;IADnB,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAGA,6BAAc;QAChB,4BAAe;GAHzC,OAAO,CAqHnB","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GitStatusEntry, GitStatusParser } from './git-status';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n// The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the\n// REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).\nconst GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';\n\n/** Shared git precondition checks + push logic for the PR/merge workflow. */\n@injectable(bindingScopeValues.Singleton)\nexport class GitExec {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly statusParser: GitStatusParser,\n ) {}\n\n /**\n * Raw `git status --porcelain` with ONLY the trailing newline(s) stripped — never `.trim()`.\n *\n * There used to be a trimmed sibling here (`uncommittedFiles`) and it was a data-destroying trap:\n * porcelain column 1 is the index state and column 2 the worktree state, so an unstaged\n * modification is \" M path\"; `trim()` eats that leading space and the line then reads \"M path\",\n * i.e. STAGED — the exact inverse of the truth. It is deleted. This method is for EMPTINESS\n * (\"is the tree dirty at all?\") and for PRINTING; anything that reasons about the two columns\n * calls {@link statusEntries} and reads booleans instead of re-parsing a string.\n *\n * Tracked changes (staged + unstaged) AND untracked files, EXCLUDING gitignored paths — so\n * `.webpieces/` tooling artifacts never count. Empty string = fully committed.\n */\n porcelainStatus(cwd: string): string {\n return this.rawGitQuery(['status', '--porcelain'], cwd,\n 'Failed to run `git status --porcelain` to check the working tree.').replace(/\\n+$/, '');\n }\n\n /**\n * The same status, PARSED: two status columns, an unquoted path, and explicit\n * `isStaged()`/`isUnstaged()`/`isUntracked()` answers. Use this for every semantic question.\n */\n statusEntries(cwd: string): GitStatusEntry[] {\n return this.statusParser.parse(this.porcelainStatus(cwd));\n }\n\n // Untracked files only (respects .gitignore). Empty string = none.\n untrackedFiles(cwd: string): string {\n return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');\n }\n\n /**\n * Precondition for every PR/merge entry point: the working tree must be fully committed. The\n * webpieces tooling deliberately does NOT `git add`/`commit` the developer's work for them. Aborts\n * with instructions if anything is uncommitted (tracked or untracked, excluding gitignored).\n */\n assertCleanTree(cwd: string): void {\n const dirty = this.porcelainStatus(cwd);\n if (dirty === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: You have uncommitted or untracked changes\\n' +\n SEP + '\\n' +\n 'The webpieces PR tooling will NOT commit your work for you. Commit your\\n' +\n 'changes, and either commit or delete any untracked files, then re-run.\\n\\n' +\n 'Working tree (git status --porcelain):\\n' +\n dirty + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Narrower guard for the merge-resolve commit point, where tracked resolutions are legitimately in\n * the tree but untracked files must NOT be swept into the squash commit. Lists any untracked files\n * and aborts so the AI commits or deletes them explicitly.\n */\n assertNoUntracked(cwd: string): void {\n const untracked = this.untrackedFiles(cwd);\n if (untracked === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: Untracked files present during merge finalize\\n' +\n SEP + '\\n' +\n 'The tooling will not sweep untracked files into the squash commit. Commit\\n' +\n 'or delete these, then re-run:\\n\\n' +\n untracked + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Run a git command that is expected to succeed; abort the process with a clear message if it\n * fails. Used for fetch/pull/checkout/commit where silently continuing on failure would operate on\n * stale or wrong state.\n */\n runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n throw new CliExitError(1, `❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})`);\n }\n }\n\n /**\n * Push HEAD to origin/<currentBranch>. Uses --force-with-lease for an existing remote branch (the\n * 3-point squash rewrites history) and -u for a brand-new branch.\n */\n ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n this.runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n }\n\n // Run a read-only git query from the repo root; abort if git itself errors. Kept `cwd`-explicit\n // because `git ls-files --others` is scoped to the cwd subtree.\n private gitQuery(args: string[], cwd: string, failMsg: string): string {\n return this.rawGitQuery(args, cwd, failMsg).trim();\n }\n\n // The untrimmed sibling — see porcelainStatus for why trimming is not always safe.\n private rawGitQuery(args: string[], cwd: string, failMsg: string): string {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n if (out.status !== 0) {\n throw new CliExitError(1, `❌ ${failMsg}`);\n }\n return out.stdout;\n }\n}\n"]}
1
+ {"version":3,"file":"git-exec.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/git-exec.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { CliExitError, RepoRootFinder } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { GitStatusEntry, GitStatusParser } from './git-status';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n// The AI-facing doc the failure messages point at (generated under `.webpieces/instruct-ai`, at the\n// REPO ROOT — resolved absolute so the AI opens it regardless of the cwd it reads the message from).\nconst GIT_WORKFLOW_DOC_NAME = 'webpieces.git-workflow.md';\n\n/**\n * The result of a git command that was allowed to fail. Data-only (per CLAUDE.md) so a caller asks\n * `outcome.ok` rather than re-deriving \"did it work\" from a status number at every site.\n */\nexport class GitOutcome {\n status: number;\n stdout: string;\n stderr: string;\n\n constructor(status: number, stdout: string, stderr: string) {\n this.status = status;\n this.stdout = stdout;\n this.stderr = stderr;\n }\n\n get ok(): boolean {\n return this.status === 0;\n }\n}\n\n/** Shared git precondition checks + push logic for the PR/merge workflow. */\n@injectable(bindingScopeValues.Singleton)\nexport class GitExec {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly statusParser: GitStatusParser,\n ) {}\n\n /**\n * Raw `git status --porcelain` with ONLY the trailing newline(s) stripped — never `.trim()`.\n *\n * There used to be a trimmed sibling here (`uncommittedFiles`) and it was a data-destroying trap:\n * porcelain column 1 is the index state and column 2 the worktree state, so an unstaged\n * modification is \" M path\"; `trim()` eats that leading space and the line then reads \"M path\",\n * i.e. STAGED — the exact inverse of the truth. It is deleted. This method is for EMPTINESS\n * (\"is the tree dirty at all?\") and for PRINTING; anything that reasons about the two columns\n * calls {@link statusEntries} and reads booleans instead of re-parsing a string.\n *\n * Tracked changes (staged + unstaged) AND untracked files, EXCLUDING gitignored paths — so\n * `.webpieces/` tooling artifacts never count. Empty string = fully committed.\n */\n porcelainStatus(cwd: string): string {\n return this.rawGitQuery(['status', '--porcelain'], cwd,\n 'Failed to run `git status --porcelain` to check the working tree.').replace(/\\n+$/, '');\n }\n\n /**\n * The same status, PARSED: two status columns, an unquoted path, and explicit\n * `isStaged()`/`isUnstaged()`/`isUntracked()` answers. Use this for every semantic question.\n */\n statusEntries(cwd: string): GitStatusEntry[] {\n return this.statusParser.parse(this.porcelainStatus(cwd));\n }\n\n // Untracked files only (respects .gitignore). Empty string = none.\n untrackedFiles(cwd: string): string {\n return this.gitQuery(['ls-files', '--others', '--exclude-standard'], cwd, 'Failed to list untracked files (git ls-files --others).');\n }\n\n /**\n * Precondition for every PR/merge entry point: the working tree must be fully committed. The\n * webpieces tooling deliberately does NOT `git add`/`commit` the developer's work for them. Aborts\n * with instructions if anything is uncommitted (tracked or untracked, excluding gitignored).\n */\n assertCleanTree(cwd: string): void {\n const dirty = this.porcelainStatus(cwd);\n if (dirty === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: You have uncommitted or untracked changes\\n' +\n SEP + '\\n' +\n 'The webpieces PR tooling will NOT commit your work for you. Commit your\\n' +\n 'changes, and either commit or delete any untracked files, then re-run.\\n\\n' +\n 'Working tree (git status --porcelain):\\n' +\n dirty + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Narrower guard for the merge-resolve commit point, where tracked resolutions are legitimately in\n * the tree but untracked files must NOT be swept into the squash commit. Lists any untracked files\n * and aborts so the AI commits or deletes them explicitly.\n */\n assertNoUntracked(cwd: string): void {\n const untracked = this.untrackedFiles(cwd);\n if (untracked === '') return;\n throw new CliExitError(1,\n '\\n' + SEP +\n '❌ ERROR: Untracked files present during merge finalize\\n' +\n SEP + '\\n' +\n 'The tooling will not sweep untracked files into the squash commit. Commit\\n' +\n 'or delete these, then re-run:\\n\\n' +\n untracked + '\\n\\n' +\n `See ${this.repoRootFinder.docPathFrom(cwd, GIT_WORKFLOW_DOC_NAME)} for the full merge + PR process.\\n` +\n SEP,\n );\n }\n\n /**\n * Run a git command that is expected to succeed; abort the process with a clear message if it\n * fails. Used for fetch/pull/checkout/commit where silently continuing on failure would operate on\n * stale or wrong state.\n */\n runGitChecked(args: string[], errMsg: string): void {\n const result = spawnSync('git', args, { stdio: 'inherit' });\n if (result.status !== 0) {\n throw new CliExitError(1, `❌ ${errMsg} (git ${args.join(' ')} exited ${String(result.status)})`);\n }\n }\n\n /**\n * Push HEAD to origin/<currentBranch>. Uses --force-with-lease for an existing remote branch (the\n * 3-point squash rewrites history) and -u for a brand-new branch.\n */\n ensurePushed(currentBranch: string): void {\n const remoteExists = spawnSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', currentBranch]).status === 0;\n if (remoteExists) {\n this.runGitChecked(['push', '--force-with-lease', 'origin', `HEAD:${currentBranch}`], 'Failed to push branch');\n } else {\n this.runGitChecked(['push', '-u', 'origin', `HEAD:${currentBranch}`], 'Failed to push new branch');\n }\n }\n\n /**\n * Run a git command that MAY legitimately fail and report the outcome instead of aborting — for the\n * questions whose \"no\" is an answer rather than an error (does this remote ref exist? did this merge\n * conflict?). Distinct from {@link runGitChecked}, which exists for the commands where continuing on\n * failure would operate on wrong state.\n */\n tryGit(args: string[], cwd: string): GitOutcome {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n return new GitOutcome(out.status ?? 1, (out.stdout ?? '').trim(), (out.stderr ?? '').trim());\n }\n\n /** Read-only git query that MUST succeed, trimmed. Public sibling of the private raw form. */\n gitQuery(args: string[], cwd: string, failMsg: string): string {\n return this.rawGitQuery(args, cwd, failMsg).trim();\n }\n\n // The untrimmed sibling — see porcelainStatus for why trimming is not always safe.\n private rawGitQuery(args: string[], cwd: string, failMsg: string): string {\n const out = spawnSync('git', args, { encoding: 'utf8', cwd });\n if (out.status !== 0) {\n throw new CliExitError(1, `❌ ${failMsg}`);\n }\n return out.stdout;\n }\n}\n"]}
@@ -0,0 +1,52 @@
1
+ import { DotWebpieces } from '@webpieces/rules-config';
2
+ /**
3
+ * The persisted half of the `wp-push-dev --resolve` state machine.
4
+ *
5
+ * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on
6
+ * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files
7
+ * before anything can be committed. So the command must be able to stop, hand the tree over, and be
8
+ * resumed by a second command that knows exactly where it left off.
9
+ *
10
+ * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.
11
+ */
12
+ export declare class PushDevState {
13
+ /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */
14
+ originalBranch: string;
15
+ /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */
16
+ tmpBranch: string;
17
+ /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */
18
+ targetRef: string;
19
+ /** The copy refs still to merge, in the order CI composes them. */
20
+ queue: string[];
21
+ /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */
22
+ current: string;
23
+ constructor(originalBranch: string, tmpBranch: string, targetRef: string, queue: string[], current?: string);
24
+ }
25
+ /**
26
+ * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it
27
+ * exists.
28
+ *
29
+ * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another
30
+ * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says
31
+ * nothing about their tree.
32
+ */
33
+ export declare class PushDevStateStore {
34
+ private readonly dotWebpieces;
35
+ constructor(dotWebpieces: DotWebpieces);
36
+ path(repoRoot: string): string;
37
+ exists(repoRoot: string): boolean;
38
+ /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */
39
+ read(repoRoot: string): PushDevState | null;
40
+ write(repoRoot: string, state: PushDevState): void;
41
+ clear(repoRoot: string): void;
42
+ /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */
43
+ require(repoRoot: string): PushDevState;
44
+ /**
45
+ * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in
46
+ * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.
47
+ */
48
+ assertIdle(repoRoot: string, attempted: string): void;
49
+ private blockedCommandList;
50
+ /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */
51
+ isBlockedDuringResolve(command: string): boolean;
52
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PushDevStateStore = exports.PushDevState = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const rules_config_1 = require("@webpieces/rules-config");
7
+ const inversify_1 = require("inversify");
8
+ const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
9
+ /**
10
+ * The persisted half of the `wp-push-dev --resolve` state machine.
11
+ *
12
+ * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on
13
+ * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files
14
+ * before anything can be committed. So the command must be able to stop, hand the tree over, and be
15
+ * resumed by a second command that knows exactly where it left off.
16
+ *
17
+ * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.
18
+ */
19
+ class PushDevState {
20
+ /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */
21
+ originalBranch;
22
+ /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */
23
+ tmpBranch;
24
+ /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */
25
+ targetRef;
26
+ /** The copy refs still to merge, in the order CI composes them. */
27
+ queue;
28
+ /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */
29
+ current;
30
+ // eslint-disable-next-line @typescript-eslint/max-params
31
+ constructor(originalBranch, tmpBranch, targetRef, queue, current = '') {
32
+ this.originalBranch = originalBranch;
33
+ this.tmpBranch = tmpBranch;
34
+ this.targetRef = targetRef;
35
+ this.queue = queue;
36
+ this.current = current;
37
+ }
38
+ }
39
+ exports.PushDevState = PushDevState;
40
+ // The wp-* commands that must REFUSE while a resolve is half-finished, and the ONE place that list is
41
+ // written. The blocked-list sentence below RENDERS ITSELF from this array — the same lesson
42
+ // merge-in-progress-guard already learned the hard way: a hand-written "don't run other commands"
43
+ // survives every edit to the actual enforcement and ends up forbidding things the flow requires.
44
+ //
45
+ // Why these and not everything: each one either rewrites the current branch (the two starts), pushes it
46
+ // (the finishes), or deletes branches (cleanup) — and during a resolve the checkout is a throwaway
47
+ // branch, not the feature branch any of them think they are acting on.
48
+ const BLOCKED_DURING_RESOLVE = [
49
+ 'wp-start-update', 'wp-finish-update',
50
+ 'wp-start-upsert-pr', 'wp-review-upsert-pr', 'wp-finish-upsert-pr',
51
+ 'wp-land-pr', 'wp-cleanup',
52
+ ];
53
+ /**
54
+ * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it
55
+ * exists.
56
+ *
57
+ * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another
58
+ * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says
59
+ * nothing about their tree.
60
+ */
61
+ let PushDevStateStore = class PushDevStateStore {
62
+ dotWebpieces;
63
+ constructor(dotWebpieces) {
64
+ this.dotWebpieces = dotWebpieces;
65
+ }
66
+ path(repoRoot) {
67
+ return this.dotWebpieces.localFile(repoRoot, rules_config_1.PUSH_DEV_STATE_FILE);
68
+ }
69
+ exists(repoRoot) {
70
+ return fs.existsSync(this.path(repoRoot));
71
+ }
72
+ /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */
73
+ read(repoRoot) {
74
+ const file = this.path(repoRoot);
75
+ if (!fs.existsSync(file))
76
+ return null;
77
+ // webpieces-disable no-unmanaged-exceptions -- a truncated/hand-edited state file must degrade to "no resolve in progress" (recoverable via --abort or a fresh --resolve), never crash every wp-* command in the tree
78
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
79
+ try {
80
+ // webpieces-disable no-any-unknown -- JSON.parse is untyped until narrowed on the next line
81
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
82
+ if (typeof raw.originalBranch !== 'string' || typeof raw.tmpBranch !== 'string')
83
+ return null;
84
+ return new PushDevState(raw.originalBranch, raw.tmpBranch, raw.targetRef ?? '', Array.isArray(raw.queue) ? raw.queue : [], raw.current ?? '');
85
+ // The parse error carries nothing a caller could act on: the ONLY meaningful reading of an
86
+ // unparseable state file is "no resolve in progress", which --abort and a fresh --resolve both
87
+ // recover from.
88
+ }
89
+ catch (err) {
90
+ const error = (0, rules_config_1.toError)(err);
91
+ void error;
92
+ return null;
93
+ }
94
+ }
95
+ write(repoRoot, state) {
96
+ fs.mkdirSync(this.dotWebpieces.local(repoRoot), { recursive: true });
97
+ fs.writeFileSync(this.path(repoRoot), JSON.stringify(state, null, 2) + '\n', 'utf8');
98
+ }
99
+ clear(repoRoot) {
100
+ const file = this.path(repoRoot);
101
+ if (fs.existsSync(file))
102
+ fs.rmSync(file);
103
+ }
104
+ /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */
105
+ require(repoRoot) {
106
+ const state = this.read(repoRoot);
107
+ if (state !== null)
108
+ return state;
109
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + '⛔ No dev-deploy resolve is in progress\n' + SEP + '\n'
110
+ + `${rules_config_1.WP_FINISH_PUSH_DEV} resumes a resolve that \`${rules_config_1.WP_PUSH_DEV} --resolve\` started, and there is no\n`
111
+ + `state file at:\n ${this.path(repoRoot)}\n\n`
112
+ + 'If you meant to publish your branch to the shared dev server, that is the one-command form:\n'
113
+ + ` ${rules_config_1.WP_PUSH_DEV}\n`);
114
+ }
115
+ /**
116
+ * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in
117
+ * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.
118
+ */
119
+ assertIdle(repoRoot, attempted) {
120
+ const state = this.read(repoRoot);
121
+ if (state === null)
122
+ return;
123
+ throw new rules_config_1.CliExitError(2, '\n' + SEP + `⛔ A dev-deploy resolve is in progress — \`pnpm ${attempted}\` is blocked\n` + SEP + '\n'
124
+ + `You are standing on the throwaway branch \`${state.tmpBranch}\`, not on \`${state.originalBranch}\`,\n`
125
+ + `so ${this.blockedCommandList()} would all act on the wrong branch.\n`
126
+ + 'That is the whole list; nothing else is blocked.\n\n'
127
+ + 'EXPECTED of you right now, and NOT blocked: read the conflicted files and edit them until every\n'
128
+ + 'conflict marker is gone. Then finish or bail out:\n'
129
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish the copy\n`
130
+ + ` ${rules_config_1.WP_FINISH_PUSH_DEV} --abort ← throw the resolution away and go back to \`${state.originalBranch}\`\n\n`
131
+ + `State file: ${this.path(repoRoot)}\n`);
132
+ }
133
+ // Rendered from BLOCKED_DURING_RESOLVE, never hand-written — see that constant.
134
+ blockedCommandList() {
135
+ return BLOCKED_DURING_RESOLVE.map((c) => `\`pnpm ${c}\``).join(', ');
136
+ }
137
+ /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */
138
+ isBlockedDuringResolve(command) {
139
+ return BLOCKED_DURING_RESOLVE.includes(command);
140
+ }
141
+ };
142
+ exports.PushDevStateStore = PushDevStateStore;
143
+ exports.PushDevStateStore = PushDevStateStore = tslib_1.__decorate([
144
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
145
+ tslib_1.__metadata("design:paramtypes", [rules_config_1.DotWebpieces])
146
+ ], PushDevStateStore);
147
+ //# sourceMappingURL=push-dev-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-dev-state.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/push-dev-state.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\n\nimport {\n CliExitError,\n DotWebpieces,\n PUSH_DEV_STATE_FILE,\n WP_FINISH_PUSH_DEV,\n WP_PUSH_DEV,\n toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The persisted half of the `wp-push-dev --resolve` state machine.\n *\n * A single-shot wrapper is IMPOSSIBLE here and that is why this file exists: `git merge` halts on\n * conflict with markers in the working tree and a non-zero exit, and a human or an AI has to edit files\n * before anything can be committed. So the command must be able to stop, hand the tree over, and be\n * resumed by a second command that knows exactly where it left off.\n *\n * Data-only (per CLAUDE.md) — the behaviour lives in {@link PushDevStateStore}.\n */\nexport class PushDevState {\n /** The branch to return to when the resolve finishes or aborts. NEVER written to by this flow. */\n originalBranch: string;\n /** The local throwaway branch the merges happen on. Deleted on finish and on abort. */\n tmpBranch: string;\n /** `<namespace>/<originalBranch>` — the remote copy this resolve republishes. */\n targetRef: string;\n /** The copy refs still to merge, in the order CI composes them. */\n queue: string[];\n /** The ref currently mid-merge (conflicted), or '' when nothing is halted. */\n current: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(originalBranch: string, tmpBranch: string, targetRef: string, queue: string[], current = '') {\n this.originalBranch = originalBranch;\n this.tmpBranch = tmpBranch;\n this.targetRef = targetRef;\n this.queue = queue;\n this.current = current;\n }\n}\n\n// The wp-* commands that must REFUSE while a resolve is half-finished, and the ONE place that list is\n// written. The blocked-list sentence below RENDERS ITSELF from this array — the same lesson\n// merge-in-progress-guard already learned the hard way: a hand-written \"don't run other commands\"\n// survives every edit to the actual enforcement and ends up forbidding things the flow requires.\n//\n// Why these and not everything: each one either rewrites the current branch (the two starts), pushes it\n// (the finishes), or deletes branches (cleanup) — and during a resolve the checkout is a throwaway\n// branch, not the feature branch any of them think they are acting on.\nconst BLOCKED_DURING_RESOLVE: readonly string[] = [\n 'wp-start-update', 'wp-finish-update',\n 'wp-start-upsert-pr', 'wp-review-upsert-pr', 'wp-finish-upsert-pr',\n 'wp-land-pr', 'wp-cleanup',\n];\n\n/**\n * Reads, writes and clears the resolve state file, and refuses the other `wp-*` commands while it\n * exists.\n *\n * LOCAL scope, deliberately: a resolve halts one worktree's checkout on a throwaway branch, and another\n * worktree has no reason to care. Sharing it would block a colleague's PR flow on state that says\n * nothing about their tree.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PushDevStateStore {\n constructor(private readonly dotWebpieces: DotWebpieces) {}\n\n path(repoRoot: string): string {\n return this.dotWebpieces.localFile(repoRoot, PUSH_DEV_STATE_FILE);\n }\n\n exists(repoRoot: string): boolean {\n return fs.existsSync(this.path(repoRoot));\n }\n\n /** The in-flight resolve, or null when none is. A corrupt file reads as null rather than crashing. */\n read(repoRoot: string): PushDevState | null {\n const file = this.path(repoRoot);\n if (!fs.existsSync(file)) return null;\n // webpieces-disable no-unmanaged-exceptions -- a truncated/hand-edited state file must degrade to \"no resolve in progress\" (recoverable via --abort or a fresh --resolve), never crash every wp-* command in the tree\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- JSON.parse is untyped until narrowed on the next line\n const raw = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial<PushDevState>;\n if (typeof raw.originalBranch !== 'string' || typeof raw.tmpBranch !== 'string') return null;\n return new PushDevState(\n raw.originalBranch, raw.tmpBranch, raw.targetRef ?? '',\n Array.isArray(raw.queue) ? raw.queue : [], raw.current ?? '');\n // The parse error carries nothing a caller could act on: the ONLY meaningful reading of an\n // unparseable state file is \"no resolve in progress\", which --abort and a fresh --resolve both\n // recover from.\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n write(repoRoot: string, state: PushDevState): void {\n fs.mkdirSync(this.dotWebpieces.local(repoRoot), { recursive: true });\n fs.writeFileSync(this.path(repoRoot), JSON.stringify(state, null, 2) + '\\n', 'utf8');\n }\n\n clear(repoRoot: string): void {\n const file = this.path(repoRoot);\n if (fs.existsSync(file)) fs.rmSync(file);\n }\n\n /** The read that MUST find a resolve — used by `wp-finish-push-dev`, whose whole job is resuming one. */\n require(repoRoot: string): PushDevState {\n const state = this.read(repoRoot);\n if (state !== null) return state;\n throw new CliExitError(2,\n '\\n' + SEP + '⛔ No dev-deploy resolve is in progress\\n' + SEP + '\\n'\n + `${WP_FINISH_PUSH_DEV} resumes a resolve that \\`${WP_PUSH_DEV} --resolve\\` started, and there is no\\n`\n + `state file at:\\n ${this.path(repoRoot)}\\n\\n`\n + 'If you meant to publish your branch to the shared dev server, that is the one-command form:\\n'\n + ` ${WP_PUSH_DEV}\\n`);\n }\n\n /**\n * Refuse `attempted` while a resolve is half-finished. Called from the app root for every command in\n * {@link BLOCKED_DURING_RESOLVE}, so no individual command has to remember to ask.\n */\n assertIdle(repoRoot: string, attempted: string): void {\n const state = this.read(repoRoot);\n if (state === null) return;\n throw new CliExitError(2,\n '\\n' + SEP + `⛔ A dev-deploy resolve is in progress — \\`pnpm ${attempted}\\` is blocked\\n` + SEP + '\\n'\n + `You are standing on the throwaway branch \\`${state.tmpBranch}\\`, not on \\`${state.originalBranch}\\`,\\n`\n + `so ${this.blockedCommandList()} would all act on the wrong branch.\\n`\n + 'That is the whole list; nothing else is blocked.\\n\\n'\n + 'EXPECTED of you right now, and NOT blocked: read the conflicted files and edit them until every\\n'\n + 'conflict marker is gone. Then finish or bail out:\\n'\n + ` ${WP_FINISH_PUSH_DEV} ← commit the resolution, resume the queue, publish the copy\\n`\n + ` ${WP_FINISH_PUSH_DEV} --abort ← throw the resolution away and go back to \\`${state.originalBranch}\\`\\n\\n`\n + `State file: ${this.path(repoRoot)}\\n`);\n }\n\n // Rendered from BLOCKED_DURING_RESOLVE, never hand-written — see that constant.\n private blockedCommandList(): string {\n return BLOCKED_DURING_RESOLVE.map((c: string): string => `\\`pnpm ${c}\\``).join(', ');\n }\n\n /** Is `command` one of the ones that must refuse mid-resolve? Used by the app root. */\n isBlockedDuringResolve(command: string): boolean {\n return BLOCKED_DURING_RESOLVE.includes(command);\n }\n}\n"]}
@@ -33,6 +33,12 @@ export declare class ReviewReportInput {
33
33
  formatErrors: string[];
34
34
  briefings: ReviewerBriefing[];
35
35
  refused: RefusedReviewer[];
36
+ /**
37
+ * `--no-optional` was passed: the human has already said to submit without the optional reviews, so the
38
+ * block that offers them is replaced by a one-line statement that they were skipped. It never suppresses
39
+ * a REQUIRED reviewer, and it is not a gate — nothing about what blocks the PR changes.
40
+ */
41
+ skipOptional: boolean;
36
42
  constructor(repoRoot: string, featureName: string, reviewPath: string);
37
43
  }
38
44
  /**
@@ -50,6 +56,9 @@ export declare class ReviewReportInput {
50
56
  * 1. `wp-finish-upsert-pr` is named as a thing to run EXACTLY ONCE in the whole block.
51
57
  * 2. The review.json instruction comes BEFORE it — and BEFORE the spawn blocks (see nextSteps).
52
58
  * 3. With zero checklists the all-clear precedes any configuration guidance.
59
+ * 4. REQUIRED reviewers are spawned unasked; OPTIONAL ones are only ever OFFERED, in one batched
60
+ * question. The two never share a step, because one instruction says "do it" and the other says
61
+ * "ask first", and an agent reading a merged list will act on the stronger of the two.
53
62
  *
54
63
  * Pure string building, no I/O. `@injectable(bindingScopeValues.Singleton)` so it is injected by type.
55
64
  */
@@ -58,6 +67,17 @@ export declare class ReviewReport {
58
67
  private readonly reviewerInstructions;
59
68
  constructor(checklistNotice: ChecklistNotice, reviewerInstructions: ReviewerInstructionsService);
60
69
  render(input: ReviewReportInput): string;
70
+ /**
71
+ * Name what this block is actually about. A repo with reviewers owed is being told to SPAWN; a repo with
72
+ * none is not, and promising subagents it does not have is the same kind of noise as explaining checklist
73
+ * configuration to a repo that configured none. Keyed on what is actually ACTIONABLE rather than on the
74
+ * applicable count: every applicable checklist already having a verdict means nothing to spawn, and so
75
+ * does a branch whose only outstanding reviews are optional ones the human already waved off.
76
+ *
77
+ * "spawn" and "ask about" are separate headings because they are separate obligations. A branch owing
78
+ * only optional reviews has nothing the AI may do unilaterally, and a heading that says SPAWN is the
79
+ * single line most likely to make it do exactly that.
80
+ */
61
81
  private header;
62
82
  /**
63
83
  * What the SCAN found: either "nothing applies here" or the already-reviewed / unreadable-verdict lines.
@@ -65,6 +85,13 @@ export declare class ReviewReport {
65
85
  * mistaken for the next action.
66
86
  */
67
87
  private scanVerdict;
88
+ /**
89
+ * `--no-optional`, stated as a VERDICT rather than left silent. Named individually, not just counted: the
90
+ * whole reason the human is allowed to skip these is that they know this diff, and the only way they can
91
+ * catch "wait, not THAT one" is to see which ones went unreviewed.
92
+ */
93
+ private skippedLines;
94
+ private allClear;
68
95
  /**
69
96
  * The ONE instruction block, and the last thing the stage prints. Numbered rather than prose-linked
70
97
  * ("Then… Finally…") so that skipping step 1 is visibly skipping a step, and worded so no earlier line
@@ -88,14 +115,40 @@ export declare class ReviewReport {
88
115
  private nextSteps;
89
116
  private writeReviewStep;
90
117
  /**
91
- * STEP 2 — one copy-paste block per owed reviewer, and nothing at all when none is owed. The prompt is
118
+ * The REQUIRED reviewers — one copy-paste block each, and nothing at all when none is owed. The prompt is
92
119
  * deliberately a POINTER and nothing else: the generated instructions file is the contract, so anything
93
120
  * restated here is a second copy that can go stale — which is exactly how a removed `success` field
94
121
  * outlived its own removal in print.
122
+ *
123
+ * These are spawned WITHOUT asking. They are the checklists the repo declared `required: true`, which is
124
+ * the repo saying the decision was already made; putting them to the human again would re-open a question
125
+ * the config exists to settle.
95
126
  */
96
127
  private spawnStep;
128
+ /**
129
+ * STEP n — the OPTIONAL reviewers: listed, never spawned unasked.
130
+ *
131
+ * This is the whole point of `required: false`. A one-line bug fix in a repo whose checklists key on a
132
+ * glob as broad as every TypeScript file otherwise pays for a dozen subagent reviews, and the only party
133
+ * who can judge whether this particular diff is worth them is the human looking at it.
134
+ *
135
+ * ONE batched multi-select question, explicitly. Asked one at a time, a human answering "no" nine times
136
+ * is being worn down rather than consulted, and by the third question the cheap thing is to say yes to
137
+ * everything — which is the state this feature exists to leave. The "None" option has to be spelled out
138
+ * too: an agent that offers a list without an explicit way to decline it has not really offered a choice.
139
+ *
140
+ * The blocking consequence is stated because it is the one non-obvious part of the contract: `required`
141
+ * governs whether a reviewer must RUN, not whether its answer counts. Choosing to run one and then
142
+ * shrugging off a red verdict would make the whole exercise theater.
143
+ */
144
+ private offerStep;
145
+ private refusedWarning;
97
146
  private finishStep;
98
147
  private owedReviewers;
148
+ private requiredOwed;
149
+ private optionalOwed;
150
+ private offerableOwed;
151
+ private actionableOwed;
99
152
  /**
100
153
  * One reviewer's block. A reviewer that already REFUSED gets the SAME spawn coordinates but a different
101
154
  * lead-in, because the action before spawning is different: its own words are printed, and the spawn is
@@ -108,5 +161,13 @@ export declare class ReviewReport {
108
161
  */
109
162
  private oneSpawnBlock;
110
163
  private leadIn;
164
+ /**
165
+ * The checklist's guidance doc, for OPTIONAL reviewers only.
166
+ *
167
+ * "4 file(s) matched" plus a broad glob does not tell a human what the review would actually look AT,
168
+ * and they are being asked to decide exactly that. Omitted for required reviewers: there is no decision
169
+ * to inform there — the reviewer runs either way, and the doc is already in its instructions file.
170
+ */
171
+ private docLine;
111
172
  private why;
112
173
  }