Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ versioning; while the package is below 1.0, minor releases may change public beh

- `graphkeeper close run`, which atomically closes one existing open run with an end
timestamp and verdict while preserving its accumulated provenance.
- Contextual `graphkeeper append --help` and `graphkeeper append claim --help` output,
including complete claim flags, source-specific requirements, and copyable examples
without repository access or mutation.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ An agent writes to `graph/` and `evidence/`, and `graphkeeper check` (and the in
| `graphkeeper check` | Run the same fast schema, append-only history, and committed-evidence checks used by the Git hook. |
| `graphkeeper query <subject>` | Resolve an exact ID or unique alias and print active claims with provenance. It does not read evidence contents. |
| `graphkeeper doctor` | Run fast validation plus file existence, containment, line-range, dangling-reference, and unused-entity checks. |
| `graphkeeper append claim ...` | Concurrency-serially append a validating claim and link it into its producing run. See the [append command reference](docs/append-commands.md) for flags and lifecycle constraints. |
| `graphkeeper append claim ...` | Concurrency-serially append a validating claim and link it into its producing run. Run `graphkeeper append claim --help` for installed syntax; see the [append command reference](docs/append-commands.md) for lifecycle constraints. |
| `graphkeeper append run ...` | Concurrency-serially create a validating run record. It remains create-only; see the [append command reference](docs/append-commands.md). |
| `graphkeeper close run --id <id> --ended <timestamp> --verdict <value>` | Concurrency-serially close one existing open run without replacing its accumulated provenance. |
| `graphkeeper update` | Check npm's stable `latest` release and globally install one exact newer version. Repository files are never changed. |
Expand Down
13 changes: 13 additions & 0 deletions docs/append-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ and closing runs. Use these commands whenever an agent changes `graph/runs.json`
`graph/claims.json`; they serialize writers, validate the candidate state, and avoid
the lost-update race caused by two sessions reading and replacing the same JSON file.

Use the installed CLI as the authoritative source for current command syntax:

```sh
graphkeeper append --help
graphkeeper append claim --help
```

The append overview identifies supported record types. Contextual claim help lists
every accepted claim flag, separates tool-output requirements from inference
requirements, and includes copyable examples. Help is read-only and works without an
initialized repository; agents do not need to inspect GraphKeeper's internal package
source to discover flags.

## Safe recording sequence

1. Resolve or add the subject entity in `graph/entities.json`. Entity IDs are stable
Expand Down
16 changes: 15 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import {
prepareAgentRemoval,
type IntegrationAction,
} from './commands/integrate.js';
import { parseAppendArguments, runAppend } from './commands/append.js';
import {
APPEND_HELP_TOPICS,
parseAppendArguments,
runAppend,
} from './commands/append.js';
import { parseCloseArguments, runClose } from './commands/close.js';
import { query } from './commands/query.js';
import { updateGraphKeeper } from './commands/update.js';
Expand All @@ -30,6 +34,10 @@ import {
GraphKeeperError,
diagnostic,
} from './lib/errors.js';
import {
renderCommandHelp,
resolveContextualHelp,
} from './lib/command-help.js';

export const EXIT_SUCCESS = EXIT_CODES.success;
export const EXIT_VALIDATION = EXIT_CODES.validation;
Expand Down Expand Up @@ -242,6 +250,12 @@ export async function run(
return EXIT_SUCCESS;
}

const contextualHelp = resolveContextualHelp(argv, APPEND_HELP_TOPICS);
if (contextualHelp !== undefined) {
io.stdout(renderCommandHelp(contextualHelp));
return EXIT_SUCCESS;
}

if (!COMMANDS.has(command)) {
io.stderr('Unknown command: ' + command);
io.stderr(USAGE);
Expand Down
177 changes: 172 additions & 5 deletions src/commands/append.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { dirname, join } from 'node:path';

import { EXIT_CODES, GraphKeeperError, diagnostic, type ExitCode } from '../lib/errors.js';
import { findGitRoot } from '../lib/git.js';
import type {
CommandHelpOption,
CommandHelpTopic,
} from '../lib/command-help.js';
import {
acquireLock,
LockTimeoutError,
Expand Down Expand Up @@ -271,14 +275,177 @@ export async function runAppend(options: AppendOptions, cwd: string = process.cw
}
}

const CLAIM_FLAGS = new Set([
'subject', 'predicate', 'object', 'confidence', 'kind', 'command',
'exit-code', 'ref', 'captured', 'basis', 'produced-by', 'created', 'id', 'supersedes',
]);
type ClaimOptionGroup =
| 'common'
| 'source'
| 'tool-output'
| 'inference'
| 'optional';

export interface ClaimOptionDefinition extends CommandHelpOption {
readonly group: ClaimOptionGroup;
}

export const CLAIM_OPTION_DEFINITIONS: readonly ClaimOptionDefinition[] = [
{
name: 'subject',
value: 'entity-id',
group: 'common',
description: 'Existing canonical entity ID.',
},
{
name: 'predicate',
value: 'value',
group: 'common',
description: 'One flat relationship or property name.',
},
{
name: 'object',
value: 'value',
group: 'common',
description: 'The claimed value.',
},
{
name: 'produced-by',
value: 'run-id',
group: 'common',
description: 'Existing open run that produced the claim.',
},
{
name: 'kind',
value: 'tool_output|inference',
group: 'source',
description: 'Source kind; tool_output is the default.',
},
{
name: 'command',
value: 'text',
group: 'tool-output',
description: 'Command recorded as inert data.',
},
{
name: 'exit-code',
value: 'integer',
group: 'tool-output',
description: 'Captured command exit code.',
},
{
name: 'ref',
value: 'reference',
group: 'tool-output',
description: 'Inclusive evidence/<path>#L<start>-L<end> reference.',
},
{
name: 'captured',
value: 'timestamp',
group: 'tool-output',
description: 'Whole-second UTC evidence capture time.',
},
{
name: 'basis',
value: 'text',
group: 'inference',
description: 'Non-empty reasoning basis; not external proof.',
},
{
name: 'confidence',
value: 'number',
group: 'optional',
description: 'Schema-valid confidence; inference cannot use 1.',
},
{
name: 'id',
value: 'claim-id',
group: 'optional',
description: 'Unique claim ID; generated when omitted.',
},
{
name: 'created',
value: 'timestamp',
group: 'optional',
description: 'Whole-second UTC time; current UTC when omitted.',
},
{
name: 'supersedes',
value: 'claim-id',
group: 'optional',
description: 'Existing active claim corrected by this claim.',
},
];

const CLAIM_FLAGS = new Set(CLAIM_OPTION_DEFINITIONS.map((option) => option.name));
const RUN_FLAGS = new Set<string>([
'id', 'started', 'tool', 'task', 'evidence', 'claims-written', 'ended', 'verdict',
]);

function helpOptions(group: ClaimOptionGroup): readonly CommandHelpOption[] {
return CLAIM_OPTION_DEFINITIONS.filter((option) => option.group === group);
}

export const APPEND_HELP_TOPIC: CommandHelpTopic = {
path: ['append'],
summary: 'Create a run or append one grounded claim through a safe writer.',
usage: [
'graphkeeper append claim [options]',
'graphkeeper append run [options]',
],
details: [
'claim Append a claim. Detailed help: graphkeeper append claim --help',
'run Create a run. See the append command reference for its complete grammar.',
],
optionGroups: [],
examples: [],
};

export const APPEND_CLAIM_HELP_TOPIC: CommandHelpTopic = {
path: ['append', 'claim'],
summary: [
'Append one flat claim and link it to an existing open producing run.',
'Recorded --command text is inert data; GraphKeeper never executes it.',
].join(' '),
usage: [
'graphkeeper append claim --subject <entity-id> --predicate <value> '
+ '--object <value> --produced-by <run-id> [source options] [optional options]',
],
details: [],
optionGroups: [
{ heading: 'Common required', options: helpOptions('common') },
{ heading: 'Source selection', options: helpOptions('source') },
{ heading: 'Tool-output required', options: helpOptions('tool-output') },
{ heading: 'Inference required', options: helpOptions('inference') },
{ heading: 'Optional', options: helpOptions('optional') },
],
examples: [
{
label: 'Tool output',
command: [
'graphkeeper append claim \\',
' --subject test_payments_flaky --predicate has_status \\',
' --object passing_with_utc_default --kind tool_output \\',
' --command "TZ=UTC npm test -- payments" --exit-code 0 \\',
' --ref evidence/utc-rerun.log#L1-L3 \\',
' --captured 2026-09-05T09:04:00Z \\',
' --produced-by run_2026-09-05-investigation_a1',
].join('\n'),
},
{
label: 'Inference',
command: [
'graphkeeper append claim \\',
' --subject test_payments_flaky --predicate may_depend_on \\',
' --object timezone_configuration --kind inference \\',
' --basis "The observed result changes when TZ changes." \\',
' --produced-by run_2026-09-05-investigation_a1',
].join('\n'),
},
],
};

export const APPEND_HELP_TOPICS: readonly CommandHelpTopic[] = [
APPEND_HELP_TOPIC,
APPEND_CLAIM_HELP_TOPIC,
];

function splitFlags(args: readonly string[]): Map<string, string> {
const map = new Map<string, string>();
for (let index = 0; index < args.length; index += 1) {
Expand Down Expand Up @@ -393,4 +560,4 @@ export function parseAppendArguments(recordType: string, args: readonly string[]
}
throw error;
}
}
}
87 changes: 87 additions & 0 deletions src/lib/command-help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
export interface CommandHelpOption {
readonly name: string;
readonly value: string;
readonly description: string;
}

export interface CommandHelpOptionGroup {
readonly heading: string;
readonly options: readonly CommandHelpOption[];
}

export interface CommandHelpExample {
readonly label: string;
readonly command: string;
}

export interface CommandHelpTopic {
readonly path: readonly string[];
readonly summary: string;
readonly usage: readonly string[];
readonly details: readonly string[];
readonly optionGroups: readonly CommandHelpOptionGroup[];
readonly examples: readonly CommandHelpExample[];
}

function isHelpToken(argument: string): boolean {
return argument === '--help' || argument === '-h';
}

function samePath(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length
&& left.every((part, index) => part === right[index]);
}

/**
* Resolve help only for an exact registered leading command path. Once the path is
* recognized, an explicit help token takes precedence over any trailing options so
* help remains informational and never reaches command validation.
*/
export function resolveContextualHelp(
argv: readonly string[],
topics: readonly CommandHelpTopic[],
): CommandHelpTopic | undefined {
if (!argv.some(isHelpToken)) return undefined;

const firstOption = argv.findIndex((argument) => argument.startsWith('-'));
const path = argv.slice(0, firstOption === -1 ? argv.length : firstOption);
return topics.find((topic) => samePath(topic.path, path));
}

function optionSyntax(option: CommandHelpOption): string {
return '--' + option.name + ' <' + option.value + '>';
}

/** Render stable plain text without reading process or repository state. */
export function renderCommandHelp(topic: CommandHelpTopic): string {
const lines = [
'GraphKeeper ' + topic.path.join(' '),
'',
topic.summary,
'',
'Usage:',
...topic.usage.map((usage) => ' ' + usage),
];

if (topic.details.length > 0) {
lines.push('', 'Commands:', ...topic.details.map((detail) => ' ' + detail));
}

for (const group of topic.optionGroups) {
const width = Math.max(...group.options.map((option) => optionSyntax(option).length));
lines.push('', group.heading + ':');
for (const option of group.options) {
lines.push(' ' + optionSyntax(option).padEnd(width) + ' ' + option.description);
}
}

if (topic.examples.length > 0) {
lines.push('', 'Examples:');
for (const example of topic.examples) {
lines.push(' ' + example.label + ':');
lines.push(...example.command.split('\n').map((line) => ' ' + line));
}
}

return lines.join('\n');
}
3 changes: 3 additions & 0 deletions templates/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ behavioral responsibility that software cannot infer reliably.
`graphkeeper close run`; do not edit graph/claims.json or graph/runs.json by hand.
The commands serialize concurrent writers so separate sessions never overwrite each
other's records.
- [GUIDANCE] Use `graphkeeper append claim --help` from the installed CLI for current
claim syntax; that installed syntax is the source of truth. Do not inspect internal
package source to discover command flags.
- [HOOK] A tool_output source records kind, command, exit_code, captured, and ref
exactly; ref identifies the supporting inclusive evidence lines.
- [HOOK] An inference source records kind and a short non-empty basis. It contains no
Expand Down
Loading