@atolis-hq/wake 0.2.55 → 0.2.57

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.
@@ -168,6 +168,10 @@ export async function buildStagePrompt(input) {
168
168
  if (resolvedWorkspaceMode === 'branch') {
169
169
  context.branch = branchNameForIssue(input.projection.issue.number);
170
170
  }
171
+ // Opaque to this file (resourceUri locator grammar is provider-specific,
172
+ // ADR 0001 §1) — passed through as-is so a provider-aware prompt template
173
+ // can interpret it without stage-prompt.ts knowing any adapter's format.
174
+ context.correlatedResources = input.projection.correlatedResources;
171
175
  const allowedTools = parseFrontmatterList(template.frontmatter.allowedTools);
172
176
  const allowedToolsListStr = allowedTools.length > 0 ? allowedTools.join(', ') : '(none)';
173
177
  context.allowedToolsList = allowedToolsListStr;
@@ -195,8 +195,10 @@ export function createTickRunner(deps) {
195
195
  return projection.issue.labels.includes(label);
196
196
  }
197
197
  function approvedMergePolicyForStage(stage) {
198
- return (stage?.watch?.find((watch) => watch.onApproved?.merge !== undefined)?.onApproved?.merge ??
199
- null);
198
+ // Schema defaults materialize a disabled merge block whenever onSuccess is
199
+ // set (e.g. approve-only watchers); treat it as no merge policy at all.
200
+ const merge = stage?.watch?.find((watch) => watch.onSuccess?.merge !== undefined)?.onSuccess?.merge ?? null;
201
+ return merge !== null && (merge.approve || merge.autoMerge) ? merge : null;
200
202
  }
201
203
  function escapeRegex(input) {
202
204
  return input.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
@@ -390,6 +392,50 @@ export function createTickRunner(deps) {
390
392
  }));
391
393
  }
392
394
  }
395
+ // The one deterministic approval transition: /approved, wake:auto, and a
396
+ // watcher child's onSuccess.approve all resolve a pending approval through
397
+ // this same event shape, so replay folds them identically.
398
+ async function applyApprovalTransition(input) {
399
+ const nextStage = lifecycle.nextStageFromSentinel(input.projection.wake.stage, 'DONE', input.workflow);
400
+ if (nextStage === null) {
401
+ return null;
402
+ }
403
+ const approvalCompletedEvent = createEventEnvelope({
404
+ eventId: `${input.approvalId}-completed`,
405
+ workItemKey: input.projection.workItemKey,
406
+ streamScope: 'work-item',
407
+ direction: 'internal',
408
+ sourceSystem: 'wake',
409
+ sourceEventType: RUN_COMPLETED_EVENT,
410
+ sourceRefs: {
411
+ repo: input.projection.issue.repo,
412
+ issueNumber: input.projection.issue.number,
413
+ runId: input.approvalId,
414
+ },
415
+ occurredAt: input.approvedAt,
416
+ ingestedAt: input.approvedAt,
417
+ trigger: 'immediate',
418
+ payload: {
419
+ action: input.pendingAction,
420
+ sentinel: 'DONE',
421
+ nextStage,
422
+ runId: input.approvalId,
423
+ reason: input.reason,
424
+ ...input.payloadExtras,
425
+ },
426
+ });
427
+ await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
428
+ await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
429
+ await deliverOutboundEvent(createLabelsEvent({
430
+ projection: input.projection,
431
+ runId: input.approvalId,
432
+ statusLabel: statusLabelForStage(nextStage),
433
+ stageLabel: stageLabelForStage(nextStage),
434
+ workflowLabel: workflowLabelForWorkflowName(input.workflowName),
435
+ occurredAt: input.approvedAt,
436
+ }));
437
+ return { nextStage };
438
+ }
393
439
  // Closes the loop on #82's review feedback: rather than scrape a PR link
394
440
  // out of the agent's free text, the agent emits a `wake-artifacts` fence
395
441
  // (domain/schema.ts's parseRunnerArtifacts) and Wake verifies each claim
@@ -1041,48 +1087,29 @@ export function createTickRunner(deps) {
1041
1087
  const approvalId = `approval-${candidate.issue.number}-${deps.clock.now().getTime()}`;
1042
1088
  const approvedAt = deps.clock.now().toISOString();
1043
1089
  const automaticApproval = approvalResolution.automatic === true;
1044
- const nextStage = lifecycle.nextStageFromSentinel(candidate.wake.stage, 'DONE', workflow);
1045
- if (nextStage === null) {
1090
+ const applied = await applyApprovalTransition({
1091
+ projection: candidate,
1092
+ pendingAction: approvalResolution.pendingAction,
1093
+ approvalId,
1094
+ approvedAt,
1095
+ reason: automaticApproval ? 'auto:approved' : 'human:approved',
1096
+ workflow,
1097
+ workflowName,
1098
+ payloadExtras: automaticApproval
1099
+ ? {
1100
+ autoResolution: {
1101
+ kind: 'awaiting-approval',
1102
+ classification: 'auto-approval',
1103
+ reasoning: approvalResolution.reason,
1104
+ },
1105
+ }
1106
+ : {
1107
+ handledCommentId: approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate),
1108
+ },
1109
+ });
1110
+ if (applied === null) {
1046
1111
  return { status: 'idle' };
1047
1112
  }
1048
- const approvalCompletedEvent = createEventEnvelope({
1049
- eventId: `${approvalId}-completed`,
1050
- workItemKey: candidate.workItemKey,
1051
- streamScope: 'work-item',
1052
- direction: 'internal',
1053
- sourceSystem: 'wake',
1054
- sourceEventType: RUN_COMPLETED_EVENT,
1055
- sourceRefs: {
1056
- repo: candidate.issue.repo,
1057
- issueNumber: candidate.issue.number,
1058
- runId: approvalId,
1059
- },
1060
- occurredAt: approvedAt,
1061
- ingestedAt: approvedAt,
1062
- trigger: 'immediate',
1063
- payload: {
1064
- action: approvalResolution.pendingAction,
1065
- sentinel: 'DONE',
1066
- nextStage,
1067
- runId: approvalId,
1068
- reason: automaticApproval ? 'auto:approved' : 'human:approved',
1069
- ...(automaticApproval
1070
- ? {}
1071
- : {
1072
- handledCommentId: approvalResolution.triggeringCommentId ?? latestHumanCommentId(candidate),
1073
- }),
1074
- ...(automaticApproval
1075
- ? {
1076
- autoResolution: {
1077
- kind: 'awaiting-approval',
1078
- classification: 'auto-approval',
1079
- reasoning: approvalResolution.reason,
1080
- },
1081
- }
1082
- : {}),
1083
- },
1084
- });
1085
- await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
1086
1113
  if (automaticApproval) {
1087
1114
  await appendAuditEvent({
1088
1115
  eventId: `${approvalId}-audit-auto-resolution`,
@@ -1102,7 +1129,7 @@ export function createTickRunner(deps) {
1102
1129
  },
1103
1130
  outcome: {
1104
1131
  approved: true,
1105
- nextStage,
1132
+ nextStage: applied.nextStage,
1106
1133
  reason: approvalResolution.reason,
1107
1134
  },
1108
1135
  timestamp: approvedAt,
@@ -1112,20 +1139,11 @@ export function createTickRunner(deps) {
1112
1139
  },
1113
1140
  });
1114
1141
  }
1115
- await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
1116
- await deliverOutboundEvent(createLabelsEvent({
1117
- projection: candidate,
1118
- runId: approvalId,
1119
- statusLabel: statusLabelForStage(nextStage),
1120
- stageLabel: stageLabelForStage(nextStage),
1121
- workflowLabel: workflowLabelForWorkflowName(workflowName),
1122
- occurredAt: approvedAt,
1123
- }));
1124
1142
  return {
1125
1143
  status: 'processed',
1126
1144
  runId: approvalId,
1127
1145
  sentinel: 'DONE',
1128
- nextStage,
1146
+ nextStage: applied.nextStage,
1129
1147
  };
1130
1148
  }
1131
1149
  else {
@@ -1733,6 +1751,10 @@ export function createTickRunner(deps) {
1733
1751
  if (watcherRun) {
1734
1752
  // Watcher-dispatched runs own PR verdict delivery and correlation
1735
1753
  // registration regardless of the target workflow/action name.
1754
+ const pendingApprovalAction = candidate.context.pendingApprovalAction;
1755
+ const watcherSuccessPolicy = watcherDispatch === null
1756
+ ? undefined
1757
+ : deps.config.workflows[watcherDispatch.parentWorkflowName]?.stages[watcherDispatch.parentStage]?.watch?.[watcherDispatch.watcherIndex]?.onSuccess;
1736
1758
  if (prReviewTargetResourceUri !== null &&
1737
1759
  (sentinel === 'DONE' || sentinel === 'FAILED')) {
1738
1760
  await deliverOutboundEvent({
@@ -1751,6 +1773,58 @@ export function createTickRunner(deps) {
1751
1773
  },
1752
1774
  });
1753
1775
  }
1776
+ else if (watcherDispatch !== null &&
1777
+ watcherSuccessPolicy?.approve === true &&
1778
+ (sentinel === 'DONE' || sentinel === 'FAILED' || sentinel === 'BLOCKED')) {
1779
+ // No PR surface to carry the verdict comment: the child's sentinel
1780
+ // is its verdict. Publish the review body for every verdict so the
1781
+ // human sees why; only DONE resolves the parent's pending gate.
1782
+ await deliverOutboundEvent(publishIntent);
1783
+ const approvalId = `${runId}-parent-approval`;
1784
+ const parentWorkflow = deps.config.workflows[watcherDispatch.parentWorkflowName];
1785
+ if (sentinel === 'DONE' &&
1786
+ isAwaitingApproval(candidate) &&
1787
+ typeof pendingApprovalAction === 'string' &&
1788
+ parentWorkflow !== undefined &&
1789
+ (await deps.stateStore.readEventEnvelope(`${approvalId}-completed`)) === null) {
1790
+ const approvedAt = eventStampNow();
1791
+ const applied = await applyApprovalTransition({
1792
+ projection: candidate,
1793
+ pendingAction: pendingApprovalAction,
1794
+ approvalId,
1795
+ approvedAt,
1796
+ reason: 'watcher:approved',
1797
+ workflow: parentWorkflow,
1798
+ workflowName: watcherDispatch.parentWorkflowName,
1799
+ });
1800
+ if (applied !== null) {
1801
+ await appendAuditEvent({
1802
+ eventId: `${approvalId}-audit-watcher-approval`,
1803
+ decisionType: 'approval.watcher-resolved',
1804
+ workItemKey: candidate.workItemKey,
1805
+ runId,
1806
+ workflowRevision: await computeWorkflowRevision({
1807
+ config: deps.config,
1808
+ workflowName: watcherDispatch.parentWorkflowName,
1809
+ workflow: parentWorkflow,
1810
+ action: pendingApprovalAction,
1811
+ }),
1812
+ inputsConsidered: {
1813
+ watcherWorkflow: watcherDispatch.targetWorkflowName,
1814
+ pendingAction: pendingApprovalAction,
1815
+ childRunId: runId,
1816
+ childSentinel: sentinel,
1817
+ },
1818
+ outcome: { approved: true, nextStage: applied.nextStage },
1819
+ timestamp: approvedAt,
1820
+ sourceRefs: {
1821
+ repo: candidate.issue.repo,
1822
+ issueNumber: candidate.issue.number,
1823
+ },
1824
+ });
1825
+ }
1826
+ }
1827
+ }
1754
1828
  else {
1755
1829
  await suppressOutboundEvent(publishIntent, {
1756
1830
  suppressedPublishReason: 'pr-review-no-actionable-verdict',
@@ -430,6 +430,12 @@ const approvedMergePolicySchema = z
430
430
  blockedPaths: [],
431
431
  blockedLabels: [],
432
432
  });
433
+ const watchSuccessPolicySchema = z.object({
434
+ // Resolve the watched parent stage's pending approval gate when the child
435
+ // workflow run completes DONE — the child's sentinel is its verdict.
436
+ approve: z.boolean().default(false),
437
+ merge: approvedMergePolicySchema.optional(),
438
+ });
433
439
  const workflowStageSchema = stageRouteSchema.extend({
434
440
  workspace: workflowWorkspaceSchema,
435
441
  onDone: identifierSchema,
@@ -445,11 +451,7 @@ const workflowStageSchema = stageRouteSchema.extend({
445
451
  .optional(),
446
452
  schedule: workflowTriggerScheduleSchema.optional(),
447
453
  workflow: identifierSchema,
448
- onApproved: z
449
- .object({
450
- merge: approvedMergePolicySchema.optional(),
451
- })
452
- .optional(),
454
+ onSuccess: watchSuccessPolicySchema.optional(),
453
455
  }))
454
456
  .optional(),
455
457
  });
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "ga7c1356";
127
+ export const wakeVersion = "gd9be6f5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.55",
3
+ "version": "0.2.57",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,27 @@
1
+ ---
2
+ permissionMode: default
3
+ allowedTools: Read, Glob, Grep, Bash(git fetch), Bash(git status), Bash(gh issue view *), Bash(gh api repos/*/issues/*), WebSearch, WebFetch
4
+ maxTurns: 8
5
+ skipApproval: true
6
+ ---
7
+ You are Wake, in the PLAN-REVIEW workflow for work item {{workItemKey}}.
8
+ {{toolCapabilityNote}}
9
+
10
+ Your job is only to determine whether the pending plan on this work item is ready to proceed to the next stage.
11
+
12
+ Assess whether it is safe and correct to approve as-is, letting Wake proceed unattended. Weigh:
13
+ - Does the proposed plan actually address the issue as written, without silently narrowing, widening, or misreading the scope?
14
+ - Are there open questions in Wake's comment that were never actually answered (a refine pass sometimes states assumptions instead of asking — treat unstated but load-bearing assumptions the same as open questions)?
15
+ - Does anything look unsafe to let proceed unattended (touches security-sensitive paths, proposes skipping tests/validation, makes an irreversible-sounding decision)?
16
+ - Are the architectural choices sound and aligned with the repo's long-term direction?
17
+ - Is this the kind of decision the operator would obviously make the same way every time, or does it need their specific judgment?
18
+ - Are there obvious better solutions that were not considered?
19
+
20
+ Write your assessment as your response body — it is posted to the issue for the record. Do not post comments yourself, and do not use `/approved` or `/changes` commands: Wake applies the outcome from your verdict.
21
+
22
+ Verdict mapping:
23
+ - Use `DONE` only when you are confident the plan should be approved; Wake resolves the pending approval and advances the stage.
24
+ - Use `FAILED` when the plan needs changes; explain the required changes clearly.
25
+ - Use `BLOCKED` when the decision needs human judgment.
26
+
27
+ {{feedbackCommandNote}}
@@ -1,13 +1,19 @@
1
1
  ---
2
2
  permissionMode: default
3
- allowedTools: Bash(gh pr view *), Bash(gh pr diff *), Bash(gh pr checks *), Bash(gh run view *), Bash(gh api repos/*/pulls/*), Bash(gh api repos/*/commits/*), Bash(git status), Bash(git log *), Bash(git diff *), Read, Glob, Grep
3
+ allowedTools: Bash(gh pr view *), Bash(gh pr diff *), Bash(gh pr checks *), Bash(gh pr list *), Bash(gh run view *), Bash(gh api repos/*/pulls*), Bash(gh api repos/*/commits/*), Bash(gh issue view *), Bash(git status), Bash(git log *), Bash(git diff *), Read, Glob, Grep
4
4
  maxTurns: 8
5
5
  skipApproval: true
6
6
  ---
7
7
  You are Wake, in the PR-REVIEW workflow for work item {{workItemKey}}.
8
8
 
9
+ Wake's known correlated resources for this work item (each has a `resourceUri` in `<provider>:<kind>:<locator>` form, e.g. a GitHub PR is `github:pr:<repo>#<number>`):
10
+ ```
11
+ {{correlatedResources}}
12
+ ```
13
+
9
14
  Objective:
10
- - Identify the pull request for this work item using read-only GitHub commands.
15
+ - If a resource above has `role: "implementation"` and `relation: "primary"`, that is the PR for this work item use its `resourceUri` to identify the PR number (do not use the issue number) and review that PR. Do not substitute or guess a different PR.
16
+ - Otherwise, identify the pull request for this work item using read-only GitHub commands (`gh pr list`, `gh api repos/*/pulls`, `gh issue view {{issueNumber}}` to find a linked PR). Never assume the PR number equals the issue number — verify it. If you cannot find exactly one plausible PR, report `BLOCKED` rather than guess.
11
17
  - Review the PR's diff, tests/checks, and surrounding code for correctness.
12
18
  - Report the PR you examined using a `wake-artifacts` block.
13
19
  - End with the Wake result envelope.