phasegate 0.254.0 → 0.283.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/CHANGELOG.md +60 -0
- package/README.ja.md +6 -3
- package/README.md +6 -3
- package/docs/contracts/attestation-v2.schema.json +110 -0
- package/docs/guide/cli-reference.md +12 -1
- package/docs/guide/configuration.md +56 -0
- package/docs/guide/installation.md +1 -1
- package/docs/templates/ci/aidlc-gate.yml +60 -2
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/open-world-obligations-context-dto.ts +30 -0
- package/scripts/harness/agent-integration/application/ports/world-obligations-query-port.ts +30 -0
- package/scripts/harness/agent-integration/application/usecases/get-open-world-obligations-context-usecase.ts +66 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +30 -27
- package/scripts/harness/agent-integration/infrastructure/adapters/world-model-open-obligations-query-adapter.ts +58 -0
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +37 -1
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/agent-integration/presentation/world-obligations-session-context.ts +60 -0
- package/scripts/harness/attestation/application/dto/attestation-document.ts +17 -4
- package/scripts/harness/attestation/application/mappers/attestation-record-mapper.ts +55 -4
- package/scripts/harness/attestation/application/ports/world-snapshot-root-provider.ts +10 -0
- package/scripts/harness/attestation/application/usecases/produce-attestation-usecase.ts +33 -7
- package/scripts/harness/attestation/composition-root.ts +5 -0
- package/scripts/harness/attestation/domain/entities/attestation-record.ts +37 -2
- package/scripts/harness/attestation/index.ts +7 -1
- package/scripts/harness/attestation/presentation/handlers/attest-handler.ts +5 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +40 -9
- package/scripts/harness/config-foundation/application/mappers/world-model-config-mapper.ts +15 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +70 -87
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +80 -88
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/domain/value-objects/world-config.ts +198 -0
- package/scripts/harness/config-foundation/index.ts +14 -15
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +24 -0
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +84 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +84 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +125 -86
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +18 -4
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- package/scripts/harness/installation/application/usecases/run-install.ts +199 -51
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +277 -69
- package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
- package/scripts/harness/integrations/pre-commit.ts +169 -27
- package/scripts/harness/main.ts +200 -136
- package/scripts/harness/traceability-model/application/dto/changed-design-fragment-dto.ts +31 -0
- package/scripts/harness/traceability-model/application/facades/design-change-read-facade.ts +14 -0
- package/scripts/harness/traceability-model/application/ports/staged-design-change-source-port.ts +9 -0
- package/scripts/harness/traceability-model/composition-root.ts +5 -0
- package/scripts/harness/traceability-model/index.ts +9 -0
- package/scripts/harness/traceability-model/infrastructure/adapters/git-staged-design-change-adapter.ts +155 -0
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +31 -0
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +57 -6
- package/scripts/harness/validator-system/composition-root.ts +25 -7
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- package/scripts/harness/validator-system/domain/ports/world-constraint-admission-policy-port.ts +28 -0
- package/scripts/harness/validator-system/domain/ports/world-constraint-rederivation-policy-port.ts +11 -0
- package/scripts/harness/validator-system/domain/services/design-change-declaration-policy.ts +75 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-admission-service.ts +78 -0
- package/scripts/harness/validator-system/domain/services/world-constraint-rederivation-service.ts +76 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +4 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +126 -18
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-admission-adapter.ts +56 -0
- package/scripts/harness/validator-system/infrastructure/adapters/world-model-constraint-rederivation-adapter.ts +56 -0
- package/scripts/harness/world-model/application/dto/pinned-design-endpoint-dto.ts +19 -0
- package/scripts/harness/world-model/application/dto/world-resolved-config-input.ts +92 -27
- package/scripts/harness/world-model/application/dto/world-snapshot-root-dto.ts +8 -0
- package/scripts/harness/world-model/application/facades/pinned-design-endpoint-facade.ts +63 -0
- package/scripts/harness/world-model/application/facades/world-snapshot-root-facade.ts +19 -0
- package/scripts/harness/world-model/application/usecases/derive-world-obligations-use-case.ts +3 -1
- package/scripts/harness/world-model/composition-root.ts +62 -33
- package/scripts/harness/world-model/index.ts +11 -0
- package/scripts/harness/world-model/infrastructure/adapters/attestation-fact-extractor.ts +48 -13
- package/scripts/harness/world-model/infrastructure/adapters/file-system-world-control-repository-adapters.ts +6 -4
- package/scripts/harness/world-model/presentation/cli/world-derive-command-handler.ts +6 -2
|
@@ -6,13 +6,9 @@
|
|
|
6
6
|
* HarnessConfigV2 の harnesses セクションから Hook 設定を読み取る
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import * as fs from
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
ConfigQueryPort,
|
|
13
|
-
HookType,
|
|
14
|
-
} from '../../domain/ports/config-query-port.js';
|
|
15
|
-
import { ProjectPaths } from '../../domain/value-objects/project-paths.js';
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import type { BaselineConfig, ConfigQueryPort, HookType } from "../../domain/ports/config-query-port.js";
|
|
11
|
+
import { ProjectPaths } from "../../domain/value-objects/project-paths.js";
|
|
16
12
|
|
|
17
13
|
interface ProjectDocsSection {
|
|
18
14
|
inception?: string;
|
|
@@ -84,8 +80,22 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
84
80
|
if (this.cachedConfig !== null) {
|
|
85
81
|
return this.cachedConfig;
|
|
86
82
|
}
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
// GitHub #40: config が JSON として壊れている場合に hook プロセス全体を throw で
|
|
84
|
+
// 落とすと、エージェントの全ツール呼び出しが遮断され config の修復自体が不能になる。
|
|
85
|
+
// main.ts の ConfigPersistenceError と同じ意味論(警告 + 既定値で続行)に揃える。
|
|
86
|
+
// ファイル不在等の fs エラーは既存契約どおり throw する(上流の catch が defaults を
|
|
87
|
+
// 適用する)。gated スコープへの書き込みは phase-gate 側が fail-closed でブロックする。
|
|
88
|
+
const raw = fs.readFileSync(this.configPath, "utf8");
|
|
89
|
+
let doc: HarnessConfigDocument;
|
|
90
|
+
try {
|
|
91
|
+
doc = JSON.parse(raw) as HarnessConfigDocument;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
`Warning: phasegate.config.json could not be parsed as JSON (${message}); continuing with default hook settings so self-repair stays possible.\n`,
|
|
96
|
+
);
|
|
97
|
+
doc = {};
|
|
98
|
+
}
|
|
89
99
|
this.cachedConfig = doc;
|
|
90
100
|
return doc;
|
|
91
101
|
}
|
|
@@ -98,10 +108,10 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
98
108
|
// pre-tool-use → agentLessonCollection
|
|
99
109
|
// post-tool-use → cascadeUpdate
|
|
100
110
|
// stop → デフォルト有効
|
|
101
|
-
if (hookType ===
|
|
111
|
+
if (hookType === "pre-tool-use") {
|
|
102
112
|
return harnesses.agentLessonCollection ?? true;
|
|
103
113
|
}
|
|
104
|
-
if (hookType ===
|
|
114
|
+
if (hookType === "post-tool-use") {
|
|
105
115
|
return harnesses.cascadeUpdate ?? true;
|
|
106
116
|
}
|
|
107
117
|
// stop はデフォルト有効
|
|
@@ -111,13 +121,9 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
111
121
|
async getProtectedFilePatterns(): Promise<string[]> {
|
|
112
122
|
const config = this.loadConfig();
|
|
113
123
|
const configured = config.protectedFiles?.patterns ?? [];
|
|
114
|
-
const principlesDocs = config.paths?.principlesDocs ??
|
|
115
|
-
const folderRulesDoc = config.paths?.folderRulesDoc ??
|
|
116
|
-
return [
|
|
117
|
-
...configured,
|
|
118
|
-
`${normalizeProjectPath(principlesDocs)}/**`,
|
|
119
|
-
normalizeProjectPath(folderRulesDoc),
|
|
120
|
-
];
|
|
124
|
+
const principlesDocs = config.paths?.principlesDocs ?? "docs/principles";
|
|
125
|
+
const folderRulesDoc = config.paths?.folderRulesDoc ?? "docs/folder_management_rules.md";
|
|
126
|
+
return [...configured, `${normalizeProjectPath(principlesDocs)}/**`, normalizeProjectPath(folderRulesDoc)];
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
async getProtectedFileExclusions(): Promise<string[]> {
|
|
@@ -135,13 +141,10 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
135
141
|
const paths = config.project?.paths;
|
|
136
142
|
const topLevelPaths = config.paths;
|
|
137
143
|
|
|
138
|
-
return ProjectPaths.create(
|
|
139
|
-
paths?.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
inception: paths?.docs?.inception ?? topLevelPaths?.inceptionDocs ?? 'docs/inception',
|
|
143
|
-
},
|
|
144
|
-
);
|
|
144
|
+
return ProjectPaths.create(paths?.source ?? ["scripts/harness"], {
|
|
145
|
+
construction: paths?.docs?.construction ?? topLevelPaths?.designDocs ?? "docs/product/construction",
|
|
146
|
+
inception: paths?.docs?.inception ?? topLevelPaths?.inceptionDocs ?? "docs/inception",
|
|
147
|
+
});
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
async getBaselineConfig(): Promise<BaselineConfig> {
|
|
@@ -149,7 +152,7 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
149
152
|
const baseline = config.baseline ?? {};
|
|
150
153
|
return {
|
|
151
154
|
enabled: baseline.enabled ?? true,
|
|
152
|
-
path: baseline.path ??
|
|
155
|
+
path: baseline.path ?? ".phasegate/baseline.json",
|
|
153
156
|
};
|
|
154
157
|
}
|
|
155
158
|
|
|
@@ -162,5 +165,5 @@ export class HarnessConfigConfigQueryAdapter implements ConfigQueryPort {
|
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
function normalizeProjectPath(path: string): string {
|
|
165
|
-
return path.replace(/\\/g,
|
|
168
|
+
return path.replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
166
169
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-304
|
|
4
|
+
|
|
5
|
+
import { createWorldModelModule, type WorldResolvedConfigInput } from "../../../world-model/index.js";
|
|
6
|
+
import type {
|
|
7
|
+
QueriedWorldObligationEntry,
|
|
8
|
+
WorldObligationsQueryPort,
|
|
9
|
+
WorldObligationsQueryResult,
|
|
10
|
+
} from "../../application/ports/world-obligations-query-port.js";
|
|
11
|
+
|
|
12
|
+
interface Options {
|
|
13
|
+
readonly rootDir: string;
|
|
14
|
+
readonly resolvedConfig?: WorldResolvedConfigInput;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class WorldModelOpenObligationsQueryAdapter implements WorldObligationsQueryPort {
|
|
18
|
+
constructor(private readonly options: Options) {}
|
|
19
|
+
|
|
20
|
+
async query(): Promise<WorldObligationsQueryResult> {
|
|
21
|
+
try {
|
|
22
|
+
const world = createWorldModelModule(this.options);
|
|
23
|
+
const derived = await world.deriveWorldObligationsUseCase.execute({ writeReport: false });
|
|
24
|
+
if (derived.status !== "derived") return { status: "unavailable" };
|
|
25
|
+
|
|
26
|
+
const report = derived.result.report;
|
|
27
|
+
const entries: QueriedWorldObligationEntry[] = [
|
|
28
|
+
...report.structuralObligations.map((entry) => ({
|
|
29
|
+
kind: "structural" as const,
|
|
30
|
+
classification: entry.classification,
|
|
31
|
+
ruleId: entry.ruleId,
|
|
32
|
+
constraintId: entry.constraintId,
|
|
33
|
+
violationFingerprint: entry.violationFingerprint,
|
|
34
|
+
})),
|
|
35
|
+
...report.repaidBaselineEntries.map((entry) => ({
|
|
36
|
+
kind: "cleanup-required" as const,
|
|
37
|
+
classification: "cleanup-required" as const,
|
|
38
|
+
ruleId: entry.ruleId,
|
|
39
|
+
constraintId: entry.constraintId,
|
|
40
|
+
violationFingerprint: entry.violationFingerprint,
|
|
41
|
+
})),
|
|
42
|
+
...report.policyDiagnostics.map((entry) => ({
|
|
43
|
+
kind: "policy-diagnostic" as const,
|
|
44
|
+
classification:
|
|
45
|
+
entry.code === "expired-waiver" ? ("expired-waiver" as const) : ("policy-diagnostic" as const),
|
|
46
|
+
ruleId: null,
|
|
47
|
+
constraintId: null,
|
|
48
|
+
violationFingerprint: null,
|
|
49
|
+
subjectId: entry.subjectId,
|
|
50
|
+
policyCode: entry.code,
|
|
51
|
+
})),
|
|
52
|
+
];
|
|
53
|
+
return { status: "available", entries: Object.freeze(entries) };
|
|
54
|
+
} catch {
|
|
55
|
+
return { status: "unavailable" };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit agent-integration
|
|
4
4
|
* @work-item-id WI-208
|
|
5
|
+
* @work-item-id WI-323
|
|
5
6
|
*
|
|
6
7
|
* PostToolUse Hook Adapter
|
|
7
8
|
* Claude Code の PostToolUse Hook エントリポイント
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import { ChildProcessCliExecutorAdapter } from
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { HandlePostToolUseUseCase } from "../application/usecases/handle-post-tool-use-usecase.js";
|
|
14
|
+
import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
|
|
15
|
+
import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
|
|
16
|
+
import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
|
|
17
|
+
import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
|
|
17
18
|
|
|
18
19
|
interface PostToolUseHookInput {
|
|
19
20
|
tool_name?: string;
|
|
@@ -25,15 +26,15 @@ async function readStdin(): Promise<string> {
|
|
|
25
26
|
for await (const chunk of process.stdin) {
|
|
26
27
|
chunks.push(chunk as Buffer);
|
|
27
28
|
}
|
|
28
|
-
return Buffer.concat(chunks).toString(
|
|
29
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
async function findConfigPath(): Promise<string> {
|
|
32
33
|
let dir = process.cwd();
|
|
33
34
|
while (true) {
|
|
34
35
|
const candidates = [
|
|
35
|
-
path.join(dir,
|
|
36
|
-
path.join(dir,
|
|
36
|
+
path.join(dir, "phasegate.config.json"),
|
|
37
|
+
path.join(dir, ".phasegate-local", "phasegate.config.json"),
|
|
37
38
|
];
|
|
38
39
|
for (const candidate of candidates) {
|
|
39
40
|
try {
|
|
@@ -45,12 +46,12 @@ async function findConfigPath(): Promise<string> {
|
|
|
45
46
|
if (parent === dir) break;
|
|
46
47
|
dir = parent;
|
|
47
48
|
}
|
|
48
|
-
return path.join(process.cwd(),
|
|
49
|
+
return path.join(process.cwd(), "phasegate.config.json");
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
function projectRootForConfig(configPath: string): string {
|
|
52
53
|
const configDir = path.dirname(configPath);
|
|
53
|
-
return path.basename(configDir) ===
|
|
54
|
+
return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
async function main(): Promise<void> {
|
|
@@ -58,7 +59,7 @@ async function main(): Promise<void> {
|
|
|
58
59
|
try {
|
|
59
60
|
raw = await readStdin();
|
|
60
61
|
} catch {
|
|
61
|
-
process.stderr.write(
|
|
62
|
+
process.stderr.write("stdin読み取りエラー\n");
|
|
62
63
|
process.exit(2);
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -72,8 +73,20 @@ async function main(): Promise<void> {
|
|
|
72
73
|
|
|
73
74
|
const toolName = input.tool_name;
|
|
74
75
|
if (!toolName) {
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
// WI-323: PostToolUse はツール実行後の lint フィードバックでありゲートではないため、
|
|
77
|
+
// tool_name 欠落は fail-open でスキップする(WI-314 / github#40 方針)。
|
|
78
|
+
// ※ pre-tool-use-hook の同ガードは書き込みゲートなので fail-closed (exit 2) を維持する。
|
|
79
|
+
const configPath = await findConfigPath();
|
|
80
|
+
await recordHookSkipEvent({
|
|
81
|
+
projectRoot: projectRootForConfig(configPath),
|
|
82
|
+
hookType: "post-tool-use",
|
|
83
|
+
reason: "TOOL_NAME_MISSING",
|
|
84
|
+
targetPaths: [],
|
|
85
|
+
});
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
"警告: stdin payload に tool_name が無いため post-tool-use hook の処理をスキップしました (fail-open)\n",
|
|
88
|
+
);
|
|
89
|
+
process.exit(0);
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
try {
|
|
@@ -93,7 +106,7 @@ async function main(): Promise<void> {
|
|
|
93
106
|
if (output.skipReason) {
|
|
94
107
|
await recordHookSkipEvent({
|
|
95
108
|
projectRoot: projectRootForConfig(configPath),
|
|
96
|
-
hookType:
|
|
109
|
+
hookType: "post-tool-use",
|
|
97
110
|
reason: output.skipReason,
|
|
98
111
|
targetPaths: [],
|
|
99
112
|
});
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
// @unit agent-integration
|
|
2
2
|
// @layer presentation
|
|
3
|
+
// @work-item-id WI-304
|
|
3
4
|
|
|
4
5
|
import { buildCiGovernance } from "../../ci-governance/composition-root.js";
|
|
6
|
+
import { createConfigFoundationModule, toWorldModelConfig } from "../../config-foundation/index.js";
|
|
7
|
+
import { GetOpenWorldObligationsContextUseCase } from "../application/usecases/get-open-world-obligations-context-usecase.js";
|
|
8
|
+
import { WorldModelOpenObligationsQueryAdapter } from "../infrastructure/adapters/world-model-open-obligations-query-adapter.js";
|
|
5
9
|
import {
|
|
6
10
|
buildIntegrityUnverifiableWarning,
|
|
7
11
|
buildIntegrityWarning,
|
|
8
12
|
buildSessionStartContext,
|
|
9
13
|
collectPhasegateStatus,
|
|
10
14
|
} from "./phasegate-status-context.js";
|
|
15
|
+
import { buildWorldObligationsSessionContext } from "./world-obligations-session-context.js";
|
|
11
16
|
|
|
12
17
|
async function readStdin(): Promise<string> {
|
|
13
18
|
const chunks: Buffer[] = [];
|
|
@@ -36,6 +41,36 @@ async function buildIntegrityContext(cwd: string): Promise<string | null> {
|
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
/**
|
|
45
|
+
* World の保存済み report は読まず、公開 facade の pure derive から現在の open obligation を要約する。
|
|
46
|
+
* config/derive の失敗は固定 warning へ縮退し、repository 由来のエラー文字列は中継しない。
|
|
47
|
+
*/
|
|
48
|
+
async function buildWorldContext(cwd: string): Promise<string | null> {
|
|
49
|
+
try {
|
|
50
|
+
const configModule = createConfigFoundationModule();
|
|
51
|
+
const resolved = await configModule.usecases.loadResolvedConfigUseCase.execute();
|
|
52
|
+
const worldConfig = toWorldModelConfig(resolved.config);
|
|
53
|
+
if (worldConfig === undefined) return null;
|
|
54
|
+
|
|
55
|
+
const contextUseCase = new GetOpenWorldObligationsContextUseCase({
|
|
56
|
+
worldObligationsQueryPort: new WorldModelOpenObligationsQueryAdapter({
|
|
57
|
+
rootDir: cwd,
|
|
58
|
+
resolvedConfig: worldConfig,
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const context = await contextUseCase.execute({
|
|
62
|
+
enabled: worldConfig.enabled && worldConfig.sessionStart.enabled,
|
|
63
|
+
});
|
|
64
|
+
return buildWorldObligationsSessionContext(context, {
|
|
65
|
+
maxItems: worldConfig.sessionStart.maxItems,
|
|
66
|
+
maxChars: worldConfig.sessionStart.maxChars,
|
|
67
|
+
});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof Error && error.name === "ConfigNotFoundError") return null;
|
|
70
|
+
return buildWorldObligationsSessionContext({ status: "unavailable" }, { maxItems: 5, maxChars: 2000 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
async function main(): Promise<void> {
|
|
40
75
|
try {
|
|
41
76
|
await readStdin();
|
|
@@ -46,7 +81,8 @@ async function main(): Promise<void> {
|
|
|
46
81
|
const status = await collectPhasegateStatus(process.cwd());
|
|
47
82
|
const baseContext = buildSessionStartContext(status);
|
|
48
83
|
const integrityContext = await buildIntegrityContext(process.cwd());
|
|
49
|
-
const
|
|
84
|
+
const worldContext = await buildWorldContext(process.cwd());
|
|
85
|
+
const additionalContext = [integrityContext, worldContext, baseContext].filter((item) => item !== null).join("\n\n");
|
|
50
86
|
|
|
51
87
|
const output = {
|
|
52
88
|
hookSpecificOutput: {
|
|
@@ -3,19 +3,20 @@
|
|
|
3
3
|
* @unit agent-integration
|
|
4
4
|
* @work-item-id WI-203
|
|
5
5
|
* @work-item-id WI-208
|
|
6
|
+
* @work-item-id WI-323
|
|
6
7
|
*
|
|
7
8
|
* Stop Hook Adapter
|
|
8
9
|
* Claude Code の Stop Hook エントリポイント
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { HandleStopUseCase } from "../application/usecases/handle-stop-usecase.js";
|
|
15
|
+
import { ChildProcessCliExecutorAdapter } from "../infrastructure/adapters/child-process-cli-executor-adapter.js";
|
|
16
|
+
import { EnvFileReentryGuardStateAdapter } from "../infrastructure/adapters/env-file-reentry-guard-state-adapter.js";
|
|
17
|
+
import { HarnessApiCliCommandRegistryAdapter } from "../infrastructure/adapters/harness-api-cli-command-registry-adapter.js";
|
|
18
|
+
import { HarnessConfigConfigQueryAdapter } from "../infrastructure/adapters/harness-config-config-query-adapter.js";
|
|
19
|
+
import { recordHookSkipEvent } from "./hook-skip-event-recorder.js";
|
|
19
20
|
|
|
20
21
|
interface StopHookInput {
|
|
21
22
|
session_id?: string;
|
|
@@ -23,8 +24,7 @@ interface StopHookInput {
|
|
|
23
24
|
|
|
24
25
|
function isCompleteCheckExecutionWiringFailure(stderr: string): boolean {
|
|
25
26
|
return (
|
|
26
|
-
/scripts\/harness\/cli\/complete-check\.ts/.test(stderr) ||
|
|
27
|
-
/ERR_MODULE_NOT_FOUND|Cannot find module/i.test(stderr)
|
|
27
|
+
/scripts\/harness\/cli\/complete-check\.ts/.test(stderr) || /ERR_MODULE_NOT_FOUND|Cannot find module/i.test(stderr)
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -40,15 +40,15 @@ async function readStdin(): Promise<string> {
|
|
|
40
40
|
for await (const chunk of process.stdin) {
|
|
41
41
|
chunks.push(chunk as Buffer);
|
|
42
42
|
}
|
|
43
|
-
return Buffer.concat(chunks).toString(
|
|
43
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function findConfigPath(): Promise<string> {
|
|
47
47
|
let dir = process.cwd();
|
|
48
48
|
while (true) {
|
|
49
49
|
const candidates = [
|
|
50
|
-
path.join(dir,
|
|
51
|
-
path.join(dir,
|
|
50
|
+
path.join(dir, "phasegate.config.json"),
|
|
51
|
+
path.join(dir, ".phasegate-local", "phasegate.config.json"),
|
|
52
52
|
];
|
|
53
53
|
for (const candidate of candidates) {
|
|
54
54
|
try {
|
|
@@ -60,12 +60,12 @@ async function findConfigPath(): Promise<string> {
|
|
|
60
60
|
if (parent === dir) break;
|
|
61
61
|
dir = parent;
|
|
62
62
|
}
|
|
63
|
-
return path.join(process.cwd(),
|
|
63
|
+
return path.join(process.cwd(), "phasegate.config.json");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function projectRootForConfig(configPath: string): string {
|
|
67
67
|
const configDir = path.dirname(configPath);
|
|
68
|
-
return path.basename(configDir) ===
|
|
68
|
+
return path.basename(configDir) === ".phasegate-local" ? path.dirname(configDir) : configDir;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async function main(): Promise<void> {
|
|
@@ -73,7 +73,7 @@ async function main(): Promise<void> {
|
|
|
73
73
|
try {
|
|
74
74
|
raw = await readStdin();
|
|
75
75
|
} catch {
|
|
76
|
-
process.stderr.write(
|
|
76
|
+
process.stderr.write("stdin読み取りエラー\n");
|
|
77
77
|
process.exit(2);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -87,13 +87,24 @@ async function main(): Promise<void> {
|
|
|
87
87
|
|
|
88
88
|
const sessionId = input.session_id;
|
|
89
89
|
if (!sessionId) {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
// WI-323: session_id 欠落は呼び出し側環境の不備であり、stop hook はゲートではないため
|
|
91
|
+
// fail-open でスキップする(WI-314 / github#40 の「hook は開発フローを止めない」方針)。
|
|
92
|
+
const configPath = await findConfigPath();
|
|
93
|
+
await recordHookSkipEvent({
|
|
94
|
+
projectRoot: projectRootForConfig(configPath),
|
|
95
|
+
hookType: "stop",
|
|
96
|
+
reason: "SESSION_ID_MISSING",
|
|
97
|
+
targetPaths: [],
|
|
98
|
+
});
|
|
99
|
+
process.stderr.write(
|
|
100
|
+
"警告: stdin payload に session_id が無いため stop hook の処理をスキップしました (fail-open)\n",
|
|
101
|
+
);
|
|
102
|
+
process.exit(0);
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
try {
|
|
95
106
|
const configPath = await findConfigPath();
|
|
96
|
-
const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy:
|
|
107
|
+
const reentryGuardStatePort = new EnvFileReentryGuardStateAdapter({ strategy: "env" });
|
|
97
108
|
const configQueryPort = new HarnessConfigConfigQueryAdapter(configPath);
|
|
98
109
|
const cliCommandRegistryPort = new HarnessApiCliCommandRegistryAdapter();
|
|
99
110
|
const cliExecutorPort = new ChildProcessCliExecutorAdapter();
|
|
@@ -107,14 +118,14 @@ async function main(): Promise<void> {
|
|
|
107
118
|
|
|
108
119
|
const output = await useCase.execute({ sessionId });
|
|
109
120
|
|
|
110
|
-
if (output.skipReason ===
|
|
121
|
+
if (output.skipReason === "REENTRY_DETECTED") {
|
|
111
122
|
await recordHookSkipEvent({
|
|
112
123
|
projectRoot: projectRootForConfig(configPath),
|
|
113
|
-
hookType:
|
|
124
|
+
hookType: "stop",
|
|
114
125
|
reason: output.skipReason,
|
|
115
126
|
targetPaths: [],
|
|
116
127
|
});
|
|
117
|
-
process.stderr.write(
|
|
128
|
+
process.stderr.write("ReentryGuard: 再入検出によりスキップ\n");
|
|
118
129
|
process.exit(0);
|
|
119
130
|
}
|
|
120
131
|
|
|
@@ -123,10 +134,8 @@ async function main(): Promise<void> {
|
|
|
123
134
|
// WI-087 finding #4: enforce=true なら exit 2 + decision JSON で turn block
|
|
124
135
|
if (output.shouldEnforceFailure === true) {
|
|
125
136
|
const reason = formatCompleteCheckFailureReason(output.cliResult.exitCode, output.cliResult.stderr);
|
|
126
|
-
process.stdout.write(`${JSON.stringify({ decision:
|
|
127
|
-
process.stderr.write(
|
|
128
|
-
`${reason} — strict mode により turn を block します\n`,
|
|
129
|
-
);
|
|
137
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason })}\n`);
|
|
138
|
+
process.stderr.write(`${reason} — strict mode により turn を block します\n`);
|
|
130
139
|
process.exit(2);
|
|
131
140
|
}
|
|
132
141
|
process.stderr.write(`Complete Check失敗 (exitCode=${output.cliResult.exitCode})\n`);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @unit agent-integration
|
|
2
|
+
// @layer presentation
|
|
3
|
+
// @work-item-id WI-304
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
OpenWorldObligationContextItemDto,
|
|
7
|
+
OpenWorldObligationsContextDto,
|
|
8
|
+
} from "../application/dto/open-world-obligations-context-dto.js";
|
|
9
|
+
|
|
10
|
+
const UNAVAILABLE_WARNING =
|
|
11
|
+
"⚠ World obligations unavailable at SessionStart; continuing fail-open. Run phasegate world:derive.";
|
|
12
|
+
const HEADER = "## World open obligations (SessionStart)";
|
|
13
|
+
const HARD_MAX_ITEMS = 5;
|
|
14
|
+
const HARD_MAX_CHARS = 2000;
|
|
15
|
+
|
|
16
|
+
export const countUnicodeScalars = (value: string): number => [...value].length;
|
|
17
|
+
|
|
18
|
+
const renderEntry = (entry: OpenWorldObligationContextItemDto): string => {
|
|
19
|
+
if (entry.kind === "policy-diagnostic") {
|
|
20
|
+
return `- [BLOCKING ${entry.classification}] subject=${entry.subjectId ?? "unknown"}`;
|
|
21
|
+
}
|
|
22
|
+
const label =
|
|
23
|
+
entry.classification === "cleanup-required"
|
|
24
|
+
? "CLEANUP"
|
|
25
|
+
: entry.classification === "waived"
|
|
26
|
+
? "WAIVED"
|
|
27
|
+
: `BLOCKING ${entry.classification}`;
|
|
28
|
+
return `- [${label}] ${entry.ruleId ?? "unknown-rule"} constraint=${entry.constraintId ?? "implicit"} fingerprint=${entry.violationFingerprint ?? "none"}`;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderAvailable = (
|
|
32
|
+
input: Extract<OpenWorldObligationsContextDto, { status: "available" }>,
|
|
33
|
+
shown: number,
|
|
34
|
+
): string => {
|
|
35
|
+
const lines = [HEADER, ...input.entries.slice(0, shown).map(renderEntry)];
|
|
36
|
+
if (input.adoptedLegacyCount > 0) lines.push(`- Adopted legacy: ${input.adoptedLegacyCount} (summary only)`);
|
|
37
|
+
if (input.entries.length === 0 && input.adoptedLegacyCount === 0) lines.push("- None.");
|
|
38
|
+
const omitted = input.entries.length - shown;
|
|
39
|
+
if (omitted > 0) lines.push(`... ${omitted} more; run phasegate world:derive`);
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function buildWorldObligationsSessionContext(
|
|
44
|
+
input: OpenWorldObligationsContextDto,
|
|
45
|
+
limits: { readonly maxItems: number; readonly maxChars: number },
|
|
46
|
+
): string | null {
|
|
47
|
+
const maxItems = Math.min(limits.maxItems, HARD_MAX_ITEMS);
|
|
48
|
+
const maxChars = Math.min(limits.maxChars, HARD_MAX_CHARS);
|
|
49
|
+
if (input.status === "disabled") return null;
|
|
50
|
+
if (input.status === "unavailable") {
|
|
51
|
+
return countUnicodeScalars(UNAVAILABLE_WARNING) <= maxChars ? UNAVAILABLE_WARNING : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const maximumShown = Math.min(input.entries.length, maxItems);
|
|
55
|
+
for (let shown = maximumShown; shown >= 0; shown -= 1) {
|
|
56
|
+
const candidate = renderAvailable(input, shown);
|
|
57
|
+
if (countUnicodeScalars(candidate) <= maxChars) return candidate;
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// @unit attestation
|
|
2
2
|
// @layer application
|
|
3
|
+
// @work-item-id WI-306
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* record format
|
|
6
|
+
* versioned record formatのplain object表現(logical_design §1.4.3 / WI-306)。
|
|
6
7
|
* ファイル入出力・stdout エコー・mapper の境界で用いる契約型。VO はプリミティブ展開する。
|
|
7
8
|
*/
|
|
8
9
|
export interface AttestationDocumentValidatorOutcome {
|
|
@@ -23,9 +24,7 @@ export interface AttestationDocumentGranularityClaim {
|
|
|
23
24
|
readonly knownLimitations: string[];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
readonly schemaVersion: "phasegate-attestation/v1";
|
|
28
|
-
readonly predicateType: string;
|
|
27
|
+
interface AttestationDocumentBase {
|
|
29
28
|
readonly subject: {
|
|
30
29
|
readonly command: string;
|
|
31
30
|
readonly gateResult: "pass" | "fail";
|
|
@@ -54,3 +53,17 @@ export interface AttestationDocument {
|
|
|
54
53
|
readonly value: string | null;
|
|
55
54
|
};
|
|
56
55
|
}
|
|
56
|
+
|
|
57
|
+
export interface AttestationDocumentV1 extends AttestationDocumentBase {
|
|
58
|
+
readonly schemaVersion: "phasegate-attestation/v1";
|
|
59
|
+
readonly predicateType: "https://phasegate.dev/attestation/gate-run/v1";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AttestationDocumentV2 extends AttestationDocumentBase {
|
|
63
|
+
readonly schemaVersion: "phasegate-attestation/v2";
|
|
64
|
+
readonly predicateType: "https://phasegate.dev/attestation/gate-run/v2";
|
|
65
|
+
/** 実行時のWorld canonical corpusRoot。個別fragment digestは保持しない。 */
|
|
66
|
+
readonly worldSnapshotRoot: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type AttestationDocument = AttestationDocumentV1 | AttestationDocumentV2;
|