codework 0.1.0 → 0.1.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 (3) hide show
  1. package/README.md +41 -0
  2. package/dist/cli.js +384 -57
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,6 +26,47 @@ Then agents should repeatedly use:
26
26
  ! codework done --workspace=my-workspace --name=<agent> --summary="..."
27
27
  ```
28
28
 
29
+ ## Waiting for another agent
30
+
31
+ `codework poll` is a bounded long-poll command for LLM agents.
32
+
33
+ If no actionable event arrives, Codework intentionally exits and tells the agent to run the same command again. This is by design: Codework stdout is injected into the agent's context, so the retry instruction must be visible to the model.
34
+
35
+ Example:
36
+
37
+ ```bash
38
+ ! codework poll --workspace=my-workspace --name=claude --wait=30
39
+ ```
40
+
41
+ If no directive arrives, the output will include:
42
+
43
+ ```text
44
+ MUST: Run the exact command below again.
45
+ RE-RUN EXACT COMMAND:
46
+ codework poll --workspace=my-workspace --name=claude --wait=30 --interval=2
47
+ ```
48
+
49
+ Do not interpret an empty poll as task completion.
50
+
51
+ ### Waiting-loop demo
52
+
53
+ Claude can join before Codex posts a directive:
54
+
55
+ ```bash
56
+ ! codework join --workspace=todo-demo --name=claude --follow=codex
57
+ ! codework poll --workspace=todo-demo --name=claude --wait=30
58
+ ```
59
+
60
+ If no actionable event has arrived, Codework returns `WAIT CONTINUATION REQUIRED` and tells Claude to run the same poll command again. Claude must keep repeating that exact command until a directive, question, blocker, or other actionable event appears.
61
+
62
+ Then Codex can post work:
63
+
64
+ ```bash
65
+ ! codework say --workspace=todo-demo --name=codex --to=claude --kind=directive --message="Implement the Todo app and verify with npm run build."
66
+ ```
67
+
68
+ Claude's next poll receives the directive and switches from waiting to handling the actionable event.
69
+
29
70
  ## Runtime support
30
71
 
31
72
  Node.js:
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { Command, CommanderError } from "commander";
5
5
  import { realpathSync } from "fs";
6
- import process5 from "process";
6
+ import process6 from "process";
7
7
  import { fileURLToPath } from "url";
8
8
 
9
9
  // src/args.ts
@@ -108,6 +108,20 @@ function parsePositiveInteger(value, optionName, fallback) {
108
108
  }
109
109
  return parsed;
110
110
  }
111
+ function parseBoundedSeconds(input) {
112
+ const value = input.cliValue ?? input.envValue;
113
+ if (value === void 0 || value.trim() === "") {
114
+ return input.fallback;
115
+ }
116
+ const parsed = Number(value);
117
+ if (!Number.isFinite(parsed) || parsed < input.min || parsed > input.max) {
118
+ throw new CodeworkError(
119
+ 2,
120
+ `Invalid ${input.optionName}: expected seconds between ${input.min} and ${input.max}.`
121
+ );
122
+ }
123
+ return parsed;
124
+ }
111
125
 
112
126
  // src/core/state.ts
113
127
  function nowIso() {
@@ -313,7 +327,19 @@ function isOwnOperationalEvent(event, agent) {
313
327
  }
314
328
  function relevantUnreadEvents(state, events, agent, since) {
315
329
  const cursor = since ?? agent.cursorEventId;
316
- return events.filter((event) => event.id > cursor).filter((event) => targetMatchesAgent(event.to, agent)).filter((event) => !isOwnOperationalEvent(event, agent)).sort((a, b) => priorityForEvent(state, agent, b) - priorityForEvent(state, agent, a) || a.id - b.id);
330
+ return events.filter((event) => event.id > cursor).filter((event) => targetMatchesAgent(event.to, agent) || event.actor === agent.follow).filter((event) => !isOwnOperationalEvent(event, agent)).sort((a, b) => priorityForEvent(state, agent, b) - priorityForEvent(state, agent, a) || a.id - b.id);
331
+ }
332
+ function isActionableEvent(event, agent) {
333
+ if (event.type === "warning.created") {
334
+ return true;
335
+ }
336
+ if (event.type === "message.posted") {
337
+ return event.kind === "directive" || event.kind === "question" || event.kind === "blocker";
338
+ }
339
+ if (event.type === "work.done") {
340
+ return agent.follow !== "user" && agent.follow !== "self" && event.actor === agent.follow;
341
+ }
342
+ return false;
317
343
  }
318
344
  function priorityForEvent(state, agent, event) {
319
345
  const follow = agent.follow;
@@ -341,11 +367,6 @@ function latestEvents(events, tail) {
341
367
  }
342
368
  return events.slice(Math.max(0, events.length - tail));
343
369
  }
344
- function advanceCursorToLatest(state, agent) {
345
- agent.cursorEventId = latestEventId(state);
346
- agent.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
347
- state.updatedAt = agent.lastSeenAt;
348
- }
349
370
  function formatEvent(event) {
350
371
  const parts = [
351
372
  `id=${event.id}`,
@@ -481,13 +502,34 @@ function resolveCodeworkPaths(options) {
481
502
  };
482
503
  }
483
504
 
505
+ // src/core/command.ts
506
+ function shellQuote(value) {
507
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) {
508
+ return value;
509
+ }
510
+ return `'${value.replace(/'/g, "'\\''")}'`;
511
+ }
512
+ function buildCanonicalPollCommand(input) {
513
+ return [
514
+ "codework",
515
+ input.kind,
516
+ `--workspace=${shellQuote(input.workspace)}`,
517
+ `--name=${shellQuote(input.name)}`,
518
+ `--wait=${formatSeconds(input.waitSeconds)}`,
519
+ `--interval=${formatSeconds(input.intervalSeconds)}`
520
+ ].join(" ");
521
+ }
522
+ function formatSeconds(value) {
523
+ return Number.isInteger(value) ? String(value) : String(value);
524
+ }
525
+
484
526
  // src/core/render.ts
485
527
  function renderGuide(options) {
486
528
  const agent = findAgent(options.state, options.agentName);
487
529
  const agentName = agent?.name ?? options.agentName ?? "(not specified)";
488
530
  const follow = agent?.follow ?? "(none)";
489
531
  const workspace = options.state.workspace;
490
- const recommendedCommand = options.recommendedCommand ?? (agent ? `codework poll --workspace=${workspace} --name=${agent.name}` : `codework status --workspace=${workspace}`);
532
+ const recommendedCommand = options.recommendedCommand ?? (agent ? defaultPollCommand(workspace, agent.name) : `codework status --workspace=${workspace}`);
491
533
  const lines = [
492
534
  "# CODEWORK CONTEXT GUIDE",
493
535
  "",
@@ -527,10 +569,11 @@ function renderGuide(options) {
527
569
  `You currently follow: ${follow}.`,
528
570
  "Interpretation:",
529
571
  ...followInterpretationLines(options.state, follow),
572
+ ...waitingForFollowTargetLines(options.state, agent, workspace),
530
573
  "",
531
574
  "## REQUIRED OPERATING LOOP",
532
575
  "",
533
- `1. Run \`codework poll --workspace=${workspace} --name=${agentName}\` before starting a new substantial step.`,
576
+ `1. Run \`${agent ? defaultPollCommand(workspace, agent.name) : `codework poll --workspace=${workspace} --name=${agentName} --wait=30 --interval=2`}\` before starting a new substantial step.`,
534
577
  "2. Read blockers first.",
535
578
  "3. If you need another agent, post a directive:",
536
579
  ` \`codework say --workspace=${workspace} --name=${agentName} --to=<agent> --kind=directive --message="..."\``,
@@ -553,7 +596,7 @@ function renderGuide(options) {
553
596
  "",
554
597
  "## UNREAD EVENTS",
555
598
  "",
556
- ...eventLines(options.unreadEvents, "No unread events."),
599
+ ...eventLines(options.unreadEvents, "No actionable unread events are shown here. Use the poll command below to wait for peer events."),
557
600
  "",
558
601
  "## WARNINGS",
559
602
  "",
@@ -579,7 +622,8 @@ function renderGuide(options) {
579
622
  "",
580
623
  `- codework guide --workspace=${workspace} --name=${agentName}`,
581
624
  `- codework status --workspace=${workspace} --name=${agentName}`,
582
- `- codework poll --workspace=${workspace} --name=${agentName}`,
625
+ `- codework poll --workspace=${workspace} --name=${agentName} --wait=30 --interval=2`,
626
+ `- codework wait --workspace=${workspace} --name=${agentName} --wait=30 --interval=2`,
583
627
  `- codework say --workspace=${workspace} --name=${agentName} --to=<agent|all> --kind=<note|directive|question|blocker> --message="..."`,
584
628
  `- codework done --workspace=${workspace} --name=${agentName} --summary="..." --tests="..."`,
585
629
  `- codework log --workspace=${workspace} --tail=20`
@@ -587,6 +631,93 @@ function renderGuide(options) {
587
631
  return `${lines.join("\n")}
588
632
  `;
589
633
  }
634
+ function renderPollResult(options) {
635
+ const follow = options.agent.follow;
636
+ const lines = [
637
+ options.title,
638
+ "",
639
+ `WORKSPACE: ${options.state.workspace}`,
640
+ `AGENT: ${options.agent.name}`,
641
+ `FOLLOW: ${follow}`,
642
+ `AUTHORITY MODE: ${authorityMode(follow)}`,
643
+ `TIMESTAMP: ${(/* @__PURE__ */ new Date()).toISOString()}`,
644
+ `POLL WINDOW: ${formatPollSeconds(options.waitSeconds)}`,
645
+ `POLL INTERVAL: ${formatPollSeconds(options.intervalSeconds)}`,
646
+ `POLL RESULT: ${options.pollResult}`,
647
+ `CONSECUTIVE EMPTY POLLS: ${options.agent.lastPollEmptyCount ?? 0}`,
648
+ "",
649
+ "## TEAM STATE",
650
+ "",
651
+ ...teamStateLines(options.state),
652
+ "",
653
+ "## FOLLOW RULE",
654
+ "",
655
+ `You currently follow: ${follow}.`,
656
+ ...pollFollowRuleLines(follow)
657
+ ];
658
+ if (options.actionableEvents.length > 0) {
659
+ lines.push(
660
+ "",
661
+ "## ACTIONABLE EVENTS",
662
+ "",
663
+ ...structuredEventLines(options.actionableEvents),
664
+ "",
665
+ "## REQUIRED NEXT ACTION",
666
+ "",
667
+ "MUST: Handle the actionable event above before polling again.",
668
+ follow !== "user" && follow !== "self" ? `MUST: Follow directives from ${follow} because your follow target is ${follow}.` : "MUST: Apply the actionable event according to your authority mode and the human's current instruction.",
669
+ `MUST: If blocked, send \`codework say --workspace=${options.state.workspace} --name=${options.agent.name} --to=${followTargetForCommand(follow)} --kind=blocker --message="..."\``,
670
+ `MUST: If complete, send \`codework done --workspace=${options.state.workspace} --name=${options.agent.name} --summary="..." --tests="..."\``,
671
+ "MUST NOT: ignore this directive and continue waiting.",
672
+ "",
673
+ "NEXT POLL COMMAND AFTER HANDLING:",
674
+ options.nextPollCommand
675
+ );
676
+ } else {
677
+ lines.push(
678
+ "",
679
+ "## NO ACTIONABLE EVENTS",
680
+ "",
681
+ "No actionable events arrived during this poll window."
682
+ );
683
+ if (options.nonActionableEvents.length > 0) {
684
+ lines.push(
685
+ "",
686
+ "## OBSERVED NON-ACTIONABLE EVENTS",
687
+ "",
688
+ ...structuredEventLines(options.nonActionableEvents)
689
+ );
690
+ }
691
+ lines.push(
692
+ "",
693
+ "## WAIT CONTINUATION REQUIRED",
694
+ "",
695
+ "STATE: waiting-for-peer-event",
696
+ "MUST: Run the exact command below again.",
697
+ "MUST: Continue repeating it until an actionable event appears, the human gives a new instruction, or you explicitly report that you are still waiting.",
698
+ "MUST NOT: treat this empty poll as task completion.",
699
+ "MUST NOT: invent teammate state.",
700
+ "MUST NOT: start unrelated work merely because no event arrived.",
701
+ "",
702
+ "RE-RUN EXACT COMMAND:",
703
+ options.canonicalCommand,
704
+ "",
705
+ "## REPORTING RULE",
706
+ "",
707
+ "If you decide to speak to the human while still waiting, use this exact report shape:",
708
+ "",
709
+ "CODEWORK WAITING REPORT:",
710
+ `- Agent: ${options.agent.name}`,
711
+ `- Workspace: ${options.state.workspace}`,
712
+ `- Waiting for: ${waitingForText(follow)}`,
713
+ `- Consecutive empty polls: ${options.agent.lastPollEmptyCount ?? 0}`,
714
+ `- Last poll command: ${options.canonicalCommand}`,
715
+ "- Next action: run the same poll command again"
716
+ );
717
+ }
718
+ return `${lines.join("\n")}
719
+ `;
720
+ }
590
721
  function renderLog(workspace, events) {
591
722
  return [
592
723
  "# CODEWORK EVENT LOG",
@@ -685,7 +816,8 @@ function teamStateLines(state) {
685
816
  }
686
817
  return agents.map((agent) => {
687
818
  const active = agent.active ? "active" : "inactive";
688
- return `- ${agent.name}: ${active}, follow=${agent.follow}, role=${agent.role ?? "(none)"}, lastSeen=${agent.lastSeenAt}, sessions=${agent.sessionCount}, cursor=${agent.cursorEventId}`;
819
+ const poll = agent.lastPollAt ? `, lastPoll=${agent.lastPollAt}, emptyPolls=${agent.lastPollEmptyCount ?? 0}` : "";
820
+ return `- ${agent.name}: ${active}, follow=${agent.follow}, role=${agent.role ?? "(none)"}, lastSeen=${agent.lastSeenAt}, sessions=${agent.sessionCount}, cursor=${agent.cursorEventId}${poll}`;
689
821
  });
690
822
  }
691
823
  function followGraphLines(state) {
@@ -732,6 +864,96 @@ function warningLines(state) {
732
864
  }
733
865
  return state.warnings.map((warning) => `- ${warning.code}: ${warning.message} (${warning.createdAt})`);
734
866
  }
867
+ function waitingForFollowTargetLines(state, agent, workspace) {
868
+ if (!agent || agent.follow === "user" || agent.follow === "self") {
869
+ return [];
870
+ }
871
+ const command = defaultPollCommand(workspace, agent.name);
872
+ return [
873
+ "",
874
+ "## WAITING FOR FOLLOW TARGET",
875
+ "",
876
+ `STATE: waiting-for-directive-from-${agent.follow}`,
877
+ `MUST: Poll for directives from ${agent.follow} before starting implementation.`,
878
+ "MUST: Run the command below now.",
879
+ `MUST NOT: begin implementation based only on the human's vague request if ${agent.follow} is your follow target.`,
880
+ "",
881
+ "RUN COMMAND:",
882
+ command,
883
+ "",
884
+ findAgentByFollowValue(state, agent.follow) ? `FOLLOW TARGET STATUS: ${agent.follow} is registered.` : `FOLLOW TARGET STATUS: ${agent.follow} is not registered yet; keep polling until directives arrive.`
885
+ ];
886
+ }
887
+ function defaultPollCommand(workspace, name) {
888
+ return buildCanonicalPollCommand({
889
+ kind: "poll",
890
+ workspace,
891
+ name,
892
+ waitSeconds: 30,
893
+ intervalSeconds: 2
894
+ });
895
+ }
896
+ function pollFollowRuleLines(follow) {
897
+ if (follow === "user") {
898
+ return [
899
+ "MUST: Wait for directives, questions, blockers, or completion signals from the workspace.",
900
+ "MUST NOT: infer teammate state from silence."
901
+ ];
902
+ }
903
+ if (follow === "self") {
904
+ return [
905
+ "MUST: Wait for actionable peer events before making claims about teammate progress.",
906
+ "MUST NOT: infer teammate state from silence."
907
+ ];
908
+ }
909
+ return [
910
+ `MUST: Wait for directives, questions, blockers, or completion signals from ${follow}.`,
911
+ `MUST NOT: infer ${follow}'s intent from silence.`
912
+ ];
913
+ }
914
+ function structuredEventLines(events) {
915
+ return events.flatMap((event) => {
916
+ const lines = [
917
+ `Event ${event.id}:`,
918
+ `- TYPE: ${event.type}`,
919
+ `- FROM: ${event.actor}`,
920
+ `- TO: ${event.to ?? "all"}`
921
+ ];
922
+ if (event.kind) {
923
+ lines.push(`- KIND: ${event.kind}`);
924
+ }
925
+ const message = eventText(event);
926
+ if (message !== "") {
927
+ lines.push("- MESSAGE:", ...indentMultiline(message));
928
+ }
929
+ return [...lines, ""];
930
+ }).slice(0, -1);
931
+ }
932
+ function eventText(event) {
933
+ const keys = ["message", "summary", "blockers", "next", "reason"];
934
+ for (const key of keys) {
935
+ const value = event.payload[key];
936
+ if (typeof value === "string" && value.trim() !== "") {
937
+ return value;
938
+ }
939
+ }
940
+ return JSON.stringify(event.payload);
941
+ }
942
+ function indentMultiline(value) {
943
+ return value.split(/\r?\n/).map((line) => ` ${line}`);
944
+ }
945
+ function followTargetForCommand(follow) {
946
+ return follow === "self" || follow === "user" ? "all" : follow;
947
+ }
948
+ function waitingForText(follow) {
949
+ if (follow === "user" || follow === "self") {
950
+ return "actionable workspace event";
951
+ }
952
+ return `directive or actionable event from ${follow}`;
953
+ }
954
+ function formatPollSeconds(value) {
955
+ return `${Number.isInteger(value) ? value : String(value)}s`;
956
+ }
735
957
 
736
958
  // src/commands/doctor.ts
737
959
  async function runDoctorCommand(ctx) {
@@ -868,9 +1090,9 @@ async function runDoneCommand(ctx, options) {
868
1090
  notice: [
869
1091
  `Work report recorded as event ${event.id}.`,
870
1092
  "Notify your follow target if the report changes their next step.",
871
- `Run \`codework poll --workspace=${workspace.value} --name=${agent.name}\` before the next substantial step.`
1093
+ `Run \`codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2\` before the next substantial step.`
872
1094
  ],
873
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1095
+ recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2`
874
1096
  }),
875
1097
  quietText: `event=${event.id} done
876
1098
  `,
@@ -914,7 +1136,7 @@ async function runGuideCommand(ctx) {
914
1136
  unreadEvents: unread,
915
1137
  allEvents: events,
916
1138
  notice: ["Full guide reprinted for the calling agent."],
917
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1139
+ recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2`
918
1140
  }),
919
1141
  quietText: `workspace=${workspace.value} agent=${agent.name} guide
920
1142
  `,
@@ -996,7 +1218,7 @@ async function runJoinCommand(ctx, options) {
996
1218
  unreadEvents: unread,
997
1219
  allEvents,
998
1220
  notice,
999
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1221
+ recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2`
1000
1222
  }),
1001
1223
  quietText: `workspace=${workspace.value} agent=${agent.name} joined
1002
1224
  `,
@@ -1164,7 +1386,7 @@ async function runNewCommand(ctx, options) {
1164
1386
  `Agent registered: ${agent.name}.`,
1165
1387
  "This agent must keep using Codework commands for coordination."
1166
1388
  ],
1167
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1389
+ recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2`
1168
1390
  });
1169
1391
  return {
1170
1392
  text,
@@ -1185,50 +1407,147 @@ function requireNewOptions(options) {
1185
1407
  }
1186
1408
 
1187
1409
  // src/commands/poll.ts
1188
- async function runPollCommand(ctx, options) {
1410
+ import process5 from "process";
1411
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1412
+ async function runPollCommand(ctx, options, commandKind = "poll") {
1189
1413
  const workspace = slugifyIdentifier(ctx.workspace, "--workspace");
1190
1414
  const name = required(ctx.name, "--name");
1191
- const since = options.since === void 0 ? void 0 : parsePositiveInteger(options.since, "--since", 0);
1192
- const tail = options.tail === void 0 ? void 0 : parsePositiveInteger(options.tail, "--tail", 0);
1415
+ const settings = normalizePollSettings(options);
1416
+ const canonicalCommand = buildCanonicalPollCommand({
1417
+ kind: commandKind,
1418
+ workspace: workspace.value,
1419
+ name,
1420
+ waitSeconds: settings.waitSeconds,
1421
+ intervalSeconds: settings.intervalSeconds
1422
+ });
1423
+ const nextPollCommand = buildCanonicalPollCommand({
1424
+ kind: "poll",
1425
+ workspace: workspace.value,
1426
+ name,
1427
+ waitSeconds: settings.waitSeconds,
1428
+ intervalSeconds: settings.intervalSeconds
1429
+ });
1193
1430
  const paths = resolveCodeworkPaths({
1194
1431
  cwd: ctx.cwd,
1195
1432
  home: ctx.home,
1196
1433
  workspace: workspace.value,
1197
1434
  debug: ctx.debug
1198
1435
  });
1199
- return withWorkspaceLock(paths.workspaceDir, async () => {
1200
- const state = await requireState(paths);
1436
+ const startedAt = Date.now();
1437
+ let readResult;
1438
+ while (true) {
1439
+ readResult = await readPollEvents(paths.workspaceDir, name, settings);
1440
+ if (readResult.actionableEvents.length > 0 || settings.once || settings.waitSeconds === 0) {
1441
+ break;
1442
+ }
1443
+ const remainingMs = startedAt + settings.waitSeconds * 1e3 - Date.now();
1444
+ if (remainingMs <= 0) {
1445
+ break;
1446
+ }
1447
+ await sleep2(Math.min(settings.intervalSeconds * 1e3, remainingMs));
1448
+ }
1449
+ const finalResult = await finalizePoll(paths.workspaceDir, name, settings, canonicalCommand);
1450
+ const pollResult = finalResult.actionableEvents.length > 0 ? "actionable-events-found" : "timeout-without-actionable-event";
1451
+ return {
1452
+ text: renderPollResult({
1453
+ title: commandKind === "wait" ? "# CODEWORK WAIT RESULT" : "# CODEWORK POLL RESULT",
1454
+ state: finalResult.state,
1455
+ agent: finalResult.agent,
1456
+ actionableEvents: finalResult.actionableEvents,
1457
+ nonActionableEvents: finalResult.nonActionableEvents,
1458
+ waitSeconds: settings.once ? 0 : settings.waitSeconds,
1459
+ intervalSeconds: settings.intervalSeconds,
1460
+ pollResult,
1461
+ canonicalCommand,
1462
+ nextPollCommand
1463
+ }),
1464
+ quietText: finalResult.actionableEvents.length === 0 ? `actionable=0 emptyPolls=${finalResult.agent.lastPollEmptyCount ?? 0}
1465
+ ` : `actionable=${finalResult.actionableEvents.length}
1466
+ `,
1467
+ json: {
1468
+ ok: true,
1469
+ command: commandKind,
1470
+ workspace: finalResult.state.workspace,
1471
+ agent: finalResult.agent,
1472
+ pollResult,
1473
+ waitSeconds: settings.waitSeconds,
1474
+ intervalSeconds: settings.intervalSeconds,
1475
+ actionableEvents: finalResult.actionableEvents,
1476
+ nonActionableEvents: finalResult.nonActionableEvents,
1477
+ cursorEventId: finalResult.agent.cursorEventId,
1478
+ canonicalCommand
1479
+ }
1480
+ };
1481
+ }
1482
+ function normalizePollSettings(options) {
1483
+ const waitSeconds = options.once ? 0 : parseBoundedSeconds({
1484
+ cliValue: options.wait,
1485
+ envValue: process5.env.CODEWORK_POLL_WAIT_SECONDS,
1486
+ optionName: "--wait",
1487
+ fallback: 30,
1488
+ min: 0,
1489
+ max: 120
1490
+ });
1491
+ const intervalSeconds = parseBoundedSeconds({
1492
+ cliValue: options.interval,
1493
+ envValue: process5.env.CODEWORK_POLL_INTERVAL_SECONDS,
1494
+ optionName: "--interval",
1495
+ fallback: 2,
1496
+ min: 1,
1497
+ max: 10
1498
+ });
1499
+ return {
1500
+ waitSeconds,
1501
+ intervalSeconds,
1502
+ once: Boolean(options.once),
1503
+ since: options.since === void 0 ? void 0 : parsePositiveInteger(options.since, "--since", 0),
1504
+ tail: options.tail === void 0 ? void 0 : parsePositiveInteger(options.tail, "--tail", 0)
1505
+ };
1506
+ }
1507
+ async function readPollEvents(workspaceDir, name, settings) {
1508
+ return withWorkspaceLock(workspaceDir, async () => {
1509
+ const state = await requireState({ workspaceDir, cwd: "", root: "", home: "" });
1201
1510
  const agent = findAgent(state, name);
1202
1511
  if (!agent) {
1203
1512
  throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
1204
1513
  }
1205
- const events = await readEvents(paths.workspaceDir);
1206
- const allUnread = relevantUnreadEvents(state, events, agent, since);
1207
- const unread = tail === void 0 ? allUnread : allUnread.slice(0, tail);
1208
- advanceCursorToLatest(state, agent);
1209
- await saveState(paths.workspaceDir, state);
1210
- return {
1211
- text: renderGuide({
1212
- state,
1213
- agentName: agent.name,
1214
- unreadEvents: unread,
1215
- allEvents: events,
1216
- notice: unread.length === 0 ? ["No unread events. Continue normal work, but keep polling before substantial changes."] : [`Unread events delivered: ${unread.length}. Cursor advanced to latest event.`],
1217
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1218
- }),
1219
- quietText: unread.length === 0 ? "unread=0\n" : `unread=${unread.length}
1220
- `,
1221
- json: {
1222
- ok: true,
1223
- command: "poll",
1224
- workspace: state.workspace,
1225
- agent,
1226
- unreadEvents: unread,
1227
- cursorEventId: agent.cursorEventId
1228
- }
1229
- };
1514
+ const events = await readEvents(workspaceDir);
1515
+ return buildPollReadResult(state, agent, events, settings);
1516
+ });
1517
+ }
1518
+ async function finalizePoll(workspaceDir, name, settings, canonicalCommand) {
1519
+ return withWorkspaceLock(workspaceDir, async () => {
1520
+ const state = await requireState({ workspaceDir, cwd: "", root: "", home: "" });
1521
+ const agent = findAgent(state, name);
1522
+ if (!agent) {
1523
+ throw new CodeworkError(2, `Agent is not registered in workspace: ${name}`);
1524
+ }
1525
+ const events = await readEvents(workspaceDir);
1526
+ const result = buildPollReadResult(state, agent, events, settings);
1527
+ const maxDisplayedEventId = result.displayedEvents.reduce((max, event) => Math.max(max, event.id), 0);
1528
+ agent.cursorEventId = maxDisplayedEventId > 0 ? maxDisplayedEventId : latestEventId(state);
1529
+ agent.lastSeenAt = (/* @__PURE__ */ new Date()).toISOString();
1530
+ agent.lastPollAt = agent.lastSeenAt;
1531
+ agent.lastPollCommand = canonicalCommand;
1532
+ agent.lastPollEmptyCount = result.actionableEvents.length > 0 ? 0 : (agent.lastPollEmptyCount ?? 0) + 1;
1533
+ state.updatedAt = agent.lastSeenAt;
1534
+ await saveState(workspaceDir, state);
1535
+ return result;
1230
1536
  });
1231
1537
  }
1538
+ function buildPollReadResult(state, agent, allEvents, settings) {
1539
+ const unread = relevantUnreadEvents(state, allEvents, agent, settings.since);
1540
+ const displayedEvents = settings.tail === void 0 ? unread : unread.slice(0, settings.tail);
1541
+ const actionableEvents = displayedEvents.filter((event) => isActionableEvent(event, agent));
1542
+ const nonActionableEvents = displayedEvents.filter((event) => !isActionableEvent(event, agent));
1543
+ return {
1544
+ state,
1545
+ agent,
1546
+ displayedEvents,
1547
+ actionableEvents,
1548
+ nonActionableEvents
1549
+ };
1550
+ }
1232
1551
 
1233
1552
  // src/commands/say.ts
1234
1553
  async function runSayCommand(ctx, options) {
@@ -1275,7 +1594,7 @@ async function runSayCommand(ctx, options) {
1275
1594
  `Message recorded: kind=${kind}, to=${to}.`,
1276
1595
  kind === "directive" ? "Directive messages must be treated as strong coordination instructions by recipients." : kind === "blocker" ? "Blocker messages are visible to all agents." : "Message is available through Codework poll/status/log."
1277
1596
  ],
1278
- recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name}`
1597
+ recommendedCommand: `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2`
1279
1598
  }),
1280
1599
  quietText: `event=${event.id} kind=${kind} to=${to}
1281
1600
  `,
@@ -1316,7 +1635,7 @@ async function runStatusCommand(ctx) {
1316
1635
  unreadEvents: unread,
1317
1636
  allEvents: events,
1318
1637
  notice,
1319
- recommendedCommand: agent ? `codework poll --workspace=${workspace.value} --name=${agent.name}` : `codework status --workspace=${workspace.value} --name=<agent>`,
1638
+ recommendedCommand: agent ? `codework poll --workspace=${workspace.value} --name=${agent.name} --wait=30 --interval=2` : `codework status --workspace=${workspace.value} --name=<agent>`,
1320
1639
  statusMode: true
1321
1640
  }),
1322
1641
  quietText: `workspace=${workspace.value} agents=${Object.keys(state.agents).length}
@@ -1333,12 +1652,17 @@ async function runStatusCommand(ctx) {
1333
1652
  };
1334
1653
  }
1335
1654
 
1655
+ // src/commands/wait.ts
1656
+ async function runWaitCommand(ctx, options) {
1657
+ return runPollCommand(ctx, options, "wait");
1658
+ }
1659
+
1336
1660
  // src/cli.ts
1337
1661
  var defaultIo = {
1338
- stdout: (text) => process5.stdout.write(text),
1339
- stderr: (text) => process5.stderr.write(text)
1662
+ stdout: (text) => process6.stdout.write(text),
1663
+ stderr: (text) => process6.stderr.write(text)
1340
1664
  };
1341
- async function main(argv = process5.argv, io = defaultIo) {
1665
+ async function main(argv = process6.argv, io = defaultIo) {
1342
1666
  const program = buildProgram(io);
1343
1667
  const parsedArgv = [argv[0] ?? "node", argv[1] ?? "codework", ...preprocessArgv(argv.slice(2))];
1344
1668
  try {
@@ -1392,9 +1716,12 @@ function buildProgram(io) {
1392
1716
  await emitResult(io, contextFrom(this), await runStatusCommand(contextFrom(this)));
1393
1717
  }
1394
1718
  );
1395
- addCommonOptions(program.command("poll").description("Read unread events for the calling agent.")).option("--since <eventId>", "Read events after this event id.").option("--tail <n>", "Limit unread events.").action(async function() {
1719
+ addCommonOptions(program.command("poll").description("Read unread events for the calling agent.")).option("--wait <seconds>", "Maximum seconds to wait for new actionable events.").option("--interval <seconds>", "Seconds between event log checks.").option("--since <eventId>", "Read events after this event id.").option("--tail <n>", "Limit unread events.").option("--once", "Read once without waiting.").action(async function() {
1396
1720
  await emitResult(io, contextFrom(this), await runPollCommand(contextFrom(this), this.opts()));
1397
1721
  });
1722
+ addCommonOptions(program.command("wait").description("Wait for actionable events for the calling agent.")).option("--wait <seconds>", "Maximum seconds to wait for new actionable events.").option("--interval <seconds>", "Seconds between event log checks.").option("--since <eventId>", "Read events after this event id.").option("--tail <n>", "Limit unread events.").option("--once", "Read once without waiting.").action(async function() {
1723
+ await emitResult(io, contextFrom(this), await runWaitCommand(contextFrom(this), this.opts()));
1724
+ });
1398
1725
  addCommonOptions(program.command("say").description("Post a message event to the workspace.")).option("--message <text>", "Message body.").option("--to <agent|all>", "Recipient.", "all").option("--kind <note|directive|question|blocker>", "Message kind.", "note").action(async function() {
1399
1726
  await emitResult(io, contextFrom(this), await runSayCommand(contextFrom(this), this.opts()));
1400
1727
  });
@@ -1418,7 +1745,7 @@ function buildProgram(io) {
1418
1745
  return program;
1419
1746
  }
1420
1747
  function addCommonOptions(command, withDefaults = false) {
1421
- command.option("--workspace <id>", "Workspace id.").option("--name <agent>", "Calling agent name.").option("--format <format>", "text | json.", withDefaults ? "text" : void 0).option("--quiet", "Only print machine/minimal output.").option("--debug", "Print diagnostics to stderr.").option("--cwd <path>", "Repository/work root.", withDefaults ? process5.cwd() : void 0).option("--home <path>", "Override Codework home.").option("--no-color", "Do not emit ANSI color.");
1748
+ command.option("--workspace <id>", "Workspace id.").option("--name <agent>", "Calling agent name.").option("--format <format>", "text | json.", withDefaults ? "text" : void 0).option("--quiet", "Only print machine/minimal output.").option("--debug", "Print diagnostics to stderr.").option("--cwd <path>", "Repository/work root.", withDefaults ? process6.cwd() : void 0).option("--home <path>", "Override Codework home.").option("--no-color", "Do not emit ANSI color.");
1422
1749
  return command;
1423
1750
  }
1424
1751
  function contextFrom(command) {
@@ -1434,7 +1761,7 @@ function contextFrom(command) {
1434
1761
  format: validateFormat(merged.format),
1435
1762
  quiet: Boolean(merged.quiet),
1436
1763
  debug: Boolean(merged.debug),
1437
- cwd: merged.cwd ?? process5.cwd(),
1764
+ cwd: merged.cwd ?? process6.cwd(),
1438
1765
  home: merged.home
1439
1766
  };
1440
1767
  }
@@ -1487,10 +1814,10 @@ function requestedFormat(argv) {
1487
1814
  }
1488
1815
  if (isDirectRun()) {
1489
1816
  const code = await main();
1490
- process5.exit(code);
1817
+ process6.exit(code);
1491
1818
  }
1492
1819
  function isDirectRun() {
1493
- const argvPath = process5.argv[1];
1820
+ const argvPath = process6.argv[1];
1494
1821
  if (!argvPath) {
1495
1822
  return false;
1496
1823
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codework",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "TypeScript CLI harness for coordinating multiple AI coding agents through stdout-as-context.",
5
5
  "type": "module",
6
6
  "bin": {