wogiflow 2.29.2 → 2.29.4

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 (35) hide show
  1. package/.claude/docs/intent-grounded-reasoning.md +1 -1
  2. package/.workflow/templates/partials/methodology-rules.hbs +30 -1
  3. package/lib/commands/team-connection.js +5 -28
  4. package/lib/utils.js +12 -26
  5. package/lib/wogi-claude +40 -1
  6. package/lib/workspace-channel-server.js +21 -0
  7. package/lib/workspace-channel-tracking.js +125 -0
  8. package/lib/workspace.js +6 -13
  9. package/package.json +2 -2
  10. package/scripts/flow +4 -0
  11. package/scripts/flow-autonomous-detector.js +29 -4
  12. package/scripts/flow-autonomous-mode.js +27 -7
  13. package/scripts/flow-completion-summary.js +2 -16
  14. package/scripts/flow-id.js +31 -0
  15. package/scripts/flow-io.js +78 -0
  16. package/scripts/flow-long-input-pending.js +110 -0
  17. package/scripts/flow-long-input-stories.js +8 -0
  18. package/scripts/flow-orchestrate.js +16 -10
  19. package/scripts/flow-question-queue.js +73 -7
  20. package/scripts/flow-scanner-base.js +77 -1
  21. package/scripts/flow-session-state.js +47 -0
  22. package/scripts/flow-source-fidelity.js +279 -0
  23. package/scripts/flow-time-format.js +42 -0
  24. package/scripts/flow-utils.js +3 -16
  25. package/scripts/flow-worker-mcp-strip.js +12 -11
  26. package/scripts/flow-workspace-summary.js +38 -19
  27. package/scripts/hooks/adapters/claude-code.js +7 -4
  28. package/scripts/hooks/core/long-input-enforcement.js +311 -0
  29. package/scripts/hooks/core/pre-tool-deps.js +185 -0
  30. package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
  31. package/scripts/hooks/core/session-context.js +26 -0
  32. package/scripts/hooks/core/task-boundary-reset.js +13 -0
  33. package/scripts/hooks/core/worker-boundary-gate.js +67 -16
  34. package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
  35. package/scripts/hooks/entry/claude-code/user-prompt-submit.js +33 -0
@@ -18,7 +18,7 @@ Research finding: across 1,309 user messages mined, first-pass agent output was
18
18
  | 1 | **Intent Bootstrap** — scaffolds product/domain/glossary/user-journeys artifacts; agnostic trap-zone detector finds structural ambiguities | `scripts/flow-intent-bootstrap.js` + `scripts/flow-trap-zone.js` |
19
19
  | 2 | **Intent Framing Pass** — per-task reasoning step; produces a Framing Artifact resolving ambiguities before any other work | `scripts/flow-intent-framing.js` |
20
20
  | 3 | **Architect Pass** — read-only sub-agent produces an 8-section pre-spec plan | `scripts/flow-architect-pass.js` + persona `.workflow/agents/architect.md` |
21
- | 4 | **Logic Adversary** — separate sub-agent on a different model critiques the plan against the 11-principle Logic Constitution (v2: P11 + sub-principles 11.1–11.4 covering observed-behavior, project rules, sibling features, and stacked-story integration) | `scripts/flow-logic-adversary.js` + rubric `.workflow/rubrics/logic-constitution-v2.md` |
21
+ | 4 | **Logic Adversary** — separate sub-agent on a different model critiques the plan against the 11-principle Logic Constitution (v3 default: P11 + sub-principles 11.1–11.6 covering observed-behavior, project rules, sibling features, generative edge-case taxonomy, stacked-story integration, and temporal source coverage) | `scripts/flow-logic-adversary.js` + rubric `.workflow/rubrics/logic-constitution-v3.md` |
22
22
  | 5 | **Session Correction Memory** — detects user corrections during a session and cross-references back to gates that passed the contradicted work | extensions in `scripts/flow-correction-detector.js` |
23
23
  | 6 | **Completion Truth Gate** — audits "done" claims against Tier 0–4 evidence; downgrades language when evidence is insufficient | `scripts/flow-completion-truth-gate.js` |
24
24
  | 7 | **Pipeline wiring + rollout** — integrates all above into `/wogi-start`, the gate registry, the eval framework | (this story) |
@@ -135,6 +135,35 @@ If artifacts don't exist yet, run `node scripts/flow-intent-bootstrap.js bootstr
135
135
 
136
136
  ---
137
137
 
138
+ ### Source Fidelity Rule (Verbatim Source Preservation)
139
+
140
+ When a long-form user request becomes a spec, channel-dispatch message, or any artifact that downstream actors will execute, the **verbatim source MUST be preserved alongside the structured derivation**.
141
+
142
+ The lossy step in cross-session/cross-worker compression is almost always at the spec-authoring layer (manager summarizing user input into a "contract"). Downstream actors then build the summary's interpretation, missing items the user explicitly named. Adversary checks won't catch this because the adversary sees only the spec, not the original prompt.
143
+
144
+ **Mandatory structure for any spec or dispatch derived from a long user prompt** (>40 lines OR ≥5 discrete items):
145
+
146
+ 1. **`## Original Request (verbatim)` block** — the user's prompt unmodified. Required at the top of the spec body.
147
+
148
+ 2. **`## Item Manifest` block** — enumerated list reconciling every source item to either:
149
+ - A specific AC in the spec, OR
150
+ - An explicit `defer-with-reason: <user-cited reason>` entry. The deferral is the user's call, not the AI's. AI-judged "low priority" is NOT a valid reason.
151
+
152
+ 3. **Channel-dispatch links the spec, not summarizes it.** Manager-to-worker channel messages that create work MUST include either the verbatim source OR a path to a saved spec file containing the verbatim source. Bare "summary contracts" sent without source link are forbidden.
153
+
154
+ **Why this rule exists:** the 2026-04-27 wogi-hub Customers > Services incident — user provided a ~50-line spec for a UI page; manager compressed into a 5-bullet "owner-locked decisions" channel-dispatch message; downstream FE worker built the bullet contract literally; result was 5 of 12 user-named features built. The build looked locally correct but didn't match the user's actual ask. Three existing safeguards all failed to catch it: long-input gate (output rolled up, not preserved as canonical), feature dossier (didn't exist for this feature — chicken-and-egg), anti-deferral rule (text only, no mechanical enforcement at spec-write time).
155
+
156
+ **Anti-rationalization checklist** — if any of these thoughts cross your mind, you are about to violate the rule:
157
+ - *"I've captured the key decisions in N bullets"* → WRONG. Items the user named are not yours to filter.
158
+ - *"The downstream worker doesn't need the full prompt; the spec is enough"* → WRONG. The spec is YOUR interpretation. The worker should be able to verify against source.
159
+ - *"The user's prompt was rambling; my summary is cleaner"* → WRONG. Cleanliness is not authority to filter user-named items.
160
+ - *"This is just an internal manager message; the user won't see it"* → WRONG. That's exactly when the lossy step happens; verbatim preservation is more important here, not less.
161
+ - *"The long-input gate already extracted the items"* → WRONG IF you don't pin its output as canonical and reconcile every spec against it.
162
+
163
+ **Enforcement:** Logic Constitution v3 sub-principle 11.6 (Temporal Source Coverage). Adversary verifies every spec against its `Original Request (verbatim)` block before approval. Specs missing the block when source qualifies for it → BLOCKED at spec_review approval. Verifier CLI: `node scripts/flow-source-fidelity.js check <spec-file>`. Worker-side fallback: `scripts/hooks/core/long-input-enforcement.js` injects forcing instruction at UserPromptSubmit when channel-dispatch arrives long-form without source-link.
164
+
165
+ ---
166
+
138
167
  ### Cross-Story Integration Tier-3 Rule
139
168
 
140
169
  When Story B layers behavior on top of infrastructure shipped by Story A (or any prior commit), Story B's IGR pass MUST treat that infrastructure as an audited dependency, not as a given. Within-module unit tests that pin Story B's local behavior do NOT verify that Story A's contract holds for Story B's usage.
@@ -162,7 +191,7 @@ When Story B layers behavior on top of infrastructure shipped by Story A (or any
162
191
  - `grep -r "<Story A's interface>"` — is the contract still intact in HEAD?
163
192
  - Write the Tier-3 test BEFORE writing Story B's code. If the test cannot be written without first standing up infrastructure that makes the integration verifiable, that's a signal the architecture needs that infrastructure too.
164
193
 
165
- Enforced by: Logic Constitution v2 sub-principle 11.4 (Stacked-story integration verification). Pre-release gate consumes this signal before tagging.
194
+ Enforced by: Logic Constitution v3 sub-principle 11.5 (Stacked-story integration verification). Pre-release gate consumes this signal before tagging.
166
195
 
167
196
  ---
168
197
 
@@ -11,6 +11,7 @@ const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const http = require('node:http');
13
13
  const https = require('node:https');
14
+ const { safeJsonParseStringStrip } = require('../../scripts/flow-io');
14
15
 
15
16
  const CONNECTION_FILE = '.workflow/team-connection.json';
16
17
  const REQUEST_TIMEOUT_MS = 15000;
@@ -18,36 +19,12 @@ const MAX_RESPONSE_BYTES = 1 * 1024 * 1024; // 1 MB cap on response body
18
19
 
19
20
  /**
20
21
  * Safely parse JSON with prototype pollution protection.
21
- * Checks for dangerous keys in parsed objects (including nested).
22
+ * Delegates to flow-io's canonical safeJsonParseStringStrip (audit dup-004
23
+ * consolidation 2026-04-26). Behavior preserved verbatim — both impls
24
+ * recursively strip __proto__/constructor/prototype keys.
22
25
  */
23
26
  function safeParseJson(str, fallback) {
24
- try {
25
- const obj = JSON.parse(str);
26
- if (obj && typeof obj === 'object') {
27
- stripDangerousKeys(obj);
28
- }
29
- return obj;
30
- } catch (_err) {
31
- return fallback;
32
- }
33
- }
34
-
35
- /**
36
- * Recursively strip __proto__, constructor, prototype keys from an object.
37
- */
38
- function stripDangerousKeys(obj) {
39
- if (!obj || typeof obj !== 'object') return;
40
- const dangerous = ['__proto__', 'constructor', 'prototype'];
41
- for (const key of dangerous) {
42
- if (Object.hasOwn(obj, key)) {
43
- delete obj[key];
44
- }
45
- }
46
- for (const key of Object.keys(obj)) {
47
- if (obj[key] && typeof obj[key] === 'object') {
48
- stripDangerousKeys(obj[key]);
49
- }
50
- }
27
+ return safeJsonParseStringStrip(str, fallback);
51
28
  }
52
29
 
53
30
  /**
package/lib/utils.js CHANGED
@@ -74,32 +74,18 @@ function findProjectRoot() {
74
74
  * @returns {Object} Parsed object or default value
75
75
  */
76
76
  function safeJsonParseContent(content, defaultValue = null) {
77
- try {
78
- // Check for prototype pollution attempts in raw content
79
- // Covers various quote styles and whitespace variants
80
- if (/__proto__|constructor\s*["'`:]|prototype\s*["'`:]/i.test(content)) {
81
- console.warn('[safeJsonParse] Suspicious content detected');
82
- return defaultValue;
83
- }
84
-
85
- const parsed = JSON.parse(content);
86
-
87
- // Validate it's an object (not primitive)
88
- if (typeof parsed !== 'object' || parsed === null) {
89
- return parsed; // Allow primitives to pass through
90
- }
91
-
92
- // Additional check: ensure no proto/constructor keys were added
93
- const keys = Object.getOwnPropertyNames(parsed);
94
- if (keys.includes('__proto__') || keys.includes('constructor') || keys.includes('prototype')) {
95
- console.warn('[safeJsonParse] Prototype pollution attempt detected');
96
- return defaultValue;
97
- }
98
-
99
- return parsed;
100
- } catch (_err) {
101
- return defaultValue;
102
- }
77
+ // Delegates to flow-io's canonical safeJsonParseStringStrip (audit dup-004
78
+ // consolidation 2026-04-26). Two intentional behavior improvements over
79
+ // the prior local impl:
80
+ // 1. Strip semantic (recursive) replaces the buggy regex-based reject
81
+ // that produced false positives on any text containing the word
82
+ // "constructor" (e.g. legitimate string values).
83
+ // 2. Primitives no longer pass through (return defaultValue). All
84
+ // callers in lib/workspace-*.js parse JSON objects, not primitives.
85
+ // Risk assessment: cloud-compat verified by user's regression test
86
+ // coverage prior to this consolidation.
87
+ const { safeJsonParseStringStrip } = require('../scripts/flow-io');
88
+ return safeJsonParseStringStrip(content, defaultValue);
103
89
  }
104
90
 
105
91
  /**
package/lib/wogi-claude CHANGED
@@ -132,9 +132,32 @@ fi
132
132
  # worker's `.mcp.json` doesn't define `wogi-workspace-channel` (e.g.
133
133
  # this is not a workspace member), fall back to the empty MCP config
134
134
  # (the strip is harmless in non-workspace contexts).
135
+ #
136
+ # SEC-003 fix (2026-04-26): validate WOGI_WORKSPACE_ROOT before using it as
137
+ # a destination path. Without validation, an attacker who can set the env
138
+ # var could redirect the channel-only MCP config write to an arbitrary
139
+ # path. Rules:
140
+ # 1. Must be absolute (start with /).
141
+ # 2. Must point to an existing directory.
142
+ # 3. Must NOT contain '..' segments (traversal guard).
143
+ # On any validation failure, fall back to $(pwd) which is bounded by the
144
+ # current working directory.
135
145
  __wogi_empty_mcp_config=""
136
146
  if [ "$__wogi_strip_mcp" -eq 1 ]; then
137
- __wogi_empty_mcp_config="${WOGI_WORKSPACE_ROOT:-$(pwd)}/.workflow/state/worker-channel-only-mcp.json"
147
+ __wogi_workspace_root_raw="${WOGI_WORKSPACE_ROOT:-}"
148
+ __wogi_workspace_root_safe=""
149
+ if [ -n "$__wogi_workspace_root_raw" ] \
150
+ && [ "${__wogi_workspace_root_raw#/}" != "$__wogi_workspace_root_raw" ] \
151
+ && [ -d "$__wogi_workspace_root_raw" ] \
152
+ && [ "${__wogi_workspace_root_raw#*..}" = "$__wogi_workspace_root_raw" ]; then
153
+ __wogi_workspace_root_safe="$__wogi_workspace_root_raw"
154
+ else
155
+ __wogi_workspace_root_safe="$(pwd)"
156
+ if [ -n "$__wogi_workspace_root_raw" ]; then
157
+ echo "[wogi-claude] WARNING: WOGI_WORKSPACE_ROOT='$__wogi_workspace_root_raw' failed validation (must be absolute, exist, no '..'); falling back to $(pwd)" >&2
158
+ fi
159
+ fi
160
+ __wogi_empty_mcp_config="$__wogi_workspace_root_safe/.workflow/state/worker-channel-only-mcp.json"
138
161
  __wogi_member_mcp_path="$(pwd)/.mcp.json"
139
162
  if command -v node >/dev/null 2>&1; then
140
163
  # Use the dedicated helper (testable; see tests/flow-worker-mcp-strip.test.js).
@@ -155,14 +178,30 @@ if [ "$__wogi_strip_mcp" -eq 1 ]; then
155
178
  else
156
179
  # Helper not found — fall back to inline extraction (legacy code path
157
180
  # for installs that pre-date the helper script).
181
+ #
182
+ # arch-004 (2026-04-26): even in the fallback, scrub prototype-pollution
183
+ # keys from the parsed .mcp.json before re-emitting. This keeps the
184
+ # bash-inline path consistent with the canonical helper's safety
185
+ # guarantees (no raw JSON.parse without proto-scrub).
158
186
  node -e '
159
187
  const fs = require("fs");
160
188
  const path = require("path");
189
+ const DANGEROUS = new Set(["__proto__","constructor","prototype"]);
190
+ function strip(v, d) {
191
+ if (d > 256 || !v || typeof v !== "object") return v;
192
+ if (Array.isArray(v)) { for (const x of v) strip(x, d+1); return v; }
193
+ for (const k of Object.getOwnPropertyNames(v)) {
194
+ if (DANGEROUS.has(k)) { delete v[k]; continue; }
195
+ strip(v[k], d+1);
196
+ }
197
+ return v;
198
+ }
161
199
  const [src, out] = process.argv.slice(1);
162
200
  let channelEntry = null;
163
201
  try {
164
202
  if (fs.existsSync(src)) {
165
203
  const cfg = JSON.parse(fs.readFileSync(src, "utf-8"));
204
+ strip(cfg, 0);
166
205
  const ws = cfg && cfg.mcpServers && cfg.mcpServers["wogi-workspace-channel"];
167
206
  if (ws) channelEntry = ws;
168
207
  }
@@ -450,6 +450,22 @@ function broadcastSSE(event) {
450
450
  }
451
451
  }
452
452
 
453
+ // ============================================================
454
+ // Dispatch tracking integration (silent-halt RCA fix, v2.29.4)
455
+ // ============================================================
456
+ // The channel server is the only place that sees EVERY inbound message,
457
+ // regardless of whether the manager dispatched via the programmatic
458
+ // `dispatchToChannel()` helper or a raw `curl POST`. Recording at this
459
+ // layer guarantees `dispatched-tasks.json` exists for every dispatch,
460
+ // closing the wogi-hub 2026-04-27 silent-halt failure shape.
461
+ //
462
+ // Helpers live in `workspace-channel-tracking.js` so they can be unit-
463
+ // tested without spawning the channel-server process. Both fail-open;
464
+ // idempotency lives at the call site (Fix A skips on existing record;
465
+ // Fix B delegates to reconcileDispatch which is idempotent).
466
+
467
+ const channelTracking = require('./workspace-channel-tracking');
468
+
453
469
  // ============================================================
454
470
  // HTTP Server
455
471
  // ============================================================
@@ -506,6 +522,11 @@ const server = http.createServer(async (req, res) => {
506
522
  : cleanBody;
507
523
  sendChannelNotification(notificationBody, meta);
508
524
 
525
+ // v2.29.4 silent-halt RCA fixes — both fail-open
526
+ const trackingCtx = { workspaceRoot: WORKSPACE_ROOT, repoName: REPO_NAME, from, body: cleanBody };
527
+ channelTracking.tryRecordInboundDispatch(trackingCtx);
528
+ channelTracking.tryReconcileInboundCompletion(trackingCtx);
529
+
509
530
  // Also broadcast to SSE subscribers
510
531
  if (sseClients.size > 0) {
511
532
  const crypto = require('node:crypto');
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Wogi Workspace — Channel Server Dispatch Tracking Helpers (v2.29.4)
5
+ *
6
+ * Pure-function helpers that the channel server's HTTP POST handler calls
7
+ * to record inbound dispatches and reconcile inbound completions. Extracted
8
+ * from `workspace-channel-server.js` so they can be unit-tested without
9
+ * spawning the channel-server process.
10
+ *
11
+ * Why these exist (silent-halt RCA, 2026-04-27):
12
+ * `recordDispatch` was only called from the programmatic dispatch helper
13
+ * (`workspace-routing.js → dispatchToChannel`). Manager AI sessions that
14
+ * used raw `curl POST http://localhost:8801` bypassed it entirely. With
15
+ * no record, the overdue detector had nothing to detect — workers could
16
+ * die silently with zero manager-side signal.
17
+ *
18
+ * Both helpers are best-effort and fail-open. Idempotency lives at the
19
+ * call site (Fix A skips when a pending record already exists; Fix B
20
+ * delegates idempotency to `reconcileDispatch` which returns `null` when
21
+ * no pending record matches).
22
+ */
23
+
24
+ const TASK_ID_PATTERN = /\bwf-[0-9a-f]{8}\b/i;
25
+ const DISPATCH_BODY_PATTERN = /^\s*\/wogi-start\s+(wf-[0-9a-f]{8})\b/i;
26
+ const QUESTION_BODY_PATTERN = /^\s*##\s*QUESTION/im;
27
+ const COMPLETION_BODY_PATTERN = /##\s*Results\b|task-complete\b/i;
28
+
29
+ /**
30
+ * Record an inbound dispatch when the channel server (running in worker
31
+ * mode) receives a `/wogi-start <id>` POST from the manager.
32
+ *
33
+ * No-ops when:
34
+ * - workspaceRoot is missing
35
+ * - body is not a non-empty string
36
+ * - this server is in manager mode (REPO_NAME === 'manager')
37
+ * - the `from` header is not the manager
38
+ * - the body does not match the dispatch pattern
39
+ * - a pending record for (taskId, repoName) already exists (idempotency)
40
+ *
41
+ * @param {Object} ctx
42
+ * @param {string} ctx.workspaceRoot
43
+ * @param {string} ctx.repoName
44
+ * @param {string} ctx.from
45
+ * @param {string} ctx.body
46
+ * @param {Object} [tracking] — injectable for tests; defaults to the lib module
47
+ * @returns {{action: 'recorded'|'skip-existing'|'skip-not-worker'|'skip-bad-from'|'skip-no-match'|'skip-no-root'|'skip-empty-body'|'error', reason?: string, taskId?: string}}
48
+ */
49
+ function tryRecordInboundDispatch(ctx, tracking) {
50
+ const { workspaceRoot, repoName, from, body } = ctx || {};
51
+ if (!workspaceRoot) return { action: 'skip-no-root' };
52
+ if (typeof body !== 'string' || !body) return { action: 'skip-empty-body' };
53
+ if (repoName === 'manager') return { action: 'skip-not-worker' };
54
+ if (from !== 'manager' && from !== 'workspace-manager') return { action: 'skip-bad-from' };
55
+ const m = body.match(DISPATCH_BODY_PATTERN);
56
+ if (!m) return { action: 'skip-no-match' };
57
+ const taskId = m[1].toLowerCase();
58
+ try {
59
+ const tr = tracking || require('./workspace-dispatch-tracking');
60
+ const existing = tr.readDispatches(workspaceRoot).find(r =>
61
+ r && r.taskId === taskId && r.repoName === repoName && r.status === 'pending'
62
+ );
63
+ if (existing) return { action: 'skip-existing', taskId };
64
+ tr.recordDispatch(workspaceRoot, {
65
+ taskId,
66
+ repoName,
67
+ dispatchedBy: from
68
+ });
69
+ return { action: 'recorded', taskId };
70
+ } catch (err) {
71
+ return { action: 'error', reason: err.message, taskId };
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Reconcile an inbound completion when the channel server (running in
77
+ * manager mode) receives a worker-side POST that looks like a completion.
78
+ *
79
+ * No-ops when:
80
+ * - workspaceRoot is missing
81
+ * - body is not a non-empty string
82
+ * - this server is in worker mode (REPO_NAME !== 'manager')
83
+ * - the `from` header IS the manager (no self-completion)
84
+ * - the body looks like a `## QUESTION:` (escalation, not completion)
85
+ * - the body does not contain `## Results` or `task-complete`
86
+ * - the body does not contain a `wf-XXXXXXXX` reference
87
+ * - reconcileDispatch finds no pending record (idempotent)
88
+ *
89
+ * @param {Object} ctx
90
+ * @param {string} ctx.workspaceRoot
91
+ * @param {string} ctx.repoName
92
+ * @param {string} ctx.from
93
+ * @param {string} ctx.body
94
+ * @param {Object} [tracking] — injectable for tests; defaults to the lib module
95
+ * @returns {{action: 'reconciled'|'skip-not-manager'|'skip-self'|'skip-question'|'skip-not-completion'|'skip-no-id'|'skip-no-pending'|'skip-no-root'|'skip-empty-body'|'error', reason?: string, taskId?: string}}
96
+ */
97
+ function tryReconcileInboundCompletion(ctx, tracking) {
98
+ const { workspaceRoot, repoName, from, body } = ctx || {};
99
+ if (!workspaceRoot) return { action: 'skip-no-root' };
100
+ if (typeof body !== 'string' || !body) return { action: 'skip-empty-body' };
101
+ if (repoName !== 'manager') return { action: 'skip-not-manager' };
102
+ if (from === 'manager' || from === 'workspace-manager') return { action: 'skip-self' };
103
+ if (QUESTION_BODY_PATTERN.test(body)) return { action: 'skip-question' };
104
+ if (!COMPLETION_BODY_PATTERN.test(body)) return { action: 'skip-not-completion' };
105
+ const m = body.match(TASK_ID_PATTERN);
106
+ if (!m) return { action: 'skip-no-id' };
107
+ const taskId = m[0].toLowerCase();
108
+ try {
109
+ const tr = tracking || require('./workspace-dispatch-tracking');
110
+ const result = tr.reconcileDispatch(workspaceRoot, taskId, 'completed', 'channel-server-completion');
111
+ if (!result) return { action: 'skip-no-pending', taskId };
112
+ return { action: 'reconciled', taskId };
113
+ } catch (err) {
114
+ return { action: 'error', reason: err.message, taskId };
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ TASK_ID_PATTERN,
120
+ DISPATCH_BODY_PATTERN,
121
+ QUESTION_BODY_PATTERN,
122
+ COMPLETION_BODY_PATTERN,
123
+ tryRecordInboundDispatch,
124
+ tryReconcileInboundCompletion
125
+ };
package/lib/workspace.js CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  const fs = require('node:fs');
19
19
  const path = require('node:path');
20
- const { safeJsonParse, DANGEROUS_KEYS } = require('../scripts/flow-io');
20
+ const { safeJsonParse, safeJsonParseStringStrip } = require('../scripts/flow-io');
21
21
 
22
22
  /**
23
23
  * wf-f747f993 — resolve the claude-spawning command for workspace sessions.
@@ -62,19 +62,12 @@ function resolveClaudeSpawnCommand(role, flags) {
62
62
  // ============================================================
63
63
 
64
64
  // Proto-pollution safe JSON parse (finding-007).
65
- // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
65
+ // Delegates to flow-io's canonical safeJsonParseStringStrip (audit dup-004
66
+ // consolidation 2026-04-26). Behavior change vs prior local impl: dangerous
67
+ // key deletion is now RECURSIVE (was top-level only). Strict improvement —
68
+ // previous impl missed nested __proto__ in package.json or workspace metadata.
66
69
  function safeParseJson(str, fallback) {
67
- try {
68
- const obj = JSON.parse(str);
69
- if (obj && typeof obj === 'object') {
70
- for (const key of Object.keys(obj)) {
71
- if (DANGEROUS_KEYS.has(key)) delete obj[key];
72
- }
73
- }
74
- return obj;
75
- } catch (_err) {
76
- return fallback;
77
- }
70
+ return safeJsonParseStringStrip(str, fallback);
78
71
  }
79
72
 
80
73
  const WORKSPACE_CONFIG_FILE = 'wogi-workspace.json';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.29.2",
3
+ "version": "2.29.4",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "flow": "./scripts/flow",
13
- "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
13
+ "test": "NODE_ENV=test node --test tests/auto-compact-prompt.test.js tests/flow-paths.test.js tests/flow-io.test.js tests/flow-config-loader.test.js tests/flow-damage-control.test.js tests/flow-output.test.js tests/flow-constants.test.js tests/flow-session-state.test.js tests/flow-hooks-integration.test.js tests/flow-utils.test.js tests/flow-security.test.js tests/flow-memory-db.test.js tests/flow-durable-session.test.js tests/flow-skill-matcher.test.js tests/flow-bridge.test.js tests/flow-proactive-compact.test.js tests/flow-cascade-completion.test.js tests/flow-capture-gate.test.js tests/flow-correction-detector-hybrid.test.js tests/flow-promote.test.js tests/flow-archive-runs.test.js tests/flow-memory.test.js tests/flow-hooks-pre-tool-helpers.test.js tests/flow-hooks-bugfix-scope-gate.test.js tests/flow-hooks-routing-gate.test.js tests/flow-hooks-phase-read-gate.test.js tests/flow-hooks-commit-log-gate.test.js tests/flow-hooks-deploy-gate.test.js tests/flow-hooks-todowrite-gate.test.js tests/flow-hooks-git-safety-gate.test.js tests/flow-hooks-scope-mutation-gate.test.js tests/flow-hooks-strike-gate.test.js tests/flow-hooks-component-check.test.js tests/flow-hooks-scope-gate.test.js tests/flow-hooks-implementation-gate.test.js tests/flow-hooks-research-gate.test.js tests/flow-hooks-loop-check.test.js tests/flow-hooks-manager-boundary-gate.test.js tests/flow-hooks-phase-gate.test.js tests/flow-hooks-pre-tool-orchestrator.test.js tests/flow-hooks-observation-capture.test.js tests/flow-hooks-task-gate.test.js tests/flow-durable-session-suspension.test.js tests/flow-health-mcp-scopes.test.js tests/flow-lean-config.test.js tests/flow-workspace-autopickup.test.js tests/flow-worker-boundary-gate.test.js tests/flow-worker-question-classifier.test.js tests/flow-completion-truth-gate-contradictions.test.js tests/flow-structure-sensor.test.js tests/flow-workspace-dispatch-tracking.test.js tests/workspace-ipc-sqlite.test.js tests/workspace-ipc-multi-worker.test.js tests/flow-story-gates.test.js tests/flow-workspace-restart-handoff.test.js tests/flow-wogi-claude-wrapper.test.js tests/flow-wave1-integrations.test.js tests/flow-wave2-integrations.test.js tests/flow-wave3-integrations.test.js tests/flow-commit-claims-gate.test.js tests/auto-review.test.js tests/gate-telemetry-surface.test.js tests/agents-md-alias.test.js tests/flow-skill-manage.test.js tests/fuzzy-patch.test.js tests/mode-schema.test.js tests/flow-feature-dossier.test.js tests/flow-autonomous-mode.test.js tests/flow-epic-cascade.test.js tests/flow-workspace-summary.test.js tests/flow-hooks-research-evidence-gate.test.js tests/flow-worker-mcp-strip.test.js tests/flow-orchestrate-corrections.test.js tests/flow-source-fidelity.test.js tests/flow-hooks-long-input-enforcement.test.js && NODE_ENV=test node tests/run-quality-gates.test.js",
14
14
  "test:syntax": "find scripts/ lib/ -name '*.js' -not -path '*/node_modules/*' -exec node --check {} +",
15
15
  "lint": "eslint scripts/ lib/ tests/",
16
16
  "lint:ci": "eslint scripts/ lib/ tests/ --max-warnings 0",
package/scripts/flow CHANGED
@@ -1068,6 +1068,10 @@ case "${1:-}" in
1068
1068
  # long-input is the new name, transcript-digest kept for backward compatibility
1069
1069
  node "$SCRIPT_DIR/flow-long-input.js" "${@:2}"
1070
1070
  ;;
1071
+ long-input-pending)
1072
+ # P11.6 mechanical-enforcement marker management
1073
+ node "$SCRIPT_DIR/flow-long-input-pending.js" "${@:2}"
1074
+ ;;
1071
1075
  permissions)
1072
1076
  # Permission management (session vs permanent)
1073
1077
  node "$SCRIPT_DIR/flow-permissions.js" "${@:2}"
@@ -36,16 +36,38 @@ const TRIGGER_PHRASES = [
36
36
  'do them all without asking'
37
37
  ];
38
38
 
39
- const STOP_PHRASES = [
39
+ // CL-001 fix (2026-04-26): split into EXACT and EXPLICIT phrase lists.
40
+ //
41
+ // Previously detectStop() did a startsWith/endsWith match on every phrase,
42
+ // which silently deactivated autonomous mode when the user said things like
43
+ // "wait for the build then continue" or "hold on let me check, then proceed"
44
+ // or "stop being so verbose but keep working" — common conversational words
45
+ // at the start/end of a message would falsely trigger deactivation.
46
+ //
47
+ // New semantic:
48
+ // EXACT_STOP_PHRASES — exact match only (common short words; if the user
49
+ // actually means it as a stop command, they'll type just that)
50
+ // EXPLICIT_STOP_PHRASES — substring match (unambiguous because they
51
+ // mention "autonomous" by name)
52
+ const EXACT_STOP_PHRASES = [
40
53
  'stop',
41
54
  'pause',
42
55
  'hold on',
43
- 'wait',
56
+ 'wait'
57
+ ];
58
+
59
+ const EXPLICIT_STOP_PHRASES = [
44
60
  'cancel autonomous',
45
61
  'exit autonomous',
46
- 'leave autonomous mode'
62
+ 'leave autonomous mode',
63
+ 'stop autonomous',
64
+ 'pause autonomous',
65
+ 'end autonomous'
47
66
  ];
48
67
 
68
+ // Backwards-compat: union for any external caller that imported STOP_PHRASES.
69
+ const STOP_PHRASES = [...EXACT_STOP_PHRASES, ...EXPLICIT_STOP_PHRASES];
70
+
49
71
  function normalize(s) {
50
72
  return String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
51
73
  }
@@ -66,7 +88,10 @@ function detect(message) {
66
88
  function detectStop(message) {
67
89
  const text = normalize(message);
68
90
  if (!text) return false;
69
- return STOP_PHRASES.some(p => text === p || text.startsWith(p + ' ') || text.endsWith(' ' + p));
91
+ // EXACT phrases require equality (don't match mid-message)
92
+ if (EXACT_STOP_PHRASES.includes(text)) return true;
93
+ // EXPLICIT phrases match anywhere (unambiguous due to "autonomous" word)
94
+ return EXPLICIT_STOP_PHRASES.some(p => text.includes(p));
70
95
  }
71
96
 
72
97
  /**
@@ -97,7 +97,15 @@ function finalize({ endReason = 'queue-drained', completed = [] } = {}) {
97
97
  * POST one or more COMPLETION-SUMMARY lines to the manager's channel-dispatch
98
98
  * HTTP bus. Synchronous + best-effort — finalize() must not throw if the
99
99
  * manager is unreachable.
100
+ *
101
+ * CL-003 fix (2026-04-26): per-call timeout reduced from 5s → 2s. On any
102
+ * failure, abort the remaining chunks and bubble up a coherent error
103
+ * (instead of looping through 5×N seconds and leaving the manager with a
104
+ * partial chunk set). Worst-case wall-clock cost: ~2s × 1 (first failure
105
+ * short-circuits) instead of unbounded N×5s for chunked payloads.
100
106
  */
107
+ const POST_TIMEOUT_MS = 2000;
108
+
101
109
  function postSummaryToManager(payload) {
102
110
  const { execFileSync } = require('node:child_process');
103
111
  const ws = require('./flow-workspace-summary');
@@ -106,14 +114,26 @@ function postSummaryToManager(payload) {
106
114
  const lines = ws.encodeMessage(enriched);
107
115
  const port = process.env.WOGI_MANAGER_PORT || '8800';
108
116
  const repo = process.env.WOGI_REPO_NAME;
117
+ let sent = 0;
109
118
  for (const line of lines) {
110
- execFileSync('curl', [
111
- '-s', '-X', 'POST',
112
- `http://127.0.0.1:${port}`,
113
- '-H', `X-Wogi-From: ${repo}`,
114
- '-H', `X-Wogi-TaskId: ${taskId}`,
115
- '--data-binary', line
116
- ], { stdio: 'ignore', timeout: 5000 });
119
+ try {
120
+ execFileSync('curl', [
121
+ '-s', '--fail', '-X', 'POST',
122
+ `http://127.0.0.1:${port}`,
123
+ '-H', `X-Wogi-From: ${repo}`,
124
+ '-H', `X-Wogi-TaskId: ${taskId}`,
125
+ '--data-binary', line
126
+ ], { stdio: 'ignore', timeout: POST_TIMEOUT_MS });
127
+ sent++;
128
+ } catch (err) {
129
+ // Short-circuit on first failure: don't waste 2s × remaining chunks.
130
+ // Manager already received `sent` chunks (possibly 0); throw a
131
+ // coherent error so finalize() can record it on result.posted.
132
+ const total = lines.length;
133
+ throw new Error(
134
+ `manager-unreachable after ${sent}/${total} chunks: ${err.message}`
135
+ );
136
+ }
117
137
  }
118
138
  }
119
139
 
@@ -21,6 +21,8 @@
21
21
  const path = require('node:path');
22
22
  const { PATHS } = require('./flow-paths');
23
23
  const { writeJson } = require('./flow-io');
24
+ // CL-006 (2026-04-26): consolidated formatDuration to flow-time-format.
25
+ const { formatDuration } = require('./flow-time-format');
24
26
 
25
27
  const SEP = '━'.repeat(58);
26
28
 
@@ -28,22 +30,6 @@ function summaryPath(runId) {
28
30
  return path.join(PATHS.state, `autonomous-run-summary-${runId}.json`);
29
31
  }
30
32
 
31
- function pad2(n) { return String(n).padStart(2, '0'); }
32
-
33
- function formatDuration(startedAt, endedAt) {
34
- if (!startedAt || !endedAt) return '0:00';
35
- const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
36
- if (!Number.isFinite(ms) || ms < 0) return '0:00';
37
- const sec = Math.floor(ms / 1000);
38
- const m = Math.floor(sec / 60);
39
- const s = sec % 60;
40
- if (m >= 60) {
41
- const h = Math.floor(m / 60);
42
- return `${h}:${pad2(m % 60)}:${pad2(s)}`;
43
- }
44
- return `${m}:${pad2(s)}`;
45
- }
46
-
47
33
  /**
48
34
  * Build the full payload object — used for both terminal render and
49
35
  * persisted JSON. Caller passes raw collected data; this normalizes shape.
@@ -97,6 +97,36 @@ function isLegacyTaskId(id) {
97
97
  return /^(TASK|BUG)-\d{3,}$/i.test(id);
98
98
  }
99
99
 
100
+ /**
101
+ * Coarse ID validation used at task-write time. Accepts any valid Wogi ID
102
+ * shape (task, sub-task, review-fix, review-finding, epic, feature, plan,
103
+ * slug, legacy). Returns boolean — for finer-grained format detection use
104
+ * `validateTaskId()`.
105
+ *
106
+ * Extracted from flow-utils.js (audit Story 12 — flow-utils decomposition,
107
+ * pattern-validator extraction). flow-utils.js keeps this name as a
108
+ * re-export for backwards compat with its 302 importers.
109
+ *
110
+ * @param {string} id
111
+ * @returns {boolean}
112
+ */
113
+ function isValidWogiId(id) {
114
+ if (!id || typeof id !== 'string') return false;
115
+ // Standard task, sub-task, review fix (wf-cr-), review finding (wf-rv-)
116
+ if (/^wf-[a-f0-9]{8}(-\d{2})?$/i.test(id)) return true;
117
+ if (/^wf-cr-[a-f0-9]{6}$/i.test(id)) return true;
118
+ if (/^wf-rv-[a-f0-9]{8}$/i.test(id)) return true;
119
+ // Epic, feature, plan IDs
120
+ if (/^(ep|ft|pl)-[a-f0-9]{8}$/i.test(id)) return true;
121
+ // Slug format: wf-<alphanum>[<alphanum or hyphen>]*<alphanum>, 5-64 chars.
122
+ // For manager-dispatched descriptive IDs. Path-safe (no dots/separators).
123
+ // Keep this in sync with validateTaskId() 'slug' branch above.
124
+ if (/^wf-[a-z0-9][a-z0-9-]{0,60}[a-z0-9]$/i.test(id)) return true;
125
+ // Legacy format
126
+ if (/^(TASK|BUG)-\d{3,}$/i.test(id)) return true;
127
+ return false;
128
+ }
129
+
100
130
  module.exports = {
101
131
  generateHashId,
102
132
  generateTaskId,
@@ -105,4 +135,5 @@ module.exports = {
105
135
  generatePlanId,
106
136
  validateTaskId,
107
137
  isLegacyTaskId,
138
+ isValidWogiId,
108
139
  };