@fitlab-ai/agent-infra 0.8.2 → 0.8.3

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 (119) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/dist/lib/cp.js +1 -1
  3. package/dist/lib/decide.js +2 -2
  4. package/dist/lib/defaults.json +1 -0
  5. package/dist/lib/init.js +7 -7
  6. package/dist/lib/merge.js +1 -1
  7. package/dist/lib/prompt.js +1 -1
  8. package/dist/lib/run/index.js +7 -7
  9. package/dist/lib/run/prompt.js +1 -1
  10. package/dist/lib/sandbox/capture.js +5 -5
  11. package/dist/lib/sandbox/clipboard/bridge.js +48 -13
  12. package/dist/lib/sandbox/clipboard/index.js +5 -6
  13. package/dist/lib/sandbox/clipboard/linux.js +91 -0
  14. package/dist/lib/sandbox/clipboard/paths.js +1 -1
  15. package/dist/lib/sandbox/clipboard/win32.js +144 -0
  16. package/dist/lib/sandbox/commands/create.js +67 -65
  17. package/dist/lib/sandbox/commands/enter.js +9 -9
  18. package/dist/lib/sandbox/commands/list-running.js +2 -2
  19. package/dist/lib/sandbox/commands/ls.js +7 -7
  20. package/dist/lib/sandbox/commands/prune.js +8 -8
  21. package/dist/lib/sandbox/commands/rebuild.js +38 -50
  22. package/dist/lib/sandbox/commands/refresh.js +2 -2
  23. package/dist/lib/sandbox/commands/rm.js +11 -11
  24. package/dist/lib/sandbox/commands/show.js +4 -4
  25. package/dist/lib/sandbox/commands/start.js +4 -4
  26. package/dist/lib/sandbox/commands/vm.js +4 -4
  27. package/dist/lib/sandbox/config.js +10 -4
  28. package/dist/lib/sandbox/constants.js +4 -1
  29. package/dist/lib/sandbox/credentials.js +1 -1
  30. package/dist/lib/sandbox/dotfiles.js +1 -1
  31. package/dist/lib/sandbox/engine.js +3 -3
  32. package/dist/lib/sandbox/engines/index.js +5 -5
  33. package/dist/lib/sandbox/engines/wsl2-paths.js +1 -1
  34. package/dist/lib/sandbox/image-build.js +80 -0
  35. package/dist/lib/sandbox/image-prune.js +2 -2
  36. package/dist/lib/sandbox/index.js +10 -10
  37. package/dist/lib/sandbox/managed-fs.js +1 -1
  38. package/dist/lib/sandbox/readme-scaffold.js +1 -1
  39. package/dist/lib/sandbox/tools.js +2 -2
  40. package/dist/lib/server/adapters/feishu/index.js +10 -3
  41. package/dist/lib/server/adapters/feishu/renderer.js +88 -0
  42. package/dist/lib/server/adapters/feishu/transport.js +2 -13
  43. package/dist/lib/server/daemon.js +64 -43
  44. package/dist/lib/server/display.js +83 -0
  45. package/dist/lib/server/index.js +2 -2
  46. package/dist/lib/server/process-control.js +2 -2
  47. package/dist/lib/server/protocol.js +2 -2
  48. package/dist/lib/server/streamer.js +5 -4
  49. package/dist/lib/task/commands/cat.js +2 -2
  50. package/dist/lib/task/commands/decisions.js +4 -4
  51. package/dist/lib/task/commands/files.js +3 -3
  52. package/dist/lib/task/commands/grep.js +3 -3
  53. package/dist/lib/task/commands/issue-body.js +4 -4
  54. package/dist/lib/task/commands/log.js +5 -5
  55. package/dist/lib/task/commands/ls.js +3 -3
  56. package/dist/lib/task/commands/show.js +1 -1
  57. package/dist/lib/task/commands/status.js +71 -25
  58. package/dist/lib/task/index.js +9 -9
  59. package/dist/lib/task/resolve-ref.js +1 -1
  60. package/dist/lib/task/workflow-warnings.js +94 -0
  61. package/dist/lib/update.js +4 -4
  62. package/lib/defaults.json +1 -0
  63. package/lib/sandbox/clipboard/bridge.ts +50 -8
  64. package/lib/sandbox/clipboard/index.ts +5 -6
  65. package/lib/sandbox/clipboard/linux.ts +124 -0
  66. package/lib/sandbox/clipboard/win32.ts +173 -0
  67. package/lib/sandbox/commands/create.ts +84 -64
  68. package/lib/sandbox/commands/rebuild.ts +42 -54
  69. package/lib/sandbox/config.ts +13 -1
  70. package/lib/sandbox/constants.ts +4 -0
  71. package/lib/sandbox/image-build.ts +134 -0
  72. package/lib/server/adapters/_contract.ts +3 -0
  73. package/lib/server/adapters/feishu/index.ts +11 -3
  74. package/lib/server/adapters/feishu/renderer.ts +99 -0
  75. package/lib/server/adapters/feishu/transport.ts +5 -18
  76. package/lib/server/daemon.ts +74 -38
  77. package/lib/server/display.ts +136 -0
  78. package/lib/server/streamer.ts +5 -4
  79. package/lib/task/commands/log.ts +3 -3
  80. package/lib/task/commands/status.ts +102 -21
  81. package/lib/task/workflow-warnings.ts +121 -0
  82. package/package.json +2 -2
  83. package/templates/.agents/rules/create-issue.github.en.md +3 -3
  84. package/templates/.agents/rules/create-issue.github.zh-CN.md +3 -3
  85. package/templates/.agents/rules/issue-pr-commands.github.en.md +7 -2
  86. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +7 -2
  87. package/templates/.agents/rules/issue-sync.github.en.md +13 -0
  88. package/templates/.agents/rules/issue-sync.github.zh-CN.md +13 -0
  89. package/templates/.agents/rules/next-step-output.en.md +15 -1
  90. package/templates/.agents/rules/next-step-output.zh-CN.md +15 -1
  91. package/templates/.agents/rules/pr-sync.github.en.md +17 -2
  92. package/templates/.agents/rules/pr-sync.github.zh-CN.md +17 -2
  93. package/templates/.agents/rules/task-management.en.md +1 -1
  94. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  95. package/templates/.agents/scripts/platform-adapters/platform-sync.github.js +57 -0
  96. package/templates/.agents/scripts/validate-artifact.js +120 -0
  97. package/templates/.agents/scripts/workflow-warnings.js +290 -0
  98. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +118 -0
  99. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +118 -0
  100. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +49 -0
  101. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +49 -0
  102. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +48 -0
  103. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +48 -0
  104. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +60 -0
  105. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +87 -0
  106. package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
  107. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +2 -0
  108. package/templates/.agents/skills/create-task/SKILL.en.md +4 -1
  109. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -1
  110. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +7 -0
  111. package/templates/.agents/templates/task.en.md +7 -0
  112. package/templates/.agents/templates/task.zh-CN.md +7 -0
  113. package/templates/.claude/commands/complete-manual-validation.en.md +9 -0
  114. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +9 -0
  115. package/templates/.gemini/commands/_project_/complete-manual-validation.en.toml +8 -0
  116. package/templates/.gemini/commands/_project_/complete-manual-validation.zh-CN.toml +8 -0
  117. package/templates/.github/workflows/metadata-sync.yml +5 -0
  118. package/templates/.opencode/commands/complete-manual-validation.en.md +11 -0
  119. package/templates/.opencode/commands/complete-manual-validation.zh-CN.md +11 -0
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs';
2
- import { resolveTaskRef } from "../resolve-ref.js";
3
- import { extractTitle } from "../frontmatter.js";
4
- import { extractSection, findSectionHeading } from "../sections.js";
5
- import { renderTemplateBody, PLACEHOLDER } from "../issue-form.js";
2
+ import { resolveTaskRef } from '../resolve-ref.js';
3
+ import { extractTitle } from '../frontmatter.js';
4
+ import { extractSection, findSectionHeading } from '../sections.js';
5
+ import { renderTemplateBody, PLACEHOLDER } from '../issue-form.js';
6
6
  const USAGE = `Usage: ai task issue-body <N | #N | TASK-id> [--template <path>]
7
7
 
8
8
  Print a deterministic Issue body extracted from a task's task.md.
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
- import { formatTable } from "../../table.js";
3
- import { resolveTaskRef } from "../resolve-ref.js";
4
- import { parseLedger, HUMAN_DECISION_STATUSES } from "../ledger.js";
2
+ import { formatTable } from '../../table.js';
3
+ import { resolveTaskRef } from '../resolve-ref.js';
4
+ import { parseLedger } from '../ledger.js';
5
5
  const USAGE = `Usage: ai task log <N | #N | TASK-id>
6
6
 
7
7
  Renders a task's activity log as a per-step status table. A step's start and
@@ -13,7 +13,7 @@ Columns: # (row) / STEP / AGENT / STARTED / DONE / NOTE
13
13
  A human-executed step shows AGENT as 'human' and, when it has no start marker,
14
14
  a '-' STARTED placeholder. Review-step NOTE also carries two human counts in
15
15
  the verdict list, right after blockers/major/minor: manual-validation
16
- and human-decision (current ledger stage total).
16
+ and human-decision (current pending ledger stage total).
17
17
  `;
18
18
  const TABLE_HEADERS = ['#', 'STEP', 'AGENT', 'STARTED', 'DONE', 'NOTE'];
19
19
  // The activity-log H2 heading is language-dependent (zh template / en template).
@@ -100,7 +100,7 @@ function pairEntries(entries) {
100
100
  function countHumanDecisionsByStage(rows) {
101
101
  const counts = new Map();
102
102
  for (const row of rows) {
103
- if (!isReviewStage(row.stage) || !HUMAN_DECISION_STATUSES.has(row.status))
103
+ if (!isReviewStage(row.stage) || row.status !== 'needs-human-decision')
104
104
  continue;
105
105
  counts.set(row.stage, (counts.get(row.stage) ?? 0) + 1);
106
106
  }
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { formatTable } from "../../table.js";
5
- import { parseTaskFrontmatter, extractTitle } from "../frontmatter.js";
6
- import { loadShortIdByTaskId } from "../short-id.js";
4
+ import { formatTable } from '../../table.js';
5
+ import { parseTaskFrontmatter, extractTitle } from '../frontmatter.js';
6
+ import { loadShortIdByTaskId } from '../short-id.js';
7
7
  const USAGE = `Usage: ai task ls [--all | --blocked | --completed]
8
8
 
9
9
  Lists tasks under .agents/workspace/. Defaults to active tasks only.
@@ -1,5 +1,5 @@
1
1
  import fs from 'node:fs';
2
- import { resolveTaskRef } from "../resolve-ref.js";
2
+ import { resolveTaskRef } from '../resolve-ref.js';
3
3
  const USAGE = `Usage: ai task show <N | #N | TASK-id>
4
4
 
5
5
  Prints the task.md content for the matching task.
@@ -1,12 +1,14 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { commandForEngine } from "../../sandbox/shell.js";
5
- import { resolveTaskRef } from "../resolve-ref.js";
6
- import { enumerateArtifacts } from "../artifacts.js";
7
- import { parseTaskFrontmatter, extractTitle } from "../frontmatter.js";
8
- import { loadShortIdByTaskId } from "../short-id.js";
9
- import { parseActivityLog, pairEntries } from "./log.js";
4
+ import { commandForEngine } from '../../sandbox/shell.js';
5
+ import { resolveTaskRef } from '../resolve-ref.js';
6
+ import { enumerateArtifacts } from '../artifacts.js';
7
+ import { parseTaskFrontmatter, extractTitle } from '../frontmatter.js';
8
+ import { loadShortIdByTaskId } from '../short-id.js';
9
+ import { getOpenWorkflowWarnings, formatWorkflowWarningSummary } from '../workflow-warnings.js';
10
+ import { parseActivityLog, pairEntries } from './log.js';
11
+ import { statusCard } from '../../server/display.js';
10
12
  const USAGE = `Usage: ai task status <N | #N | TASK-id>
11
13
 
12
14
  Prints an aggregated "health check" view for a task: header, metadata,
@@ -267,6 +269,9 @@ function renderStatus(model) {
267
269
  if (model.title)
268
270
  lines.push(model.title);
269
271
  lines.push('', 'Metadata', ...renderPairs(model.metadata));
272
+ if (model.workflowWarnings.length > 0) {
273
+ lines.push('', `Workflow Warnings (${model.workflowWarnings.length} open)`, ...formatWorkflowWarningSummary(model.workflowWarnings).map((line) => ` ${line}`));
274
+ }
270
275
  lines.push('', `Artifacts (${model.artifacts.count})`);
271
276
  if (model.artifacts.groups.length === 0) {
272
277
  lines.push(' (none)');
@@ -302,6 +307,59 @@ function renderStatus(model) {
302
307
  ]));
303
308
  return lines;
304
309
  }
310
+ function modelTone(model) {
311
+ if (model.workflow.state === 'in-progress')
312
+ return model.workflow.stale === 'yes' ? 'warning' : 'running';
313
+ if (model.metadata.some(([key, value]) => key === 'status' && value === 'completed'))
314
+ return 'success';
315
+ if (model.metadata.some(([key, value]) => key === 'status' && (value === 'blocked' || value === 'cancelled'))) {
316
+ return 'danger';
317
+ }
318
+ return 'info';
319
+ }
320
+ function buildFromResolved(input) {
321
+ const content = fs.readFileSync(input.taskMdPath, 'utf8');
322
+ const fm = parseTaskFrontmatter(content);
323
+ const run = input.run ?? makeRunner(input.repoRoot);
324
+ const artifacts = enumerateArtifacts(input.taskDir);
325
+ const workflow = collectWorkflow(content, input.now);
326
+ return {
327
+ taskId: input.taskId,
328
+ shortId: input.shortId ?? loadShortIdByTaskId(input.repoRoot).get(input.taskId) ?? DASH,
329
+ title: extractTitle(content),
330
+ metadata: collectMetadata(fm),
331
+ workflowWarnings: getOpenWorkflowWarnings(content),
332
+ artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
333
+ workflow,
334
+ runtime: collectRuntime(input.taskDir, workflow, run),
335
+ git: collectGit(fm.branch ?? '', run)
336
+ };
337
+ }
338
+ function buildStatusModel(refOrInput, options = {}) {
339
+ if (typeof refOrInput !== 'string') {
340
+ return buildFromResolved(refOrInput);
341
+ }
342
+ const resolved = resolveTaskRef(refOrInput);
343
+ if (!resolved.ok) {
344
+ throw new Error(resolved.message);
345
+ }
346
+ return buildFromResolved({
347
+ taskId: resolved.taskId,
348
+ taskDir: resolved.taskDir,
349
+ taskMdPath: resolved.taskMdPath,
350
+ repoRoot: resolved.repoRoot,
351
+ run: options.run,
352
+ now: options.now
353
+ });
354
+ }
355
+ function statusModelToDisplay(model) {
356
+ return statusCard(`Task ${model.taskId} (${model.shortId})`, modelTone(model), [
357
+ ['workflow', model.workflow.state],
358
+ ['step', model.workflow.step],
359
+ ['runtime', model.runtime.status],
360
+ ['git', model.git.uncommitted]
361
+ ], renderStatus(model).join('\n'));
362
+ }
305
363
  function status(args = []) {
306
364
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
307
365
  process.stdout.write(USAGE);
@@ -309,30 +367,18 @@ function status(args = []) {
309
367
  process.exitCode = 1;
310
368
  return;
311
369
  }
312
- const resolved = resolveTaskRef(args[0]);
313
- if (!resolved.ok) {
314
- process.stderr.write(`ai task status: ${resolved.message}\n`);
370
+ let model;
371
+ try {
372
+ model = buildStatusModel(args[0]);
373
+ }
374
+ catch (error) {
375
+ process.stderr.write(`ai task status: ${error instanceof Error ? error.message : String(error)}\n`);
315
376
  process.exitCode = 1;
316
377
  return;
317
378
  }
318
- const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
319
- const fm = parseTaskFrontmatter(content);
320
- const run = makeRunner(resolved.repoRoot);
321
- const artifacts = enumerateArtifacts(resolved.taskDir);
322
- const workflow = collectWorkflow(content);
323
- const model = {
324
- taskId: resolved.taskId,
325
- shortId: loadShortIdByTaskId(resolved.repoRoot).get(resolved.taskId) ?? DASH,
326
- title: extractTitle(content),
327
- metadata: collectMetadata(fm),
328
- artifacts: { count: artifacts.length, groups: groupArtifacts(artifacts) },
329
- workflow,
330
- runtime: collectRuntime(resolved.taskDir, workflow, run),
331
- git: collectGit(fm.branch ?? '', run)
332
- };
333
379
  for (const line of renderStatus(model)) {
334
380
  process.stdout.write(`${line}\n`);
335
381
  }
336
382
  }
337
- export { status, makeRunner, collectMetadata, groupArtifacts, collectGit, collectWorkflow, collectRuntime, renderStatus, METADATA_KEYS };
383
+ export { status, makeRunner, buildStatusModel, collectMetadata, groupArtifacts, collectGit, collectWorkflow, collectRuntime, renderStatus, statusModelToDisplay, METADATA_KEYS };
338
384
  //# sourceMappingURL=status.js.map
@@ -41,48 +41,48 @@ export async function runTask(args) {
41
41
  }
42
42
  switch (subcommand) {
43
43
  case 'cat': {
44
- const { cat } = await import("./commands/cat.js");
44
+ const { cat } = await import('./commands/cat.js');
45
45
  cat(rest);
46
46
  break;
47
47
  }
48
48
  case 'decisions':
49
49
  case 'd': {
50
- const { decisions } = await import("./commands/decisions.js");
50
+ const { decisions } = await import('./commands/decisions.js');
51
51
  decisions(rest);
52
52
  break;
53
53
  }
54
54
  case 'files': {
55
- const { files } = await import("./commands/files.js");
55
+ const { files } = await import('./commands/files.js');
56
56
  files(rest);
57
57
  break;
58
58
  }
59
59
  case 'grep': {
60
- const { grep } = await import("./commands/grep.js");
60
+ const { grep } = await import('./commands/grep.js');
61
61
  grep(rest);
62
62
  break;
63
63
  }
64
64
  case 'issue-body': {
65
- const { issueBody } = await import("./commands/issue-body.js");
65
+ const { issueBody } = await import('./commands/issue-body.js');
66
66
  issueBody(rest);
67
67
  break;
68
68
  }
69
69
  case 'log': {
70
- const { log } = await import("./commands/log.js");
70
+ const { log } = await import('./commands/log.js');
71
71
  log(rest);
72
72
  break;
73
73
  }
74
74
  case 'ls': {
75
- const { ls } = await import("./commands/ls.js");
75
+ const { ls } = await import('./commands/ls.js');
76
76
  ls(rest);
77
77
  break;
78
78
  }
79
79
  case 'show': {
80
- const { show } = await import("./commands/show.js");
80
+ const { show } = await import('./commands/show.js');
81
81
  show(rest);
82
82
  break;
83
83
  }
84
84
  case 'status': {
85
- const { status } = await import("./commands/status.js");
85
+ const { status } = await import('./commands/status.js');
86
86
  status(rest);
87
87
  break;
88
88
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync, spawnSync } from 'node:child_process';
4
- import { normalizeShortIdInput } from "./short-id.js";
4
+ import { normalizeShortIdInput } from './short-id.js';
5
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
6
  // Flat-structured workspace dirs that hold tasks under `{dir}/{taskId}/task.md`.
7
7
  // Note: `archive` uses a three-level YYYY/MM/DD layout and is handled separately.
@@ -0,0 +1,94 @@
1
+ import { extractSection } from './sections.js';
2
+ const WORKFLOW_WARNING_HEADINGS = ['工作流告警', 'Workflow Warnings'];
3
+ const WORKFLOW_WARNING_STATUSES = new Set(['open', 'resolved', 'ignored']);
4
+ const WORKFLOW_WARNING_SEVERITIES = new Set(['IMPORTANT', 'ACTION_REQUIRED']);
5
+ function splitTableRow(line) {
6
+ let value = line.trim();
7
+ if (!value.startsWith('|'))
8
+ return [];
9
+ value = value.replace(/^\|/, '').replace(/\|$/, '');
10
+ const cells = [];
11
+ let cell = '';
12
+ for (let index = 0; index < value.length; index += 1) {
13
+ const char = value[index];
14
+ if (char === '|' && !isEscapedAt(value, index)) {
15
+ cells.push(unescapeTableCell(cell.trim()));
16
+ cell = '';
17
+ continue;
18
+ }
19
+ cell += char;
20
+ }
21
+ cells.push(unescapeTableCell(cell.trim()));
22
+ return cells;
23
+ }
24
+ function unescapeTableCell(value) {
25
+ let output = '';
26
+ for (let index = 0; index < value.length; index += 1) {
27
+ const char = value[index];
28
+ const next = value[index + 1];
29
+ if (char === '\\' && (next === '\\' || next === '|')) {
30
+ output += next;
31
+ index += 1;
32
+ continue;
33
+ }
34
+ output += char;
35
+ }
36
+ return output;
37
+ }
38
+ function isEscapedAt(value, index) {
39
+ let backslashes = 0;
40
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === '\\'; cursor -= 1) {
41
+ backslashes += 1;
42
+ }
43
+ return backslashes % 2 === 1;
44
+ }
45
+ function isSeparatorRow(cells) {
46
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
47
+ }
48
+ /**
49
+ * Parses well-formed workflow warning rows. Malformed rows are intentionally
50
+ * ignored here; validate-artifact is responsible for reporting structure errors.
51
+ */
52
+ function parseWorkflowWarnings(content) {
53
+ const section = extractSection(content, WORKFLOW_WARNING_HEADINGS);
54
+ if (!section)
55
+ return [];
56
+ const rows = [];
57
+ for (const rawLine of section.split(/\r?\n/)) {
58
+ const cells = splitTableRow(rawLine);
59
+ if (cells.length === 0)
60
+ continue;
61
+ if ((cells[0] ?? '').toLowerCase() === 'id')
62
+ continue;
63
+ if (isSeparatorRow(cells))
64
+ continue;
65
+ if (cells.length < 11)
66
+ continue;
67
+ rows.push({
68
+ id: cells[0] ?? '',
69
+ time: cells[1] ?? '',
70
+ step: cells[2] ?? '',
71
+ severity: cells[3] ?? '',
72
+ code: cells[4] ?? '',
73
+ status: cells[5] ?? '',
74
+ target: cells[6] ?? '',
75
+ message: cells[7] ?? '',
76
+ action: cells[8] ?? '',
77
+ resolvedAt: cells[9] ?? '',
78
+ resolution: cells[10] ?? ''
79
+ });
80
+ }
81
+ return rows;
82
+ }
83
+ function getOpenWorkflowWarnings(content) {
84
+ return parseWorkflowWarnings(content).filter((warning) => warning.status === 'open');
85
+ }
86
+ function formatWorkflowWarningSummary(warnings) {
87
+ return warnings.map((warning) => {
88
+ const target = warning.target ? ` ${warning.target}` : '';
89
+ const action = warning.action ? ` - ${warning.action}` : '';
90
+ return `${warning.id} [${warning.severity}] ${warning.code}${target}${action}`;
91
+ });
92
+ }
93
+ export { WORKFLOW_WARNING_HEADINGS, WORKFLOW_WARNING_STATUSES, WORKFLOW_WARNING_SEVERITIES, parseWorkflowWarnings, getOpenWorkflowWarnings, formatWorkflowWarningSummary };
94
+ //# sourceMappingURL=workflow-warnings.js.map
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { info, ok, err } from "./log.js";
4
- import { resolveTemplateDir } from "./paths.js";
5
- import { renderFile, copySkillDir, KNOWN_PLATFORMS } from "./render.js";
6
- import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js";
3
+ import { info, ok, err } from './log.js';
4
+ import { resolveTemplateDir } from './paths.js';
5
+ import { renderFile, copySkillDir, KNOWN_PLATFORMS } from './render.js';
6
+ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from './builtin-tuis.js';
7
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
8
8
  const CONFIG_DIR = '.agents';
9
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
package/lib/defaults.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "gemini-cli",
15
15
  "opencode"
16
16
  ],
17
+ "refreshIntervalDays": 7,
17
18
  "dockerfile": null,
18
19
  "vm": {
19
20
  "cpu": null,
@@ -30,8 +30,13 @@ type BridgeOptions = {
30
30
  createDetector?: () => CtrlVDetector;
31
31
  };
32
32
 
33
+ type TextImageClipboardAdapter = ClipboardAdapter & {
34
+ readImageFromText?: (text: string) => Buffer | null;
35
+ };
36
+
33
37
  const FALLBACK_PREFIX = 'Warning: clipboard image paste bridge disabled';
34
38
  const PARTIAL_ESCAPE_FLUSH_MS = 30;
39
+ const BRACKETED_PASTE_RE = /^\x1b\[200~([\s\S]*)\x1b\[201~$/u;
35
40
 
36
41
  // Node's stdin.setRawMode(true) uses libuv's RAW mode, which (unlike the
37
42
  // cfmakeraw that `docker exec -it` applies on the non-bridge path) keeps ONLCR
@@ -147,7 +152,7 @@ async function runBridge({
147
152
  clearFlushTimer();
148
153
  for (const token of detector.feed(inputDecoder.write(chunk))) {
149
154
  if (token.kind === 'text') {
150
- child.write(token.raw);
155
+ handleText(token.raw, child);
151
156
  } else {
152
157
  handleCtrlV(token, child);
153
158
  }
@@ -157,7 +162,7 @@ async function runBridge({
157
162
  flushTimer = null;
158
163
  for (const token of detector.flush()) {
159
164
  if (token.kind === 'text') {
160
- child.write(token.raw);
165
+ handleText(token.raw, child);
161
166
  } else {
162
167
  handleCtrlV(token, child);
163
168
  }
@@ -169,6 +174,37 @@ async function runBridge({
169
174
  const onSigint = () => child.kill('SIGINT');
170
175
  const onSigterm = () => child.kill('SIGTERM');
171
176
 
177
+ function writeImagePaste(png: Buffer, target: PtyProcess): void {
178
+ const filename = pngClipboardFilename(png);
179
+ writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
180
+ pruneClipboardDir(clipboardHostDir(home));
181
+ target.write(buildBracketedPaste(containerClipboardPath(filename)));
182
+ }
183
+
184
+ function handleText(raw: string, target: PtyProcess): void {
185
+ const textAdapter = adapter as TextImageClipboardAdapter;
186
+ const pastedText = extractSinglePathPaste(raw);
187
+ if (!textAdapter.readImageFromText || pastedText === null) {
188
+ target.write(raw);
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const png = textAdapter.readImageFromText(pastedText);
194
+ if (!png) {
195
+ target.write(raw);
196
+ return;
197
+ }
198
+ writeImagePaste(png, target);
199
+ } catch (error) {
200
+ target.write(raw);
201
+ if (!warnedPasteFailure) {
202
+ warnedPasteFailure = true;
203
+ writeStderr(`Warning: clipboard image paste failed; forwarded pasted text (${error instanceof Error ? error.message : 'unknown error'})\n`);
204
+ }
205
+ }
206
+ }
207
+
172
208
  function handleCtrlV(match: CtrlVMatch, target: PtyProcess): void {
173
209
  try {
174
210
  // readImagePng returns null both for "no image on clipboard" and for
@@ -181,10 +217,7 @@ async function runBridge({
181
217
  target.write(match.raw);
182
218
  return;
183
219
  }
184
- const filename = pngClipboardFilename(png);
185
- writeClipboardPngAtomic(clipboardHostDir(home), filename, png);
186
- pruneClipboardDir(clipboardHostDir(home));
187
- target.write(buildBracketedPaste(containerClipboardPath(filename)));
220
+ writeImagePaste(png, target);
188
221
  } catch (error) {
189
222
  target.write(match.raw);
190
223
  if (!warnedPasteFailure) {
@@ -219,14 +252,14 @@ async function runBridge({
219
252
  try {
220
253
  for (const token of detector.feed(inputDecoder.end())) {
221
254
  if (token.kind === 'text') {
222
- child.write(token.raw);
255
+ handleText(token.raw, child);
223
256
  } else {
224
257
  handleCtrlV(token, child);
225
258
  }
226
259
  }
227
260
  for (const token of detector.flush()) {
228
261
  if (token.kind === 'text') {
229
- child.write(token.raw);
262
+ handleText(token.raw, child);
230
263
  }
231
264
  }
232
265
  } catch {
@@ -244,6 +277,15 @@ async function runBridge({
244
277
  }
245
278
  }
246
279
 
280
+ function extractSinglePathPaste(raw: string): string | null {
281
+ const match = raw.match(BRACKETED_PASTE_RE);
282
+ const text = (match ? match[1] : raw)?.trim();
283
+ if (!text || /[\r\n]/u.test(text)) {
284
+ return null;
285
+ }
286
+ return text;
287
+ }
288
+
247
289
  function onceExit(
248
290
  child: PtyProcess,
249
291
  stdin: NodeJS.ReadStream
@@ -1,7 +1,9 @@
1
1
  import { platform } from 'node:os';
2
2
  import { createDarwinClipboardAdapter, type DarwinClipboardAdapter } from './darwin.ts';
3
+ import { createLinuxClipboardAdapter, type LinuxClipboardAdapter } from './linux.ts';
4
+ import { createWin32ClipboardAdapter, type Win32ClipboardAdapter } from './win32.ts';
3
5
 
4
- export type ClipboardAdapter = DarwinClipboardAdapter;
6
+ export type ClipboardAdapter = DarwinClipboardAdapter | LinuxClipboardAdapter | Win32ClipboardAdapter;
5
7
 
6
8
  export function createClipboardAdapter({
7
9
  platformName = platform()
@@ -10,12 +12,9 @@ export function createClipboardAdapter({
10
12
  case 'darwin':
11
13
  return createDarwinClipboardAdapter();
12
14
  case 'linux':
13
- // Future work: dispatch based on $WAYLAND_DISPLAY (wl-paste) or $DISPLAY (xclip);
14
- // see Issue #386 follow-up. Returning null disables the bridge for now.
15
- return null;
15
+ return createLinuxClipboardAdapter();
16
16
  case 'win32':
17
- // Future work: native Win32 clipboard reader. Returning null disables the bridge.
18
- return null;
17
+ return createWin32ClipboardAdapter();
19
18
  default:
20
19
  return null;
21
20
  }
@@ -0,0 +1,124 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import type { ExecFileSyncOptions } from 'node:child_process';
6
+
7
+ const PROBE_TIMEOUT_MS = 2_000;
8
+ const READ_IMAGE_TIMEOUT_MS = 5_000;
9
+ const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
10
+
11
+ type ExecFn = (cmd: string, args: string[], options?: ExecFileSyncOptions) => Buffer | string;
12
+ type ExecToFileFn = (cmd: string, args: string[], outputPath: string, timeout: number) => void;
13
+
14
+ type LinuxBackend =
15
+ | { kind: 'wayland'; command: 'wl-paste' }
16
+ | { kind: 'x11'; command: 'xclip' };
17
+
18
+ export type LinuxClipboardAdapter = {
19
+ available(): { ok: true } | { ok: false; reason: string };
20
+ readImagePng(): Buffer | null;
21
+ };
22
+
23
+ export function createLinuxClipboardAdapter({
24
+ env = process.env,
25
+ execFn = execFileSync,
26
+ execToFileFn = execToFile,
27
+ mkdtempFn = fs.mkdtempSync,
28
+ readFileFn = fs.readFileSync,
29
+ rmFn = fs.rmSync
30
+ }: {
31
+ env?: NodeJS.ProcessEnv;
32
+ execFn?: ExecFn;
33
+ execToFileFn?: ExecToFileFn;
34
+ mkdtempFn?: typeof fs.mkdtempSync;
35
+ readFileFn?: typeof fs.readFileSync;
36
+ rmFn?: typeof fs.rmSync;
37
+ } = {}): LinuxClipboardAdapter {
38
+ return {
39
+ available() {
40
+ const backend = selectBackend(env);
41
+ try {
42
+ execFn(backend.command, versionArgs(backend), {
43
+ encoding: 'utf8',
44
+ timeout: PROBE_TIMEOUT_MS
45
+ });
46
+ return { ok: true };
47
+ } catch {
48
+ return { ok: false, reason: unavailableReason(backend) };
49
+ }
50
+ },
51
+ readImagePng() {
52
+ const backend = selectBackend(env);
53
+ try {
54
+ const mimeTypes = String(execFn(backend.command, mimeArgs(backend), {
55
+ encoding: 'utf8',
56
+ timeout: PROBE_TIMEOUT_MS
57
+ }));
58
+ if (!hasPngMime(mimeTypes)) {
59
+ return null;
60
+ }
61
+
62
+ const tmpDir = mkdtempFn(path.join(os.tmpdir(), 'agent-infra-clipboard-'));
63
+ const outputPath = path.join(tmpDir, 'clipboard.png');
64
+ try {
65
+ execToFileFn(backend.command, imageArgs(backend), outputPath, READ_IMAGE_TIMEOUT_MS);
66
+ const png = Buffer.from(readFileFn(outputPath));
67
+ return isPng(png) ? png : null;
68
+ } finally {
69
+ rmFn(tmpDir, { recursive: true, force: true });
70
+ }
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ };
76
+ }
77
+
78
+ function selectBackend(env: NodeJS.ProcessEnv): LinuxBackend {
79
+ return env.WAYLAND_DISPLAY?.trim()
80
+ ? { kind: 'wayland', command: 'wl-paste' }
81
+ : { kind: 'x11', command: 'xclip' };
82
+ }
83
+
84
+ function versionArgs(backend: LinuxBackend): string[] {
85
+ return backend.kind === 'wayland' ? ['--version'] : ['-version'];
86
+ }
87
+
88
+ function mimeArgs(backend: LinuxBackend): string[] {
89
+ return backend.kind === 'wayland'
90
+ ? ['--list-types']
91
+ : ['-selection', 'clipboard', '-t', 'TARGETS', '-o'];
92
+ }
93
+
94
+ function imageArgs(backend: LinuxBackend): string[] {
95
+ return backend.kind === 'wayland'
96
+ ? ['-t', 'image/png']
97
+ : ['-selection', 'clipboard', '-t', 'image/png', '-o'];
98
+ }
99
+
100
+ function unavailableReason(backend: LinuxBackend): string {
101
+ return backend.kind === 'wayland'
102
+ ? 'Wayland clipboard tool wl-paste is unavailable; install wl-clipboard to enable image paste'
103
+ : 'X11 clipboard tool xclip is unavailable; install xclip to enable image paste';
104
+ }
105
+
106
+ function hasPngMime(output: string): boolean {
107
+ return output.split(/\s+/u).some((type) => type === 'image/png');
108
+ }
109
+
110
+ function execToFile(cmd: string, args: string[], outputPath: string, timeout: number): void {
111
+ const fd = fs.openSync(outputPath, 'w', 0o600);
112
+ try {
113
+ execFileSync(cmd, args, {
114
+ timeout,
115
+ stdio: ['ignore', fd, 'pipe']
116
+ });
117
+ } finally {
118
+ fs.closeSync(fd);
119
+ }
120
+ }
121
+
122
+ function isPng(buffer: Buffer): boolean {
123
+ return buffer.length >= PNG_MAGIC.length && PNG_MAGIC.every((byte, index) => buffer[index] === byte);
124
+ }