wogiflow 2.26.2 → 2.29.0
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/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +84 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -385,6 +385,62 @@ function incrementStopAttempts(maxAttempts = 10) {
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Remove the routing-pending flag file WITHOUT writing a cleared-marker.
|
|
390
|
+
*
|
|
391
|
+
* Distinct from clearRoutingPending(): that writes a 15s cleared-marker to
|
|
392
|
+
* suppress re-setting during skill chains. This helper just deletes the flag
|
|
393
|
+
* so stale pending state (e.g. from a crashed prior turn) can't block tools
|
|
394
|
+
* on the next check. The cleared-marker path is left untouched — so if a
|
|
395
|
+
* /wogi-* skill is actively executing, its suppression window is preserved.
|
|
396
|
+
*
|
|
397
|
+
* Used by phase transitions: moving between phases should not leave stale
|
|
398
|
+
* pending flags blocking tools, but must not create a bypass window either.
|
|
399
|
+
*
|
|
400
|
+
* @returns {{ removed: boolean }}
|
|
401
|
+
*/
|
|
402
|
+
function removeRoutingFlag() {
|
|
403
|
+
try {
|
|
404
|
+
fs.unlinkSync(ROUTING_FLAG_PATH);
|
|
405
|
+
return { removed: true };
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err.code === 'ENOENT') return { removed: true };
|
|
408
|
+
if (process.env.DEBUG) {
|
|
409
|
+
console.error(`[routing-gate] removeRoutingFlag failed: ${err.message}`);
|
|
410
|
+
}
|
|
411
|
+
return { removed: false };
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Reset routing state entirely — delete BOTH the pending flag and the
|
|
417
|
+
* cleared-marker. Leaves a clean slate so the next UserPromptSubmit re-arms
|
|
418
|
+
* the gate normally without any inherited bypass window.
|
|
419
|
+
*
|
|
420
|
+
* Used by PostCompact: compaction produces fresh context, so any in-flight
|
|
421
|
+
* skill-chain suppression or stale pending flag from the pre-compact turn
|
|
422
|
+
* is no longer relevant. Caller is responsible for re-arming via
|
|
423
|
+
* setRoutingPending() if the post-reset state needs a pending flag.
|
|
424
|
+
*
|
|
425
|
+
* @returns {{ reset: boolean }}
|
|
426
|
+
*/
|
|
427
|
+
function resetRoutingState() {
|
|
428
|
+
let ok = true;
|
|
429
|
+
for (const p of [ROUTING_FLAG_PATH, ROUTING_CLEARED_PATH]) {
|
|
430
|
+
try {
|
|
431
|
+
fs.unlinkSync(p);
|
|
432
|
+
} catch (err) {
|
|
433
|
+
if (err.code !== 'ENOENT') {
|
|
434
|
+
ok = false;
|
|
435
|
+
if (process.env.DEBUG) {
|
|
436
|
+
console.error(`[routing-gate] resetRoutingState unlink ${p} failed: ${err.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return { reset: ok };
|
|
442
|
+
}
|
|
443
|
+
|
|
388
444
|
module.exports = {
|
|
389
445
|
isRoutingGateEnabled,
|
|
390
446
|
hasActiveTask,
|
|
@@ -394,6 +450,8 @@ module.exports = {
|
|
|
394
450
|
isRoutingRecentlyCleared,
|
|
395
451
|
checkRoutingGate,
|
|
396
452
|
incrementStopAttempts,
|
|
453
|
+
removeRoutingFlag,
|
|
454
|
+
resetRoutingState,
|
|
397
455
|
ROUTING_FLAG_PATH,
|
|
398
456
|
ROUTING_CLEARED_PATH
|
|
399
457
|
};
|
|
@@ -138,6 +138,12 @@ const KNOWN_STATE_FILES = new Set([
|
|
|
138
138
|
'.routing-pending',
|
|
139
139
|
'.routing-cleared',
|
|
140
140
|
'.gates-passed.json',
|
|
141
|
+
|
|
142
|
+
// Task-boundary restart machinery (wf-39e9dc09, R-336, wf-f267ea2a)
|
|
143
|
+
'task-just-completed',
|
|
144
|
+
'task-boundary-last-triggered',
|
|
145
|
+
'task-boundary-clean-completion.json',
|
|
146
|
+
'pending-question.json',
|
|
141
147
|
]);
|
|
142
148
|
|
|
143
149
|
/** Known directory names within state/ (not files) */
|
|
@@ -871,6 +877,108 @@ function formatContextForInjection(context) {
|
|
|
871
877
|
// Non-critical — history file may not exist; continue with normal context
|
|
872
878
|
}
|
|
873
879
|
|
|
880
|
+
// AUTO-PICKUP after clean completion (wf-f267ea2a).
|
|
881
|
+
// When the prior task completed cleanly AND the ready queue is non-empty AND
|
|
882
|
+
// no pending-question marker exists, instruct the AI to immediately invoke
|
|
883
|
+
// /wogi-start <nextReadyId> on the first user message rather than asking
|
|
884
|
+
// "what's next?". This is the main-mode mirror of workspace.autoPickupChannelDispatches.
|
|
885
|
+
//
|
|
886
|
+
// Marker is consumed (deleted) on every SessionStart that observes it,
|
|
887
|
+
// regardless of whether AUTO-PICKUP fires — so a stale marker can't loop
|
|
888
|
+
// across unrelated future restarts. Fail-open throughout: any error or
|
|
889
|
+
// missing config falls back to the default "proceed with next instruction".
|
|
890
|
+
try {
|
|
891
|
+
const cleanMarkerPath = path.join(PATHS.state, 'task-boundary-clean-completion.json');
|
|
892
|
+
if (fs.existsSync(cleanMarkerPath)) {
|
|
893
|
+
const config = getConfig();
|
|
894
|
+
const tbr = config.taskBoundaryReset || {};
|
|
895
|
+
const flagEnabled = tbr.autoPickupNextTask !== false; // default true
|
|
896
|
+
const pendingQuestionPath = path.join(PATHS.state, 'pending-question.json');
|
|
897
|
+
const hasPendingQuestion = fs.existsSync(pendingQuestionPath);
|
|
898
|
+
|
|
899
|
+
// Read the marker for diagnostic context (which task completed)
|
|
900
|
+
const markerPayload = safeJsonParse(cleanMarkerPath, null);
|
|
901
|
+
|
|
902
|
+
// Find next ready task. Prefer the first non-epic task — epics are
|
|
903
|
+
// containers whose work lives in their child stories, and auto-picking an
|
|
904
|
+
// epic produces a restart loop (wogi-start on an epic has no actionable
|
|
905
|
+
// next step when its children are not yet ready-queue tasks). If the
|
|
906
|
+
// queue contains only epics, emit no auto-pickup (safer to stop the loop
|
|
907
|
+
// than to re-enter it).
|
|
908
|
+
//
|
|
909
|
+
// Cascade-after-decomposition (Story E / wf-e28b6cd8): when the marker
|
|
910
|
+
// carries a `nextTaskId` (set by the cascade helper after epic
|
|
911
|
+
// decomposition), use that explicitly. Verify the ID still exists in
|
|
912
|
+
// ready.json before honoring it; if missing/stale, fall back to default
|
|
913
|
+
// first-actionable resolution.
|
|
914
|
+
let nextTaskId = null;
|
|
915
|
+
let nextTaskTitle = null;
|
|
916
|
+
try {
|
|
917
|
+
const ready = getReadyData();
|
|
918
|
+
const queue = Array.isArray(ready?.ready) ? ready.ready : [];
|
|
919
|
+
if (markerPayload && markerPayload.nextTaskId) {
|
|
920
|
+
const explicit = queue.find(t => t && t.id === markerPayload.nextTaskId);
|
|
921
|
+
if (explicit) {
|
|
922
|
+
nextTaskId = explicit.id;
|
|
923
|
+
nextTaskTitle = explicit.title || null;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (!nextTaskId) {
|
|
927
|
+
const firstActionable = queue.find(t => t && t.type !== 'epic');
|
|
928
|
+
if (firstActionable) {
|
|
929
|
+
nextTaskId = firstActionable.id || null;
|
|
930
|
+
nextTaskTitle = firstActionable.title || null;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} catch (_err) { /* fall through — no auto-pickup if ready.json unreadable */ }
|
|
934
|
+
|
|
935
|
+
const shouldInject = flagEnabled && !hasPendingQuestion && nextTaskId;
|
|
936
|
+
|
|
937
|
+
if (shouldInject) {
|
|
938
|
+
output += `### ⚡ AUTO-PICKUP MODE ACTIVE\n`;
|
|
939
|
+
output += `Prior task **${markerPayload?.completedTaskId || 'unknown'}** (${markerPayload?.completedTaskTitle || 'no title'}) completed cleanly. The next ready task is **${nextTaskId}** — ${nextTaskTitle || '(no title)'}.\n\n`;
|
|
940
|
+
output += `**ON THE FIRST USER MESSAGE IN THIS SESSION (regardless of content): immediately invoke \`Skill(skill="wogi-start", args="${nextTaskId}")\`.**\n\n`;
|
|
941
|
+
output += `Do NOT ask "what's next?". Do NOT summarize prior work. Do NOT propose alternatives. The user has authorized autonomous continuation across this epic. If you have a question that genuinely cannot be resolved by self-challenge, use \`flow ask "<question>"\` so the next restart defers correctly (R-336).\n\n`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Always consume the marker (single-use), regardless of whether we injected
|
|
945
|
+
try { fs.unlinkSync(cleanMarkerPath); } catch (_err) { /* best effort */ }
|
|
946
|
+
}
|
|
947
|
+
} catch (_err) {
|
|
948
|
+
// Non-critical — fall through to default context
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Autonomous walk-away mode rehydration (wf-d712002e / Story C).
|
|
952
|
+
// Disk is canonical; cache evaporates at SIGTERM. SessionStart re-hydrates
|
|
953
|
+
// the cache from disk and surfaces the active-mode reminder so the AI
|
|
954
|
+
// continues to honor autonomous routing across task-boundary restarts.
|
|
955
|
+
// Stale-flag detection (older than autonomousMode.stalenessThresholdMs,
|
|
956
|
+
// default 1h) emits an interruption notice and clears the flag — no
|
|
957
|
+
// auto-resume; the user must explicitly say "continue" / "resume".
|
|
958
|
+
try {
|
|
959
|
+
const sessionState = require('../../flow-session-state');
|
|
960
|
+
const result = sessionState.rehydrateAutonomousFromDisk();
|
|
961
|
+
if (result.hydrated && result.mode) {
|
|
962
|
+
const mode = result.mode;
|
|
963
|
+
output += `### ⚡ AUTONOMOUS MODE ACTIVE\n`;
|
|
964
|
+
output += `Walk-away run \`${mode.runId}\` is in progress (trigger: "${mode.trigger}", started ${mode.activatedAt}).\n\n`;
|
|
965
|
+
output += `**Routing rules** for this run:\n`;
|
|
966
|
+
output += `- productBehavior / ux questions → \`queue-for-review\` (do NOT ask the user; \`flow-question-queue\` collects them).\n`;
|
|
967
|
+
output += `- engineering / naming / implementation → decide autonomously.\n`;
|
|
968
|
+
output += `- low-confidence technical → self-adversarial challenge to ≥90% confidence; if still uncertain after the cap, queue.\n`;
|
|
969
|
+
output += `- Blocking errors (typecheck/test/conflict) → fix autonomously; only surface if fundamentally un-fixable.\n\n`;
|
|
970
|
+
output += `**No hedging**. Forbidden phrases (per feedback-patterns.md 2026-04-16): "let me know if", "should I continue", "awaiting your signal", "standing by", "would you like me to". The user is walked away; there is no one to answer until the run ends.\n\n`;
|
|
971
|
+
output += `**Exit conditions**: ready queue drains, user types "stop"/"pause", or fatal error. On exit: render completion summary (terminal block + JSON payload at \`autonomous-run-summary-${mode.runId}.json\`).\n\n`;
|
|
972
|
+
} else if (result.reason === 'stale' && result.staleMode) {
|
|
973
|
+
const stale = result.staleMode;
|
|
974
|
+
output += `### ⚠️ Autonomous Run Interrupted\n`;
|
|
975
|
+
output += `A previous autonomous run (\`${stale.runId}\`, started ${stale.activatedAt}) exceeded the staleness threshold and has been cleared.\n\n`;
|
|
976
|
+
output += `Review \`.workflow/state/autonomous-run-summary-${stale.runId}.json\` (if present) to see what completed before the interruption. The session is now in standard interactive mode. To resume, the user must explicitly say "continue" / "resume autonomous run" — do not auto-restart.\n\n`;
|
|
977
|
+
}
|
|
978
|
+
} catch (_err) {
|
|
979
|
+
// Non-critical — autonomous-mode injection failure must not block session start
|
|
980
|
+
}
|
|
981
|
+
|
|
874
982
|
// Workspace worker auto-resume (wf-restart-handoff / 2.22.2).
|
|
875
983
|
// CRITICAL priority — shown at the top so the model acts on it before
|
|
876
984
|
// anything else. Fires when a worker session starts with queued channel
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Session End: Memory Proposal Surfacing
|
|
5
|
+
*
|
|
6
|
+
* Reads pending IGR-artifact edit proposals staged by `flow memory propose`
|
|
7
|
+
* and returns a structured summary for the session-end adapter to display.
|
|
8
|
+
*
|
|
9
|
+
* Agent-proposed edits do NOT auto-apply. The user reviews at session-end
|
|
10
|
+
* and runs `flow memory approve <id>` / `flow memory reject <id>`.
|
|
11
|
+
*
|
|
12
|
+
* Story: wf-4434851f (IGR artifact edit proposals).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const store = require('../../../lib/memory-proposal-store');
|
|
16
|
+
|
|
17
|
+
function summarizePendingMemoryProposals() {
|
|
18
|
+
let pending;
|
|
19
|
+
try {
|
|
20
|
+
pending = store.listProposals({ status: 'pending' });
|
|
21
|
+
} catch (_err) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (!pending || pending.length === 0) return null;
|
|
25
|
+
|
|
26
|
+
const byOp = { append: 0, 'replace-section': 0, 'replace-all': 0 };
|
|
27
|
+
const byBlock = {};
|
|
28
|
+
const previews = [];
|
|
29
|
+
for (const p of pending) {
|
|
30
|
+
byOp[p.op] = (byOp[p.op] || 0) + 1;
|
|
31
|
+
byBlock[p.block] = (byBlock[p.block] || 0) + 1;
|
|
32
|
+
try {
|
|
33
|
+
previews.push(store.previewProposal(p));
|
|
34
|
+
} catch (_err) {
|
|
35
|
+
// Non-fatal — skip a broken preview but keep the rest.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
count: pending.length,
|
|
41
|
+
byOp,
|
|
42
|
+
byBlock,
|
|
43
|
+
proposals: pending,
|
|
44
|
+
previews,
|
|
45
|
+
message: formatMessage(pending.length, byOp, previews),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatMessage(count, byOp, previews) {
|
|
50
|
+
const plural = count !== 1 ? 's' : '';
|
|
51
|
+
const breakdown = Object.entries(byOp)
|
|
52
|
+
.filter(([, n]) => n > 0)
|
|
53
|
+
.map(([op, n]) => `${n} ${op}`)
|
|
54
|
+
.join(', ');
|
|
55
|
+
return [
|
|
56
|
+
`${count} pending memory proposal${plural} (${breakdown}):`,
|
|
57
|
+
...previews,
|
|
58
|
+
'',
|
|
59
|
+
'Review: flow memory list',
|
|
60
|
+
'Approve: flow memory approve <id>',
|
|
61
|
+
'Reject: flow memory reject <id> [--reason <text>]',
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { summarizePendingMemoryProposals };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Session End: Skill Proposal Surfacing
|
|
5
|
+
*
|
|
6
|
+
* Reads pending skill proposals staged by `flow skill propose|patch|remove`
|
|
7
|
+
* and returns a structured summary for the session-end adapter to display.
|
|
8
|
+
*
|
|
9
|
+
* Agent-proposed changes do NOT auto-apply. The user reviews at session-end
|
|
10
|
+
* and runs `flow skill promote <name>` / `flow skill reject <name>`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const store = require('../../../lib/skill-proposal-store');
|
|
14
|
+
|
|
15
|
+
function summarizePendingProposals() {
|
|
16
|
+
let pending;
|
|
17
|
+
try {
|
|
18
|
+
pending = store.listProposals({ status: 'pending' });
|
|
19
|
+
} catch (_err) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (!pending || pending.length === 0) return null;
|
|
23
|
+
|
|
24
|
+
const byAction = { propose: 0, patch: 0, remove: 0 };
|
|
25
|
+
const lines = [];
|
|
26
|
+
for (const p of pending) {
|
|
27
|
+
byAction[p.action] = (byAction[p.action] || 0) + 1;
|
|
28
|
+
const icon = p.action === 'propose' ? '+' : p.action === 'patch' ? '~' : '-';
|
|
29
|
+
const rationale = p.rationale ? ` — ${p.rationale}` : '';
|
|
30
|
+
lines.push(` ${icon} ${p.skillName} (${p.action}, ${p.id})${rationale}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
count: pending.length,
|
|
35
|
+
byAction,
|
|
36
|
+
proposals: pending,
|
|
37
|
+
lines,
|
|
38
|
+
message: formatMessage(pending.length, byAction, lines),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatMessage(count, byAction, lines) {
|
|
43
|
+
const plural = count !== 1 ? 's' : '';
|
|
44
|
+
const breakdown = Object.entries(byAction)
|
|
45
|
+
.filter(([, n]) => n > 0)
|
|
46
|
+
.map(([a, n]) => `${n} ${a}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
return [
|
|
49
|
+
`${count} pending skill proposal${plural} (${breakdown}):`,
|
|
50
|
+
...lines,
|
|
51
|
+
'',
|
|
52
|
+
'Review with: flow skill pending',
|
|
53
|
+
'Approve: flow skill promote <name>',
|
|
54
|
+
'Discard: flow skill reject <name>',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { summarizePendingProposals };
|
|
@@ -68,6 +68,31 @@ function handleSessionEnd(input) {
|
|
|
68
68
|
result.logged = false;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// Surface pending skill proposals staged by `flow skill propose|patch|remove`.
|
|
72
|
+
// These await user approval (`flow skill promote|reject`) at session end.
|
|
73
|
+
try {
|
|
74
|
+
const { summarizePendingProposals } = require('./session-end-skill-proposals');
|
|
75
|
+
const summary = summarizePendingProposals();
|
|
76
|
+
if (summary) {
|
|
77
|
+
result.pendingSkillProposals = summary;
|
|
78
|
+
}
|
|
79
|
+
} catch (_err) {
|
|
80
|
+
// Non-critical — never block session end
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Surface pending IGR-artifact memory proposals staged by `flow memory
|
|
84
|
+
// propose`. These await user approval (`flow memory approve|reject`) at
|
|
85
|
+
// session end. Story: wf-4434851f.
|
|
86
|
+
try {
|
|
87
|
+
const { summarizePendingMemoryProposals } = require('./session-end-memory-proposals');
|
|
88
|
+
const summary = summarizePendingMemoryProposals();
|
|
89
|
+
if (summary) {
|
|
90
|
+
result.pendingMemoryProposals = summary;
|
|
91
|
+
}
|
|
92
|
+
} catch (_err) {
|
|
93
|
+
// Non-critical — never block session end
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
// Scratch directory cleanup — remove temp files created during session
|
|
72
97
|
try {
|
|
73
98
|
const fs = require('node:fs');
|
|
@@ -45,7 +45,7 @@ function getSetupConfig() {
|
|
|
45
45
|
* @returns {Object} Result: { needsSetup, message, action, context }
|
|
46
46
|
*/
|
|
47
47
|
function handleSetup(options = {}) {
|
|
48
|
-
const {
|
|
48
|
+
const { _trigger = 'init' } = options;
|
|
49
49
|
|
|
50
50
|
if (!isSetupEnabled()) {
|
|
51
51
|
return {
|
|
@@ -51,6 +51,7 @@ const { safeJsonParse } = require('../../flow-io');
|
|
|
51
51
|
|
|
52
52
|
const PENDING_MARKER_FILE = 'task-just-completed';
|
|
53
53
|
const LAST_TRIGGERED_FILE = 'task-boundary-last-triggered';
|
|
54
|
+
const CLEAN_COMPLETION_MARKER_FILE = 'task-boundary-clean-completion.json';
|
|
54
55
|
// Window during which a recentlyCompleted[0] entry is considered "fresh
|
|
55
56
|
// enough" to retro-mark Phase 1 from the Stop hook. Large enough to cover
|
|
56
57
|
// a slow quality-gate run; small enough that a session opened hours later
|
|
@@ -69,6 +70,56 @@ function getLastTriggeredPath() {
|
|
|
69
70
|
return path.join(PATHS.state, LAST_TRIGGERED_FILE);
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
function getCleanCompletionMarkerPath() {
|
|
74
|
+
return path.join(PATHS.state, CLEAN_COMPLETION_MARKER_FILE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Write the clean-completion marker when Phase 2 successfully restarts after a
|
|
79
|
+
* cleanly-completed task. The next session's SessionStart hook reads this
|
|
80
|
+
* marker to decide whether to inject an AUTO-PICKUP block (wf-f267ea2a).
|
|
81
|
+
*
|
|
82
|
+
* Best-effort — failure here does NOT abort the restart; auto-pickup just
|
|
83
|
+
* won't fire for this restart.
|
|
84
|
+
*/
|
|
85
|
+
function writeCleanCompletionMarker(taskId, taskTitle, options = {}) {
|
|
86
|
+
// Durable write (Story E / wf-e28b6cd8 — Blocker M2 fix).
|
|
87
|
+
//
|
|
88
|
+
// Original failure mode: writeFileSync schedules the data; the kernel buffers
|
|
89
|
+
// it; SIGTERM is dispatched; the process exits before the buffer reaches
|
|
90
|
+
// disk. The restarted session sees no marker → cascade silently fails.
|
|
91
|
+
//
|
|
92
|
+
// Fix: write to a tmp file, fsync the FILE, atomic-rename to final, fsync
|
|
93
|
+
// the DIRECTORY. The directory fsync is what guarantees the rename itself
|
|
94
|
+
// is durable across the SIGTERM/relaunch boundary.
|
|
95
|
+
try {
|
|
96
|
+
const p = getCleanCompletionMarkerPath();
|
|
97
|
+
const dir = path.dirname(p);
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
const payload = {
|
|
100
|
+
version: 1,
|
|
101
|
+
completedTaskId: taskId || null,
|
|
102
|
+
completedTaskTitle: taskTitle || null,
|
|
103
|
+
completedAt: new Date().toISOString(),
|
|
104
|
+
...(options.nextTaskId ? { nextTaskId: options.nextTaskId } : {})
|
|
105
|
+
};
|
|
106
|
+
const data = JSON.stringify(payload);
|
|
107
|
+
const tmp = `${p}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
108
|
+
const fd = fs.openSync(tmp, 'w');
|
|
109
|
+
try {
|
|
110
|
+
fs.writeSync(fd, data);
|
|
111
|
+
fs.fsyncSync(fd);
|
|
112
|
+
} finally {
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
fs.renameSync(tmp, p);
|
|
116
|
+
try {
|
|
117
|
+
const dfd = fs.openSync(dir, 'r');
|
|
118
|
+
try { fs.fsyncSync(dfd); } finally { fs.closeSync(dfd); }
|
|
119
|
+
} catch (_err) { /* directory fsync is best-effort */ }
|
|
120
|
+
} catch (_err) { /* best effort — auto-pickup just won't fire if this fails */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
72
123
|
function readLastTriggered() {
|
|
73
124
|
try {
|
|
74
125
|
return safeJsonParse(getLastTriggeredPath(), null);
|
|
@@ -155,9 +206,13 @@ function checkPreconditions() {
|
|
|
155
206
|
* Returns a result object for diagnostics; never throws. If something goes
|
|
156
207
|
* wrong, the Stop hook should continue with its normal flow.
|
|
157
208
|
*
|
|
158
|
-
* @
|
|
209
|
+
* @param {Object} [opts]
|
|
210
|
+
* @param {string} [opts.transcriptPath] - Claude Code transcript path, used by
|
|
211
|
+
* the main-mode question classifier safety net. When absent, the classifier
|
|
212
|
+
* step is skipped (fail-open).
|
|
213
|
+
* @returns {Promise<{ triggered: boolean, reason?: string, flagPath?: string, parentPid?: number }>}
|
|
159
214
|
*/
|
|
160
|
-
function consumeAndTriggerRestart() {
|
|
215
|
+
async function consumeAndTriggerRestart(opts = {}) {
|
|
161
216
|
const markerPath = getPendingMarkerPath();
|
|
162
217
|
if (!fs.existsSync(markerPath)) {
|
|
163
218
|
return { triggered: false, reason: 'no-pending-marker' };
|
|
@@ -173,6 +228,42 @@ function consumeAndTriggerRestart() {
|
|
|
173
228
|
}
|
|
174
229
|
} catch (_err) { /* flow-ask may not be present in older installs; degrade open */ }
|
|
175
230
|
|
|
231
|
+
// Main-mode question classifier safety net (wf-191d5f6e). Catches the case
|
|
232
|
+
// where the AI ends a turn with an open user-facing question but forgot to
|
|
233
|
+
// call `flow ask` first. On a YES classification, auto-write the
|
|
234
|
+
// pending-question marker and defer the restart. Fail-open throughout —
|
|
235
|
+
// any error or skip falls through to normal restart logic.
|
|
236
|
+
try {
|
|
237
|
+
const isWorker = process.env.WOGI_WORKSPACE_ROOT &&
|
|
238
|
+
process.env.WOGI_REPO_NAME &&
|
|
239
|
+
process.env.WOGI_REPO_NAME !== 'manager';
|
|
240
|
+
if (!isWorker) {
|
|
241
|
+
const config = getConfig();
|
|
242
|
+
const clf = config.mainModeQuestionClassifier;
|
|
243
|
+
const enabled = clf?.enabled !== false; // default true
|
|
244
|
+
if (enabled && opts.transcriptPath) {
|
|
245
|
+
const { classifyQuestion } = require('../../flow-worker-question-classifier');
|
|
246
|
+
const result = await classifyQuestion({
|
|
247
|
+
mode: 'main',
|
|
248
|
+
transcriptPath: opts.transcriptPath,
|
|
249
|
+
minConfidence: Number.isFinite(clf?.minConfidence) ? clf.minConfidence : 70,
|
|
250
|
+
model: typeof clf?.model === 'string' ? clf.model : undefined
|
|
251
|
+
});
|
|
252
|
+
if (result?.blocked) {
|
|
253
|
+
try {
|
|
254
|
+
const { markQuestionPending } = require('../../flow-ask');
|
|
255
|
+
markQuestionPending(`auto-deferred: ${String(result.reason || 'classifier detected open question').slice(0, 500)}`);
|
|
256
|
+
} catch (_err) { /* best effort — marker write failure falls through to restart */ }
|
|
257
|
+
return { triggered: false, reason: 'auto-deferred-question-detected' };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (process.env.DEBUG) {
|
|
263
|
+
console.error(`[task-boundary-reset] main-mode classifier error (fail-open): ${err.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
176
267
|
const pre = checkPreconditions();
|
|
177
268
|
if (!pre.ready) {
|
|
178
269
|
if (process.env.DEBUG) {
|
|
@@ -210,6 +301,13 @@ function consumeAndTriggerRestart() {
|
|
|
210
301
|
return { triggered: false, reason: `flag-write-failed: ${err.message}` };
|
|
211
302
|
}
|
|
212
303
|
|
|
304
|
+
// Write the clean-completion marker so the NEXT session's SessionStart
|
|
305
|
+
// hook can inject the AUTO-PICKUP block (wf-f267ea2a). This is gated on
|
|
306
|
+
// config.taskBoundaryReset.autoPickupNextTask in session-context.js — the
|
|
307
|
+
// marker is always written here when Phase 2 fires (cheap, harmless), and
|
|
308
|
+
// session-context decides whether to act on it.
|
|
309
|
+
writeCleanCompletionMarker(markerPayload?.taskId, markerPayload?.taskTitle);
|
|
310
|
+
|
|
213
311
|
// SIGTERM our parent (claude). The wrapper sees the flag on claude's exit
|
|
214
312
|
// and restarts. If SIGTERM turns out to not shut claude down cleanly in
|
|
215
313
|
// real testing, try SIGHUP or SIGINT as fallbacks (see spec wf-39e9dc09).
|
|
@@ -329,6 +427,8 @@ module.exports = {
|
|
|
329
427
|
checkPreconditions,
|
|
330
428
|
hasPendingMarker,
|
|
331
429
|
getPendingMarkerPath,
|
|
430
|
+
getCleanCompletionMarkerPath,
|
|
431
|
+
writeCleanCompletionMarker,
|
|
332
432
|
|
|
333
433
|
// Back-compat: earlier code calls this name. Route it to Phase 1 so existing
|
|
334
434
|
// wiring in task-completed.js still does the right thing (mark the marker,
|
|
@@ -352,8 +452,14 @@ if (require.main === module) {
|
|
|
352
452
|
process.exit(0);
|
|
353
453
|
}
|
|
354
454
|
if (arg === 'consume') {
|
|
355
|
-
|
|
356
|
-
|
|
455
|
+
consumeAndTriggerRestart().then((r) => {
|
|
456
|
+
console.log(JSON.stringify(r, null, 2));
|
|
457
|
+
process.exit(0);
|
|
458
|
+
}).catch((err) => {
|
|
459
|
+
console.error(err.message);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
});
|
|
462
|
+
return;
|
|
357
463
|
}
|
|
358
464
|
console.log('Usage: node task-boundary-reset.js <check|has-pending|mark|consume>');
|
|
359
465
|
process.exit(2);
|
|
@@ -99,7 +99,78 @@ function isWorkspaceWorker() {
|
|
|
99
99
|
return true;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Path-discipline gate (Story B / wf-ab59f0e4 — Phase 4.5).
|
|
104
|
+
*
|
|
105
|
+
* Single-writer invariant: workers NEVER write into the workspace-manager
|
|
106
|
+
* tree (`<workspace>/.workspace/**`); the manager NEVER writes into worker
|
|
107
|
+
* member-repo `.workflow/state/**` paths. Cross-process state coordination
|
|
108
|
+
* happens exclusively via the channel-dispatch HTTP bus.
|
|
109
|
+
*
|
|
110
|
+
* Without this check, a confused worker (or hostile prompt-injection
|
|
111
|
+
* payload that talked the worker into editing manager state) could corrupt
|
|
112
|
+
* `dispatched-tasks.json` for ALL workers in the workspace. The check
|
|
113
|
+
* fails LOUD (block + clear error) so the boundary violation is impossible
|
|
114
|
+
* to ignore — silent corruption is the worst-case alternative.
|
|
115
|
+
*
|
|
116
|
+
* Returns the same `{ blocked, reason?, message? }` shape as
|
|
117
|
+
* `checkWorkerBoundary` so the caller can compose the two checks.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} toolName
|
|
120
|
+
* @param {Object} toolInput - { file_path } for Edit/Write
|
|
121
|
+
* @returns {{ blocked: boolean, reason?: string, message?: string }}
|
|
122
|
+
*/
|
|
123
|
+
function checkPathDiscipline(toolName, toolInput) {
|
|
124
|
+
if (!process.env.WOGI_WORKSPACE_ROOT) return { blocked: false };
|
|
125
|
+
const writeTools = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
126
|
+
if (!writeTools.has(toolName)) return { blocked: false };
|
|
127
|
+
const filePath = toolInput && (toolInput.file_path || toolInput.notebook_path);
|
|
128
|
+
if (typeof filePath !== 'string' || !filePath) return { blocked: false };
|
|
129
|
+
|
|
130
|
+
const repo = process.env.WOGI_REPO_NAME || '';
|
|
131
|
+
const root = process.env.WOGI_WORKSPACE_ROOT;
|
|
132
|
+
const managerStateDir = `${root.replace(/\/+$/, '')}/.workspace/`;
|
|
133
|
+
const memberStateDirRegex = /\/members?\/[^/]+\/\.workflow\/state\//;
|
|
134
|
+
|
|
135
|
+
if (repo && repo !== 'manager') {
|
|
136
|
+
if (filePath.startsWith(managerStateDir)) {
|
|
137
|
+
return {
|
|
138
|
+
blocked: true,
|
|
139
|
+
reason: 'path-discipline-worker',
|
|
140
|
+
message: [
|
|
141
|
+
`PATH DISCIPLINE: workers MUST NOT write to manager-owned files.`,
|
|
142
|
+
``,
|
|
143
|
+
`Blocked: ${filePath}`,
|
|
144
|
+
``,
|
|
145
|
+
`${managerStateDir}** is owned by the manager process.`,
|
|
146
|
+
`Use the channel-dispatch HTTP bus to communicate with the manager;`,
|
|
147
|
+
`never edit shared workspace state directly. See Story B (wf-ab59f0e4)`,
|
|
148
|
+
`Phase 4.5 in .workflow/changes/wf-ab59f0e4.md.`
|
|
149
|
+
].join('\n')
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (repo === 'manager' && memberStateDirRegex.test(filePath)) {
|
|
155
|
+
return {
|
|
156
|
+
blocked: true,
|
|
157
|
+
reason: 'path-discipline-manager',
|
|
158
|
+
message: [
|
|
159
|
+
`PATH DISCIPLINE: manager MUST NOT write to worker member-repo state.`,
|
|
160
|
+
``,
|
|
161
|
+
`Blocked: ${filePath}`,
|
|
162
|
+
``,
|
|
163
|
+
`Worker member-repos own their own .workflow/state/. Send a channel`,
|
|
164
|
+
`dispatch to the worker if state changes are needed there.`
|
|
165
|
+
].join('\n')
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { blocked: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
102
172
|
module.exports = {
|
|
103
173
|
checkWorkerBoundary,
|
|
174
|
+
checkPathDiscipline,
|
|
104
175
|
isWorkspaceWorker
|
|
105
176
|
};
|