wogiflow 2.29.1 → 2.29.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.claude/docs/intent-grounded-reasoning.md +1 -1
  2. package/.workflow/templates/partials/methodology-rules.hbs +60 -0
  3. package/lib/commands/team-connection.js +5 -28
  4. package/lib/mode-schema.js +2 -1
  5. package/lib/utils.js +12 -26
  6. package/lib/wogi-claude +40 -1
  7. package/lib/workspace-messages.js +2 -1
  8. package/lib/workspace.js +7 -14
  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-corrections.js +158 -0
  19. package/scripts/flow-orchestrate.js +22 -97
  20. package/scripts/flow-question-queue.js +73 -7
  21. package/scripts/flow-scanner-base.js +77 -1
  22. package/scripts/flow-session-state.js +47 -0
  23. package/scripts/flow-source-fidelity.js +279 -0
  24. package/scripts/flow-time-format.js +42 -0
  25. package/scripts/flow-utils.js +3 -16
  26. package/scripts/flow-worker-mcp-strip.js +12 -11
  27. package/scripts/flow-workspace-summary.js +38 -19
  28. package/scripts/hooks/adapters/claude-code.js +7 -4
  29. package/scripts/hooks/core/long-input-enforcement.js +311 -0
  30. package/scripts/hooks/core/pre-tool-deps.js +185 -0
  31. package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
  32. package/scripts/hooks/core/session-context.js +26 -0
  33. package/scripts/hooks/core/task-boundary-reset.js +13 -0
  34. package/scripts/hooks/core/worker-boundary-gate.js +67 -16
  35. package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
  36. 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 adds Principle 11 Platform Capability Grounding) | `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,66 @@ 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
+
167
+ ### Cross-Story Integration Tier-3 Rule
168
+
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.
170
+
171
+ **Mandatory for every layering story:**
172
+
173
+ 1. **Architect output names upstream dependencies.** A "Dependencies" section lists prior stories/commits + the specific contract relied on (interface signature, file format, transport, invariant). "I'm reusing Story A's X" is not enough; quote the contract.
174
+
175
+ 2. **Adversary challenges the dependency.** "What if Story A's invariant doesn't hold? What's the failure mode? What evidence proves Story A's contract is intact for THIS usage?" The adversary's job is finding the assumption Story B silently inherits.
176
+
177
+ 3. **At least one Tier-3 integration test exercises the chain end-to-end.** Not a unit test of Story B in isolation — a test that simulates a real run through both stories' code paths. If Story A's output flows into Story B's input, the test feeds a real Story-A output through Story B and asserts the output. Mark the test `// regression-tier3` so future readers know its purpose.
178
+
179
+ 4. **Pre-release gate verifies stacked coverage.** Before tagging a release, identify any commits that layer on prior commits in the same release. For each, confirm a Tier-3 integration test exists. Missing Tier-3 + stacked stories → block release.
180
+
181
+ **Why:** unit tests within a story boundary catch the story's own bugs but miss every regression where the story's correct behavior depends on a broken upstream. The 2026-04-26 incident (audit-channel-transport-001) was caused by exactly this gap: Story A stripped MCP servers including the workspace-channel transport itself; Story B layered task-completion routing on top; both stories' tests passed; manager dispatch silently failed in production. Self-IGR caught Story B's local correctness but missed that the upstream contract was broken.
182
+
183
+ **Anti-rationalization:**
184
+ - *"The upstream story has its own tests"* → WRONG. Their tests pin THEIR contract. Your Tier-3 test pins YOUR usage of their contract.
185
+ - *"It's expensive to set up an integration test"* → WRONG. The 2026-04-26 incident cost a v2.29.1 hot-fix release. Set up time amortizes; regression cost compounds.
186
+ - *"Self-IGR is enough; we don't need the actual adversary subagent"* → WRONG. Self-IGR pattern-matches on the same model that wrote the plan; the cross-story dependency is exactly the blind spot a different-model adversary catches.
187
+
188
+ **How to apply** (concrete checks for any layering story):
189
+ - `git log --oneline <prior-N-commits>` — which earlier work does this story sit on?
190
+ - For each, write the contract you're relying on: "Story A delivers X via Y."
191
+ - `grep -r "<Story A's interface>"` — is the contract still intact in HEAD?
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.
193
+
194
+ Enforced by: Logic Constitution v3 sub-principle 11.5 (Stacked-story integration verification). Pre-release gate consumes this signal before tagging.
195
+
196
+ ---
197
+
138
198
  ### Autonomous Walk-Away Mode
139
199
 
140
200
  The user can dump N items, say "go until you finish" / "autonomous mode" / "run this autonomously" / "don't bother me, just do it" (or similar phrases — see `flow-autonomous-detector.js`), and walk away. While the run is active:
@@ -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
  /**
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { DANGEROUS_KEYS } = require('../scripts/flow-io');
5
6
 
6
7
  const MODES_DIR = path.join(process.cwd(), '.workflow', 'modes');
7
8
 
@@ -17,7 +18,7 @@ const REQUIRED_FIELDS = ['name', 'roleDefinition', 'whenToUse'];
17
18
  const OPTIONAL_FIELDS = ['customInstructions', 'allowedToolGroups'];
18
19
  const ALL_FIELDS = new Set([...REQUIRED_FIELDS, ...OPTIONAL_FIELDS]);
19
20
 
20
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
21
+ // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
21
22
 
22
23
  function parseModeYaml(content, sourceLabel = '<inline>') {
23
24
  const result = Object.create(null);
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
  }
@@ -11,6 +11,7 @@
11
11
  const fs = require('node:fs');
12
12
  const path = require('node:path');
13
13
  const { safeReadJson } = require('./utils');
14
+ const { DANGEROUS_KEYS } = require('../scripts/flow-io');
14
15
  const crypto = require('node:crypto');
15
16
 
16
17
  // ============================================================
@@ -170,7 +171,7 @@ function updateMessageStatus(workspaceRoot, messageId, newStatus, extra = {}) {
170
171
  message.updatedAt = new Date().toISOString();
171
172
  // Safe merge: filter dangerous keys to prevent prototype pollution
172
173
  if (extra && typeof extra === 'object') {
173
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
174
+ // DANGEROUS_KEYS imported from scripts/flow-io canonical (audit dup-002 / wf-9fc4970b).
174
175
  for (const [key, value] of Object.entries(extra)) {
175
176
  if (!DANGEROUS_KEYS.has(key)) {
176
177
  message[key] = value;
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 } = 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.
@@ -61,20 +61,13 @@ function resolveClaudeSpawnCommand(role, flags) {
61
61
  // Constants
62
62
  // ============================================================
63
63
 
64
- // Proto-pollution safe JSON parse (finding-007)
65
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
64
+ // Proto-pollution safe JSON parse (finding-007).
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.1",
3
+ "version": "2.29.3",
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 && 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
  };
@@ -246,6 +246,82 @@ function safeJsonParseString(jsonString, defaultValue = null) {
246
246
  }
247
247
  }
248
248
 
249
+ /**
250
+ * Recursively strip prototype-pollution keys from a parsed object/array.
251
+ * Mutates in place; returns the same reference. Use when the caller wants
252
+ * to filter dangerous content rather than reject the whole payload.
253
+ *
254
+ * Sibling to checkForDangerousKeys (which DETECTS without modifying). This
255
+ * is the strip variant used by lib/* JSON parsers that want to keep
256
+ * structurally-valid content but defang any __proto__/constructor/prototype
257
+ * keys nested anywhere in the tree.
258
+ */
259
+ // Sentinel returned when stripDangerousKeys hits the depth cap. Distinct from
260
+ // `null` (legitimate JSON value) so callers can distinguish "hit the cap" from
261
+ // "successfully scrubbed null".
262
+ const STRIP_TOO_DEEP = Object.freeze({ __wogiTooDeep: true });
263
+
264
+ const STRIP_MAX_DEPTH = 256;
265
+
266
+ function stripDangerousKeys(value, depth = 0) {
267
+ // SEC-001 fix (2026-04-26): bound recursion AND fail-safe at the cap.
268
+ // Previous impl returned the partially-stripped value, which left dangerous
269
+ // keys live in subtrees past depth 32 — caller could then merge them and
270
+ // pollute Object.prototype. New behavior: return STRIP_TOO_DEEP sentinel so
271
+ // safeJsonParseStringStrip can fall back to defaultValue. Cap raised from
272
+ // 32 → 256 so legitimate nesting never trips it.
273
+ if (depth > STRIP_MAX_DEPTH) return STRIP_TOO_DEEP;
274
+ if (!value || typeof value !== 'object') return value;
275
+ if (Array.isArray(value)) {
276
+ for (let i = 0; i < value.length; i++) {
277
+ const r = stripDangerousKeys(value[i], depth + 1);
278
+ if (r === STRIP_TOO_DEEP) return STRIP_TOO_DEEP;
279
+ }
280
+ return value;
281
+ }
282
+ for (const key of Object.getOwnPropertyNames(value)) {
283
+ if (DANGEROUS_KEYS.has(key)) {
284
+ delete value[key];
285
+ continue;
286
+ }
287
+ const r = stripDangerousKeys(value[key], depth + 1);
288
+ if (r === STRIP_TOO_DEEP) return STRIP_TOO_DEEP;
289
+ }
290
+ return value;
291
+ }
292
+
293
+ /**
294
+ * Parse a JSON string and STRIP any prototype-pollution keys recursively.
295
+ * Returns the sanitized parsed object (or defaultValue on parse error).
296
+ *
297
+ * Differs from safeJsonParseString: that function REJECTS the whole payload
298
+ * if dangerous keys are present (returns defaultValue). This function
299
+ * returns the parsed object with dangerous keys removed. Pick based on
300
+ * threat model:
301
+ * - reject (safeJsonParseString) — fail-loud, refuse hostile content
302
+ * - strip (safeJsonParseStringStrip) — fail-soft, sanitize and proceed
303
+ *
304
+ * Added as part of audit dup-004 consolidation (2026-04-26): unifies the
305
+ * lib/utils.safeJsonParseContent / lib/workspace.safeParseJson /
306
+ * lib/commands/team-connection.safeParseJson trio under a single canonical
307
+ * helper. Preserves the lib/* "strip and proceed" semantic.
308
+ *
309
+ * @param {string} jsonString
310
+ * @param {*} [defaultValue=null]
311
+ * @returns {object|Array|*} sanitized parsed value, or defaultValue
312
+ */
313
+ function safeJsonParseStringStrip(jsonString, defaultValue = null) {
314
+ try {
315
+ const parsed = JSON.parse(jsonString);
316
+ if (typeof parsed !== 'object' || parsed === null) return defaultValue;
317
+ const stripped = stripDangerousKeys(parsed);
318
+ if (stripped === STRIP_TOO_DEEP) return defaultValue;
319
+ return stripped;
320
+ } catch (_err) {
321
+ return defaultValue;
322
+ }
323
+ }
324
+
249
325
  // ============================================================
250
326
  // Text File Operations
251
327
  // ============================================================
@@ -694,6 +770,8 @@ module.exports = {
694
770
  writeJson,
695
771
  safeJsonParse,
696
772
  safeJsonParseString,
773
+ safeJsonParseStringStrip,
774
+ stripDangerousKeys,
697
775
 
698
776
  // Text File Operations
699
777
  readFile,