memorix 1.2.0 → 1.2.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/CHANGELOG.md +14 -1
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/dist/cli/index.js +677 -441
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +830 -646
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +110 -5
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +14 -1
- package/dist/sdk.js +830 -646
- package/dist/sdk.js.map +1 -1
- package/docs/API_REFERENCE.md +3 -1
- package/docs/DEVELOPMENT.md +4 -0
- package/docs/SETUP.md +7 -1
- package/docs/knowledge/workflows/memorix-release.md +57 -0
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/commands/codegraph.ts +2 -3
- package/src/cli/commands/knowledge.ts +40 -0
- package/src/cli/commands/serve-http.ts +14 -41
- package/src/cli/commands/setup.ts +9 -3
- package/src/codegraph/auto-context.ts +3 -15
- package/src/codegraph/current-facts.ts +19 -1
- package/src/codegraph/task-lens.ts +49 -5
- package/src/dashboard/server.ts +15 -46
- package/src/knowledge/claims.ts +51 -1
- package/src/knowledge/types.ts +1 -0
- package/src/knowledge/workflows.ts +34 -3
- package/src/memory/auto-relations.ts +21 -0
- package/src/memory/graph-scope.ts +46 -0
- package/src/orchestrate/verify-gate.ts +33 -10
- package/src/server.ts +75 -13
package/docs/API_REFERENCE.md
CHANGED
|
@@ -120,6 +120,8 @@ The built-in Lite provider indexes common code files with lightweight file, symb
|
|
|
120
120
|
```bash
|
|
121
121
|
memorix knowledge init --mode local
|
|
122
122
|
memorix knowledge status
|
|
123
|
+
memorix knowledge claims
|
|
124
|
+
memorix knowledge review --id <claim-id> --review approved --detail "checked current source evidence"
|
|
123
125
|
memorix knowledge compile
|
|
124
126
|
memorix knowledge lint
|
|
125
127
|
memorix knowledge apply --proposal <id>
|
|
@@ -128,7 +130,7 @@ memorix knowledge workflow select --task "prepare a release"
|
|
|
128
130
|
memorix knowledge workflow preview --id <workflow-id> --agent codex
|
|
129
131
|
```
|
|
130
132
|
|
|
131
|
-
The advanced MCP action tool is `memorix_knowledge`. It is registered only in the `team` and `full` tool profiles so normal agents keep the compact micro/lite tool surface. Its actions are `workspace_init`, `status`, `compile`, `lint`, `proposal_apply`, `workflow_import`, `workflow_list`, `workflow_select`, `workflow_preview`, `workflow_apply`, and `workflow_run`.
|
|
133
|
+
The advanced MCP action tool is `memorix_knowledge`. It is registered only in the `team` and `full` tool profiles so normal agents keep the compact micro/lite tool surface. Its actions are `workspace_init`, `status`, `claim_list`, `claim_review`, `compile`, `lint`, `proposal_apply`, `workflow_import`, `workflow_list`, `workflow_select`, `workflow_preview`, `workflow_apply`, and `workflow_run`. Explicit agent observations become `needs-review` claim candidates: check their source evidence, then use `claim_review` with a reason to approve or reject them. Only approved claims can be compiled into publishable Knowledge Workspace pages. Ordinary coding work should stay on `memorix_project_context`.
|
|
132
134
|
|
|
133
135
|
### Cross-Agent Handoff Receipt
|
|
134
136
|
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -271,6 +271,10 @@ to make sure provenance diagnostics match runtime behavior.
|
|
|
271
271
|
|
|
272
272
|
## 9. Release Workflow
|
|
273
273
|
|
|
274
|
+
The repository's canonical workflow is [Memorix release](knowledge/workflows/memorix-release.md).
|
|
275
|
+
It is the versioned source for agents and maintainers; its checks supplement,
|
|
276
|
+
rather than replace, the explicit maintainer approval required to publish.
|
|
277
|
+
|
|
274
278
|
Recommended release flow:
|
|
275
279
|
|
|
276
280
|
1. update docs and version metadata
|
package/docs/SETUP.md
CHANGED
|
@@ -276,12 +276,18 @@ Manual stdio shape:
|
|
|
276
276
|
"mcpServers": {
|
|
277
277
|
"memorix": {
|
|
278
278
|
"command": "memorix",
|
|
279
|
-
"args": ["serve"]
|
|
279
|
+
"args": ["serve"],
|
|
280
|
+
"alwaysLoad": true
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
```
|
|
284
285
|
|
|
286
|
+
Keep `alwaysLoad: true` for a manually managed Claude Code entry so its small
|
|
287
|
+
Memorix tool set is available during print-mode startup. `memorix doctor agents
|
|
288
|
+
--agent claude` reports a missing eager-load setting, and `memorix repair agents
|
|
289
|
+
--agent claude` restores it without replacing unrelated MCP servers.
|
|
290
|
+
|
|
285
291
|
### Cursor
|
|
286
292
|
|
|
287
293
|
Project config: `.cursor/mcp.json`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: memorix-release
|
|
3
|
+
title: Memorix release
|
|
4
|
+
description: Prepare a Memorix release with evidence, package verification, and explicit maintainer approval.
|
|
5
|
+
status: active
|
|
6
|
+
version: 1
|
|
7
|
+
taskLenses: [release]
|
|
8
|
+
triggers: [release, publish, npm, version, changelog]
|
|
9
|
+
allowedAgents: [codex, claude-code, cursor, windsurf, copilot, antigravity, gemini-cli, openclaw, hermes, omp, kiro, opencode, trae]
|
|
10
|
+
verificationGates:
|
|
11
|
+
- Version and changelog match the release scope
|
|
12
|
+
- npm run lint passes
|
|
13
|
+
- npm run build passes
|
|
14
|
+
- npm test passes
|
|
15
|
+
- Focused live MCP smoke passes when server behavior changed
|
|
16
|
+
- Package smoke passes
|
|
17
|
+
- CI passes on the release commit
|
|
18
|
+
- Maintainer approval is explicit before publishing
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Inspect
|
|
22
|
+
|
|
23
|
+
Read the current version, changelog, Git state, open release risks, and the
|
|
24
|
+
diff that will ship. Do not treat a release tag, an old test run, or a local
|
|
25
|
+
token as proof that this exact commit is ready.
|
|
26
|
+
|
|
27
|
+
## Prepare
|
|
28
|
+
|
|
29
|
+
Update version metadata, changelog, and user-facing documentation only for
|
|
30
|
+
changes that are actually included. Keep package workspace visibility and npm
|
|
31
|
+
access settings unchanged unless the release specifically changes them.
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
Run `npm run lint`, `npm run build`, and `npm test`. Run focused tests for the
|
|
36
|
+
changed surface. When MCP behavior changed, use a real isolated stdio client to
|
|
37
|
+
exercise the affected tool path. Inspect `npm pack --dry-run --json` and run a
|
|
38
|
+
package smoke from the packed artifact when packaging changed.
|
|
39
|
+
|
|
40
|
+
## Review
|
|
41
|
+
|
|
42
|
+
Record what passed, what was not run, and any remaining risk. Confirm that the
|
|
43
|
+
release commit has passing CI. A workflow may prepare evidence, but publishing
|
|
44
|
+
requires explicit maintainer approval.
|
|
45
|
+
|
|
46
|
+
## Publish
|
|
47
|
+
|
|
48
|
+
After explicit maintainer approval, publish through the approved release path.
|
|
49
|
+
Never put credentials in the repository, release notes, workflow files, or
|
|
50
|
+
command output. Verify the published package version without exposing tokens.
|
|
51
|
+
|
|
52
|
+
## Follow Up
|
|
53
|
+
|
|
54
|
+
Check a fresh install or `npx` invocation, create or update the GitHub release
|
|
55
|
+
if appropriate, and reply to affected issue reporters with the concrete fix and
|
|
56
|
+
version. Store the verification evidence as project knowledge rather than an
|
|
57
|
+
unstructured chat note.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { CodeGraphStore } from '../../codegraph/store.js';
|
|
|
3
3
|
import { refreshProjectLite } from '../../codegraph/lite-provider.js';
|
|
4
4
|
import { assembleContextPackForTask, attachTaskWorkset, buildContextPackPrompt } from '../../codegraph/context-pack.js';
|
|
5
5
|
import { backfillMissingObservationCodeRefs } from '../../codegraph/binder.js';
|
|
6
|
-
import { collectCurrentProjectFacts } from '../../codegraph/current-facts.js';
|
|
6
|
+
import { collectCurrentProjectFacts, formatGitFact } from '../../codegraph/current-facts.js';
|
|
7
7
|
import { resolveTaskLens } from '../../codegraph/task-lens.js';
|
|
8
8
|
import { getExternalCodeGraphContext, inspectExternalCodeGraph } from '../../codegraph/external-provider.js';
|
|
9
9
|
import type { CodeGraphProviderQuality } from '../../codegraph/types.js';
|
|
@@ -71,8 +71,7 @@ function compactFacts(project: { rootPath: string }): { facts: string[]; dirty:
|
|
|
71
71
|
facts.push('Latest changelog: ' + current.latestChangelog.version
|
|
72
72
|
+ (current.latestChangelog.date ? ' (' + current.latestChangelog.date + ')' : ''));
|
|
73
73
|
}
|
|
74
|
-
facts.push(
|
|
75
|
-
+ (current.git.dirty ? 'dirty worktree' : 'clean worktree'));
|
|
74
|
+
facts.push(formatGitFact(current.git));
|
|
76
75
|
return { facts, dirty: current.git.dirty };
|
|
77
76
|
}
|
|
78
77
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
2
|
import { CodeGraphStore } from '../../codegraph/store.js';
|
|
3
3
|
import { ClaimStore } from '../../knowledge/claim-store.js';
|
|
4
|
+
import { reviewClaim } from '../../knowledge/claims.js';
|
|
4
5
|
import { applyKnowledgeProposal, compileKnowledgeWorkspace, lintKnowledgeWorkspace } from '../../knowledge/wiki.js';
|
|
5
6
|
import { initializeKnowledgeWorkspace, loadKnowledgeWorkspace } from '../../knowledge/workspace.js';
|
|
6
7
|
import { KnowledgeWorkspaceStore } from '../../knowledge/workspace-store.js';
|
|
@@ -28,6 +29,8 @@ function usage(): string {
|
|
|
28
29
|
' memorix knowledge init [--mode local]',
|
|
29
30
|
' memorix knowledge init --mode versioned --path C:\\project\\docs\\knowledge',
|
|
30
31
|
' memorix knowledge status',
|
|
32
|
+
' memorix knowledge claims',
|
|
33
|
+
' memorix knowledge review --id <claim-id> --review approved --detail "checked current source evidence"',
|
|
31
34
|
' memorix knowledge compile',
|
|
32
35
|
' memorix knowledge lint',
|
|
33
36
|
' memorix knowledge apply --proposal <id> [--force]',
|
|
@@ -57,6 +60,11 @@ function workflowVerdict(value: unknown): 'passed' | 'failed' | 'not-run' | unde
|
|
|
57
60
|
throw new Error('workflow verdict must be passed, failed, or not-run');
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function claimReviewState(value: unknown): 'approved' | 'rejected' {
|
|
64
|
+
if (value === 'approved' || value === 'rejected') return value;
|
|
65
|
+
throw new Error('claim review must be approved or rejected');
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
function evidenceList(value: unknown): string[] {
|
|
61
69
|
if (typeof value !== 'string' || !value.trim()) return [];
|
|
62
70
|
return [...new Set(value.split(',').map(item => item.trim()).filter(Boolean))];
|
|
@@ -81,6 +89,8 @@ export default defineCommand({
|
|
|
81
89
|
failure: { type: 'string', description: 'Sanitized failure reason for a workflow run' },
|
|
82
90
|
snapshot: { type: 'string', description: 'Starting code snapshot id for a workflow run' },
|
|
83
91
|
evidence: { type: 'string', description: 'Comma-separated evidence ids selected for a workflow run' },
|
|
92
|
+
review: { type: 'string', description: 'Claim review verdict: approved or rejected' },
|
|
93
|
+
detail: { type: 'string', description: 'Evidence check performed for a claim review' },
|
|
84
94
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
85
95
|
},
|
|
86
96
|
run: async ({ args }) => {
|
|
@@ -120,6 +130,36 @@ export default defineCommand({
|
|
|
120
130
|
const workspaceStore = new KnowledgeWorkspaceStore();
|
|
121
131
|
await workspaceStore.init(dataDir);
|
|
122
132
|
|
|
133
|
+
if (action === 'claims') {
|
|
134
|
+
const claims = claimStore.listClaims(project.id, { limit: 100 });
|
|
135
|
+
emitResult(
|
|
136
|
+
{ project, workspace, claims },
|
|
137
|
+
'Claims: ' + claims.length + ' total, ' + claims.filter(claim => claim.reviewState === 'needs-review').length + ' awaiting review.',
|
|
138
|
+
asJson,
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (action === 'review') {
|
|
144
|
+
const claimId = requiredText(args.id, 'claim id');
|
|
145
|
+
const existing = claimStore.getClaim(claimId);
|
|
146
|
+
if (!existing || existing.projectId !== project.id) {
|
|
147
|
+
emitError('Claim was not found for this knowledge workspace.', asJson);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const claim = reviewClaim(claimStore, {
|
|
151
|
+
claimId,
|
|
152
|
+
reviewState: claimReviewState(args.review),
|
|
153
|
+
detail: requiredText(args.detail, 'review detail'),
|
|
154
|
+
});
|
|
155
|
+
emitResult(
|
|
156
|
+
{ project, workspace, claim },
|
|
157
|
+
'Claim ' + claim.id + ' marked ' + claim.reviewState + '.',
|
|
158
|
+
asJson,
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
123
163
|
if (action === 'workflow') {
|
|
124
164
|
const workflowAction = (positional[1] || 'list').toLowerCase();
|
|
125
165
|
const workflowStore = new WorkflowStore();
|
|
@@ -23,6 +23,7 @@ import { defineCommand } from 'citty';
|
|
|
23
23
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
24
24
|
import type { ObservationStore } from '../../store/obs-store.js';
|
|
25
25
|
import { resolveToolProfile } from '../../server/tool-profile.js';
|
|
26
|
+
import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
26
27
|
|
|
27
28
|
export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
28
29
|
|
|
@@ -764,6 +765,7 @@ export default defineCommand({
|
|
|
764
765
|
source?: string;
|
|
765
766
|
commitHash?: string;
|
|
766
767
|
filesModified?: string[];
|
|
768
|
+
relatedEntities?: string[];
|
|
767
769
|
status?: string;
|
|
768
770
|
importance?: number;
|
|
769
771
|
accessCount?: number;
|
|
@@ -860,12 +862,7 @@ export default defineCommand({
|
|
|
860
862
|
} catch { /* best effort */ }
|
|
861
863
|
|
|
862
864
|
// Project-scoped graph counts (must match /graph and /export)
|
|
863
|
-
const
|
|
864
|
-
observations.filter(o => o.entityName).map(o => o.entityName!)
|
|
865
|
-
);
|
|
866
|
-
const projectEntities = graph.entities.filter((e: any) => projectEntityNames.has(e.name));
|
|
867
|
-
const projectEntitySet = new Set(projectEntities.map((e: any) => e.name));
|
|
868
|
-
const projectRelations = graph.relations.filter((r: any) => projectEntitySet.has(r.from) && projectEntitySet.has(r.to));
|
|
865
|
+
const projectGraph = scopeKnowledgeGraphToProject(graph, observations);
|
|
869
866
|
|
|
870
867
|
let maintenance = { total: 0, pending: 0, running: 0, retrying: 0, completed: 0, failed: 0 };
|
|
871
868
|
let lifecycle: unknown;
|
|
@@ -877,8 +874,8 @@ export default defineCommand({
|
|
|
877
874
|
} catch { /* optional maintenance diagnostics */ }
|
|
878
875
|
|
|
879
876
|
sendJson({
|
|
880
|
-
entities:
|
|
881
|
-
relations:
|
|
877
|
+
entities: projectGraph.entities.length,
|
|
878
|
+
relations: projectGraph.relations.length,
|
|
882
879
|
observations: observations.length,
|
|
883
880
|
nextId,
|
|
884
881
|
typeCounts,
|
|
@@ -930,19 +927,9 @@ export default defineCommand({
|
|
|
930
927
|
await initGraphStore(graphDataDir);
|
|
931
928
|
const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
932
929
|
|
|
933
|
-
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
allObs
|
|
937
|
-
.filter(o => o.entityName)
|
|
938
|
-
.map(o => o.entityName!)
|
|
939
|
-
);
|
|
940
|
-
|
|
941
|
-
const entities = fullGraph.entities.filter(e => projectEntityNames.has(e.name));
|
|
942
|
-
const entityNameSet = new Set(entities.map(e => e.name));
|
|
943
|
-
const relations = fullGraph.relations.filter(r => entityNameSet.has(r.from) && entityNameSet.has(r.to));
|
|
944
|
-
|
|
945
|
-
sendJson({ entities, relations });
|
|
930
|
+
const allObs = await loadDashboardProjectObservations(graphDataDir, graphProjectId, 'active');
|
|
931
|
+
const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
|
|
932
|
+
sendJson({ entities: scoped.entities, relations: scoped.relations });
|
|
946
933
|
return;
|
|
947
934
|
}
|
|
948
935
|
|
|
@@ -1012,24 +999,15 @@ export default defineCommand({
|
|
|
1012
999
|
const allObs = await loadDashboardProjectObservations(kgDataDir, kgProjectId, 'active');
|
|
1013
1000
|
const skills = await getMiniSkillStore().loadByProject(kgProjectId);
|
|
1014
1001
|
|
|
1015
|
-
// Project-scope graph store (same logic as /api/graph)
|
|
1016
1002
|
const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
1017
|
-
const
|
|
1018
|
-
const projectEntityNames = new Set(
|
|
1019
|
-
graphObs
|
|
1020
|
-
.filter(o => o.entityName)
|
|
1021
|
-
.map(o => o.entityName!)
|
|
1022
|
-
);
|
|
1023
|
-
const scopedEntities = fullGraph.entities.filter(e => projectEntityNames.has(e.name));
|
|
1024
|
-
const scopedEntityNameSet = new Set(scopedEntities.map(e => e.name));
|
|
1025
|
-
const scopedRelations = fullGraph.relations.filter(r => scopedEntityNameSet.has(r.from) && scopedEntityNameSet.has(r.to));
|
|
1003
|
+
const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
|
|
1026
1004
|
|
|
1027
1005
|
const graph = generateKnowledgeGraph({
|
|
1028
1006
|
projectId: kgProjectId,
|
|
1029
1007
|
observations: allObs,
|
|
1030
1008
|
miniSkills: skills,
|
|
1031
|
-
graphEntities:
|
|
1032
|
-
graphRelations:
|
|
1009
|
+
graphEntities: scoped.entities,
|
|
1010
|
+
graphRelations: scoped.relations,
|
|
1033
1011
|
});
|
|
1034
1012
|
|
|
1035
1013
|
sendJson(graph);
|
|
@@ -1369,19 +1347,14 @@ export default defineCommand({
|
|
|
1369
1347
|
const { initGraphStore, getGraphStore } = await import('../../store/graph-store.js');
|
|
1370
1348
|
await initGraphStore(expDataDir);
|
|
1371
1349
|
const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
1372
|
-
const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active')
|
|
1350
|
+
const observations = await loadDashboardProjectObservations(expDataDir, expProjectId, 'active');
|
|
1373
1351
|
const expStore = await getDashboardObservationStore(expDataDir);
|
|
1374
1352
|
const nextId = await expStore.loadIdCounter();
|
|
1375
|
-
const
|
|
1376
|
-
observations.filter(o => o.entityName).map(o => o.entityName!),
|
|
1377
|
-
);
|
|
1378
|
-
const exportEntities = fullGraph.entities.filter((e: any) => exportEntityNames.has(e.name));
|
|
1379
|
-
const exportEntitySet = new Set(exportEntities.map((e: any) => e.name));
|
|
1380
|
-
const exportRelations = fullGraph.relations.filter((r: any) => exportEntitySet.has(r.from) && exportEntitySet.has(r.to));
|
|
1353
|
+
const scoped = scopeKnowledgeGraphToProject(fullGraph, observations);
|
|
1381
1354
|
const exportData = {
|
|
1382
1355
|
project: { id: expProjectId, name: expProjectName },
|
|
1383
1356
|
exportedAt: new Date().toISOString(),
|
|
1384
|
-
graph: { entities:
|
|
1357
|
+
graph: { entities: scoped.entities, relations: scoped.relations },
|
|
1385
1358
|
observations,
|
|
1386
1359
|
nextId,
|
|
1387
1360
|
};
|
|
@@ -1062,7 +1062,7 @@ function tryInstallOmpPackage(packagePath: string): { ok: boolean; message: stri
|
|
|
1062
1062
|
};
|
|
1063
1063
|
}
|
|
1064
1064
|
|
|
1065
|
-
async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: boolean): Promise<void> {
|
|
1065
|
+
export async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: boolean): Promise<void> {
|
|
1066
1066
|
const { installAgentGuidance, installHooks } = await import('../../hooks/installers/index.js');
|
|
1067
1067
|
const targetRoot = global ? homedir() : process.cwd();
|
|
1068
1068
|
const hasPluginPackage = plan.actions.includes('plugin-package') && PLUGIN_PACKAGE_AGENTS.has(agent);
|
|
@@ -1200,13 +1200,19 @@ async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: bool
|
|
|
1200
1200
|
} else if (hasOmpPackage) {
|
|
1201
1201
|
p.log.info(`${agent}: extension command, hook events, and skills are bundled in the Oh-my-Pi package`);
|
|
1202
1202
|
} else if (hasExtensionPackage) {
|
|
1203
|
-
if (plan.actions.includes('hooks')
|
|
1203
|
+
if (plan.actions.includes('hooks')) {
|
|
1204
1204
|
const result = await installHooks(agent, targetRoot, global);
|
|
1205
1205
|
p.log.success(`${agent}: integration files -> ${result.configPath}`);
|
|
1206
|
+
} else if (plan.actions.includes('project-guidance')) {
|
|
1207
|
+
const rulesPath = await installAgentGuidance(agent, targetRoot, global);
|
|
1208
|
+
p.log.success(`${agent}: guidance -> ${rulesPath}`);
|
|
1206
1209
|
}
|
|
1207
|
-
} else if (
|
|
1210
|
+
} else if (plan.actions.includes('hooks')) {
|
|
1208
1211
|
const result = await installHooks(agent, targetRoot, global);
|
|
1209
1212
|
p.log.success(`${agent}: integration files -> ${result.configPath}`);
|
|
1213
|
+
} else if (plan.actions.includes('project-guidance')) {
|
|
1214
|
+
const rulesPath = await installAgentGuidance(agent, targetRoot, global);
|
|
1215
|
+
p.log.success(`${agent}: guidance -> ${rulesPath}`);
|
|
1210
1216
|
}
|
|
1211
1217
|
|
|
1212
1218
|
if (agent === 'pi') {
|
|
@@ -4,7 +4,7 @@ import { getResolvedConfig } from '../config/resolved-config.js';
|
|
|
4
4
|
import { buildTaskWorkset, type TaskWorkset, type WorksetCaution } from '../knowledge/workset.js';
|
|
5
5
|
import type { ProjectInfo } from '../types.js';
|
|
6
6
|
import { backfillMissingObservationCodeRefs, type CodeRefBackfillResult } from './binder.js';
|
|
7
|
-
import { collectCurrentProjectFacts, type CurrentProjectFacts } from './current-facts.js';
|
|
7
|
+
import { collectCurrentProjectFacts, formatGitFact, type CurrentProjectFacts } from './current-facts.js';
|
|
8
8
|
import { refreshProjectLite } from './lite-provider.js';
|
|
9
9
|
import {
|
|
10
10
|
getExternalCodeGraphContext,
|
|
@@ -430,15 +430,7 @@ function formatCurrentFactsLines(facts: CurrentProjectFacts): string[] {
|
|
|
430
430
|
lines.push(`- Latest changelog: ${facts.latestChangelog.version}${facts.latestChangelog.date ? ` (${facts.latestChangelog.date})` : ''}`);
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
|
|
434
|
-
if (facts.git.detached) {
|
|
435
|
-
gitParts.push('detached HEAD');
|
|
436
|
-
} else if (facts.git.branch) {
|
|
437
|
-
gitParts.push(`branch ${facts.git.branch}`);
|
|
438
|
-
}
|
|
439
|
-
if (facts.git.commit) gitParts.push(`commit ${facts.git.commit}`);
|
|
440
|
-
gitParts.push(facts.git.dirty ? 'dirty worktree' : 'clean worktree');
|
|
441
|
-
lines.push(`- Git: ${gitParts.join(', ')}`);
|
|
433
|
+
lines.push('- ' + formatGitFact(facts.git));
|
|
442
434
|
if (facts.git.latestCommit) lines.push(`- Latest commit: ${facts.git.latestCommit}`);
|
|
443
435
|
lines.push('- Current facts above outrank progress/dev-log files when they conflict.');
|
|
444
436
|
|
|
@@ -464,11 +456,7 @@ function worksetFactLines(facts: CurrentProjectFacts): string[] {
|
|
|
464
456
|
lines.push('Latest changelog: ' + facts.latestChangelog.version
|
|
465
457
|
+ (facts.latestChangelog.date ? ' (' + facts.latestChangelog.date + ')' : ''));
|
|
466
458
|
}
|
|
467
|
-
|
|
468
|
-
if (facts.git.branch) gitParts.push('branch ' + facts.git.branch);
|
|
469
|
-
if (facts.git.commit) gitParts.push('commit ' + facts.git.commit);
|
|
470
|
-
gitParts.push(facts.git.dirty ? 'dirty worktree' : 'clean worktree');
|
|
471
|
-
lines.push('Git: ' + gitParts.join(', '));
|
|
459
|
+
lines.push(formatGitFact(facts.git));
|
|
472
460
|
for (const note of facts.staleNotes.slice(0, 1)) {
|
|
473
461
|
lines.push(
|
|
474
462
|
'Historical note: ' + note.path
|
|
@@ -10,6 +10,7 @@ export interface CurrentProjectFacts {
|
|
|
10
10
|
date?: string;
|
|
11
11
|
};
|
|
12
12
|
git: {
|
|
13
|
+
available: boolean;
|
|
13
14
|
branch?: string;
|
|
14
15
|
commit?: string;
|
|
15
16
|
latestCommit?: string;
|
|
@@ -72,11 +73,18 @@ function runGit(rootPath: string, args: string[]): string | undefined {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function readGitFacts(rootPath: string): CurrentProjectFacts['git'] {
|
|
75
|
-
const
|
|
76
|
+
const available = runGit(rootPath, ['rev-parse', '--is-inside-work-tree']) === 'true';
|
|
77
|
+
if (!available) {
|
|
78
|
+
return { available: false, dirty: false, detached: false };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const branch = runGit(rootPath, ['symbolic-ref', '--quiet', '--short', 'HEAD'])
|
|
82
|
+
?? runGit(rootPath, ['branch', '--show-current']);
|
|
76
83
|
const commit = runGit(rootPath, ['rev-parse', '--short', 'HEAD']);
|
|
77
84
|
const latestCommit = runGit(rootPath, ['log', '-1', '--pretty=%s']);
|
|
78
85
|
const dirty = Boolean(runGit(rootPath, ['status', '--porcelain']));
|
|
79
86
|
return {
|
|
87
|
+
available: true,
|
|
80
88
|
...(branch ? { branch } : {}),
|
|
81
89
|
...(commit ? { commit } : {}),
|
|
82
90
|
...(latestCommit ? { latestCommit } : {}),
|
|
@@ -85,6 +93,16 @@ function readGitFacts(rootPath: string): CurrentProjectFacts['git'] {
|
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
|
96
|
+
export function formatGitFact(git: CurrentProjectFacts['git']): string {
|
|
97
|
+
if (!git.available) return 'Git: unavailable';
|
|
98
|
+
const parts: string[] = [];
|
|
99
|
+
if (git.detached) parts.push('detached HEAD');
|
|
100
|
+
else if (git.branch) parts.push('branch ' + git.branch);
|
|
101
|
+
if (git.commit) parts.push('commit ' + git.commit);
|
|
102
|
+
parts.push(git.dirty ? 'dirty worktree' : 'clean worktree');
|
|
103
|
+
return 'Git: ' + parts.join(', ');
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
function parseProgressNote(content: string): Pick<ProjectStaleNote, 'lastUpdated' | 'branchHint'> {
|
|
89
107
|
const lastUpdated = content.match(/Last updated\*\*:\s*(\d{4}-\d{2}-\d{2})/i)
|
|
90
108
|
?? content.match(/Last updated:\s*(\d{4}-\d{2}-\d{2})/i);
|
|
@@ -115,6 +115,7 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
|
|
|
115
115
|
bugfix: [
|
|
116
116
|
'bug',
|
|
117
117
|
'crash',
|
|
118
|
+
'incident',
|
|
118
119
|
'debug',
|
|
119
120
|
'error',
|
|
120
121
|
'fail',
|
|
@@ -125,6 +126,7 @@ const KEYWORDS: Record<Exclude<TaskLensId, 'general'>, string[]> = {
|
|
|
125
126
|
'repro',
|
|
126
127
|
'报错',
|
|
127
128
|
'崩溃',
|
|
129
|
+
'故障',
|
|
128
130
|
'失败',
|
|
129
131
|
'修复',
|
|
130
132
|
'问题',
|
|
@@ -157,11 +159,53 @@ function tokenize(text: string): string[] {
|
|
|
157
159
|
.filter(token => token.length > 1 && !STOP_WORDS.has(token));
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Match a task keyword only when the task states it as an action or intent,
|
|
164
|
+
* rather than prohibiting that action. Workflow selection shares this rule so
|
|
165
|
+
* a safety constraint cannot reintroduce an intent rejected by task routing.
|
|
166
|
+
*/
|
|
167
|
+
export function containsTaskKeyword(text: string, keyword: string): boolean {
|
|
168
|
+
const matcher = /^[a-z0-9_-]+$/i.test(keyword)
|
|
169
|
+
? new RegExp(`(^|[^a-z0-9_-])${keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=[^a-z0-9_-]|$)`, 'gi')
|
|
170
|
+
: new RegExp(keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
171
|
+
let match: RegExpExecArray | null;
|
|
172
|
+
while ((match = matcher.exec(text)) !== null) {
|
|
173
|
+
const keywordIndex = match.index + (match[1]?.length ?? 0);
|
|
174
|
+
if (!isNegatedTaskKeyword(text, keywordIndex)) return true;
|
|
163
175
|
}
|
|
164
|
-
return
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Task text often carries safety constraints such as "do not publish". Those
|
|
181
|
+
* constraints must not route a debugging task into the release workflow.
|
|
182
|
+
* Keep this deliberately small and local: it only suppresses a keyword inside
|
|
183
|
+
* the same sentence when the sentence explicitly negates an action.
|
|
184
|
+
*/
|
|
185
|
+
function isNegatedTaskKeyword(text: string, keywordIndex: number): boolean {
|
|
186
|
+
const boundary = Math.max(
|
|
187
|
+
text.lastIndexOf('.', keywordIndex - 1),
|
|
188
|
+
text.lastIndexOf('!', keywordIndex - 1),
|
|
189
|
+
text.lastIndexOf('?', keywordIndex - 1),
|
|
190
|
+
text.lastIndexOf(';', keywordIndex - 1),
|
|
191
|
+
text.lastIndexOf('\n', keywordIndex - 1),
|
|
192
|
+
text.lastIndexOf('\u3002', keywordIndex - 1),
|
|
193
|
+
text.lastIndexOf('\uff01', keywordIndex - 1),
|
|
194
|
+
text.lastIndexOf('\uff1f', keywordIndex - 1),
|
|
195
|
+
text.lastIndexOf('\uff1b', keywordIndex - 1),
|
|
196
|
+
);
|
|
197
|
+
let prefix = text.slice(boundary + 1, keywordIndex).toLowerCase();
|
|
198
|
+
const contrast = /(?:,|\uff0c)\s*(?:but|however|instead|yet|\u4f46\u662f|\u4f46|\u800c\u662f)\s*/gi;
|
|
199
|
+
let contrastMatch: RegExpExecArray | null;
|
|
200
|
+
let contrastEnd = -1;
|
|
201
|
+
while ((contrastMatch = contrast.exec(prefix)) !== null) contrastEnd = contrast.lastIndex;
|
|
202
|
+
if (contrastEnd >= 0) prefix = prefix.slice(contrastEnd);
|
|
203
|
+
if (/\b(?:do|does|did|should|must|can|could|will|would|may|might)\s+not\b/.test(prefix)) return true;
|
|
204
|
+
if (/\b(?:don't|dont|never|without|avoid|skip)\b/.test(prefix)) return true;
|
|
205
|
+
if (/(?:^|[\s,])no\s+(?:[a-z0-9_-]+\s*){0,4}$/i.test(prefix)) return true;
|
|
206
|
+
const chineseClauseStart = Math.max(prefix.lastIndexOf(','), prefix.lastIndexOf('\uff0c'), prefix.lastIndexOf('\u3001'));
|
|
207
|
+
const chineseClause = prefix.slice(chineseClauseStart + 1);
|
|
208
|
+
return /(?:\u4e0d\u8981|\u4e0d\u5e94|\u4e0d\u53ef|\u4e0d\u80fd|\u4e0d\u4f1a|\u7981\u6b62|\u52ff|\u4e0d)\s*(?:\u7acb\u5373|\u76f4\u63a5|\u518d|\u73b0\u5728|\u64c5\u81ea|\u5148)?\s*$/.test(chineseClause);
|
|
165
209
|
}
|
|
166
210
|
|
|
167
211
|
export function resolveTaskLens(task?: string): TaskLens {
|
|
@@ -175,7 +219,7 @@ export function resolveTaskLens(task?: string): TaskLens {
|
|
|
175
219
|
};
|
|
176
220
|
|
|
177
221
|
for (const id of LENS_PRIORITY) {
|
|
178
|
-
const score = KEYWORDS[id].reduce((sum, keyword) => sum + (
|
|
222
|
+
const score = KEYWORDS[id].reduce((sum, keyword) => sum + (containsTaskKeyword(normalized, keyword) ? 1 : 0), 0);
|
|
179
223
|
const priority = LENS_PRIORITY.indexOf(id);
|
|
180
224
|
if (score > best.score || (score === best.score && score > 0 && priority < best.priority)) {
|
|
181
225
|
best = { id, score, priority };
|