sortie-dogs 0.6.1 → 0.7.1

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/README.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Sortie-dogs
2
2
 
3
- **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
4
-
5
- > **Project status: Experimental / unsupported.** No stability, compatibility,
6
- > or support guarantees are provided. Mk2A2 remains the canonical internal workflow.
3
+ **Give OpenCode a task; get a bounded, validated implementation loop instead of an open-ended agent run.**
4
+
5
+ Sortie-dogs is an execution harness for OpenCode, applying harness-engineering
6
+ principles through mechanical write boundaries, validation, review, and bounded
7
+ orchestration.
8
+
9
+ > **Project status: Beta.** Stable in regular use. As a pre-1.0 release,
10
+ > configuration and runtime assets may still change between releases.
7
11
 
8
12
  [![npm](https://img.shields.io/npm/v/sortie-dogs)](https://www.npmjs.com/package/sortie-dogs)
9
13
  [![license](https://img.shields.io/npm/l/sortie-dogs)](LICENSE)
@@ -20,7 +24,7 @@ Requirements: Node.js 22.6 or newer, npm, and OpenCode.
20
24
 
21
25
  Guides: [日本語](docs/guide-ja.md) · [简体中文](docs/guide-zh-CN.md) · [CLI testing](docs/cli-testing.md)
22
26
 
23
- Release: [v0.6.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.6.1)
27
+ Release: [v0.7.1](https://github.com/zufall-upon/Sortie-dogs/releases/tag/v0.7.1)
24
28
 
25
29
  ## Quick start
26
30
 
@@ -2,5 +2,5 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export declare const RUNTIME_ASSET_VERSION = "0.3.50-observability-reflection-v1";
5
+ export declare const RUNTIME_ASSET_VERSION = "0.3.52-acceptance-continuity-v1";
6
6
  export type RuntimeAssetVersion = typeof RUNTIME_ASSET_VERSION;
@@ -2,4 +2,4 @@
2
2
  * Version of the installable runtime assets. Kept in its own module so the plugin can compare an
3
3
  * installed project marker without importing every asset body.
4
4
  */
5
- export const RUNTIME_ASSET_VERSION = "0.3.50-observability-reflection-v1";
5
+ export const RUNTIME_ASSET_VERSION = "0.3.52-acceptance-continuity-v1";
@@ -0,0 +1,20 @@
1
+ export declare const ACCEPTANCE_CONTINUITY_EXTENSION: "sortie-dogs/acceptance-continuity";
2
+ export declare const ACCEPTANCE_CONTINUITY_SCHEMA_VERSION: "0.1";
3
+ export declare const ACCEPTANCE_CONTINUITY_AUTHORITY: "dispatch";
4
+ export declare const MAX_ACCEPTANCE_CONTINUITY_BYTES: number;
5
+ export declare const MAX_ACCEPTANCE_CRITERIA = 24;
6
+ export interface AcceptanceContinuityLedger {
7
+ readonly schema_version: "0.1";
8
+ readonly authority: "dispatch";
9
+ readonly task_id: string;
10
+ readonly criteria: readonly string[];
11
+ readonly fingerprint: string;
12
+ readonly parent_fingerprint: "none" | string;
13
+ }
14
+ export interface AcceptanceContinuityInspection {
15
+ readonly ledger: AcceptanceContinuityLedger | undefined;
16
+ readonly error: "absent" | "malformed" | "oversize" | undefined;
17
+ }
18
+ export declare function normalizeAcceptanceCriteria(criteria: readonly string[]): string[];
19
+ export declare function acceptanceContinuityFingerprint(criteria: readonly string[]): string;
20
+ export declare function inspectAcceptanceContinuity(handoff: unknown): AcceptanceContinuityInspection;
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ export const ACCEPTANCE_CONTINUITY_EXTENSION = "sortie-dogs/acceptance-continuity";
3
+ export const ACCEPTANCE_CONTINUITY_SCHEMA_VERSION = "0.1";
4
+ export const ACCEPTANCE_CONTINUITY_AUTHORITY = "dispatch";
5
+ export const MAX_ACCEPTANCE_CONTINUITY_BYTES = 32 * 1024;
6
+ export const MAX_ACCEPTANCE_CRITERIA = 24;
7
+ const FINGERPRINT = /^sha256:[0-9a-f]{64}$/u;
8
+ const MAX_TASK_ID = 256;
9
+ const MAX_CRITERION = 2_000;
10
+ function object(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ export function normalizeAcceptanceCriteria(criteria) {
14
+ return criteria.map((criterion) => criterion.replace(/\r\n?/gu, "\n").normalize("NFC"));
15
+ }
16
+ export function acceptanceContinuityFingerprint(criteria) {
17
+ const canonical = JSON.stringify(normalizeAcceptanceCriteria(criteria));
18
+ return `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
19
+ }
20
+ export function inspectAcceptanceContinuity(handoff) {
21
+ if (!object(handoff) || !object(handoff.ext) || !(ACCEPTANCE_CONTINUITY_EXTENSION in handoff.ext)) {
22
+ return { ledger: undefined, error: "absent" };
23
+ }
24
+ const raw = handoff.ext[ACCEPTANCE_CONTINUITY_EXTENSION];
25
+ let bytes;
26
+ try {
27
+ bytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
28
+ }
29
+ catch {
30
+ return { ledger: undefined, error: "malformed" };
31
+ }
32
+ if (bytes > MAX_ACCEPTANCE_CONTINUITY_BYTES)
33
+ return { ledger: undefined, error: "oversize" };
34
+ if (!object(raw) || Object.keys(raw).some((key) => ![
35
+ "schema_version", "authority", "task_id", "criteria", "fingerprint", "parent_fingerprint",
36
+ ].includes(key)) || raw.schema_version !== ACCEPTANCE_CONTINUITY_SCHEMA_VERSION ||
37
+ raw.authority !== ACCEPTANCE_CONTINUITY_AUTHORITY || typeof raw.task_id !== "string" ||
38
+ raw.task_id.length === 0 || raw.task_id.length > MAX_TASK_ID || !Array.isArray(raw.criteria) ||
39
+ raw.criteria.length === 0 || raw.criteria.length > MAX_ACCEPTANCE_CRITERIA ||
40
+ !raw.criteria.every((criterion) => typeof criterion === "string" && criterion.length > 0 && criterion.length <= MAX_CRITERION) ||
41
+ new Set(raw.criteria).size !== raw.criteria.length || typeof raw.fingerprint !== "string" ||
42
+ !FINGERPRINT.test(raw.fingerprint) || !(raw.parent_fingerprint === "none" ||
43
+ (typeof raw.parent_fingerprint === "string" && FINGERPRINT.test(raw.parent_fingerprint)))) {
44
+ return { ledger: undefined, error: "malformed" };
45
+ }
46
+ const criteria = normalizeAcceptanceCriteria(raw.criteria);
47
+ if (new Set(criteria).size !== criteria.length || acceptanceContinuityFingerprint(criteria) !== raw.fingerprint) {
48
+ return { ledger: undefined, error: "malformed" };
49
+ }
50
+ return {
51
+ ledger: {
52
+ schema_version: ACCEPTANCE_CONTINUITY_SCHEMA_VERSION,
53
+ authority: ACCEPTANCE_CONTINUITY_AUTHORITY,
54
+ task_id: raw.task_id,
55
+ criteria,
56
+ fingerprint: raw.fingerprint,
57
+ parent_fingerprint: raw.parent_fingerprint,
58
+ },
59
+ error: undefined,
60
+ };
61
+ }
@@ -0,0 +1,54 @@
1
+ export declare const RETAINED_STATE_EXTENSION: "sortie-dogs/retained-state";
2
+ export declare const RETAINED_STATE_SCHEMA_VERSION: "0.1";
3
+ export declare const RETAINED_STATE_AUTHORITY: "shadow";
4
+ export declare const MAX_RETAINED_STATE_BYTES: number;
5
+ export declare const MAX_RETAINED_STATE_ITEMS = 12;
6
+ export declare const MAX_RETAINED_STATE_WARNINGS = 8;
7
+ export interface RetainedValidationAttempt {
8
+ readonly command: string;
9
+ readonly exit: number | null;
10
+ readonly fingerprint: string;
11
+ }
12
+ export interface NextEvidenceDecision {
13
+ readonly schema_version: "0.1";
14
+ readonly authority: "shadow";
15
+ readonly gap_id: string;
16
+ readonly blocked_acceptance: string;
17
+ readonly question: string;
18
+ readonly expected_discrimination: string;
19
+ readonly action: string;
20
+ readonly stop_condition: string;
21
+ }
22
+ export interface AdmissionReceipt {
23
+ readonly evidence_id: string;
24
+ readonly source_agent: string;
25
+ readonly source_revision: string;
26
+ readonly evidence_fingerprint: string;
27
+ readonly supports: readonly string[];
28
+ readonly contradicts: readonly string[];
29
+ readonly freshness_basis: string;
30
+ readonly status: "recorded" | "recorded_with_warnings";
31
+ readonly warnings: readonly string[];
32
+ }
33
+ export interface RetainedStateCapsule {
34
+ readonly schema_version: "0.1";
35
+ readonly authority: "shadow";
36
+ readonly task_id: string;
37
+ readonly acceptance_fingerprint: string;
38
+ readonly source_manifest: "none" | readonly string[];
39
+ readonly operation_manifest: "none" | string;
40
+ readonly validation_history: readonly RetainedValidationAttempt[];
41
+ readonly blockers: readonly string[];
42
+ readonly next_action: string;
43
+ readonly next_evidence_decision?: NextEvidenceDecision;
44
+ readonly admissions?: readonly AdmissionReceipt[];
45
+ }
46
+ export interface RetainedStateWarning {
47
+ readonly code: "malformed" | "oversize" | "optional_invalid";
48
+ readonly message: string;
49
+ }
50
+ export interface RetainedStateInspection {
51
+ readonly capsule: RetainedStateCapsule | undefined;
52
+ readonly warnings: readonly RetainedStateWarning[];
53
+ }
54
+ export declare function inspectRetainedStateCapsule(handoff: unknown): RetainedStateInspection;
@@ -0,0 +1,81 @@
1
+ export const RETAINED_STATE_EXTENSION = "sortie-dogs/retained-state";
2
+ export const RETAINED_STATE_SCHEMA_VERSION = "0.1";
3
+ export const RETAINED_STATE_AUTHORITY = "shadow";
4
+ export const MAX_RETAINED_STATE_BYTES = 16 * 1024;
5
+ export const MAX_RETAINED_STATE_ITEMS = 12;
6
+ export const MAX_RETAINED_STATE_WARNINGS = 8;
7
+ const MAX_SHORT = 256;
8
+ const MAX_TEXT = 1000;
9
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
10
+ const isText = (value, max = MAX_TEXT) => typeof value === "string" && value.length > 0 && value.length <= max;
11
+ const isBoundedList = (value, max = MAX_RETAINED_STATE_ITEMS) => Array.isArray(value) && value.length <= max;
12
+ const isSourceManifest = (value) => value === "none" || (isBoundedList(value) && value.length === new Set(value).size && value.every((item) => isText(item, 512)));
13
+ function validAttempt(value) {
14
+ const exit = isObject(value) ? value.exit : undefined;
15
+ return isObject(value) && isText(value.command, 1000) && (exit === null || (typeof exit === "number" && Number.isInteger(exit) && exit >= -1 && exit <= 255)) && isText(value.fingerprint, MAX_SHORT);
16
+ }
17
+ function validDecision(value) {
18
+ return isObject(value) && value.schema_version === "0.1" && value.authority === "shadow" &&
19
+ isText(value.gap_id, MAX_SHORT) && isText(value.blocked_acceptance) && isText(value.question) &&
20
+ isText(value.expected_discrimination) && isText(value.action) && isText(value.stop_condition);
21
+ }
22
+ function validAdmission(value) {
23
+ return isObject(value) && isText(value.evidence_id, MAX_SHORT) && isText(value.source_agent, MAX_SHORT) &&
24
+ isText(value.source_revision, MAX_SHORT) && isText(value.evidence_fingerprint, MAX_SHORT) &&
25
+ isBoundedList(value.supports) && value.supports.every((item) => isText(item, MAX_SHORT)) &&
26
+ isBoundedList(value.contradicts) && value.contradicts.every((item) => isText(item, MAX_SHORT)) &&
27
+ isText(value.freshness_basis) && (value.status === "recorded" || value.status === "recorded_with_warnings") &&
28
+ isBoundedList(value.warnings, MAX_RETAINED_STATE_WARNINGS) && value.warnings.every((item) => isText(item, MAX_SHORT));
29
+ }
30
+ export function inspectRetainedStateCapsule(handoff) {
31
+ if (!isObject(handoff) || !isObject(handoff.ext) || !(RETAINED_STATE_EXTENSION in handoff.ext)) {
32
+ return { capsule: undefined, warnings: [] };
33
+ }
34
+ const raw = handoff.ext[RETAINED_STATE_EXTENSION];
35
+ let bytes;
36
+ try {
37
+ bytes = new TextEncoder().encode(JSON.stringify(raw)).byteLength;
38
+ }
39
+ catch {
40
+ return { capsule: undefined, warnings: [{ code: "malformed", message: "Retained state was ignored because it is malformed." }] };
41
+ }
42
+ if (bytes > MAX_RETAINED_STATE_BYTES)
43
+ return { capsule: undefined, warnings: [{ code: "oversize", message: "Retained state was ignored because it exceeds its bound." }] };
44
+ if (!isObject(raw) || raw.schema_version !== "0.1" || raw.authority !== "shadow" || !isText(raw.task_id, MAX_SHORT) ||
45
+ !isText(raw.acceptance_fingerprint, MAX_SHORT) || !isSourceManifest(raw.source_manifest) ||
46
+ !(raw.operation_manifest === "none" || isText(raw.operation_manifest, 512)) ||
47
+ !isBoundedList(raw.validation_history) || !raw.validation_history.every(validAttempt) ||
48
+ !isBoundedList(raw.blockers) || !raw.blockers.every((item) => isText(item)) || !isText(raw.next_action)) {
49
+ return { capsule: undefined, warnings: [{ code: "malformed", message: "Retained state was ignored because it is malformed." }] };
50
+ }
51
+ const capsule = {
52
+ schema_version: "0.1",
53
+ authority: "shadow",
54
+ task_id: raw.task_id,
55
+ acceptance_fingerprint: raw.acceptance_fingerprint,
56
+ source_manifest: structuredClone(raw.source_manifest),
57
+ operation_manifest: raw.operation_manifest,
58
+ validation_history: structuredClone(raw.validation_history),
59
+ blockers: structuredClone(raw.blockers),
60
+ next_action: raw.next_action,
61
+ };
62
+ const warnings = [];
63
+ if ("next_evidence_decision" in raw) {
64
+ if (validDecision(raw.next_evidence_decision))
65
+ capsule.next_evidence_decision = structuredClone(raw.next_evidence_decision);
66
+ else
67
+ warnings.push({ code: "optional_invalid", message: "An optional retained-state decision was ignored." });
68
+ }
69
+ if ("admissions" in raw) {
70
+ if (isBoundedList(raw.admissions)) {
71
+ const validAdmissions = raw.admissions.filter(validAdmission);
72
+ if (validAdmissions.length > 0 || raw.admissions.length === 0)
73
+ capsule.admissions = structuredClone(validAdmissions);
74
+ if (validAdmissions.length !== raw.admissions.length)
75
+ warnings.push({ code: "optional_invalid", message: "Invalid retained-state admission receipts were ignored." });
76
+ }
77
+ else
78
+ warnings.push({ code: "optional_invalid", message: "Optional retained-state admission receipts were ignored." });
79
+ }
80
+ return { capsule, warnings };
81
+ }
package/dist/index.d.ts CHANGED
@@ -19,3 +19,7 @@ export type { HandoffDenialReason, OpenCodeEvent, OpenCodeHooks, OpenCodePlugin,
19
19
  export { CONSULTATION_CAPABILITIES, CONSULTATION_ROLE_POLICY, MAX_REVIEW_ARTIFACT_BYTES, SOURCE_REVIEW_RISK_TAGS, STRATEGY_TRIGGERS, evaluateReviewAvailability, evaluateReviewGate, evaluateSourceReviewRequirement, isSourceReviewRiskTag, requiresSourceReview, shouldConsultStrategy, validateReviewArtifact, validateReviewVerdict, } from "./core/consultation.js";
20
20
  export type { ConsultationAdapter, ConsultationCapability, ConsultationRequest, ConsultationResult, ReviewArtifact, ReviewAvailability, ReviewFinding, ReviewFindingSeverity, ReviewGateInput, ReviewGateResult, ReviewVerdict, ReviewVerdictKind, SourceReviewConsultationRequest, SourceReviewConsultationResult, SourceReviewRequirement, SourceReviewRequirementInput, SourceReviewRiskTag, StrategyConsultationRequest, StrategyConsultationResult, StrategyTrigger, StrategyTriggerInput, UnavailableConsultationResult, ValidationResult, } from "./core/consultation.js";
21
21
  export type * from "./core/types.js";
22
+ export { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA, normalizeAcceptanceCriteria, } from "./core/acceptance-continuity.js";
23
+ export type { AcceptanceContinuityInspection, AcceptanceContinuityLedger, } from "./core/acceptance-continuity.js";
24
+ export { inspectRetainedStateCapsule, MAX_RETAINED_STATE_BYTES, MAX_RETAINED_STATE_ITEMS, MAX_RETAINED_STATE_WARNINGS, RETAINED_STATE_AUTHORITY, RETAINED_STATE_EXTENSION, RETAINED_STATE_SCHEMA_VERSION, } from "./core/retained-state.js";
25
+ export type { AdmissionReceipt, NextEvidenceDecision, RetainedStateCapsule, RetainedStateInspection, RetainedStateWarning, RetainedValidationAttempt, } from "./core/retained-state.js";
package/dist/index.js CHANGED
@@ -15,3 +15,5 @@ export { SortieDogsPlugin } from "./plugin/index.js";
15
15
  */
16
16
  export { HandoffDeniedError, InvalidModelTargetError, ModelRoutingDeniedError, isExplicitTaskHandoff, } from "./plugin/index.js";
17
17
  export { CONSULTATION_CAPABILITIES, CONSULTATION_ROLE_POLICY, MAX_REVIEW_ARTIFACT_BYTES, SOURCE_REVIEW_RISK_TAGS, STRATEGY_TRIGGERS, evaluateReviewAvailability, evaluateReviewGate, evaluateSourceReviewRequirement, isSourceReviewRiskTag, requiresSourceReview, shouldConsultStrategy, validateReviewArtifact, validateReviewVerdict, } from "./core/consultation.js";
18
+ export { ACCEPTANCE_CONTINUITY_AUTHORITY, ACCEPTANCE_CONTINUITY_EXTENSION, ACCEPTANCE_CONTINUITY_SCHEMA_VERSION, acceptanceContinuityFingerprint, inspectAcceptanceContinuity, MAX_ACCEPTANCE_CONTINUITY_BYTES, MAX_ACCEPTANCE_CRITERIA, normalizeAcceptanceCriteria, } from "./core/acceptance-continuity.js";
19
+ export { inspectRetainedStateCapsule, MAX_RETAINED_STATE_BYTES, MAX_RETAINED_STATE_ITEMS, MAX_RETAINED_STATE_WARNINGS, RETAINED_STATE_AUTHORITY, RETAINED_STATE_EXTENSION, RETAINED_STATE_SCHEMA_VERSION, } from "./core/retained-state.js";
@@ -89,25 +89,27 @@ const ROLLOVER_PROMPT = [
89
89
  "If a section has nothing, write exactly '- なし'. Never delete a heading.",
90
90
  "At most 2 content lines per section. Keep the whole output under 1200 characters.",
91
91
  "Copy task identity, manifest paths, commands, exit codes, and counter values character-for-character.",
92
+ "Never author, shorten, infer, or treat summary text as acceptance authority. Preserve the exact acceptance fingerprint and handoff path; the resumed coordinator rereads that immutable handoff before dispatch.",
93
+ "The unmet-user-requirements, constraints, candidate queue, blocker, and next-action sections never replace the immutable handoff acceptance ledger.",
92
94
  "The coordinator final report immediately before compaction is the newest source of truth. Its outcomes and next action override older context.",
93
95
  "Never list a unit as uncommitted or next when that final report says it was committed or completed.",
94
96
  "Drop the finished unit's conversation, raw logs, diffs, and tool output.",
95
- "Preserve task identity, every accepted fact, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, and the exact next action. If a value is absent, write なし; never guess one.",
97
+ "Preserve task identity, acceptance fingerprint, exact handoff path, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, and the exact next action. If a value is absent, write なし; never guess one.",
96
98
  "Never write credentials, API keys, tokens, personal data, or source code.",
97
99
  "Never output an HTML comment.",
98
100
  "",
99
101
  ROLLOVER_TOKEN,
100
102
  "",
101
103
  "## 未達のユーザー要求",
102
- "- <まだ満たされていない明示要求>",
104
+ "- <acceptance criteriaを除く、まだ満たされていない明示要求>",
103
105
  "",
104
106
  "## task identity と制約",
105
107
  "- <task_id・候補識別子・project root。該当なしはなし>",
106
- "- <守り続ける制約と確定済みの受け入れ基準>",
108
+ "- <守り続ける非acceptance制約。該当なしはなし>",
107
109
  "",
108
110
  "## manifest",
109
- "- source_manifest: <exact entries または none>",
110
- "- operation_manifest: <exact path または none>",
111
+ "- handoff_path: <exact path または none> / acceptance_fingerprint: <exact fingerprint または none>",
112
+ "- source_manifest: <exact entries または none> / operation_manifest: <exact path または none>",
111
113
  "",
112
114
  "## validation履歴",
113
115
  "- <command> | exit <code> | <fingerprint>",
@@ -116,7 +118,7 @@ const ROLLOVER_PROMPT = [
116
118
  "- batchTarget: <n> / batchAttempted: <n> / batchCommitted: <n> / batchReconciled: <n>",
117
119
  "",
118
120
  "## tracker batch state",
119
- "- inventoryFingerprint: <fingerprint> / candidateQueue: <bounded identity, status, ordering, implementation root, acceptance fingerprint, acceptance hashes, and redacted acceptance digest records>",
121
+ "- inventoryFingerprint: <fingerprint> / candidateQueue: <bounded identity, status, ordering, implementation root, exact handoff path, and opaque acceptance fingerprint only>",
120
122
  "- pendingTrackerUpdates: <terminal outcomes awaiting flush> / flushState: <pending | flushed | reconciliation-required>",
121
123
  "",
122
124
  "## 未解決blocker",
@@ -127,7 +129,7 @@ const ROLLOVER_PROMPT = [
127
129
  ].join("\n");
128
130
  const RECOVERY_ROLLOVER_PROMPT = ROLLOVER_PROMPT
129
131
  .replace("The coordinator final report immediately before compaction is the newest source of truth. Its outcomes and next action override older context.", "The recovery report overrides only terminal outcomes and batch counters. Preserve unmet user requirements, ordered scope, no-stop constraints, and the exact next unit from the conversation.")
130
- .replace("Preserve task identity, every accepted fact, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, and the exact next action. If a value is absent, write なし; never guess one.", "Preserve task identity, every accepted fact, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, unmet ordered scope, and the exact next unit. If a value is absent, write なし; never guess one.");
132
+ .replace("Preserve task identity, acceptance fingerprint, exact handoff path, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, and the exact next action. If a value is absent, write なし; never guess one.", "Preserve task identity, acceptance fingerprint, exact handoff path, both manifests, ordered validation history, batchTarget, batchAttempted, batchCommitted, batchReconciled, inventory fingerprint, bounded candidate queue, pending tracker updates, tracker flush state, blocker state, unmet ordered scope, and the exact next unit. Never derive acceptance from summary text. If a value is absent, write なし; never guess one.");
131
133
  const ROLLOVER_HEADINGS = [
132
134
  "## 未達のユーザー要求",
133
135
  "## task identity と制約",
@@ -403,7 +405,12 @@ export function createContinuationHooks(client, directory, policySource, timings
403
405
  const entries = [];
404
406
  for (const { index, line } of topLevelProtocolLines(text)) {
405
407
  const explicit = /^status:\s*(DONE|BLOCKED|NEED_DECISION)\b/iu.exec(line)?.[1]?.toUpperCase();
406
- const status = explicit ??
408
+ const aliasMatch = /^([✅⛔❓])[ \t]+conclusion:\s*status:\s*(DONE|BLOCKED|NEED_DECISION)\b/iu.exec(line);
409
+ const aliasStatus = aliasMatch?.[2]?.toUpperCase();
410
+ const alias = aliasStatus === "DONE" && aliasMatch?.[1] === "✅" ? "DONE" :
411
+ aliasStatus === "BLOCKED" && aliasMatch?.[1] === "⛔" ? "BLOCKED" :
412
+ aliasStatus === "NEED_DECISION" && aliasMatch?.[1] === "❓" ? "NEED_DECISION" : undefined;
413
+ const status = explicit ?? alias ??
407
414
  (/^✅[ \t]+\*\*DONE\*\*/u.test(line) ? "DONE" :
408
415
  /^⛔[ \t]+\*\*BLOCKED\*\*/u.test(line) ? "BLOCKED" :
409
416
  /^❓[ \t]+\*\*NEED_DECISION\*\*/u.test(line) ? "NEED_DECISION" : undefined);
@@ -413,15 +420,21 @@ export function createContinuationHooks(client, directory, policySource, timings
413
420
  }
414
421
  return entries;
415
422
  }
423
+ function firstCheckpoint(text) {
424
+ const firstLine = topLevelProtocolLines(text).find(({ line }) => line.trim().length > 0);
425
+ return firstLine === undefined
426
+ ? undefined
427
+ : checkpointEntries(text).find(({ index }) => index === firstLine.index);
428
+ }
416
429
  function checkpointStatus(text) {
417
- return checkpointEntries(text).at(-1)?.status;
430
+ return firstCheckpoint(text)?.status;
418
431
  }
419
432
  function terminalCheckpoint(text) {
420
433
  const status = checkpointStatus(text);
421
434
  return status === "DONE" || status === "NEED_DECISION" || (status === "BLOCKED" && trueBlockerReport(text));
422
435
  }
423
436
  function trueBlockerReport(text) {
424
- const checkpoint = checkpointEntries(text).at(-1);
437
+ const checkpoint = firstCheckpoint(text);
425
438
  if (checkpoint?.status !== "BLOCKED")
426
439
  return false;
427
440
  return topLevelProtocolLines(text).some(({ line, index }) => index > checkpoint.index &&
@@ -472,7 +485,7 @@ export function createContinuationHooks(client, directory, policySource, timings
472
485
  function observeRecoveryReport(state, report) {
473
486
  if (state.recoveryObservedRevision === state.turnRevision)
474
487
  return state.recoveryRepeatCount;
475
- const checkpoint = checkpointEntries(report).at(-1);
488
+ const checkpoint = firstCheckpoint(report);
476
489
  const checkpointLine = checkpoint === undefined
477
490
  ? undefined
478
491
  : topLevelProtocolLines(report).find(({ index }) => index === checkpoint.index)?.line;