@atolis-hq/wake 0.2.40 → 0.2.42

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.
@@ -1,4 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
+ import { autoApprovalLabel, resolveAutoApprovalIntent } from '../../core/approval-intents.js';
2
3
  import { defaultAgentIdentity } from '../../domain/schema.js';
3
4
  import { buildResourceUri } from '../../domain/resource-uri.js';
4
5
  import { wakeStageLabelPrefix } from '../../domain/stages.js';
@@ -18,8 +19,6 @@ const pollOverlapMs = 60 * 60 * 1000;
18
19
  // expectedEcho bookkeeping surviving a crash (#145).
19
20
  const wakeCommentMarker = '<!-- wake:agent -->';
20
21
  const githubSource = 'github';
21
- const autoApprovalLabel = 'wake:auto';
22
- const autoApprovalCommands = new Set(['yolo', 'autoapprove']);
23
22
  export function wakeIdempotencyMarker(idempotencyKey) {
24
23
  return typeof idempotencyKey === 'string'
25
24
  ? `<!-- wake:idempotency ${idempotencyKey} -->`
@@ -138,15 +137,6 @@ function issueAssignees(issue) {
138
137
  .map((assignee) => assignee.login)
139
138
  .filter((login) => typeof login === 'string');
140
139
  }
141
- function autoApprovalCommandName(body) {
142
- for (const line of (body ?? '').split(/\r?\n/)) {
143
- const match = /^\/([A-Za-z0-9_.-]+)\b/.exec(line.trim());
144
- if (match?.[1] !== undefined && autoApprovalCommands.has(match[1].toLowerCase())) {
145
- return match[1].toLowerCase();
146
- }
147
- }
148
- return null;
149
- }
150
140
  function issueWithAutoApprovalLabel(issue) {
151
141
  return {
152
142
  ...issue,
@@ -382,7 +372,7 @@ export function createGitHubIssuesWorkSource(deps) {
382
372
  continue;
383
373
  }
384
374
  if (!autoApprovalLabelAdded &&
385
- autoApprovalCommandName(comment.body) !== null &&
375
+ resolveAutoApprovalIntent(comment.body) !== null &&
386
376
  comment.user?.type !== 'Bot' &&
387
377
  !(comment.body ?? '').includes(wakeCommentMarker) &&
388
378
  (deps.selfLogin === undefined || comment.user?.login !== deps.selfLogin)) {
@@ -466,7 +456,7 @@ export function createGitHubIssuesWorkSource(deps) {
466
456
  continue;
467
457
  }
468
458
  if (!autoApprovalLabelAdded &&
469
- autoApprovalCommandName(comment.body) !== null &&
459
+ resolveAutoApprovalIntent(comment.body) !== null &&
470
460
  comment.user?.type !== 'Bot' &&
471
461
  !(comment.body ?? '').includes(wakeCommentMarker) &&
472
462
  (deps.selfLogin === undefined || comment.user?.login !== deps.selfLogin)) {
@@ -0,0 +1,36 @@
1
+ import { AUTONOMOUS_DECISION_AUDIT_EVENT } from '../domain/schema.js';
2
+ function formatValue(value) {
3
+ if (value === undefined)
4
+ return 'n/a';
5
+ if (typeof value === 'string')
6
+ return value;
7
+ return JSON.stringify(value);
8
+ }
9
+ function auditEventsForWorkItem(events, workItemKey) {
10
+ return events
11
+ .filter((event) => event.workItemKey === workItemKey &&
12
+ event.sourceEventType === AUTONOMOUS_DECISION_AUDIT_EVENT)
13
+ .sort((left, right) => left.ingestedAt.localeCompare(right.ingestedAt));
14
+ }
15
+ export async function runAuditCommand(input) {
16
+ const log = input.log ?? console.log;
17
+ const [workItemKey] = input.args;
18
+ if (workItemKey === undefined) {
19
+ throw new Error('Usage: wake audit <workItemKey>');
20
+ }
21
+ const events = auditEventsForWorkItem(await input.stateStore.listEventEnvelopes(), workItemKey);
22
+ if (events.length === 0) {
23
+ log(`No autonomous audit events found for ${workItemKey}.`);
24
+ return;
25
+ }
26
+ log(`Autonomous audit history for ${workItemKey}`);
27
+ for (const event of events) {
28
+ const payload = event.payload;
29
+ log('');
30
+ log(`${formatValue(payload.timestamp)} ${formatValue(payload.decisionType)}`);
31
+ log(` runId: ${formatValue(payload.runId)}`);
32
+ log(` workflowRevision: ${formatValue(payload.workflowRevision)}`);
33
+ log(` inputsConsidered: ${formatValue(payload.inputsConsidered)}`);
34
+ log(` outcome: ${formatValue(payload.outcome)}`);
35
+ }
36
+ }
@@ -0,0 +1,16 @@
1
+ export const autoApprovalLabel = 'wake:auto';
2
+ const autoApprovalCommands = new Set(['yolo', 'autoapprove']);
3
+ export function resolveAutoApprovalIntent(body) {
4
+ for (const line of (body ?? '').split(/\r?\n/)) {
5
+ const match = /^\/([A-Za-z0-9_.-]+)\b/.exec(line.trim());
6
+ const command = match?.[1]?.toLowerCase();
7
+ if (command !== undefined && autoApprovalCommands.has(command)) {
8
+ return {
9
+ kind: 'auto-approval-opt-in',
10
+ label: autoApprovalLabel,
11
+ command,
12
+ };
13
+ }
14
+ }
15
+ return null;
16
+ }
@@ -0,0 +1,82 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { AUTONOMOUS_DECISION_AUDIT_EVENT } from '../domain/schema.js';
5
+ import { createEventEnvelope } from '../lib/event-log.js';
6
+ function stableJson(value) {
7
+ if (value === null || typeof value !== 'object') {
8
+ return JSON.stringify(value);
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
12
+ }
13
+ const record = value;
14
+ return `{${Object.keys(record)
15
+ .sort()
16
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
17
+ .join(',')}}`;
18
+ }
19
+ async function promptRevisionInput(input) {
20
+ if (input.action === undefined) {
21
+ return {};
22
+ }
23
+ const promptsRoot = input.config.paths.promptsRoot;
24
+ if (promptsRoot === undefined) {
25
+ return { prompt: { action: input.action, status: 'not-configured' } };
26
+ }
27
+ for (const suffix of ['.md', '.start.md', '.resume.md']) {
28
+ const path = join(promptsRoot, `${input.action}${suffix}`);
29
+ try {
30
+ return {
31
+ prompt: {
32
+ action: input.action,
33
+ path,
34
+ sha256: createHash('sha256')
35
+ .update(await readFile(path, 'utf8'))
36
+ .digest('hex'),
37
+ },
38
+ };
39
+ }
40
+ catch {
41
+ // Try the next supported prompt template name.
42
+ }
43
+ }
44
+ return { prompt: { action: input.action, status: 'missing' } };
45
+ }
46
+ export async function workflowRevision(input) {
47
+ const revisionInput = {
48
+ workflowName: input.workflowName,
49
+ workflow: input.workflow,
50
+ ...(await promptRevisionInput({
51
+ config: input.config,
52
+ ...(input.action === undefined ? {} : { action: input.action }),
53
+ })),
54
+ };
55
+ return `sha256:${createHash('sha256').update(stableJson(revisionInput)).digest('hex')}`;
56
+ }
57
+ export function createAutonomousDecisionAuditEvent(input) {
58
+ return createEventEnvelope({
59
+ eventId: input.eventId,
60
+ workItemKey: input.workItemKey,
61
+ streamScope: 'work-item',
62
+ direction: 'internal',
63
+ sourceSystem: 'wake',
64
+ sourceEventType: AUTONOMOUS_DECISION_AUDIT_EVENT,
65
+ sourceRefs: {
66
+ ...(input.sourceRefs ?? {}),
67
+ runId: input.runId,
68
+ },
69
+ occurredAt: input.timestamp,
70
+ ingestedAt: input.timestamp,
71
+ trigger: 'context-only',
72
+ payload: {
73
+ decisionType: input.decisionType,
74
+ workItemId: input.workItemKey,
75
+ runId: input.runId,
76
+ workflowRevision: input.workflowRevision,
77
+ inputsConsidered: input.inputsConsidered,
78
+ outcome: input.outcome,
79
+ timestamp: input.timestamp,
80
+ },
81
+ });
82
+ }
@@ -2,6 +2,7 @@ import { awaitingApprovalRunnerSentinel, failedRunnerSentinel } from '../domain/
2
2
  import { resolveCustomCommand } from '../domain/custom-commands.js';
3
3
  import { builtInDefaultWorkflowDefinition, chooseAction as chooseWorkflowAction, isKnownWorkflowStage, selectWorkflowForEvent, workflowForProjection, } from '../domain/workflows.js';
4
4
  import { alwaysManualIgnoredLabels } from '../domain/manual-labels.js';
5
+ import { autoApprovalLabel } from './approval-intents.js';
5
6
  function isAwaitingApproval(issue) {
6
7
  const context = issue.context;
7
8
  return context.lastRunSentinel === awaitingApprovalRunnerSentinel;
@@ -23,7 +24,6 @@ const approvedCommandPattern = /^\/approved\b/i;
23
24
  const changesCommandPattern = /^\/changes\b/i;
24
25
  const prReviewApprovalMarker = '<!-- wake:pr-review-approved -->';
25
26
  const prReviewChangesMarker = '<!-- wake:pr-review-changes-requested -->';
26
- const autoApprovalLabel = 'wake:auto';
27
27
  // The action Wake runs when a correlated PR gets new reviewer feedback while
28
28
  // the work item is awaiting approval. Not configurable per workflow: it's a
29
29
  // lateral response to a PR surface, not a workflow stage.
@@ -17,6 +17,7 @@ import { createOutbox } from './outbox.js';
17
17
  import { createEventResolver } from './event-resolver.js';
18
18
  import { createStaleRunReconciler } from './stale-run-reconciler.js';
19
19
  import { createWorkspaceCleanup } from './workspace-cleanup.js';
20
+ import { createAutonomousDecisionAuditEvent, workflowRevision as computeWorkflowRevision, } from './audit-events.js';
20
21
  import { createRunLease, isRunLeaseExpired, renewRunLease, runLeaseRenewalIntervalMs, } from './run-lease.js';
21
22
  import { currentProcessIdentity, processIdentityMatches } from '../lib/process-identity.js';
22
23
  import { readJsonFile, writeJsonFile } from '../lib/json-file.js';
@@ -172,6 +173,9 @@ export function createTickRunner(deps) {
172
173
  function eventStampNow() {
173
174
  return deps.clock.now().toISOString();
174
175
  }
176
+ async function appendAuditEvent(input) {
177
+ await deps.stateStore.appendEventEnvelope(createAutonomousDecisionAuditEvent(input));
178
+ }
175
179
  // The watchlist is every resource currently correlated to an open work
176
180
  // item, deduplicated by exact resourceUri. It is derived once per tick from
177
181
  // the pre-poll projection snapshot (see runTick's ordering note) and handed
@@ -309,6 +313,38 @@ export function createTickRunner(deps) {
309
313
  await retryUnconfirmedDeliveries();
310
314
  const watchlistProjections = await deps.stateStore.listIssueStates();
311
315
  const inboundEvents = await ingestInboundEvents(await deps.workSource.pollEvents({ watch: deriveWatchlist(watchlistProjections) }));
316
+ for (const event of inboundEvents) {
317
+ const trigger = event.payload.trigger;
318
+ const workflowName = typeof event.payload.workflow === 'string' ? event.payload.workflow : undefined;
319
+ const workflow = workflowName === undefined ? undefined : deps.config.workflows[workflowName];
320
+ if (trigger?.kind !== 'schedule' || workflowName === undefined || workflow === undefined) {
321
+ continue;
322
+ }
323
+ const timestamp = eventStampNow();
324
+ await appendAuditEvent({
325
+ eventId: `${event.eventId}-audit-trigger-fired`,
326
+ decisionType: 'trigger.fired',
327
+ workItemKey: event.workItemKey,
328
+ runId: event.eventId,
329
+ workflowRevision: await computeWorkflowRevision({
330
+ config: deps.config,
331
+ workflowName,
332
+ workflow,
333
+ }),
334
+ inputsConsidered: {
335
+ workflow: workflowName,
336
+ schedule: workflow.trigger?.schedule,
337
+ trigger,
338
+ sourceEventId: event.eventId,
339
+ },
340
+ outcome: {
341
+ fired: true,
342
+ sourceEventType: event.sourceEventType,
343
+ },
344
+ timestamp,
345
+ sourceRefs: event.sourceRefs,
346
+ });
347
+ }
312
348
  const projections = await deps.stateStore.listIssueStates();
313
349
  await cleanupClosedIssueWorkspaces(projections);
314
350
  if (inboundEvents.length > 0) {
@@ -661,6 +697,35 @@ export function createTickRunner(deps) {
661
697
  },
662
698
  });
663
699
  await deps.stateStore.appendEventEnvelope(approvalCompletedEvent);
700
+ if (automaticApproval) {
701
+ await appendAuditEvent({
702
+ eventId: `${approvalId}-audit-auto-resolution`,
703
+ decisionType: 'approval.auto-resolved',
704
+ workItemKey: candidate.workItemKey,
705
+ runId: approvalId,
706
+ workflowRevision: await computeWorkflowRevision({
707
+ config: deps.config,
708
+ workflowName,
709
+ workflow,
710
+ action: approvalResolution.pendingAction,
711
+ }),
712
+ inputsConsidered: {
713
+ labels: candidate.issue.labels,
714
+ pendingAction: approvalResolution.pendingAction,
715
+ pendingApprovalAllowAutoApproval: candidate.context.pendingApprovalAllowAutoApproval === true,
716
+ },
717
+ outcome: {
718
+ approved: true,
719
+ nextStage,
720
+ reason: approvalResolution.reason,
721
+ },
722
+ timestamp: approvedAt,
723
+ sourceRefs: {
724
+ repo: candidate.issue.repo,
725
+ issueNumber: candidate.issue.number,
726
+ },
727
+ });
728
+ }
664
729
  await projectionUpdater.rebuildFromEvents([approvalCompletedEvent]);
665
730
  await deliverOutboundEvent(createLabelsEvent({
666
731
  projection: candidate,
@@ -791,6 +856,38 @@ export function createTickRunner(deps) {
791
856
  },
792
857
  });
793
858
  await deps.stateStore.appendEventEnvelope(claimedEvent);
859
+ if (watcherRun && watcherDispatch !== null && watcherTriggerForRun !== undefined) {
860
+ await appendAuditEvent({
861
+ eventId: `${runId}-audit-watcher-dispatched`,
862
+ decisionType: 'watcher.dispatched',
863
+ workItemKey: candidate.workItemKey,
864
+ runId,
865
+ workflowRevision: await computeWorkflowRevision({
866
+ config: deps.config,
867
+ workflowName,
868
+ workflow: deps.config.workflows[workflowName] ?? workflow,
869
+ action,
870
+ }),
871
+ inputsConsidered: {
872
+ parentWorkflowName: watcherDispatch.parentWorkflowName,
873
+ parentStage: watcherDispatch.parentStage,
874
+ watcherIndex: watcherDispatch.watcherIndex,
875
+ watcherStatus: watcherStatus(candidate),
876
+ trigger: watcherTriggerForRun,
877
+ },
878
+ outcome: {
879
+ dispatched: true,
880
+ targetWorkflowName: workflowName,
881
+ action,
882
+ claimedStage,
883
+ },
884
+ timestamp: claimedAt,
885
+ sourceRefs: {
886
+ repo: candidate.issue.repo,
887
+ issueNumber: candidate.issue.number,
888
+ },
889
+ });
890
+ }
794
891
  await transitionRunLifecycle('CLAIMED');
795
892
  await projectionUpdater.rebuildFromEvents([claimedEvent]);
796
893
  if (watcherStateKeyForRun !== undefined && watcherTriggerForRun !== undefined) {
@@ -907,6 +1004,44 @@ export function createTickRunner(deps) {
907
1004
  runnerResult,
908
1005
  occurredAt: finishedAt,
909
1006
  });
1007
+ await appendAuditEvent({
1008
+ eventId: `${runId}-audit-review-verdict`,
1009
+ decisionType: 'review.verdict',
1010
+ workItemKey: candidate.workItemKey,
1011
+ runId,
1012
+ workflowRevision: await computeWorkflowRevision({
1013
+ config: deps.config,
1014
+ workflowName,
1015
+ workflow: deps.config.workflows[workflowName] ?? workflow,
1016
+ action,
1017
+ }),
1018
+ inputsConsidered: {
1019
+ sourceRevision,
1020
+ watcherTrigger: watcherTriggerForRun,
1021
+ verifiedTargetResourceUri: prReviewTargetResourceUri,
1022
+ rawSentinel,
1023
+ },
1024
+ outcome: {
1025
+ sentinel,
1026
+ verdict: prReviewTargetResourceUri === null
1027
+ ? 'uncertain'
1028
+ : sentinel === 'DONE'
1029
+ ? 'approved'
1030
+ : sentinel === 'FAILED'
1031
+ ? 'changes-requested'
1032
+ : 'uncertain',
1033
+ reasoning: parsedRunnerResult.body,
1034
+ runner: {
1035
+ model: runnerResult.model,
1036
+ cli: runnerResult.cli,
1037
+ },
1038
+ },
1039
+ timestamp: finishedAt,
1040
+ sourceRefs: {
1041
+ repo: candidate.issue.repo,
1042
+ issueNumber: candidate.issue.number,
1043
+ },
1044
+ });
910
1045
  }
911
1046
  else if (workspaceMode === 'branch') {
912
1047
  await registerReportedArtifacts({
@@ -210,6 +210,7 @@ export const WORK_ITEM_CREATED_EVENT = 'wake.workitem.created';
210
210
  export const CORRELATION_REGISTERED_EVENT = 'wake.correlation.registered';
211
211
  export const CORRELATION_RETRACTED_EVENT = 'wake.correlation.retracted';
212
212
  export const CORRELATION_PRIMARY_CONFLICT_EVENT = 'wake.correlation.primary-conflict';
213
+ export const AUTONOMOUS_DECISION_AUDIT_EVENT = 'wake.audit.autonomous-decision';
213
214
  /**
214
215
  * Shared key for events whose resource failed mint qualification (spec D1').
215
216
  * Never a real work item: `readIssueState(UNRESOLVED_WORK_ITEM_KEY)` always
package/dist/src/main.js CHANGED
@@ -19,6 +19,7 @@ import { createGitHubArtifactVerifier } from './adapters/github/github-artifact-
19
19
  import { createGitHubClient } from './adapters/github/github-client.js';
20
20
  import { createGitHubIssuesWorkSource } from './adapters/github/github-issues-work-source.js';
21
21
  import { createGitHubPullRequestActivitySource } from './adapters/github/github-pull-request-activity-source.js';
22
+ import { runAuditCommand } from './cli/audit-command.js';
22
23
  import { runCorrelateCommand } from './cli/correlate-command.js';
23
24
  import { runDoctorCommand } from './cli/doctor-command.js';
24
25
  import { runInitCommand } from './cli/init-command.js';
@@ -701,6 +702,15 @@ async function runCorrelate(args) {
701
702
  readFlag: readFlagBeforeCommandTerminator,
702
703
  });
703
704
  }
705
+ async function runAudit(args) {
706
+ const wakeRoot = resolve(readFlagBeforeCommandTerminator('--wake-root', args) ?? process.cwd());
707
+ const stateStore = createStateStore({ wakeRoot });
708
+ await stateStore.ensureWakeRoot();
709
+ await runAuditCommand({
710
+ args,
711
+ stateStore,
712
+ });
713
+ }
704
714
  async function runSmoke(args) {
705
715
  const runtime = await buildRuntime(args);
706
716
  const explicitKind = args[0] === 'claude' || args[0] === 'codex' || args[0] === 'cursor' ? args[0] : undefined;
@@ -783,6 +793,7 @@ export function printUsage(stream) {
783
793
  ' wake stop Stop the sandbox container gracefully',
784
794
  ' wake smoke Smoke-test the configured runner',
785
795
  ' wake ui Run the control-plane UI server',
796
+ ' wake audit Show autonomous decision audit history',
786
797
  ' wake correlate Manually correlate a resource to a work item',
787
798
  ' wake doctor Diagnose config/GitHub/Docker/sandbox setup problems',
788
799
  ' wake --version Print the installed Wake version',
@@ -792,14 +803,22 @@ export function printUsage(stream) {
792
803
  ' 1. wake init ./wake-home',
793
804
  ' 2. cd wake-home && wake start',
794
805
  '',
795
- 'Runtime commands (tick/start/ui/smoke/correlate/validate-state) auto-delegate into the sandbox',
806
+ 'Runtime commands (tick/start/ui/smoke/audit/correlate/validate-state) auto-delegate into the sandbox',
796
807
  'when docker/Dockerfile exists at --wake-root (i.e. after `wake sandbox build`),',
797
808
  'defaulting --wake-root to the current directory. Pass --no-sandbox to run',
798
809
  'directly on the host instead.',
799
810
  '',
800
811
  ].join('\n'));
801
812
  }
802
- const runtimeCommands = new Set(['tick', 'start', 'ui', 'smoke', 'correlate', 'validate-state']);
813
+ const runtimeCommands = new Set([
814
+ 'tick',
815
+ 'start',
816
+ 'ui',
817
+ 'smoke',
818
+ 'audit',
819
+ 'correlate',
820
+ 'validate-state',
821
+ ]);
803
822
  export async function dispatchMainCommand(input) {
804
823
  const command = input.args[0] ?? 'help';
805
824
  if (command === '--version' || command === '-v') {
@@ -855,6 +874,12 @@ export async function dispatchMainCommand(input) {
855
874
  else if (command === 'ui') {
856
875
  await input.runUi(hostArgs);
857
876
  }
877
+ else if (command === 'audit') {
878
+ if (input.runAudit === undefined) {
879
+ throw new CliUsageError('audit command is not available in this runtime');
880
+ }
881
+ await input.runAudit(hostArgs);
882
+ }
858
883
  else if (command === 'smoke') {
859
884
  await input.runSmoke(hostArgs);
860
885
  }
@@ -991,6 +1016,7 @@ async function main() {
991
1016
  runStart,
992
1017
  runSmoke,
993
1018
  runUi,
1019
+ runAudit,
994
1020
  runCorrelate,
995
1021
  runValidateState,
996
1022
  execIntoSandbox: async (commandArgs) => {
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g8ab2018";
127
+ export const wakeVersion = "g09bbeb0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.40",
3
+ "version": "0.2.42",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {