@openrig/cli 0.6.8 → 0.6.9

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 (178) hide show
  1. package/daemon/assets/plugins/openrig-core/hooks/scripts/refocus.cjs +41 -2
  2. package/daemon/assets/plugins/openrig-core/skills/messaging-the-human/SKILL.md +12 -4
  3. package/daemon/assets/plugins/openrig-core/skills/refocusing/scripts/trace-to-root.py +218 -7
  4. package/daemon/context-packs/help/manifest.yaml +1 -1
  5. package/daemon/context-packs/onboarding-width/manifest.yaml +1 -1
  6. package/daemon/context-packs/onboarding-width/public-what-you-can-do.md +17 -7
  7. package/daemon/context-packs/reference/manifest.yaml +1 -1
  8. package/daemon/context-packs/reference/whats-new.md +69 -74
  9. package/daemon/context-packs/skills/agent-operated-software/manifest.yaml +1 -1
  10. package/daemon/context-packs/skills/agent-operated-workflows/manifest.yaml +1 -1
  11. package/daemon/context-packs/skills/applying-a-permission-policy/manifest.yaml +1 -1
  12. package/daemon/context-packs/skills/claude-compaction-restore/manifest.yaml +1 -1
  13. package/daemon/context-packs/skills/core/agent-starters/manifest.yaml +1 -1
  14. package/daemon/context-packs/skills/core/agent-startup-and-context-ingestion/manifest.yaml +1 -1
  15. package/daemon/context-packs/skills/core/cross-host-rig-commands/SKILL.md +1 -0
  16. package/daemon/context-packs/skills/core/cross-host-rig-commands/manifest.yaml +1 -1
  17. package/daemon/context-packs/skills/core/human-in-the-loop/manifest.yaml +1 -1
  18. package/daemon/context-packs/skills/core/messaging-the-human/SKILL.md +12 -4
  19. package/daemon/context-packs/skills/core/messaging-the-human/manifest.yaml +1 -1
  20. package/daemon/context-packs/skills/core/openrig-architect/manifest.yaml +1 -1
  21. package/daemon/context-packs/skills/core/openrig-cmux/manifest.yaml +1 -1
  22. package/daemon/context-packs/skills/core/openrig-herdr/manifest.yaml +1 -1
  23. package/daemon/context-packs/skills/core/openrig-skills/manifest.yaml +1 -1
  24. package/daemon/context-packs/skills/core/openrig-software-factory/manifest.yaml +1 -1
  25. package/daemon/context-packs/skills/core/openrig-upgrade/manifest.yaml +1 -1
  26. package/daemon/context-packs/skills/core/rig-bundles-and-shareable-artifacts/manifest.yaml +1 -1
  27. package/daemon/context-packs/skills/core/rig-lifecycle/manifest.yaml +1 -1
  28. package/daemon/context-packs/skills/core/session-source-fork/manifest.yaml +1 -1
  29. package/daemon/context-packs/skills/core/specification-system/manifest.yaml +1 -1
  30. package/daemon/context-packs/skills/core/topology-mutation-and-seat-management/manifest.yaml +1 -1
  31. package/daemon/context-packs/skills/core/watchdog/manifest.yaml +1 -1
  32. package/daemon/context-packs/skills/delegating-work/manifest.yaml +1 -1
  33. package/daemon/context-packs/skills/forming-an-openrig-mental-model/manifest.yaml +1 -1
  34. package/daemon/context-packs/skills/loading-addressable-markdown/manifest.yaml +1 -1
  35. package/daemon/context-packs/skills/mission-slice-sop/manifest.yaml +1 -1
  36. package/daemon/context-packs/skills/openrig-operating-model/manifest.yaml +1 -1
  37. package/daemon/context-packs/skills/openrig-user/manifest.yaml +1 -1
  38. package/daemon/context-packs/skills/orienting-to-an-inherited-seat/manifest.yaml +1 -1
  39. package/daemon/context-packs/skills/pm/backlog-capture/manifest.yaml +1 -1
  40. package/daemon/context-packs/skills/pm/context-builder/manifest.yaml +1 -1
  41. package/daemon/context-packs/skills/pm/exec-summary/manifest.yaml +1 -1
  42. package/daemon/context-packs/skills/pm/office-hours/manifest.yaml +1 -1
  43. package/daemon/context-packs/skills/pm/plan-review/manifest.yaml +1 -1
  44. package/daemon/context-packs/skills/pm/requirements-writer/manifest.yaml +1 -1
  45. package/daemon/context-packs/skills/pm/ui-mockup/manifest.yaml +1 -1
  46. package/daemon/context-packs/skills/pods/development-team/manifest.yaml +1 -1
  47. package/daemon/context-packs/skills/pods/orchestration-team/manifest.yaml +1 -1
  48. package/daemon/context-packs/skills/pods/oversight-team/manifest.yaml +1 -1
  49. package/daemon/context-packs/skills/pods/review-team/manifest.yaml +1 -1
  50. package/daemon/context-packs/skills/process/agent-browser/manifest.yaml +1 -1
  51. package/daemon/context-packs/skills/process/context-engineering/manifest.yaml +1 -1
  52. package/daemon/context-packs/skills/process/dogfood/manifest.yaml +1 -1
  53. package/daemon/context-packs/skills/process/frontend-design/manifest.yaml +1 -1
  54. package/daemon/context-packs/skills/process/systematic-debugging/manifest.yaml +1 -1
  55. package/daemon/context-packs/skills/process/test-driven-development/manifest.yaml +1 -1
  56. package/daemon/context-packs/skills/process/verification-before-completion/manifest.yaml +1 -1
  57. package/daemon/context-packs/skills/queue-handoff/manifest.yaml +1 -1
  58. package/daemon/context-packs/skills/refocusing/manifest.yaml +1 -1
  59. package/daemon/context-packs/skills/refocusing/scripts/trace-to-root.py +218 -7
  60. package/daemon/context-packs/skills/retiring-and-inheriting-a-seat/manifest.yaml +1 -1
  61. package/daemon/context-packs/skills/seat-continuity-and-handover/manifest.yaml +1 -1
  62. package/daemon/context-packs/skills/session-compaction-and-restore/manifest.yaml +1 -1
  63. package/daemon/context-packs/skills/software-for-agents/manifest.yaml +1 -1
  64. package/daemon/context-packs/world-example/manifest.yaml +1 -1
  65. package/daemon/context-packs/world-public/manifest.yaml +1 -1
  66. package/daemon/dist/build-info.js +3 -3
  67. package/daemon/dist/db/all-migrations.d.ts.map +1 -1
  68. package/daemon/dist/db/all-migrations.js +2 -0
  69. package/daemon/dist/db/all-migrations.js.map +1 -1
  70. package/daemon/dist/db/migrations/098_queue_transitions_qitem_id_order.d.ts +3 -0
  71. package/daemon/dist/db/migrations/098_queue_transitions_qitem_id_order.d.ts.map +1 -0
  72. package/daemon/dist/db/migrations/098_queue_transitions_qitem_id_order.js +12 -0
  73. package/daemon/dist/db/migrations/098_queue_transitions_qitem_id_order.js.map +1 -0
  74. package/daemon/dist/domain/current-work.d.ts +74 -0
  75. package/daemon/dist/domain/current-work.d.ts.map +1 -1
  76. package/daemon/dist/domain/current-work.js +165 -0
  77. package/daemon/dist/domain/current-work.js.map +1 -1
  78. package/daemon/dist/domain/gateway/connections-projection.d.ts +2 -0
  79. package/daemon/dist/domain/gateway/connections-projection.d.ts.map +1 -1
  80. package/daemon/dist/domain/gateway/connections-projection.js +3 -1
  81. package/daemon/dist/domain/gateway/connections-projection.js.map +1 -1
  82. package/daemon/dist/domain/gateway/gateway-subsystem.d.ts +3 -1
  83. package/daemon/dist/domain/gateway/gateway-subsystem.d.ts.map +1 -1
  84. package/daemon/dist/domain/gateway/gateway-subsystem.js +3 -1
  85. package/daemon/dist/domain/gateway/gateway-subsystem.js.map +1 -1
  86. package/daemon/dist/domain/gateway/slack/queue-access.d.ts +3 -0
  87. package/daemon/dist/domain/gateway/slack/queue-access.d.ts.map +1 -1
  88. package/daemon/dist/domain/gateway/slack/queue-access.js +5 -1
  89. package/daemon/dist/domain/gateway/slack/queue-access.js.map +1 -1
  90. package/daemon/dist/domain/gateway/slack/slack-subsystem.d.ts.map +1 -1
  91. package/daemon/dist/domain/gateway/slack/slack-subsystem.js +74 -19
  92. package/daemon/dist/domain/gateway/slack/slack-subsystem.js.map +1 -1
  93. package/daemon/dist/domain/gateway/slack/socket-inbound.d.ts +37 -0
  94. package/daemon/dist/domain/gateway/slack/socket-inbound.d.ts.map +1 -1
  95. package/daemon/dist/domain/gateway/slack/socket-inbound.js +281 -38
  96. package/daemon/dist/domain/gateway/slack/socket-inbound.js.map +1 -1
  97. package/daemon/dist/domain/gateway/slack/state-store.d.ts +14 -1
  98. package/daemon/dist/domain/gateway/slack/state-store.d.ts.map +1 -1
  99. package/daemon/dist/domain/gateway/slack/state-store.js +16 -0
  100. package/daemon/dist/domain/gateway/slack/state-store.js.map +1 -1
  101. package/daemon/dist/domain/human-questions.d.ts +12 -3
  102. package/daemon/dist/domain/human-questions.d.ts.map +1 -1
  103. package/daemon/dist/domain/human-questions.js +10 -5
  104. package/daemon/dist/domain/human-questions.js.map +1 -1
  105. package/daemon/dist/domain/kernel-boot-tracker.d.ts +20 -1
  106. package/daemon/dist/domain/kernel-boot-tracker.d.ts.map +1 -1
  107. package/daemon/dist/domain/kernel-boot-tracker.js +66 -14
  108. package/daemon/dist/domain/kernel-boot-tracker.js.map +1 -1
  109. package/daemon/dist/domain/kernel-boot.d.ts +4 -0
  110. package/daemon/dist/domain/kernel-boot.d.ts.map +1 -1
  111. package/daemon/dist/domain/kernel-boot.js +26 -1
  112. package/daemon/dist/domain/kernel-boot.js.map +1 -1
  113. package/daemon/dist/domain/native-process-lineage.d.ts.map +1 -1
  114. package/daemon/dist/domain/native-process-lineage.js +30 -7
  115. package/daemon/dist/domain/native-process-lineage.js.map +1 -1
  116. package/daemon/dist/domain/queue-repository.d.ts +12 -0
  117. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  118. package/daemon/dist/domain/queue-repository.js +47 -1
  119. package/daemon/dist/domain/queue-repository.js.map +1 -1
  120. package/daemon/dist/domain/queue-transition-log.d.ts +12 -4
  121. package/daemon/dist/domain/queue-transition-log.d.ts.map +1 -1
  122. package/daemon/dist/domain/queue-transition-log.js +24 -0
  123. package/daemon/dist/domain/queue-transition-log.js.map +1 -1
  124. package/daemon/dist/domain/scope/scope-approve.d.ts +21 -0
  125. package/daemon/dist/domain/scope/scope-approve.d.ts.map +1 -1
  126. package/daemon/dist/domain/scope/scope-approve.js +48 -4
  127. package/daemon/dist/domain/scope/scope-approve.js.map +1 -1
  128. package/daemon/dist/routes/gateway.d.ts.map +1 -1
  129. package/daemon/dist/routes/gateway.js +2 -0
  130. package/daemon/dist/routes/gateway.js.map +1 -1
  131. package/daemon/dist/routes/kernel-status.js +3 -1
  132. package/daemon/dist/routes/kernel-status.js.map +1 -1
  133. package/daemon/dist/routes/queue.d.ts.map +1 -1
  134. package/daemon/dist/routes/queue.js +17 -2
  135. package/daemon/dist/routes/queue.js.map +1 -1
  136. package/daemon/dist/routes/scope-approve.d.ts.map +1 -1
  137. package/daemon/dist/routes/scope-approve.js +3 -0
  138. package/daemon/dist/routes/scope-approve.js.map +1 -1
  139. package/daemon/docs/reference/refocus-channel.md +33 -0
  140. package/daemon/docs/reference/slack-app-setup.md +13 -0
  141. package/daemon/docs/reference/whats-new.md +69 -74
  142. package/daemon/specs/agents/shared/skills/core/cross-host-rig-commands/SKILL.md +1 -0
  143. package/daemon/specs/agents/shared/skills/core/messaging-the-human/SKILL.md +12 -4
  144. package/dist/build-info.js +3 -3
  145. package/dist/commands/chatroom.d.ts.map +1 -1
  146. package/dist/commands/chatroom.js +25 -5
  147. package/dist/commands/chatroom.js.map +1 -1
  148. package/dist/commands/queue.d.ts.map +1 -1
  149. package/dist/commands/queue.js +3 -0
  150. package/dist/commands/queue.js.map +1 -1
  151. package/dist/commands/scope.d.ts.map +1 -1
  152. package/dist/commands/scope.js +10 -1
  153. package/dist/commands/scope.js.map +1 -1
  154. package/dist/commands/send.d.ts.map +1 -1
  155. package/dist/commands/send.js +23 -5
  156. package/dist/commands/send.js.map +1 -1
  157. package/dist/commands/slack.d.ts +17 -0
  158. package/dist/commands/slack.d.ts.map +1 -1
  159. package/dist/commands/slack.js +41 -0
  160. package/dist/commands/slack.js.map +1 -1
  161. package/dist/commands/status.d.ts.map +1 -1
  162. package/dist/commands/status.js +5 -0
  163. package/dist/commands/status.js.map +1 -1
  164. package/dist/cross-host-types.d.ts +1 -1
  165. package/dist/cross-host-types.d.ts.map +1 -1
  166. package/dist/lib/scope/scope-fs.d.ts +13 -0
  167. package/dist/lib/scope/scope-fs.d.ts.map +1 -1
  168. package/dist/lib/scope/scope-fs.js +28 -4
  169. package/dist/lib/scope/scope-fs.js.map +1 -1
  170. package/dist/remote-host-ops.d.ts +4 -0
  171. package/dist/remote-host-ops.d.ts.map +1 -1
  172. package/dist/remote-host-ops.js +20 -2
  173. package/dist/remote-host-ops.js.map +1 -1
  174. package/package.json +1 -1
  175. package/tui/dist/connections/connections-model.d.ts +5 -0
  176. package/tui/dist/connections/connections-model.d.ts.map +1 -1
  177. package/tui/dist/connections/connections-model.js +16 -1
  178. package/tui/dist/connections/connections-model.js.map +1 -1
@@ -102,10 +102,29 @@ function readConfiguredContent(home) {
102
102
  // current work, its basis travels to the trace script, which alone decides how to render it;
103
103
  // a failed or unreadable answer is passed as UNKNOWN. Never guess a work node here: a guess
104
104
  // would silently re-point the whole trace.
105
+ // OPR.0.7.0.12 — the work packet, switched off by default: the trace receives the daemon's
106
+ // labelled work candidates and carries duties and notes as text. With it off, every path below
107
+ // is unchanged.
108
+ function workPacketEnabled() {
109
+ return enabled(process.env.OPENRIG_REFOCUS_WORK_PACKET, false);
110
+ }
111
+
105
112
  function readQueueWhoami(timeout = 2_000) {
106
- const result = spawnSync("rig", ["queue", "whoami", "--json"], {
113
+ const args = ["queue", "whoami", "--json", ...(workPacketEnabled() ? ["--work-candidates"] : [])];
114
+ let result = spawnSync("rig", args, {
107
115
  encoding: "utf8", env: process.env, timeout, maxBuffer: 16 * 1024 * 1024,
108
116
  });
117
+ // A rig CLI older than this hook rejects the flag. Ask again without it, inside what is left of
118
+ // the budget, so the role and the strict answer still arrive (just without candidates).
119
+ if (args.includes("--work-candidates") && !result.error && result.status !== 0
120
+ && /unknown option/i.test(`${result.stderr || ""}${result.stdout || ""}`)) {
121
+ const remaining = remainingLookupBudget();
122
+ if (remaining > 250) {
123
+ result = spawnSync("rig", ["queue", "whoami", "--json"], {
124
+ encoding: "utf8", env: process.env, timeout: remaining, maxBuffer: 16 * 1024 * 1024,
125
+ });
126
+ }
127
+ }
109
128
  if (result.error) return { unknown: `queue whoami failed: ${result.error.message}` };
110
129
  if (result.status !== 0 || !result.stdout || !result.stdout.trim()) {
111
130
  return { unknown: `queue whoami exited ${result.status ?? "without a status"} with no answer` };
@@ -208,11 +227,31 @@ function renderTrace() {
208
227
  }
209
228
  role = renderRole(whoami);
210
229
  }
211
- if (work.start) args.push("--work-start", work.start);
230
+ const packet = workPacketEnabled();
231
+ let evidence = null;
232
+ if (packet) {
233
+ args.push("--packet");
234
+ if (trees !== "topology") {
235
+ if (!whoami) {
236
+ const timeout = remainingLookupBudget();
237
+ whoami = timeout > 250 ? readQueueWhoami(timeout) : { unknown: "no budget left" };
238
+ }
239
+ const candidates = whoami.answer?.workCandidates;
240
+ if (candidates && typeof candidates === "object") evidence = JSON.stringify(candidates);
241
+ }
242
+ }
243
+ // An explicit start always wins. Otherwise the candidates replace the strict answer: the
244
+ // strict node ignores mission-only rows, so it could hide a second mission in flight.
245
+ const explicitStart = Boolean(process.env.OPENRIG_REFOCUS_WORK_NODE);
246
+ if (work.start && (explicitStart || !evidence)) args.push("--work-start", work.start);
247
+ else if (evidence) args.push("--work-candidates", "-");
212
248
  else if (work.basis) args.push("--work-basis", work.basis);
213
249
  else if (work.unknown) args.push("--work-unknown", work.unknown);
250
+ // With an explicit start, the evidence supplies only held and next work.
251
+ if (explicitStart && evidence) args.push("--work-candidates", "-");
214
252
  const result = spawnSync(process.env.PYTHON || "python3", args, {
215
253
  encoding: "utf8",
254
+ ...(evidence ? { input: evidence } : {}),
216
255
  env: traceEnv(trees),
217
256
  timeout: 2_000,
218
257
  maxBuffer: 16 * 1024 * 1024,
@@ -104,10 +104,18 @@ a JSON array of 1–4 questions, each
104
104
  options (labels up to 75 characters, at most one recommended). Slack shows each
105
105
  question as a row of buttons. Each click records that answer on the item, and
106
106
  the decision resolves once every question has one. You then receive one reply
107
- row listing the answers, and the item's `humanAnswers` holds the option ids. The
108
- human may instead type a reply in the thread; that resolves the decision as
109
- usual, so read the reply rather than assuming an option was picked. Keep the
110
- brief complete: the questions add buttons, they do not replace the explanation.
107
+ row listing the answers. In `humanAnswers`, clicked answers are option-id strings.
108
+ A whole typed reply is an object:
109
+ `{kind: "typed-reply", text: "…", placement: "first-unanswered", unansweredCount: N}`.
110
+ It is placed automatically under the first unanswered question; the person did
111
+ not select that question. `N` counts the other question slots still empty in
112
+ `humanAnswers` after that placement.
113
+ Earlier button answers stay intact. Read `text` as the person's words, never as
114
+ an option id even if the strings match. The typed reply still closes the decision
115
+ with the other questions unanswered; `done` is not approval. If every button
116
+ answer was already recorded, that final answer set stays intact and the typed
117
+ reply remains in its correlated reply row. Keep the brief complete: the questions
118
+ add buttons, they do not replace the explanation.
111
119
 
112
120
  If an existing agent-owned row must wait for a **decision**, block it on the **new live qitem ID**
113
121
  (`rig queue block <work-id> --on <human-qitem-id> ...`), not on the human address.
@@ -2,10 +2,12 @@
2
2
  """Render topology and work context by ascending directory paths only."""
3
3
 
4
4
  import argparse
5
+ import datetime
5
6
  import json
6
7
  import os
7
8
  import re
8
9
  import subprocess
10
+ import sys
9
11
  from pathlib import Path
10
12
 
11
13
  SHELVES = {"rigs", "pods", "seats", "missions", "slices"}
@@ -148,7 +150,82 @@ def light_learned(text):
148
150
  return body[:boundary if boundary > 0 else 800].rstrip() + "\n[… use --depth full for the rest]"
149
151
 
150
152
 
151
- def render_topology(start, root, depth, failures):
153
+ # OPR.0.7.0.12 — the refocus packet (--packet). Duties and notes are carried as text by
154
+ # conventional section, with an honest "not found" and a bounded excerpt otherwise; nobody is
155
+ # asked to add headings. Old files are named with their measured age as a cue, not a verdict.
156
+ DUTY_HEADINGS = ("my job here", "standing duties")
157
+ NOTES_STATE_HEADINGS = ("current state",)
158
+ HEADING = re.compile(r"^(#{1,6})\s+(.*?)\s*#*\s*$")
159
+ DEFAULT_NOTES_MAX_AGE_DAYS = 14
160
+
161
+
162
+ def heading_sections(text, wanted):
163
+ """[(title, body)] for each Markdown heading whose title starts with a wanted phrase."""
164
+ lines = re.sub(r"^---\s*\n.*?\n---\s*\n", "", text or "", count=1, flags=re.S).splitlines()
165
+ found = []
166
+ for index, line in enumerate(lines):
167
+ match = HEADING.match(line)
168
+ if not match or not match.group(2).strip().lower().startswith(wanted):
169
+ continue
170
+ level = len(match.group(1))
171
+ body = []
172
+ for later in lines[index + 1:]:
173
+ heading = HEADING.match(later)
174
+ if heading and len(heading.group(1)) <= level:
175
+ break
176
+ body.append(later)
177
+ found.append((match.group(2).strip(), "\n".join(body).strip()))
178
+ return found
179
+
180
+
181
+ def by_section(text, wanted, path):
182
+ found = heading_sections(text, wanted)
183
+ shown = "\n\n".join(f"{title}\n{body}" if body else title for title, body in found)
184
+ missing = [phrase for phrase in wanted if not any(title.lower().startswith(phrase) for title, _ in found)]
185
+ if not missing:
186
+ return shown
187
+ if not found:
188
+ names = " or ".join(f'"{phrase.upper()}"' for phrase in wanted)
189
+ return f"SECTION NOT FOUND — no {names} heading in {path}; bounded excerpt follows\n{light_learned(text)}"
190
+ # Some sections matched: name each one that did not, with the same bounded excerpt, so text
191
+ # kept under another heading is not silently dropped.
192
+ names = ", ".join(f'"{phrase.upper()}"' for phrase in missing)
193
+ return f"{shown}\n\nSECTION NOT FOUND — no {names} heading in {path}; bounded excerpt follows\n{light_learned(text)}"
194
+
195
+
196
+ def notes_max_age_days():
197
+ try:
198
+ value = float(os.environ.get("OPENRIG_REFOCUS_NOTES_MAX_AGE_DAYS", DEFAULT_NOTES_MAX_AGE_DAYS))
199
+ except ValueError:
200
+ return DEFAULT_NOTES_MAX_AGE_DAYS
201
+ return value if value > 0 else DEFAULT_NOTES_MAX_AGE_DAYS
202
+
203
+
204
+ def age_cue(path, text):
205
+ """An OLD NOTES line when the file is past the threshold, measured from `updated:` or mtime."""
206
+ threshold = notes_max_age_days()
207
+ updated, source = None, "mtime"
208
+ match = re.match(r"^---\s*\n(.*?)\n---", text or "", re.S)
209
+ if match:
210
+ found = re.search(r"^updated:\s*[\"']?(\d{4}-\d{2}-\d{2})", match.group(1), re.M)
211
+ if found:
212
+ try:
213
+ updated = datetime.datetime.strptime(found.group(1), "%Y-%m-%d").replace(tzinfo=datetime.timezone.utc)
214
+ source = "updated"
215
+ except ValueError:
216
+ updated = None
217
+ if updated is None:
218
+ try:
219
+ updated = datetime.datetime.fromtimestamp(path.stat().st_mtime, datetime.timezone.utc)
220
+ except OSError:
221
+ return None
222
+ age = (datetime.datetime.now(datetime.timezone.utc) - updated).total_seconds() / 86400
223
+ if age <= threshold:
224
+ return None
225
+ return f"OLD NOTES — {path}, {age:.0f} days old, source {source}, threshold {threshold:g} days"
226
+
227
+
228
+ def render_topology(start, root, depth, failures, packet=False):
152
229
  output = ["## TOPOLOGY TRACE", f"root: {root}", f"start: {start}"]
153
230
  nodes, error = ascent(start, root)
154
231
  if error:
@@ -161,12 +238,21 @@ def render_topology(start, root, depth, failures):
161
238
  if text is None:
162
239
  output.append(f"\n### {label}\nMISSING LINK — {chain_file}")
163
240
  continue
164
- body = text.strip() if depth == "full" else light_learned(text)
241
+ if depth == "full":
242
+ body = text.strip()
243
+ elif packet and node == nodes[-1]:
244
+ # The seat's own file: its job and standing duties as text.
245
+ body = by_section(text, DUTY_HEADINGS, chain_file)
246
+ else:
247
+ body = light_learned(text)
165
248
  output.append(f"\n### {label} · LEARNED.md\n{body}")
249
+ cue = age_cue(chain_file, text) if packet else None
250
+ if cue:
251
+ output.append(cue)
166
252
  return "\n".join(output)
167
253
 
168
254
 
169
- def render_work(start, root, depth, failures, fallback=None):
255
+ def render_work(start, root, depth, failures, fallback=None, packet=False):
170
256
  output = ["## WORK TRACE", f"root: {root}", f"start: {start}"]
171
257
  if fallback:
172
258
  output.append(f"FALLBACK — {fallback}")
@@ -201,6 +287,19 @@ def render_work(start, root, depth, failures, fallback=None):
201
287
  output.append(f"NOTES RESOLUTION GAP — resolved {notes_name} became unreadable at {notes_path}")
202
288
  else:
203
289
  output.append(f"\nNOTES · {notes_name}\n{notes_text.strip()}")
290
+ cue = age_cue(notes_path, notes_text) if packet else None
291
+ if cue:
292
+ output.append(cue)
293
+ elif packet:
294
+ notes_text = read(notes_path)
295
+ if notes_text is None:
296
+ failures.append(f"unreadable notes: {notes_path}")
297
+ output.append(f"NOTES RESOLUTION GAP — resolved {notes_name} became unreadable at {notes_path}")
298
+ else:
299
+ output.append(f"\nNOTES · {notes_name}\n{by_section(notes_text, NOTES_STATE_HEADINGS, notes_path)}")
300
+ cue = age_cue(notes_path, notes_text)
301
+ if cue:
302
+ output.append(cue)
204
303
  else:
205
304
  try:
206
305
  size = notes_path.stat().st_size
@@ -211,9 +310,107 @@ def render_work(start, root, depth, failures, fallback=None):
211
310
  output.append(f"NOTES · {notes_name} · {size} bytes · {notes_path}")
212
311
  else:
213
312
  output.append(f"NOTES GAP — no readable mission notes at {node}")
313
+
314
+ # The SDLC convention keeps a node's current delivery state in PROGRESS.md; carry it
315
+ # as text when present. Its absence is not a gap: many nodes have no PROGRESS.md.
316
+ progress_path = node / "PROGRESS.md"
317
+ progress_text = read(progress_path) if packet else None
318
+ if progress_text is not None:
319
+ progress_body = progress_text.strip() if depth == "full" else by_section(progress_text, NOTES_STATE_HEADINGS, progress_path)
320
+ output.append(f"\nPROGRESS · PROGRESS.md\n{progress_body}")
321
+ cue = age_cue(progress_path, progress_text)
322
+ if cue:
323
+ output.append(cue)
324
+ return "\n".join(output)
325
+
326
+
327
+ def candidate_line(candidate):
328
+ name = candidate.get("mission") or "?"
329
+ if candidate.get("slice"):
330
+ name += f" / {candidate['slice']}"
331
+ intent_text = candidate.get("intent") or "no readable intent"
332
+ line = f"- {name} — {intent_text} — source: {candidate.get('source') or 'unknown'}"
333
+ if candidate.get("unresolved"):
334
+ line += f" — UNRESOLVED ON THIS HOST: {candidate['unresolved']}"
335
+ return line
336
+
337
+
338
+ def render_held_and_next(evidence):
339
+ output = []
340
+ held = evidence.get("held") or []
341
+ if held:
342
+ output.append("HELD WORK (blocked; this is not next):")
343
+ for row in held:
344
+ output.append(f"- {row.get('qitemId') or 'a row'} — {row.get('summary') or 'no summary'} — "
345
+ f"blocked on {row.get('blockedOn') or 'unknown'} — "
346
+ f"continuation: {row.get('continuation') or 'none recorded'}")
347
+ following = evidence.get("next") or []
348
+ if following:
349
+ output.append("POSSIBLE NEXT WORK (pending; no order implied):")
350
+ for row in following:
351
+ missions = ", ".join(row.get("missions") or [])
352
+ output.append(f"- {row.get('qitemId') or 'a row'} — {row.get('summary') or 'no summary'}"
353
+ + (f" [mission: {missions}]" if missions else ""))
354
+ if evidence.get("heldAndNextTruncated"):
355
+ output.append("(held and next lists were cut; `rig queue list` has the rest)")
214
356
  return "\n".join(output)
215
357
 
216
358
 
359
+ def render_candidates(evidence, root, depth, failures):
360
+ """The work section from labelled queue evidence. One direct, resolved tag is shown as the
361
+ work with its source; anything less certain lists every candidate and asks the agent."""
362
+ candidates = evidence.get("candidates") or []
363
+ unknown = evidence.get("unknown") or []
364
+ if (len(candidates) == 1 and not unknown and candidates[0].get("kind") == "tag"
365
+ and candidates[0].get("workNodePath") and not candidates[0].get("unresolved")):
366
+ only = candidates[0]
367
+ notes = [f"WORK FROM QUEUE EVIDENCE — {only.get('source')}. If this is stale, say what you are actually doing."]
368
+ if not only.get("slice"):
369
+ notes.append("no slice named")
370
+ traced = render_work(Path(only["workNodePath"]), root, depth, failures, packet=True)
371
+ head, _, rest = traced.partition("\n")
372
+ return "\n".join([head, *notes, rest])
373
+
374
+ missions_root = evidence.get("missionsRoot") or str(root / "missions")
375
+ output = ["## WORK — name it"]
376
+ if not candidates and not unknown:
377
+ output.append("No in-progress queue row names your work. If you are working from messages, "
378
+ "name the mission they belong to; if you have no current work, say so.")
379
+ elif len(candidates) > 1:
380
+ output.append(f"{len(candidates)} candidates are in flight. Say which mission you are actually working on.")
381
+ else:
382
+ output.append("The queue evidence does not settle your work. Confirm a candidate or name your mission.")
383
+ if candidates:
384
+ output.append("Candidates (evidence, not a verdict; none is adopted for you):")
385
+ output.extend(candidate_line(candidate) for candidate in candidates)
386
+ if unknown:
387
+ output.append("In-progress rows the queue cannot place:")
388
+ output.extend(f"- {row.get('qitemId') or 'a row'} — {row.get('summary') or 'no summary'}: {row.get('reason')}"
389
+ for row in unknown)
390
+ if not candidates:
391
+ names = evidence.get("missionsOnHost") or []
392
+ if names:
393
+ listing = ", ".join(names) + (", …" if evidence.get("missionsOnHostTruncated") else "")
394
+ output.append(f"Missions on this host: {listing}")
395
+ output.append("Then read that mission's current intent as text:")
396
+ output.append(f" python3 {Path(__file__).resolve()} --trees work --packet --work-start {missions_root}/<mission>")
397
+ orientation = render_work(root, root, "light", failures, packet=True)
398
+ _, _, rest = orientation.partition("\n")
399
+ output.append("\n## ORIENTATION — the project-level chain, not your work\n" + rest.lstrip("\n"))
400
+ return "\n".join(output)
401
+
402
+
403
+ def read_candidates(source):
404
+ try:
405
+ raw = sys.stdin.read() if source == "-" else Path(source).read_text(encoding="utf8")
406
+ evidence = json.loads(raw)
407
+ except (OSError, json.JSONDecodeError) as error:
408
+ return None, f"work candidates unreadable: {error}"
409
+ if not isinstance(evidence, dict) or not isinstance(evidence.get("candidates", []), list):
410
+ return None, "work candidates have an invalid shape"
411
+ return evidence, None
412
+
413
+
217
414
  def canonical_seat(session):
218
415
  """(member, rig) for a canonical session name, else None so the caller asks `rig whoami`.
219
416
 
@@ -279,6 +476,8 @@ def main():
279
476
  parser.add_argument("--work-basis", help="the daemon's reason no current work node was named")
280
477
  parser.add_argument("--work-unknown", help="why the current work node could not be read")
281
478
  parser.add_argument("--check", action="store_true", help="exit nonzero on trace or notes resolution failure; rendered text is unchanged")
479
+ parser.add_argument("--packet", action="store_true", help="carry duties and notes as text by section, with old-notes cues")
480
+ parser.add_argument("--work-candidates", help="labelled work evidence as JSON (a path, or - for stdin), from `rig queue whoami --work-candidates`")
282
481
  args = parser.parse_args()
283
482
 
284
483
  sections = []
@@ -293,7 +492,7 @@ def main():
293
492
  sections.append(gap("## TOPOLOGY TRACE\nTRACE GAP — topology.root is unresolved"))
294
493
  else:
295
494
  start = Path(args.topology_start) if args.topology_start else derive_topology_start(root)
296
- sections.append(render_topology(start, root, args.depth, failures) if start else
495
+ sections.append(render_topology(start, root, args.depth, failures, packet=args.packet) if start else
297
496
  gap("## TOPOLOGY TRACE\nTRACE GAP — current topology node is unresolved; pass --topology-start with a literal absolute path (or configure OPENRIG_REFOCUS_TOPOLOGY_NODE separately)"))
298
497
 
299
498
  if args.trees in {"work", "both"}:
@@ -304,10 +503,19 @@ def main():
304
503
  # Precedence: an explicit start wins; then the hook's daemon answer (basis or unknown);
305
504
  # only a standalone run with neither falls back to inferring from the working directory.
306
505
  explicit = args.work_start or os.environ.get("OPENRIG_REFOCUS_WORK_NODE")
506
+ evidence = None
507
+ if args.work_candidates:
508
+ evidence, error = read_candidates(args.work_candidates)
509
+ if error:
510
+ sections.append(gap(f"## WORK TRACE\nTRACE GAP — {error}"))
307
511
  if explicit:
308
- sections.append(render_work(Path(explicit), root, args.depth, failures))
512
+ sections.append(render_work(Path(explicit), root, args.depth, failures, packet=args.packet))
513
+ elif evidence is not None:
514
+ sections.append(render_candidates(evidence, root, args.depth, failures))
515
+ elif args.work_candidates:
516
+ pass # the unreadable-evidence gap above is the answer; never fall back to the root
309
517
  elif args.work_basis == NO_CURRENT_BATON_BASIS:
310
- sections.append(render_work(root, root, args.depth, failures, fallback=(
518
+ sections.append(render_work(root, root, args.depth, failures, packet=args.packet, fallback=(
311
519
  f"no current typed baton ({args.work_basis}). Showing the project-level chain from the work root: "
312
520
  "a broad orientation, not evidence of a current mission")))
313
521
  elif args.work_basis:
@@ -316,8 +524,11 @@ def main():
316
524
  sections.append(gap(f"## WORK TRACE\nTRACE GAP — current work node UNKNOWN: {args.work_unknown}"))
317
525
  else:
318
526
  start = derive_work_start(root)
319
- sections.append(render_work(start, root, args.depth, failures) if start else
527
+ sections.append(render_work(start, root, args.depth, failures, packet=args.packet) if start else
320
528
  gap("## WORK TRACE\nTRACE GAP — current work node is unresolved; pass --work-start with a literal absolute path (or configure OPENRIG_REFOCUS_WORK_NODE separately)"))
529
+ held_and_next = render_held_and_next(evidence) if evidence else ""
530
+ if held_and_next:
531
+ sections.append(held_and_next)
321
532
 
322
533
  print("\n\n".join(sections))
323
534
  return 1 if args.check and failures else 0
@@ -1,6 +1,6 @@
1
1
  # Agent help: the single troubleshooting and escalation guide (docs/reference/help.md).
2
2
  name: help
3
- version: "0.6.8"
3
+ version: "0.6.9"
4
4
  taxonomy: world
5
5
  purpose: "What an agent does when its user says OpenRig isn't working: diagnose, fix within permissions, verify, and escalate to OpenRig support."
6
6
  files:
@@ -1,6 +1,6 @@
1
1
  # Public capability and source-map width for the default onboarding walk.
2
2
  name: onboarding-width
3
- version: "0.6.8"
3
+ version: "0.6.9"
4
4
  taxonomy: world
5
5
  purpose: "The public capability map and source map referenced by the default onboarding walk."
6
6
  files:
@@ -11,7 +11,7 @@ not know exists.** So the point of what follows is not skill. It is recognition:
11
11
  lands, something rhymes, and you go check instead of building.
12
12
 
13
13
  Eighty-seven top-level verbs ship (capability canon refreshed through
14
- `capability-delta-v0.6.8`). This marker describes the pack's teaching, not publication or live
14
+ `capability-delta-v0.6.9`). This marker describes the pack's teaching, not publication or live
15
15
  adoption. Read this once for shape, and let it make you suspicious that a thing already exists.
16
16
  Model-divergence proclamations are live product (trust them; pins use canonical
17
17
  model IDs).
@@ -89,6 +89,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
89
89
  confirm by effect, never relaunch on it. A send or wake to a seat whose latest hook is a
90
90
  question waiting for a person is refused with `target_needs_input`, even when its screen
91
91
  can't be read: read it with `rig capture` and get the question answered instead of retrying.
92
+ Over `--host` to an http-registered host, a send that may have reached the remote reads
93
+ `remote-outcome-unknown` ("Delivery UNCONFIRMED"): check the target with `rig capture <session>
94
+ --host <id>` before any resend. Only a host that was never reached reads as unreachable.
92
95
  - **`rig send --raw`** — send exact text or keystrokes without the From/To envelope;
93
96
  the interactive-prompt guard still applies. To deliberately drive an interactive prompt,
94
97
  **`--dangerously-interact --reason "<why>"`** is the explicit override and implies raw text.
@@ -110,7 +113,9 @@ unseen — which makes messages the one delivery channel that never gets skipped
110
113
  runtime tokens.
111
114
  - **`rig chatroom wait`** — block until a peer actually says something, instead of polling
112
115
  `capture` in a loop. Also `history`, `watch`, `topic` for a named thread that stays retrievable.
113
- Its `--timeout` is in seconds and ignores a suffix, so `2m` means 2 seconds.
116
+ Its `--timeout` is in seconds and ignores a suffix, so `2m` means 2 seconds. Poll timeouts,
117
+ connection refusals, and reset or closed connections are retried until that deadline; other errors,
118
+ including an error answer from the daemon such as a removed rig, still end the wait.
114
119
  - **`rig stream emit` / `list` / `watch` / `archive`** — drop an observation somewhere the *next*
115
120
  agent will find it. Emitting costs nothing and does not interrupt anyone; the value appears when
116
121
  someone lists the stream before starting.
@@ -148,7 +153,10 @@ unseen — which makes messages the one delivery channel that never gets skipped
148
153
  readiness for the delivery decision and `queue create --verify` for its receipt; connector
149
154
  setup or verification is not an outbound human message. After a connection gap the daemon
150
155
  catches up on missed channel messages and marks them late; `rig slack status` shows the
151
- connection and the catch-up state. **`rig slack channel-map list` / `set <match> <channel>` /
156
+ connection, the catch-up state, and the count of retained retry records for inbound messages,
157
+ reactions and click answers (kept across restarts, not a distinct-message count). A retained alert whose row closed before its retry isn't
158
+ posted; the row says why. Its `Delivery:` line says whether events are confirmed arriving;
159
+ "connected" alone is not delivery, and a connection that stops delivering is replaced. **`rig slack channel-map list` / `set <match> <channel>` /
152
160
  `remove <match>`** sends a rig's or seat's human-bound posts to its own channel (a seat match
153
161
  beats a rig match; the rest use the default channel). After a change, invite the app to each
154
162
  channel, run `rig slack verify`, and rewire the connector (`rig slack disable` then `enable`, or
@@ -340,8 +348,8 @@ not have to tear one down to change it, and you rarely have to start from nothin
340
348
  human mode it first prints one line with each agent's last known activity, or “unknown”: a
341
349
  best-effort snapshot, not a record of what was interrupted. With `--host`, a remote teardown
342
350
  that reports errors exits 2.
343
- - **`rig launch <rig> [seat]`** — one seat is down; start just that one, without disturbing the
344
- rest. Add `--plan` to preview it first, locally or with `--host`; a plan is never sent to a
351
+ - **`rig launch <rigId> [seat]`** — one seat is down; start just that one, without disturbing the
352
+ rest. It takes the rig ID from `rig ps --json`; a rig name gives `rig_not_found`. Add `--plan` to preview it first, locally or with `--host`; a plan is never sent to a
345
353
  daemon older than 0.5.9, which would ignore it and launch.
346
354
  - **`rig seat launch <seat> --fresh --reason <why>`** — deliberately create a blank occupant for
347
355
  exactly one existing seat. It uses no resume, fork, rebuild, snapshot, or restore packet;
@@ -519,7 +527,8 @@ scheme, and nothing downstream can see it.
519
527
  - **`rig scope slice progress` / `mission progress`** — record that a step moved, in a form the
520
528
  progress view can parse.
521
529
  - **`rig scope slice approve`** — freeze a decision — *this is the plan* or *this is delivered* —
522
- so the freeze is recorded rather than asserted in chat.
530
+ so the freeze is recorded rather than asserted in chat. With `--workspace`, approve stamps the
531
+ tree you named, and refuses one with no `missions/` folder.
523
532
  - **`rig scope slice close` / `ship` / `move`** — retire it with the reason attached, move it into
524
533
  the release it belongs to preserving git history, or re-file it under a different mission.
525
534
  - **`rig scope slice stage` / `verified`** — how mature is this, and *when was it last checked and
@@ -679,7 +688,8 @@ reaching them is ordinary work rather than an escalation.
679
688
  request, then boundedly inspect its transport receipt. Posted means posted, not read;
680
689
  pending, failed, or indeterminate delivery leaves the row intact. Inspect the same row's
681
690
  transitions before retrying. `rig send` remains agent-terminal delivery only.
682
- `--human-questions-file` adds clickable decision questions.
691
+ `--human-questions-file` adds clickable decision questions. A typed Slack reply to one is kept
692
+ in `humanAnswers`, placed under the first unanswered question; closing isn't approval.
683
693
  - **When project policy calls for human judgment or an update, load `messaging-the-human`.**
684
694
  Use `rig context get skills/core/messaging-the-human/SKILL.md` for a complete decision brief,
685
695
  related supplemental detail and explicit quiet-update intent. An FYI creates no approval
@@ -1,7 +1,7 @@
1
1
  # Reference guides the help pack links to, loaded one file or section at a time by address
2
2
  # (rig context get reference/<file>#<section>). Each file is a symlink to docs/reference/.
3
3
  name: reference
4
- version: "0.6.8"
4
+ version: "0.6.9"
5
5
  taxonomy: world
6
6
  purpose: "Version-matched reference guides the help guide links to. Load a single file or section by address; don't load the whole pack."
7
7
  files: