@principles/host-runtime 0.12.0 → 0.12.2
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/dist/codex-artificer-trusted-facts.d.ts +7 -0
- package/dist/codex-artificer-trusted-facts.js +17 -0
- package/dist/codex-rule-context-provider.js +82 -13
- package/dist/design-gate-facts.d.ts +37 -0
- package/dist/design-gate-facts.js +249 -0
- package/dist/governance-signal-admission.d.ts +16 -2
- package/dist/governance-signal-admission.js +27 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/production-rulehost-gate.js +37 -1
- package/dist/workspace-behavior-evidence.d.ts +9 -0
- package/dist/workspace-behavior-evidence.js +85 -0
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ArtificerTrustedFactsPort } from '@principles/core/runtime-v2';
|
|
2
|
+
/**
|
|
3
|
+
* Codex generation can author rules against the same DesignGateFact schema
|
|
4
|
+
* that the native PreToolUse provider supplies. This declares capability only;
|
|
5
|
+
* it never resolves the current generated PI task as approved.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createCodexArtificerTrustedFactsPort(): ArtificerTrustedFactsPort;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Codex generation can author rules against the same DesignGateFact schema
|
|
4
|
+
* that the native PreToolUse provider supplies. This declares capability only;
|
|
5
|
+
* it never resolves the current generated PI task as approved.
|
|
6
|
+
*/
|
|
7
|
+
export function createCodexArtificerTrustedFactsPort() {
|
|
8
|
+
return {
|
|
9
|
+
resolveRequiredFact: ({ factKind }) => ({
|
|
10
|
+
resolution: 'unknown',
|
|
11
|
+
detail: factKind === 'design_approval'
|
|
12
|
+
? 'Generation verifies host fact capability; current task approval is resolved only at native PreToolUse.'
|
|
13
|
+
: `Codex has no trusted source for required fact '${factKind}'.`,
|
|
14
|
+
}),
|
|
15
|
+
describeRequiredFact: describeCodexRuleContextFact,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -2,19 +2,57 @@ import path from 'node:path';
|
|
|
2
2
|
import { computeBehaviorFacts, } from '@principles/core/runtime-v2';
|
|
3
3
|
import { assembleBehaviorHistory } from './behavior-history.js';
|
|
4
4
|
import { readWorkspaceRuleContextRowsBefore } from './workspace-behavior-evidence.js';
|
|
5
|
+
import { readLatestCodexDesignGateDeclaration, resolveCodexDesignGateFact } from './design-gate-facts.js';
|
|
5
6
|
const HISTORY_LIMIT = 100;
|
|
6
7
|
const MAX_REASON_LENGTH = 240;
|
|
8
|
+
const MAX_PATCH_BYTES = 1024 * 1024;
|
|
9
|
+
const APPROVED_READONLY_COMMANDS = new Set([
|
|
10
|
+
'git status',
|
|
11
|
+
'git status --short',
|
|
12
|
+
'git diff --stat',
|
|
13
|
+
'git log -1 --oneline',
|
|
14
|
+
]);
|
|
7
15
|
function isRecord(value) {
|
|
8
16
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
17
|
}
|
|
10
|
-
function
|
|
18
|
+
function patchTargets(params, projectDir) {
|
|
19
|
+
if (params === null)
|
|
20
|
+
return undefined;
|
|
21
|
+
const patch = typeof params.patch === 'string'
|
|
22
|
+
? params.patch
|
|
23
|
+
: typeof params.input === 'string' ? params.input : null;
|
|
24
|
+
if (patch === null || Buffer.byteLength(patch, 'utf8') > MAX_PATCH_BYTES)
|
|
25
|
+
return undefined;
|
|
26
|
+
const targets = [];
|
|
27
|
+
for (const line of patch.split(/\r?\n/)) {
|
|
28
|
+
const match = /^\*\*\* (?:Add|Update|Delete) File: (.+)$|^\*\*\* Move (?:from|to): (.+)$/.exec(line);
|
|
29
|
+
if (!match)
|
|
30
|
+
continue;
|
|
31
|
+
const value = (match[1] ?? match[2] ?? '').trim();
|
|
32
|
+
if (value.length === 0 || value.includes('\0'))
|
|
33
|
+
return undefined;
|
|
34
|
+
targets.push(path.resolve(projectDir, value));
|
|
35
|
+
}
|
|
36
|
+
return targets.length > 0 ? targets : undefined;
|
|
37
|
+
}
|
|
38
|
+
function safeReadonlyCommand(params) {
|
|
39
|
+
if (params === null)
|
|
40
|
+
return false;
|
|
41
|
+
const command = typeof params.command === 'string' ? params.command : null;
|
|
42
|
+
return command !== null && APPROVED_READONLY_COMMANDS.has(command.trim());
|
|
43
|
+
}
|
|
44
|
+
function unavailable(reason, designGate) {
|
|
11
45
|
const history = {
|
|
12
46
|
status: 'unavailable',
|
|
13
47
|
unavailableReason: reason.replace(/\s+/g, ' ').slice(0, MAX_REASON_LENGTH),
|
|
14
48
|
truncated: false,
|
|
15
49
|
calls: [],
|
|
16
50
|
};
|
|
17
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
version: 2,
|
|
53
|
+
history,
|
|
54
|
+
facts: { ...computeBehaviorFacts(history, null, null), ...(designGate !== undefined ? { designGate } : {}) },
|
|
55
|
+
};
|
|
18
56
|
}
|
|
19
57
|
/**
|
|
20
58
|
* Read a bounded same-session trajectory window for Codex PreToolUse events.
|
|
@@ -27,24 +65,55 @@ export function createCodexRuleContextProvider(options) {
|
|
|
27
65
|
const workspaceDir = path.resolve(options.workspaceDir);
|
|
28
66
|
const projectDir = path.resolve(options.projectDir);
|
|
29
67
|
return (request) => {
|
|
68
|
+
const canonicalKind = options.toolSemantics.resolve(request.toolName);
|
|
69
|
+
const targetKind = canonicalKind === 'agent' ? 'other' : canonicalKind;
|
|
70
|
+
const raw = request.rawPayload;
|
|
71
|
+
const rawParams = isRecord(raw) && isRecord(raw.toolInput) && isRecord(raw.toolInput.params)
|
|
72
|
+
? raw.toolInput.params
|
|
73
|
+
: null;
|
|
74
|
+
const readonlyExecute = targetKind === 'execute' && safeReadonlyCommand(rawParams);
|
|
75
|
+
// Fail fast before the fact resolution below: a non-string or empty
|
|
76
|
+
// sessionId cannot bind a declaration (an empty id resolves the fact to
|
|
77
|
+
// unknown) and would otherwise throw inside resolveCodexDesignGateFact,
|
|
78
|
+
// surfacing as a provider_failed fault instead of the precise reason.
|
|
79
|
+
// The enabled and workspace-scope checks deliberately stay AFTER
|
|
80
|
+
// resolution: the design-gate fact is independent of the
|
|
81
|
+
// rulecode_context_v2 capability (it reads the governance declaration
|
|
82
|
+
// store directly), so those degraded paths must still carry the resolved
|
|
83
|
+
// fact for enforcement (SPEC-RULECODE-CODEX-LOCAL-CLOSURE-001 §6).
|
|
84
|
+
if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0) {
|
|
85
|
+
return unavailable('session_id_invalid');
|
|
86
|
+
}
|
|
87
|
+
const designGate = resolveCodexDesignGateFact({
|
|
88
|
+
governanceWorkspacePath: workspaceDir,
|
|
89
|
+
projectWorkspacePath: projectDir,
|
|
90
|
+
hostSessionId: request.sessionId,
|
|
91
|
+
targetPath: targetKind === 'execute' ? projectDir : request.targetPath,
|
|
92
|
+
...(targetKind === 'write' && request.toolName === 'apply_patch'
|
|
93
|
+
? { targetPaths: patchTargets(rawParams, projectDir) ?? [] }
|
|
94
|
+
: {}),
|
|
95
|
+
...(targetKind === 'execute' ? { projectScopedExecution: true } : {}),
|
|
96
|
+
toolKind: readonlyExecute ? 'read' : targetKind,
|
|
97
|
+
}, {
|
|
98
|
+
getLatestDeclaration(projectWorkspacePath, hostSessionId) {
|
|
99
|
+
return readLatestCodexDesignGateDeclaration(workspaceDir, projectWorkspacePath, hostSessionId);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
30
102
|
if (!options.enabled)
|
|
31
|
-
return unavailable('codex_rule_context_disabled');
|
|
103
|
+
return unavailable('codex_rule_context_disabled', designGate);
|
|
32
104
|
if (path.resolve(request.workspaceDir) !== workspaceDir)
|
|
33
|
-
return unavailable('workspace_scope_mismatch');
|
|
34
|
-
if (typeof request.sessionId !== 'string' || request.sessionId.trim().length === 0)
|
|
35
|
-
return unavailable('session_id_invalid');
|
|
36
|
-
const raw = request.rawPayload;
|
|
105
|
+
return unavailable('workspace_scope_mismatch', designGate);
|
|
37
106
|
const toolInput = isRecord(raw) && isRecord(raw.toolInput) ? raw.toolInput : null;
|
|
38
107
|
if (toolInput === null
|
|
39
108
|
|| toolInput.toolName !== request.toolName
|
|
40
109
|
|| !isRecord(toolInput.params)
|
|
41
110
|
|| !path.isAbsolute(projectDir)
|
|
42
111
|
|| options.currentToolUseId.trim().length === 0) {
|
|
43
|
-
return unavailable('codex_pre_tool_scope_mismatch');
|
|
112
|
+
return unavailable('codex_pre_tool_scope_mismatch', designGate);
|
|
44
113
|
}
|
|
45
114
|
const cutoff = options.cutoffAt;
|
|
46
115
|
if (!(cutoff instanceof Date) || !Number.isFinite(cutoff.getTime()))
|
|
47
|
-
return unavailable('history_cutoff_invalid');
|
|
116
|
+
return unavailable('history_cutoff_invalid', designGate);
|
|
48
117
|
try {
|
|
49
118
|
const historyRows = readWorkspaceRuleContextRowsBefore(workspaceDir, {
|
|
50
119
|
sessionId: request.sessionId,
|
|
@@ -54,9 +123,9 @@ export function createCodexRuleContextProvider(options) {
|
|
|
54
123
|
limit: HISTORY_LIMIT,
|
|
55
124
|
});
|
|
56
125
|
if (historyRows.unscopedPriorRows)
|
|
57
|
-
return unavailable('trajectory_project_scope_incomplete');
|
|
126
|
+
return unavailable('trajectory_project_scope_incomplete', designGate);
|
|
58
127
|
if (historyRows.rows.length === 0)
|
|
59
|
-
return unavailable('no_prior_session_history');
|
|
128
|
+
return unavailable('no_prior_session_history', designGate);
|
|
60
129
|
const history = assembleBehaviorHistory(historyRows.rows, historyRows.truncated, {
|
|
61
130
|
projectDir,
|
|
62
131
|
toolSemantics: options.toolSemantics,
|
|
@@ -79,11 +148,11 @@ export function createCodexRuleContextProvider(options) {
|
|
|
79
148
|
return {
|
|
80
149
|
version: 2,
|
|
81
150
|
history,
|
|
82
|
-
facts,
|
|
151
|
+
facts: { ...facts, designGate },
|
|
83
152
|
};
|
|
84
153
|
}
|
|
85
154
|
catch (error) {
|
|
86
|
-
return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}
|
|
155
|
+
return unavailable(`trajectory_history_unavailable:${error instanceof Error ? error.message : String(error)}`, designGate);
|
|
87
156
|
}
|
|
88
157
|
};
|
|
89
158
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type DesignGateFact } from '@principles/core/runtime-v2';
|
|
2
|
+
export declare const DESIGN_GATE_FACT_KIND: 'design_approval';
|
|
3
|
+
export declare const DESIGN_GATE_FACT_INPUT_PATH: 'input.context.facts.designGate';
|
|
4
|
+
export declare const CODEX_DESIGN_GATE_SOURCE: 'codex-native-pretool-design-gate-v1';
|
|
5
|
+
export interface DesignGateSpecSnapshot {
|
|
6
|
+
realPath: string;
|
|
7
|
+
digest: string;
|
|
8
|
+
byteLength: number;
|
|
9
|
+
}
|
|
10
|
+
export interface CodexDesignGateRequest {
|
|
11
|
+
governanceWorkspacePath: string;
|
|
12
|
+
projectWorkspacePath: string;
|
|
13
|
+
hostSessionId: string;
|
|
14
|
+
targetPath: string;
|
|
15
|
+
/** All paths named by a bounded native patch payload, when available. */
|
|
16
|
+
targetPaths?: readonly string[];
|
|
17
|
+
/** True only when the Owner explicitly scoped execute invocations to the project root. */
|
|
18
|
+
projectScopedExecution?: boolean;
|
|
19
|
+
toolKind: 'read' | 'search' | 'write' | 'execute' | 'other';
|
|
20
|
+
}
|
|
21
|
+
export interface DesignGateDeclarationReader {
|
|
22
|
+
getLatestDeclaration(projectWorkspacePath: string, hostSessionId: string): unknown;
|
|
23
|
+
}
|
|
24
|
+
/** Read and hash the current SPEC after realpath confinement on every call. */
|
|
25
|
+
export declare function readDesignGateSpecSnapshot(projectWorkspacePath: string, specPath: string): DesignGateSpecSnapshot;
|
|
26
|
+
/** Resolve a live Codex event against the latest Owner declaration, without caching or writes. */
|
|
27
|
+
export declare function resolveCodexDesignGateFact(request: CodexDesignGateRequest, declarationReader: DesignGateDeclarationReader): DesignGateFact;
|
|
28
|
+
/** Read the current declaration from the governance workspace's existing state.db. */
|
|
29
|
+
export declare function readLatestCodexDesignGateDeclaration(governanceWorkspacePath: string, projectWorkspacePath: string, hostSessionId: string): unknown;
|
|
30
|
+
export declare function describeCodexRuleContextFact(input: {
|
|
31
|
+
factKind: string;
|
|
32
|
+
}): {
|
|
33
|
+
supported: boolean;
|
|
34
|
+
schemaVersion?: string;
|
|
35
|
+
inputPath?: string;
|
|
36
|
+
source?: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DESIGN_GATE_FACT_SCHEMA_VERSION, SqliteConnection, SqliteDesignGateStore, isDesignGateDeclaration, } from '@principles/core/runtime-v2';
|
|
5
|
+
export const DESIGN_GATE_FACT_KIND = 'design_approval';
|
|
6
|
+
export const DESIGN_GATE_FACT_INPUT_PATH = 'input.context.facts.designGate';
|
|
7
|
+
export const CODEX_DESIGN_GATE_SOURCE = 'codex-native-pretool-design-gate-v1';
|
|
8
|
+
const MAX_SPEC_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
function inside(parent, child) {
|
|
10
|
+
const relative = path.relative(parent, child);
|
|
11
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
12
|
+
}
|
|
13
|
+
function realPathForTarget(candidate) {
|
|
14
|
+
if (!path.isAbsolute(candidate))
|
|
15
|
+
throw new Error('target_path_not_absolute');
|
|
16
|
+
let current = path.resolve(candidate);
|
|
17
|
+
const suffix = [];
|
|
18
|
+
while (true) {
|
|
19
|
+
try {
|
|
20
|
+
const realCurrent = fs.realpathSync.native(current);
|
|
21
|
+
return path.join(realCurrent, ...suffix.reverse());
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
const parent = path.dirname(current);
|
|
25
|
+
if (parent === current)
|
|
26
|
+
throw new Error('target_path_unresolvable');
|
|
27
|
+
suffix.push(path.basename(current));
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Read and hash the current SPEC after realpath confinement on every call. */
|
|
33
|
+
export function readDesignGateSpecSnapshot(projectWorkspacePath, specPath) {
|
|
34
|
+
if (!path.isAbsolute(projectWorkspacePath) || !path.isAbsolute(specPath))
|
|
35
|
+
throw new Error('spec_path_invalid');
|
|
36
|
+
const projectRoot = fs.realpathSync.native(projectWorkspacePath);
|
|
37
|
+
const realPath = fs.realpathSync.native(specPath);
|
|
38
|
+
if (!inside(projectRoot, realPath))
|
|
39
|
+
throw new Error('spec_path_invalid');
|
|
40
|
+
// Open once and validate the descriptor: the checked and the read bytes are
|
|
41
|
+
// guaranteed to be the same file (no path-recheck race between stat and read).
|
|
42
|
+
const fd = fs.openSync(realPath, 'r');
|
|
43
|
+
try {
|
|
44
|
+
const stat = fs.fstatSync(fd);
|
|
45
|
+
if (!stat.isFile() || stat.size > MAX_SPEC_BYTES)
|
|
46
|
+
throw new Error('spec_path_invalid');
|
|
47
|
+
const bytes = fs.readFileSync(fd);
|
|
48
|
+
if (bytes.byteLength > MAX_SPEC_BYTES)
|
|
49
|
+
throw new Error('spec_path_invalid');
|
|
50
|
+
return {
|
|
51
|
+
realPath,
|
|
52
|
+
digest: createHash('sha256').update(bytes).digest('hex'),
|
|
53
|
+
byteLength: bytes.byteLength,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function scopeReference(paths, actions) {
|
|
61
|
+
const canonical = JSON.stringify({
|
|
62
|
+
paths: [...paths].sort((left, right) => left.localeCompare(right)),
|
|
63
|
+
actions: [...actions].sort((left, right) => left.localeCompare(right)),
|
|
64
|
+
});
|
|
65
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
66
|
+
}
|
|
67
|
+
function unknownFact(taskRef, reason, requiresApproval = true) {
|
|
68
|
+
return {
|
|
69
|
+
version: 1,
|
|
70
|
+
sourceRef: null,
|
|
71
|
+
revision: null,
|
|
72
|
+
requiresApproval,
|
|
73
|
+
taskRef,
|
|
74
|
+
specDigest: null,
|
|
75
|
+
scopeRef: null,
|
|
76
|
+
decisionTaskRef: null,
|
|
77
|
+
decisionSpecDigest: null,
|
|
78
|
+
decisionScopeRef: null,
|
|
79
|
+
resolution: 'unknown',
|
|
80
|
+
revoked: false,
|
|
81
|
+
invalidationReason: reason,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function targetIsInDeclaredScope(projectPath, targetPath, scopePaths) {
|
|
85
|
+
try {
|
|
86
|
+
const target = realPathForTarget(targetPath);
|
|
87
|
+
const projectRoot = fs.realpathSync.native(projectPath);
|
|
88
|
+
if (!inside(projectRoot, target))
|
|
89
|
+
return null;
|
|
90
|
+
for (const declaredPath of scopePaths) {
|
|
91
|
+
const resolved = path.isAbsolute(declaredPath) ? declaredPath : path.resolve(projectRoot, declaredPath);
|
|
92
|
+
const scopePath = realPathForTarget(resolved);
|
|
93
|
+
if (!inside(projectRoot, scopePath))
|
|
94
|
+
continue;
|
|
95
|
+
let directory = false;
|
|
96
|
+
try {
|
|
97
|
+
directory = fs.statSync(scopePath).isDirectory();
|
|
98
|
+
}
|
|
99
|
+
catch { /* a new file may be an exact scope */ }
|
|
100
|
+
if (target === scopePath || (directory && inside(scopePath, target)))
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function everyTargetIsInDeclaredScope(projectPath, targetPaths, scopePaths) {
|
|
110
|
+
if (targetPaths.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
for (const targetPath of targetPaths) {
|
|
113
|
+
if (!path.isAbsolute(targetPath))
|
|
114
|
+
return null;
|
|
115
|
+
const matches = targetIsInDeclaredScope(projectPath, targetPath, scopePaths);
|
|
116
|
+
if (matches !== true)
|
|
117
|
+
return matches;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/** Resolve a live Codex event against the latest Owner declaration, without caching or writes. */
|
|
122
|
+
export function resolveCodexDesignGateFact(request, declarationReader) {
|
|
123
|
+
const taskRef = request.hostSessionId.trim().length > 0 ? `codex:${request.hostSessionId}` : null;
|
|
124
|
+
if (taskRef === null)
|
|
125
|
+
return unknownFact(null, 'host_identity_unavailable');
|
|
126
|
+
if (!path.isAbsolute(request.governanceWorkspacePath) || !path.isAbsolute(request.projectWorkspacePath)) {
|
|
127
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
128
|
+
}
|
|
129
|
+
// Codex's declared read/search tool semantics are sufficient evidence that
|
|
130
|
+
// the current action is investigative and does not require design approval.
|
|
131
|
+
if (request.toolKind === 'read' || request.toolKind === 'search') {
|
|
132
|
+
return {
|
|
133
|
+
version: 1,
|
|
134
|
+
sourceRef: null,
|
|
135
|
+
revision: null,
|
|
136
|
+
requiresApproval: false,
|
|
137
|
+
taskRef,
|
|
138
|
+
specDigest: null,
|
|
139
|
+
scopeRef: null,
|
|
140
|
+
decisionTaskRef: null,
|
|
141
|
+
decisionSpecDigest: null,
|
|
142
|
+
decisionScopeRef: null,
|
|
143
|
+
resolution: 'not_required',
|
|
144
|
+
revoked: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (request.toolKind !== 'write' && request.toolKind !== 'execute') {
|
|
148
|
+
return unknownFact(taskRef, 'action_scope_unknown');
|
|
149
|
+
}
|
|
150
|
+
// An execute declaration scoped to `.` governs invocations from this native
|
|
151
|
+
// Codex project/session. It does not claim to confine the process effects.
|
|
152
|
+
let projectRoot;
|
|
153
|
+
let governanceRoot;
|
|
154
|
+
try {
|
|
155
|
+
projectRoot = fs.realpathSync.native(request.projectWorkspacePath);
|
|
156
|
+
governanceRoot = fs.realpathSync.native(request.governanceWorkspacePath);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
160
|
+
}
|
|
161
|
+
let declarationValue;
|
|
162
|
+
try {
|
|
163
|
+
declarationValue = declarationReader.getLatestDeclaration(projectRoot, request.hostSessionId);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return unknownFact(taskRef, 'declaration_unavailable');
|
|
167
|
+
}
|
|
168
|
+
if (declarationValue === null)
|
|
169
|
+
return unknownFact(taskRef, 'unregistered_scope');
|
|
170
|
+
if (!isDesignGateDeclaration(declarationValue))
|
|
171
|
+
return unknownFact(taskRef, 'declaration_invalid');
|
|
172
|
+
const declaration = declarationValue;
|
|
173
|
+
try {
|
|
174
|
+
if (fs.realpathSync.native(declaration.projectWorkspacePath) !== projectRoot
|
|
175
|
+
|| fs.realpathSync.native(declaration.governanceWorkspacePath) !== governanceRoot
|
|
176
|
+
|| declaration.hostSessionId !== request.hostSessionId
|
|
177
|
+
|| declaration.taskRef !== taskRef) {
|
|
178
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return unknownFact(taskRef, 'project_scope_mismatch');
|
|
183
|
+
}
|
|
184
|
+
const action = request.toolKind;
|
|
185
|
+
if (!declaration.scope.actions.includes(action))
|
|
186
|
+
return unknownFact(taskRef, 'action_scope_unknown');
|
|
187
|
+
const projectScopedExecution = request.toolKind === 'execute'
|
|
188
|
+
&& request.projectScopedExecution === true
|
|
189
|
+
&& declaration.scope.paths.includes('.');
|
|
190
|
+
const targetScope = projectScopedExecution
|
|
191
|
+
? true
|
|
192
|
+
: request.targetPaths !== undefined
|
|
193
|
+
? everyTargetIsInDeclaredScope(projectRoot, request.targetPaths, declaration.scope.paths)
|
|
194
|
+
: request.toolKind === 'execute'
|
|
195
|
+
? null
|
|
196
|
+
: targetIsInDeclaredScope(projectRoot, request.targetPath, declaration.scope.paths);
|
|
197
|
+
if (targetScope !== true)
|
|
198
|
+
return unknownFact(taskRef, 'target_scope_unknown');
|
|
199
|
+
let snapshot;
|
|
200
|
+
try {
|
|
201
|
+
snapshot = readDesignGateSpecSnapshot(projectRoot, declaration.specPath);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return unknownFact(taskRef, 'spec_path_invalid');
|
|
205
|
+
}
|
|
206
|
+
if (snapshot.digest !== declaration.specDigest)
|
|
207
|
+
return unknownFact(taskRef, 'spec_digest_mismatch');
|
|
208
|
+
const currentScopeRef = scopeReference(declaration.scope.paths, declaration.scope.actions);
|
|
209
|
+
const resolution = declaration.revoked ? 'not_approved' : declaration.decision;
|
|
210
|
+
return {
|
|
211
|
+
version: 1,
|
|
212
|
+
sourceRef: declaration.sourceRef,
|
|
213
|
+
revision: declaration.revision,
|
|
214
|
+
requiresApproval: declaration.revoked || declaration.decision !== 'not_required',
|
|
215
|
+
taskRef,
|
|
216
|
+
specDigest: snapshot.digest,
|
|
217
|
+
scopeRef: currentScopeRef,
|
|
218
|
+
decisionTaskRef: declaration.taskRef,
|
|
219
|
+
decisionSpecDigest: declaration.specDigest,
|
|
220
|
+
decisionScopeRef: currentScopeRef,
|
|
221
|
+
resolution,
|
|
222
|
+
revoked: declaration.revoked,
|
|
223
|
+
...(declaration.revoked ? { invalidationReason: 'declaration_revoked' } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Read the current declaration from the governance workspace's existing state.db. */
|
|
227
|
+
export function readLatestCodexDesignGateDeclaration(governanceWorkspacePath, projectWorkspacePath, hostSessionId) {
|
|
228
|
+
const connection = new SqliteConnection({
|
|
229
|
+
workspaceDir: governanceWorkspacePath,
|
|
230
|
+
readonly: true,
|
|
231
|
+
bootstrapIfMissing: false,
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
return new SqliteDesignGateStore(connection).getLatestDeclaration(projectWorkspacePath, hostSessionId);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
connection.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function describeCodexRuleContextFact(input) {
|
|
241
|
+
if (input.factKind !== DESIGN_GATE_FACT_KIND)
|
|
242
|
+
return { supported: false };
|
|
243
|
+
return {
|
|
244
|
+
supported: DESIGN_GATE_FACT_SCHEMA_VERSION === '1',
|
|
245
|
+
schemaVersion: DESIGN_GATE_FACT_SCHEMA_VERSION,
|
|
246
|
+
inputPath: DESIGN_GATE_FACT_INPUT_PATH,
|
|
247
|
+
source: CODEX_DESIGN_GATE_SOURCE,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
@@ -146,8 +146,22 @@ export type AdmitGovernanceSignalsResult = {
|
|
|
146
146
|
} | Degradation;
|
|
147
147
|
/** Public generic intake stays Codex + explicit WorkBuddy only. */
|
|
148
148
|
export declare function admitGovernanceSignals(input: AdmitGovernanceSignalsInput): AdmitGovernanceSignalsResult;
|
|
149
|
-
/**
|
|
150
|
-
|
|
149
|
+
/**
|
|
150
|
+
* AUDIT-P3-01 (2026-10-21): WorkBuddy live-keyword 检测接入共享 live 词库
|
|
151
|
+
* (`<workspace>/.state/correction_keywords.json`,seed ∪ learned,mtime 刷新,
|
|
152
|
+
* 文件缺失/损坏降级 seed-only)。返回的是完整共享 store——"只允许 high 精度
|
|
153
|
+
* correction 词触发自动 Pain"由 assess 的 matched 判据(isSignal && STRONG &&
|
|
154
|
+
* correction && matchedPrecision==='high')与 admitOne 的 isStrongHighPrecision
|
|
155
|
+
* 门共同强制,与 OpenClaw 检测共享同一套 precision 语义(earned-high 需
|
|
156
|
+
* TP≥3 且零 FP;owner_promoted/seed 需权重≥0.7)。
|
|
157
|
+
*/
|
|
158
|
+
export declare function resolveSharedCorrectionKeywordStoreForDetection(workspaceDir: string): UnifiedKeywordStore;
|
|
159
|
+
/**
|
|
160
|
+
* Pure check: no DB, no classifier, no historical transcript. The optional
|
|
161
|
+
* store parameter is how the shared live vocabulary enters (AUDIT-P3-01);
|
|
162
|
+
* omitted, the check keeps the P0 four-term projection (backward compatible).
|
|
163
|
+
*/
|
|
164
|
+
export declare function assessWorkbuddyLiveKeyword(text: string, sessionId: string, store?: UnifiedKeywordStore): {
|
|
151
165
|
matched: boolean;
|
|
152
166
|
reason: 'matched' | 'not_a_signal' | 'prompt_too_large';
|
|
153
167
|
};
|
|
@@ -726,17 +726,33 @@ function workbuddyP0KeywordStore() {
|
|
|
726
726
|
}
|
|
727
727
|
return { version: seed.version, terms };
|
|
728
728
|
}
|
|
729
|
-
/**
|
|
730
|
-
|
|
729
|
+
/**
|
|
730
|
+
* AUDIT-P3-01 (2026-10-21): WorkBuddy live-keyword 检测接入共享 live 词库
|
|
731
|
+
* (`<workspace>/.state/correction_keywords.json`,seed ∪ learned,mtime 刷新,
|
|
732
|
+
* 文件缺失/损坏降级 seed-only)。返回的是完整共享 store——"只允许 high 精度
|
|
733
|
+
* correction 词触发自动 Pain"由 assess 的 matched 判据(isSignal && STRONG &&
|
|
734
|
+
* correction && matchedPrecision==='high')与 admitOne 的 isStrongHighPrecision
|
|
735
|
+
* 门共同强制,与 OpenClaw 检测共享同一套 precision 语义(earned-high 需
|
|
736
|
+
* TP≥3 且零 FP;owner_promoted/seed 需权重≥0.7)。
|
|
737
|
+
*/
|
|
738
|
+
export function resolveSharedCorrectionKeywordStoreForDetection(workspaceDir) {
|
|
739
|
+
return createSharedCorrectionKeywordStore({ workspaceDir }).resolve();
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Pure check: no DB, no classifier, no historical transcript. The optional
|
|
743
|
+
* store parameter is how the shared live vocabulary enters (AUDIT-P3-01);
|
|
744
|
+
* omitted, the check keeps the P0 four-term projection (backward compatible).
|
|
745
|
+
*/
|
|
746
|
+
export function assessWorkbuddyLiveKeyword(text, sessionId, store) {
|
|
731
747
|
if (Buffer.byteLength(text, 'utf8') > WORKBUDDY_MAX_PROMPT_BYTES) {
|
|
732
748
|
return { matched: false, reason: 'prompt_too_large' };
|
|
733
749
|
}
|
|
734
|
-
const
|
|
750
|
+
const resolved = store ?? workbuddyP0KeywordStore();
|
|
735
751
|
// Prefer precision to recall: reject quoted/embedded/reported phrases.
|
|
736
|
-
if (!Object.keys(
|
|
752
|
+
if (!Object.keys(resolved.terms).some(term => text.trimStart().toLowerCase().startsWith(term.toLowerCase()))) {
|
|
737
753
|
return { matched: false, reason: 'not_a_signal' };
|
|
738
754
|
}
|
|
739
|
-
const out = collectSync(text, sessionId,
|
|
755
|
+
const out = collectSync(text, sessionId, resolved, SYNC_DETECTION_CONFIG, new Date().toISOString());
|
|
740
756
|
const matched = out.isSignal && out.strength === 'STRONG' &&
|
|
741
757
|
out.type === 'correction' && out.matchedPrecision === 'high';
|
|
742
758
|
return { matched, reason: matched ? 'matched' : 'not_a_signal' };
|
|
@@ -769,7 +785,11 @@ export function admitTrustedWorkbuddyLiveCorrection(input) {
|
|
|
769
785
|
if (prompt.trimStart().startsWith('PD记录痛苦')) {
|
|
770
786
|
return { ok: false, reason: 'explicit_owner_statement_reserved', nextAction: 'Use original Owner intake; no keyword fallback.' };
|
|
771
787
|
}
|
|
772
|
-
|
|
788
|
+
// AUDIT-P3-01: 预检与准入共用同一份共享 live 词库(单一真理源)——预检命中
|
|
789
|
+
// 词与 admitOne 的检测判定不会漂移。自动 Pain 仍只允许 high 精度 correction
|
|
790
|
+
// 词(两道门各自强制),无 LLM、无 transcript 读取。
|
|
791
|
+
const sharedStore = resolveSharedCorrectionKeywordStoreForDetection(input.workspaceDir);
|
|
792
|
+
if (!assessWorkbuddyLiveKeyword(prompt, sessionId, sharedStore).matched) {
|
|
773
793
|
return { ok: true, outcomes: [] };
|
|
774
794
|
}
|
|
775
795
|
const nativeIdentity = JSON.stringify(['workbuddy', sessionId, generationId]);
|
|
@@ -785,7 +805,7 @@ export function admitTrustedWorkbuddyLiveCorrection(input) {
|
|
|
785
805
|
text: prompt,
|
|
786
806
|
observedAt: (input.now ?? new Date()).toISOString(),
|
|
787
807
|
}],
|
|
788
|
-
keywordStore:
|
|
808
|
+
keywordStore: sharedStore,
|
|
789
809
|
...(input.now ? { now: input.now } : {}),
|
|
790
810
|
...(input.databaseFactory ? { databaseFactory: input.databaseFactory } : {}),
|
|
791
811
|
}, true);
|
package/dist/index.d.ts
CHANGED
|
@@ -91,3 +91,6 @@ export * from './behavior-history.js';
|
|
|
91
91
|
export * from './codex-rule-context-provider.js';
|
|
92
92
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
93
93
|
export * from './explicit-owner-statement-prefix.js';
|
|
94
|
+
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
95
|
+
export { readDesignGateSpecSnapshot, describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
96
|
+
export { createCodexArtificerTrustedFactsPort } from './codex-artificer-trusted-facts.js';
|
package/dist/index.js
CHANGED
|
@@ -307,3 +307,6 @@ export * from './behavior-history.js';
|
|
|
307
307
|
export * from './codex-rule-context-provider.js';
|
|
308
308
|
export { createWorkspaceBehaviorEvidencePort } from './workspace-behavior-evidence.js';
|
|
309
309
|
export * from './explicit-owner-statement-prefix.js';
|
|
310
|
+
export { listObservedDesignGateSessions } from './workspace-behavior-evidence.js';
|
|
311
|
+
export { readDesignGateSpecSnapshot, describeCodexRuleContextFact } from './design-gate-facts.js';
|
|
312
|
+
export { createCodexArtificerTrustedFactsPort } from './codex-artificer-trusted-facts.js';
|
|
@@ -43,6 +43,13 @@ async function withinGateDeadline(value, startedAt) {
|
|
|
43
43
|
function isRecord(value) {
|
|
44
44
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
45
|
}
|
|
46
|
+
function requiresDesignGateFact(value) {
|
|
47
|
+
if (!isRecord(value) || !Object.hasOwn(value, 'requiredFactKinds'))
|
|
48
|
+
return false;
|
|
49
|
+
return Array.isArray(value.requiredFactKinds)
|
|
50
|
+
&& value.requiredFactKinds.every((kind) => typeof kind === 'string')
|
|
51
|
+
&& value.requiredFactKinds.includes('design_approval');
|
|
52
|
+
}
|
|
46
53
|
function readToolInput(rawPayload) {
|
|
47
54
|
if (!isRecord(rawPayload) || !Object.hasOwn(rawPayload, 'toolInput'))
|
|
48
55
|
return null;
|
|
@@ -375,7 +382,24 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
375
382
|
// sibling rules (v1 or well-fed v2) keep evaluating, and no
|
|
376
383
|
// governance state changes. Intentional behavior change vs the
|
|
377
384
|
// pre-fix "legal unavailable still executed" path.
|
|
378
|
-
|
|
385
|
+
//
|
|
386
|
+
// PRI-1019 exception: a rule whose ONLY declared dependence is
|
|
387
|
+
// the design_approval fact stays eligible on an actionable fact
|
|
388
|
+
// (resolution !== 'unknown') even when history is unavailable.
|
|
389
|
+
// SPEC-RULECODE-CODEX-LOCAL-CLOSURE-001 §6 requires a managed
|
|
390
|
+
// action with an authoritative not_approved to be blocked
|
|
391
|
+
// regardless of history, and the Artificer prompt contract
|
|
392
|
+
// (V2_CONTEXT_INSTRUCTION) requires history-dependent behavior
|
|
393
|
+
// to allow while the independently supplied fact stays
|
|
394
|
+
// enforceable. Without this exception the first managed action
|
|
395
|
+
// of a new session (no prior history) would never evaluate the
|
|
396
|
+
// rule, and an authoritative not_approved could not block.
|
|
397
|
+
const designGateFact = context?.facts.designGate;
|
|
398
|
+
const designGateActionable = designGateFact !== undefined && designGateFact.resolution !== 'unknown'
|
|
399
|
+
? designGateFact
|
|
400
|
+
: undefined;
|
|
401
|
+
if ((context === undefined || context.history.status !== 'available')
|
|
402
|
+
&& !(requiresDesignGateFact(content.ruleHostInputContract) && designGateActionable !== undefined)) {
|
|
379
403
|
// Reference identity against the frozen sentinel distinguishes
|
|
380
404
|
// "the provider faulted" from "the provider legitimately
|
|
381
405
|
// declared history unavailable" (same wire shape, different
|
|
@@ -388,6 +412,18 @@ export function createProductionRuleHostGate(options = {}) {
|
|
|
388
412
|
addWarning(warnings, `rule_context_v2_unavailable: ${fault} (activation=${activationId})`, 'enable and wire the host rule context provider, or regenerate the rule without context dependence');
|
|
389
413
|
continue;
|
|
390
414
|
}
|
|
415
|
+
const inputContract = content.ruleHostInputContract;
|
|
416
|
+
if (isRecord(inputContract) && Object.hasOwn(inputContract, 'requiredFactKinds')) {
|
|
417
|
+
if (!Array.isArray(inputContract.requiredFactKinds)
|
|
418
|
+
|| !inputContract.requiredFactKinds.every((kind) => typeof kind === 'string')) {
|
|
419
|
+
addWarning(warnings, `rule_required_fact_contract_invalid: activation=${activationId}`, 'regenerate the rule artifact from validated Scribe execution conditions');
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (requiresDesignGateFact(inputContract) && designGateActionable === undefined) {
|
|
423
|
+
addWarning(warnings, `rule_required_fact_unavailable: design_approval (activation=${activationId})`, 'verify the current Owner design declaration and scope before retrying');
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
391
427
|
}
|
|
392
428
|
const ruleId = typeof content.ruleId === 'string' ? content.ruleId : typeof row.source_rule_id === 'string' ? row.source_rule_id : artifactId;
|
|
393
429
|
const principleId = typeof content.principleId === 'string' ? content.principleId : typeof row.source_principle_id === 'string' ? row.source_principle_id : ruleId;
|
|
@@ -19,3 +19,12 @@ export declare function readWorkspaceRuleContextRowsBefore(workspaceDir: string,
|
|
|
19
19
|
truncated: boolean;
|
|
20
20
|
unscopedPriorRows: boolean;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* List only Codex sessions proven by normalized host evidence and a matching
|
|
24
|
+
* project-scoped trajectory row. The trajectory schema has no host_kind on
|
|
25
|
+
* tool_calls, so that table alone is never used to infer the host.
|
|
26
|
+
*/
|
|
27
|
+
export declare function listObservedDesignGateSessions(workspaceDir: string): {
|
|
28
|
+
projectWorkspacePath: string;
|
|
29
|
+
hostSessionId: string;
|
|
30
|
+
}[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Read-only adapter over the canonical trajectory store; never creates/migrates it. */
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import Database from 'better-sqlite3';
|
|
5
|
+
import { SqliteConnection } from '@principles/core/runtime-v2';
|
|
4
6
|
import { createOwnerEvidenceScopeError } from '@principles/core/runtime-v2/internalization/evidence-readiness';
|
|
5
7
|
import { BehaviorExamplePackAssembler } from './behavior-example-pack-assembler.js';
|
|
6
8
|
import { resolveWorkspaceHostToolSemantics } from './host-tool-semantic-resolver.js';
|
|
@@ -119,3 +121,86 @@ export function readWorkspaceRuleContextRowsBefore(workspaceDir, input) {
|
|
|
119
121
|
db.close();
|
|
120
122
|
}
|
|
121
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* List only Codex sessions proven by normalized host evidence and a matching
|
|
126
|
+
* project-scoped trajectory row. The trajectory schema has no host_kind on
|
|
127
|
+
* tool_calls, so that table alone is never used to infer the host.
|
|
128
|
+
*/
|
|
129
|
+
export function listObservedDesignGateSessions(workspaceDir) {
|
|
130
|
+
const connection = new SqliteConnection({ workspaceDir, readonly: true, bootstrapIfMissing: false });
|
|
131
|
+
let evidenceRefs;
|
|
132
|
+
try {
|
|
133
|
+
const db = connection.getDb();
|
|
134
|
+
const exists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'intervention_evidence_records'").get();
|
|
135
|
+
if (exists === undefined)
|
|
136
|
+
return [];
|
|
137
|
+
const rawRows = db.prepare(`
|
|
138
|
+
SELECT native_refs_json, recorded_at
|
|
139
|
+
FROM intervention_evidence_records
|
|
140
|
+
ORDER BY recorded_at DESC
|
|
141
|
+
LIMIT 1000
|
|
142
|
+
`).all();
|
|
143
|
+
const found = new Map();
|
|
144
|
+
for (const raw of rawRows) {
|
|
145
|
+
if (!record(raw) || typeof raw.native_refs_json !== 'string' || typeof raw.recorded_at !== 'string')
|
|
146
|
+
continue;
|
|
147
|
+
let refs;
|
|
148
|
+
try {
|
|
149
|
+
refs = JSON.parse(raw.native_refs_json);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!record(refs) || refs.hostKind !== 'codex' || typeof refs.sessionId !== 'string' || refs.sessionId.trim().length === 0)
|
|
155
|
+
continue;
|
|
156
|
+
if (!found.has(refs.sessionId))
|
|
157
|
+
found.set(refs.sessionId, raw.recorded_at);
|
|
158
|
+
}
|
|
159
|
+
evidenceRefs = [...found].map(([hostSessionId, lastObservedAt]) => ({ hostSessionId, lastObservedAt }));
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
connection.close();
|
|
163
|
+
}
|
|
164
|
+
const trajectoryPath = path.join(workspaceDir, '.state', 'trajectory.db');
|
|
165
|
+
const trajectory = new Database(trajectoryPath, { readonly: true, fileMustExist: true });
|
|
166
|
+
try {
|
|
167
|
+
const selectProject = trajectory.prepare(`
|
|
168
|
+
SELECT project_dir, MAX(created_at) AS last_seen
|
|
169
|
+
FROM tool_calls
|
|
170
|
+
WHERE session_id = ? AND project_dir IS NOT NULL
|
|
171
|
+
GROUP BY project_dir
|
|
172
|
+
ORDER BY last_seen DESC
|
|
173
|
+
`);
|
|
174
|
+
const candidates = [];
|
|
175
|
+
for (const evidence of evidenceRefs) {
|
|
176
|
+
const rows = selectProject.all(evidence.hostSessionId);
|
|
177
|
+
for (const raw of rows) {
|
|
178
|
+
if (!record(raw) || typeof raw.project_dir !== 'string' || !path.isAbsolute(raw.project_dir)
|
|
179
|
+
|| typeof raw.last_seen !== 'string')
|
|
180
|
+
continue;
|
|
181
|
+
try {
|
|
182
|
+
candidates.push({
|
|
183
|
+
projectWorkspacePath: fs.realpathSync.native(raw.project_dir),
|
|
184
|
+
hostSessionId: evidence.hostSessionId,
|
|
185
|
+
seenAt: raw.last_seen > evidence.lastObservedAt ? raw.last_seen : evidence.lastObservedAt,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch { /* stale project paths are not offered as selectable sessions */ }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const unique = new Map();
|
|
192
|
+
for (const candidate of candidates) {
|
|
193
|
+
const key = `${candidate.projectWorkspacePath}\0${candidate.hostSessionId}`;
|
|
194
|
+
const current = unique.get(key);
|
|
195
|
+
if (!current || candidate.seenAt > current.seenAt)
|
|
196
|
+
unique.set(key, candidate);
|
|
197
|
+
}
|
|
198
|
+
return [...unique.values()]
|
|
199
|
+
.sort((left, right) => right.seenAt.localeCompare(left.seenAt))
|
|
200
|
+
.slice(0, 100)
|
|
201
|
+
.map(({ projectWorkspacePath, hostSessionId }) => ({ projectWorkspacePath, hostSessionId }));
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
trajectory.close();
|
|
205
|
+
}
|
|
206
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principles/host-runtime",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint": "eslint \"src/**/*.ts\""
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@principles/core": "^1.295.
|
|
34
|
+
"@principles/core": "^1.295.3",
|
|
35
35
|
"@principles/install-layout": "^0.4.0",
|
|
36
36
|
"better-sqlite3": "^13.0.3",
|
|
37
37
|
"js-yaml": "^5.4.3"
|