Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All notable changes to codexclaw are documented here. The format follows

### Changed

- Goalplans can record pending user decisions: `cxc loop ask --session <id> --id <q> --question <text> [--recommendation <text>] [--work-phase <id>]...` links a question the agent already asked to the phases that wait on it, and `cxc loop decide --session <id> --id <q> --answer <text>` records the answer. Linked phases are not runnable while the decision is open; unrelated phases stay ready. When every remaining phase and unmet criterion waits on an open decision, the Stop hook lets an IDLE turn end instead of asking to start another phase; the goal stays active and cannot be completed early. Old plans load unchanged (#262).
- The absolute Stop continuation cap (24) now counts per genuine user turn instead of per session, and the release prints one notice per turn (#254).

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C680-brightgreen" alt="3,680 tests">
<img src="https://img.shields.io/badge/tests-3%2C708-brightgreen" alt="3,708 tests">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-31-blue" alt="31 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C680-brightgreen" alt="3,680 tests">
<img src="https://img.shields.io/badge/tests-3%2C708-brightgreen" alt="3,708 tests">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-31-blue" alt="31 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

<p align="center">
<a href="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml"><img src="https://github.com/lidge-jun/codexclaw/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<img src="https://img.shields.io/badge/tests-3%2C680-brightgreen" alt="3,680 tests">
<img src="https://img.shields.io/badge/tests-3%2C708-brightgreen" alt="3,708 tests">
<img src="https://img.shields.io/badge/skills-29-blue" alt="29 skills">
<img src="https://img.shields.io/badge/hooks-31-blue" alt="31 hooks">
<a href="https://lidge-jun.github.io/codexclaw/"><img src="https://img.shields.io/badge/docs-codexclaw-black" alt="Documentation"></a>
Expand Down
92 changes: 88 additions & 4 deletions plugins/codexclaw/components/pabcd-state/dist/goalplan-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
*/
import {
addGoalplanTask,
askGoalplanDecision,
decideGoalplanDecision,
openDecisionIdsForPhase,
buildGoalplan,
completeGoalplanTask,
goalplanDefinitionIntegrityReasons,
Expand Down Expand Up @@ -106,6 +109,12 @@ import { applySteeringBatch } from "./steering.js";












Expand All @@ -124,6 +133,8 @@ const VERBS = new Set ([
"add-task",
"complete-task",
"meet-criterion",
"ask",
"decide",
]);


Expand All @@ -137,6 +148,7 @@ const VERBS = new Set ([




const VERB_RULES = {
init: { allowed: new Set(["--objective", "--session", "--criterion", "--schema-version", "--cwd"]), repeatable: new Set(["--criterion"]), usage: "init --objective <text> [--session <id>] [--criterion <text>]... [--schema-version <n>] [--cwd <path>]" },
show: { allowed: new Set(["--slug", "--objective", "--session", "--cwd"]), repeatable: new Set(), usage: "show (--slug <slug> | --objective <text> | --session <id>) [--cwd <path>]" },
Expand All @@ -148,6 +160,8 @@ const VERB_RULES = {
"add-task": { allowed: new Set(["--session", "--work-phase", "--id", "--title", "--depends-on", "--cwd"]), repeatable: new Set(["--depends-on"]), usage: "add-task --session <id> --work-phase <id> --id <id> --title <text> [--depends-on <task-id>]... [--cwd <path>]" },
"complete-task": { allowed: new Set(["--session", "--work-phase", "--id", "--outcome", "--cwd"]), repeatable: new Set(), usage: "complete-task --session <id> --work-phase <id> --id <id> --outcome <text> [--cwd <path>]" },
"meet-criterion": { allowed: new Set(["--session", "--id", "--evidence", "--cwd"]), repeatable: new Set(), usage: "meet-criterion --session <id> --id <id> --evidence <text> [--cwd <path>]" },
ask: { allowed: new Set(["--session", "--id", "--question", "--recommendation", "--work-phase", "--cwd"]), repeatable: new Set(["--work-phase"]), usage: "ask --session <id> --id <id> --question <text> [--recommendation <text>] [--work-phase <id>]... [--cwd <path>]" },
decide: { allowed: new Set(["--session", "--id", "--answer", "--cwd"]), repeatable: new Set(), usage: "decide --session <id> --id <id> --answer <text> [--cwd <path>]" },
help: { allowed: new Set(), repeatable: new Set(), usage: "--help" },
};

Expand All @@ -163,12 +177,12 @@ export function parseGoalplanCliArgs(argv , cwd )
}
if (!VERBS.has(verb)) {
return {
error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase|ready|add-task|complete-task|meet-criterion); run cxc loop --help`,
error: `unknown loop verb '${argv[0] ?? ""}' (expected init|show|validate|steer|add-criterion|add-work-phase|ready|add-task|complete-task|meet-criterion|ask|decide); run cxc loop --help`,
};
}
const selected = verb ;
const rule = VERB_RULES[selected];
const out = { verb: selected, cwd, criteria: [], dependsOn: [] };
const out = { verb: selected, cwd, criteria: [], dependsOn: [], workPhaseIds: [] };
const seen = new Set ();
const reject = (message ) => ({ error: `${selected}: ${message}` });
for (let i = 1; i < argv.length; i++) {
Expand Down Expand Up @@ -213,7 +227,17 @@ export function parseGoalplanCliArgs(argv , cwd )
case "--presented": out.presented = value; break;
case "--id": out.id = value; break;
case "--title": out.title = value; break;
case "--work-phase": out.workPhaseId = value; break;
case "--work-phase": {
if (selected !== "ask") { out.workPhaseId = value; break; }
const phaseId = value.trim();
if (!phaseId) return reject("--work-phase requires one non-empty id");
if (out.workPhaseIds .includes(phaseId)) return reject(`--work-phase must not repeat id '${phaseId}'`);
out.workPhaseIds .push(phaseId);
break;
}
case "--question": out.question = value; break;
case "--recommendation": out.recommendation = value; break;
case "--answer": out.answer = value; break;
case "--outcome": out.outcome = value; break;
case "--schema-version": {
const parsed = Number(value);
Expand Down Expand Up @@ -438,6 +462,13 @@ function runReady(args , plan ) {

const phases = readyWorkPhases(plan);
const tasks = readyTasks(plan);
const openDecisions = (plan.decisions ?? []).filter((decision) => decision.status === "open")
.map(({ id, question, recommendation, askedAt }) => ({ id, question, ...(recommendation === undefined ? {} : { recommendation }), askedAt }));
const awaitingDecisions = plan.workPhases
.filter((wp) => wp.status === "pending" || wp.status === "in_progress")
.map((wp) => ({ workPhaseId: wp.id, decisionIds: openDecisionIdsForPhase(plan, wp).filter((id) =>
(plan.decisions ?? []).some((decision) => decision.id === id && decision.status === "open")) }))
.filter((entry) => entry.decisionIds.length > 0);
if (args.json === true) {
return {
output: JSON.stringify({
Expand All @@ -455,6 +486,7 @@ function runReady(args , plan ) {
id: entry.task.id,
title: entry.task.title,
})),
...(plan.decisions === undefined ? {} : { openDecisions, awaitingDecisions }),
}),
code: 0,
};
Expand All @@ -467,9 +499,52 @@ function runReady(args , plan ) {
lines.push(tasks.length > 0
? `readyTasks: ${tasks.map((entry) => `${entry.workPhaseId}/${entry.task.id} (${entry.task.title})`).join("; ")}`
: "readyTasks: none");
if (plan.decisions !== undefined) {
lines.push(openDecisions.length > 0
? `openDecisions: ${openDecisions.map((decision) => `${decision.id} (${decision.question})`).join("; ")}`
: "openDecisions: none");
lines.push(awaitingDecisions.length > 0
? `awaitingDecisions: ${awaitingDecisions.map((entry) => `${entry.workPhaseId}: ${entry.decisionIds.join(", ")}`).join("; ")}`
: "awaitingDecisions: none");
}
return { output: lines.join("\n"), code: 0 };
}

/** Record a host-submitted question or its answer under the goalplan write lock. */
function runDecision(args ) {
const session = (args.session ?? "").trim();
if (!session) return { output: `loop ${args.verb}: --session <id> is required`, code: 1 };
if (!isCanonicalSessionId(session)) return { output: `loop ${args.verb}: session id is not canonical`, code: 1 };
const slug = readState(args.cwd, session).slug;
if (!slug) return { output: `loop ${args.verb}: session '${session}' has no bound goalplan - run \`cxc loop init --session ${session}\` first`, code: 1 };
const id = (args.id ?? "").trim();
if (args.verb === "ask" && (!id || !(args.question ?? "").trim())) {
return { output: "loop ask: --id and non-empty --question are required", code: 1 };
}
if (args.verb === "decide" && (!id || !(args.answer ?? "").trim())) {
return { output: "loop decide: --id and non-empty --answer are required", code: 1 };
}

const locked = withGoalplanWriteLock (args.cwd, slug, (plan) => {
const result = args.verb === "ask"
? askGoalplanDecision(plan, {
id, question: args.question , recommendation: args.recommendation,
workPhaseIds: args.workPhaseIds ?? [], askedAt: new Date().toISOString(),
})
: decideGoalplanDecision(plan, id, args.answer , new Date().toISOString());
if (result.kind === "rejected") return { kind: "rejected", reason: result.reason };
if (result.kind === "unchanged") return { kind: "unchanged", reason: result.reason };
writeGoalplan(args.cwd, result.plan);
return { kind: "changed" };
});
if (locked.kind === "locked" || locked.kind === "unreadable") {
return { output: `loop ${args.verb}: ${locked.reason}`, code: 1 };
}
if (locked.value.kind === "rejected") return { output: `loop ${args.verb}: ${locked.value.reason}`, code: 1 };
if (locked.value.kind === "unchanged") return { output: `loop ${args.verb}: ${locked.value.reason}; nothing to do`, code: 0 };
return { output: `loop ${args.verb}: ${slug} ${id} applied`, code: 0 };
}

/**
* 060 wp6: the three lifecycle verbs share one locked read-modify-write.
*
Expand Down Expand Up @@ -610,6 +685,12 @@ function renderPlanLines(plan , lock )
for (const c of plan.criteria) {
lines.push(` - ${c.id} [${c.status}] ${c.scenario}`);
}
for (const decision of plan.decisions ?? []) {
if (decision.status !== "open") continue;
lines.push(` - ${decision.id} [open] ${decision.question}`);
const waiting = plan.workPhases.filter((wp) => wp.awaitsDecision?.includes(decision.id));
lines.push(` waiting: ${waiting.map((wp) => wp.id).join(", ") || "none"}`);
}
return lines.join("\n");
}

Expand All @@ -623,7 +704,7 @@ export function renderGoalplanHelp() {
"cxc loop — durable goalplan for a multi-cycle PABCD loop",
"",
"Usage:",
...(["init", "show", "validate", "steer", "add-criterion", "add-work-phase", "ready", "add-task", "complete-task", "meet-criterion", "help"] )
...(["init", "show", "validate", "steer", "add-criterion", "add-work-phase", "ready", "add-task", "complete-task", "meet-criterion", "ask", "decide", "help"] )
.map((verb) => ` cxc loop ${VERB_RULES[verb].usage}`),
"",
"Notes:",
Expand All @@ -639,6 +720,8 @@ export function renderGoalplanHelp() {
" additionally require an approved finalGate, and no verb in this build opens a",
" final-gate review round, so opt in only if you can record that gate yourself.",
" meet-criterion requires non-empty captured evidence for the same reason.",
" Send the question through the host first, then record it with ask; ask never sends a message.",
" Record the user's reply with decide. It changes only the decision record.",
"",
"steer --batch-json expects an object with:",
' { "idempotencyKey": "<unique>", "rationale": "<why>", "evidence": "<proof>",',
Expand Down Expand Up @@ -710,6 +793,7 @@ export function runGoalplanCli(args ) {
}

if (args.verb === "steer") return runSteer(args);
if (args.verb === "ask" || args.verb === "decide") return runDecision(args);

if (args.verb === "add-criterion" || args.verb === "add-work-phase") return runAddOp(args);

Expand Down
Loading
Loading