@chinhae/atw-cli 0.2.6 → 0.2.7
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.
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.7",
|
|
3
|
+
"description": "Active-task pointers are no longer borrowed by count (#608 upstream); the Pi extension inherits a pointer only through the window's own session lineage and tells a fresh window to bind the sole in-progress task instead of asking.",
|
|
4
|
+
"breaking": false,
|
|
5
|
+
"recommendMigrate": false,
|
|
6
|
+
"changelog": "resolve_active_task() no longer treats a sole runtime session file as proof of ownership: allow_single_session_fallback defaults to False and only inject-subagent-context.py opts in, because copilot/gemini/qoder child agents cannot inherit a parent session id (mirrors upstream Trellis #608). Main sessions on every platform therefore see no active task until task.py start binds the window. The Pi extension now moves the previous session's pointer to the new session when session_start reports reason=new or fork with a previousSessionFile — the lineage Pi itself declares for /clear and fork — so a /clear in the same window keeps its task without counting pointer files or trusting key prefixes (#512 holds). Its startup <ready> block also tells a fresh window with exactly one implement/accept-stage task assigned to the developer to run task.py start for it rather than asking whether to create a task, and to ask when several qualify or the only candidate is still in planning.",
|
|
7
|
+
"migrations": [],
|
|
8
|
+
"notes": "No migration required. Changed files are all tracked by atw update: .atw/scripts/common/active_task.py, the shared inject-subagent-context.py hook under each platform directory, and .pi/extensions/atw/index.ts. Expect one explicit task.py start per new window on every platform from now on; a Pi /clear keeps the binding automatically."
|
|
9
|
+
}
|
|
@@ -648,17 +648,15 @@ def resolve_active_task(
|
|
|
648
648
|
platform_input: dict[str, Any] | None = None,
|
|
649
649
|
platform: str | None = None,
|
|
650
650
|
*,
|
|
651
|
-
allow_single_session_fallback: bool =
|
|
651
|
+
allow_single_session_fallback: bool = False,
|
|
652
652
|
allow_environment_context: bool = True,
|
|
653
653
|
) -> ActiveTask:
|
|
654
654
|
"""Resolve the active task from session runtime state only.
|
|
655
655
|
|
|
656
|
-
A stale session task is returned as stale.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
(copilot, gemini, qoder) that don't inherit the parent's session id. ≥2
|
|
661
|
-
files or 0 files yield ActiveTask(None) — refuses to guess across windows.
|
|
656
|
+
A stale session task is returned as stale.
|
|
657
|
+
Missing or unmatched session identity does not infer ownership from the
|
|
658
|
+
number of session files. Pull-based child-agent callers that cannot inherit
|
|
659
|
+
a parent identity must opt into the compatibility fallback explicitly.
|
|
662
660
|
"""
|
|
663
661
|
context_key = resolve_context_key(
|
|
664
662
|
platform_input,
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
openSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from "node:fs";
|
|
2
11
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
12
|
import {
|
|
4
13
|
delimiter,
|
|
@@ -1055,17 +1064,19 @@ function parseAgentFM(c: string): AgentConfig {
|
|
|
1055
1064
|
return cfg;
|
|
1056
1065
|
}
|
|
1057
1066
|
|
|
1067
|
+
function keyForSessionId(sessionId: string): string {
|
|
1068
|
+
const normalized = sessionId.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1069
|
+
if (!normalized) return `pi_${hash(sessionId)}`;
|
|
1070
|
+
return `pi_${normalized}${normalized === sessionId ? "" : `_${hash(sessionId)}`}`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1058
1073
|
function contextKey(input?: unknown, ctx?: PiExtensionContext): string | null {
|
|
1059
1074
|
const sessionId =
|
|
1060
1075
|
callStr(ctx?.sessionManager?.getSessionId, ctx?.sessionManager) ??
|
|
1061
1076
|
str(process.env.PI_SESSION_ID) ??
|
|
1062
1077
|
str(process.env.PI_SESSIONID) ??
|
|
1063
1078
|
lookupStr(input, ["session_id", "sessionId", "sessionID"]);
|
|
1064
|
-
if (sessionId)
|
|
1065
|
-
const normalized = sessionId.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1066
|
-
if (!normalized) return `pi_${hash(sessionId)}`;
|
|
1067
|
-
return `pi_${normalized}${normalized === sessionId ? "" : `_${hash(sessionId)}`}`;
|
|
1068
|
-
}
|
|
1079
|
+
if (sessionId) return keyForSessionId(sessionId);
|
|
1069
1080
|
const transcriptPath =
|
|
1070
1081
|
callStr(ctx?.sessionManager?.getSessionFile, ctx?.sessionManager) ??
|
|
1071
1082
|
lookupStr(input, ["transcript_path", "transcriptPath", "transcript"]);
|
|
@@ -1117,6 +1128,55 @@ function readTaskDir(root: string, key: string | null): string | null {
|
|
|
1117
1128
|
}
|
|
1118
1129
|
}
|
|
1119
1130
|
|
|
1131
|
+
// ── Session lineage ───────────────────────────────────────────────────
|
|
1132
|
+
// 只读 pi 会话文件的首行头部拿会话 id;会话文件可能很大,不整读。
|
|
1133
|
+
function readSessionHeaderId(sessionFile: string): string | null {
|
|
1134
|
+
let fd: number | null = null;
|
|
1135
|
+
try {
|
|
1136
|
+
fd = openSync(sessionFile, "r");
|
|
1137
|
+
const buf = Buffer.alloc(65536);
|
|
1138
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
1139
|
+
const line = buf.toString("utf-8", 0, n).split("\n")[0] ?? "";
|
|
1140
|
+
const header = JSON.parse(line) as JsonObject;
|
|
1141
|
+
return header.type === "session" ? str(header.id) : null;
|
|
1142
|
+
} catch {
|
|
1143
|
+
return null;
|
|
1144
|
+
} finally {
|
|
1145
|
+
if (fd !== null) closeSync(fd);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// 同一窗口用 /clear(reason=new)或 fork 换出新会话时,把前一个会话的活动任务指针
|
|
1150
|
+
// 搬到新会话名下。只认 pi 在 session_start 事件里给出的 previousSessionFile 这条
|
|
1151
|
+
// 明确血缘,不看运行时目录里有几个指针、也不看前缀(#512)。新开的窗口
|
|
1152
|
+
// (reason=startup)没有前身,什么都不继承。搬移而非复制,避免 /clear 链条堆积指针。
|
|
1153
|
+
function inheritSessionPointer(
|
|
1154
|
+
root: string,
|
|
1155
|
+
key: string | null,
|
|
1156
|
+
event: unknown,
|
|
1157
|
+
): boolean {
|
|
1158
|
+
if (!key || !isObj(event)) return false;
|
|
1159
|
+
const reason = str(event.reason);
|
|
1160
|
+
const previous = str(event.previousSessionFile);
|
|
1161
|
+
if ((reason !== "new" && reason !== "fork") || !previous) return false;
|
|
1162
|
+
const parentId = readSessionHeaderId(previous);
|
|
1163
|
+
if (!parentId) return false;
|
|
1164
|
+
const parentKey = keyForSessionId(parentId);
|
|
1165
|
+
if (parentKey === key) return false;
|
|
1166
|
+
const dir = join(root, ".atw", ".runtime", "sessions");
|
|
1167
|
+
const ownPath = join(dir, `${key}.json`);
|
|
1168
|
+
const parentPath = join(dir, `${parentKey}.json`);
|
|
1169
|
+
try {
|
|
1170
|
+
if (existsSync(ownPath) || !existsSync(parentPath)) return false;
|
|
1171
|
+
const ctx = JSON.parse(readText(parentPath)) as JsonObject;
|
|
1172
|
+
if (!str(ctx.current_task)) return false;
|
|
1173
|
+
renameSync(parentPath, ownPath);
|
|
1174
|
+
return true;
|
|
1175
|
+
} catch {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1120
1180
|
// ── Workflow State Breadcrumb ─────────────────────────────────────────
|
|
1121
1181
|
const WF_RE =
|
|
1122
1182
|
/\[workflow-state:([A-Za-z0-9_-]+)\]\s*\n([\s\S]*?)\n\s*\[\/workflow-state:\1\]/g;
|
|
@@ -1194,7 +1254,13 @@ function buildStartupContext(
|
|
|
1194
1254
|
FIRST_REPLY_NOTICE,
|
|
1195
1255
|
overview,
|
|
1196
1256
|
workflow,
|
|
1197
|
-
|
|
1257
|
+
[
|
|
1258
|
+
"<ready>",
|
|
1259
|
+
"Use the current workflow state to decide whether to create, continue, or skip an ATW task.",
|
|
1260
|
+
"When CURRENT TASK is (none) but ACTIVE TASKS lists exactly one task assigned to you that is already at the implement or accept stage, this window is merely not bound to it yet: run `python3 ./.atw/scripts/task.py start <task-dir>` for that task, then continue from its stage. Do not ask whether to create a new task in that case.",
|
|
1261
|
+
"If several tasks qualify, or the only candidate is still in a planning stage (discover / specify / slice), ask which task to bind instead of running start: start moves a planning-stage task straight into implement.",
|
|
1262
|
+
"</ready>",
|
|
1263
|
+
].join("\n"),
|
|
1198
1264
|
]
|
|
1199
1265
|
.filter(Boolean)
|
|
1200
1266
|
.join("\n\n");
|
|
@@ -1916,7 +1982,8 @@ export default function atwExtension(pi: {
|
|
|
1916
1982
|
|
|
1917
1983
|
// Events
|
|
1918
1984
|
pi.on?.("session_start", (event, ctx) => {
|
|
1919
|
-
getKey(event, ctx);
|
|
1985
|
+
const k = getKey(event, ctx);
|
|
1986
|
+
inheritSessionPointer(resolveRoot(ctx), k, event);
|
|
1920
1987
|
ctx?.ui?.notify?.(
|
|
1921
1988
|
"ATW project context is available. Use /atw-start to bootstrap or /atw-continue to resume.",
|
|
1922
1989
|
"info",
|
|
@@ -1093,7 +1093,11 @@ def main():
|
|
|
1093
1093
|
|
|
1094
1094
|
# Get current task directory. May be None here — the AGENTS_REQUIRE_TASK
|
|
1095
1095
|
# check below is what refuses to continue without one.
|
|
1096
|
-
task_dir = get_current_task(
|
|
1096
|
+
task_dir = get_current_task(
|
|
1097
|
+
repo_root,
|
|
1098
|
+
input_data,
|
|
1099
|
+
allow_single_session_fallback=True,
|
|
1100
|
+
)
|
|
1097
1101
|
|
|
1098
1102
|
# implement/check need task directory
|
|
1099
1103
|
if subagent_type in AGENTS_REQUIRE_TASK:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chinhae/atw-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "A Team Workflow — an AI coding workflow framework that encodes your team's conventions into the process",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"inquirer": "^9.3.7",
|
|
34
34
|
"undici": "^6.21.0",
|
|
35
35
|
"zod": "^4.4.2",
|
|
36
|
-
"@chinhae/atw-core": "0.2.
|
|
36
|
+
"@chinhae/atw-core": "0.2.7"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@eslint/js": "^9.18.0",
|