Skip to content

Commit c8cb597

Browse files
clawplaysclaude
andcommitted
Release 1.9.5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7298c84 commit c8cb597

18 files changed

Lines changed: 169 additions & 12 deletions

assets/for-ai/ar/change-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
1. أنشئ العمل الجديد عبر `ospec change <change-name> [path]`، ويبقى `ospec new` alias للتوافق.
1212
2. إذا وُجد active change مطابق فتابعه ولا تنشئ نسخة مكررة.
13-
3. تدخل تغييرات batch إلى queue وتُنفذ بالتتابع في worktree المشترك.
13+
3. تدخل تغييرات batch إلى queue وتُنفذ بالتتابع في worktree المشترك. يجب استخدام worktree تسلسلياً: يُحظر الإغلاق (verify/finalize/archive) عند وجود ملفات غير مودعة خارج نطاق `affects` وعقد التوثيق في proposal، لذا أودع أو خزّن أو اعزل التغييرات غير المنسوبة، وصرّح بـ `affects` بصدق، ولا تدع تعديلات جلسة متزامنة تنزلق إلى الأرشيف.
1414
4. حافظ فقط على `proposal.md` و`tasks.md` و`state.json` و`verification.md` و`review.md`، ولا تنشئ design أو plan أو task graph أو worker أو review provenance artifacts الخاصة بـ Goal.
1515
5. شغّل فحوص المشروع المرتبطة فعلاً بالتغيير وسجّل الأوامر والنتائج في `verification.md`، ولا تفرض build أو lint أو test أو TDD أو debug غير ذي صلة.
1616
6. ينفذ AI الحالي مراجعة خفيفة واحدة. يمكن إغلاق `APPROVED` و`APPROVED_WITH_CONCERNS` تلقائياً، بينما توقف `PENDING` و`NEEDS_CHANGES` و`BLOCKED` الإغلاق.

assets/for-ai/en-US/change-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ At start, read `.skillrc`, the relevant `SKILL.index.json` entries, `proposal.md
1010

1111
1. Create new work with `ospec change <change-name> [path]`; `ospec new` remains a compatibility alias.
1212
2. Continue an existing matching active change instead of duplicating it.
13-
3. Batch changes go through the queue and run sequentially in the shared worktree.
13+
3. Batch changes go through the queue and run sequentially in the shared worktree. The worktree must be used serially: closeout (verify/finalize/archive) blocks on uncommitted files outside the proposal `affects` and documentation scopes, so commit, stash, or isolate unattributed dirty files, declare `affects` honestly, and never let a concurrent session's edits ride along into the archive.
1414
4. Keep only `proposal.md`, `tasks.md`, `state.json`, `verification.md`, and `review.md` aligned. Do not create Goal design, plan, task graph, worker, or review-provenance artifacts.
1515
5. Run project checks relevant to the actual change and record commands and results in `verification.md`; do not require unrelated build, lint, test, TDD, or debug commands.
1616
6. The current AI performs one lightweight review. `APPROVED` and `APPROVED_WITH_CONCERNS` may close automatically; `PENDING`, `NEEDS_CHANGES`, and `BLOCKED` stop closeout.

assets/for-ai/ja-JP/change-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
1. 新規作業は `ospec change <change-name> [path]` で作成し、`ospec new` は互換 alias として残します。
1212
2. 一致する active change が既にあれば重複作成せず継続します。
13-
3. batch change は queue に入れ、共有 worktree で順番に実行します。
13+
3. batch change は queue に入れ、共有 worktree で順番に実行します。worktree は直列で使用します。closeout(verify/finalize/archive)は proposal の `affects` と文書契約の範囲外にある未コミットファイルでブロックされるため、帰属不明の変更は先にコミット・stash・隔離し、`affects` を正しく宣言し、並行セッションの編集をアーカイブに紛れ込ませないでください。
1414
4. `proposal.md``tasks.md``state.json``verification.md``review.md` だけを同期し、Goal の design、plan、task graph、worker、review provenance artifacts は作りません。
1515
5. 実際の change に関連する project check だけを実行して command と結果を `verification.md` に記録します。無関係な build、lint、test、TDD、debug は要求しません。
1616
6. 現在の AI が軽量 review を 1 回行います。`APPROVED``APPROVED_WITH_CONCERNS` は自動 closeout 可能で、`PENDING``NEEDS_CHANGES``BLOCKED` は停止します。

assets/for-ai/zh-CN/change-protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
1. 新工作使用 `ospec change <change-name> [path]``ospec new` 保留为兼容别名。
1212
2. 已有匹配的 active change 时继续它,不要重复创建。
13-
3. 批量 change 进入 queue,在共享工作区依次执行。
13+
3. 批量 change 进入 queue,在共享工作区依次执行。工作区必须串行使用:闭环(verify/finalize/archive)会阻塞在超出 proposal `affects` 与文档契约范围的未提交文件上;发现无主脏文件时先提交、暂存或隔离,并如实声明 `affects`,不得把并发会话的改动卷入归档。
1414
4. 只维护 `proposal.md``tasks.md``state.json``verification.md``review.md`;不要创建 Goal 的设计、计划、task graph、worker 或 review provenance artifacts。
1515
5. 只运行与实际改动有关的项目检查,并把命令和结果记录到 `verification.md`;不得强制执行无关的 build、lint、test、TDD 或 debug 命令。
1616
6. 当前 AI 完成一次轻量 review。`APPROVED``APPROVED_WITH_CONCERNS` 可以自动收口;`PENDING``NEEDS_CHANGES``BLOCKED` 必须停止。

assets/global-skills/codex/ospec-change/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ ospec finalize [changes/active/<change>] --force-archive --confirm-force-archive
7777
## Guardrails
7878

7979
- Progress checklists track reality: tick each proposal.md acceptance criterion as it is actually verified — archiving blocks while proposal.md has unchecked items. review.md stays your one lightweight review; record its decision and complete its checklist before closeout. Never tick an item whose work was not done.
80+
- The worktree is serial: closeout blocks on uncommitted files outside the proposal `affects` and documentation scopes. Declare `affects` honestly, and when unattributed dirty files appear (for example another session's edits), commit, stash, or isolate them instead of archiving over them.
8081
- Do not assume dashboard workflows exist.
8182
- Do not confuse repository initialization with change execution.
8283
- Do not enter queue mode unless the user explicitly asks for queue behavior.

dist/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const VerifyCommand_1 = require("./commands/VerifyCommand");
6262
const WorkflowCommand_1 = require("./commands/WorkflowCommand");
6363
const LayoutCommand_1 = require("./commands/LayoutCommand");
6464
const services_1 = require("./services");
65-
const CLI_VERSION = '1.9.4';
65+
const CLI_VERSION = '1.9.5';
6666
function showInitUsage() {
6767
console.log('Usage: ospec init [root-dir] [--summary "..."] [--tech-stack node,react] [--architecture "..."] [--document-language en-US|zh-CN|ja-JP|ar]');
6868
}

dist/commands/NewCommand.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export interface NewCommandOptions {
2121
}
2222
export declare class NewCommand extends BaseCommand {
2323
execute(featureName: string, rootDir?: string, options?: NewCommandOptions): Promise<void>;
24+
/**
25+
* Classic changes assume serial execution in a shared worktree; closeout
26+
* blocks on uncommitted files outside the change's declared scope. Surface
27+
* pre-existing dirt at creation time so unrelated concurrent work is
28+
* committed, stashed, or isolated before implementation starts.
29+
*/
30+
private warnOnDirtyWorkspace;
2431
private acquireChangeCreationLease;
2532
private releaseChangeCreationLease;
2633
private readChangeCreationLockOwner;

dist/commands/NewCommand.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3636
exports.NewCommand = void 0;
3737
const path = __importStar(require("path"));
3838
const crypto_1 = require("crypto");
39+
const child_process_1 = require("child_process");
3940
const fs_1 = require("fs");
4041
const constants_1 = require("../core/constants");
4142
const services_1 = require("../services");
@@ -68,6 +69,7 @@ class NewCommand extends BaseCommand_1.BaseCommand {
6869
const featureDir = PathUtils_1.PathUtils.getChangeDir(targetDir, placement, featureName, config);
6970
this.logger.info(`Creating ${placement === constants_1.DIR_NAMES.QUEUED ? 'queued change' : 'change'}: ${featureName}`);
7071
creationLease = await this.acquireChangeCreationLease(targetDir, config);
72+
this.warnOnDirtyWorkspace(targetDir);
7173
if (placement === constants_1.DIR_NAMES.ACTIVE) {
7274
const sessionDir = path.join(targetDir, '.ospec');
7375
const jsonPath = path.join(sessionDir, 'session-brief.json');
@@ -213,6 +215,31 @@ class NewCommand extends BaseCommand_1.BaseCommand {
213215
await this.releaseChangeCreationLease(creationLease);
214216
}
215217
}
218+
/**
219+
* Classic changes assume serial execution in a shared worktree; closeout
220+
* blocks on uncommitted files outside the change's declared scope. Surface
221+
* pre-existing dirt at creation time so unrelated concurrent work is
222+
* committed, stashed, or isolated before implementation starts.
223+
*/
224+
warnOnDirtyWorkspace(targetDir) {
225+
try {
226+
const status = (0, child_process_1.spawnSync)('git', ['status', '--porcelain', '--untracked-files=all', '--no-renames'], { cwd: targetDir, encoding: 'utf8', windowsHide: true });
227+
if (status.error || status.status !== 0)
228+
return;
229+
const dirty = status.stdout
230+
.split(/\r?\n/)
231+
.map(line => line.trimEnd())
232+
.filter(line => line.length > 0)
233+
.map(line => line.slice(3).trim().replace(/^"|"$/g, '').replace(/\\/g, '/'))
234+
.filter(filePath => filePath !== '.skillrc' && !filePath.startsWith('.ospec/'));
235+
if (dirty.length === 0)
236+
return;
237+
this.warn(`Workspace already has ${dirty.length} uncommitted file change(s) (e.g. ${dirty.slice(0, 3).join(', ')}). A classic change assumes serial execution in this worktree, and closeout blocks on files outside the change's declared affects scope. Commit, stash, or isolate unrelated work first.`);
238+
}
239+
catch {
240+
// The workspace warning is best-effort and must never block creation.
241+
}
242+
}
216243
async acquireChangeCreationLease(targetDir, config) {
217244
const changesRoot = (0, ProjectLayout_1.resolveManagedPath)(targetDir, constants_1.DIR_NAMES.CHANGES, config);
218245
await services_1.services.fileService.ensureDir(changesRoot);

dist/services/ClassicChangeCloseoutService.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,23 @@ export interface ClassicChangePluginAnalysis {
1818
archiveReady: boolean;
1919
checks: ChangeStatusCheck[];
2020
}
21+
export interface ClassicChangeWorkspaceAnalysis {
22+
archiveReady: boolean;
23+
outOfScopePaths: string[];
24+
checks: ChangeStatusCheck[];
25+
}
2126
export declare class ClassicChangeCloseoutService {
2227
private readonly fileService;
2328
constructor(fileService: FileService);
29+
/**
30+
* Classic changes assume serial execution in a shared worktree. This guard is
31+
* the classic counterpart of the Goal workspace gate: every uncommitted file
32+
* must belong to the change (its container, managed OSpec bookkeeping, or the
33+
* proposal's declared affects/documentation scopes). Unattributed dirty files
34+
* block closeout so another session's concurrent edits cannot be silently
35+
* archived with this change. Non-Git directories skip the check.
36+
*/
37+
analyzeWorkspaceScope(projectRoot: string, featureDir: string, proposalPath: string): Promise<ClassicChangeWorkspaceAnalysis>;
2438
analyzeDocumentationContract(projectRoot: string, proposalPath: string): Promise<ClassicChangeDocumentationAnalysis>;
2539
analyzeReview(reviewPath: string): Promise<ClassicChangeReviewAnalysis>;
2640
analyzePluginGates(changePath: string, activatedSteps: string[], workflow: PluginWorkflowComposer): Promise<ClassicChangePluginAnalysis>;

dist/services/ClassicChangeCloseoutService.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3636
exports.ClassicChangeCloseoutService = void 0;
3737
const path = __importStar(require("path"));
3838
const fs_1 = require("fs");
39+
const child_process_1 = require("child_process");
3940
const helpers_1 = require("../utils/helpers");
4041
const CHANGE_TYPES = new Set(['bugfix', 'feature', 'maintenance', 'docs']);
4142
const DOCUMENTATION_IMPACTS = new Set(['none', 'required']);
@@ -54,6 +55,92 @@ class ClassicChangeCloseoutService {
5455
constructor(fileService) {
5556
this.fileService = fileService;
5657
}
58+
/**
59+
* Classic changes assume serial execution in a shared worktree. This guard is
60+
* the classic counterpart of the Goal workspace gate: every uncommitted file
61+
* must belong to the change (its container, managed OSpec bookkeeping, or the
62+
* proposal's declared affects/documentation scopes). Unattributed dirty files
63+
* block closeout so another session's concurrent edits cannot be silently
64+
* archived with this change. Non-Git directories skip the check.
65+
*/
66+
async analyzeWorkspaceScope(projectRoot, featureDir, proposalPath) {
67+
const skipped = (message) => ({
68+
archiveReady: true,
69+
outOfScopePaths: [],
70+
checks: [
71+
{
72+
name: 'change.workspace_scope',
73+
status: 'pass',
74+
message,
75+
},
76+
],
77+
});
78+
const status = (0, child_process_1.spawnSync)('git', ['status', '--porcelain', '--untracked-files=all', '--no-renames'], { cwd: projectRoot, encoding: 'utf8', windowsHide: true });
79+
if (status.error || status.status !== 0) {
80+
return skipped('Workspace scope check skipped (not a Git repository)');
81+
}
82+
let affects = [];
83+
let documentationUpdates = [];
84+
try {
85+
const proposal = (0, helpers_1.parseFrontmatterDocument)(await this.fileService.readFile(proposalPath));
86+
const normalizeScope = (item) => String(item || '')
87+
.trim()
88+
.replace(/\\/g, '/')
89+
.replace(/^\.\//, '')
90+
.replace(/\/+$/, '');
91+
affects = Array.isArray(proposal.data?.affects)
92+
? proposal.data.affects.map(normalizeScope).filter(Boolean)
93+
: [];
94+
documentationUpdates = Array.isArray(proposal.data?.documentation_updates)
95+
? proposal.data.documentation_updates.map(normalizeScope).filter(Boolean)
96+
: [];
97+
}
98+
catch {
99+
// Proposal readiness is reported by the documentation contract checks.
100+
}
101+
const changePrefix = path
102+
.relative(projectRoot, featureDir)
103+
.replace(/\\/g, '/')
104+
.replace(/\/+$/, '');
105+
const managedPrefixes = ['.ospec', 'changes', 'for-ai', 'docs/project'];
106+
const managedFiles = new Set(['.skillrc', 'SKILL.md', 'SKILL.index.json']);
107+
const scopes = [...affects, ...documentationUpdates];
108+
const withinScope = (filePath, scope) => filePath === scope || filePath.startsWith(`${scope}/`);
109+
const dirtyPaths = status.stdout
110+
.split(/\r?\n/)
111+
.map(line => line.trimEnd())
112+
.filter(line => line.length > 0)
113+
.map(line => line.slice(3).trim().replace(/^"|"$/g, '').replace(/\\/g, '/'));
114+
const outOfScopePaths = dirtyPaths.filter(filePath => {
115+
if (managedFiles.has(filePath))
116+
return false;
117+
if (changePrefix && withinScope(filePath, changePrefix))
118+
return false;
119+
if (managedPrefixes.some(prefix => withinScope(filePath, prefix)))
120+
return false;
121+
return !scopes.some(scope => withinScope(filePath, scope));
122+
});
123+
if (outOfScopePaths.length === 0) {
124+
return skipped(dirtyPaths.length === 0
125+
? 'Workspace is clean'
126+
: 'All uncommitted changes belong to this change scope');
127+
}
128+
const preview = outOfScopePaths.slice(0, 6).join(', ');
129+
const suffix = outOfScopePaths.length > 6 ? `, ... +${outOfScopePaths.length - 6} more` : '';
130+
return {
131+
archiveReady: false,
132+
outOfScopePaths,
133+
checks: [
134+
{
135+
name: 'change.workspace_scope',
136+
status: 'fail',
137+
message: scopes.length === 0
138+
? `proposal.md declares no affects scope, so ${outOfScopePaths.length} uncommitted file change(s) cannot be attributed to this change: ${preview}${suffix}. Declare affects in proposal.md, or commit/stash/isolate unrelated work before closeout.`
139+
: `Workspace has ${outOfScopePaths.length} uncommitted file change(s) outside this change's declared scope: ${preview}${suffix}. Commit, stash, or isolate unrelated work (or extend proposal.md affects) before closeout.`,
140+
},
141+
],
142+
};
143+
}
57144
async analyzeDocumentationContract(projectRoot, proposalPath) {
58145
if (!(await this.fileService.exists(proposalPath))) {
59146
return {

0 commit comments

Comments
 (0)