wogiflow 2.29.2 → 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.
- package/.claude/docs/intent-grounded-reasoning.md +1 -1
- package/.workflow/templates/partials/methodology-rules.hbs +30 -1
- package/lib/commands/team-connection.js +5 -28
- package/lib/utils.js +12 -26
- package/lib/wogi-claude +40 -1
- package/lib/workspace.js +6 -13
- package/package.json +2 -2
- package/scripts/flow +4 -0
- package/scripts/flow-autonomous-detector.js +29 -4
- package/scripts/flow-autonomous-mode.js +27 -7
- package/scripts/flow-completion-summary.js +2 -16
- package/scripts/flow-id.js +31 -0
- package/scripts/flow-io.js +78 -0
- package/scripts/flow-long-input-pending.js +110 -0
- package/scripts/flow-long-input-stories.js +8 -0
- package/scripts/flow-orchestrate.js +16 -10
- package/scripts/flow-question-queue.js +73 -7
- package/scripts/flow-scanner-base.js +77 -1
- package/scripts/flow-session-state.js +47 -0
- package/scripts/flow-source-fidelity.js +279 -0
- package/scripts/flow-time-format.js +42 -0
- package/scripts/flow-utils.js +3 -16
- package/scripts/flow-worker-mcp-strip.js +12 -11
- package/scripts/flow-workspace-summary.js +38 -19
- package/scripts/hooks/adapters/claude-code.js +7 -4
- package/scripts/hooks/core/long-input-enforcement.js +311 -0
- package/scripts/hooks/core/pre-tool-deps.js +185 -0
- package/scripts/hooks/core/pre-tool-orchestrator.js +22 -0
- package/scripts/hooks/core/session-context.js +26 -0
- package/scripts/hooks/core/task-boundary-reset.js +13 -0
- package/scripts/hooks/core/worker-boundary-gate.js +67 -16
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +21 -95
- 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 (
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
}
|
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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
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 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
'
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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.
|
package/scripts/flow-id.js
CHANGED
|
@@ -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
|
};
|
package/scripts/flow-io.js
CHANGED
|
@@ -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,
|