@diff-review-system/drs 4.1.0 → 5.0.0-rc.1

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 (145) hide show
  1. package/.pi/agents/task/okf-wiki-maintainer.md +72 -0
  2. package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +2 -0
  3. package/.pi/workflows/github-pr-review-post.yaml +29 -19
  4. package/.pi/workflows/github-pr-review.yaml +23 -0
  5. package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +2 -0
  6. package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +2 -0
  7. package/.pi/workflows/gitlab-mr-review.yaml +6 -0
  8. package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +2 -0
  9. package/.pi/workflows/release-changelog-finalize.yaml +10 -0
  10. package/.pi/workflows/repository-wiki-check.yaml +40 -0
  11. package/.pi/workflows/repository-wiki-sync.yaml +187 -0
  12. package/.wiki-site/.vitepress/config.mts +452 -0
  13. package/.wiki-site/.vitepress/theme/PageLead.vue +122 -0
  14. package/.wiki-site/.vitepress/theme/index.ts +12 -0
  15. package/.wiki-site/.vitepress/theme/style.css +270 -0
  16. package/README.md +38 -4
  17. package/dist/cli/index.js +3 -127
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/init.d.ts.map +1 -1
  20. package/dist/cli/init.js +0 -3
  21. package/dist/cli/init.js.map +1 -1
  22. package/dist/cli/run-agent.d.ts +3 -0
  23. package/dist/cli/run-agent.d.ts.map +1 -1
  24. package/dist/cli/run-agent.js +56 -5
  25. package/dist/cli/run-agent.js.map +1 -1
  26. package/dist/cli/wiki.d.ts +3 -0
  27. package/dist/cli/wiki.d.ts.map +1 -0
  28. package/dist/cli/wiki.js +174 -0
  29. package/dist/cli/wiki.js.map +1 -0
  30. package/dist/cli/workflow.d.ts +2 -0
  31. package/dist/cli/workflow.d.ts.map +1 -1
  32. package/dist/cli/workflow.js +344 -31
  33. package/dist/cli/workflow.js.map +1 -1
  34. package/dist/github/client.d.ts.map +1 -1
  35. package/dist/github/client.js +2 -2
  36. package/dist/github/client.js.map +1 -1
  37. package/dist/github/platform-adapter.d.ts.map +1 -1
  38. package/dist/github/platform-adapter.js +15 -1
  39. package/dist/github/platform-adapter.js.map +1 -1
  40. package/dist/gitlab/client.d.ts +4 -0
  41. package/dist/gitlab/client.d.ts.map +1 -1
  42. package/dist/gitlab/client.js +17 -1
  43. package/dist/gitlab/client.js.map +1 -1
  44. package/dist/gitlab/platform-adapter.d.ts.map +1 -1
  45. package/dist/gitlab/platform-adapter.js +17 -1
  46. package/dist/gitlab/platform-adapter.js.map +1 -1
  47. package/dist/lib/agent-permissions.d.ts +44 -0
  48. package/dist/lib/agent-permissions.d.ts.map +1 -0
  49. package/dist/lib/agent-permissions.js +358 -0
  50. package/dist/lib/agent-permissions.js.map +1 -0
  51. package/dist/lib/agent-result.d.ts +2 -0
  52. package/dist/lib/agent-result.d.ts.map +1 -1
  53. package/dist/lib/comment-poster.d.ts +1 -1
  54. package/dist/lib/comment-poster.d.ts.map +1 -1
  55. package/dist/lib/comment-poster.js +38 -16
  56. package/dist/lib/comment-poster.js.map +1 -1
  57. package/dist/lib/config.d.ts +6 -1
  58. package/dist/lib/config.d.ts.map +1 -1
  59. package/dist/lib/config.js +17 -0
  60. package/dist/lib/config.js.map +1 -1
  61. package/dist/lib/error-comment-poster.d.ts +1 -1
  62. package/dist/lib/error-comment-poster.d.ts.map +1 -1
  63. package/dist/lib/error-comment-poster.js +15 -7
  64. package/dist/lib/error-comment-poster.js.map +1 -1
  65. package/dist/lib/okf-wiki.d.ts +65 -0
  66. package/dist/lib/okf-wiki.d.ts.map +1 -0
  67. package/dist/lib/okf-wiki.js +647 -0
  68. package/dist/lib/okf-wiki.js.map +1 -0
  69. package/dist/lib/platform-client.d.ts +2 -0
  70. package/dist/lib/platform-client.d.ts.map +1 -1
  71. package/dist/lib/project-setup.d.ts +0 -3
  72. package/dist/lib/project-setup.d.ts.map +1 -1
  73. package/dist/lib/project-setup.js +1 -15
  74. package/dist/lib/project-setup.js.map +1 -1
  75. package/dist/lib/review-artifact.d.ts +11 -1
  76. package/dist/lib/review-artifact.d.ts.map +1 -1
  77. package/dist/lib/review-artifact.js +202 -0
  78. package/dist/lib/review-artifact.js.map +1 -1
  79. package/dist/lib/review-orchestrator.d.ts +6 -1
  80. package/dist/lib/review-orchestrator.d.ts.map +1 -1
  81. package/dist/lib/review-orchestrator.js +3 -1
  82. package/dist/lib/review-orchestrator.js.map +1 -1
  83. package/dist/lib/wiki-delta.d.ts +71 -0
  84. package/dist/lib/wiki-delta.d.ts.map +1 -0
  85. package/dist/lib/wiki-delta.js +524 -0
  86. package/dist/lib/wiki-delta.js.map +1 -0
  87. package/dist/lib/wiki-run-summary.d.ts +45 -0
  88. package/dist/lib/wiki-run-summary.d.ts.map +1 -0
  89. package/dist/lib/wiki-run-summary.js +168 -0
  90. package/dist/lib/wiki-run-summary.js.map +1 -0
  91. package/dist/lib/wiki-search.d.ts +24 -0
  92. package/dist/lib/wiki-search.d.ts.map +1 -0
  93. package/dist/lib/wiki-search.js +200 -0
  94. package/dist/lib/wiki-search.js.map +1 -0
  95. package/dist/lib/wiki-site-graph.d.ts +33 -0
  96. package/dist/lib/wiki-site-graph.d.ts.map +1 -0
  97. package/dist/lib/wiki-site-graph.js +446 -0
  98. package/dist/lib/wiki-site-graph.js.map +1 -0
  99. package/dist/lib/wiki-site-safety.d.ts +6 -0
  100. package/dist/lib/wiki-site-safety.d.ts.map +1 -0
  101. package/dist/lib/wiki-site-safety.js +83 -0
  102. package/dist/lib/wiki-site-safety.js.map +1 -0
  103. package/dist/lib/wiki-site-smoke.d.ts +16 -0
  104. package/dist/lib/wiki-site-smoke.d.ts.map +1 -0
  105. package/dist/lib/wiki-site-smoke.js +270 -0
  106. package/dist/lib/wiki-site-smoke.js.map +1 -0
  107. package/dist/lib/wiki-site.d.ts +26 -0
  108. package/dist/lib/wiki-site.d.ts.map +1 -0
  109. package/dist/lib/wiki-site.js +221 -0
  110. package/dist/lib/wiki-site.js.map +1 -0
  111. package/dist/lib/workflow/planning.d.ts +1 -0
  112. package/dist/lib/workflow/planning.d.ts.map +1 -1
  113. package/dist/lib/workflow/planning.js +53 -2
  114. package/dist/lib/workflow/planning.js.map +1 -1
  115. package/dist/lib/workflow/types.d.ts +5 -0
  116. package/dist/lib/workflow/types.d.ts.map +1 -1
  117. package/dist/lib/workflow-artifacts.d.ts.map +1 -1
  118. package/dist/lib/workflow-artifacts.js +23 -1
  119. package/dist/lib/workflow-artifacts.js.map +1 -1
  120. package/dist/pi/sdk.d.ts.map +1 -1
  121. package/dist/pi/sdk.js +270 -17
  122. package/dist/pi/sdk.js.map +1 -1
  123. package/dist/runtime/built-in-paths.d.ts +0 -1
  124. package/dist/runtime/built-in-paths.d.ts.map +1 -1
  125. package/dist/runtime/built-in-paths.js +0 -7
  126. package/dist/runtime/built-in-paths.js.map +1 -1
  127. package/dist/runtime/client.d.ts +3 -0
  128. package/dist/runtime/client.d.ts.map +1 -1
  129. package/dist/runtime/client.js +4 -0
  130. package/dist/runtime/client.js.map +1 -1
  131. package/dist/temporal/executor.d.ts.map +1 -1
  132. package/dist/temporal/executor.js +9 -2
  133. package/dist/temporal/executor.js.map +1 -1
  134. package/dist/temporal/retry-policy.d.ts +1 -1
  135. package/dist/temporal/retry-policy.d.ts.map +1 -1
  136. package/dist/temporal/retry-policy.js +6 -1
  137. package/dist/temporal/retry-policy.js.map +1 -1
  138. package/dist/temporal/workflows.d.ts.map +1 -1
  139. package/dist/temporal/workflows.js +16 -3
  140. package/dist/temporal/workflows.js.map +1 -1
  141. package/package.json +23 -13
  142. package/dist/lib/skills.d.ts +0 -16
  143. package/dist/lib/skills.d.ts.map +0 -1
  144. package/dist/lib/skills.js +0 -140
  145. package/dist/lib/skills.js.map +0 -1
@@ -16,17 +16,21 @@ import { resolveCursorFixLinkOptions } from '../lib/cursor-fix-link.js';
16
16
  import { extractHtmlDocument, parseArtifactOutputPointer, readArtifactOutputPointer, validateHtmlArtifact, } from '../lib/html-artifact.js';
17
17
  import { getCanonicalDiffCommand, resolveBaseBranch } from '../lib/repository-validator.js';
18
18
  import { formatCodeQualityReport, generateCodeQualityReport } from '../lib/code-quality-report.js';
19
+ import { formatOkfValidationErrors, synchronizeOkfIndexes, validateOkfBundle, } from '../lib/okf-wiki.js';
20
+ import { checkWikiClean, planWikiUpdate, recordWikiState, } from '../lib/wiki-delta.js';
21
+ import { createWikiRunSummary, formatWikiRunSummaryHuman, formatWikiRunSummaryMarkdown, getWikiRunSummary, } from '../lib/wiki-run-summary.js';
19
22
  import { createGitHubClient } from '../github/client.js';
20
23
  import { GitHubPlatformAdapter } from '../github/platform-adapter.js';
21
24
  import { createGitLabClient } from '../gitlab/client.js';
22
25
  import { GitLabPlatformAdapter } from '../gitlab/platform-adapter.js';
23
26
  import { loadWorkflowArtifact, saveWorkflowArtifact, updateWorkflowArtifact, workflowArtifactExists, } from '../lib/workflow-artifacts.js';
24
- import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
27
+ import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, reviewArtifactToReviewResult, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
28
+ import { renderAgentPermissions, renderAgentValidation } from '../lib/agent-permissions.js';
25
29
  import { runAgent } from './run-agent.js';
26
30
  import { TraceCollector } from '../lib/trace-collector.js';
27
31
  import { renderTraceHtml } from '../lib/trace-html.js';
28
32
  import { buildWorkflowGraph, formatWorkflowGraphMermaid, } from '../lib/workflow/graph.js';
29
- import { computeActiveWorkflowNodes, createSkippedWorkflowNodeResult, findWorkflowSegmentIndex, getNodeKind, getNodeNeeds, getWorkflowExecutionOrder, getWorkflowExecutionWaves, getWorkflowNodeSkipReason, getWorkflowNodes, hasWorkflowControlNodes, normalizeWorkflowBooleanLike, renderTemplate, runControlWorkflowNode, splitWorkflowSegments, } from '../lib/workflow/planning.js';
33
+ import { computeActiveWorkflowNodes, createSkippedWorkflowNodeResult, findWorkflowSegmentIndex, getNodeKind, getNodeNeeds, getWorkflowExecutionOrder, getWorkflowExecutionWaves, getWorkflowNodeSkipReason, getWorkflowNodes, hasWorkflowControlNodes, isPotentialWorkspaceMutation, normalizeWorkflowBooleanLike, renderTemplate, runControlWorkflowNode, splitWorkflowSegments, } from '../lib/workflow/planning.js';
30
34
  import { getWorkflowInputConfigType } from '../lib/workflow/input.js';
31
35
  export { compileWorkflowPlan, } from '../lib/workflow/compiled-plan.js';
32
36
  function createWorkflowLock() {
@@ -186,7 +190,7 @@ function hasConfiguredAgentPrompt(config, agentId) {
186
190
  const runConfig = resolveAgentRunConfig(config, agentId);
187
191
  return runConfig.prompt !== undefined || runConfig.promptFile !== undefined;
188
192
  }
189
- function createAgentOptions(prompt, options, workingDir) {
193
+ function createAgentOptions(prompt, options, workingDir, node, context) {
190
194
  return {
191
195
  prompt,
192
196
  jsonOutput: false,
@@ -196,6 +200,16 @@ function createAgentOptions(prompt, options, workingDir) {
196
200
  quiet: true,
197
201
  allowImplicitStdin: false,
198
202
  ignoreConfiguredOutput: true,
203
+ ...(node.permissions
204
+ ? {
205
+ permissions: renderAgentPermissions(node.permissions, (value) => renderTemplate(value, context)),
206
+ }
207
+ : {}),
208
+ ...(node.validation
209
+ ? {
210
+ validation: renderAgentValidation(node.validation, (value) => renderTemplate(value, context)),
211
+ }
212
+ : {}),
199
213
  };
200
214
  }
201
215
  async function writeWorkflowFile(workingDir, relativeOutputPath, content) {
@@ -255,7 +269,7 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
255
269
  throw new Error(`Workflow agent node "${nodeId}" must define input or configure ` +
256
270
  `agents.overrides.${agentId}.run.prompt/promptFile.`);
257
271
  }
258
- const agentOptions = createAgentOptions(prompt, options, workingDir);
272
+ const agentOptions = createAgentOptions(prompt, options, workingDir, node, context);
259
273
  if (executionContext?.traceCollector && prompt) {
260
274
  agentOptions.traceCollector = executionContext.traceCollector;
261
275
  executionContext.traceCollector.setContext(nodeId, agentId, prompt);
@@ -273,6 +287,8 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
273
287
  type: 'agent',
274
288
  agent: agentId,
275
289
  response: result.response,
290
+ usage: result.usage,
291
+ workspaceChanges: result.workspaceChanges,
276
292
  output,
277
293
  writes,
278
294
  };
@@ -291,7 +307,7 @@ async function runAgentsWorkflowNode(config, nodeId, node, options, workingDir,
291
307
  `agents.overrides.${missingPromptAgent}.run.prompt/promptFile.`);
292
308
  }
293
309
  }
294
- const responses = await Promise.all(agentIds.map((agentId) => runAgent(config, agentId, createAgentOptions(prompt, options, workingDir))));
310
+ const responses = await Promise.all(agentIds.map((agentId) => runAgent(config, agentId, createAgentOptions(prompt, options, workingDir, node, context))));
295
311
  const response = responses
296
312
  .map((result) => `## ${result.agent}\n\n${result.response.trim()}`.trim())
297
313
  .join('\n\n');
@@ -392,6 +408,27 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
392
408
  if (node.action === 'code-quality-report') {
393
409
  return runCodeQualityReportWorkflowNode(nodeId, node, workingDir, context);
394
410
  }
411
+ if (node.action === 'plan-wiki-update') {
412
+ return runPlanWikiUpdateWorkflowNode(nodeId, node, workingDir, context);
413
+ }
414
+ if (node.action === 'sync-okf-indexes') {
415
+ return runSyncOkfIndexesWorkflowNode(nodeId, node, workingDir, context);
416
+ }
417
+ if (node.action === 'validate-okf-wiki') {
418
+ return runValidateOkfWikiWorkflowNode(nodeId, node, workingDir, context);
419
+ }
420
+ if (node.action === 'summarize-wiki-run') {
421
+ return runSummarizeWikiRunWorkflowNode(nodeId, node, context);
422
+ }
423
+ if (node.action === 'record-wiki-state') {
424
+ return runRecordWikiStateWorkflowNode(nodeId, node, workingDir, context);
425
+ }
426
+ if (node.action === 'check-wiki-state') {
427
+ return runCheckWikiStateWorkflowNode(nodeId, node, workingDir, context);
428
+ }
429
+ if (node.action === 'check-wiki-clean') {
430
+ return runCheckWikiCleanWorkflowNode(nodeId, node, workingDir, context);
431
+ }
395
432
  if (node.action === 'post-comment') {
396
433
  return runPostCommentWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
397
434
  }
@@ -545,13 +582,45 @@ async function runGitBranchWorkflowNode(nodeId, node, workingDir, context, execu
545
582
  async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext) {
546
583
  const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
547
584
  const message = requireStringActionOption(nodeId, node, 'message', context);
585
+ const useChangeRequestAuthor = getBooleanActionOption(node, 'useChangeRequestAuthor', context);
586
+ let commitGit = git;
587
+ if (useChangeRequestAuthor) {
588
+ const configuredSource = getStringActionOption(node, 'source', context)?.trim();
589
+ const sourceArtifact = configuredSource ?? 'change';
590
+ const source = context.artifacts[sourceArtifact];
591
+ const platform = isReviewSource(source) && typeof source.context.platform === 'string'
592
+ ? source.context.platform
593
+ : undefined;
594
+ const pullRequest = isReviewSource(source) && isPullRequest(source.context.pullRequest)
595
+ ? source.context.pullRequest
596
+ : undefined;
597
+ const name = pullRequest?.author.trim();
598
+ const email = pullRequest?.authorEmail?.trim();
599
+ if (!isWorkflowPlatform(platform) ||
600
+ !name ||
601
+ name === 'Unknown' ||
602
+ /[\0\r\n]/.test(name) ||
603
+ !email ||
604
+ /[\0\r\n]/.test(email) ||
605
+ !/^[^\s<>@]+@[^\s<>@]+$/.test(email)) {
606
+ throw new Error(`Workflow git-commit node "${nodeId}" with.useChangeRequestAuthor requires source artifact "${sourceArtifact}" to contain a GitHub PR or GitLab MR creator identity.`);
607
+ }
608
+ const environment = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
609
+ commitGit = simpleGit({ baseDir: workingDir }).env({
610
+ ...environment,
611
+ GIT_AUTHOR_NAME: name,
612
+ GIT_AUTHOR_EMAIL: email,
613
+ GIT_COMMITTER_NAME: name,
614
+ GIT_COMMITTER_EMAIL: email,
615
+ });
616
+ }
548
617
  const paths = hasActionOption(node, 'paths') || hasActionOption(node, 'path')
549
618
  ? getPathActionOption(nodeId, node, context, workingDir)
550
619
  : undefined;
551
620
  if (paths) {
552
621
  await git.add(paths);
553
622
  }
554
- const commit = paths ? await git.commit(message, paths) : await git.commit(message);
623
+ const commit = paths ? await commitGit.commit(message, paths) : await commitGit.commit(message);
555
624
  const output = {
556
625
  commit: commit.commit,
557
626
  message,
@@ -1124,16 +1193,59 @@ function createPlatformChangeSource(platform, name, projectId, pullRequest, chan
1124
1193
  workingDir,
1125
1194
  };
1126
1195
  }
1196
+ function getPatchChangeCounts(patch) {
1197
+ let additions = 0;
1198
+ let deletions = 0;
1199
+ for (const line of patch.split('\n')) {
1200
+ if (line.startsWith('+'))
1201
+ additions += 1;
1202
+ if (line.startsWith('-'))
1203
+ deletions += 1;
1204
+ }
1205
+ return { additions, deletions };
1206
+ }
1127
1207
  async function loadGitHubChangeSource(nodeId, node, workingDir, context, executionContext) {
1128
1208
  const owner = requireStringActionOption(nodeId, node, 'owner', context);
1129
1209
  const repo = requireStringActionOption(nodeId, node, 'repo', context);
1130
1210
  const prNumber = requireNumberActionOption(nodeId, node, 'pr', context);
1131
1211
  const projectId = `${owner}/${repo}`;
1132
1212
  const platformClient = getWorkflowPlatformClient(executionContext, 'github');
1133
- const [pullRequest, changedFiles] = await Promise.all([
1134
- platformClient.getPullRequest(projectId, prNumber),
1135
- platformClient.getChangedFiles(projectId, prNumber),
1136
- ]);
1213
+ const requireCompleteDiff = getBooleanActionOption(node, 'requireCompleteDiff', context);
1214
+ let pullRequest;
1215
+ let changedFiles;
1216
+ if (requireCompleteDiff) {
1217
+ const before = await platformClient.getPullRequest(projectId, prNumber);
1218
+ changedFiles = await platformClient.getChangedFiles(projectId, prNumber);
1219
+ const after = await platformClient.getPullRequest(projectId, prNumber);
1220
+ if (before.headSha !== after.headSha) {
1221
+ throw new Error(`Workflow change-source node "${nodeId}" cannot review an unstable pull request head.`);
1222
+ }
1223
+ const platformData = after.platformData;
1224
+ const expectedFileCount = platformData && typeof platformData === 'object' && 'changed_files' in platformData
1225
+ ? platformData.changed_files
1226
+ : undefined;
1227
+ if (typeof expectedFileCount !== 'number' ||
1228
+ !Number.isInteger(expectedFileCount) ||
1229
+ expectedFileCount !== changedFiles.length) {
1230
+ throw new Error(`Workflow change-source node "${nodeId}" did not receive the complete GitHub file list.`);
1231
+ }
1232
+ const incompleteFiles = changedFiles.filter((file) => {
1233
+ if (!file.patch || file.patch.length === 0)
1234
+ return true;
1235
+ const counts = getPatchChangeCounts(file.patch);
1236
+ return counts.additions !== file.additions || counts.deletions !== file.deletions;
1237
+ });
1238
+ if (incompleteFiles.length > 0) {
1239
+ throw new Error(`Workflow change-source node "${nodeId}" did not receive complete patches for: ${incompleteFiles.map((file) => file.filename).join(', ')}`);
1240
+ }
1241
+ pullRequest = after;
1242
+ }
1243
+ else {
1244
+ [pullRequest, changedFiles] = await Promise.all([
1245
+ platformClient.getPullRequest(projectId, prNumber),
1246
+ platformClient.getChangedFiles(projectId, prNumber),
1247
+ ]);
1248
+ }
1137
1249
  return createPlatformChangeSource('github', `GitHub PR ${projectId}#${prNumber}`, projectId, pullRequest, changedFiles, workingDir);
1138
1250
  }
1139
1251
  async function loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext) {
@@ -1556,6 +1668,154 @@ async function runCodeQualityReportWorkflowNode(nodeId, node, workingDir, contex
1556
1668
  writes: reportPath,
1557
1669
  };
1558
1670
  }
1671
+ async function runSyncOkfIndexesWorkflowNode(nodeId, node, workingDir, context) {
1672
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1673
+ const version = getStringActionOption(node, 'version', context)?.trim() ?? '0.1';
1674
+ const result = await synchronizeOkfIndexes(workingDir, root, version);
1675
+ return {
1676
+ id: nodeId,
1677
+ type: 'action',
1678
+ action: node.action,
1679
+ response: `synchronized ${result.indexes} OKF indexes under ${result.root} (${result.updated} updated)`,
1680
+ output: result,
1681
+ };
1682
+ }
1683
+ function getWikiInstructionsActionOptions(node, context) {
1684
+ const instructionsPath = getStringActionOption(node, 'instructionsPath', context)?.trim();
1685
+ const instructions = getStringActionOption(node, 'instructions', context);
1686
+ return {
1687
+ ...(instructionsPath ? { instructionsPath } : {}),
1688
+ ...(instructions !== undefined ? { instructions } : {}),
1689
+ };
1690
+ }
1691
+ async function runPlanWikiUpdateWorkflowNode(nodeId, node, workingDir, context) {
1692
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1693
+ const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
1694
+ const result = await planWikiUpdate(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
1695
+ return {
1696
+ id: nodeId,
1697
+ type: 'action',
1698
+ action: node.action,
1699
+ response: `${result.mode}: ${result.reason}`,
1700
+ output: result,
1701
+ };
1702
+ }
1703
+ async function runValidateOkfWikiWorkflowNode(nodeId, node, workingDir, context) {
1704
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1705
+ const version = getStringActionOption(node, 'version', context)?.trim() ?? '0.1';
1706
+ const result = await validateOkfBundle(workingDir, root, version);
1707
+ if (!result.valid) {
1708
+ throw new Error(`Workflow validate-okf-wiki node "${nodeId}" found ${result.errors.length} error(s):\n${formatOkfValidationErrors(result)}`);
1709
+ }
1710
+ return {
1711
+ id: nodeId,
1712
+ type: 'action',
1713
+ action: node.action,
1714
+ response: `validated ${result.concepts} OKF concepts under ${result.root} with ` +
1715
+ `${result.warnings.length} warning(s); graph: ${result.graph.nodeCount} nodes, ` +
1716
+ `${result.graph.directedEdgeCount} directed edges, ` +
1717
+ `${result.graph.orphanConceptCount} orphans, ` +
1718
+ `${result.graph.weaklyConnectedConceptCount} weakly connected`,
1719
+ output: result,
1720
+ };
1721
+ }
1722
+ function runSummarizeWikiRunWorkflowNode(nodeId, node, context) {
1723
+ const planName = getStringActionOption(node, 'plan', context)?.trim() ?? 'wikiDelta';
1724
+ const validationName = getStringActionOption(node, 'validation', context)?.trim() ?? 'wikiValidation';
1725
+ const agentNodeId = getStringActionOption(node, 'agentNode', context)?.trim() ?? 'maintain-wiki';
1726
+ const plan = requireWikiUpdatePlan(context.artifacts[planName], nodeId, planName);
1727
+ const validation = requireWikiValidation(context.artifacts[validationName], nodeId, validationName);
1728
+ const agentNode = context.nodes[agentNodeId];
1729
+ if (plan.shouldRun &&
1730
+ (!agentNode || agentNode.status === 'skipped' || agentNode.type !== 'agent')) {
1731
+ throw new Error(`Workflow summarize-wiki-run node "${nodeId}" expected completed agent node "${agentNodeId}".`);
1732
+ }
1733
+ const startedAt = context.startedAt ? Date.parse(context.startedAt) : Date.now();
1734
+ const summary = createWikiRunSummary({
1735
+ plan,
1736
+ validation,
1737
+ modelInvoked: plan.shouldRun,
1738
+ usage: plan.shouldRun ? agentNode?.usage : undefined,
1739
+ workspaceChanges: plan.shouldRun ? agentNode?.workspaceChanges : undefined,
1740
+ elapsedMs: Date.now() - (Number.isFinite(startedAt) ? startedAt : Date.now()),
1741
+ });
1742
+ const response = formatWikiRunSummaryHuman(summary);
1743
+ return {
1744
+ id: nodeId,
1745
+ type: 'action',
1746
+ action: node.action,
1747
+ response,
1748
+ output: { ...validation, summary, summaryMarkdown: formatWikiRunSummaryMarkdown(summary) },
1749
+ };
1750
+ }
1751
+ function requireWikiUpdatePlan(value, nodeId, artifactName) {
1752
+ if (!isRecord(value) ||
1753
+ !['generate', 'reconcile', 'update', 'noop'].includes(String(value.mode)) ||
1754
+ typeof value.shouldRun !== 'boolean' ||
1755
+ typeof value.changedPathCount !== 'number') {
1756
+ throw new Error(`Workflow summarize-wiki-run node "${nodeId}" needs a wiki update plan artifact at "${artifactName}".`);
1757
+ }
1758
+ return value;
1759
+ }
1760
+ function requireWikiValidation(value, nodeId, artifactName) {
1761
+ if (!isRecord(value) ||
1762
+ typeof value.valid !== 'boolean' ||
1763
+ typeof value.root !== 'string' ||
1764
+ typeof value.concepts !== 'number' ||
1765
+ !Array.isArray(value.errors) ||
1766
+ !Array.isArray(value.warnings) ||
1767
+ !isRecord(value.graph)) {
1768
+ throw new Error(`Workflow summarize-wiki-run node "${nodeId}" needs an OKF validation artifact at "${artifactName}".`);
1769
+ }
1770
+ return value;
1771
+ }
1772
+ function isRecord(value) {
1773
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1774
+ }
1775
+ async function runRecordWikiStateWorkflowNode(nodeId, node, workingDir, context) {
1776
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1777
+ const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
1778
+ const result = await recordWikiState(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
1779
+ return {
1780
+ id: nodeId,
1781
+ type: 'action',
1782
+ action: node.action,
1783
+ response: `recorded wiki state for ${result.root} at ${result.gitHead}`,
1784
+ output: result,
1785
+ writes: statePath,
1786
+ };
1787
+ }
1788
+ async function runCheckWikiCleanWorkflowNode(nodeId, node, workingDir, context) {
1789
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1790
+ const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
1791
+ const result = await checkWikiClean(workingDir, root, statePath);
1792
+ if (!result.clean) {
1793
+ throw new Error(`Workflow check-wiki-clean node "${nodeId}" found stale wiki output:\n${result.changedPaths.map((filePath) => `- ${filePath}`).join('\n')}`);
1794
+ }
1795
+ return {
1796
+ id: nodeId,
1797
+ type: 'action',
1798
+ action: node.action,
1799
+ response: `wiki bundle ${result.root} and state ${result.statePath} are current`,
1800
+ output: result,
1801
+ };
1802
+ }
1803
+ async function runCheckWikiStateWorkflowNode(nodeId, node, workingDir, context) {
1804
+ const root = getStringActionOption(node, 'root', context)?.trim() ?? 'wiki';
1805
+ const statePath = getStringActionOption(node, 'statePath', context)?.trim() ?? '.drs/wiki-state.json';
1806
+ const result = await planWikiUpdate(workingDir, root, statePath, getWikiInstructionsActionOptions(node, context));
1807
+ if (result.shouldRun) {
1808
+ const paths = result.changedPaths.map((filePath) => `- ${filePath}`).join('\n');
1809
+ throw new Error(`Workflow check-wiki-state node "${nodeId}" found a stale wiki (${result.mode}): ${result.reason}${paths ? `\n${paths}` : ''}`);
1810
+ }
1811
+ return {
1812
+ id: nodeId,
1813
+ type: 'action',
1814
+ action: node.action,
1815
+ response: result.reason,
1816
+ output: result,
1817
+ };
1818
+ }
1559
1819
  async function runDescribeWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
1560
1820
  const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
1561
1821
  const source = context.artifacts[sourceArtifact];
@@ -1648,15 +1908,35 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
1648
1908
  const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
1649
1909
  const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
1650
1910
  const source = context.artifacts[sourceArtifact];
1651
- const reviewResult = context.artifacts[reviewArtifact];
1911
+ const reviewValue = context.artifacts[reviewArtifact];
1652
1912
  if (!isReviewSource(source)) {
1653
1913
  throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewSource artifact.`);
1654
1914
  }
1655
- if (!isReviewResult(reviewResult)) {
1656
- throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewResult artifact.`);
1657
- }
1658
1915
  const target = resolvePostTarget(nodeId, node, context, executionContext, source);
1659
1916
  const pullRequest = target.pullRequest;
1917
+ const canonicalArtifactMode = hasActionOption(node, 'expectedHeadSha');
1918
+ const expectedHeadSha = getStringActionOption(node, 'expectedHeadSha', context)?.trim();
1919
+ let reviewResult;
1920
+ if (canonicalArtifactMode) {
1921
+ if (!expectedHeadSha || !pullRequest) {
1922
+ throw new Error(`Workflow post-review-comments node "${nodeId}" needs pull request metadata and a non-empty expectedHeadSha to validate a canonical review artifact.`);
1923
+ }
1924
+ reviewResult = reviewArtifactToReviewResult(reviewValue, {
1925
+ platform: target.platform,
1926
+ projectId: target.projectId,
1927
+ changeKind: target.platform === 'gitlab' ? 'mr' : 'pr',
1928
+ changeNumber: target.prNumber,
1929
+ expectedHeadSha,
1930
+ currentHeadSha: pullRequest.headSha,
1931
+ changedFiles: source.files,
1932
+ });
1933
+ }
1934
+ else if (isReviewResult(reviewValue)) {
1935
+ reviewResult = reviewValue;
1936
+ }
1937
+ else {
1938
+ throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewResult artifact or a canonical review artifact with expectedHeadSha.`);
1939
+ }
1660
1940
  const platformData = pullRequest?.platformData;
1661
1941
  const lineValidator = createWorkflowLineValidator(target.platform, source);
1662
1942
  const createInlinePosition = lineValidator
@@ -1664,10 +1944,15 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
1664
1944
  : undefined;
1665
1945
  const shouldRemoveErrorComment = !hasActionOption(node, 'removeErrorComment') ||
1666
1946
  getBooleanActionOption(node, 'removeErrorComment', context);
1667
- await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
1668
- if (shouldRemoveErrorComment) {
1669
- await removeErrorComment(target.platformClient, target.projectId, target.prNumber);
1947
+ const assertCurrentHead = expectedHeadSha
1948
+ ? async () => {
1949
+ const latestPullRequest = await target.platformClient.getPullRequest(target.projectId, target.prNumber);
1950
+ if (latestPullRequest.headSha !== expectedHeadSha) {
1951
+ throw new Error('Pull request head changed before review comments could be posted.');
1952
+ }
1670
1953
  }
1954
+ : undefined;
1955
+ await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
1671
1956
  const cursorFixLinks = resolveCursorFixLinkOptions(config, target.projectId, workingDir);
1672
1957
  await postReviewComments(target.platformClient, target.projectId, target.prNumber, reviewResult.summary, reviewResult.issues, reviewResult.changeSummary, reviewResult.usage, platformData, lineValidator, createInlinePosition, cursorFixLinks, pullRequest
1673
1958
  ? {
@@ -1675,6 +1960,8 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
1675
1960
  sourceBranch: pullRequest.sourceBranch,
1676
1961
  targetBranch: pullRequest.targetBranch,
1677
1962
  }
1963
+ : undefined, assertCurrentHead, shouldRemoveErrorComment
1964
+ ? () => removeErrorComment(target.platformClient, target.projectId, target.prNumber, assertCurrentHead)
1678
1965
  : undefined);
1679
1966
  });
1680
1967
  return {
@@ -2004,11 +2291,17 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
2004
2291
  console.warn = () => undefined;
2005
2292
  }
2006
2293
  try {
2007
- return await executeReview(config, {
2294
+ const reviewSource = {
2008
2295
  ...sourceForReview,
2009
2296
  workingDir: sourceForReview.workingDir ?? workingDir,
2010
2297
  debug: options.debug,
2011
2298
  thinkingLevel: options.thinkingLevel,
2299
+ };
2300
+ if (!node.permissions) {
2301
+ return await executeReview(config, reviewSource);
2302
+ }
2303
+ return await executeReview(config, reviewSource, {
2304
+ permissions: renderAgentPermissions(node.permissions, (value) => renderTemplate(value, context)),
2012
2305
  });
2013
2306
  }
2014
2307
  finally {
@@ -2214,16 +2507,21 @@ async function runSingleWorkflowNode(config, nodeId, node, options, workingDir,
2214
2507
  if (getWorkflowNodeSkipReason(node, context)) {
2215
2508
  return createSkippedWorkflowNodeResult(nodeId);
2216
2509
  }
2217
- if (kind === 'agent') {
2218
- return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2219
- }
2220
- if (kind === 'agents') {
2221
- return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
2222
- }
2223
- if (kind === 'control') {
2224
- throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
2225
- }
2226
- return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2510
+ const run = async () => {
2511
+ if (kind === 'agent') {
2512
+ return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2513
+ }
2514
+ if (kind === 'agents') {
2515
+ return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
2516
+ }
2517
+ if (kind === 'control') {
2518
+ throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
2519
+ }
2520
+ return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
2521
+ };
2522
+ return isPotentialWorkspaceMutation(node)
2523
+ ? withWorkflowLock(executionContext.locks.workspace, run)
2524
+ : run();
2227
2525
  }
2228
2526
  /**
2229
2527
  * In-process {@link NodeExecutor} that dispatches a single non-control node
@@ -2256,6 +2554,7 @@ export async function runWorkflowNodeLocally(config, nodeId, node, options, work
2256
2554
  locks: {
2257
2555
  exit: createWorkflowLock(),
2258
2556
  console: createWorkflowLock(),
2557
+ workspace: createWorkflowLock(),
2259
2558
  },
2260
2559
  };
2261
2560
  return runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
@@ -2289,6 +2588,7 @@ export async function runWorkflowFromCompiledPlan(config, plan, options = {}) {
2289
2588
  return executeWorkflowRun(config, plan.workflowName, plan.nodes, plan.executionOrder, plan.waves, plan.output, false, options);
2290
2589
  }
2291
2590
  async function executeWorkflowRun(config, workflowName, workflowNodes, executionOrder, executionWaves, workflowOutput, hasControl, options, controlSegments) {
2591
+ const startedAt = new Date().toISOString();
2292
2592
  const workflow = config.workflows?.[workflowName];
2293
2593
  if (!workflow) {
2294
2594
  throw new Error(`Unknown workflow "${workflowName}".`);
@@ -2298,7 +2598,7 @@ async function executeWorkflowRun(config, workflowName, workflowNodes, execution
2298
2598
  const nodes = {};
2299
2599
  const artifacts = {};
2300
2600
  const loop = {};
2301
- const context = { inputs, nodes, artifacts, loop };
2601
+ const context = { startedAt, inputs, nodes, artifacts, loop };
2302
2602
  const executionContext = {
2303
2603
  gitClients: new Map(),
2304
2604
  platformClients: {},
@@ -2306,6 +2606,7 @@ async function executeWorkflowRun(config, workflowName, workflowNodes, execution
2306
2606
  locks: {
2307
2607
  exit: createWorkflowLock(),
2308
2608
  console: createWorkflowLock(),
2609
+ workspace: createWorkflowLock(),
2309
2610
  },
2310
2611
  };
2311
2612
  const nodeExecutor = new LocalNodeExecutor(config, options, workingDir, executionContext);
@@ -2397,8 +2698,14 @@ async function executeWorkflowRun(config, workflowName, workflowNodes, execution
2397
2698
  if (options.jsonOutput) {
2398
2699
  console.log(formatWorkflowJson(result));
2399
2700
  }
2400
- else if (typeof result.output === 'string' && result.output.trim()) {
2401
- console.log(`\n${result.output}`);
2701
+ else {
2702
+ const wikiSummary = getWikiRunSummary(result.output);
2703
+ if (wikiSummary) {
2704
+ console.log(`\n${formatWikiRunSummaryHuman(wikiSummary)}`);
2705
+ }
2706
+ else if (typeof result.output === 'string' && result.output.trim()) {
2707
+ console.log(`\n${result.output}`);
2708
+ }
2402
2709
  }
2403
2710
  return result;
2404
2711
  }
@@ -2500,6 +2807,8 @@ function buildWorkflowDetail(name, workflow, workingDir) {
2500
2807
  output: node.output,
2501
2808
  writes: node.writes,
2502
2809
  json: node.json,
2810
+ permissions: node.permissions,
2811
+ validation: node.validation,
2503
2812
  routes: getWorkflowNodeRoutes(node),
2504
2813
  })),
2505
2814
  graph,
@@ -2587,6 +2896,10 @@ export function showWorkflow(config, workflowName, options = {}) {
2587
2896
  console.log(` output: ${node.output}`);
2588
2897
  if (node.writes)
2589
2898
  console.log(` writes: ${node.writes}`);
2899
+ if (node.permissions)
2900
+ console.log(` permissions: ${JSON.stringify(node.permissions)}`);
2901
+ if (node.validation)
2902
+ console.log(` validation: ${JSON.stringify(node.validation)}`);
2590
2903
  if (node.input)
2591
2904
  console.log(` input: ${node.input.split('\n')[0]}`);
2592
2905
  if (node.with && Object.keys(node.with).length > 0) {